``cuda.compute`` Developer Overview =================================== This document provides an overview of the internal structure of ``cuda.compute``. At a high level, ``cuda.compute`` exposes CUDA C++ parallel algorithms through a Python API. Internally, it combines Python-side operator compilation, CUDA C++ source generation, and runtime just-in-time (JIT) compilation and linking. We start with a simplified prototype. As we encounter the limitations of that simplified model, we introduce the additional mechanisms needed by the full implementation, referring to the relevant source code where useful. We begin with a minimal example that invokes a CUDA C++ kernel from Python. In this simplified prototype, the kernel takes a single integer argument and prints it. .. code-block:: c++ #include __global__ void kernel(int value) { std::printf("thread %d: %d\n", threadIdx.x, value); } extern "C" void launcher(int value) { kernel<<<1, 4>>>(value); cudaDeviceSynchronize(); } We can compile this code using ``nvcc``: .. code-block:: bash nvcc -Xcompiler=-fPIC -x cu kernel.cu -shared -o libkernel.so The resulting shared library exports the host function ``launcher``, which we can call from Python using ``ctypes``: .. code-block:: python import ctypes bindings = ctypes.CDLL('libkernel.so') bindings.launcher.argtypes = [ctypes.c_int] bindings.launcher(42) Running that Python code produces: .. code-block:: bash thread 0: 42 thread 1: 42 thread 2: 42 thread 3: 42 The example above works because all of the behavior is fixed ahead of time in the CUDA C++ source. The kernel and the operation it performs are both known in advance. A library primitive such as reduction is different. Its behavior depends not only on the input type, but also on the operator being applied. A practical Python API therefore cannot be limited to a single built-in case such as summing ``float`` values. It needs to support many data types and user-provided operators. That means the CUDA C++ side must be able to invoke device code that originates in Python. Reduction is a useful motivating example, but to keep the mechanics simple we will start with a much smaller building block: compiling a simple Python function and making it callable from CUDA C++. The same technique later applies to user-provided reduction operators. We can compile such a Python function to PTX using `Numba-CUDA `_ as follows: .. code-block:: python import numba.cuda def op(value): return 2 * value ptx, _ = numba.cuda.compile(op, sig=numba.int32(numba.int32)) That'd give us the following PTX code: .. code-block:: bash .visible .func (.param .b32 func_retval0) op(.param .b32 op_param_0) { .reg .b32 %r<3>; ld.param.u32 %r1, [op_param_0]; shl.b32 %r2, %r1, 1; st.param.b32 [func_retval0+0], %r2; ret; } At this point, the Python function has been compiled to device code, but the CUDA C++ side still needs a way to refer to it. Conceptually, we would like to treat the operator as an externally defined device function and call it from CUDA C++: .. code-block:: c++ #include extern "C" __device__ int op(int a); // defined in Python extern "C" __global__ void kernel(int value) { std::printf("thread %d: %d\n", threadIdx.x, op(value)); } extern "C" void launcher(int value) { kernel<<<1, 4>>>(value); cudaDeviceSynchronize(); } This raises the next question: how do we combine device code produced from Python with CUDA C++ code that calls it? The difficulty is not just that the operator's implementation comes from Python. The CUDA C++ side must also declare and call that operator with the correct signature. In the code above, the operator has the fixed signature ``int op(int)``. A real API cannot assume that. The user might supply an operator on ``float``, ``complex``, or some user-defined type, and the generated CUDA C++ code has to match that interface exactly. In other words, the declaration of ``op`` and the CUDA C++ source that calls it depend on the user's types and operator signature. That means the CUDA C++ side must be generated and compiled at runtime. Using ``nvcc`` for that would make the API depend on an external compiler toolchain being available on every user machine. Instead, we use NVRTC, which is designed for runtime compilation of CUDA C++. Our Python code is now: .. code-block:: python import ctypes import numba.cuda def op(value): return 2 * value ptx, _ = numba.cuda.compile(op, sig=numba.int32(numba.int32)) bindings = ctypes.CDLL('./build/libkernel.so') bindings.launcher.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int] bindings.launcher(42, ptx.encode('utf-8'), len(ptx)) Correspondingly, the C++ launcher now accepts the operator PTX as an additional argument. Inside the launcher, the CUDA C++ kernel is now assembled as a source string and compiled with NVRTC: .. code-block:: c++ extern "C" void launcher(int value, const char* op_ptx, int op_ptx_size) { cudaSetDevice(0); // Kernel is now a string! std::string kernel_source = R"XXX( extern "C" __device__ int op(int a); extern "C" __global__ void kernel(int value) { printf("thread %d prints value %d\n", threadIdx.x, op(value)); } )XXX"; Once that source string has been assembled, we compile it to PTX with NVRTC: .. code-block:: c++ nvrtcProgram prog; const char *name = "test_kernel"; nvrtcCreateProgram(&prog, kernel_source.c_str(), name, 0, nullptr, nullptr); cudaDeviceProp deviceProp; cudaGetDeviceProperties(&deviceProp, 0); const int cc_major = deviceProp.major; const int cc_minor = deviceProp.minor; const std::string arch = std::string("-arch=sm_") + std::to_string(cc_major) + std::to_string(cc_minor); const char* args[] = { arch.c_str(), "-rdc=true" }; const int num_args = sizeof(args) / sizeof(args[0]); // Compile the CUDA C++ kernel to PTX std::size_t ptx_size{}; nvrtcResult compile_result = nvrtcCompileProgram(prog, num_args, args); nvrtcGetPTXSize(prog, &ptx_size); std::unique_ptr ptx{new char[ptx_size]}; nvrtcGetPTX(prog, ptx.get()); nvrtcDestroyProgram(&prog); At this point, we have two PTX inputs: PTX for the generated CUDA C++ kernel and PTX for the Python-defined operator. We can combine them using nvJitLink: .. code-block:: c++ const char* link_options[] = { arch.c_str() }; // Link PTX comping from kernel and PTX coming from Python operator nvJitLinkHandle handle; nvJitLinkCreate(&handle, 1, link_options); nvJitLinkAddData(handle, NVJITLINK_INPUT_PTX, ptx.get(), ptx_size, name); nvJitLinkAddData(handle, NVJITLINK_INPUT_PTX, op_ptx, op_ptx_size, name); nvJitLinkComplete(handle); // Get resulting cubin std::size_t cubin_size{}; nvJitLinkGetLinkedCubinSize(handle, &cubin_size); std::unique_ptr cubin{new char[cubin_size]}; nvJitLinkGetLinkedCubin(handle, cubin.get()); nvJitLinkDestroy(&handle); The result of linking is a cubin containing the generated kernel and the Python-defined operator. We can load that cubin as a CUDA library, retrieve the kernel from it, and launch it: .. code-block:: c++ // Load cubin CUlibrary library; cuLibraryLoadData(&library, cubin.get(), nullptr, nullptr, 0, nullptr, nullptr, 0); // Get kernel pointer out of the library CUkernel kernel; cuLibraryGetKernel(&kernel, library, "kernel"); // Launch the kernel void *kernel_args[] = { &value }; cuLaunchKernel((CUfunction)kernel, 1, 1, 1, 4, 1, 1, 0, 0, kernel_args, nullptr); Now the output of the Python program would be: .. code-block:: bash thread 0 prints value 84 thread 1 prints value 84 thread 2 prints value 84 thread 3 prints value 84 This works, but it is still not optimal from a performance perspective. If the operator were compiled as part of the same CUDA C++ translation unit as the kernel, the compiler could inline it directly. In the PTX-linked version above, however, the generated cubin still contains a call to ``op`` instead of the operator body itself. To address this, we switch to a different intermediate representation. Instead of PTX, we use `LTO-IR `_. LTO-IR preserves enough information for link-time optimization, which allows the operator to be inlined into the generated kernel. On the Python side, switching from PTX to LTO-IR requires only a small change: .. code-block:: python ltoir, _ = numba.cuda.compile(op, sig=numba.int32(numba.int32), output="ltoir") On the C++ side, we make the same switch from PTX to LTO-IR: .. code-block:: c++ const char* args[] = { arch.c_str(), "-rdc=true", "-dlto" }; const int num_args = sizeof(args) / sizeof(args[0]); nvrtcResult compile_result = nvrtcCompileProgram(prog, num_args, args); std::size_t ltoir_size{}; nvrtcGetLTOIRSize(prog, <oir_size); std::unique_ptr ltoir{new char[ltoir_size]}; nvrtcGetLTOIR(prog, ltoir.get()); nvrtcDestroyProgram(&prog); const char* link_options[] = { "-lto", arch.c_str() }; nvJitLinkHandle handle; nvJitLinkCreate(&handle, 2, link_options); nvJitLinkAddData(handle, NVJITLINK_INPUT_LTOIR, ltoir.get(), ltoir_size, name); nvJitLinkAddData(handle, NVJITLINK_INPUT_LTOIR, op_ltoir, op_ltoir_size, name); If you inspect the generated cubin now, you will no longer see a call to ``op``. Instead, the operator has been inlined into the kernel, which improves performance. That is the key benefit of switching from PTX to LTO-IR. At this point, we have a working prototype that can pass Python-defined operators into CUDA C++ kernels without sacrificing performance. The next problem is user-defined data types. So far, the examples have used built-in scalar types, but a practical API also needs to support types whose layout is only known on the Python side. Fortunately, the kernel source is already being assembled as a string at runtime. That means we can also generate the type information needed by the CUDA C++ side. As a concrete example, suppose we want to pass a ``numba.complex128`` value into the kernel. The C++ side does not see the original Python type definition, but that is not an issue. It only needs a storage type with matching size and alignment, and can type-erase everything else. .. code-block:: c++ extern "C" void launcher(void *value_ptr, int type_size, int type_alignment, const char* op_ltoir, int op_ltoir_size) { std::string storage_t = "struct __align__(" + std::to_string(type_alignment) + ")" + "storage_t { char data[" + std::to_string(type_size) + "]; };"; std::string kernel_source = storage_t + R"XXX( extern "C" __device__ int op(char *state); extern "C" __global__ void kernel(storage_t value) { printf("thread %d prints value %d\n", threadIdx.x, op(value.data)); } )XXX"; // ... void *kernel_args[] = { value_ptr }; cuLaunchKernel((CUfunction)kernel, 1, 1, 1, 4, 1, 1, 0, 0, kernel_args, nullptr); In this version, the operator takes a type-erased pointer. On the Python side, we therefore pass a pointer to the ``numba.complex128`` value, together with the size and alignment needed to construct a matching storage type on the C++ side: .. code-block:: python import ctypes import numba import numba.cuda import numpy as np def op(value): return numba.int32(value[0].real + value[0].imag) value_type = numba.complex128 context = numba.cuda.descriptor.cuda_target.target_context size = context.get_value_type(value_type).get_abi_size(context.target_data) alignment = context.get_value_type(value_type).get_abi_alignment(context.target_data) ltoir, _ = numba.cuda.compile(op, sig=numba.int32(numba.types.CPointer(value_type)), output='ltoir') value = np.array([1 + 2j], dtype=np.complex128) type_erased_value_ptr = value.ctypes.data_as(ctypes.c_void_p) bindings = ctypes.CDLL('./build/libkernel.so') bindings.launcher.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_int] bindings.launcher(type_erased_value_ptr, size, alignment, ltoir, len(ltoir)) In this example, we obtain the size and alignment of ``numba.complex128`` from Numba's type system. The remaining detail is how to pass the value to ``cuLaunchKernel``. Kernel arguments are described to ``cuLaunchKernel`` as pointers to host memory from which the launch parameters are copied. In Python, that host-memory pointer can be obtained in a few ways, for example with ``ctypes.byref`` or by placing the value in a ``numpy.array`` and retrieving the array's address with ``value.ctypes.data_as(ctypes.c_void_p)``. One more ingredient is needed to get closer to the full ``cuda.compute`` implementation. The kernels in the CUDA C++ Core Compute Libraries are templates, so our generated kernel must be a template as well. .. code-block:: c++ std::string kernel_source = storage_t + R"XXX( extern "C" __device__ int op(char *state); template __global__ void kernel(T value) { printf("thread %d prints value %d\n", threadIdx.x, op(value.data)); } )XXX"; Defining the kernel as a template is still not enough. We also need to instantiate that template for the generated storage type. NVRTC provides the necessary API for that: .. code-block:: c++ nvrtcProgram prog; const char *name = "test_kernel"; nvrtcCreateProgram(&prog, kernel_source.c_str(), name, 0, nullptr, nullptr); // Get the name of the instantiated kernel std::string kernel_name = "kernel"; // Instantiate kernel template nvrtcAddNameExpression(prog, kernel_name.c_str()); // ... // Get lowered name of the kernel const char* kernel_lowered_name; // _Z6kernelI9storage_tEvT_ nvrtcGetLoweredName(prog, kernel_name.c_str(), &kernel_lowered_name); // ... // Use it to get kernel pointer cuLibraryGetKernel(&kernel, library, kernel_lowered_name); With these pieces in place, we can connect the simplified prototype back to ``cuda.compute``. At a high level, the ``cuda.compute`` API follows the same overall structure, but packages it into three stages. Using parallel reduction as an example: #. In the first stage, ``cuda.compute.make_reduce_into(...)`` constructs a reusable reduction object: ``reducer = cuda.compute.make_reduce_into(d_in=d_in, d_out=d_out, op=op, h_init=h_init)`` Here ``op`` is a Python function that must be made available to the CUDA kernel. As in the simplified prototype above, this stage compiles ``op`` to LTO-IR, generates the corresponding CUDA C++ source, instantiates the necessary kernels, and compiles them with NVRTC. The resulting build state is stored inside the returned reduction object. At this stage, the concrete runtime values of the provided arrays do not matter yet; later calls may use different pointers or sizes, as long as the interface remains compatible. #. In the second stage, that reduction object is used to query the amount of temporary storage required by the algorithm: ``temp_storage_size = reducer(temp_storage=None, d_in=d_input, d_out=d_output, num_items=num_items, op=op, h_init=h_init)`` This returns the size of the temporary storage buffer, which must be allocated in device-accessible memory. No kernels are launched at this stage. #. In the third stage, the algorithm is executed using the allocated temporary storage: ``reducer(temp_storage=temp_storage, d_in=d_input, d_out=d_output, num_items=num_items, op=op, h_init=h_init)`` At this point, the kernels stored in the reduction object are launched and the reduction is performed. Build results and device state ------------------------------ An algorithm is built in one of two ways. A **default build** (``compute_capability=None``, the common path) targets the current CUDA device — it queries that device's compute capability, then compiles and loads for it in one step. An **explicit ahead-of-time (AOT) build** (a ``compute_capability=`` argument naming one or more compute capabilities) names its targets directly, compiles for each without loading, and needs no GPU, so it can run on a build machine with no device (see :ref:`cuda.compute.ahead_of_time_compilation`). Either way, building produces a native build result — a Cython build-result object wrapping the corresponding C runtime struct — that carries two kinds of state with different device affinity: * The **compiled payload** (the compiled device code and its launch policy) depends only on the target compute capability. It is device-independent: the same payload is valid on any device of that compute capability. * **Loaded state** is created when the build result is loaded for execution — the registered ``CUlibrary`` and the kernel handles resolved from it. It belongs to the device (and context) it was loaded on. CUB launch paths resolve a ``CUkernel`` to the current-context ``CUfunction`` and may get or set kernel attributes on it, and CUDA kernel-attribute behavior is device-specific. Consequently, a *loaded* build result cannot be shared across two devices even when they have the same compute capability: its handles are device-specific. The compiled payload can be reused, but each device needs its own loaded result — built directly, or reconstructed from the shared payload. This is a property of CUDA and the build struct itself, independent of caching or free-threaded Python. The next section describes how ``cuda.compute`` caches build results to reuse the payload while giving each device its own loaded state. Caching and free-threaded Python -------------------------------- The user-facing cache behavior is described in :ref:`cuda.compute.caching`. This section describes the implementation contracts that keep that behavior correct for free-threaded Python and multi-GPU use. Two cache layers ++++++++++++++++ Internally, ``cuda.compute`` separates two kinds of cached state: * **Wrapper objects** are the Python objects returned by ``make_*`` APIs, such as ``make_reduce_into``. They own per-call descriptor state and are cached per Python thread by ``cache_with_registered_key_functions`` in ``cuda/compute/_caching.py``. Keeping wrapper caches thread-local avoids sharing mutable wrapper state across concurrent calls from free-threaded Python. * **Per-cc build results** (``_PerCCBuildResults``) hold one *canonical* Cython build result per target compute capability — the single authoritative result for that cc, carrying the device-independent compiled payload described in `Build results and device state`_. They are cached by ``cache_build_results`` and may be shared by wrapper objects in different Python threads — and, for default builds, across same-cc devices (except on the v2 HostJIT backend today; see `Device keying`_). Each device's loaded result is tracked separately within the entry, so sharing an entry never shares device-specific state. The normal cache-hit path is intentionally cheap. A wrapper-cache hit is thread-local and does not consult the process-wide build-result cache. When a wrapper is constructed, a completed build-result hit requires one process-wide dictionary lookup and does not take an explicit cache lock. The two build kinds then diverge because they differ in whether the target device is known when the wrapper is constructed. A default build already knows its device — the wrapper cache queried it to build and keys the wrapper to it — so the wrapper resolves that device's loaded result once at construction and stores a direct reference; executing it then needs no current-device query and no shared-cache lookup. An explicit AOT or deserialized wrapper has no such binding — an AOT build targets compute capabilities with no GPU queried, and a deserialized wrapper is reconstructed without a device binding — so its device is known only at call time. Each call resolves the per-device loaded result from a dictionary inside the per-cc build results, where a completed lookup also takes no explicit lock. Design requirements +++++++++++++++++++ The free-threading design is constrained by the following requirements: * Importing ``cuda.compute`` in a free-threaded CPython interpreter must not re-enable the GIL. * Free-threading support should not add global locking or shared-state contention to the normal single-threaded execution path. Wrapper cache hits should be thread-local, and normal algorithm execution should not take a global cache lock. * Mutable wrapper state must not be shared across threads. * Expensive native build results should still be shared across threads when they are safe to share. * Same-key concurrent cold builds should build once; waiters should receive the same result or observe the same exception. The current free-threading support boundary is the ``minimal-cu12`` and ``minimal-cu13`` extras. These extras omit Numba and Numba CUDA. Consequently, free-threaded support currently covers built-in ``OpKind`` operations and externally compiled ``RawOp`` operations, but not Python-callable operators. The full ``cu12`` and ``cu13`` extras remain outside the support claim until the Numba CUDA dependency is replaced by a free-threading-compatible implementation. CI runs ``test_free_threading_stress.py`` directly from the minimal test job. The v1 backend is covered across the supported CUDA 12 and 13 lanes, and a separate CTK 13.X minimal job runs the same suite against the v2 HostJIT backend. Pytest runs each suite in one process while the stress tests create and synchronize their own worker threads. Build and validation requirements +++++++++++++++++++++++++++++++++ The Cython extension that backs ``cuda.compute`` must opt in to free-threaded execution: .. code-block:: cython # cython: freethreading_compatible=True Without this marker, importing the extension in a free-threaded CPython process can cause CPython to re-enable the GIL. The generated extension should advertise ``Py_MOD_GIL_NOT_USED`` and importing ``cuda.compute`` should leave ``sys._is_gil_enabled()`` false. The free-threaded wheel must also keep its free-threaded ABI tag after repair and merge steps. For CPython 3.14, the expected wheel tag contains ``cp314-cp314t`` rather than the regular ``cp314-cp314`` tag. The acceptance criteria for a free-threaded build are: * the wheel has the expected ``cp314-cp314t`` ABI tag; * importing ``cuda.compute`` does not re-enable the GIL; * the free-threading stress suite passes without forcing ``PYTHON_GIL=0`` or ``-X gil=0``. Device keying +++++++++++++ User-facing multi-GPU behavior and requirements are described in :ref:`cuda.compute.multi_gpu`; this section covers the keying mechanism. For the default build path, the wrapper cache includes the current CUDA runtime device ordinal and compute capability in its key: wrapper objects hold device-bound state, so each device (and thread) receives its own wrapper. The shared build-result cache is keyed by compute capability alone — the compiled payload depends only on the cc — so one shared entry serves every same-cc device ordinal. Per-device loaded state lives inside the shared entry. The device that built the entry loads the canonical result in place; each additional same-cc device loads its own clone of the compiled payload through serialization (serialize, deserialize without loading, then load on the new device) instead of running a full native compilation, and the clone re-validates the payload against the current device. When the backend cannot serialize build results (the v2 HostJIT backend today), sharing is not possible, so default builds are keyed per device ordinal instead and each device builds its own entry. Explicit AOT builds cannot include a device ordinal in their compilation key — they build with no GPU queried — so their canonical results are shared process-wide by specialization and target compute capabilities. Unlike a default build, an AOT build compiles without loading, so no device owns the canonical result until first execution: the first device to run claims and loads it, and other same-cc devices load their own clone, exactly as above. The first implementation intentionally keys shared build results by CUDA runtime device ordinal rather than by CUDA context handle. User-managed CUDA driver contexts are not a target use case for ``cuda.compute``. CUDA runtime, ``cuda.core``, CuPy, and PyTorch-style applications are expected to use the primary-context model, and language frontends generally prefer that model. Concurrent build coordination +++++++++++++++++++++++++++++ When several threads miss the same cache key at once, only one should run the expensive build and the rest should wait for its result. A shared helper provides this coordination, a pattern called *single-flight*. The cache dictionary stores either a completed value or a temporary ``_InFlightBuild`` entry. On a miss, each caller creates a candidate in-flight entry, and ``dict.setdefault`` elects one caller to run the builder. Other callers receive the winning entry and wait on its ``threading.Event``. If the operation succeeds, the in-flight entry is replaced by the completed result and all waiting threads receive that same object. If it fails, the exception is propagated to the waiting threads and the failed entry is removed so that a later call can retry. Completed-result hits do not allocate an in-flight entry or take an explicit cache lock. The same helper coordinates two kinds of misses: a compilation miss in the process-wide build cache, where ``cache_build_results`` runs the native build once per specialization, and a per-device load miss inside a per-cc build result, where ``resolve`` loads (or clones and loads) the result once per device. When adding a new algorithm, the factory that returns the reusable wrapper object should use ``cache_with_registered_key_functions``. The wrapper constructor should pass the expensive native build operation to ``cache_build_results``, which returns two values: the shared build results, and the loaded result bound to the constructing device (``None`` for an explicit AOT build, which has no constructing device). Store both; ``__call__`` passes them to ``resolve_build_result`` (see any algorithm class for the pattern). Do not perform an expensive native build before entering ``cache_build_results``; otherwise same-key cold factory calls can duplicate the build and bypass single-flight coordination. The specialization key must include every argument that can affect generated code, type layout, policy selection, or native build state. It should not include runtime-only values such as array pointers, array contents, item counts, streams, or temporary-storage pointers unless those values change the compiled interface. User-object and descriptor contracts ++++++++++++++++++++++++++++++++++++ Wrapper objects returned by ``make_*`` APIs are not safe for concurrent calls from multiple threads. If two threads need the same algorithm specialization, each thread should call the factory and receive its own wrapper object, or the caller must externally serialize access to a shared wrapper. The wrapper updates its Cython ``Iterator``, ``Op``, ``Value``, and algorithm-specific descriptors before each native call, so concurrent calls through the same wrapper could overwrite the descriptor state another thread is about to use. The same contract applies to wrappers reconstructed by ``deserialize()`` — they are the same classes with the same mutable descriptors. Unlike the factories, ``deserialize()`` does not hand each calling thread its own object through the per-thread wrapper cache: every call constructs a fresh, uncached wrapper. The natural deserialize-once-and-share pattern therefore reintroduces exactly the descriptor races the per-thread factory cache prevents. Threads that need a deserialized algorithm concurrently should each deserialize the blob themselves; that performs no recompilation, at the cost of an independent native load per object. Read-only iterator and operator objects may be shared across threads. The iterator base class uses a per-iterator lock for first-time lazy construction of advance, input-dereference, and output-dereference ``Op`` objects; cached access after that remains lock-free. This lock does not make arbitrary mutation safe: concurrent mutation of iterator state, operator state, captured state, or child iterators remains unsupported unless the caller synchronizes externally. Mutable execution state belongs to one thread at a time unless the caller provides synchronization. This includes output arrays, temporary-storage buffers, streams, ``DoubleBuffer`` instances, and other objects whose state changes as part of a launch. Backend-specific notes ++++++++++++++++++++++ The v1 NVRTC/nvJitLink backend and the v2 HostJIT backend have different free-threading risk surfaces and must be audited independently. v1 stresses NVRTC, nvJitLink, CUDA library loading, and CUB host dispatch. v2 adds HostJIT compiler state, LLVM/Clang initialization, persistent PCH paths, generated source/cubin artifacts, and dynamic loader lifetime. Transform has one additional v1 native-cache rule. Each transform build result owns a native cache of launch configurations (``async_config`` / ``prefetch_config``) in ``c/parallel/src/transform.cu``. Because one build result is shared by every thread using the same specialization, and the Cython bindings release the GIL around the native call, each configuration is filled exactly once through ``std::call_once``; later calls on any thread only pay the ``once_flag`` fast-path check. This holds on every interpreter build — regular GIL builds also execute the native call concurrently once the GIL is released, so the cache must be thread-safe unconditionally. The v2 backend addresses the same transform concern differently, and only on Windows. HostJIT compiles generated code with ``-fno-threadsafe-statics`` because the Windows CRT guard support that thread-safe function-local statics require is unavailable. Generated CUB code still initializes function-local statics lazily — transform's launch configuration among them — so a per-build-result ``first_call_gate`` (``c/parallel.v2/src/util/first_call_gate.h``) serializes the first successful call into each generated function; after it completes, an atomic fast-path check lets later concurrent calls proceed without locking. Empty calls bypass the gate because they return before CUB initializes the static. This covers transform and binary search; other platforms keep thread-safe statics and need no gate. Clearing caches +++++++++++++++ ``clear_all_caches()`` is process-local. It clears all known per-thread wrapper caches through a weak registry of live thread cache containers, and it clears the shared build-result cache. Separate Python processes build and cache independently. Calling ``clear_all_caches()`` concurrently with active factory calls or algorithm execution is not supported unless the caller synchronizes externally. Source map ---------- For readers who want to connect this overview back to the source tree: * The Python-facing API, operator compilation, and the logic for constructing and invoking reusable algorithm objects live under ``python/cuda_cccl/cuda/compute/``. * The lower-level C/C++ runtime compilation and kernel-building machinery lives under ``c/parallel/`` (and ``c/parallel.v2/`` for the v2 HostJIT backend). * User-facing examples for ``cuda.compute`` live under ``python/cuda_cccl/tests/compute/examples/``.