class_docstr: Generates and executes code that performs prefix sums ("scans") on
  arbitrary types, with many possible tweaks.
  
  Usage example::
  
      from pyopencl.scan import GenericScanKernel
      knl = GenericScanKernel(
              context, np.int32,
              arguments="__global int *ary",
              input_expr="ary[i]",
              scan_expr="a+b", neutral="0",
              output_statement="ary[i+1] = item;")
  
      a = cl.array.arange(queue, 10000, dtype=np.int32)
      knl(a, queue=queue)
  
  .. automethod:: __init__
  .. automethod:: __call__
class_docstr_tok: ["Generates","and","executes","code","that","performs","prefix","sums","(","``","scans","''",")","on","arbitrary","types",",","with","many","possible","tweaks",".","Usage","example",":",":","from","pyopencl.scan","import","GenericScanKernel","knl","=","GenericScanKernel","(","context",",","np.int32",",","arguments=","''","__global","int","*","ary","''",",","input_expr=","''","ary","[","i","]","''",",","scan_expr=","''","a+b","''",",","neutral=","''","0","''",",","output_statement=","''","ary","[","i+1","]","=","item",";","''",")","a","=","cl.array.arange","(","queue",",","10000",",","dtype=np.int32",")","knl","(","a",",","queue=queue",")","..","automethod",":",":","__init__","..","automethod",":",":","__call__"]
code: def _finish_setup_impl(self) -> None:
      # {{{ find usable workgroup/k-group size, build first-level scan
  
      trip_count = 0
  
      avail_local_mem = min(dev.local_mem_size for dev in self.devices)
  
      if "CUDA" in self.devices[0].platform.name:
          # not sure where these go, but roughly this much seems unavailable.
          avail_local_mem -= 0x400
  
      is_cpu = self.devices[0].type & cl.device_type.CPU
      is_gpu = self.devices[0].type & cl.device_type.GPU
  
      if is_cpu:
          # (about the widest vector a CPU can support, also taking
          # into account that CPUs don't hide latency by large work groups
          max_scan_wg_size = 16
          wg_size_multiples = 4
      else:
          max_scan_wg_size = min(
              dev.max_work_group_size for dev in self.devices
          )
          wg_size_multiples = 64
  
      # Intel beignet fails "Out of shared local memory" in test_scan int64
      # and asserts in test_sort with this enabled:
      # https://github.com/inducer/pyopencl/pull/238
      # A beignet bug report (outside of pyopencl) suggests packed structs
      # (which this is) can even give wrong results:
      # https://bugs.freedesktop.org/show_bug.cgi?id=98717
      # TODO: does this also affect Intel Compute Runtime?
      use_bank_conflict_avoidance = (
          self.dtype.itemsize > 4
          and self.dtype.itemsize % 8 == 0
          and is_gpu
          and "beignet" not in self.devices[0].platform.version.lower()
      )
  
      # k_group_size should be a power of two because of in-kernel
      # division by that number.
  
      solutions = []
      for k_exp in range(0, 9):
          for wg_size in range(
              wg_size_multiples, max_scan_wg_size + 1, wg_size_multiples
          ):
              k_group_size = 2**k_exp
              lmem_use = self.get_local_mem_use(
                  wg_size, k_group_size, use_bank_conflict_avoidance
              )
              if lmem_use <= avail_local_mem:
                  solutions.append(
                      (wg_size * k_group_size, k_group_size, wg_size)
                  )
  
      if is_gpu:
          for wg_size_floor in [256, 192, 128]:
              have_sol_above_floor = any(
                  wg_size >= wg_size_floor
                  for _, _, wg_size in solutions
              )
  
              if have_sol_above_floor:
                  # delete all solutions not meeting the wg size floor
                  solutions = [
                      (total, try_k_group_size, try_wg_size)
                      for total, try_k_group_size, try_wg_size in solutions
                      if try_wg_size >= wg_size_floor
                  ]
                  break
  
      _, k_group_size, max_scan_wg_size = max(solutions)
  
      while True:
          candidate_scan_gen_info = self.generate_scan_kernel(
              max_scan_wg_size,
              self.parsed_args,
              _process_code_for_macro(self.input_expr),
              self.is_segment_start_expr,
              input_fetch_exprs=self.input_fetch_exprs,
              is_first_level=True,
              store_segment_start_flags=self.store_segment_start_flags,
              k_group_size=k_group_size,
              use_bank_conflict_avoidance=use_bank_conflict_avoidance,
          )
  
          candidate_scan_info = candidate_scan_gen_info.build(
              self.context, self.options
          )
  
          # Will this device actually let us execute this kernel
          # at the desired work group size? Building it is the
          # only way to find out.
          kernel_max_wg_size = min(
              candidate_scan_info.kernel.get_work_group_info(
                  cl.kernel_work_group_info.WORK_GROUP_SIZE, dev
              )
              for dev in self.devices
          )
  
          if candidate_scan_info.wg_size <= kernel_max_wg_size:
              break
          else:
              max_scan_wg_size = min(
                  kernel_max_wg_size, max_scan_wg_size
              )
  
          trip_count += 1
          assert trip_count <= 20
  
      self.first_level_scan_gen_info = candidate_scan_gen_info
      assert (
          _round_down_to_power_of_2(candidate_scan_info.wg_size)
          == candidate_scan_info.wg_size
      )
  
      # }}}
  
      # {{{ build second-level scan
  
      from pyopencl.tools import VectorArg
  
      second_level_arguments = self.parsed_args + [
          VectorArg(self.dtype, "interval_sums")
      ]
  
      second_level_build_kwargs: Dict[str, Optional[str]] = {}
      if self.is_segmented:
          second_level_arguments.append(
              VectorArg(
                  self.index_dtype,
                  "g_first_segment_start_in_interval_input",
              )
          )
  
          # is_segment_start_expr answers the question "should previous sums
          # spill over into this item". And since
          # g_first_segment_start_in_interval_input answers the question if a
          # segment boundary was found in an interval of data, then if not,
          # it's ok to spill over.
          second_level_build_kwargs[
              "is_segment_start_expr"
          ] = "g_first_segment_start_in_interval_input[i] != NO_SEG_BOUNDARY"
      else:
          second_level_build_kwargs["is_segment_start_expr"] = None
  
      self.second_level_scan_gen_info = self.generate_scan_kernel(
          max_scan_wg_size,
          arguments=second_level_arguments,
          input_expr="interval_sums[i]",
          input_fetch_exprs=[],
          is_first_level=False,
          store_segment_start_flags=False,
          k_group_size=k_group_size,
          use_bank_conflict_avoidance=use_bank_conflict_avoidance,
          **second_level_build_kwargs,
      )
  
      # }}}
  
      # {{{ generate final update kernel
  
      update_wg_size = min(max_scan_wg_size, 256)
  
      final_update_tpl = _make_template(UPDATE_SOURCE)
      final_update_src = str(
          final_update_tpl.render(
              wg_size=update_wg_size,
              output_statement=self.output_statement,
              arg_offset_adjustment=get_arg_offset_adjuster_code(
                  self.parsed_args
              ),
              argument_signature=", ".join(
                  arg.declarator() for arg in self.parsed_args
              ),
              is_segment_start_expr=self.is_segment_start_expr,
              input_expr=_process_code_for_macro(self.input_expr),
              use_lookbehind_update=self.use_lookbehind_update,
              **self.code_variables,
          )
      )
  
      update_scalar_arg_dtypes = get_arg_list_scalar_arg_dtypes(
          self.parsed_args
      ) + [self.index_dtype, self.index_dtype, None, None]
      if self.is_segmented:
          # g_first_segment_start_in_interval
          update_scalar_arg_dtypes.append(None)
      if self.store_segment_start_flags:
          update_scalar_arg_dtypes.append(None)  # g_segment_start_flags
  
      self.final_update_gen_info = _GeneratedFinalUpdateKernelInfo(
          final_update_src,
          self.name_prefix + "_final_update",
          update_scalar_arg_dtypes,
          update_wg_size,
      )
  
imports: from abc import ABC, abstractmethod
  from dataclasses import dataclass
  from typing import Any, Dict, List, Optional, Set, Tuple, Union
  import numpy
  import pyopencl
  import pyopencl.array
  from pyopencl.tools import KernelTemplateBase, DtypedArgument, bitlog2, context_dependent_memoize, dtype_to_ctype, get_arg_list_scalar_arg_dtypes, get_arg_offset_adjuster_code, _process_code_for_macro, _NumpyTypesKeyBuilder
  import pyopencl._mymako
  from pyopencl._cluda import CLUDA_PREAMBLE
  from pytools.persistent_dict import WriteOncePersistentDict
  import logging
label_desc: Use image node_id 2 for calling the GenericScanKernel obj's underlying member method code with example usage: obj._finish_setup_impl() without return types