class_docstr: Given arrays *values* and *keys* of equal length
  and a number *nkeys* of keys, returns a tuple `(starts,
  lists)`, as follows: *values* and *keys* are sorted
  by *keys*, and the sorted *values* is returned as
  *lists*. Then for each index *i* in ``range(nkeys)``,
  *starts[i]* is written to indicating where the
  group of *values* belonging to the key with index
  *i* begins. It implicitly ends at *starts[i+1]*.
  
  ``starts`` is built so that it has ``nkeys + 1`` entries, so that
  the *i*'th entry is the start of the *i*'th list, and the
  *i*'th entry is the index one past the *i*'th list's end,
  even for the last list.
  
  This implies that all lists are contiguous.
  
  .. note:: This functionality is provided as a preview. Its
      interface is subject to change until this notice is removed.
  
  .. versionadded:: 2013.1
code: def get_kernels(self, key_dtype, value_dtype, starts_dtype):
      from pyopencl.tools import VectorArg, ScalarArg
  
      by_target_sorter = RadixSort(
          self.context,
          [
              VectorArg(value_dtype, "values"),
              VectorArg(key_dtype, "keys"),
          ],
          key_expr="keys[i]",
          sort_arg_names=["values", "keys"],
      )
  
      from pyopencl.elementwise import ElementwiseTemplate
  
      start_finder = ElementwiseTemplate(
          arguments="""//CL//
                  starts_t *key_group_starts,
                  key_t *keys_sorted_by_key,
                  """,
          operation=r"""//CL//
                  key_t my_key = keys_sorted_by_key[i];
  
                  if (i == 0 || my_key != keys_sorted_by_key[i-1])
                      key_group_starts[my_key] = i;
                  """,
          name="find_starts",
      ).build(
          self.context,
          type_aliases=(
              ("key_t", starts_dtype),
              ("starts_t", starts_dtype),
          ),
          var_values=(),
      )
  
      bound_propagation_scan = GenericScanKernel(
          self.context,
          starts_dtype,
          arguments=[
              VectorArg(starts_dtype, "starts"),
              # starts has length n+1
              ScalarArg(key_dtype, "nkeys"),
          ],
          input_expr="starts[nkeys-i]",
          scan_expr="min(a, b)",
          neutral=_make_cl_int_literal(
              np.iinfo(starts_dtype).max, starts_dtype
          ),
          output_statement="starts[nkeys-i] = item;",
      )
  
      return _KernelInfo(
          by_target_sorter=by_target_sorter,
          start_finder=start_finder,
          bound_propagation_scan=bound_propagation_scan,
      )
  
imports: from dataclasses import dataclass
  from typing import Optional
  import numpy
  import pyopencl
  import pyopencl.array
  from pyopencl.elementwise import ElementwiseKernel
  from pyopencl.scan import ScanTemplate, GenericScanKernel
  from pyopencl.tools import dtype_to_ctype, get_arg_offset_adjuster_code
  from pytools import memoize, memoize_method
  from mako.template import Template
label_desc: Use image node_id 2 for calling the KeyValueSorter obj's underlying member method code with example usage: obj.get_kernels(key_dtype, value_dtype, starts_dtype) and returns: _KernelInfo