feat(cccl): integrate missing CCCL directories — python/, ci/, .agent/, docs/, test/

Sparse-checkout from NVIDIA/cccl main branch to complete cccl_upstream:

Added:
- python/cuda_cccl/ (226 files) — Python bindings for device-level algorithms
  Critical for muh toolchain: cuda.compute.reduce_into, scan, radix_sort, etc.
  Includes 204 .py files with full test coverage for all 27 algorithms
- ci/ (163 files) — Build/test infrastructure
  build_cub.sh, test_cub.sh, build_and_test_targets.sh, matrix.yaml
  Directly maps to our [INFRA-CI] and [INFRA-BUILD] items
- .agent/skills/ (7 files) — NVIDIA's own agent skills for CCCL
  cccl-style/SKILL.md, cccl-test/SKILL.md, sass-diff/SKILL.md
- docs/ (491 files) — Official CCCL documentation
  CI references, CMake guides, Python compute docs, libcudacxx PTX docs
- test/ (12 files) — Top-level integration tests (cuda_smoke, stdpar)
- Root configs: .clang-format, .clang-tidy, CONTRIBUTING.md, pyproject.toml
- CLAUDE.md symlink → AGENTS.md (NVIDIA's standard)

cccl_upstream now mirrors full NVIDIA/cccl structure:
  Before: 42M (cub + thrust + libcudacxx + cudax + c + examples + benchmarks)
  After:  53M (+python +ci +docs +.agent +test +configs)

This completes the CCCL base needed for:
- [muh-bench] items: ci/util/build_and_test_targets.sh for targeted builds
- [CCCL-verify] items: python/cuda_cccl/tests/ as reference implementations
- [CCCL-test] items: ci/test_cub.sh, ci/test_thrust.sh
- Agent workflow: .agent/skills/ for consistent style and test patterns
This commit is contained in:
muh-bot
2026-08-07 02:34:33 +00:00
parent 3f97dca7ad
commit 2a7ca101d7
908 changed files with 121615 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
build.log
.python-version
# CMake
CMakeFiles/
CMakeCache.txt
cmake_install.cmake
Makefile
*.cmake
!CMakeLists.txt
# IDE
.idea/
.vscode/
*.swp
*.swo
# Generated by CMake at build time
cuda/compute/_build_info.py
cuda/compute/cu12/
cuda/compute/cu13/

View File

@@ -0,0 +1,187 @@
# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
cmake_minimum_required(VERSION 3.30)
# Must be set before project() initializes the CUDA language; otherwise CMake
# < 3.23 defaults to sm_52, which is below CCCL's minimum supported arch.
include(../../cmake/CCCLCheckCudaArchitectures.cmake)
set(
CMAKE_CUDA_ARCHITECTURES
"${minimum_cccl_arch}"
CACHE STRING
"CUDA architectures for CCCL"
)
project(cuda_cccl DESCRIPTION "Python package cuda_cccl" LANGUAGES CUDA CXX C)
find_package(CUDAToolkit REQUIRED)
set(CUDA_VERSION_MAJOR ${CUDAToolkit_VERSION_MAJOR})
set(CUDA_VERSION_DIR "cu${CUDA_VERSION_MAJOR}")
message(
STATUS
"Building for CUDA ${CUDA_VERSION_MAJOR}, output directory: ${CUDA_VERSION_DIR}"
)
# Build cuda_cccl against either cccl.c.parallel (v1, NVRTC) by default or
# cccl.c.parallel.v2 (HostJIT) when CCCL_PYTHON_USE_V2=ON. v2 is opt-in until
# it replaces v1 across the matrix.
set(_cccl_root ../..)
set(CCCL_TOPLEVEL_PROJECT ON) # Enable the developer builds
option(
CCCL_PYTHON_USE_V2
"Build cuda_cccl against cccl.c.parallel.v2 (HostJIT)."
OFF
)
if (CCCL_PYTHON_USE_V2)
set(CCCL_ENABLE_C_PARALLEL_V2 ON)
set(CCCL_C_PARALLEL_V2_LIBRARY_OUTPUT_DIRECTORY ${SKBUILD_PROJECT_NAME})
set(_cccl_c_parallel_target cccl.c.parallel.v2)
set(_using_v2_py "True")
else()
set(CCCL_ENABLE_C_PARALLEL ON)
set(CCCL_C_PARALLEL_LIBRARY_OUTPUT_DIRECTORY ${SKBUILD_PROJECT_NAME})
set(_cccl_c_parallel_target cccl.c.parallel)
set(_using_v2_py "False")
endif()
# Surface the v1/v2 choice to Python (tests use it to skip v2-only failures,
# and __init__.py uses it to wire up wheel-bundled hostjit header paths).
# Generated into the build dir and installed via CMake — writing into the
# source tree would miss scikit-build-core's package-file snapshot.
set(_build_info_py "${CMAKE_CURRENT_BINARY_DIR}/_build_info.py")
file(
WRITE "${_build_info_py}"
"# Auto-generated by CMakeLists.txt; do not edit.\nUSING_V2 = ${_using_v2_py}\n"
)
install(FILES "${_build_info_py}" DESTINATION cuda/compute)
# Just install the rest:
set(libcudacxx_ENABLE_INSTALL_RULES ON)
set(CUB_ENABLE_INSTALL_RULES ON)
set(Thrust_ENABLE_INSTALL_RULES ON)
# Install to our output location:
include(GNUInstallDirs)
set(old_libdir "${CMAKE_INSTALL_LIBDIR}") # push
set(old_includedir "${CMAKE_INSTALL_INCLUDEDIR}") # push
set(CMAKE_INSTALL_LIBDIR "cuda/cccl/headers/lib")
set(CMAKE_INSTALL_INCLUDEDIR "cuda/cccl/headers/include")
add_subdirectory(${_cccl_root} _parent_cccl)
set(CMAKE_INSTALL_LIBDIR "${old_libdir}") # pop
set(CMAKE_INSTALL_INCLUDEDIR "${old_includedir}") # pop
# Install version-specific binaries
set(_cccl_c_parallel_install_targets ${_cccl_c_parallel_target})
if (CCCL_PYTHON_USE_V2)
list(APPEND _cccl_c_parallel_install_targets libnvcc)
endif()
install(
TARGETS ${_cccl_c_parallel_install_targets}
DESTINATION cuda/compute/${CUDA_VERSION_DIR}/cccl
)
# Build and install Cython extension
find_package(Python3 COMPONENTS Interpreter Development.Module REQUIRED)
set(CYTHON_version_command "${Python3_EXECUTABLE}" -m cython --version)
execute_process(
COMMAND ${CYTHON_version_command}
OUTPUT_VARIABLE CYTHON_version_output
ERROR_VARIABLE CYTHON_version_output
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
COMMAND_ERROR_IS_FATAL ANY
)
if ("${CYTHON_version_output}" MATCHES "^[Cc]ython version ([^,]+)")
set(CYTHON_VERSION "${CMAKE_MATCH_1}")
else()
message(
FATAL_ERROR
"Failed to parse Cython version from:\n${CYTHON_version_output}"
)
endif()
# -3 generates source for Python 3
# -M generates depfile
# -t cythonizes if PYX is newer than preexisting output
# -w sets working directory
set(
CYTHON_FLAGS
-3
-M
-t
-w
"${cuda_cccl_SOURCE_DIR}"
)
message(STATUS "Using Cython ${CYTHON_VERSION}")
set(pyx_source_file "${cuda_cccl_SOURCE_DIR}/cuda/compute/_bindings_impl.pyx")
set(_generated_extension_src "${cuda_cccl_BINARY_DIR}/_bindings_impl.c")
set(_depfile "${cuda_cccl_BINARY_DIR}/_bindings_impl.c.dep")
# Backend-conditional Cython .pxi files. Where v1 and v2 expose different
# struct layouts or call signatures, the .pyx `include`s a generated .pxi
# whose source is chosen here. The helpers inside present a uniform interface
# so the rest of _bindings_impl.pyx stays backend-agnostic.
if (CCCL_PYTHON_USE_V2)
set(_backend_suffix "v2")
else()
set(_backend_suffix "v1")
endif()
foreach (
_pxi_stem
segmented_reduce_backend
binary_search_backend
op_code_type
serialization
)
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/cuda/compute/_bindings_${_pxi_stem}_${_backend_suffix}.pxi"
"${CMAKE_CURRENT_BINARY_DIR}/_bindings_${_pxi_stem}.pxi"
COPYONLY
)
endforeach()
# Custom Cython compilation command. `-I ${BINARY_DIR}` lets the .pyx's
# `include "_bindings_..._backend.pxi"` resolve to the file we configured
# above.
add_custom_command(
OUTPUT "${_generated_extension_src}"
COMMAND
"${Python3_EXECUTABLE}" -m cython
# gersemi: off
${CYTHON_FLAGS}
-I "${CMAKE_CURRENT_BINARY_DIR}"
"${pyx_source_file}"
--output-file "${_generated_extension_src}"
# gersemi: on
DEPENDS "${pyx_source_file}"
DEPFILE "${_depfile}"
COMMENT "Cythonizing ${pyx_source_file} for CUDA ${CUDA_VERSION_MAJOR}"
)
add_custom_target(
cythonize_bindings_impl
ALL
DEPENDS "${_generated_extension_src}"
)
python3_add_library(
_bindings_impl
MODULE
WITH_SOABI
"${_generated_extension_src}"
)
add_dependencies(_bindings_impl cythonize_bindings_impl)
target_link_libraries(
_bindings_impl
PRIVATE #
${_cccl_c_parallel_target}
CUDA::cuda_driver
)
set_target_properties(_bindings_impl PROPERTIES INSTALL_RPATH "$ORIGIN/cccl")
install(TARGETS _bindings_impl DESTINATION cuda/compute/${CUDA_VERSION_DIR})

View File

@@ -0,0 +1 @@
../../LICENSE

View File

@@ -0,0 +1,49 @@
# CUDA CCCL Python Package
[`cuda.cccl`](https://nvidia.github.io/cccl/unstable/python)
provides a Pythonic interface to the
[CUDA Core Compute Libraries](https://nvidia.github.io/cccl/unstable/cpp.html#cccl-cpp-libraries).
It provides the following modules:
- **`cuda.compute`** - Device-level parallel algorithms (reduce, scan, sort, etc.) and iterators
- **`cuda.cccl.headers`** - Programmatic access to CCCL headers
## Installation
Install from PyPI:
```bash
pip install cuda-cccl[cu13] # For CUDA 13.x (pip-installed cuda-toolkit)
pip install cuda-cccl[cu12] # For CUDA 12.x (pip-installed cuda-toolkit)
```
If you already have a CUDA toolkit on your system and do not want pip to
install it, use the `sysctk` variants:
```bash
pip install cuda-cccl[sysctk13] # For CUDA 13.x (system CUDA toolkit)
pip install cuda-cccl[sysctk12] # For CUDA 12.x (system CUDA toolkit)
```
For a minimal install without Numba (useful when supplying pre-compiled operators):
```bash
pip install cuda-cccl[minimal-cu13] # pip-installed cuda-toolkit
pip install cuda-cccl[minimal-sysctk13] # system CUDA toolkit
```
Install from conda-forge:
```bash
conda install -c conda-forge cccl-python
```
**Requirements:** Python 3.10+, CUDA Toolkit 12.x or 13.x, NVIDIA GPU with Compute Capability 7.5+
## Documentation
For complete documentation, examples, and API reference, visit:
- **Full Documentation**: [nvidia.github.io/cccl/unstable/python](https://nvidia.github.io/cccl/unstable/python)
- **Repository**: [github.com/NVIDIA/cccl](https://github.com/NVIDIA/cccl)
- **Examples**: [github.com/NVIDIA/cccl/tree/main/python/cuda_cccl/tests/compute/examples](https://github.com/NVIDIA/cccl/tree/main/python/cuda_cccl/tests/compute/examples)

View File

@@ -0,0 +1,19 @@
# Build artifacts
build/
bin/
*.o
*.so
*.a
compile_commands.json
# Python cache
__pycache__/
*.pyc
*.pyo
# Benchmark results
results*/
# Pixi
.pixi/
pixi.lock

View File

@@ -0,0 +1,12 @@
# CCCL Python cuda.compute benchmarks
This directory contains the code for the Python cuda.compute benchmarks.
They are migrated from the original C++ benchmarks and they should match the C++ implementations
as closely as possible.
The original C++ benchmarks are available in this repository in: ../../../../cub/benchmarks/bench/
We follow the same directory structure and naming conventions converting to Python were appropriate.
The code for cuda.compute is in this repository under: `../../../../python/cuda_cccl/cuda/compute/`. Look into this directory when searching for existing APIs in Python.
The benchmarks use nvbench to run the benchmarks and report the results.

View File

@@ -0,0 +1,147 @@
# cuda.compute Benchmarks
Compare Python `cuda.compute` performance against C++ CUB implementations.
## Setup
This project uses [pixi](https://pixi.sh) to manage environments and dependencies.
Two environments are available:
- **`wheel`** - Uses the released `cuda-cccl` package
- **`source`** - Builds `cuda-cccl` from the local repository
### Build C++ Benchmarks
Build CUB benchmarks using the CI script (one-time, ~13 minutes):
```bash
cd /path/to/cccl
./ci/build_cub.sh -arch 89 # Use your GPU arch (89=RTX 4090, 80=A100, 90=H100)
```
Binaries are built to: `build/cub/bin/`
## Run Benchmarks
### Using pixi tasks
```bash
# Run Python benchmarks (released cuda-cccl)
pixi run -e wheel bench
# Run Python benchmarks (local source build)
pixi run -e source bench
# Run Python benchmarks with reduced parameter set
pixi run -e wheel bench-quick
# Run just one benchmark
pixi run -e wheel bench -b transform/fill
# Run C++ benchmarks
pixi run -e wheel bench-cpp
# Run both Python and C++ benchmarks
pixi run -e wheel bench-all
```
### Using run_benchmarks.py directly
```bash
# Run both C++ and Python (default)
pixi run -e wheel python run_benchmarks.py -b transform/fill -d 0
# Run only C++
pixi run -e wheel python run_benchmarks.py -b transform/fill --cpp
# Run only Python
pixi run -e wheel python run_benchmarks.py -b transform/fill --py
# Show help
pixi run -e wheel python run_benchmarks.py --help
```
To run the benchmarks using the "quick" configuration:
```bash
pixi run -e wheel python run_benchmarks.py --quick
```
## Compare Results
```bash
pixi run -e wheel python analysis/python_vs_cpp_summary.py -b transform/fill
```
## Web Report
A simple page used to visualize a set of results.
- Requires `results/` to be populated with benchmark results.
First generate a manifest:
```bash
pixi run -e wheel python analysis/generate_web_report_manifest.py \
--results-dir results \
--output results/manifest.json
```
Build the web report single file app:
```bash
cd analysis/web-report
npm install
npm run build
```
This will output a single file app to `analysis/web-report/dist/` copy it to the `results/` directory and:
```bash
cd results/
python3 -m http.server
```
Now its possible to share the results directory as a zip/tar file.
## Manual Usage
### List benchmark configurations
```bash
# Python
pixi run -e wheel python transform/fill.py --list
# C++
/path/to/cccl/build/cub/bin/cub.bench.transform.fill.base --list
```
### Run with custom options
```bash
# Python - specific type and size
pixi run -e wheel python transform/fill.py --axis "T=I32" --axis "Elements[pow2]=20" --devices 0
# C++ - save JSON
/path/to/cccl/build/cub/bin/cub.bench.transform.fill.base \
--json results/transform/fill_cpp.json \
--devices 0
```
### Compare manually
```bash
pixi run -e wheel python analysis/python_vs_cpp_summary.py \
results/transform/fill_py.json \
results/transform/fill_cpp.json \
--device 0
```
## AI commands
These are using the .opencode folder but can be moved to other Agents.
### /migration-status
Generates a report of the migration status for each benchmark in CUB.

View File

@@ -0,0 +1,132 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for histogram_even using cuda.compute.
C++ equivalent: cub/benchmarks/bench/histogram/even.cu
Notes:
- The C++ benchmark uses Entropy axis with nvbench_helper bit entropy generation
- Migration: Python matches the bitwise-AND entropy approach and skips some I8/I16 large-bin cases due to CUDA errors.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import FUNDAMENTAL_TYPES as TYPE_MAP
from utils import as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import make_histogram_even
def get_upper_level(dtype, num_bins, num_elements):
"""
Compute upper level for histogram bins.
Mirrors C++ get_upper_level() from histogram_common.cuh
"""
if np.issubdtype(dtype, np.integer):
# For integer types, upper_level = min(num_bins, max_value_for_type)
max_val = np.iinfo(dtype).max
return dtype(min(num_bins, max_val))
else:
# For floating point types, upper_level = num_elements
return dtype(num_elements)
def bench_histogram_even(state: bench.State):
type_str = state.get_string("SampleT{ct}")
dtype = TYPE_MAP[type_str]
num_elements = int(state.get_int64("Elements{io}"))
num_bins = int(state.get_int64("Bins"))
entropy_str = state.get_string("Entropy")
# Skip invalid configurations (like C++ does)
# For integer types, skip if num_bins > max value representable by SampleT
if np.issubdtype(dtype, np.integer):
max_val = np.iinfo(dtype).max
if num_bins > max_val:
state.skip("Number of bins exceeds what SampleT can represent")
return
num_levels = num_bins + 1
lower_level = dtype(0)
upper_level = get_upper_level(dtype, num_bins, num_elements)
alloc_stream = as_cupy_stream(state.get_stream())
d_samples = generate_data_with_entropy(
num_elements,
dtype,
entropy_str,
alloc_stream,
min_val=lower_level,
max_val=upper_level,
)
# Output histogram (counter type is int32 in C++)
with alloc_stream:
d_histogram = cp.zeros(num_bins, dtype=np.int32)
alloc_stream.synchronize()
h_num_output_levels = np.array([num_levels], dtype=np.int32)
h_lower_level = np.array([lower_level], dtype=dtype)
h_upper_level = np.array([upper_level], dtype=dtype)
histogrammer = make_histogram_even(
d_samples=d_samples,
d_histogram=d_histogram,
h_num_output_levels=h_num_output_levels,
h_lower_level=h_lower_level,
h_upper_level=h_upper_level,
num_samples=num_elements,
)
temp_storage_bytes = histogrammer(
temp_storage=None,
d_samples=d_samples,
d_histogram=d_histogram,
h_num_output_levels=h_num_output_levels,
h_lower_level=h_lower_level,
h_upper_level=h_upper_level,
num_samples=num_elements,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
state.add_element_count(num_elements)
state.add_global_memory_reads(num_elements * d_samples.dtype.itemsize)
state.add_global_memory_writes(num_bins * d_histogram.dtype.itemsize)
def launcher(launch: bench.Launch):
histogrammer(
temp_storage=temp_storage,
d_samples=d_samples,
d_histogram=d_histogram,
h_num_output_levels=h_num_output_levels,
h_lower_level=h_lower_level,
h_upper_level=h_upper_level,
num_samples=num_elements,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_histogram_even)
b.set_name("base")
b.add_string_axis("SampleT{ct}", list(TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 29, 4))
b.add_int64_axis("Bins", [32, 128, 2048, 2097152])
b.add_string_axis("Entropy", ["0.201", "1.000"])
bench.run_all_benchmarks(sys.argv)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,95 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
import pytest
from host_benchmark_cases import (
CALL_CASES,
CASES,
HostBenchmarkCase,
patch_wrapper_to_skip_native_compute,
synchronize,
)
import cuda.compute as cc
pytest.importorskip("pytest_benchmark")
BUILD_TIME_ROUNDS = 10
ONESHOT_ROUNDS = 20
ONESHOT_ITERATIONS = 100
TWOSHOT_ROUNDS = 20
TWOSHOT_ITERATIONS = 1000
def _case_params(cases: list[HostBenchmarkCase]) -> list[pytest.ParameterSet]:
params = []
for case in cases:
marks = []
if case.skip_reason is not None:
marks.append(pytest.mark.skip(reason=case.skip_reason))
params.append(pytest.param(case, id=case.name, marks=marks))
return params
@pytest.mark.benchmark(group="cuda.compute.host.build_time")
@pytest.mark.parametrize("case", _case_params(CASES))
def test_build_time(benchmark, case: HostBenchmarkCase):
state = case.setup()
synchronize()
def setup() -> None:
cc.clear_all_caches()
def build():
return case.make_wrapper(state)
benchmark.pedantic(
build,
setup=setup,
rounds=BUILD_TIME_ROUNDS,
iterations=1,
warmup_rounds=0,
)
@pytest.mark.benchmark(group="cuda.compute.host.oneshot_cached")
@pytest.mark.parametrize("case", _case_params(CALL_CASES))
def test_oneshot_cached_host_overhead(benchmark, case: HostBenchmarkCase):
cc.clear_all_caches()
state = case.setup()
wrapper = case.make_wrapper(state)
patch_wrapper_to_skip_native_compute(wrapper, case.noop_return_kind)
synchronize()
def call() -> None:
case.oneshot(state)
benchmark.pedantic(
call,
rounds=ONESHOT_ROUNDS,
iterations=ONESHOT_ITERATIONS,
warmup_rounds=0,
)
@pytest.mark.benchmark(group="cuda.compute.host.twoshot_call")
@pytest.mark.parametrize("case", _case_params(CALL_CASES))
def test_twoshot_call_host_overhead(benchmark, case: HostBenchmarkCase):
cc.clear_all_caches()
state = case.setup()
wrapper = case.make_wrapper(state)
patch_wrapper_to_skip_native_compute(wrapper, case.noop_return_kind)
synchronize()
def call() -> None:
case.twoshot(state, wrapper)
benchmark.pedantic(
call,
rounds=TWOSHOT_ROUNDS,
iterations=TWOSHOT_ITERATIONS,
warmup_rounds=0,
)

View File

@@ -0,0 +1,97 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for merge_sort keys using cuda.compute.
C++ equivalent: cub/benchmarks/bench/merge_sort/keys.cu
Notes:
- The C++ benchmark uses Entropy axis to control data distribution
- Uses less_t comparison operator (ascending sort)
- Keys only (no values) - see pairs.cu for key-value sorting
- Migration: Python fixes offsets and approximates entropy generation.
- OffsetT axis is omitted because the Python API does not expose offset type.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import SIGNED_TYPES, as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import OpKind, make_merge_sort
def bench_merge_sort_keys(state: bench.State):
type_str = state.get_string("T{ct}")
dtype = SIGNED_TYPES[type_str]
num_elements = int(state.get_int64("Elements{io}"))
entropy_str = state.get_string("Entropy")
alloc_stream = as_cupy_stream(state.get_stream())
d_in_keys = generate_data_with_entropy(
num_elements, dtype, entropy_str, alloc_stream
)
# Output array for sorted keys (merge_sort requires separate output)
with alloc_stream:
d_out_keys = cp.empty(num_elements, dtype=dtype)
alloc_stream.synchronize()
sorter = make_merge_sort(
d_in_keys=d_in_keys,
d_in_values=None,
d_out_keys=d_out_keys,
d_out_values=None,
op=OpKind.LESS,
)
temp_storage_bytes = sorter(
temp_storage=None,
d_in_keys=d_in_keys,
d_in_values=None,
d_out_keys=d_out_keys,
d_out_values=None,
op=OpKind.LESS,
num_items=num_elements,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
state.add_element_count(num_elements)
state.add_global_memory_reads(num_elements * d_in_keys.dtype.itemsize, "Size")
state.add_global_memory_writes(num_elements * d_out_keys.dtype.itemsize)
def launcher(launch: bench.Launch):
sorter(
temp_storage=temp_storage,
d_in_keys=d_in_keys,
d_in_values=None,
d_out_keys=d_out_keys,
d_out_values=None,
op=OpKind.LESS,
num_items=num_elements,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_merge_sort_keys)
b.set_name("base")
b.add_string_axis("T{ct}", list(SIGNED_TYPES.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 29, 4))
b.add_string_axis("Entropy", ["1.000", "0.201"])
# Note: OffsetT axis from C++ is not exposed in Python API
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,110 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for merge_sort pairs using cuda.compute.
C++ equivalent: cub/benchmarks/bench/merge_sort/pairs.cu
Notes:
- Uses Entropy axis to control key distribution
- Keys and values are sorted together
- Migration: Python omits int128 values and OffsetT axis.
- OffsetT axis is omitted because the Python API does not expose offset type.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import (
INTEGRAL_TYPES,
SIGNED_TYPES,
as_cupy_stream,
generate_data_with_entropy,
)
import cuda.bench as bench
from cuda.compute import OpKind, make_merge_sort
KEY_TYPE_MAP = SIGNED_TYPES
VALUE_TYPE_MAP = INTEGRAL_TYPES
def bench_merge_sort_pairs(state: bench.State):
key_type_str = state.get_string("KeyT{ct}")
value_type_str = state.get_string("ValueT{ct}")
key_dtype = KEY_TYPE_MAP[key_type_str]
value_dtype = VALUE_TYPE_MAP[value_type_str]
num_elements = int(state.get_int64("Elements{io}"))
entropy_str = state.get_string("Entropy")
alloc_stream = as_cupy_stream(state.get_stream())
d_in_keys = generate_data_with_entropy(
num_elements, key_dtype, entropy_str, alloc_stream
)
with alloc_stream:
d_in_values = generate_data_with_entropy(
num_elements, value_dtype, "1.000", alloc_stream
)
d_out_keys = cp.empty(num_elements, dtype=key_dtype)
d_out_values = cp.empty(num_elements, dtype=value_dtype)
alloc_stream.synchronize()
sorter = make_merge_sort(
d_in_keys=d_in_keys,
d_in_values=d_in_values,
d_out_keys=d_out_keys,
d_out_values=d_out_values,
op=OpKind.LESS,
)
temp_storage_bytes = sorter(
temp_storage=None,
d_in_keys=d_in_keys,
d_in_values=d_in_values,
d_out_keys=d_out_keys,
d_out_values=d_out_values,
op=OpKind.LESS,
num_items=num_elements,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
state.add_element_count(num_elements)
state.add_global_memory_reads(num_elements * d_in_keys.dtype.itemsize)
state.add_global_memory_reads(num_elements * d_in_values.dtype.itemsize)
state.add_global_memory_writes(num_elements * d_out_keys.dtype.itemsize)
state.add_global_memory_writes(num_elements * d_out_values.dtype.itemsize)
def launcher(launch: bench.Launch):
sorter(
temp_storage=temp_storage,
d_in_keys=d_in_keys,
d_in_values=d_in_values,
d_out_keys=d_out_keys,
d_out_values=d_out_values,
op=OpKind.LESS,
num_items=num_elements,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_merge_sort_pairs)
b.set_name("base")
b.add_string_axis("KeyT{ct}", list(KEY_TYPE_MAP.keys()))
b.add_string_axis("ValueT{ct}", list(VALUE_TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 29, 4))
b.add_string_axis("Entropy", ["1.000", "0.201"])
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,138 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for three_way_partition using cuda.compute.
C++ equivalent: cub/benchmarks/bench/partition/three_way.cu
Notes:
- The C++ benchmark uses Entropy axis to control data distribution
- Uses less_then_t<T> predicate operators to divide data into three partitions:
- First partition: items < left_border (max/3)
- Second partition: items < right_border (max*2/3)
- Third partition (unselected): items >= right_border
- T axis covers fundamental types (C++ fundamental_types minus int128)
- Migration: Python uses FUNDAMENTAL_TYPES; omits OffsetT axis.
- OffsetT axis is omitted because the Python API does not expose offset type.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import FUNDAMENTAL_TYPES, as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import make_three_way_partition
def bench_three_way_partition(state: bench.State):
type_str = state.get_string("T{ct}")
dtype = FUNDAMENTAL_TYPES[type_str]
num_elements = int(state.get_int64("Elements{io}"))
entropy_str = state.get_string("Entropy")
alloc_stream = as_cupy_stream(state.get_stream())
if np.issubdtype(dtype, np.integer):
info = np.iinfo(dtype)
min_val = 0
max_val = info.max
else:
info = np.finfo(dtype)
min_val = 0.0
max_val = info.max
left_border = max_val // 3 if np.issubdtype(dtype, np.integer) else max_val / 3
right_border = left_border * 2
d_in = generate_data_with_entropy(
num_elements,
dtype,
entropy_str,
alloc_stream,
min_val=min_val,
max_val=max_val,
)
with alloc_stream:
d_first_part_out = cp.empty(num_elements, dtype=dtype)
d_second_part_out = cp.empty(num_elements, dtype=dtype)
d_unselected_out = cp.empty(num_elements, dtype=dtype)
# d_num_selected_out stores [num_first_part, num_second_part]
d_num_selected_out = cp.empty(2, dtype=np.int32)
alloc_stream.synchronize()
# Convert borders to the correct type for closure capture
left_thresh = dtype(left_border)
right_thresh = dtype(right_border)
def select_first_part(x):
return x < left_thresh
def select_second_part(x):
return x < right_thresh
partitioner = make_three_way_partition(
d_in=d_in,
d_first_part_out=d_first_part_out,
d_second_part_out=d_second_part_out,
d_unselected_out=d_unselected_out,
d_num_selected_out=d_num_selected_out,
select_first_part_op=select_first_part,
select_second_part_op=select_second_part,
)
temp_storage_bytes = partitioner(
temp_storage=None,
d_in=d_in,
d_first_part_out=d_first_part_out,
d_second_part_out=d_second_part_out,
d_unselected_out=d_unselected_out,
d_num_selected_out=d_num_selected_out,
select_first_part_op=select_first_part,
select_second_part_op=select_second_part,
num_items=num_elements,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
state.add_element_count(num_elements)
state.add_global_memory_reads(num_elements * d_in.dtype.itemsize)
state.add_global_memory_writes(num_elements * d_in.dtype.itemsize)
# C++ reports add_global_memory_writes<offset_t>(1) — 1 element of offset type.
state.add_global_memory_writes(d_num_selected_out.dtype.itemsize)
def launcher(launch: bench.Launch):
partitioner(
temp_storage=temp_storage,
d_in=d_in,
d_first_part_out=d_first_part_out,
d_second_part_out=d_second_part_out,
d_unselected_out=d_unselected_out,
d_num_selected_out=d_num_selected_out,
select_first_part_op=select_first_part,
select_second_part_op=select_second_part,
num_items=num_elements,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_three_way_partition)
b.set_name("base")
b.add_string_axis("T{ct}", list(FUNDAMENTAL_TYPES.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 29, 4))
b.add_string_axis("Entropy", ["1.000", "0.544", "0.000"])
# Note: OffsetT axis from C++ is not exposed in Python API
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,89 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
[workspace]
channels = ["conda-forge"]
platforms = ["linux-64"]
channel-priority = "disabled"
# CUDA 13.1 system requirement
[feature.cu13.system-requirements]
cuda = "13"
[feature.cu13.dependencies]
cuda-version = "13.1.*"
# Python benchmark dependencies
[feature.bench.dependencies]
python = "3.13.*"
numpy = "*"
cupy = "*"
pytest-benchmark = "*"
pyyaml = "*"
pre-commit = "*"
[feature.bench.pypi-dependencies]
cuda-bench = ">=0.2.0"
# C++ benchmark build dependencies
[feature.cpp-bench.dependencies]
cmake = "*"
ninja = "*"
cxx-compiler = "*"
fmt = "*"
cuda-cudart-dev = "*"
[feature.cpp-bench.target.linux-64.dependencies]
cuda-crt-dev_linux-64 = "*"
cuda-driver-dev_linux-64 = "*"
[feature.cpp-bench.target.linux-64.activation.env]
CUDA_HOME = "$CONDA_PREFIX/targets/x86_64-linux"
# Important: cuda-cccl installation variants
# Released version from PyPI
[feature.cccl-wheel.pypi-dependencies]
cuda-cccl = { version = ">=0.1.0", extras = ["cu13"] }
# Local source build (editable install from repo)
# Needs nvcc, compilers, and CUDA dev libraries for scikit-build-core to build cuda-cccl
[feature.cccl-source.dependencies]
cuda-nvcc = "*"
cuda-nvrtc-dev = "*"
libnvjitlink-dev = "*"
cuda-cudart-dev = "*"
cuda-driver-dev = "*"
c-compiler = "*"
cxx-compiler = "*"
[feature.cccl-source.pypi-dependencies]
cuda-cccl = { path = "../..", editable = true, extras = ["cu13"] }
# Environments
[environments]
wheel = { features = ["cu13", "bench", "cpp-bench", "cccl-wheel"] }
source = { features = ["cu13", "bench", "cpp-bench", "cccl-source"] }
# Tasks
[tasks.bench]
cmd = ["python", "run_benchmarks.py", "--py"]
description = "Run Python cuda.compute benchmarks"
[tasks.bench-quick]
cmd = ["python", "run_benchmarks.py", "--py", "--quick"]
description = "Run Python benchmarks with reduced parameter set"
[tasks.bench-cpp]
cmd = ["python", "run_benchmarks.py", "--cpp"]
description = "Run C++ CUB benchmarks"
[tasks.bench-all]
cmd = ["python", "run_benchmarks.py"]
description = "Run both Python and C++ benchmarks"
[tasks.pre-commit]
cmd = ["pre-commit", "run", "--all-files"]
cwd = "../../../.."
description = "Run pre-commit checks on the entire repo"

View File

@@ -0,0 +1,130 @@
# Quick mode configurations for fast benchmark testing
# Used by run_benchmarks.py with --quick flag
#
# Each benchmark lists axis name -> value pairs.
# Use C++ axis names (with {ct}/{io} suffixes where applicable).
# The script strips these suffixes for Python benchmarks.
#
# For power-of-two axes (Elements{io}, MaxSegSize, MaxSegmentSize, SegmentSize, Segments{io}),
# values are exponents (e.g., 16 means 2^16 = 65536). SegmentSize is converted to
# actual values for Python benchmarks.
#
# NOTE: Only specify axes that exist in BOTH C++ and Python benchmarks.
# Some C++ benchmarks have extra axes (like OffsetT{ct}) not in Python.
transform/fill:
"T{ct}": "I32"
"Elements{io}": 16
transform/babelstream:
"T{ct}": "F32"
"Elements{io}": 16
transform/heavy:
"Heaviness{ct}": "64"
"Elements{io}": 16
transform/fib:
"Elements{io}": 16
transform/grayscale:
"T{ct}": "F32"
"Elements{io}": 16
transform/complex_cmp:
"Elements{io}": 16
transform_reduce/sum:
"T{ct}": "I32"
"Elements{io}": 16
reduce/sum:
"T{ct}": "I32"
"Elements{io}": 16
reduce/min:
"T{ct}": "I32"
"Elements{io}": 16
reduce/custom:
"T{ct}": "I32"
"Elements{io}": 16
reduce/nondeterministic:
"T{ct}": "I32"
"Elements{io}": 16
scan/exclusive/sum:
"T{ct}": "I32"
"Elements{io}": 16
scan/exclusive/custom:
"T{ct}": "I32"
"Elements{io}": 16
histogram/even:
"SampleT{ct}": "I32"
"Elements{io}": 16
"Bins": 128
"Entropy": "1.000"
select/if:
"T{ct}": "I32"
"Elements{io}": 16
"Entropy": "1.000"
select/unique_by_key:
"KeyT{ct}": "I32"
"ValueT{ct}": "I32"
"Elements{io}": 16
"MaxSegSize": 4
radix_sort/keys:
"T{ct}": "I32"
"Elements{io}": 16
"Entropy": "1.000"
radix_sort/pairs:
"KeyT{ct}": "I32"
"ValueT{ct}": "I32"
"Elements{io}": 16
"Entropy": "1.000"
merge_sort/keys:
"T{ct}": "I32"
"Elements{io}": 16
"Entropy": "1.000"
merge_sort/pairs:
"KeyT{ct}": "I32"
"ValueT{ct}": "I32"
"Elements{io}": 16
"Entropy": "1.000"
segmented_sort/keys:
benchmarks:
power:
"T{ct}": "I32"
"Elements{io}": 22
"Segments{io}": 12
"Entropy": "1.000"
small:
"T{ct}": "I32"
"Elements{io}": 22
"MaxSegmentSize": 1
large:
"T{ct}": "I32"
"Elements{io}": 22
"MaxSegmentSize": 10
segmented_reduce/variable_sum:
benchmarks:
variable_default:
"T{ct}": "I32"
"Elements{io}": 16
"MaxSegmentSize": 4
partition/three_way:
"T{ct}": "I32"
"Elements{io}": 16
"Entropy": "1.000"

View File

@@ -0,0 +1,97 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for radix_sort keys using cuda.compute.
C++ equivalent: cub/benchmarks/bench/radix_sort/keys.cu
Notes:
- The C++ benchmark uses Entropy axis to control data distribution
- Sort order is always ascending (C++ benchmark hardcodes this)
- Keys only (no values) - see radix_sort/pairs.cu for key-value sorting
- begin_bit=0, end_bit=sizeof(T)*8 (full key comparison)
- Migration: Python fixes offsets, excludes int128, and approximates entropy generation.
- OffsetT axis is omitted because the Python API does not expose offset type.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import FUNDAMENTAL_TYPES as TYPE_MAP
from utils import as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import SortOrder, make_radix_sort
def bench_radix_sort_keys(state: bench.State):
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_elements = int(state.get_int64("Elements{io}"))
entropy_str = state.get_string("Entropy")
alloc_stream = as_cupy_stream(state.get_stream())
d_in_keys = generate_data_with_entropy(
num_elements, dtype, entropy_str, alloc_stream
)
# Output array for sorted keys
with alloc_stream:
d_out_keys = cp.empty(num_elements, dtype=dtype)
alloc_stream.synchronize()
sorter = make_radix_sort(
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=None,
d_out_values=None,
order=SortOrder.ASCENDING,
)
temp_storage_bytes = sorter(
temp_storage=None,
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=None,
d_out_values=None,
num_items=num_elements,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
state.add_element_count(num_elements)
state.add_global_memory_reads(num_elements * d_in_keys.dtype.itemsize, "Size")
state.add_global_memory_writes(num_elements * d_out_keys.dtype.itemsize)
def launcher(launch: bench.Launch):
sorter(
temp_storage=temp_storage,
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=None,
d_out_values=None,
num_items=num_elements,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_radix_sort_keys)
b.set_name("base")
b.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 29, 4))
b.add_string_axis("Entropy", ["1.000", "0.544", "0.201"])
# Note: OffsetT axis from C++ is not exposed in Python API
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,109 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for radix_sort pairs (key-value) using cuda.compute.
C++ equivalent: cub/benchmarks/bench/radix_sort/pairs.cu
Notes:
- The C++ benchmark uses Entropy axis to control key data distribution
- Sort order is always ascending (C++ benchmark hardcodes this)
- Keys and values are sorted together (values rearranged by key order)
- begin_bit=0, end_bit=sizeof(KeyT)*8 (full key comparison)
- C++ uses integral_types for keys and int8/16/32/64(+int128) for values
- Migration: Python matches C++ integral_types for both keys and values; omits int128 and OffsetT axis.
- OffsetT axis is omitted because the Python API does not expose offset type.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import INTEGRAL_TYPES, as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import SortOrder, make_radix_sort
KEY_TYPE_MAP = INTEGRAL_TYPES
VALUE_TYPE_MAP = INTEGRAL_TYPES
def bench_radix_sort_pairs(state: bench.State):
key_type_str = state.get_string("KeyT{ct}")
value_type_str = state.get_string("ValueT{ct}")
key_dtype = KEY_TYPE_MAP[key_type_str]
value_dtype = VALUE_TYPE_MAP[value_type_str]
num_elements = int(state.get_int64("Elements{io}"))
entropy_str = state.get_string("Entropy")
alloc_stream = as_cupy_stream(state.get_stream())
d_in_keys = generate_data_with_entropy(
num_elements, key_dtype, entropy_str, alloc_stream
)
d_in_values = generate_data_with_entropy(
num_elements, value_dtype, "1.000", alloc_stream
)
with alloc_stream:
d_out_keys = cp.empty(num_elements, dtype=key_dtype)
d_out_values = cp.empty(num_elements, dtype=value_dtype)
alloc_stream.synchronize()
sorter = make_radix_sort(
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=d_in_values,
d_out_values=d_out_values,
order=SortOrder.ASCENDING,
)
temp_storage_bytes = sorter(
temp_storage=None,
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=d_in_values,
d_out_values=d_out_values,
num_items=num_elements,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
state.add_element_count(num_elements)
state.add_global_memory_reads(num_elements * d_in_keys.dtype.itemsize)
state.add_global_memory_reads(num_elements * d_in_values.dtype.itemsize)
state.add_global_memory_writes(num_elements * d_out_keys.dtype.itemsize)
state.add_global_memory_writes(num_elements * d_out_values.dtype.itemsize)
def launcher(launch: bench.Launch):
sorter(
temp_storage=temp_storage,
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=d_in_values,
d_out_values=d_out_values,
num_items=num_elements,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_radix_sort_pairs)
b.set_name("base")
b.add_string_axis("KeyT{ct}", list(KEY_TYPE_MAP.keys()))
b.add_string_axis("ValueT{ct}", list(VALUE_TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 29, 4))
b.add_string_axis("Entropy", ["1.000", "0.201"])
# Note: OffsetT axis from C++ is not exposed in Python API
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,82 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for reduce custom operation using cuda.compute.reduce_into.
C++ equivalent: cub/benchmarks/bench/reduce/custom.cu
Notes:
- Uses a custom max operator (not OpKind) to exercise generic path
- int128 and complex32 are not supported by cupy
- Migration: Python limits to basic numeric types; C++ includes int128/complex.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import SIGNED_TYPES as TYPE_MAP
from utils import as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import make_reduce_into
def max_op(a, b):
return a if a > b else b
def bench_reduce_custom(state: bench.State):
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_items = int(state.get_int64("Elements{io}"))
alloc_stream = as_cupy_stream(state.get_stream())
with alloc_stream:
d_in = generate_data_with_entropy(num_items, dtype, "1.000", alloc_stream)
d_out = cp.empty(1, dtype=dtype)
h_init = np.zeros(1, dtype=dtype)
reducer = make_reduce_into(d_in=d_in, d_out=d_out, op=max_op, h_init=h_init)
temp_storage_bytes = reducer(
temp_storage=None,
d_in=d_in,
d_out=d_out,
num_items=num_items,
op=max_op,
h_init=h_init,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
state.add_element_count(num_items)
state.add_global_memory_reads(num_items * d_in.dtype.itemsize, "Size")
state.add_global_memory_writes(d_out.dtype.itemsize)
def launcher(launch: bench.Launch):
reducer(
temp_storage=temp_storage,
d_in=d_in,
d_out=d_out,
num_items=num_items,
op=max_op,
h_init=h_init,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_reduce_custom)
b.set_name("base")
b.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 29, 4))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,85 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for reduce min operation using cuda.compute.reduce_into.
C++ equivalent: cub/benchmarks/bench/reduce/min.cu
Notes:
- Uses OpKind.MINIMUM for minimum reduction
- C++ uses cuda::minimum<> which CUB recognizes for optimized code paths (DPX on Hopper+)
- int128 and complex32 are not supported by cupy
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import FUNDAMENTAL_TYPES as TYPE_MAP
from utils import as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import OpKind, make_reduce_into
def bench_reduce_min(state: bench.State):
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_items = int(state.get_int64("Elements{io}"))
alloc_stream = as_cupy_stream(state.get_stream())
with alloc_stream:
d_in = generate_data_with_entropy(num_items, dtype, "1.000", alloc_stream)
d_out = cp.empty(1, dtype=dtype)
# Initial value for min reduction (max value of type)
if np.issubdtype(dtype, np.integer):
init_val = np.iinfo(dtype).max
else:
init_val = np.finfo(dtype).max
h_init = np.array([init_val], dtype=dtype)
reducer = make_reduce_into(d_in=d_in, d_out=d_out, op=OpKind.MINIMUM, h_init=h_init)
temp_storage_bytes = reducer(
temp_storage=None,
d_in=d_in,
d_out=d_out,
num_items=num_items,
op=OpKind.MINIMUM,
h_init=h_init,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
state.add_element_count(num_items)
state.add_global_memory_reads(num_items * d_in.dtype.itemsize, "Size")
state.add_global_memory_writes(d_out.dtype.itemsize)
def launcher(launch: bench.Launch):
reducer(
temp_storage=temp_storage,
d_in=d_in,
d_out=d_out,
num_items=num_items,
op=OpKind.MINIMUM,
h_init=h_init,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_reduce_min)
b.set_name("base")
b.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 29, 4))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,87 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for nondeterministic reduce sum using cuda.compute.reduce_into.
C++ equivalent: cub/benchmarks/bench/reduce/nondeterministic.cu
Notes:
- Uses Determinism.NOT_GUARANTEED
- C++ tests int32, int64, float, double
- Migration: Python fixes offsets; C++ exposes an OffsetT axis.
- OffsetT axis is omitted because the Python API does not expose offset type.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import ALL_TYPES as _ALL_TYPES
from utils import as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import Determinism, OpKind, make_reduce_into
TYPE_MAP = {k: _ALL_TYPES[k] for k in ("I32", "I64", "F32", "F64")}
def bench_reduce_nondeterministic(state: bench.State):
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_items = int(state.get_int64("Elements{io}"))
alloc_stream = as_cupy_stream(state.get_stream())
with alloc_stream:
d_in = generate_data_with_entropy(num_items, dtype, "1.000", alloc_stream)
d_out = cp.empty(1, dtype=dtype)
h_init = np.zeros(1, dtype=dtype)
reducer = make_reduce_into(
d_in=d_in,
d_out=d_out,
op=OpKind.PLUS,
h_init=h_init,
determinism=Determinism.NOT_GUARANTEED,
)
temp_storage_bytes = reducer(
temp_storage=None,
d_in=d_in,
d_out=d_out,
num_items=num_items,
op=OpKind.PLUS,
h_init=h_init,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
state.add_element_count(num_items)
state.add_global_memory_reads(num_items * d_in.dtype.itemsize, "Size")
state.add_global_memory_writes(1 * d_out.dtype.itemsize)
def launcher(launch: bench.Launch):
reducer(
temp_storage=temp_storage,
d_in=d_in,
d_out=d_out,
num_items=num_items,
op=OpKind.PLUS,
h_init=h_init,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_reduce_nondeterministic)
b.set_name("base")
b.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 29, 4))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,80 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for reduce sum operation using cuda.compute.reduce_into.
C++ equivalent: cub/benchmarks/bench/reduce/sum.cu
Notes:
- int128 and complex32 are not supported by cupy
- Migration: Python excludes int128/complex; C++ supports more types/tuning.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import SIGNED_TYPES as TYPE_MAP
from utils import as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import OpKind, make_reduce_into
def bench_reduce_sum(state: bench.State):
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_items = int(state.get_int64("Elements{io}"))
alloc_stream = as_cupy_stream(state.get_stream())
with alloc_stream:
d_in = generate_data_with_entropy(num_items, dtype, "1.000", alloc_stream)
d_out = cp.empty(1, dtype=dtype)
# Initial value for reduction
h_init = np.zeros(1, dtype=dtype)
reducer = make_reduce_into(d_in=d_in, d_out=d_out, op=OpKind.PLUS, h_init=h_init)
temp_storage_bytes = reducer(
temp_storage=None,
d_in=d_in,
d_out=d_out,
num_items=num_items,
op=OpKind.PLUS,
h_init=h_init,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
state.add_element_count(num_items)
state.add_global_memory_reads(num_items * d_in.dtype.itemsize, "Size")
state.add_global_memory_writes(d_out.dtype.itemsize)
def launcher(launch: bench.Launch):
reducer(
temp_storage=temp_storage,
d_in=d_in,
d_out=d_out,
num_items=num_items,
op=OpKind.PLUS,
h_init=h_init,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_reduce_sum)
b.set_name("base")
b.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 29, 4))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,528 @@
#!/usr/bin/env python3
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Run Python cuda.compute and C++ CUB benchmarks.
Prerequisites: C++ benchmarks must be built first via:
cd /path/to/cccl && ./ci/build_cub.sh -arch 89
Usage:
python run_benchmarks.py [options]
Options:
-d, --device ID GPU device ID [default: 0]
-b, --benchmark NAME Run specific benchmark only [default: all]
--py Only run Python benchmarks
--cpp Only run C++ benchmarks
--quick, -q Run with reduced parameter set for fast testing
--profile Run each config once (nvbench profile mode), no sampling
-h, --help Show this help message
Benchmark names follow CUB structure:
e.g. transform/fill, transform/babelstream
Examples:
python run_benchmarks.py # Run all benchmarks
python run_benchmarks.py -b transform/fill # Only fill benchmark
python run_benchmarks.py -b reduce/sum -d 0 # Reduce sum on device 0
python run_benchmarks.py -b scan/exclusive/sum --py # Only Python
python run_benchmarks.py --quick # Quick mode (reduced params)
python run_benchmarks.py -b merge_sort/keys -q # Quick mode, single benchmark
"""
import argparse
import os
import shlex
import subprocess
import sys
from pathlib import Path
# ============================================================================
# Configuration
# ============================================================================
SCRIPT_DIR = Path(__file__).parent
CCCL_ROOT = SCRIPT_DIR.parents[3]
RESULTS_DIR = SCRIPT_DIR / "results"
CUB_BENCH_DIR = CCCL_ROOT / "build" / "cub" / "bin"
QUICK_CONFIG_FILE = SCRIPT_DIR / "quick_configs.yaml"
# Supported benchmarks (Python implementations available)
SUPPORTED_BENCHMARKS = [
"transform/fill",
"transform/babelstream",
"transform/heavy",
"transform/fib",
"transform/grayscale",
"transform/complex_cmp",
"transform_reduce/sum",
"reduce/sum",
"reduce/min",
"reduce/custom",
"reduce/nondeterministic",
"scan/exclusive/sum",
"scan/exclusive/custom",
"histogram/even",
"select/if",
"select/unique_by_key",
"radix_sort/keys",
"radix_sort/pairs",
"merge_sort/keys",
"merge_sort/pairs",
"segmented_sort/keys",
"segmented_reduce/variable_sum",
"partition/three_way",
]
# Axes that use power-of-two values (need [pow2] suffix for nvbench)
# These are the base names (without {ct}/{io} suffixes)
POW2_AXES_CPP = {
"Elements",
"GuaranteedMaxSegSize",
"MaxSegSize",
"MaxSegmentSize",
"SegmentSize",
"Segments",
}
POW2_AXES_PY = {
"Elements",
"GuaranteedMaxSegSize",
"MaxSegSize",
"MaxSegmentSize",
"Segments",
}
# Axis name mappings from C++ to Python.
# Keep type axes in their C++ form (`{ct}`) to match benchmark axis names.
CPP_TO_PY_AXIS_MAP = {
"T{ct}": "T{ct}",
"KeyT{ct}": "KeyT{ct}",
"ValueT{ct}": "ValueT{ct}",
"SampleT{ct}": "SampleT{ct}",
"Heaviness{ct}": "Heaviness{ct}",
}
def strip_axis_suffix(axis_name: str) -> str:
"""Strip {ct} or {io} suffix from axis name for Python benchmarks.
e.g., "T{ct}" -> "T{ct}", "Elements{io}" -> "Elements{io}".
"""
if axis_name in CPP_TO_PY_AXIS_MAP:
return CPP_TO_PY_AXIS_MAP[axis_name]
# Generic suffix stripping for any other axes
if axis_name.endswith("{ct}"):
return axis_name.rsplit("{", 1)[0]
if axis_name.endswith("{io}"):
return axis_name
return axis_name
def get_base_axis_name(axis_name: str) -> str:
"""Get base axis name (without suffix) for POW2 check.
e.g., "Elements{io}" -> "Elements"
"""
if axis_name.endswith("{ct}") or axis_name.endswith("{io}"):
return axis_name.rsplit("{", 1)[0]
return axis_name
# ============================================================================
# Helper Functions
# ============================================================================
def print_banner(msg: str) -> None:
"""Print a banner message."""
print()
print("=" * 72)
print(msg)
print("=" * 72)
print()
def print_section(msg: str) -> None:
"""Print a section header."""
print()
print("-" * 72)
print(msg)
print("-" * 72)
def load_quick_configs() -> dict:
"""Load quick mode configurations from YAML file."""
import yaml # only needed for --quick
with open(QUICK_CONFIG_FILE) as f:
return yaml.safe_load(f)
def build_axis_args_from_config(axis_config: dict, for_python: bool) -> list:
"""Build --axis arguments for nvbench CLI from an axis config dict.
Args:
axis_config: Dict of axis_name -> value
for_python: If True, strip C++ suffixes for Python benchmarks
"""
args = []
for axis_name, value in axis_config.items():
# For Python, strip C++ suffixes from axis names
if for_python:
axis_name = strip_axis_suffix(axis_name)
# Check if this is a power-of-two axis (using base name)
base_name = get_base_axis_name(axis_name)
if for_python and base_name == "SegmentSize":
actual_value = 2 ** int(value)
args.extend(["--axis", f"{axis_name}={actual_value}"])
continue
pow2_axes = POW2_AXES_PY if for_python else POW2_AXES_CPP
if base_name in pow2_axes:
args.extend(["--axis", f"{axis_name}[pow2]={value}"])
else:
args.extend(["--axis", f"{axis_name}={value}"])
return args
def get_quick_config_entry(benchmark: str, quick_configs: dict) -> dict:
"""Get quick config entry for a benchmark, raising if missing."""
if benchmark not in quick_configs:
raise ValueError(
f"Benchmark '{benchmark}' not found in quick_configs.yaml.\n"
f"Cannot run in --quick mode. Add configuration for this benchmark."
)
return quick_configs[benchmark]
def get_cpp_binary(benchmark: str) -> str:
"""Get C++ binary name from benchmark path.
e.g., "transform/fill" -> "cub.bench.transform.fill.base"
"""
return f"cub.bench.{benchmark.replace('/', '.')}.base"
def get_py_script(benchmark: str) -> Path:
"""Get Python script path from benchmark path.
e.g., "transform/fill" -> "transform/fill.py"
"""
return SCRIPT_DIR / f"{benchmark}.py"
def get_result_path(benchmark: str, suffix: str) -> Path:
"""Get results file path.
e.g., "transform/fill", "cpp" -> "results/transform/fill_cpp.json"
"""
bench_path = Path(benchmark)
return RESULTS_DIR / bench_path.parent / f"{bench_path.name}_{suffix}.json"
def get_log_path(benchmark: str, suffix: str) -> Path:
"""Get log file path under results/logs.
e.g., "transform/fill", "cpp" -> "results/logs/transform/fill_cpp.log"
"""
bench_path = Path(benchmark)
return RESULTS_DIR / "logs" / bench_path.parent / f"{bench_path.name}_{suffix}.log"
def run_and_log(cmd: list, log_path: Path, env: dict | None = None) -> dict:
"""Run command and write stdout/stderr to log file."""
log_path.parent.mkdir(parents=True, exist_ok=True)
with open(log_path, "w", encoding="utf-8") as log_file:
log_file.write(f"Command: {shlex.join(cmd)}\n\n")
log_file.flush()
try:
result = subprocess.run(
cmd, check=False, stdout=log_file, stderr=log_file, env=env
)
except Exception as exc: # noqa: BLE001
log_file.write("\nERROR: Runner failed to execute command.\n")
log_file.write(f"{exc}\n")
return {"status": "error", "returncode": None, "error": str(exc)}
status = "ok" if result.returncode == 0 else "failed"
return {"status": status, "returncode": result.returncode}
# ============================================================================
# Benchmark Runner
# ============================================================================
def run_benchmark(
benchmark: str,
device: str,
run_py: bool,
run_cpp: bool,
quick_mode: bool,
quick_configs: dict,
profile: bool,
) -> dict:
"""Run a single benchmark.
Returns dict with paths to generated result files.
"""
cpp_binary = get_cpp_binary(benchmark)
py_script = get_py_script(benchmark)
cpp_result = get_result_path(benchmark, "cpp")
py_result = get_result_path(benchmark, "py")
cpp_log = get_log_path(benchmark, "cpp")
py_log = get_log_path(benchmark, "py")
# Ensure results subdirectory exists
cpp_result.parent.mkdir(parents=True, exist_ok=True)
# Build axis arguments for quick mode
cpp_axis_args = []
py_axis_args = []
if quick_mode:
config_entry = get_quick_config_entry(benchmark, quick_configs)
if "benchmarks" in config_entry:
for bench_name, axis_config in config_entry["benchmarks"].items():
cpp_axis_args.extend(["--benchmark", bench_name])
cpp_axis_args.extend(
build_axis_args_from_config(axis_config, for_python=False)
)
py_axis_args.extend(["--benchmark", bench_name])
py_axis_args.extend(
build_axis_args_from_config(axis_config, for_python=True)
)
else:
cpp_axis_args = build_axis_args_from_config(config_entry, for_python=False)
py_axis_args = build_axis_args_from_config(config_entry, for_python=True)
results = {}
# Run C++ benchmark
if run_cpp:
print(f"Running C++ benchmark: {cpp_binary}")
cpp_bin = CUB_BENCH_DIR / cpp_binary
if not cpp_bin.exists():
print(f"ERROR: C++ binary not found: {cpp_bin}")
print()
print("Please build C++ benchmarks first:")
print(f" cd {CCCL_ROOT}")
print(" ./ci/build_cub.sh -arch <your_gpu_arch> # e.g., 89 for RTX 4090")
print()
print("Available benchmarks in build directory:")
if CUB_BENCH_DIR.exists():
binaries = list(CUB_BENCH_DIR.glob("*.base"))[:10]
for b in binaries:
print(f" {b.name}")
if len(list(CUB_BENCH_DIR.glob("*.base"))) > 10:
print(" ...")
else:
print(f" (directory not found: {CUB_BENCH_DIR})")
sys.exit(1)
cmd = [str(cpp_bin), "--json", str(cpp_result), "--devices", device]
cmd.extend(cpp_axis_args)
if profile:
cmd.append("--profile")
# Ensure the CUB build lib dir (containing libnvbench.so) is on the
# dynamic linker search path for the child process.
cpp_lib_dir = str(CUB_BENCH_DIR.parent / "lib")
cpp_env = os.environ.copy()
existing_ld = cpp_env.get("LD_LIBRARY_PATH", "")
cpp_env["LD_LIBRARY_PATH"] = (
f"{cpp_lib_dir}:{existing_ld}" if existing_ld else cpp_lib_dir
)
cpp_status = run_and_log(cmd, cpp_log, env=cpp_env)
print(f" Results: {cpp_result}")
print(f" Log: {cpp_log}")
if cpp_status["status"] != "ok":
print(f" WARNING: C++ benchmark failed (exit {cpp_status['returncode']}).")
results["cpp"] = cpp_result
results["cpp_status"] = cpp_status
# Run Python benchmark
if run_py:
print(f"Running Python benchmark: {py_script.relative_to(SCRIPT_DIR)}")
if not py_script.exists():
print(f"ERROR: Python script not found: {py_script}")
sys.exit(1)
cmd = [
sys.executable,
str(py_script),
"--json",
str(py_result),
"--devices",
device,
]
cmd.extend(py_axis_args)
if profile:
cmd.append("--profile")
py_status = run_and_log(cmd, py_log)
print(f" Results: {py_result}")
print(f" Log: {py_log}")
if py_status["status"] != "ok":
print(
f" WARNING: Python benchmark failed (exit {py_status['returncode']})."
)
results["py"] = py_result
results["py_status"] = py_status
return results
# ============================================================================
# Main
# ============================================================================
def main():
parser = argparse.ArgumentParser(
description="Run Python cuda.compute and C++ CUB benchmarks",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""
Supported benchmarks:
{chr(10).join(f" {b}" for b in SUPPORTED_BENCHMARKS)}
""",
)
parser.add_argument(
"-d", "--device", default="0", help="GPU device ID [default: 0]"
)
parser.add_argument(
"-b", "--benchmark", help="Run specific benchmark only [default: all]"
)
parser.add_argument("--py", action="store_true", help="Only run Python benchmarks")
parser.add_argument("--cpp", action="store_true", help="Only run C++ benchmarks")
parser.add_argument(
"-q",
"--quick",
action="store_true",
help="Run with reduced parameter set for fast testing",
)
parser.add_argument(
"--profile",
action="store_true",
help="Run each benchmark configuration once (nvbench profile mode) with "
"no sampling -- for smoke-testing that benchmarks execute without error, "
"not for measuring performance.",
)
args = parser.parse_args()
# Determine what to run
run_py = True
run_cpp = True
if args.py and not args.cpp:
run_cpp = False
elif args.cpp and not args.py:
run_py = False
# Load quick configs if needed
quick_configs = {}
if args.quick:
quick_configs = load_quick_configs()
# Validate and determine benchmarks to run
if args.benchmark:
if args.benchmark not in SUPPORTED_BENCHMARKS:
print(f"ERROR: Benchmark '{args.benchmark}' not supported.")
print()
print("Available benchmarks:")
for b in SUPPORTED_BENCHMARKS:
print(f" {b}")
sys.exit(1)
benchmarks_to_run = [args.benchmark]
else:
benchmarks_to_run = SUPPORTED_BENCHMARKS
# Print configuration
print_banner("CCCL Benchmark Runner")
print("Configuration:")
print(f" CCCL Root: {CCCL_ROOT}")
print(f" C++ Binaries: {CUB_BENCH_DIR}")
print(f" Results Dir: {RESULTS_DIR}")
print(f" Device: {args.device}")
print(f" Benchmarks: {' '.join(benchmarks_to_run)}")
print(f" Run C++: {run_cpp}")
print(f" Run Python: {run_py}")
print(f" Quick Mode: {args.quick}")
print(f" Profile Mode: {args.profile}")
print()
# Check C++ binaries directory exists (if running C++)
if run_cpp and not CUB_BENCH_DIR.exists():
print(f"ERROR: C++ benchmark directory not found: {CUB_BENCH_DIR}")
print()
print("Please build C++ benchmarks first:")
print(f" cd {CCCL_ROOT}")
print(" ./ci/build_cub.sh -arch <your_gpu_arch> # e.g., 89 for RTX 4090")
sys.exit(1)
# Run benchmarks
all_results = {}
for bench in benchmarks_to_run:
print_section(f"Benchmark: {bench}")
results = run_benchmark(
bench, args.device, run_py, run_cpp, args.quick, quick_configs, args.profile
)
all_results[bench] = results
print()
# Print summary
print_banner("Summary")
print(f"Results directory: {RESULTS_DIR}")
print()
print("Generated files:")
for bench in benchmarks_to_run:
cpp_result = get_result_path(bench, "cpp")
py_result = get_result_path(bench, "py")
cpp_log = get_log_path(bench, "cpp")
py_log = get_log_path(bench, "py")
print(f" {bench}:")
if cpp_result.exists():
print(f" C++ results: {cpp_result}")
if cpp_log.exists():
print(f" C++ log: {cpp_log}")
if py_result.exists():
print(f" Python results: {py_result}")
if py_log.exists():
print(f" Python log: {py_log}")
status = all_results.get(bench, {})
cpp_status = status.get("cpp_status")
py_status = status.get("py_status")
if cpp_status and cpp_status.get("status") != "ok":
print(
" C++ status: "
f"{cpp_status.get('status')} (exit {cpp_status.get('returncode')})"
)
if py_status and py_status.get("status") != "ok":
print(
" Python status: "
f"{py_status.get('status')} (exit {py_status.get('returncode')})"
)
print()
# Exit non-zero if any benchmark failed so CI (e.g. the --profile smoke gate)
# catches benchmark rot instead of silently passing on printed warnings.
failed = []
for bench in benchmarks_to_run:
results = all_results.get(bench, {})
for key in ("cpp_status", "py_status"):
if results.get(key, {}).get("status") not in (None, "ok"):
failed.append(bench)
break
if failed:
print(f"ERROR: {len(failed)} benchmark(s) failed: {failed}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,83 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for exclusive scan custom operation using cuda.compute.exclusive_scan.
C++ equivalent: cub/benchmarks/bench/scan/exclusive/custom.cu
Notes:
- Uses a custom max operator (not OpKind)
- int128 and complex32 are not supported by cupy
- Migration: Python fixes offsets; C++ exposes an OffsetT axis.
- OffsetT axis is omitted because the Python API does not expose offset type.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
import cupy as cp
import numpy as np
from utils import SIGNED_TYPES as TYPE_MAP
from utils import as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import make_exclusive_scan
def max_op(a, b):
return a if a > b else b
def bench_scan_exclusive_custom(state: bench.State):
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_items = int(state.get_int64("Elements{io}"))
alloc_stream = as_cupy_stream(state.get_stream())
with alloc_stream:
d_in = generate_data_with_entropy(num_items, dtype, "1.000", alloc_stream)
d_out = cp.empty(num_items, dtype=dtype)
h_init = np.zeros(1, dtype=dtype)
scanner = make_exclusive_scan(d_in=d_in, d_out=d_out, op=max_op, init_value=h_init)
temp_storage_bytes = scanner(
temp_storage=None,
d_in=d_in,
d_out=d_out,
op=max_op,
init_value=h_init,
num_items=num_items,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
state.add_element_count(num_items)
state.add_global_memory_reads(num_items * d_in.dtype.itemsize, "Size")
state.add_global_memory_writes(num_items * d_out.dtype.itemsize)
def launcher(launch: bench.Launch):
scanner(
temp_storage=temp_storage,
d_in=d_in,
d_out=d_out,
op=max_op,
init_value=h_init,
num_items=num_items,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_scan_exclusive_custom)
b.set_name("base")
b.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 33, 4))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,84 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for exclusive scan sum operation using cuda.compute.exclusive_scan.
C++ equivalent: cub/benchmarks/bench/scan/exclusive/sum.cu
Notes:
- int128 and complex32 are not supported by cupy
- Migration: Python fixes offsets; C++ exposes an OffsetT axis.
- OffsetT axis is omitted because the Python API does not expose offset type.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
import cupy as cp
import numpy as np
from utils import SIGNED_TYPES as TYPE_MAP
from utils import as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import OpKind, make_exclusive_scan
def bench_scan_exclusive_sum(state: bench.State):
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_items = int(state.get_int64("Elements{io}"))
alloc_stream = as_cupy_stream(state.get_stream())
with alloc_stream:
d_in = generate_data_with_entropy(num_items, dtype, "1.000", alloc_stream)
# Output is same size as input for scan
d_out = cp.empty(num_items, dtype=dtype)
# Initial value for scan (identity for addition)
h_init = np.zeros(1, dtype=dtype)
scanner = make_exclusive_scan(
d_in=d_in, d_out=d_out, op=OpKind.PLUS, init_value=h_init
)
temp_storage_bytes = scanner(
temp_storage=None,
d_in=d_in,
d_out=d_out,
op=OpKind.PLUS,
init_value=h_init,
num_items=num_items,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
state.add_element_count(num_items)
state.add_global_memory_reads(num_items * d_in.dtype.itemsize, "Size")
state.add_global_memory_writes(num_items * d_out.dtype.itemsize)
def launcher(launch: bench.Launch):
scanner(
temp_storage=temp_storage,
d_in=d_in,
d_out=d_out,
op=OpKind.PLUS,
init_value=h_init,
num_items=num_items,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_scan_exclusive_sum)
b.set_name("base")
b.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 33, 4))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,165 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for segmented_reduce (sum) with variable-size segments.
C++ equivalent: cub/benchmarks/bench/segmented_reduce/variable_sum.cu (uses variable_base.cuh)
Notes:
- Implements four sub-benchmarks: variable_default, variable_small_dynamic,
variable_medium_dynamic, variable_large_dynamic.
- OffsetT axis is omitted because the Python API does not expose offset type.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import (
ALL_TYPES,
as_cupy_stream,
generate_data_with_entropy,
generate_uniform_segment_offsets,
)
import cuda.bench as bench
from cuda.compute import OpKind, make_segmented_reduce
TYPE_MAP = {k: ALL_TYPES[k] for k in ("I32", "I64", "F32", "F64")}
def run_segmented_reduce(
state: bench.State,
d_in,
d_out,
h_init,
start_offsets,
end_offsets,
num_segments,
guaranteed_max_seg_size,
):
reducer = make_segmented_reduce(
d_in=d_in,
d_out=d_out,
start_offsets_in=start_offsets,
end_offsets_in=end_offsets,
op=OpKind.PLUS,
h_init=h_init,
)
temp_storage_bytes = reducer(
temp_storage=None,
d_in=d_in,
d_out=d_out,
num_segments=num_segments,
start_offsets_in=start_offsets,
end_offsets_in=end_offsets,
op=OpKind.PLUS,
h_init=h_init,
max_segment_size=guaranteed_max_seg_size,
)
alloc_stream = as_cupy_stream(state.get_stream())
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
def launcher(launch: bench.Launch):
reducer(
temp_storage=temp_storage,
d_in=d_in,
d_out=d_out,
num_segments=num_segments,
start_offsets_in=start_offsets,
end_offsets_in=end_offsets,
op=OpKind.PLUS,
h_init=h_init,
max_segment_size=guaranteed_max_seg_size,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False, sync=True)
def bench_variable_segmented_reduce(state: bench.State):
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_elements = int(state.get_int64("Elements{io}"))
max_segment_size = int(state.get_int64("MaxSegmentSize"))
guaranteed_max_seg_size = int(state.get_int64("GuaranteedMaxSegSize"))
# Skip cases where hint would be incorrect (max > guaranteed)
if guaranteed_max_seg_size != 0 and max_segment_size > guaranteed_max_seg_size:
state.skip("max_segment_size > guaranteed_max_seg_size")
return
min_segment_size = 1
offsets = generate_uniform_segment_offsets(
num_elements, min_segment_size, max_segment_size
)
alloc_stream = as_cupy_stream(state.get_stream())
h_init = np.zeros(1, dtype=dtype)
d_in = generate_data_with_entropy(num_elements, dtype, "1.000", alloc_stream)
with alloc_stream:
start_offsets = cp.asarray(offsets[:-1], dtype=np.int64)
end_offsets = cp.asarray(offsets[1:], dtype=np.int64)
d_out = cp.empty(int(start_offsets.size), dtype=dtype)
alloc_stream.synchronize()
num_segments = int(start_offsets.size)
state.add_element_count(num_elements)
state.add_global_memory_reads(num_elements * d_in.dtype.itemsize)
state.add_global_memory_writes(num_segments * d_out.dtype.itemsize)
state.add_global_memory_reads((num_segments + 1) * start_offsets.dtype.itemsize)
run_segmented_reduce(
state,
d_in,
d_out,
h_init,
start_offsets,
end_offsets,
num_segments,
guaranteed_max_seg_size,
)
if __name__ == "__main__":
# Default: no size hint — uses generic large-reduce kernel regardless of segment size
b_default = bench.register(bench_variable_segmented_reduce)
b_default.set_name("variable_default")
b_default.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b_default.add_int64_power_of_two_axis("Elements{io}", range(16, 28, 4))
b_default.add_int64_power_of_two_axis("MaxSegmentSize", range(1, 17, 1))
b_default.add_int64_axis("GuaranteedMaxSegSize", [0])
# Small segments (116 items): hint enables warp-level reduction
b_small = bench.register(bench_variable_segmented_reduce)
b_small.set_name("variable_small_dynamic")
b_small.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b_small.add_int64_power_of_two_axis("Elements{io}", range(16, 28, 4))
b_small.add_int64_power_of_two_axis("MaxSegmentSize", range(1, 5, 1))
b_small.add_int64_power_of_two_axis("GuaranteedMaxSegSize", range(1, 5, 1))
# Medium segments (32256 items): hint enables warp-level reduction
b_medium = bench.register(bench_variable_segmented_reduce)
b_medium.set_name("variable_medium_dynamic")
b_medium.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b_medium.add_int64_power_of_two_axis("Elements{io}", range(16, 28, 4))
b_medium.add_int64_power_of_two_axis("MaxSegmentSize", range(5, 9, 1))
b_medium.add_int64_power_of_two_axis("GuaranteedMaxSegSize", range(5, 9, 1))
# Large segments (512+ items): hint enables block-level reduction
b_large = bench.register(bench_variable_segmented_reduce)
b_large.set_name("variable_large_dynamic")
b_large.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b_large.add_int64_power_of_two_axis("Elements{io}", range(16, 28, 4))
b_large.add_int64_power_of_two_axis("MaxSegmentSize", range(9, 17, 1))
b_large.add_int64_power_of_two_axis("GuaranteedMaxSegSize", range(9, 17, 1))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,167 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for segmented_sort keys using cuda.compute.
C++ equivalent: cub/benchmarks/bench/segmented_sort/keys.cu
Notes:
- Implements three sub-benchmarks: power, small, large
- Power uses power-law segment sizes with Entropy axis
- Small/large use uniform segment sizes with MaxSegmentSize axis
- Migration: uniform offsets use min_segment_size ~ max/2.
- OffsetT axis is omitted because the Python API does not expose offset type.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import (
FUNDAMENTAL_TYPES as TYPE_MAP,
)
from utils import (
as_cupy_stream,
generate_data_with_entropy,
generate_power_law_offsets,
generate_uniform_segment_offsets,
)
import cuda.bench as bench
from cuda.compute import SortOrder, make_segmented_sort
def run_segmented_sort(
state: bench.State,
d_in_keys,
d_out_keys,
start_offsets,
end_offsets,
num_items,
num_segments,
):
sorter = make_segmented_sort(
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=None,
d_out_values=None,
start_offsets_in=start_offsets,
end_offsets_in=end_offsets,
order=SortOrder.ASCENDING,
)
temp_storage_bytes = sorter(
temp_storage=None,
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=None,
d_out_values=None,
num_items=num_items,
num_segments=num_segments,
start_offsets_in=start_offsets,
end_offsets_in=end_offsets,
)
alloc_stream = as_cupy_stream(state.get_stream())
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
def launcher(launch: bench.Launch):
sorter(
temp_storage=temp_storage,
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=None,
d_out_values=None,
num_items=num_items,
num_segments=num_segments,
start_offsets_in=start_offsets,
end_offsets_in=end_offsets,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False, sync=True)
def bench_segmented_sort(state: bench.State, use_power_law: bool):
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_elements = int(state.get_int64("Elements{io}"))
alloc_stream = as_cupy_stream(state.get_stream())
if use_power_law:
num_segments = int(state.get_int64("Segments{io}"))
entropy_str = state.get_string("Entropy")
else:
max_segment_size = int(state.get_int64("MaxSegmentSize"))
min_segment_size = max(1, max_segment_size // 2)
entropy_str = "1.000"
if use_power_law:
offsets = generate_power_law_offsets(num_elements, num_segments)
else:
offsets = generate_uniform_segment_offsets(
num_elements, min_segment_size, max_segment_size
)
d_in_keys = generate_data_with_entropy(
num_elements, dtype, entropy_str, alloc_stream
)
with alloc_stream:
d_out_keys = cp.empty(num_elements, dtype=dtype)
start_offsets = cp.asarray(offsets[:-1], dtype=np.int64)
end_offsets = cp.asarray(offsets[1:], dtype=np.int64)
alloc_stream.synchronize()
num_segments = int(start_offsets.size)
state.add_element_count(num_elements)
state.add_global_memory_reads(num_elements * d_in_keys.dtype.itemsize)
state.add_global_memory_writes(num_elements * d_out_keys.dtype.itemsize)
state.add_global_memory_reads((num_segments + 1) * start_offsets.dtype.itemsize)
run_segmented_sort(
state,
d_in_keys,
d_out_keys,
start_offsets,
end_offsets,
num_elements,
num_segments,
)
def bench_segmented_sort_power(state: bench.State):
bench_segmented_sort(state, use_power_law=True)
def bench_segmented_sort_uniform(state: bench.State):
bench_segmented_sort(state, use_power_law=False)
if __name__ == "__main__":
b_power = bench.register(bench_segmented_sort_power)
b_power.set_name("power")
b_power.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b_power.add_int64_power_of_two_axis("Elements{io}", range(22, 31, 4))
b_power.add_int64_power_of_two_axis("Segments{io}", range(12, 21, 4))
b_power.add_string_axis("Entropy", ["1.000", "0.201"])
b_small = bench.register(bench_segmented_sort_uniform)
b_small.set_name("small")
b_small.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b_small.add_int64_power_of_two_axis("Elements{io}", range(22, 31, 4))
b_small.add_int64_power_of_two_axis("MaxSegmentSize", range(1, 9, 1))
b_large = bench.register(bench_segmented_sort_uniform)
b_large.set_name("large")
b_large.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b_large.add_int64_power_of_two_axis("Elements{io}", range(22, 31, 4))
b_large.add_int64_power_of_two_axis("MaxSegmentSize", range(10, 19, 2))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,119 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for select_if using cuda.compute.
C++ equivalent: cub/benchmarks/bench/select/if.cu
Notes:
- The C++ benchmark uses a `less_then_t<T>` predicate with threshold based on entropy
- Entropy controls what fraction of elements are selected:
- 1.000 → selects ~100% (threshold = max value)
- 0.544 → selects ~54.4% (threshold at 54.4% of range)
- 0.000 → selects ~0% (threshold = min value)
- InPlace axis controls whether output can alias input (not exposed in Python API)
- Migration: Python cannot expose InPlace axis; output is sized to num_elements but metrics use actual selected count.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import (
ENTROPY_TO_PROB,
as_cupy_stream,
generate_data_with_entropy,
lerp_min_max,
)
from utils import (
FUNDAMENTAL_TYPES as TYPE_MAP,
)
import cuda.bench as bench
from cuda.compute import make_select
# Entropy values from C++ benchmark
# These control the selection threshold and thus how many elements are selected
def bench_select_if(state: bench.State):
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_elements = int(state.get_int64("Elements{io}"))
entropy_str = state.get_string("Entropy")
probability = ENTROPY_TO_PROB[entropy_str]
threshold = lerp_min_max(dtype, probability)
alloc_stream = as_cupy_stream(state.get_stream())
# Match C++ benchmark: input data generation is independent of Entropy.
# Entropy only controls the selection threshold.
d_in = generate_data_with_entropy(num_elements, dtype, "1.000", alloc_stream)
with alloc_stream:
selected_elements = int(cp.count_nonzero(d_in < threshold).get())
d_out = cp.empty(selected_elements, dtype=dtype)
d_num_selected = cp.zeros(1, dtype=np.int64)
alloc_stream.synchronize()
# Create predicate: select elements less than threshold
# For numba device functions, we need to use the value directly in closure
thresh_val = threshold
def less_than_threshold(x):
return x < thresh_val
selector = make_select(
d_in=d_in,
d_out=d_out,
d_num_selected_out=d_num_selected,
cond=less_than_threshold,
)
temp_storage_bytes = selector(
temp_storage=None,
d_in=d_in,
d_out=d_out,
d_num_selected_out=d_num_selected,
cond=less_than_threshold,
num_items=num_elements,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
state.add_element_count(num_elements)
state.add_global_memory_reads(num_elements * d_in.dtype.itemsize)
state.add_global_memory_writes(selected_elements * d_out.dtype.itemsize)
state.add_global_memory_writes(1 * d_num_selected.dtype.itemsize)
def launcher(launch: bench.Launch):
selector(
temp_storage=temp_storage,
d_in=d_in,
d_out=d_out,
d_num_selected_out=d_num_selected,
cond=less_than_threshold,
num_items=num_elements,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_select_if)
b.set_name("base")
b.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 29, 4))
b.add_string_axis("Entropy", ["1.000", "0.544", "0.000"])
# Note: InPlace axis is not exposed in Python API, so we skip it
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,137 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for unique_by_key using cuda.compute.
C++ equivalent: cub/benchmarks/bench/select/unique_by_key.cu
Notes:
- The C++ benchmark uses MaxSegSize axis to control segment sizes
- Uses equal_to comparison operator for key equality
- Generates key segments with sizes between 1 and MaxSegSize
- Both keys and values are processed
- Migration: Python fixes offsets and generates key segments on GPU to mirror C++.
- OffsetT axis is omitted because the Python API does not expose offset type.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import INTEGRAL_TYPES, SIGNED_TYPES, as_cupy_stream, generate_key_segments
import cuda.bench as bench
from cuda.compute import OpKind, make_unique_by_key
KEY_TYPE_MAP = INTEGRAL_TYPES
VALUE_TYPE_MAP = {**SIGNED_TYPES, "C32": np.complex64}
def bench_unique_by_key(state: bench.State):
key_type_str = state.get_string("KeyT{ct}")
value_type_str = state.get_string("ValueT{ct}")
key_dtype = KEY_TYPE_MAP[key_type_str]
value_dtype = VALUE_TYPE_MAP[value_type_str]
num_elements = int(state.get_int64("Elements{io}"))
max_seg_size = int(state.get_int64("MaxSegSize"))
if num_elements > np.iinfo(np.int32).max:
state.skip("Skipping: num_elements exceeds int32 limits")
return
alloc_stream = as_cupy_stream(state.get_stream())
d_in_keys = generate_key_segments(
num_elements,
key_dtype,
min_segment_size=1,
max_segment_size=max_seg_size,
stream=alloc_stream,
)
with alloc_stream:
d_in_values = cp.zeros(num_elements, dtype=value_dtype)
d_out_keys = cp.empty(num_elements, dtype=key_dtype)
d_out_values = cp.empty(num_elements, dtype=value_dtype)
d_num_selected = cp.empty(1, dtype=np.int32)
alloc_stream.synchronize()
uniquer = make_unique_by_key(
d_in_keys=d_in_keys,
d_in_items=d_in_values,
d_out_keys=d_out_keys,
d_out_items=d_out_values,
d_out_num_selected=d_num_selected,
op=OpKind.EQUAL_TO,
)
temp_storage_bytes = uniquer(
temp_storage=None,
d_in_keys=d_in_keys,
d_in_items=d_in_values,
d_out_keys=d_out_keys,
d_out_items=d_out_values,
d_out_num_selected=d_num_selected,
op=OpKind.EQUAL_TO,
num_items=num_elements,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
# Run once before timing to materialize the number of selected runs,
# matching the C++ metric accounting flow.
uniquer(
temp_storage=temp_storage,
d_in_keys=d_in_keys,
d_in_items=d_in_values,
d_out_keys=d_out_keys,
d_out_items=d_out_values,
d_out_num_selected=d_num_selected,
op=OpKind.EQUAL_TO,
num_items=num_elements,
stream=alloc_stream,
)
alloc_stream.synchronize()
num_runs = int(d_num_selected.get()[0])
state.add_element_count(num_elements)
state.add_global_memory_reads(int(num_elements * d_in_keys.dtype.itemsize))
state.add_global_memory_reads(int(num_elements * d_in_values.dtype.itemsize))
state.add_global_memory_writes(int(num_runs * d_out_keys.dtype.itemsize))
state.add_global_memory_writes(int(num_runs * d_out_values.dtype.itemsize))
state.add_global_memory_writes(int(d_num_selected.dtype.itemsize))
def launcher(launch: bench.Launch):
uniquer(
temp_storage=temp_storage,
d_in_keys=d_in_keys,
d_in_items=d_in_values,
d_out_keys=d_out_keys,
d_out_items=d_out_values,
d_out_num_selected=d_num_selected,
op=OpKind.EQUAL_TO,
num_items=num_elements,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_unique_by_key)
b.set_name("base")
b.add_string_axis("KeyT{ct}", list(KEY_TYPE_MAP.keys()))
b.add_string_axis("ValueT{ct}", list(VALUE_TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 29, 4))
b.add_int64_power_of_two_axis("MaxSegSize", [1, 4, 8])
# Note: OffsetT axis from C++ is not exposed in Python API
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,237 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for BabelStream operations using cuda.compute transforms.
C++ equivalent: cub/benchmarks/bench/transform/babelstream.cu
Notes:
- Migration: Python omits OffsetT axis and int128 types.
- OffsetT axis is omitted because the Python API does not expose offset type.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
from utils import ALL_TYPES as _ALL_TYPES
from utils import as_cupy_stream
import cuda.bench as bench
import cuda.compute
from cuda.compute import ZipIterator
TYPE_MAP = {k: _ALL_TYPES[k] for k in ("I8", "I16", "F32", "F64")}
START_A = 11
START_B = 2
START_C = 1
START_SCALAR = -2
assert START_A == START_A + START_B + START_SCALAR * START_C
def _reset_pools():
cp.get_default_memory_pool().free_all_blocks()
cp.get_default_pinned_memory_pool().free_all_blocks()
def bench_mul(state: bench.State):
"""
Benchmark: b[i] = c[i] * scalar
Unary transform with scalar multiplication.
"""
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_items = int(state.get_int64("Elements{io}"))
_reset_pools()
alloc_stream = as_cupy_stream(state.get_stream())
try:
with alloc_stream:
c = cp.full(num_items, START_C, dtype=dtype)
b = cp.full(num_items, START_B, dtype=dtype)
except (MemoryError, cp.cuda.memory.OutOfMemoryError):
state.skip("Skipping: out of memory.")
return
scalar = dtype(START_SCALAR)
def mul_op(ci):
return ci * scalar
transform = cuda.compute.make_unary_transform(d_in=c, d_out=b, op=mul_op)
state.add_element_count(num_items)
state.add_global_memory_reads(num_items * c.dtype.itemsize)
state.add_global_memory_writes(num_items * b.dtype.itemsize)
def launcher(launch: bench.Launch):
transform(
d_in=c, d_out=b, op=mul_op, num_items=num_items, stream=launch.get_stream()
)
state.exec(launcher, batched=False)
def bench_add(state: bench.State):
"""
Benchmark: c[i] = a[i] + b[i]
Binary transform with addition.
"""
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_items = int(state.get_int64("Elements{io}"))
_reset_pools()
alloc_stream = as_cupy_stream(state.get_stream())
try:
with alloc_stream:
a = cp.full(num_items, START_A, dtype=dtype)
b = cp.full(num_items, START_B, dtype=dtype)
c = cp.full(num_items, START_C, dtype=dtype)
except (MemoryError, cp.cuda.memory.OutOfMemoryError):
state.skip("Skipping: out of memory.")
return
def add_op(ai, bi):
return ai + bi
transform = cuda.compute.make_binary_transform(d_in1=a, d_in2=b, d_out=c, op=add_op)
state.add_element_count(num_items)
state.add_global_memory_reads(2 * num_items * a.dtype.itemsize)
state.add_global_memory_writes(num_items * c.dtype.itemsize)
def launcher(launch: bench.Launch):
transform(
d_in1=a,
d_in2=b,
d_out=c,
op=add_op,
num_items=num_items,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
def bench_triad(state: bench.State):
"""
Benchmark: a[i] = b[i] + scalar * c[i]
Binary transform with fused multiply-add.
"""
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_items = int(state.get_int64("Elements{io}"))
_reset_pools()
alloc_stream = as_cupy_stream(state.get_stream())
try:
with alloc_stream:
a = cp.full(num_items, START_A, dtype=dtype)
b = cp.full(num_items, START_B, dtype=dtype)
c = cp.full(num_items, START_C, dtype=dtype)
except (MemoryError, cp.cuda.memory.OutOfMemoryError):
state.skip("Skipping: out of memory.")
return
scalar = dtype(START_SCALAR)
def triad_op(bi, ci):
return bi + scalar * ci
transform = cuda.compute.make_binary_transform(
d_in1=b, d_in2=c, d_out=a, op=triad_op
)
state.add_element_count(num_items)
state.add_global_memory_reads(2 * num_items * a.dtype.itemsize)
state.add_global_memory_writes(num_items * a.dtype.itemsize)
def launcher(launch: bench.Launch):
transform(
d_in1=b,
d_in2=c,
d_out=a,
op=triad_op,
num_items=num_items,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
def bench_nstream(state: bench.State):
"""
Benchmark: a[i] = a[i] + b[i] + scalar * c[i]
Ternary transform using ZipIterator to combine (a, b, c) as input.
"""
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_items = int(state.get_int64("Elements{io}"))
_reset_pools()
alloc_stream = as_cupy_stream(state.get_stream())
try:
with alloc_stream:
a = cp.full(num_items, START_A, dtype=dtype)
b = cp.full(num_items, START_B, dtype=dtype)
c = cp.full(num_items, START_C, dtype=dtype)
except (MemoryError, cp.cuda.memory.OutOfMemoryError):
state.skip("Skipping: out of memory.")
return
scalar = dtype(START_SCALAR)
# Use ZipIterator to combine 3 inputs into one for unary transform
zip_in = ZipIterator(a, b, c)
def nstream_op(abc):
return abc[0] + abc[1] + scalar * abc[2]
transform = cuda.compute.make_unary_transform(d_in=zip_in, d_out=a, op=nstream_op)
state.add_element_count(num_items)
state.add_global_memory_reads(3 * num_items * a.dtype.itemsize)
state.add_global_memory_writes(num_items * a.dtype.itemsize)
def launcher(launch: bench.Launch):
# Update ZipIterator state for each iteration
zip_in_iter = ZipIterator(a, b, c)
transform(
d_in=zip_in_iter,
d_out=a,
op=nstream_op,
num_items=num_items,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
# Registry of all BabelStream benchmarks
BENCHMARKS = {
"mul": bench_mul,
"add": bench_add,
"triad": bench_triad,
"nstream": bench_nstream,
}
if __name__ == "__main__":
for name, bench_fn in BENCHMARKS.items():
b = bench.register(bench_fn)
b.set_name(name)
b.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 33, 4))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,105 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for complex comparison using cuda.compute.
C++ equivalent: cub/benchmarks/bench/transform/complex_cmp.cu
Notes:
- Uses two overlapping input ranges (in[0:n-1], in[1:n])
- Output is boolean array of size n-1
- Benchmark name is "compare_complex" to match C++
- Migration: Python uses explicit lexicographic compare for complex64.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import math
import cupy as cp
import numpy as np
from utils import as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import make_binary_transform
_COMPLEX_EPS = np.finfo(np.float32).eps
_COMPLEX_THRESHOLD = _COMPLEX_EPS * 2.0
def less_complex(a, b):
mag0 = math.sqrt(a.real * a.real + a.imag * a.imag)
mag1 = math.sqrt(b.real * b.real + b.imag * b.imag)
if math.isnan(mag0) or math.isnan(mag1):
return False
if math.isinf(mag0) or math.isinf(mag1):
scaler = 0.5
mag0 = math.sqrt(
(a.real * scaler) * (a.real * scaler)
+ (a.imag * scaler) * (a.imag * scaler)
)
mag1 = math.sqrt(
(b.real * scaler) * (b.real * scaler)
+ (b.imag * scaler) * (b.imag * scaler)
)
if abs(mag0 - mag1) < _COMPLEX_THRESHOLD:
phase0 = math.atan2(a.imag, a.real)
phase1 = math.atan2(b.imag, b.real)
return phase0 < phase1
return mag0 < mag1
def bench_compare_complex(state: bench.State):
num_elements = int(state.get_int64("Elements{io}"))
alloc_stream = as_cupy_stream(state.get_stream())
try:
with alloc_stream:
real = generate_data_with_entropy(
num_elements, np.float32, "1.000", alloc_stream
)
imag = generate_data_with_entropy(
num_elements, np.float32, "1.000", alloc_stream
)
d_in = (real + 1j * imag).astype(np.complex64)
d_out = cp.empty(num_elements - 1, dtype=np.bool_)
except (MemoryError, cp.cuda.memory.OutOfMemoryError):
state.skip("Skipping: out of memory.")
return
num_items = num_elements - 1
transformer = make_binary_transform(
d_in1=d_in[:-1], d_in2=d_in[1:], d_out=d_out, op=less_complex
)
state.add_element_count(num_elements)
state.add_global_memory_reads(num_elements * d_in.dtype.itemsize)
state.add_global_memory_writes(num_elements * d_out.dtype.itemsize)
def launcher(launch: bench.Launch):
transformer(
d_in1=d_in[:-1],
d_in2=d_in[1:],
d_out=d_out,
op=less_complex,
num_items=num_items,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_compare_complex)
b.set_name("compare_complex")
b.add_int64_power_of_two_axis("Elements{io}", range(16, 33, 4))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,94 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for transform fibonacci using cuda.compute.
C++ equivalent: cub/benchmarks/bench/transform/fib.cu
Notes:
- Input values are int64 in [0, 42]
- Output values are uint32
- Benchmark name is "fibonacci" to match C++
- Migration: Python fixes offsets to int64.
- OffsetT axis is omitted because the Python API does not expose offset type.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import make_unary_transform
def fib_op(n):
t1 = 0
t2 = 1
if n < 1:
return t1
if n == 1:
return t1
if n == 2:
return t2
i = 3
while i <= n:
next_val = t1 + t2
t1 = t2
t2 = next_val
i += 1
return t2
def bench_transform_fib(state: bench.State):
# Axes
num_elements = int(state.get_int64("Elements{io}"))
alloc_stream = as_cupy_stream(state.get_stream())
try:
with alloc_stream:
d_in = generate_data_with_entropy(
num_elements,
np.int64,
"1.000",
alloc_stream,
min_val=np.int64(0),
max_val=np.int64(42),
)
d_out = cp.empty(num_elements, dtype=np.uint32)
except (MemoryError, cp.cuda.memory.OutOfMemoryError):
state.skip("Skipping: out of memory.")
return
transformer = make_unary_transform(d_in=d_in, d_out=d_out, op=fib_op)
state.add_element_count(num_elements)
state.add_global_memory_reads(num_elements * d_in.dtype.itemsize)
state.add_global_memory_writes(num_elements * d_out.dtype.itemsize)
def launcher(launch: bench.Launch):
transformer(
d_in=d_in,
d_out=d_out,
op=fib_op,
num_items=num_elements,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_transform_fib)
b.set_name("fibonacci")
b.add_int64_power_of_two_axis("Elements{io}", range(16, 33, 4))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,72 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for fill operation using cuda.compute.ConstantIterator.
C++ equivalent: cub/benchmarks/bench/transform/fill.cu
Notes:
- Migration: Python matches C++ integral_types (I8-I64); no tune parameters.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
from utils import INTEGRAL_TYPES as TYPE_MAP
from utils import as_cupy_stream
import cuda.bench as bench
import cuda.compute
from cuda.compute import ConstantIterator, OpKind
def bench_fill(state: bench.State):
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_items = int(state.get_int64("Elements{io}"))
# Setup data
alloc_stream = as_cupy_stream(state.get_stream())
try:
with alloc_stream:
d_out = cp.empty(num_items, dtype=dtype)
except (MemoryError, cp.cuda.memory.OutOfMemoryError):
state.skip("Skipping: out of memory.")
return
# Python equivalent of C++ return_constant<T>{42}
constant_it = ConstantIterator(dtype(42))
transform = cuda.compute.make_unary_transform(
d_in=constant_it, d_out=d_out, op=OpKind.IDENTITY
)
state.add_element_count(num_items)
state.add_global_memory_reads(0)
state.add_global_memory_writes(num_items * d_out.dtype.itemsize)
def launcher(launch: bench.Launch):
transform(
d_in=constant_it,
d_out=d_out,
op=OpKind.IDENTITY,
num_items=num_items,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_fill)
b.set_name("fill")
b.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 33, 4))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,89 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for transform grayscale using cuda.compute.
C++ equivalent: cub/benchmarks/bench/transform/grayscale.cu
Notes:
- Input is an RGB struct with three channels
- Output is grayscale value of the same type
- Benchmark name is "grayscale" to match C++
- Migration: Python uses AoS (`gpu_struct`) to mirror C++ `rgb_t<T>`.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
from utils import FLOAT_TYPES as TYPE_MAP
from utils import as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import gpu_struct, make_unary_transform
def bench_transform_grayscale(state: bench.State):
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_elements = int(state.get_int64("Elements{io}"))
# Grayscale weights
w_r = dtype(0.2989)
w_g = dtype(0.587)
w_b = dtype(0.114)
RGB = gpu_struct({"r": dtype, "g": dtype, "b": dtype})
def to_grayscale(pixel: RGB):
return w_r * pixel.r + w_g * pixel.g + w_b * pixel.b
alloc_stream = as_cupy_stream(state.get_stream())
try:
with alloc_stream:
r_data = generate_data_with_entropy(
num_elements, dtype, "1.000", alloc_stream
)
g_data = generate_data_with_entropy(
num_elements, dtype, "1.000", alloc_stream
)
b_data = generate_data_with_entropy(
num_elements, dtype, "1.000", alloc_stream
)
d_pixels = cp.empty(num_elements, dtype=RGB.dtype)
d_pixels["r"] = r_data
d_pixels["g"] = g_data
d_pixels["b"] = b_data
d_out = cp.empty(num_elements, dtype=dtype)
except (MemoryError, cp.cuda.memory.OutOfMemoryError):
state.skip("Skipping: out of memory.")
return
transformer = make_unary_transform(d_in=d_pixels, d_out=d_out, op=to_grayscale)
state.add_element_count(num_elements)
state.add_global_memory_reads(num_elements * d_pixels.dtype.itemsize)
state.add_global_memory_writes(num_elements * d_out.dtype.itemsize)
def launcher(launch: bench.Launch):
transformer(
d_in=d_pixels,
d_out=d_out,
op=to_grayscale,
num_items=num_elements,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_transform_grayscale)
b.set_name("grayscale")
b.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 33, 4))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,138 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""Python benchmark for heavy transform using cuda.compute.
C++ equivalent: cub/benchmarks/bench/transform/heavy.cu
Notes:
- Migration: Python uses Numba local arrays to emulate register pressure.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numba
import numpy as np
from numba import cuda as lang
from utils import as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
import cuda.compute
def _heavy_op_32(data):
reg = lang.local.array(shape=32, dtype=numba.uint32)
reg[0] = data
for i in range(1, 32):
x = reg[i - 1]
reg[i] = x * x + 1
for i in range(32):
x = reg[i]
reg[i] = (x * x) % 19
for i in range(32):
reg[i] = reg[32 - i - 1] * reg[i]
out = data - data # uint32(0)
for i in range(32):
out += reg[i]
return out
def _heavy_op_64(data):
reg = lang.local.array(shape=64, dtype=numba.uint32)
reg[0] = data
for i in range(1, 64):
x = reg[i - 1]
reg[i] = x * x + 1
for i in range(64):
x = reg[i]
reg[i] = (x * x) % 19
for i in range(64):
reg[i] = reg[64 - i - 1] * reg[i]
out = data - data
for i in range(64):
out += reg[i]
return out
def _heavy_op_128(data):
reg = lang.local.array(shape=128, dtype=numba.uint32)
reg[0] = data
for i in range(1, 128):
x = reg[i - 1]
reg[i] = x * x + 1
for i in range(128):
x = reg[i]
reg[i] = (x * x) % 19
for i in range(128):
reg[i] = reg[128 - i - 1] * reg[i]
out = data - data
for i in range(128):
out += reg[i]
return out
def _heavy_op_256(data):
reg = lang.local.array(shape=256, dtype=numba.uint32)
reg[0] = data
for i in range(1, 256):
x = reg[i - 1]
reg[i] = x * x + 1
for i in range(256):
x = reg[i]
reg[i] = (x * x) % 19
for i in range(256):
reg[i] = reg[256 - i - 1] * reg[i]
out = data - data
for i in range(256):
out += reg[i]
return out
_HEAVY_OPS = {
32: _heavy_op_32,
64: _heavy_op_64,
128: _heavy_op_128,
256: _heavy_op_256,
}
def bench_heavy(state: bench.State):
# Axes
n_regs = int(state.get_string("Heaviness{ct}"))
size = int(state.get_int64("Elements{io}"))
alloc_stream = as_cupy_stream(state.get_stream())
try:
with alloc_stream:
d_in = generate_data_with_entropy(size, np.uint32, "1.000", alloc_stream)
d_out = cp.empty(size, dtype=np.uint32)
except (MemoryError, cp.cuda.memory.OutOfMemoryError):
state.skip("Skipping: out of memory.")
return
op = _HEAVY_OPS[n_regs]
transform = cuda.compute.make_unary_transform(d_in=d_in, d_out=d_out, op=op)
state.add_element_count(size)
state.add_global_memory_reads(size * d_in.dtype.itemsize)
state.add_global_memory_writes(size * d_out.dtype.itemsize)
def launcher(launch: bench.Launch):
transform(
d_in=d_in, d_out=d_out, op=op, num_items=size, stream=launch.get_stream()
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_heavy)
b.set_name("heavy")
b.add_string_axis("Heaviness{ct}", [str(v) for v in (32, 64, 128, 256)])
b.add_int64_power_of_two_axis("Elements{io}", range(16, 33, 4))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,86 @@
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Python benchmark for transform reduce sum using cuda.compute.
C++ equivalent: cub/benchmarks/bench/transform_reduce/sum.cu
Notes:
- Uses TransformIterator with a square operation
- OffsetT axis from C++ is fixed to Python default (int64)
- Migration: Python fixes offsets and omits int128/complex types.
- OffsetT axis is omitted because the Python API does not expose offset type.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import cupy as cp
import numpy as np
from utils import SIGNED_TYPES as TYPE_MAP
from utils import as_cupy_stream, generate_data_with_entropy
import cuda.bench as bench
from cuda.compute import OpKind, TransformIterator, make_reduce_into
def square_op(x):
return x * x
def bench_transform_reduce_sum(state: bench.State):
type_str = state.get_string("T{ct}")
dtype = TYPE_MAP[type_str]
num_items = int(state.get_int64("Elements{io}"))
alloc_stream = as_cupy_stream(state.get_stream())
with alloc_stream:
d_in = generate_data_with_entropy(num_items, dtype, "1.000", alloc_stream)
d_out = cp.empty(1, dtype=dtype)
transform_it = TransformIterator(d_in, square_op)
h_init = np.zeros(1, dtype=dtype)
reducer = make_reduce_into(
d_in=transform_it, d_out=d_out, op=OpKind.PLUS, h_init=h_init
)
temp_storage_bytes = reducer(
temp_storage=None,
d_in=transform_it,
d_out=d_out,
num_items=num_items,
op=OpKind.PLUS,
h_init=h_init,
)
with alloc_stream:
temp_storage = cp.empty(temp_storage_bytes, dtype=np.uint8)
state.add_element_count(num_items)
state.add_global_memory_reads(num_items * d_in.dtype.itemsize, "Size")
state.add_global_memory_writes(1 * d_out.dtype.itemsize)
def launcher(launch: bench.Launch):
reducer(
temp_storage=temp_storage,
d_in=transform_it,
d_out=d_out,
num_items=num_items,
op=OpKind.PLUS,
h_init=h_init,
stream=launch.get_stream(),
)
state.exec(launcher, batched=False)
if __name__ == "__main__":
b = bench.register(bench_transform_reduce_sum)
b.set_name("base")
b.add_string_axis("T{ct}", list(TYPE_MAP.keys()))
b.add_int64_power_of_two_axis("Elements{io}", range(16, 29, 4))
bench.run_all_benchmarks(sys.argv)

View File

@@ -0,0 +1,252 @@
import cupy as cp
import numpy as np
import cuda.bench as bench
ALL_TYPES = {
"I8": np.int8,
"I16": np.int16,
"I32": np.int32,
"I64": np.int64,
"U8": np.uint8,
"U16": np.uint16,
"U32": np.uint32,
"U64": np.uint64,
"F32": np.float32,
"F64": np.float64,
}
SIGNED_TYPES = {k: ALL_TYPES[k] for k in ("I8", "I16", "I32", "I64", "F32", "F64")}
FLOAT_TYPES = {k: ALL_TYPES[k] for k in ("F32", "F64")}
# Matches C++ integral_types = {int8_t, int16_t, int32_t, int64_t}
INTEGRAL_TYPES = {k: ALL_TYPES[k] for k in ("I8", "I16", "I32", "I64")}
# Matches C++ fundamental_types = {int8..int64, [int128,] float, double}
# int128 is excluded because it is not supported by numpy/cupy.
FUNDAMENTAL_TYPES = {k: ALL_TYPES[k] for k in ("I8", "I16", "I32", "I64", "F32", "F64")}
ENTROPY_TO_STEPS = {
"1.000": 0,
"0.811": 1,
"0.544": 2,
"0.337": 3,
"0.201": 4,
"0.000": 0,
}
ENTROPY_TO_PROB = {
"1.000": 1.0,
"0.811": 0.811,
"0.544": 0.544,
"0.337": 0.337,
"0.201": 0.201,
"0.000": 0.0,
}
def as_cupy_stream(cs: bench.CudaStream) -> cp.cuda.Stream:
"""Convert nvbench CudaStream to CuPy Stream."""
return cp.cuda.ExternalStream(cs.addressof())
def lerp_min_max(dtype, probability):
"""Interpolate between min/max for dtype like nvbench_helper.cuh."""
if probability == 1.0:
if np.issubdtype(dtype, np.integer):
return np.iinfo(dtype).max
return np.finfo(dtype).max
if np.issubdtype(dtype, np.integer):
min_val = float(np.iinfo(dtype).min)
max_val = float(np.iinfo(dtype).max)
else:
min_val = float(np.finfo(dtype).min)
max_val = float(np.finfo(dtype).max)
return dtype(min_val + probability * (max_val - min_val))
def _bitwise_and(a, b, dtype):
if np.issubdtype(dtype, np.floating):
view_dtype = cp.uint32 if dtype == np.float32 else cp.uint64
return (a.view(view_dtype) & b.view(view_dtype)).view(dtype)
return a & b
def _uniform_random(num_elements, dtype, min_val, max_val):
rand = cp.random.random(num_elements)
if np.issubdtype(dtype, np.floating):
return ((float(max_val) - float(min_val)) * rand + float(min_val)).astype(dtype)
min_f = float(min_val)
max_f = float(max_val)
return cp.floor((max_f - min_f + 1) * rand + min_f).astype(dtype)
def generate_data_with_entropy(
num_elements, dtype, entropy_str, stream, min_val=None, max_val=None
):
"""Generate data with nvbench_helper-style bit entropy."""
if min_val is None or max_val is None:
if np.issubdtype(dtype, np.integer):
info = np.iinfo(dtype)
default_min = info.min
default_max = info.max
else:
info = np.finfo(dtype)
default_min = info.tiny
default_max = info.max
min_val = default_min if min_val is None else min_val
max_val = default_max if max_val is None else max_val
steps = ENTROPY_TO_STEPS[entropy_str]
with stream:
if entropy_str == "0.000":
scalar = _uniform_random(1, dtype, min_val, max_val)[0]
data = cp.full(num_elements, scalar, dtype=dtype)
else:
data = _uniform_random(num_elements, dtype, min_val, max_val)
for _ in range(steps):
tmp = _uniform_random(num_elements, dtype, min_val, max_val)
data = _bitwise_and(data, tmp, dtype)
return data
def generate_uniform_segment_offsets(num_elements, min_segment_size, max_segment_size):
num_elements = int(num_elements)
if min_segment_size <= 0:
raise ValueError("min_segment_size must be positive")
if max_segment_size < min_segment_size:
raise ValueError("max_segment_size must be >= min_segment_size")
num_segments_est = int(np.ceil(num_elements / min_segment_size))
if num_segments_est > np.iinfo(np.int32).max:
raise MemoryError("Too many segments for int32 offsets")
sizes = cp.random.randint(
min_segment_size,
max_segment_size + 1,
size=num_segments_est,
dtype=cp.int64,
)
cumsum = cp.cumsum(sizes)
cutoff = int(
cp.searchsorted(
cumsum, cp.asarray(num_elements, dtype=cp.int64), side="left"
).item()
)
sizes = sizes[: cutoff + 1]
prev = 0 if cutoff == 0 else int(cumsum[cutoff - 1].item())
sizes[cutoff] = num_elements - prev
offsets = cp.empty(cutoff + 2, dtype=cp.int64)
offsets[0] = 0
offsets[1:] = cp.cumsum(sizes)
offsets[-1] = num_elements
return offsets
def generate_power_law_offsets(num_elements, num_segments):
if num_segments <= 0:
return cp.asarray([0, num_elements], dtype=cp.int64)
# Mirror nvbench_helper power-law generation:
# draw log-normal samples, normalize to total elements,
# floor to integer segment sizes, then distribute remainder
# across the first `diff` segments.
samples = cp.random.lognormal(3.0, 1.2, size=num_segments)
if int(cp.count_nonzero(samples).item()) == 0:
samples = cp.ones(num_segments, dtype=cp.float64)
sample_sum = float(samples.sum().item())
sizes = cp.floor(samples * num_elements / sample_sum).astype(cp.int64)
diff = int(num_elements - sizes.sum().item())
if diff > 0:
sizes[:diff] += 1
offsets = cp.empty(num_segments + 1, dtype=cp.int64)
offsets[0] = 0
offsets[1:] = cp.cumsum(sizes)
return offsets
def generate_fixed_segment_offsets(num_elements, segment_size, stream):
num_segments = max(1, num_elements // segment_size)
actual_elements = num_segments * segment_size
with stream:
start_offsets = cp.arange(0, actual_elements, segment_size, dtype=np.int64)
end_offsets = cp.arange(
segment_size, actual_elements + 1, segment_size, dtype=np.int64
)
return start_offsets, end_offsets, num_segments, actual_elements
def generate_key_segments(
num_elements, key_dtype, min_segment_size, max_segment_size, stream
):
"""Generate GPU key segments (runs of equal keys) matching C++ generate.uniform.key_segments.
All computation stays on GPU via CuPy. We avoid ``cp.repeat`` (which
doesn't accept a device array for *repeats*) by building a segment-id
array through ``cp.searchsorted`` on cumulative offsets instead.
"""
num_elements = int(num_elements)
if min_segment_size <= 0:
raise ValueError("min_segment_size must be positive")
if max_segment_size < min_segment_size:
raise ValueError("max_segment_size must be >= min_segment_size")
num_segments_est = int(np.ceil(num_elements / min_segment_size))
if num_segments_est > np.iinfo(np.int32).max:
raise MemoryError("Too many segments for int32 offsets")
with stream:
sizes = cp.random.randint(
min_segment_size,
max_segment_size + 1,
size=num_segments_est,
dtype=cp.int64,
)
cumsum = cp.cumsum(sizes)
# Find how many full segments fit within num_elements
cutoff = int(
cp.searchsorted(
cumsum, cp.asarray(num_elements, dtype=cp.int64), side="left"
).item()
)
sizes = sizes[: cutoff + 1]
prev = 0 if cutoff == 0 else int(cumsum[cutoff - 1].item())
sizes[cutoff] = num_elements - prev
# Build cumulative offsets for the final segments
offsets = cp.empty(cutoff + 2, dtype=cp.int64)
offsets[0] = 0
offsets[1:] = cp.cumsum(sizes)
# Instead of cp.repeat (which doesn't support device repeats),
# use searchsorted to map each element index to its segment id.
indices = cp.arange(num_elements, dtype=cp.int64)
# searchsorted(offsets[1:], indices, side="right") gives the segment id
segment_ids = cp.searchsorted(offsets[1:], indices, side="right")
# Map segment ids to key values, wrapping within dtype range
if np.issubdtype(key_dtype, np.integer):
info = np.iinfo(key_dtype)
if np.dtype(key_dtype).itemsize < 8:
range_size = int(info.max) - int(info.min) + 1
keys = ((segment_ids % range_size) + int(info.min)).astype(
key_dtype, copy=False
)
else:
# For int64, avoid Python overflow: just cast directly
keys = segment_ids.astype(key_dtype, copy=False)
else:
keys = segment_ids.astype(key_dtype, copy=False)
return keys

View File

@@ -0,0 +1,27 @@
"""
CUDA Core Library (CCCL) Python Package
"""
import importlib.metadata
try:
__version__ = importlib.metadata.version("cuda-cccl")
except Exception:
__version__ = "0.0.0"
from .headers.include_paths import get_include_paths
# cuda.bindings is required, but instead of being listed as a required dependency,
# it is installed via an extra (e.g., [cu12] or [cu13]).
#
# One of the first things we should do is check that it is available, and raise
# a helpful error message if it is not.
try:
import cuda.bindings as _cuda_bindings # type: ignore
except ImportError:
raise ImportError(
"cuda.bindings is not installed. Please install the appropriate extra cuda-cccl[cu12] or cuda-cccl[cu13]."
) from None
del _cuda_bindings
__all__ = ["get_include_paths", "__version__"]

View File

@@ -0,0 +1,24 @@
# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
CUDA version detection utilities shared across the cccl package.
"""
from typing import Optional
import cuda.bindings
def detect_cuda_version() -> Optional[int]:
cuda_version = cuda.bindings.__version__
return int(cuda_version.split(".")[0])
def get_recommended_extra(cuda_version: Optional[int]) -> str:
"""Get the recommended pip extra for the detected CUDA version."""
if cuda_version == 13:
return "cu13"
else:
return "cu12"

View File

@@ -0,0 +1,7 @@
# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from .include_paths import get_include_paths
__all__ = ["__version__", "get_include_paths"]

View File

@@ -0,0 +1 @@
# Intentionally empty

View File

@@ -0,0 +1,51 @@
# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
import sys
from dataclasses import dataclass
from functools import lru_cache
from importlib.resources import as_file, files
from pathlib import Path
from typing import Optional
# type: ignore[import-not-found]
from cuda.pathfinder import find_nvidia_header_directory
@dataclass
class IncludePaths:
cuda: Optional[Path]
libcudacxx: Optional[Path]
cub: Optional[Path]
thrust: Optional[Path]
def as_tuple(self):
# Note: higher-level ... lower-level order:
return (self.thrust, self.cub, self.libcudacxx, self.cuda)
@lru_cache()
def get_include_paths(probe_file: str = "cub/version.cuh") -> IncludePaths:
cuda_incl = find_nvidia_header_directory("cudart")
if cuda_incl is None:
raise RuntimeError("Unable to locate CUDA include directory.")
with as_file(files("cuda.cccl.headers.include")) as f:
cccl_incl = Path(f)
probe_file_path = Path(probe_file)
if not (cccl_incl / probe_file_path).exists():
for sp in sys.path:
cccl_incl = Path(sp).resolve() / "cuda" / "cccl" / "headers" / "include"
if (cccl_incl / probe_file_path).exists():
break
else:
raise RuntimeError("Unable to locate CCCL include directory.")
return IncludePaths(
cuda=cuda_incl,
libcudacxx=cccl_incl,
cub=cccl_incl,
thrust=cccl_incl,
)

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2024, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License -Identifier: Apache-2.0 WITH LLVM-exception
from . import experimental
__all__ = [
"experimental",
]

View File

@@ -0,0 +1,24 @@
# Copyright (c) 2025, NVIDIA CORPORATION.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# alias for backwards compatibility
from warnings import warn
from cuda.compute import * # noqa: F403
warn(
"The module cuda.cccl.parallel.experimental is deprecated. Use cuda.compute instead.",
FutureWarning,
)

View File

@@ -0,0 +1,162 @@
# Copyright (c) 2024, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
# When built against the v2 (HostJIT) backend, the JIT loads Clang's CUDA
# headers and our cuda_minimal stubs from paths that don't exist on the
# user's machine. The wheel bundles both under cuda/cccl/headers/{clang,…};
# point hostjit at them via the env vars its detectDefaultConfig() reads.
# Only sets vars that aren't already configured by the user, and skips
# silently if the bundled directories are absent (e.g. v1 builds).
def _configure_hostjit_paths() -> None:
import os
from pathlib import Path
try:
from ._build_info import USING_V2 # type: ignore[import-not-found]
except ImportError:
return
if not USING_V2:
return
# Probe for actual file presence, not just directory existence: editable
# (`pip install -e`) installs leave behind empty placeholder dirs in the
# source tree (with just `__pycache__`), so `is_dir()` succeeds but the
# bundled headers are absent. In that case, leave the env vars unset and
# let the C library use its build-time CLANG_HEADERS_DIR / HOSTJIT_INCLUDE_DIR
# macros (pointing at the LLVM source tree under the CMake build dir).
headers_dir = Path(__file__).resolve().parent.parent / "cccl" / "headers"
clang_dir = headers_dir / "clang"
if (
clang_dir / "__clang_cuda_math_forward_declares.h"
).is_file() and not os.environ.get("HOSTJIT_CLANG_PATH"):
os.environ["HOSTJIT_CLANG_PATH"] = str(clang_dir)
if (
headers_dir / "hostjit" / "cuda_minimal" / "__clang_cuda_runtime_wrapper.h"
).is_file() and not os.environ.get("HOSTJIT_INCLUDE_PATH"):
os.environ["HOSTJIT_INCLUDE_PATH"] = str(headers_dir)
_configure_hostjit_paths()
from ._bindings import _BINDINGS_AVAILABLE # type: ignore[attr-defined] # noqa: E402
if not _BINDINGS_AVAILABLE:
__all__ = ["_BINDINGS_AVAILABLE"]
def __getattr__(name):
raise AttributeError(
f"Cannot access 'cuda.compute.{name}' because CUDA bindings are not available."
"This typically means you're running on a CPU-only machine without CUDA drivers installed."
)
else:
from ._caching import clear_all_caches
from ._proxy import ProxyArray, ProxyValue
from .algorithms import (
DoubleBuffer,
SortOrder,
binary_transform,
deserialize,
exclusive_scan,
histogram_even,
inclusive_scan,
lower_bound,
make_binary_transform,
make_exclusive_scan,
make_histogram_even,
make_inclusive_scan,
make_lower_bound,
make_merge_sort,
make_radix_sort,
make_reduce_into,
make_segmented_reduce,
make_segmented_sort,
make_select,
make_three_way_partition,
make_unary_transform,
make_unique_by_key,
make_upper_bound,
merge_sort,
radix_sort,
reduce_into,
segmented_reduce,
segmented_sort,
select,
serialize,
three_way_partition,
unary_transform,
unique_by_key,
upper_bound,
)
from .determinism import Determinism
from .iterators import (
CacheModifiedInputIterator,
ConstantIterator,
CountingIterator,
DiscardIterator,
PermutationIterator,
ReverseIterator,
ShuffleIterator,
TransformIterator,
TransformOutputIterator,
ZipIterator,
)
from .op import OpKind
from .struct import gpu_struct
__all__ = [
"_BINDINGS_AVAILABLE",
"serialize",
"deserialize",
"ProxyArray",
"ProxyValue",
"binary_transform",
"clear_all_caches",
"CacheModifiedInputIterator",
"ConstantIterator",
"CountingIterator",
"DiscardIterator",
"DoubleBuffer",
"exclusive_scan",
"gpu_struct",
"histogram_even",
"inclusive_scan",
"lower_bound",
"make_binary_transform",
"make_exclusive_scan",
"make_select",
"make_histogram_even",
"make_inclusive_scan",
"make_lower_bound",
"make_merge_sort",
"make_radix_sort",
"make_reduce_into",
"make_segmented_reduce",
"make_segmented_sort",
"make_three_way_partition",
"make_unary_transform",
"make_unique_by_key",
"make_upper_bound",
"merge_sort",
"OpKind",
"Determinism",
"PermutationIterator",
"radix_sort",
"reduce_into",
"ReverseIterator",
"ShuffleIterator",
"segmented_reduce",
"segmented_sort",
"select",
"SortOrder",
"TransformIterator",
"TransformOutputIterator",
"three_way_partition",
"unary_transform",
"unique_by_key",
"upper_bound",
"ZipIterator",
]

View File

@@ -0,0 +1,87 @@
# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
# _bindings.py is a shim module that imports symbols from a
# _bindings_impl extension module. The shim serves the following purposes:
#
# 1. Import a CUDA-specific extension. The cuda.cccl wheel ships with multiple
# extensions, one for each CUDA version. At runtime, this shim chooses the
# appropriate extension based on the detected CUDA version, and imports all
# symbols from it.
#
# 2. Preload `nvrtc` and `nvJitLink` before importing the extension.
# These shared libraries are indirect dependencies, pulled in via the direct
# dependency `cccl.c.parallel`. To ensure reliable symbol resolution at
# runtime, we explicitly load them first using `cuda.pathfinder`.
# Without this step, importing the Cython extension directly may fail or behave
# inconsistently depending on environment setup and dynamic linker behavior.
# This indirection ensures the right loading order, regardless of how
# `_bindings` is first imported across the codebase.
#
# 3. On Windows, add the directory containing cccl.c.parallel's dependent DLL
# (e.g. cuda/cccl/parallel/experimental/cu13/_bindings_impl.cp312-win_amd64.pyd)
# to the current process's DLL search path using `os.add_dll_directory`.
from __future__ import annotations
import importlib
import os
from cuda.cccl._cuda_version_utils import detect_cuda_version, get_recommended_extra
from cuda.pathfinder import ( # type: ignore[import-not-found]
load_nvidia_dynamic_lib,
)
def _load_cuda_libraries():
# Load appropriate libraries for the detected CUDA version
for libname in ("nvrtc", "nvJitLink"):
load_nvidia_dynamic_lib(libname)
_load_cuda_libraries()
# Import the appropriate bindings implementation depending on what
# CUDA version is available:
cuda_version = detect_cuda_version()
if cuda_version not in [12, 13]:
raise RuntimeError(
f"Unsupported CUDA version: {cuda_version}. Only CUDA 12 and 13 are supported."
)
# `extra_name` is one of "cu12", "cu13", etc.
extra_name = get_recommended_extra(cuda_version)
module_suffix = f".{extra_name}._bindings_impl"
module_fullname = __package__ + module_suffix
# On Windows, ensure the dependent DLLs next to the extension are discoverable.
# The extension lives at .../experimental/<extra_name>/_bindings_impl.*.pyd
# and its dependent DLLs are under .../experimental/<extra_name>/cccl/.
if os.name == "nt":
spec = importlib.util.find_spec(module_fullname)
if spec and spec.origin:
dll_dir = os.path.join(os.path.dirname(spec.origin), "cccl")
if os.path.isdir(dll_dir):
# Assign the DLL directory handle to a global such that it stays
# alive for the lifetime of this module (and thus, keeps the DLL
# directory in the search path).
try:
_cccl_dll_dir_handle = os.add_dll_directory(dll_dir) # noqa: F841
except Exception:
pass
_BINDINGS_AVAILABLE = False
try:
bindings_module = importlib.import_module(module_suffix, __package__)
# Import all symbols from the module
globals().update(bindings_module.__dict__)
_BINDINGS_AVAILABLE = True
except ImportError as e:
import warnings
warnings.warn(
f"CUDA CCCL bindings for CUDA {cuda_version} not available: {e}",
RuntimeWarning,
)

View File

@@ -0,0 +1,658 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
import ctypes
from enum import IntEnum
from typing import Any, Optional
from typing_extensions import Buffer
class OpKind(IntEnum):
_value_: int
STATELESS = ...
STATEFUL = ...
PLUS = ...
MINUS = ...
MULTIPLIES = ...
DIVIDES = ...
MODULUS = ...
EQUAL_TO = ...
NOT_EQUAL_TO = ...
GREATER = ...
LESS = ...
GREATER_EQUAL = ...
LESS_EQUAL = ...
LOGICAL_AND = ...
LOGICAL_OR = ...
LOGICAL_NOT = ...
BIT_AND = ...
BIT_OR = ...
BIT_XOR = ...
BIT_NOT = ...
IDENTITY = ...
NEGATE = ...
MINIMUM = ...
MAXIMUM = ...
class TypeEnum(IntEnum):
_value_: int
INT8 = ...
INT16 = ...
INT32 = ...
INT64 = ...
UINT8 = ...
UINT16 = ...
UINT32 = ...
UINT64 = ...
FLOAT16 = ...
FLOAT32 = ...
FLOAT64 = ...
STORAGE = ...
BOOLEAN = ...
class IteratorKind(IntEnum):
_value_: int
POINTER = ...
ITERATOR = ...
class SortOrder(IntEnum):
_value_: int
ASCENDING = ...
DESCENDING = ...
class InitKind(IntEnum):
_value_: int
NO_INIT = ...
FUTURE_VALUE_INIT = ...
VALUE_INIT = ...
class Determinism(IntEnum):
_value_: int
NOT_GUARANTEED = ...
RUN_TO_RUN = ...
GPU_TO_GPU = ...
class BinarySearchMode(IntEnum):
_value_: int
LOWER_BOUND = ...
UPPER_BOUND = ...
class Op:
def __init__(
self,
name: Optional[str] = ...,
operator_type: OpKind = ...,
ltoir=None,
state=None,
state_alignment: int = 1,
extra_ltoirs=None,
): ...
@property
def state(self) -> bytes: ...
@state.setter
def state(self, new_value: bytes) -> None: ...
@property
def name(self) -> str: ...
@property
def ltoir(self) -> bytes: ...
@property
def state_alignment(self) -> int: ...
@property
def operator_type(self) -> OpKind: ...
@property
def code(self) -> Any: ...
@property
def extra_code(self) -> list: ...
class TypeInfo:
def __init__(self, size: int, alignment: int, type_enum: TypeEnum): ...
@property
def size(self) -> int: ...
@property
def alignment(self) -> int: ...
@property
def typenum(self) -> int: ...
def as_bytes(self) -> bytes: ...
class Value:
def __init__(self, type: TypeInfo, state: memoryview): ...
@property
def type(self) -> TypeInfo: ...
@property
def state(self) -> memoryview: ...
@state.setter
def state(self, new_value: memoryview) -> None: ...
def as_bytes(self) -> bytes: ...
class Pointer:
def __init__(self, arg): ...
def make_pointer_object(ptr: int | ctypes.c_void_p, owner: Any) -> Pointer: ...
class IteratorState(Buffer):
def __init__(self, arg): ...
@property
def size(self) -> int: ...
class Iterator:
def __init__(
self,
alignment: int,
iterator_type: IteratorKind,
advance_fn: Op,
dereference_fn: Op,
value_type: TypeInfo,
state=None,
host_advance_fn=None,
):
pass
@property
def advance_op(self) -> Op: ...
@property
def dereference_op(self) -> Op: ...
@property
def dereference_or_assign_op(self) -> Op: ...
@property
def state(self): ...
@state.setter
def state(self, value) -> None: ...
@property
def type(self) -> IteratorKind: ...
@property
def alignment(self) -> int: ...
@property
def value_type(self) -> TypeInfo: ...
def as_bytes(self) -> bytes: ...
def is_kind_pointer(self) -> bool: ...
def is_kind_iterator(self) -> bool: ...
@property
def host_advance_fn(self): ...
@host_advance_fn.setter
def host_advance_fn(self, value) -> None: ...
class CommonData:
def __init__(
self,
cc_major: int,
cc_minor: int,
cub_path: str,
thrust_path: str,
libcudacxx_path: str,
ctk_path: str,
): ...
@property
def compute_capability(self) -> tuple[int, int]: ...
@property
def cub_path(self) -> str: ...
@property
def thrust_path(self) -> str: ...
@property
def libcudacxx_path(self) -> str: ...
@property
def ctk_path(self) -> str: ...
# ------------
# DeviceReduce
# ------------
class DeviceReduceBuildResult:
def __init__(
self,
d_in: Iterator,
d_out: Iterator,
binary_op: Op,
h_init: Value,
determinism: Determinism,
info: CommonData,
): ...
def compute(
self,
temp_storage_ptr: int | None,
temp_storage_nbytes: int,
d_in: Iterator,
d_out: Iterator,
num_items: int,
binary_op: Op,
h_init: Value,
stream,
) -> int: ...
def compute_nondeterministic(
self,
temp_storage_ptr: int | None,
temp_storage_nbytes: int,
d_in: Iterator,
d_out: Iterator,
num_items: int,
binary_op: Op,
h_init: Value,
stream,
) -> int: ...
def serialize(self) -> bytes: ...
@staticmethod
def deserialize(
blob: bytes, load: bool = ..., check_cc: bool = ...
) -> DeviceReduceBuildResult: ...
@staticmethod
def compile(*args) -> DeviceReduceBuildResult: ...
def load(self) -> None: ...
@property
def determinism(self) -> int: ...
# ----------
# DeviceScan
# ----------
class DeviceScanBuildResult:
def __init__(
self,
d_in: Iterator,
d_out: Iterator,
binary_op: Op,
init_type: TypeInfo,
force_inclusive: bool,
init_kind: InitKind,
info: CommonData,
): ...
def compute_inclusive(
self,
temp_storage_ptr: int | None,
temp_storage_nbytes: int,
d_in: Iterator,
d_out: Iterator,
num_items: int,
binary_op: Op,
h_init: Value,
stream,
) -> int: ...
def compute_exclusive(
self,
temp_storage_ptr: int | None,
temp_storage_nbytes: int,
d_in: Iterator,
d_out: Iterator,
num_items: int,
binary_op: Op,
h_init: Value,
stream,
) -> int: ...
def compute_inclusive_future_value(
self,
temp_storage_ptr: int | None,
temp_storage_nbytes: int,
d_in: Iterator,
d_out: Iterator,
num_items: int,
binary_op: Op,
h_init: Iterator,
stream,
) -> int: ...
def compute_exclusive_future_value(
self,
temp_storage_ptr: int | None,
temp_storage_nbytes: int,
d_in: Iterator,
d_out: Iterator,
num_items: int,
binary_op: Op,
h_init: Iterator,
stream,
) -> int: ...
def compute_inclusive_no_init(
self,
temp_storage_ptr: int | None,
temp_storage_nbytes: int,
d_in: Iterator,
d_out: Iterator,
num_items: int,
binary_op: Op,
h_init: None,
stream,
) -> int: ...
def serialize(self) -> bytes: ...
@staticmethod
def deserialize(
blob: bytes, load: bool = ..., check_cc: bool = ...
) -> DeviceScanBuildResult: ...
@staticmethod
def compile(*args) -> DeviceScanBuildResult: ...
def load(self) -> None: ...
# ---------------------
# DeviceSegmentedReduce
# ---------------------
class DeviceSegmentedReduceBuildResult:
def __init__(
self,
d_in: Iterator,
d_out: Iterator,
start_offsets: Iterator,
end_offsets: Iterator,
binary_op: Op,
h_init: Value,
info: CommonData,
): ...
def compute(
self,
temp_storage_ptr: int | None,
temp_storage_nbytes: int,
d_in: Iterator,
d_out: Iterator,
num_items: int,
start_offsets: Iterator,
end_offsets: Iterator,
binary_op: Op,
h_init: Value,
max_segment_size: int | None = None,
stream=None,
) -> int: ...
def serialize(self) -> bytes: ...
@staticmethod
def deserialize(
blob: bytes, load: bool = ..., check_cc: bool = ...
) -> DeviceSegmentedReduceBuildResult: ...
@staticmethod
def compile(*args) -> DeviceSegmentedReduceBuildResult: ...
def load(self) -> None: ...
# ---------------
# DeviceMergeSort
# ---------------
class DeviceMergeSortBuildResult:
def __init__(
self,
d_in_keys: Iterator,
d_in_items: Iterator,
d_out_keys: Iterator,
d_out_items: Iterator,
binary_op: Op,
info: CommonData,
): ...
def compute(
self,
temp_storage_ptr: int | None,
temp_storage_nbytes: int,
d_in_keys: Iterator,
d_in_items: Iterator,
d_out_keys: Iterator,
d_out_items: Iterator,
num_items: int,
binary_op: Op,
stream,
) -> int: ...
def serialize(self) -> bytes: ...
@staticmethod
def deserialize(
blob: bytes, load: bool = ..., check_cc: bool = ...
) -> DeviceMergeSortBuildResult: ...
@staticmethod
def compile(*args) -> DeviceMergeSortBuildResult: ...
def load(self) -> None: ...
# -----------------
# DeviceUniqueByKey
# -----------------
class DeviceUniqueByKeyBuildResult:
def __init__(
self,
d_keys_in: Iterator,
d_values_in: Iterator,
d_keys_out: Iterator,
d_values_out: Iterator,
d_num_selected_out: Iterator,
binary_op: Op,
info: CommonData,
): ...
def compute(
self,
temp_storage_ptr: int | None,
temp_storage_nbytes: int,
d_keys_in: Iterator,
d_values_in: Iterator,
d_keys_out: Iterator,
d_values_out: Iterator,
d_num_selected_out: Iterator,
binary_op: Op,
num_items: int,
stream,
) -> int: ...
def serialize(self) -> bytes: ...
@staticmethod
def deserialize(
blob: bytes, load: bool = ..., check_cc: bool = ...
) -> DeviceUniqueByKeyBuildResult: ...
@staticmethod
def compile(*args) -> DeviceUniqueByKeyBuildResult: ...
def load(self) -> None: ...
# -----------------
# DeviceRadixSort
# -----------------
class DeviceRadixSortBuildResult:
def __init__(self): ...
def compute(
self,
temp_storage_ptr: int | None,
temp_storage_nbytes: int,
d_keys_in: Iterator,
d_keys_out: Iterator,
d_values_in: Iterator,
d_values_out: Iterator,
decomposer_op: Op,
num_items: int,
begin_bit: int,
end_bit: int,
is_overwrite_okay: bool,
selector: int,
stream,
) -> tuple[int, int]: ...
def serialize(self) -> bytes: ...
@staticmethod
def deserialize(
blob: bytes, load: bool = ..., check_cc: bool = ...
) -> DeviceRadixSortBuildResult: ...
@staticmethod
def compile(*args) -> DeviceRadixSortBuildResult: ...
def load(self) -> None: ...
# --------------------
# DeviceUnaryTransform
# --------------------
class DeviceUnaryTransform:
def __init__(
self,
d_in: Iterator,
d_out: Iterator,
op: Op,
info: CommonData,
): ...
def compute(
self,
d_in: Iterator,
d_out: Iterator,
num_items: int,
stream,
) -> None: ...
def serialize(self) -> bytes: ...
@staticmethod
def deserialize(
blob: bytes, load: bool = ..., check_cc: bool = ...
) -> DeviceUnaryTransform: ...
@staticmethod
def compile(*args) -> DeviceUnaryTransform: ...
def load(self) -> None: ...
# ---------------------
# DeviceBinaryTransform
# ---------------------
class DeviceBinaryTransform:
def __init__(
self,
d_in1: Iterator,
d_in2: Iterator,
d_out: Iterator,
op: Op,
info: CommonData,
): ...
def compute(
self,
d_in1: Iterator,
d_in2: Iterator,
d_out: Iterator,
num_items: int,
stream,
) -> None: ...
def serialize(self) -> bytes: ...
@staticmethod
def deserialize(
blob: bytes, load: bool = ..., check_cc: bool = ...
) -> DeviceBinaryTransform: ...
@staticmethod
def compile(*args) -> DeviceBinaryTransform: ...
def load(self) -> None: ...
# ---------------
# DeviceHistogram
# ---------------
class DeviceHistogramBuildResult:
def __init__(
self,
num_channels: int,
num_active_channels: int,
d_samples: Iterator,
num_levels: int,
d_histogram: Iterator,
level_type: TypeInfo,
num_rows: int,
row_stride_samples: int,
is_evenly_segmented: bool,
info: CommonData,
): ...
def compute_even(
self,
d_samples: Iterator,
d_histogram: Iterator,
h_num_output_levels: Value,
h_lower_level: Value,
h_upper_level: Value,
num_row_pixels: int,
num_rows: int,
row_stride_samples: int,
stream,
) -> None: ...
def serialize(self) -> bytes: ...
@staticmethod
def deserialize(
blob: bytes, load: bool = ..., check_cc: bool = ...
) -> DeviceHistogramBuildResult: ...
@staticmethod
def compile(*args) -> DeviceHistogramBuildResult: ...
def load(self) -> None: ...
# -------------------
# DeviceBinarySearch
# -------------------
class DeviceBinarySearchBuildResult:
def __init__(
self,
mode: BinarySearchMode,
d_data: Iterator,
d_values: Iterator,
d_out: Iterator,
comparison_op: Op,
info: CommonData,
): ...
def compute(
self,
d_data: Iterator,
num_items: int,
d_values: Iterator,
num_values: int,
d_out: Iterator,
comparison_op: Op,
stream,
) -> None: ...
def serialize(self) -> bytes: ...
@staticmethod
def deserialize(
blob: bytes, load: bool = ..., check_cc: bool = ...
) -> DeviceBinarySearchBuildResult: ...
@staticmethod
def compile(*args) -> DeviceBinarySearchBuildResult: ...
def load(self) -> None: ...
# -----------------
# DeviceSegmentedSort
# -----------------
class DeviceSegmentedSortBuildResult:
def __init__(self): ...
def compute(
self,
temp_storage_ptr: int | None,
temp_storage_nbytes: int,
d_in_keys: Iterator,
d_out_keys: Iterator,
d_in_values: Iterator,
d_out_values: Iterator,
num_items: int,
num_segments: int,
d_begin_offsets: Iterator,
d_end_offsets: Iterator,
is_overwrite_okay: bool,
selector: int,
stream,
) -> tuple[int, int]: ...
def serialize(self) -> bytes: ...
@staticmethod
def deserialize(
blob: bytes, load: bool = ..., check_cc: bool = ...
) -> DeviceSegmentedSortBuildResult: ...
@staticmethod
def compile(*args) -> DeviceSegmentedSortBuildResult: ...
def load(self) -> None: ...
# ---------------------
# DeviceThreeWayPartition
# ---------------------
class DeviceThreeWayPartitionBuildResult:
def __init__(
self,
d_in: Iterator,
d_first_part_out: Iterator,
d_second_part_out: Iterator,
d_unselected_out: Iterator,
d_num_selected_out: Iterator,
select_first_part_op: Op,
select_second_part_op: Op,
info: CommonData,
): ...
def compute(
self,
d_in: Iterator,
d_first_part_out: Iterator,
d_second_part_out: Iterator,
d_unselected_out: Iterator,
d_num_selected_out: Iterator,
num_items: int,
stream,
) -> int: ...
def serialize(self) -> bytes: ...
@staticmethod
def deserialize(
blob: bytes, load: bool = ..., check_cc: bool = ...
) -> DeviceThreeWayPartitionBuildResult: ...
@staticmethod
def compile(*args) -> DeviceThreeWayPartitionBuildResult: ...
def load(self) -> None: ...

View File

@@ -0,0 +1,17 @@
# v1 (cccl.c.parallel, NVRTC) — binary_search build_result_t struct +
# uniform cubin-bytes helper. v1 nests a transform build_result and carries
# op-state metadata; v2 (sibling file) flattens to top-level cubin fields.
cdef extern from "cccl/c/binary_search.h":
cdef struct cccl_device_binary_search_build_result_t 'cccl_device_binary_search_build_result_t':
cccl_device_transform_build_result_t transform
size_t op_state_size
size_t op_state_alignment
cdef inline bytes _binary_search_cubin_bytes(
cccl_device_binary_search_build_result_t* b,
):
return PyBytes_FromStringAndSize(
<const char*>b.transform.payload, b.transform.payload_size
)

View File

@@ -0,0 +1,15 @@
# v2 (cccl.c.parallel.v2, HostJIT) — binary_search build_result_t struct +
# uniform cubin-bytes helper. v2 uses payload/payload_size matching v1.
cdef extern from "cccl/c/binary_search.h":
cdef struct cccl_device_binary_search_build_result_t 'cccl_device_binary_search_build_result_t':
void* payload
size_t payload_size
cdef inline bytes _binary_search_cubin_bytes(
cccl_device_binary_search_build_result_t* b,
):
return PyBytes_FromStringAndSize(
<const char*>b.payload, b.payload_size
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
# v1 (cccl.c.parallel, NVRTC) — cccl_op_code_type enum + string-to-enum helper.
# Selected at CMake configure time and configure_file'd to the build dir as
# `_bindings_op_code_type.pxi`. v1's types.h does not define CCCL_OP_LLVM_IR.
cdef extern from "cccl/c/types.h":
cdef enum cccl_op_code_type:
CCCL_OP_LTOIR
CCCL_OP_CPP_SOURCE
cdef inline cccl_op_code_type _parse_code_type(str s) noexcept:
if s == "cpp_source":
return CCCL_OP_CPP_SOURCE
return CCCL_OP_LTOIR

View File

@@ -0,0 +1,17 @@
# v2 (cccl.c.parallel.v2, HostJIT) — cccl_op_code_type enum + string-to-enum
# helper. Selected at CMake configure time and configure_file'd to the build
# dir as `_bindings_op_code_type.pxi`. v2's types.h adds CCCL_OP_LLVM_IR.
cdef extern from "cccl/c/types.h":
cdef enum cccl_op_code_type:
CCCL_OP_LTOIR
CCCL_OP_CPP_SOURCE
CCCL_OP_LLVM_IR
cdef inline cccl_op_code_type _parse_code_type(str s) noexcept:
if s == "llvm_ir":
return CCCL_OP_LLVM_IR
if s == "cpp_source":
return CCCL_OP_CPP_SOURCE
return CCCL_OP_LTOIR

View File

@@ -0,0 +1,40 @@
# v1 (cccl.c.parallel, NVRTC) — segmented_reduce extern + uniform call helper.
# Selected at CMake configure time and configure_file'd to the build dir as
# `_bindings_segmented_reduce_backend.pxi`. v1's signature takes
# `size_t max_segment_size` between `init` and `stream`.
cdef extern from "cccl/c/segmented_reduce.h":
cdef CUresult cccl_device_segmented_reduce(
cccl_device_segmented_reduce_build_result_t,
void *,
size_t *,
cccl_iterator_t,
cccl_iterator_t,
uint64_t,
cccl_iterator_t,
cccl_iterator_t,
cccl_op_t,
cccl_value_t,
size_t,
CUstream
) nogil
cdef inline CUresult _call_segmented_reduce(
cccl_device_segmented_reduce_build_result_t bld,
void* storage_ptr,
size_t* storage_sz,
cccl_iterator_t d_in,
cccl_iterator_t d_out,
uint64_t num_items,
cccl_iterator_t start_offsets,
cccl_iterator_t end_offsets,
cccl_op_t op_data,
cccl_value_t init,
size_t max_segment_size,
CUstream stream,
) nogil:
return cccl_device_segmented_reduce(
bld, storage_ptr, storage_sz, d_in, d_out, num_items,
start_offsets, end_offsets, op_data, init, max_segment_size, stream
)

View File

@@ -0,0 +1,38 @@
# v2 (cccl.c.parallel.v2, HostJIT) — segmented_reduce extern + uniform call
# helper. v2 dropped `size_t max_segment_size`; the helper accepts it for
# signature-compatibility with v1 and silently ignores it.
cdef extern from "cccl/c/segmented_reduce.h":
cdef CUresult cccl_device_segmented_reduce(
cccl_device_segmented_reduce_build_result_t,
void *,
size_t *,
cccl_iterator_t,
cccl_iterator_t,
uint64_t,
cccl_iterator_t,
cccl_iterator_t,
cccl_op_t,
cccl_value_t,
CUstream
) nogil
cdef inline CUresult _call_segmented_reduce(
cccl_device_segmented_reduce_build_result_t bld,
void* storage_ptr,
size_t* storage_sz,
cccl_iterator_t d_in,
cccl_iterator_t d_out,
uint64_t num_items,
cccl_iterator_t start_offsets,
cccl_iterator_t end_offsets,
cccl_op_t op_data,
cccl_value_t init,
size_t max_segment_size,
CUstream stream,
) nogil:
return cccl_device_segmented_reduce(
bld, storage_ptr, storage_sz, d_in, d_out, num_items,
start_offsets, end_offsets, op_data, init, stream
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
# v2 (HostJIT) backend — serialize/deserialize/compile/load not supported.
# Included at the end of _bindings_impl.pyx; provides stub functions so that
# the class methods exist but raise a clear error if called.
_NOT_SUPPORTED_MSG = (
"serialize/deserialize (and ahead-of-time compile/load) is not supported "
"with the HostJIT (v2) backend."
)
def _reduce_serialize(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _reduce_deserialize(blob, load=True, check_cc=True): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _reduce_compile(*args): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _reduce_load(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _scan_serialize(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _scan_deserialize(blob, load=True, check_cc=True): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _scan_compile(*args): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _scan_load(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _segmented_reduce_serialize(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _segmented_reduce_deserialize(blob, load=True, check_cc=True): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _segmented_reduce_compile(*args): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _segmented_reduce_load(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _merge_sort_serialize(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _merge_sort_deserialize(blob, load=True, check_cc=True): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _merge_sort_compile(*args): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _merge_sort_load(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _unique_by_key_serialize(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _unique_by_key_deserialize(blob, load=True, check_cc=True): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _unique_by_key_compile(*args): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _unique_by_key_load(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _radix_sort_serialize(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _radix_sort_deserialize(blob, load=True, check_cc=True): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _radix_sort_compile(*args): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _radix_sort_load(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _unary_transform_serialize(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _unary_transform_deserialize(blob, load=True, check_cc=True): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _unary_transform_compile(*args): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _unary_transform_load(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _binary_transform_serialize(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _binary_transform_deserialize(blob, load=True, check_cc=True): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _binary_transform_compile(*args): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _binary_transform_load(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _histogram_serialize(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _histogram_deserialize(blob, load=True, check_cc=True): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _histogram_compile(*args): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _histogram_load(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _binary_search_serialize(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _binary_search_deserialize(blob, load=True, check_cc=True): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _binary_search_compile(*args): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _binary_search_load(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _three_way_partition_serialize(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _three_way_partition_deserialize(blob, load=True, check_cc=True): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _three_way_partition_compile(*args): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _three_way_partition_load(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _segmented_sort_serialize(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _segmented_sort_deserialize(blob, load=True, check_cc=True): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _segmented_sort_compile(*args): raise NotImplementedError(_NOT_SUPPORTED_MSG)
def _segmented_sort_load(self): raise NotImplementedError(_NOT_SUPPORTED_MSG)

View File

@@ -0,0 +1,740 @@
# Copyright (c) 2024, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
import functools
import threading
import types
import weakref
from typing import Any, Callable, Hashable, NamedTuple, TypeVar
import numpy as np
from cuda.core import Device
try:
from cuda.core._utils.cuda_utils import CUDAError
except ImportError:
from cuda.core.experimental._utils.cuda_utils import CUDAError
from ._utils.protocols import get_dtype, get_shape, is_device_array
from .struct import _Struct
try:
from ._build_info import USING_V2 # type: ignore[import-not-found]
except ImportError:
USING_V2 = False
# Whether the backend can serialize build results (the v2 HostJIT backend
# cannot, today). Serialization is what lets same-cc devices share one default
# build — a non-owner device loads by cloning the shared entry through
# serialize -> deserialize -> load — so backends without it key default builds
# per device ordinal and build independently per device instead.
# TODO: delete this flag (and every branch on it) once v2 supports
# build-result serialization.
_BACKEND_SERIALIZES_BUILD_RESULTS = not USING_V2
# Registry thet maps type -> key function for extracting cache key
# from a value of that type.
_KEY_FUNCTIONS: dict[type, Callable[[Any], Hashable]] = {}
def _type_fqn(v):
# fully-qualified type name to distinguish np.ndarray from cp.ndarray from GpuStruct
return f"{type(v).__module__}.{type(v).__name__}"
def _key_for(value: Any) -> Hashable:
"""
Extract a cache key from a value using the registered KEY_FUNCTIONS.
This function checks the type of the value and delegates to the
appropriate registered keyer. Falls back to using the value
directly if no keyer is registered.
Args:
value: The value to extract a cache key from
Returns:
A hashable cache key
"""
# Handle sequences (lists, tuples) by recursively converting to tuple
if isinstance(value, (list, tuple)):
return tuple(_key_for(item) for item in value)
# Check for exact type match first
value_type = type(value)
if value_type in _KEY_FUNCTIONS:
return _KEY_FUNCTIONS[value_type](value)
# DeviceArrayLike is not a runtime-checkable protocol, so
# we cannot isinstance() with it.
if is_device_array(value):
return (_type_fqn(value), get_dtype(value))
# Check for instance match (handles inheritance)
for registered_type, keyer in _KEY_FUNCTIONS.items():
if isinstance(value, registered_type):
return keyer(value)
# Fallback: use value directly (assumes it's hashable)
return value
# The specialization part of every cache key: what _make_cache_key_from_args
# extracts from the user-facing arguments (dtypes, op identity, iterator
# kinds, ...).
_SpecializationKey = tuple[Hashable, ...]
def _make_cache_key_from_args(*args, **kwargs) -> _SpecializationKey:
"""
Create a cache key from function arguments.
Args:
*args: Positional arguments
**kwargs: Keyword arguments
Returns:
A tuple containing the extracted cache keys
"""
positional_keys = tuple(_key_for(arg) for arg in args)
# Sort kwargs by key name for consistent ordering
if kwargs:
sorted_kwargs = sorted(kwargs.items())
kwarg_keys = tuple((k, _key_for(v)) for k, v in sorted_kwargs)
return positional_keys + (kwarg_keys,)
return positional_keys
# Process-wide registry of all algorithm caches.
_process_wide_cache_registry: dict[str, object] = {}
class _ThreadLocalCaches:
"""
Container for wrapper caches owned by a single Python thread.
Each thread gets its own instance via ``threading.local()``. We use
``__weakref__`` to enable the process-wide registry of caches to hold weak
references to the thread's caches. That way, if a thread exits, its caches
will be garbage collected and removed from the registry even if the
process-wide registry still references them.
"""
__slots__ = ("wrapper_caches", "__weakref__")
def __init__(self) -> None:
# Outer key: decorated algorithm factory name, e.g.,
# make_reduce_into.__qualname__. Inner key: _WrapperCacheKey. No
# thread id is needed in either key because each thread holds a
# separate thread local object of this class.
self.wrapper_caches: dict[str, dict[_WrapperCacheKey, Any]] = {}
class _InFlightBuild:
"""
Coordination state for one shared build-result currently being built.
The first thread for a cache key runs the builder. Other threads wait on
``event`` and receive either the completed build result or the builder's
exception.
"""
def __init__(self) -> None:
self.event = threading.Event()
self.result: Any = None
self.exception: BaseException | None = None
# A compute capability packed into one int as major * 10 + minor, e.g.
# (9, 0) -> 90 and (12, 0) -> 120; cc_to_key / key_to_cc in _cccl_interop
# convert to and from the (major, minor) pair form. Purely documentary:
# mypy treats it as int.
_PackedCCKey = int
class _DeviceBuildTarget(NamedTuple):
"""
Target identity of a per-device wrapper or build entry.
Two roles. In the wrapper cache it keys every default-build wrapper:
wrappers hold device-bound state (their construction-time binding), so
each device gets its own. In the build-results cache it is used only when
the backend cannot serialize build results (the v2 HostJIT backend
today): sharing one entry across same-cc devices requires cloning it
through serialization, so such backends key default builds per device
ordinal instead.
TODO: once v2 supports build-result serialization, delete the build-cache
role (the _BACKEND_SERIALIZES_BUILD_RESULTS branch in
cache_build_results); the wrapper-cache role remains.
NamedTuples compare as plain tuples, so all target kinds must keep
structurally disjoint layouts (arity or element types) to never compare
equal to each other.
"""
device_id: int
cc: tuple[int, int]
class _DefaultBuildTarget(NamedTuple):
"""
Target identity of a default build shared across same-cc devices.
Holds the packed cc alone: the compiled payload depends only on the cc,
and _PerCCBuildResults.resolve() gives each device its own loaded state.
See _DeviceBuildTarget for the cross-kind equality constraint.
"""
cc_key: _PackedCCKey
class _AOTBuildTarget(NamedTuple):
"""
Target identity of an explicit AOT build, with no device attached.
Holds the normalized, sorted, packed compute-capability keys. See
_DeviceBuildTarget for the cross-kind equality constraint.
"""
cc_keys: tuple[_PackedCCKey, ...]
# Inner key of a thread's per-factory wrapper cache; see _ThreadLocalCaches.
# The target is None for explicit AOT builds: the decorator performs no device
# query or cc normalization there, and the raw compute_capability kwarg is
# already part of the specialization. Differently spelled ccs (80 vs (8, 0))
# therefore yield distinct wrappers, which still share one compiled build
# because cache_build_results normalizes its own key.
_WrapperCacheKey = tuple[_DeviceBuildTarget | None, _SpecializationKey]
# Composite key of the process-wide build-results cache:
# (build-result type, build target (device or AOT), specialization). The
# build-result type is the algorithm's Cython class from _bindings (e.g.
# DeviceReduceBuildResult), namespacing entries per algorithm.
# TODO: drop _DeviceBuildTarget from this union once v2 supports build-result
# serialization; it then only keys the wrapper cache.
_BuildResultsCacheKey = tuple[
type,
_DefaultBuildTarget | _DeviceBuildTarget | _AOTBuildTarget,
_SpecializationKey,
]
_thread_local = threading.local()
# Process wide registry of per-thread caches. It enables a thread to call
# clear_all_caches() to clear all caches across all threads.
_process_wide_thread_cache_registry: weakref.WeakSet[_ThreadLocalCaches] = (
weakref.WeakSet()
)
_process_wide_thread_cache_registry_lock = threading.Lock()
# _InFlightBuild entries are temporary: replaced by the completed build
# results or removed on builder failure.
_process_wide_build_results_cache: dict[
_BuildResultsCacheKey, _PerCCBuildResults | _InFlightBuild
] = {}
_CACHE_MISS = object()
_KeyT = TypeVar("_KeyT", bound=Hashable)
def _cache_single_flight(
cache: dict[_KeyT, Any], cache_key: _KeyT, builder: Callable[[], Any]
) -> Any:
"""Return a cached value, coalescing concurrent builds for the same key.
``cache`` may be any single-flight dict — currently the process-wide
build-results cache and the per-device loaded results inside each
_PerCCBuildResults. Its entries are one of two things: a completed value,
which is terminal, or a temporary _InFlightBuild while the one elected
caller runs ``builder``; other callers wait on its event and receive the
same result or exception. A failed builder's entry is removed so a later
call retries.
"""
cache_entry = cache.get(cache_key, _CACHE_MISS)
if cache_entry is _CACHE_MISS:
in_flight = _InFlightBuild()
# setdefault elects one builder without an explicit lock on cache hits.
cache_entry = cache.setdefault(cache_key, in_flight)
if cache_entry is in_flight:
try:
result = builder()
in_flight.result = result
cache[cache_key] = result
except BaseException as exc:
in_flight.exception = exc
cache.pop(cache_key, None)
raise
finally:
in_flight.event.set()
return result
if isinstance(cache_entry, _InFlightBuild):
cache_entry.event.wait()
if cache_entry.exception is not None:
raise cache_entry.exception
return cache_entry.result
return cache_entry
class _PerCCBuildResults(dict[_PackedCCKey, Any]):
"""One algorithm specialization's compiled build results, keyed by target cc.
Instances may be shared process-wide across threads: the factory build
cache hands all same-specialization wrappers one instance. The compiled
payload depends only on the cc, but a loaded build result holds
device-specific native state, so devices never share one. The first
device to execute claims and loads the canonical build result in place;
each additional device lazily loads its own clone of the compiled payload
(serialize -> deserialize -> load). This class tracks the loaded result —
canonical or clone — assigned to each device.
"""
def __init__(
self,
build_results: dict[_PackedCCKey, Any],
*,
loaded_device_id: int | None = None,
) -> None:
super().__init__(build_results)
# The single device that claimed each cc's canonical result and loads
# it in place; every other device clones instead. The atomic claim
# (resolve()'s setdefault) is what prevents double-loading the
# canonical object.
self._owner_devices: dict[_PackedCCKey, int] = {}
# The loaded result each (cc key, device ordinal) pair executes: the
# canonical result for its owner device; an independent clone — or,
# when cloning fails, an independently built result — for every other
# device. Also holds temporary _InFlightBuild entries while a first
# load is in flight.
self._loaded_results: dict[tuple[_PackedCCKey, int], Any] = {}
# Loading the canonical result mutates its native handle fields, while
# cloning serializes its payload. Serialize those source operations, but
# keep completed-result lookups lock-free.
self._source_locks = {cc: threading.Lock() for cc in self}
if loaded_device_id is not None:
# The caller already built and loaded the single entry on
# loaded_device_id: pre-record the post-conditions resolve()'s
# owner-load path would otherwise produce on first use.
if len(self) != 1:
raise ValueError("A device-bound _PerCCBuildResults must be singular")
for cc, build_result in self.items():
self._owner_devices[cc] = loaded_device_id
self._loaded_results[(cc, loaded_device_id)] = build_result
def resolve(self, cc: _PackedCCKey, device_id: int) -> Any:
"""Return the build result loaded for ``device_id`` without recompiling."""
# Completed loads are terminal — never removed or replaced — so the
# warm path is one lock-free lookup with no dict mutation and no
# closure allocation. Misses and in-flight loads (which only exist
# around the first load per device) fall through to the single-flight
# machinery below.
loaded = self._loaded_results.get((cc, device_id))
if loaded is not None and not isinstance(loaded, _InFlightBuild):
return loaded
source = self[cc]
owner_device = self._owner_devices.setdefault(cc, device_id)
def load_for_device():
if owner_device == device_id:
with self._source_locks[cc]:
source.load()
return source
with self._source_locks[cc]:
blob = source.serialize()
result = type(source).deserialize(blob, load=False, check_cc=True)
result.load()
return result
return _cache_single_flight(
self._loaded_results, (cc, device_id), load_for_device
)
def serialize_build_result(self, cc: _PackedCCKey) -> bytes:
"""Serialize a canonical result without racing its first device load."""
with self._source_locks[cc]:
return self[cc].serialize()
def _get_current_device_info() -> tuple[int, tuple[int, int]]:
device = Device()
cc_major, cc_minor = device.compute_capability
return device.device_id, (cc_major, cc_minor)
def _get_thread_caches() -> _ThreadLocalCaches:
caches = getattr(_thread_local, "caches", None)
if caches is None:
caches = _ThreadLocalCaches()
_thread_local.caches = caches
with _process_wide_thread_cache_registry_lock:
_process_wide_thread_cache_registry.add(caches)
return caches
def _clear_wrapper_caches(cache_name: str | None = None) -> None:
with _process_wide_thread_cache_registry_lock:
thread_caches = list(_process_wide_thread_cache_registry)
for caches in thread_caches:
if cache_name is None:
caches.wrapper_caches.clear()
else:
caches.wrapper_caches.pop(cache_name, None)
def cache_build_results(
build_result_type: type,
*key_args,
compute_capability,
builder: Callable[[], Any],
) -> Any:
"""
Cache the shared Cython build results for one specialization.
Current-device builds are keyed by compute capability alone and shared
across same-cc device ordinals: the compiled payload only depends on the
cc, and _PerCCBuildResults.resolve() gives each device its own loaded
state. When the backend cannot serialize build results (the v2 HostJIT
backend today), a non-owner device has no way to load the shared entry —
loading it clones through serialization — so default builds are keyed per
device ordinal instead and each device builds its own entry. Explicit AOT
builds have no current device and are keyed by their normalized target
compute capabilities. The key intentionally excludes the current Python
thread so wrappers can share compiled results.
Args:
build_result_type: Cython build-result type. This separates entries
that may otherwise have identical specialization keys.
*key_args: Positional values used to form the specialization part of
the cache key.
compute_capability: Explicit AOT target or ``None`` for the current
device.
builder: Callable that creates the _PerCCBuildResults on a cache miss.
Exactly one thread runs this callable for a given key while other
threads wait for the result.
Returns:
``(build_results, bound_result)``: the cached or newly built
_PerCCBuildResults and, for current-device builds, the loaded result
bound to the constructing device — resolved once here so ``__call__``
needs no device query. ``bound_result`` is ``None`` for explicit AOT
builds, which resolve per call.
"""
from ._cccl_interop import cc_to_key, normalize_compute_capabilities
if compute_capability is None:
# The factory decorator already queried the device on the wrapper-cache
# miss path and hands the result through thread-local state; fall back
# to a fresh query for direct construction (e.g. deserialization).
device_info = getattr(_thread_local, "factory_device_info", None)
if device_info is None:
device_info = _get_current_device_info()
device_id, cc = device_info
packed_cc = cc_to_key(cc)
# TODO: reduce to _DefaultBuildTarget(packed_cc) once v2 supports
# build-result serialization.
target_key = (
_DefaultBuildTarget(packed_cc)
if _BACKEND_SERIALIZES_BUILD_RESULTS
else _DeviceBuildTarget(device_id, cc)
)
user_cache_key = _make_cache_key_from_args(*key_args)
cache_key = (build_result_type, target_key, user_cache_key)
build_results = _cache_single_flight(
_process_wide_build_results_cache, cache_key, builder
)
return build_results, _bind_default_build(
build_results, packed_cc, device_id, builder
)
target_ccs = normalize_compute_capabilities(compute_capability)
assert target_ccs is not None
aot_target_key = _AOTBuildTarget(tuple(cc_to_key(cc) for cc in target_ccs))
user_cache_key = _make_cache_key_from_args(*key_args)
aot_cache_key = (build_result_type, aot_target_key, user_cache_key)
return (
_cache_single_flight(_process_wide_build_results_cache, aot_cache_key, builder),
None,
)
def _bind_default_build(
build_results, packed_cc: _PackedCCKey, device_id: int, builder
):
"""Resolve the loaded result the constructing device executes.
Default wrappers are bound to the device that was current at factory-call
time (their wrapper-cache key includes it), so the binding is resolved
once here and reused by every ``__call__`` with no device query. For the
device that built the shared entry this is a warm lookup; another same-cc
device loads its own clone of the compiled payload here (serialize ->
deserialize -> load, milliseconds where a build costs a second).
Cloning is only an optimization and its failures have no stable exception
type, so any resolve failure falls back to a full build for this device —
recorded in the shared per-device slot so same-device threads share it —
and genuine errors surface from the build path itself.
"""
try:
return build_results.resolve(packed_cc, device_id)
except Exception:
def build_privately():
(result,) = builder().values()
return result
return _cache_single_flight(
build_results._loaded_results, (packed_cc, device_id), build_privately
)
class _CacheWithRegisteredKeyFunctions:
"""
Decorator to cache the result of the decorated function.
The cache key is automatically computed from the decorated function's
arguments using the registered key functions.
"""
def __call__(self, func: Callable) -> Callable:
"""
Decorator to cache the result of the decorated function.
Args:
func: The function whose result is to be cached.
Notes
-----
Default builds append the current CUDA device and compute capability to
the cache key. Explicit AOT builds include their normalized target
compute capabilities without querying a device.
"""
cache_name = func.__qualname__
@functools.wraps(func)
def inner(*args, **kwargs):
user_cache_key = _make_cache_key_from_args(*args, **kwargs)
# When the caller targets explicit compute capabilities, that value
# is already part of user_cache_key (it arrives as a kwarg) and we
# must NOT query a device — the whole point is to build without a
# GPU. Otherwise, salt the key with the current device's cc so a
# build cached on one device isn't reused on another.
if kwargs.get("compute_capability") is None:
# Only device-availability failures should be reinterpreted as
# "pass compute_capability": no driver / no device raises
# CUDAError, and querying device 0 on a machine with zero
# devices raises ValueError. Anything else (a real bug) must
# propagate untouched. The original error is chained and echoed
# so a genuine driver/permission failure isn't hidden behind a
# misleading "no device" message.
try:
device_id, cc = _get_current_device_info()
except (CUDAError, ValueError) as e:
raise RuntimeError(
"make_<algo> was called without compute_capability and the "
f"current CUDA device could not be queried ({e}). Pass "
"compute_capability=<cc or list of ccs> to compile without "
"a GPU (e.g. with ProxyArray / ProxyValue)."
) from e
target = _DeviceBuildTarget(device_id, cc)
target_cc_arg = cc
else:
target = None
target_cc_arg = kwargs.get("compute_capability")
# No thread id in the key: the containing cache is threading.local,
# so each thread only ever sees its own entries.
cache_key = (target, user_cache_key)
thread_caches = _get_thread_caches()
cache = thread_caches.wrapper_caches.setdefault(cache_name, {})
if cache_key not in cache:
# Shared device code (operators, iterators) is compiled to LTO-IR
# once and linked into every per-arch build result, so it must target
# the lowest requested cc (nvJitLink requires final SM >= each
# linked input's arch). Set that target around the build.
from ._target_cc import target_cc
# Hand the device info queried above to cache_build_results
# (reached through the wrapper's __init__) so the miss path
# does not construct a second cuda.core Device. Saved/restored
# so nested factory calls fall back to their own query.
previous_device_info = getattr(
_thread_local, "factory_device_info", None
)
_thread_local.factory_device_info = target
try:
with target_cc(target_cc_arg):
result = func(*args, **kwargs)
finally:
_thread_local.factory_device_info = previous_device_info
cache[cache_key] = result
return cache[cache_key]
inner.cache_clear = lambda: _clear_wrapper_caches(cache_name) # type: ignore[attr-defined]
# Register the cache in the central registry
_process_wide_cache_registry[func.__qualname__] = inner
return inner
def register(self, type_: type, key_function: Callable[[Any], Hashable]) -> None:
"""
Register a key function for a specific type.
A key function extracts a hashable cache key from a value.
Args:
type_: The type to register
key_function: A callable that takes an instance of type_ and
returns a hashable cache key
"""
_KEY_FUNCTIONS[type_] = key_function
cache_with_registered_key_functions = _CacheWithRegisteredKeyFunctions()
def _make_hashable(value):
# duck-type check for numba.cuda.CUDADispatcher:
if hasattr(value, "py_func") and callable(value.py_func):
return CachableFunction(value.py_func)
elif is_device_array(value):
# Ops with device arrays in globals/closures will be handled
# by stateful op machinery, which enables updating the state
# (pointers). Thus, we only cache on the dtype and shape of
# the referenced array, but not its pointer.
return (get_dtype(value), get_shape(value))
elif isinstance(value, (np.number, np.bool_)):
return ("numpy.scalar", value.dtype.str, value.tobytes())
elif isinstance(value, (bool, int, float)):
# Python scalars are immutable values; key them by type and value so
# equal-valued scalars share a cache entry. Without this they fall
# through to ``id(value)`` below, and a fresh (non-interned) ``int``/
# ``float`` with the same value misses the build cache on every call.
# ``_type_fqn`` keeps ``True`` distinct from ``1``/``1.0`` (and avoids
# collisions between like-named scalar subclasses from other modules).
return ("python.scalar", _type_fqn(value), value)
elif isinstance(value, (list, tuple)):
return tuple(_make_hashable(v) for v in value)
elif isinstance(value, dict):
return tuple(
sorted((_make_hashable(k), _make_hashable(v)) for k, v in value.items())
)
else:
return id(value)
def clear_all_caches():
"""
Clear all algorithm caches.
This function clears cached algorithm wrappers and completed build results
in the current process, forcing recompilation on the next invocation.
Useful for benchmarking compilation time.
This function is not synchronized with active factory calls or algorithm
execution. Callers that use it in a multi-threaded program must externally
synchronize with all threads that may create or use cuda.compute algorithm
objects. If a build is already in progress, that build may complete after
this function returns and repopulate the completed build-result cache.
Example
-------
>>> import cuda.compute
>>> cuda.compute.clear_all_caches()
"""
_clear_wrapper_caches()
_process_wide_build_results_cache.clear()
# Auxiliary caches registered process-wide (e.g. _jit._infer_return_type)
# must be cleared too, so builds after a clear really are cold. Factory
# entries' cache_clear is idempotent with _clear_wrapper_caches above.
for cached_func in _process_wide_cache_registry.values():
cached_func.cache_clear()
class CachableFunction:
"""
A type that wraps a function and provides custom comparison
(__eq__) and hash (__hash__) implementations.
The purpose of this class is to enable caching and comparison of
functions based on their bytecode, constants, and closures, while
ignoring other attributes such as their names or docstrings.
"""
# TODO: eventually, move this class to _jit.py as it only
# has to do with caching of Python callables that will be
# JIT compiled.
def __init__(self, func):
self._func = func
closure = func.__closure__ if func.__closure__ is not None else []
contents = []
# Make closure contents hashable
for cell in closure:
contents.append(_make_hashable(cell.cell_contents))
self._identity = (
func.__name__,
func.__code__.co_code,
func.__code__.co_consts,
tuple(contents),
tuple(
# if `name` is found in __globals__, try and hash
# the referenced object. If `name` is not found in
# __globals__, (e.g., `name` is part of a dotted
# name like `np.argmax`), for caching purposes we
# use the hash of the name itself. Assumes numba
# known how to interpret the dotted name at JIT
# time.
_make_hashable(func.__globals__.get(name, name))
for name in func.__code__.co_names
),
)
def __eq__(self, other):
return self._identity == other._identity
def __hash__(self):
return hash(self._identity)
def __repr__(self):
return str(self._func)
# Register keyers for built-in types
cache_with_registered_key_functions.register(
np.ndarray, lambda arr: ("numpy.ndarray", arr.dtype)
)
cache_with_registered_key_functions.register(
types.FunctionType, lambda fn: CachableFunction(fn)
)
cache_with_registered_key_functions.register(_Struct, lambda v: (_type_fqn(v), v.dtype))
def _register_proxy_types():
# Registered lazily to avoid importing _proxy (and numpy-dtype construction)
# at module import time; the keys are dtype-only so equal-dtype proxies share
# a cache entry.
from ._proxy import ProxyArray, ProxyValue
cache_with_registered_key_functions.register(
ProxyArray, lambda v: ("ProxyArray", v.dtype)
)
cache_with_registered_key_functions.register(
ProxyValue, lambda v: ("ProxyValue", v.dtype)
)
_register_proxy_types()

View File

@@ -0,0 +1,471 @@
# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
import enum
import functools
import os
import subprocess
import tempfile
import warnings
from typing import Callable, List
try:
from cuda.core import Device as CudaDevice
except ImportError:
from cuda.core.experimental import Device as CudaDevice
import numpy as np
# TODO: adding a type-ignore here because `cuda` being a
# namespace package confuses mypy when `cuda.<something_else>`
# is installed, but not `cuda.cccl`. For namespace packages,
# it appears we need to actually install the sub-package
# in order for mypy to find its py.typed file. However, CI
# does type checking of `cuda.cccl` without actually installing
# it.
#
# We need to find a better solution for this.
from cuda.cccl import get_include_paths # type: ignore
from . import types
from ._bindings import (
CommonData,
Iterator,
IteratorKind,
IteratorState,
Op,
OpKind,
Pointer,
TypeEnum,
TypeInfo,
Value,
make_pointer_object,
)
from ._caching import _PerCCBuildResults
from ._utils.protocols import get_data_pointer, get_dtype, is_contiguous
from .iterators._base import IteratorBase
from .typing import DeviceArrayLike, GpuStruct
# Mapping from numpy dtype to TypeEnum for creating TypeInfo
_NUMPY_DTYPE_TO_ENUM = {
np.dtype("int8"): TypeEnum.INT8,
np.dtype("int16"): TypeEnum.INT16,
np.dtype("int32"): TypeEnum.INT32,
np.dtype("int64"): TypeEnum.INT64,
np.dtype("uint8"): TypeEnum.UINT8,
np.dtype("uint16"): TypeEnum.UINT16,
np.dtype("uint32"): TypeEnum.UINT32,
np.dtype("uint64"): TypeEnum.UINT64,
np.dtype("float16"): TypeEnum.FLOAT16,
np.dtype("float32"): TypeEnum.FLOAT32,
np.dtype("float64"): TypeEnum.FLOAT64,
np.dtype("bool"): TypeEnum.BOOLEAN,
}
@functools.lru_cache(maxsize=256)
def _type_info_from_dtype(dtype: np.dtype) -> TypeInfo:
"""
Create a TypeInfo from a numpy dtype.
Handles both primitive types and structured dtypes.
"""
dtype = np.dtype(dtype)
# Handle structured dtypes
if dtype.type == np.void and dtype.fields is not None:
return TypeInfo(dtype.itemsize, dtype.alignment, TypeEnum.STORAGE)
if dtype.kind == "c":
return TypeInfo(dtype.itemsize, dtype.alignment, TypeEnum.STORAGE)
# Fallback for any other type
type_enum = _NUMPY_DTYPE_TO_ENUM.get(dtype, TypeEnum.STORAGE)
return TypeInfo(dtype.itemsize, dtype.alignment, type_enum)
def _is_well_known_op(op: OpKind) -> bool:
return isinstance(op, OpKind) and op not in (OpKind.STATELESS, OpKind.STATEFUL)
def _device_array_to_cccl_iter(array: DeviceArrayLike) -> Iterator:
from ._proxy import ProxyArray
if not is_contiguous(array):
raise ValueError("Non-contiguous arrays are not supported.")
dtype = get_dtype(array)
info = _type_info_from_dtype(dtype)
state_info = _type_info_from_dtype(np.intp)
# A ProxyArray has no GPU allocation: leave the pointer NULL for build-time
# (ahead-of-time) compilation. The real pointer is bound at __call__ via
# set_cccl_iterator_state().
state = None if isinstance(array, ProxyArray) else get_data_pointer(array)
return Iterator(
state_info.alignment,
IteratorKind.POINTER,
Op(),
Op(),
info,
# Note: this is slightly slower, but supports all ndarray-like objects
# as long as they support CAI
# TODO: switch to use gpumemoryview once it's ready
state=state,
)
def _none_to_cccl_iter() -> Iterator:
# Any type could be used here, we just need to pass NULL.
info = _type_info_from_dtype(np.uint8)
return Iterator(info.alignment, IteratorKind.POINTER, Op(), Op(), info, state=None)
class _IteratorIO(enum.Enum):
INPUT = 0
OUTPUT = 1
def _to_cccl_iter(
it: DeviceArrayLike | IteratorBase | None, io_kind: _IteratorIO
) -> Iterator:
if it is None:
return _none_to_cccl_iter()
if isinstance(it, IteratorBase):
return it.to_cccl_iter(io_kind == _IteratorIO.OUTPUT)
return _device_array_to_cccl_iter(it)
def to_cccl_input_iter(array_or_iterator) -> Iterator:
return _to_cccl_iter(array_or_iterator, _IteratorIO.INPUT)
def to_cccl_output_iter(array_or_iterator) -> Iterator:
return _to_cccl_iter(array_or_iterator, _IteratorIO.OUTPUT)
def to_cccl_value_state(array_or_struct: np.ndarray | GpuStruct) -> memoryview:
from ._proxy import _PROXY_VALUE_DATA_ERROR, ProxyValue
if isinstance(array_or_struct, ProxyValue):
# Reached only if a proxy leaks into an execute call — proxies describe
# types for build, they carry no data to run with.
raise RuntimeError(_PROXY_VALUE_DATA_ERROR)
if isinstance(array_or_struct, np.ndarray):
assert array_or_struct.flags.contiguous
data = array_or_struct.data.cast("B")
return data
else:
# it's a GpuStruct, use the array underlying it
return to_cccl_value_state(array_or_struct._data)
def to_cccl_value(array_or_struct: np.ndarray | GpuStruct) -> Value:
from ._proxy import ProxyValue
if isinstance(array_or_struct, ProxyValue):
# Build-time placeholder: describe the type with a correctly sized zero
# buffer. The real value bytes are bound at __call__ via
# set_cccl_value_state().
info = _type_info_from_dtype(array_or_struct.dtype)
zero_bytes = memoryview(bytearray(array_or_struct.dtype.itemsize))
return Value(info, zero_bytes)
if isinstance(array_or_struct, np.ndarray):
info = _type_info_from_dtype(array_or_struct.dtype)
return Value(info, array_or_struct.data.cast("B"))
else:
# it's a GpuStruct, use the array underlying it
return to_cccl_value(array_or_struct._data)
def set_cccl_value_state(cccl_value: Value, array_or_struct: np.ndarray | GpuStruct):
"""
Set the state of a CCCL Value object from a numpy array or GpuStruct.
Args:
cccl_value: The CCCL Value binding object
array_or_struct: The numpy array or GpuStruct to get the state from
"""
cccl_value.state = to_cccl_value_state(array_or_struct)
def get_value_type(
d_in: DeviceArrayLike | IteratorBase | GpuStruct | np.ndarray,
):
from ._proxy import ProxyValue
from .struct import _Struct
if isinstance(d_in, IteratorBase):
return d_in.value_type
if isinstance(d_in, ProxyValue):
return types.from_numpy_dtype(d_in.dtype)
if isinstance(d_in, _Struct):
return type(d_in)._type_descriptor # type: ignore[union-attr]
dtype = get_dtype(d_in)
if dtype.type == np.void:
return types.from_numpy_dtype(dtype)
return types.from_numpy_dtype(dtype)
def set_cccl_iterator_state(cccl_it: Iterator, input_it):
if cccl_it.is_kind_pointer():
ptr = get_data_pointer(input_it)
ptr_obj = make_pointer_object(ptr, input_it)
cccl_it.state = ptr_obj
else:
state_ = input_it.state
if isinstance(state_, (IteratorState, Pointer)):
cccl_it.state = state_
else:
cccl_it.state = make_pointer_object(state_, input_it)
@functools.lru_cache()
def get_includes() -> List[str]:
def as_option(p):
if p is None:
return ""
return f"-I{p}"
paths = get_include_paths().as_tuple()
opts = [as_option(path) for path in paths]
return opts
def _check_compile_result(cubin: bytes):
# check compiled code for LDL/STL instructions
temp_cubin_file = tempfile.NamedTemporaryFile(delete=False)
try:
temp_cubin_file.write(cubin)
out = subprocess.run(
["nvdisasm", "-gi", temp_cubin_file.name], capture_output=True
)
if out.returncode != 0:
raise RuntimeError("nvdisasm failed")
sass = out.stdout.decode("utf-8")
except FileNotFoundError:
sass = "nvdiasm not found, skipping SASS validation"
warnings.warn(sass)
assert "LDL" not in sass, "LDL instruction found in SASS"
assert "STL" not in sass, "STL instruction found in SASS"
return temp_cubin_file.name
# this global variable controls whether the compile result is checked
# for LDL/STL instructions. Should be set to `True` for testing only.
_check_sass: bool = False
def _common_data_for_cc(cc):
"""Build a ``CommonData`` for a given compute capability.
``cc`` is a ``(major, minor)`` pair. When ``None``, the current device's
compute capability is queried (requires a live GPU).
"""
if cc is None:
cc_major, cc_minor = CudaDevice().compute_capability
else:
cc_major, cc_minor = cc
cub_path, thrust_path, libcudacxx_path, cuda_include_path = get_includes()
return CommonData(
cc_major, cc_minor, cub_path, thrust_path, libcudacxx_path, cuda_include_path
)
def call_build(build_impl_fn: Callable, *args, cc=None, **kwargs):
"""Build (compile + load) via ``build_impl_fn``, supplying compute capability and paths.
``cc`` is an optional ``(major, minor)`` pair; when ``None`` the current
device's compute capability is used (the default, load-bearing behavior).
Returns the loaded build result.
"""
global _check_sass
common_data = _common_data_for_cc(cc)
result = build_impl_fn(
*args,
common_data,
**kwargs,
)
if _check_sass:
cubin = result._get_cubin()
temp_cubin_file_name = _check_compile_result(cubin)
os.unlink(temp_cubin_file_name)
return result
def call_compile(build_impl_cls: Callable, *args, cc, **kwargs):
"""Compile only (no load) for an explicit compute capability ``cc``.
``build_impl_cls`` is a ``Device<Algo>BuildResult`` type exposing a
``compile(...)`` staticmethod. Unlike :func:`call_build`, this never touches
the CUDA driver — it can run on a machine with no GPU. The returned build
result is *not* loaded; call ``.load()`` (once, on a matching device) before
executing. ``cc`` is a ``(major, minor)`` pair and is required.
"""
common_data = _common_data_for_cc(cc)
# build_impl_cls is a Device<Algo>BuildResult class exposing a compile()
# staticmethod; it's typed Callable here, so silence the attr check.
return build_impl_cls.compile(*args, common_data, **kwargs) # type: ignore[attr-defined]
def build_for_ccs(build_impl_cls: Callable, *args, compute_capability=None, **kwargs):
"""Build the ``{cc_key: build_result}`` map for an algorithm.
With ``compute_capability=None`` (the default), this performs a fused
build+load for the current device and returns a single-entry map whose
result is already loaded. Otherwise it compiles (without loading) for each
requested compute capability and returns ``{cc_key: build_result}``, with
each result loaded lazily on first use by ``resolve_build_result``.
"""
ccs = normalize_compute_capabilities(compute_capability)
if ccs is None:
# Fused build+load for the current device. Query its cc once (clear error
# if no device) and pass it through, so call_build doesn't re-query.
device_id, cc_key = current_device_info()
build_result = call_build(build_impl_cls, *args, cc=key_to_cc(cc_key), **kwargs)
# The fused build already loaded the kernels; mark it so the lazy
# load() in resolve_build_result() is a no-op (a second C load would leak /
# re-register the library).
build_result._loaded = True
return _PerCCBuildResults({cc_key: build_result}, loaded_device_id=device_id)
return _PerCCBuildResults(
{
cc_to_key(cc): call_compile(build_impl_cls, *args, cc=cc, **kwargs)
for cc in ccs
}
)
def cc_to_key(cc) -> int:
"""Normalize a compute capability to the integer key ``major * 10 + minor``.
Accepts an int (``90``, ``75``), a ``(major, minor)`` pair, or a string
like ``"90"`` / ``"9.0"``.
"""
if isinstance(cc, (tuple, list)):
major, minor = cc
return int(major) * 10 + int(minor)
if isinstance(cc, str):
cc = cc.replace(".", "")
return int(cc)
return int(cc)
def key_to_cc(key: int):
"""Inverse of :func:`cc_to_key`: integer key -> ``(major, minor)`` pair."""
return (key // 10, key % 10)
def normalize_compute_capabilities(compute_capability):
"""Normalize the ``compute_capability=`` argument of ``make_<algo>``.
Returns a sorted list of unique ``(major, minor)`` pairs, or ``None`` to
mean "use the current device" (the default build path). Accepts a single
cc (int / pair / str) or a list thereof.
"""
if compute_capability is None:
return None
if isinstance(compute_capability, (int, str)):
ccs = [compute_capability]
elif (
isinstance(compute_capability, tuple)
and len(compute_capability) == 2
and all(isinstance(x, int) for x in compute_capability)
):
# a single (major, minor) pair
ccs = [compute_capability]
else:
ccs = list(compute_capability)
keys = sorted({cc_to_key(cc) for cc in ccs})
if not keys:
raise ValueError("compute_capability list is empty")
return [key_to_cc(k) for k in keys]
def current_device_info() -> tuple[int, int]:
"""Return the current device ordinal and packed compute-capability key.
Raises a clear, actionable error if no CUDA device is available: building
without a GPU has no device to infer the target arch from, so the caller
must pass an explicit ``compute_capability=``.
"""
try:
device = CudaDevice()
cc = device.compute_capability
except Exception as e:
raise RuntimeError(
"No compute_capability was given and no CUDA device is available to target."
) from e
return device.device_id, cc_to_key(tuple(cc))
def current_device_cc_key() -> int:
"""The current device's compute capability as a ``major * 10 + minor`` key."""
return current_device_info()[1]
def current_device_id() -> int:
"""The current CUDA device ordinal, without a compute-capability query.
The compute-capability query roughly doubles the cost of
``current_device_info()``, and callers that only key per-device state
(see ``resolve_build_result``) run on every algorithm invocation.
"""
try:
return CudaDevice().device_id
except Exception as e:
raise RuntimeError("No CUDA device is available to execute on.") from e
def resolve_build_result(build_results: dict, bound_result=None):
"""Load the build result for the current device.
``bound_result`` is a default-build wrapper's construction-time binding
(see cache_build_results): already the loaded result for the wrapper's
device, returned without any device query. Deserialized wrappers have no
binding and resolve per call.
"""
if bound_result is not None:
return bound_result
# Wrappers always hold a _PerCCBuildResults (build_for_ccs and
# deserialization both produce one); the per-device ownership/clone
# protocol in resolve() relies on it, so fail loudly on anything else
# rather than fall back to an unprotected load.
assert isinstance(build_results, _PerCCBuildResults)
if len(build_results) == 1:
# A singular _PerCCBuildResults is used as-is whatever the current device's
# compute capability is (single-target blobs were already cc-checked at
# deserialization), so only the device ordinal is needed to key the
# per-device loaded state. This path runs on every call of AOT and
# deserialized wrappers; skip the costlier compute-capability query.
(build_result_cc,) = build_results
device_id = current_device_id()
else:
device_id, device_cc_key = current_device_info()
build_result_cc = device_cc_key
if build_result_cc not in build_results:
available = ", ".join(
f"{maj}.{minor}"
for maj, minor in (key_to_cc(k) for k in sorted(build_results))
)
major, minor = key_to_cc(build_result_cc)
raise RuntimeError(
f"This algorithm was compiled for compute capabilities [{available}], "
f"but the current device has compute capability {major}.{minor}. "
f"Rebuild with compute_capability including {major}{minor}."
)
return build_results.resolve(build_result_cc, device_id)

View File

@@ -0,0 +1,172 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
C++ code generation and compilation infrastructure.
"""
from __future__ import annotations
import functools
from cuda.cccl import get_include_paths
from cuda.core import Device, Program, ProgramOptions
from ._bindings import TypeEnum
from ._device_code import DeviceCode
try:
from ._build_info import USING_V2 # type: ignore[import-not-found]
except ImportError:
USING_V2 = False
def _get_arch_string() -> str:
"""Target arch string for iterator LTO-IR compilation.
Honors the build's target compute capability (set for multi-arch / no-GPU
builds) so iterator device code is compiled for the lowest target arch and
links into every build result; falls back to the current device otherwise.
"""
from ._target_cc import get_target_cc
cc = get_target_cc()
if cc is None:
cc = Device().compute_capability
cc_major, cc_minor = cc
return f"sm_{cc_major}{cc_minor}"
@functools.lru_cache(maxsize=1)
def _get_include_paths() -> list[str]:
"""Get include paths for CCCL headers."""
paths = get_include_paths().as_tuple()
return [p for p in paths if p is not None]
def compile_cpp_to_ltoir(
source: str,
arch: str | None = None,
) -> bytes:
"""
Compile C++ source code to LTOIR.
Args:
source: C++ source code string
arch: Target architecture (e.g., "sm_80"). If None, uses current device.
Returns:
LTOIR bytes
Example:
source = '''
extern "C" __device__ void my_add(void* a, void* b, void* result) {
*static_cast<int*>(result) = *static_cast<int*>(a) + *static_cast<int*>(b);
}
'''
ltoir = compile_cpp_to_ltoir(source)
"""
# Resolve the concrete arch before the cache lookup so the key reflects the
# compute capability compiled for. If arch stays None (the usual iterator/op
# call, resolved from target_cc), every target collapses to one key and
# LTO-IR built for one arch can be reused for another, which nvJitLink
# rejects.
if arch is None:
arch = _get_arch_string()
return _compile_cpp_to_ltoir_cached(source, arch)
@functools.lru_cache(maxsize=256)
def _compile_cpp_to_ltoir_cached(source: str, arch: str) -> bytes:
# Get include paths
include_paths = _get_include_paths()
# Configure compilation options for LTO
opts = ProgramOptions(
arch=arch,
relocatable_device_code=True,
link_time_optimization=True,
std="c++20",
define_macro="__NV_NO_VECTOR_DEPRECATION_DIAG",
include_path=include_paths,
)
# Compile to LTOIR
program = Program(source, "c++", options=opts)
result = program.compile("ltoir")
return result.code
# Expose the cached-callable surface (cache_info/cache_clear) on the public
# entry point, backed by the arch-aware inner cache.
compile_cpp_to_ltoir.cache_clear = _compile_cpp_to_ltoir_cached.cache_clear # type: ignore[attr-defined]
compile_cpp_to_ltoir.cache_info = _compile_cpp_to_ltoir_cached.cache_info # type: ignore[attr-defined]
def compile_cpp_op_code(source: str, arch: str | None = None) -> DeviceCode:
"""Compile C++ wrapper source to whatever form the active backend prefers.
Returns a :class:`DeviceCode` wrapping the bytes and the matching format tag.
Cached so identical iterator structures produce identical code bytes —
callers can inspect ``cache_info()`` to verify symbol determinism.
"""
# v2 keeps the C++ source verbatim (arch-independent); v1 resolves the
# concrete arch before caching (see compile_cpp_to_ltoir).
if USING_V2:
return _compile_cpp_op_code_cached(source, None)
if arch is None:
arch = _get_arch_string()
return _compile_cpp_op_code_cached(source, arch)
@functools.lru_cache(maxsize=256)
def _compile_cpp_op_code_cached(source: str, arch: str | None) -> DeviceCode:
if USING_V2:
return DeviceCode(op_bytes=source.encode("utf-8"), kind="cpp_source")
return DeviceCode(op_bytes=compile_cpp_to_ltoir(source, arch=arch), kind="ltoir")
compile_cpp_op_code.cache_clear = _compile_cpp_op_code_cached.cache_clear # type: ignore[attr-defined]
compile_cpp_op_code.cache_info = _compile_cpp_op_code_cached.cache_info # type: ignore[attr-defined]
def cpp_type_from_descriptor(type_desc) -> str | None:
"""
Get the C++ type name from a TypeDescriptor.
Important: for efficiency, this function returns None
for non-primitive types. Callers must take care
to handle that case appropriately.
"""
# Map TypeEnum to C++ types
type_map = {
TypeEnum.INT8: "int8_t",
TypeEnum.INT16: "int16_t",
TypeEnum.INT32: "int32_t",
TypeEnum.INT64: "int64_t",
TypeEnum.UINT8: "uint8_t",
TypeEnum.UINT16: "uint16_t",
TypeEnum.UINT32: "uint32_t",
TypeEnum.UINT64: "uint64_t",
TypeEnum.FLOAT16: "__half",
TypeEnum.FLOAT32: "float",
TypeEnum.FLOAT64: "double",
TypeEnum.BOOLEAN: "bool",
TypeEnum.STORAGE: None,
}
return type_map[type_desc.info.typenum]
def make_variable_declaration(type_desc, name: str) -> str:
"""
Generate a C++ variable declaration, like "int32_t temp;"
or "alignas(8) char temp[16];"
"""
cpp_type = cpp_type_from_descriptor(type_desc)
if cpp_type is not None:
return f"{cpp_type} {name};"
# STORAGE type - use aligned char array
return f"alignas({type_desc.alignment}) char {name}[{type_desc.size}];"

View File

@@ -0,0 +1,45 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Public type for passing compiled device-side operator code into ``Op`` /
``RawOp``. Wraps the bytes together with their format tag so the two cannot
get out of sync as they flow through the binding layer.
Lives in its own module to keep ``op.py`` and the Cython ``_bindings_impl``
free of import-cycle headaches: the Cython side duck-types on
``(op_bytes, kind)`` attributes and never imports the class directly.
"""
from __future__ import annotations
from dataclasses import dataclass
# Tag values mirror the C-side ``cccl_op_code_type`` enum.
_VALID_KINDS = ("ltoir", "llvm_ir", "cpp_source")
@dataclass(frozen=True)
class DeviceCode:
"""A compiled-or-source device-code blob ready to hand to ``Op``.
Args:
op_bytes: the raw blob (LTO-IR, LLVM bitcode, or C++ source bytes).
kind: one of ``"ltoir"`` (default), ``"llvm_ir"``, ``"cpp_source"``;
tells the backend how to interpret ``op_bytes``.
For most uses you don't construct ``DeviceCode`` directly — the internal
JIT-compile helpers return one, and the iterator/algorithm machinery
forwards them. Construct explicitly when feeding a ``RawOp`` from outside
the default pipeline.
"""
op_bytes: bytes
kind: str = "ltoir"
def __post_init__(self):
if self.kind not in _VALID_KINDS:
raise ValueError(
f"DeviceCode.kind must be one of {_VALID_KINDS!r}; got {self.kind!r}"
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,370 @@
# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
ODR (One Definition Rule) Helpers for CCCL Python Interop.
This module provides utilities to create wrapper functions for
device functions that are defined in Python and JIT compiled by Numba.
On the C++ side, these functions are declared as `extern "C"` functions with
void* parameters - the arguments types can not be known at C++ compile time.
Thus, the helpers in this module generate wrapper device functions that accept
void* arguments (matching C++ declarations), cast them to the correct
typed arguments, load/store values as needed, and call the original
function with properly typed arguments.
Example flow:
User provides: def add(x: int32, y: int32) -> int32
Wrapper signature: void(void*, void*, void*) # x_ptr, y_ptr, result_ptr
C++ sees: extern "C" void wrapped_add(void*, void*, void*);
"""
from __future__ import annotations
import enum
import itertools
import textwrap
import threading
from typing import TYPE_CHECKING
from numba import types
from numba.core.extending import intrinsic
from ._utils import sanitize_identifier
if TYPE_CHECKING:
from numba.core.typing import Signature
# Global counter to generate unique symbol names even when the same function
# is used multiple times (e.g., as both selectors in `three_way_partition`).
_wrapper_name_counter = itertools.count()
_wrapper_name_lock = threading.Lock()
__all__ = [
"create_op_void_ptr_wrapper",
"create_stateful_op_void_ptr_wrapper",
"create_advance_void_ptr_wrapper",
"create_input_dereference_void_ptr_wrapper",
"create_output_dereference_void_ptr_wrapper",
]
class _ArgMode(enum.Enum):
"""How a void* argument should be handled in wrapper codegen."""
LOAD = "load" # Cast to typed pointer, load value
PTR = "ptr" # Cast to typed pointer, pass pointer directly
STORE = "store" # Cast to typed pointer, store return value here
# Unpack packed data pointers into array structs
STATE = "state"
class _ArgSpec:
"""Specification for a wrapper argument."""
__slots__ = ("numba_type", "mode")
def __init__(self, numba_type, mode: _ArgMode):
self.numba_type = numba_type
self.mode = mode
def _build_numba_array_struct(context, builder, array_type, data_ptr, info):
"""Build a numba array struct from a data pointer and array info.
Args:
context: Numba codegen context
builder: LLVM IR builder
array_type: Numba Array type for the array
data_ptr: LLVM value for the data pointer
info: Dict with 'shape', 'itemsize', 'strides' for the array
Returns:
LLVM value representing the array struct
"""
import llvmlite.ir as ir
from numba.cuda.np.arrayobj import make_array, populate_array
out_ary = make_array(array_type)(context, builder)
populate_array(
out_ary,
data=data_ptr,
shape=[ir.Constant(ir.IntType(64), info["shape"])],
strides=[ir.Constant(ir.IntType(64), info["strides"])],
itemsize=info["itemsize"],
meminfo=None,
)
return out_ary._getvalue()
def _unpack_state_arrays(context, builder, packed_ptr, type_info_pairs):
"""Unpack packed data pointers into numba array structs.
Args:
context: Numba codegen context
builder: LLVM IR builder
packed_ptr: void* pointing to an array of data pointers
type_info_pairs: List of (array_type, info) tuples
Returns:
List of LLVM values representing the unpacked array structs
"""
import llvmlite.ir as ir
# Cast void* to pointer-to-pointer (array of pointers)
ptr_type = ir.IntType(64).as_pointer()
base_ptr = builder.bitcast(packed_ptr, ptr_type.as_pointer())
result = []
for j, (array_type, info) in enumerate(type_info_pairs):
# Load j-th pointer from the array and cast to correct type
elem_ptr = builder.gep(base_ptr, [ir.Constant(ir.IntType(32), j)])
dtype_llvm = context.get_value_type(array_type.dtype)
typed_ptr_ptr = builder.bitcast(elem_ptr, dtype_llvm.as_pointer().as_pointer())
data_ptr = builder.load(typed_ptr_ptr)
# Build array struct from pointer
array_val = _build_numba_array_struct(
context, builder, array_type, data_ptr, info
)
result.append(array_val)
return result
def _codegen_void_ptr_wrapper(
context, builder, args, arg_specs, func_device, inner_sig
):
"""Generate LLVM IR for a void* wrapper function.
This is the codegen implementation shared by all void* wrappers.
It processes each argument according to its _ArgSpec mode, calls
the inner function, and stores the result if needed.
Args:
context: Numba codegen context
builder: LLVM IR builder
args: LLVM values for the void* arguments
arg_specs: List of _ArgSpec describing each argument
func_device: The device function to call
inner_sig: Numba signature for the inner function
Returns:
LLVM dummy value (for void return)
"""
input_vals = []
state_array_vals = []
ret_ptr = None
for i, (arg, spec) in enumerate(zip(args, arg_specs)):
match spec.mode:
case _ArgMode.LOAD:
# Cast void* to typed pointer and load value
llvm_type = context.get_value_type(spec.numba_type)
typed_ptr = builder.bitcast(arg, llvm_type.as_pointer())
val = builder.load(typed_ptr)
input_vals.append(val)
case _ArgMode.PTR:
# Cast void* to typed pointer, pass pointer directly
llvm_type = context.get_value_type(spec.numba_type.dtype)
typed_ptr = builder.bitcast(arg, llvm_type.as_pointer())
input_vals.append(typed_ptr)
case _ArgMode.STORE:
# Cast void* to typed pointer for storing result
llvm_type = context.get_value_type(spec.numba_type)
ret_ptr = builder.bitcast(arg, llvm_type.as_pointer())
case _ArgMode.STATE:
# Cast void* to a packed array of pointers and unpack them
array_vals = _unpack_state_arrays(
context, builder, arg, spec.numba_type
)
state_array_vals.extend(array_vals)
case _:
raise ValueError(f"Invalid arg mode: {spec.mode}")
# Prepend state arrays at the beginning (inner_sig expects state args first)
input_vals = state_array_vals + input_vals
# Call the inner function
cres = context.compile_subroutine(builder, func_device, inner_sig, caching=False)
result = context.call_internal(builder, cres.fndesc, inner_sig, input_vals)
# Store result if needed
if ret_ptr is not None:
builder.store(result, ret_ptr)
return context.get_dummy_value()
def _create_void_ptr_wrapper(
func, name: str, arg_specs: list[_ArgSpec], inner_sig: "Signature"
):
"""
Given a function and a list of _ArgSpec, create a wrapper function
that takes all void* arguments, bitcasts them to the
appropriate typed pointers, and calls the inner function with
the typed arguments. Each void* argument is handled according
to its _ArgSpec.
Args:
func: The function to wrap (will be compiled as device function)
name: Base name for the wrapper function
arg_specs: List of _ArgSpec describing each void* argument
inner_sig: Numba signature for the inner function call
Returns:
Tuple of (wrapper_func, wrapper_sig)
"""
from numba.cuda import jit as cuda_jit
# Wrap function as device function
func_device = cuda_jit(device=True)(func)
# Generate argument names and signature
arg_names = [f"arg_{i}" for i in range(len(arg_specs))]
arg_str = ", ".join(arg_names)
void_sig = types.void(*(types.voidptr for _ in arg_specs))
# Create unique wrapper name using global counter
sanitized_name = sanitize_identifier(name)
if not sanitized_name.isidentifier():
raise ValueError(
f"Function name '{name}' cannot be sanitized into a valid identifier"
)
for arg_name in arg_names:
if not arg_name.isidentifier():
raise ValueError(
f"Invalid argument name '{arg_name}' - must be a valid identifier"
)
with _wrapper_name_lock:
unique_suffix = next(_wrapper_name_counter)
wrapper_name = f"wrapped_{sanitized_name}_{unique_suffix}"
# We need exec() here because Numba's @intrinsic decorator requires:
# 1. A function with a specific signature visible at parse time
# 2. The number of arguments must match the wrapper signature
# The actual codegen logic is in _codegen_void_ptr_wrapper - this just
# creates the minimal intrinsic shell that delegates to it.
wrapper_src = textwrap.dedent(f"""
@intrinsic
def impl(typingctx, {arg_str}):
def codegen(context, builder, impl_sig, args):
return codegen_helper(context, builder, args, arg_specs, func_device, inner_sig)
return void_sig, codegen
def {wrapper_name}({arg_str}):
return impl({arg_str})
""")
local_dict = {
"intrinsic": intrinsic,
"void_sig": void_sig,
"arg_specs": arg_specs,
"func_device": func_device,
"inner_sig": inner_sig,
"codegen_helper": _codegen_void_ptr_wrapper,
}
exec(wrapper_src, {}, local_dict)
wrapper_func = local_dict[wrapper_name]
wrapper_func.__globals__.update(local_dict)
return wrapper_func, void_sig
def create_op_void_ptr_wrapper(op, sig: "Signature"):
"""Creates a wrapper function for user-defined operators like unary or binary operators.
The wrapper takes N+1 arguments where N is the number of input arguments to `op`, the last
argument is a pointer to the result.
"""
arg_specs = [_ArgSpec(t, _ArgMode.LOAD) for t in sig.args]
arg_specs.append(_ArgSpec(sig.return_type, _ArgMode.STORE))
return _create_void_ptr_wrapper(op, op.__name__, arg_specs, sig)
def create_stateful_op_void_ptr_wrapper(
op, sig: "Signature", state_array_types, state_info
):
"""Creates a wrapper function for a stateful operator with void* arguments.
The wrapper takes N+2 void* arguments:
- states_ptr: pointer to packed array of data pointers for state arrays
- N input args: one for each regular input argument
- result: pointer where result is stored
Args:
op: The user's callable operator
sig: The signature of the operator (state_array1, state_array2, ..., regular_arg1, regular_arg2, ...) -> return_type
state_array_types: List/tuple of numba Array types for the state parameters
state_info: List/tuple of dicts with 'shape', 'itemsize', 'strides' for each state array
Returns:
Tuple of (wrapper_func, wrapper_sig)
"""
num_states = len(state_array_types)
# Build arg_specs: states_ptr + regular inputs + result
# The packed state arrays spec goes first, then regular LOAD args, then STORE for result
# numba_type is a list of (array_type, info) tuples
type_info_pairs = list(zip(state_array_types, state_info))
arg_specs = [_ArgSpec(type_info_pairs, _ArgMode.STATE)]
for i in range(num_states, len(sig.args)):
arg_specs.append(_ArgSpec(sig.args[i], _ArgMode.LOAD))
arg_specs.append(_ArgSpec(sig.return_type, _ArgMode.STORE))
return _create_void_ptr_wrapper(op, op.__name__, arg_specs, sig)
def create_advance_void_ptr_wrapper(advance_fn, state_ptr_type):
"""Creates a wrapper function for iterator advance method.
The wrapper takes 2 void* arguments:
- state pointer
- offset pointer (points to uint64 value)
"""
arg_specs = [
_ArgSpec(state_ptr_type, _ArgMode.PTR),
_ArgSpec(types.uint64, _ArgMode.LOAD), # uint64 is the offset type
]
inner_sig = types.void(state_ptr_type, types.uint64)
return _create_void_ptr_wrapper(
advance_fn, advance_fn.__name__, arg_specs, inner_sig
)
def create_input_dereference_void_ptr_wrapper(deref_fn, state_ptr_type, value_type):
"""Creates a wrapper function for input iterator dereference method.
The wrapper takes 2 void* arguments:
- state pointer
- result pointer (function writes result here)
"""
arg_specs = [
_ArgSpec(state_ptr_type, _ArgMode.PTR),
_ArgSpec(types.CPointer(value_type), _ArgMode.PTR),
]
inner_sig = types.void(state_ptr_type, types.CPointer(value_type))
return _create_void_ptr_wrapper(deref_fn, deref_fn.__name__, arg_specs, inner_sig)
def create_output_dereference_void_ptr_wrapper(deref_fn, state_ptr_type, value_type):
"""Creates a wrapper function for output iterator dereference method.
The wrapper takes 2 void* arguments:
- state pointer
- value pointer (value to write)
"""
arg_specs = [
_ArgSpec(state_ptr_type, _ArgMode.PTR),
_ArgSpec(value_type, _ArgMode.LOAD),
]
inner_sig = types.void(state_ptr_type, value_type)
return _create_void_ptr_wrapper(deref_fn, deref_fn.__name__, arg_specs, inner_sig)

View File

@@ -0,0 +1,132 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""Device-less placeholders for ahead-of-time (no-GPU) compilation.
``ProxyArray`` / ``ProxyValue`` describe *only* the dtype (and, for arrays,
shape/contiguity) of an argument, with no backing GPU allocation. Pass them to
``make_<algo>(...)`` together with ``compute_capability=`` to compile an
algorithm on a machine that has no GPU (or no live data), then ``serialize()``
the result. The real device arrays / scalars are supplied later at ``__call__``.
Accessing a proxy's data pointer raises ``RuntimeError`` — a proxy can be used
to *build* an algorithm but never to *run* one.
"""
from __future__ import annotations
import numpy as np
_PROXY_DATA_ERROR = (
"ProxyArray has no GPU data — it is a build-time placeholder only. "
"Pass a real device array when calling the compiled algorithm."
)
_PROXY_VALUE_DATA_ERROR = (
"ProxyValue has no data — it is a build-time placeholder only. "
"Pass a real scalar or numpy array when calling the compiled algorithm."
)
class _ProxyCAI(dict):
"""CAI dict whose 'data' key raises on access."""
def __missing__(self, key):
if key == "data":
raise RuntimeError(_PROXY_DATA_ERROR)
raise KeyError(key)
def get(self, key, default=None):
# dict.get() bypasses __missing__, so guard it too: a consumer that
# defensively does cai.get("data") must still hit the loud failure
# rather than silently receiving a null pointer.
if key == "data":
raise RuntimeError(_PROXY_DATA_ERROR)
return super().get(key, default)
class ProxyArray:
"""Dtype-only placeholder for a device array.
Use in place of a real device array when calling ``make_<algo>()`` to
trigger ahead-of-time compilation without allocating GPU memory — for
example, on a build machine that has no GPU or no live data.
Satisfies the ``DeviceArrayLike`` protocol:
* ``is_device_array(proxy)`` -> ``True``
* ``get_dtype(proxy)`` -> the dtype supplied at construction
* ``get_data_pointer(proxy)``-> raises ``RuntimeError``
* ``is_contiguous(proxy)`` -> ``True``
Accessing the data pointer raises ``RuntimeError``; passing a
``ProxyArray`` to a compiled algorithm's ``__call__`` is not supported.
Example::
from cuda.compute import ProxyArray, make_reduce_into, OpKind
import numpy as np
reducer = make_reduce_into(
d_in=ProxyArray(np.float32),
d_out=ProxyArray(np.float32),
op=OpKind.PLUS,
h_init=np.zeros(1, dtype=np.float32),
compute_capability=[80, 90],
)
reducer.serialize()
"""
__slots__ = ("_dtype",)
def __init__(self, dtype):
self._dtype = np.dtype(dtype)
@property
def dtype(self) -> np.dtype:
return self._dtype
@property
def __cuda_array_interface__(self) -> dict:
return _ProxyCAI(
{
"shape": (1,),
"typestr": self._dtype.str,
"version": 3,
"strides": None, # C-contiguous
# "data" is intentionally absent — accessing it raises RuntimeError
}
)
def __repr__(self) -> str:
return f"ProxyArray(dtype={self._dtype})"
class ProxyValue:
"""Dtype-only placeholder for a scalar / initial-value argument.
Use in place of a real numpy scalar or array when calling ``make_<algo>()``
to trigger ahead-of-time compilation without real data — for example, for
the ``h_init`` argument of :func:`~cuda.compute.make_reduce_into`.
Accessing the data of a ``ProxyValue`` raises ``RuntimeError``; passing
one to a compiled algorithm's ``__call__`` is not supported.
"""
__slots__ = ("_dtype",)
def __init__(self, dtype):
self._dtype = np.dtype(dtype)
@property
def dtype(self) -> np.dtype:
return self._dtype
def __repr__(self) -> str:
return f"ProxyValue(dtype={self._dtype})"
def is_proxy(obj) -> bool:
"""True if *obj* is a build-time placeholder (ProxyArray/ProxyValue)."""
return isinstance(obj, (ProxyArray, ProxyValue))

View File

@@ -0,0 +1,41 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""Serialization for cuda.compute algorithms."""
from __future__ import annotations
from .dispatch import deserialize as deserialize
from .dispatch import serialize as serialize
from .serializable import BOOL as BOOL
from .serializable import BUILD_RESULT as BUILD_RESULT
from .serializable import BUILD_RESULTS as BUILD_RESULTS
from .serializable import CONDITIONAL as CONDITIONAL
from .serializable import ENUM as ENUM
from .serializable import ITER as ITER
from .serializable import NESTED as NESTED
from .serializable import OP as OP
from .serializable import U8 as U8
from .serializable import U32 as U32
from .serializable import U64 as U64
from .serializable import VALUE as VALUE
from .serializable import Serializable as Serializable
__all__ = [
"serialize",
"deserialize",
"Serializable",
"ITER",
"OP",
"VALUE",
"U8",
"U32",
"U64",
"BOOL",
"ENUM",
"CONDITIONAL",
"BUILD_RESULT",
"BUILD_RESULTS",
"NESTED",
]

View File

@@ -0,0 +1,217 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""Byte serialization of CCCL descriptors (``cccl_op_t``, ``cccl_iterator_t``, etc.)."""
from __future__ import annotations
import struct
import numpy as np
from cuda.cccl import __version__ as _PKG_VERSION
from .._bindings import Iterator, IteratorKind, Op, OpKind, TypeEnum, TypeInfo, Value
from .._device_code import DeviceCode
# An opaque 8-byte marker identifying a cuda.compute serialization blob.
_MAGIC = b"CCCLPYS1"
class Writer:
"""Append-only little-endian byte buffer."""
def __init__(self) -> None:
self.buf = bytearray()
def u8(self, v: int) -> None:
self.buf += struct.pack("<B", v)
def u32(self, v: int) -> None:
self.buf += struct.pack("<I", v)
def u64(self, v: int) -> None:
self.buf += struct.pack("<Q", v)
def blob(self, b: bytes) -> None:
self.u64(len(b))
self.buf += b
def text(self, s: str) -> None:
self.blob(s.encode("utf-8"))
def getvalue(self) -> bytes:
return bytes(self.buf)
class Reader:
"""Bounds-checked little-endian reader over a bytes blob."""
def __init__(self, data: bytes) -> None:
self._data = memoryview(data)
self.pos = 0
def _take(self, n: int) -> memoryview:
end = self.pos + n
if end > len(self._data):
raise ValueError("serialization descriptor blob truncated")
out = self._data[self.pos : end]
self.pos = end
return out
def u8(self) -> int:
return struct.unpack("<B", self._take(1))[0]
def u32(self) -> int:
return struct.unpack("<I", self._take(4))[0]
def u64(self) -> int:
return struct.unpack("<Q", self._take(8))[0]
def blob(self) -> bytes:
return bytes(self._take(self.u64()))
def text(self) -> str:
return self.blob().decode("utf-8")
def remaining(self) -> bytes:
"""Bytes after the descriptor region: the C build_result blob."""
return bytes(self._data[self.pos :])
# --- framing -----------------------------------------------------------------
def _check_header(r: Reader) -> None:
"""Validate the magic and package-version stamp at the start of a blob."""
if bytes(r._take(len(_MAGIC))) != _MAGIC:
raise ValueError(
"serialization blob: bad magic (not a cuda.compute serialization blob)"
)
version = r.text()
if version != _PKG_VERSION:
raise ValueError(
"serialization blob: cuda-cccl version mismatch "
f"(blob={version!r}, current={_PKG_VERSION!r}); "
"re-serialize with this version of cuda-cccl"
)
def begin(algo_tag: str) -> Writer:
"""Start a descriptor sidecar with the magic/version/algo header.
``algo_tag`` is the algorithm class's ``__qualname__`` (e.g. ``"_Reduce"``).
"""
w = Writer()
w.buf += _MAGIC
w.text(_PKG_VERSION)
w.text(algo_tag)
return w
def open(blob: bytes, expected_algo: str) -> Reader:
"""Validate the header and return a reader positioned at the first field."""
r = Reader(blob)
_check_header(r)
algo = r.text()
if algo != expected_algo:
raise ValueError(
f"serialization blob: wrong algorithm (blob tag={algo!r}, expected={expected_algo!r})"
)
return r
def peek_algo(blob: bytes) -> str:
"""Return the algorithm tag (class ``__qualname__``) from a blob header
without consuming the blob.
Validates magic + version. Used by the generic ``deserialize`` dispatcher to
pick the right algorithm reconstructor.
"""
r = Reader(blob)
_check_header(r)
return r.text()
# --- descriptor (de)serialization --------------------------------------------
def write_type_info(w: Writer, ti: TypeInfo) -> None:
w.u64(ti.size)
w.u64(ti.alignment)
w.u32(int(ti.typenum))
def read_type_info(r: Reader) -> TypeInfo:
size = r.u64()
alignment = r.u64()
type_enum = r.u32()
return TypeInfo(size, alignment, TypeEnum(type_enum))
def write_op(w: Writer, op: Op) -> None:
# Serialize the operator's device code in full so reconstruction needs no
# JIT; only per-call op state is omitted.
w.u32(int(op.operator_type))
w.text(op.name)
w.blob(op.ltoir)
w.text(op.code.kind)
w.u32(op.state_alignment)
# State size is structural: it fixes op_data.size at construction. The state
# bytes themselves are bound per-call.
w.u64(len(op.state))
extras = op.extra_code
w.u32(len(extras))
for dc in extras:
w.blob(dc.op_bytes)
w.text(dc.kind)
def read_op(r: Reader) -> Op:
operator_type = OpKind(r.u32())
name = r.text()
code = r.blob()
code_kind = r.text()
state_alignment = r.u32()
state_size = r.u64()
n_extra = r.u32()
extras = [DeviceCode(op_bytes=r.blob(), kind=r.text()) for _ in range(n_extra)]
return Op(
name=name,
operator_type=operator_type,
ltoir=DeviceCode(op_bytes=code, kind=code_kind),
state=bytes(state_size), # zero placeholder; real bytes bound per-call
state_alignment=state_alignment,
extra_ltoirs=extras,
)
def write_iterator(w: Writer, it: Iterator) -> None:
w.u8(1 if it.is_kind_pointer() else 0)
w.u32(it.alignment)
write_type_info(w, it.value_type)
write_op(w, it.advance_op)
write_op(w, it.dereference_or_assign_op)
def read_iterator(r: Reader) -> Iterator:
kind = IteratorKind.POINTER if r.u8() else IteratorKind.ITERATOR
alignment = r.u32()
value_type = read_type_info(r)
advance = read_op(r)
deref = read_op(r)
# state is bound per-call (set_cccl_iterator_state); start with none.
return Iterator(alignment, kind, advance, deref, value_type, state=None)
def write_value(w: Writer, val: Value) -> None:
# Only the type is static; the value bytes are bound per-call.
write_type_info(w, val.type)
def read_value(r: Reader) -> Value:
value_type = read_type_info(r)
# Placeholder state sized to the value type; __call__ rebinds the real bytes.
placeholder = np.zeros(value_type.size, dtype=np.uint8)
return Value(value_type, placeholder)

View File

@@ -0,0 +1,60 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""Public, free-standing serialize/deserialize entry points."""
from __future__ import annotations
from typing import Any
from . import codec
from .serializable import Serializable
def serialize(algorithm: Any) -> bytes:
"""Serialize a built algorithm into a blob of bytes.
Args:
algorithm: An object returned by a ``make_*`` factory (e.g.
:func:`make_reduce_into`, :func:`make_exclusive_scan`).
Returns:
A versioned, self-describing byte blob. Reconstruct it with
:func:`deserialize`; no objects required at load time.
"""
if not callable(getattr(type(algorithm), "serialize", None)):
raise TypeError(
f"{type(algorithm).__name__} is not a serializable algorithm "
"(expected an object from a make_* factory)."
)
return algorithm.serialize()
def deserialize(blob: bytes):
"""Reconstruct a built algorithm from a blob produced by :func:`serialize`.
Warning:
The returned object is **not safe to use from multiple threads
concurrently**. Do not deserialize once and share the object across
threads: every call writes its arguments (array pointers, sizes,
operator and initial-value state) into the object before launching, so
overlapping calls can launch kernels with another thread's arguments —
silently wrong results or CUDA errors, with no exception raised at the
point of misuse. Unlike the ``make_*`` factories, which hand each
calling thread its own cached object, ``deserialize`` returns a fresh
uncached object with no per-thread protection. For concurrent use,
call :func:`deserialize` in each thread — reconstruction performs no
recompilation, so per-thread deserialization from one shared blob is
cheap. One thread at a time (for example, handing the object between
threads with proper ordering) is fine.
Raises:
ValueError: if the blob is malformed or its algorithm tag is unknown.
"""
tag = codec.peek_algo(blob)
try:
cls = Serializable._registry[tag]
except KeyError:
raise ValueError(f"serialization blob: unknown algorithm tag {tag!r}") from None
return cls.deserialize(blob)

View File

@@ -0,0 +1,285 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""Schema-driven serialize/deserialize base for cuda.compute algorithms.
A built-algorithm class declares a ``__serialization_schema__`` listing its
serialized members as ``(attr_name, kind)`` pairs, including its ``build_result``
as a ``BUILD_RESULT(<type>)`` member. ``Serializable`` provides generic
``serialize``/``deserialize`` that walk the schema, so subclasses need no
hand-written codec and both directions share one field order. Subclasses
auto-register by their ``__qualname__`` for the free-function
``deserialize`` dispatcher.
"""
from __future__ import annotations
from typing import Any, Callable, TypeVar
from . import codec
class _Kind:
"""A serialized member kind: writes/reads one value.
``obj`` is the instance being (de)serialized; most kinds ignore it, but
``CONDITIONAL`` uses it to read a selector member deserialized earlier.
"""
__slots__ = ()
def write(self, w: codec.Writer, value: Any, obj: Any) -> None:
raise NotImplementedError
def read(self, r: codec.Reader, obj: Any) -> Any:
raise NotImplementedError
class _Descriptor(_Kind):
"""Iterator / Op / Value descriptor, delegating to the codec codec."""
__slots__ = ("_write", "_read")
def __init__(self, writer: Callable, reader: Callable) -> None:
self._write = writer
self._read = reader
def write(self, w: codec.Writer, value: Any, obj: Any) -> None:
self._write(w, value)
def read(self, r: codec.Reader, obj: Any) -> Any:
return self._read(r)
ITER = _Descriptor(codec.write_iterator, codec.read_iterator)
OP = _Descriptor(codec.write_op, codec.read_op)
VALUE = _Descriptor(codec.write_value, codec.read_value)
class _Scalar(_Kind):
"""A fixed-width little-endian unsigned integer (u8/u32/u64)."""
__slots__ = ("width",)
def __init__(self, width: int) -> None:
self.width = width
def write(self, w: codec.Writer, value: Any, obj: Any) -> None:
{1: w.u8, 4: w.u32, 8: w.u64}[self.width](int(value))
def read(self, r: codec.Reader, obj: Any) -> int:
return {1: r.u8, 4: r.u32, 8: r.u64}[self.width]()
U8, U32, U64 = _Scalar(1), _Scalar(4), _Scalar(8)
class _Bool(_Kind):
"""A boolean, stored as a u8 (0/1)."""
__slots__ = ()
def write(self, w: codec.Writer, value: Any, obj: Any) -> None:
w.u8(1 if value else 0)
def read(self, r: codec.Reader, obj: Any) -> bool:
return bool(r.u8())
BOOL = _Bool()
class _Enum(_Kind):
"""An IntEnum member, stored as a u8 and reconstructed as the enum type."""
__slots__ = ("enum_cls",)
def __init__(self, enum_cls: Any) -> None:
self.enum_cls = enum_cls
def write(self, w: codec.Writer, value: Any, obj: Any) -> None:
w.u8(int(value))
def read(self, r: codec.Reader, obj: Any) -> Any:
return self.enum_cls(r.u8())
def ENUM(enum_cls: Any) -> _Enum:
"""Schema kind for a u8-backed IntEnum member."""
return _Enum(enum_cls)
class _SubObject(_Kind):
"""A sub-object with its own ``serialize()``/``deserialize()``, carried as a
length-prefixed blob. Used for the C ``build_result`` and for a nested
``Serializable`` (e.g. select wrapping three_way_partition)."""
__slots__ = ("cls",)
def __init__(self, cls: Any) -> None:
self.cls = cls
def write(self, w: codec.Writer, value: Any, obj: Any) -> None:
w.blob(value.serialize())
def read(self, r: codec.Reader, obj: Any) -> Any:
return self.cls.deserialize(r.blob())
def BUILD_RESULT(cls: type) -> _SubObject:
"""Schema kind for an algorithm's ``Device<Algo>BuildResult`` member."""
return _SubObject(cls)
class _BuildResults(_Kind):
"""A ``{cc: Device<Algo>BuildResult}`` mapping — one compiled build result per
target compute capability.
Wire form: ``u32`` count, then for each entry a ``u32`` cc key
(``cc_major * 10 + cc_minor``) followed by the length-prefixed build_result
blob. Entries are written in sorted-key order so the encoding is
deterministic. On read, each build_result is deserialized *without* loading
(``load=False``); the matching build result is loaded lazily on first call, so a
multi-arch artifact stays portable across GPUs and needs no live device to
deserialize.
"""
__slots__ = ("cls",)
def __init__(self, cls: Any) -> None:
self.cls = cls
def write(self, w: codec.Writer, value: Any, obj: Any) -> None:
from .._caching import _PerCCBuildResults
# Wrappers always hold a _PerCCBuildResults (build_for_ccs and
# read() below both produce one). serialize_build_result takes the
# per-cc source lock, so serialization cannot observe a source whose
# first device load is still in progress; a plain dict here would
# silently bypass that lock.
assert isinstance(value, _PerCCBuildResults)
ccs = sorted(value)
w.u32(len(ccs))
for cc in ccs:
w.u32(int(cc))
w.blob(value.serialize_build_result(cc))
def read(self, r: codec.Reader, obj: Any) -> Any:
count = r.u32()
# A single-target blob must match this device, so validate its cc-major
# eagerly (clear error at deserialize). A multi-arch blob legitimately
# carries build results for other archs, so defer the cc check — resolve_build_result
# picks the matching one at call time. Kernel load stays lazy either way.
check_cc = count == 1
entries = [(r.u32(), r.blob()) for _ in range(count)]
result: dict[int, Any] = {}
for cc, blob in entries:
if cc in result:
raise ValueError(
f"duplicate compute-capability key {cc} in build_results blob"
)
result[cc] = self.cls.deserialize(blob, load=False, check_cc=check_cc)
from .._caching import _PerCCBuildResults
return _PerCCBuildResults(result)
def BUILD_RESULTS(cls: type) -> _BuildResults:
"""Schema kind for a ``{cc: Device<Algo>BuildResult}`` build result mapping."""
return _BuildResults(cls)
def NESTED(cls: type) -> _SubObject:
"""Schema kind for a nested ``Serializable`` member (its blob is embedded)."""
return _SubObject(cls)
class _Conditional(_Kind):
"""A member whose kind depends on an earlier member's value.
``selector`` names a member deserialized *before* this one; ``branches``
maps each possible selector value to the kind to use (or ``None`` for an
absent member that (de)serializes to ``None``).
"""
__slots__ = ("selector", "branches")
def __init__(self, selector: str, branches: dict) -> None:
self.selector = selector
self.branches = branches
def _kind(self, obj: Any) -> "_Kind | None":
return self.branches[getattr(obj, self.selector)]
def write(self, w: codec.Writer, value: Any, obj: Any) -> None:
kind = self._kind(obj)
if kind is not None:
kind.write(w, value, obj)
def read(self, r: codec.Reader, obj: Any) -> Any:
kind = self._kind(obj)
return None if kind is None else kind.read(r, obj)
def CONDITIONAL(selector: str, branches: dict) -> _Conditional:
"""Schema kind for a member whose kind is chosen by ``selector``'s value."""
return _Conditional(selector, branches)
_S = TypeVar("_S", bound="Serializable")
class Serializable:
"""Mixin providing schema-driven serialize/deserialize + registration."""
__slots__ = ()
# __qualname__ -> subclass, populated as algorithm modules are imported.
_registry: dict[str, type[Serializable]] = {}
# Subclasses declare their serialized members here.
__serialization_schema__: tuple = ()
# Construction-time binding of a default-build wrapper's loaded result
# (see cache_build_results). Annotation only: storage comes from each
# subclass's __slots__; __init__ or deserialize() below assigns it.
_bound_build_result: Any
def __init_subclass__(cls, **kwargs: Any) -> None:
super().__init_subclass__(**kwargs)
Serializable._registry[cls.__qualname__] = cls
def _after_deserialize(self) -> None:
"""Hook to bind derived, non-serialized state after schema members are read.
Called once at the end of ``deserialize``. Subclasses that keep a cached
attribute derived from serialized members (and set it in ``__init__``)
override this to rebind it; the default is a no-op.
"""
def serialize(self) -> bytes:
"""Serialize this built algorithm to a self-contained serialization blob."""
w = codec.begin(type(self).__qualname__)
for attr, kind in self.__serialization_schema__:
kind.write(w, getattr(self, attr), self)
return w.getvalue()
@classmethod
def deserialize(cls: type[_S], blob: bytes) -> _S:
"""Reconstruct a built algorithm from a blob; no objects required.
Members are read in schema order and set on the instance as they are
read, so a ``CONDITIONAL`` member can consult a selector read earlier.
"""
r = codec.open(blob, cls.__qualname__)
obj = cls.__new__(cls)
# deserialize() bypasses __init__, which is where default-build
# wrappers bind their loaded result (see cache_build_results); an
# unbound wrapper resolves per call instead.
obj._bound_build_result = None
for attr, kind in cls.__serialization_schema__:
setattr(obj, attr, kind.read(r, obj))
obj._after_deserialize()
return obj

View File

@@ -0,0 +1,58 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""Target compute-capability context for device-code (LTO-IR) compilation.
Operators (numba) and iterators (NVRTC C++) are compiled to LTO-IR once and
linked into every per-arch build result of an algorithm. nvJitLink requires the final
target SM to be **at least as new** as every linked LTO/PTX input's arch. So for
a multi-arch build, this shared device code must be compiled for the **lowest**
target arch; otherwise linking a (say) sm_89 operator into an sm_80 cubin fails
with ``nvJitLink error``.
This module holds a context-local "target cc" that the leaf compilers
(``_jit`` for operators, ``_cpp_compile`` for iterators) consult. It is set
around a build by the caching decorator that wraps every ``make_<algo>``.
``None`` means "use the current device" — the default single-target behavior,
unchanged.
"""
from __future__ import annotations
import contextlib
import contextvars
# (major, minor) tuple, or None to mean "current device default".
_target_cc: contextvars.ContextVar = contextvars.ContextVar(
"cccl_target_cc", default=None
)
def get_target_cc():
"""The current build's target cc as ``(major, minor)``, or ``None``.
``None`` means device code should target the current device (the default).
"""
return _target_cc.get()
@contextlib.contextmanager
def target_cc(compute_capability):
"""Set the shared-device-code target cc for the duration of a build.
``compute_capability`` is the ``make_<algo>`` argument (``None`` / int /
``(major, minor)`` / list). For a multi-arch build the shared operator /
iterator LTO-IR is compiled for the **lowest** requested arch so it links
into every build result. ``None`` leaves the current-device default in place.
"""
from ._cccl_interop import normalize_compute_capabilities
ccs = normalize_compute_capabilities(compute_capability)
# normalized list is sorted ascending, so ccs[0] is the minimum target.
cc = ccs[0] if ccs else None
token = _target_cc.set(cc)
try:
yield
finally:
_target_cc.reset(token)

View File

@@ -0,0 +1,22 @@
from __future__ import annotations
import re
__all__ = ["sanitize_identifier"]
def sanitize_identifier(name: str) -> str:
"""Sanitize a name to be a valid Python/LLVM identifier.
This replaces any character that isn't alphanumeric or underscore with
an underscore. This is needed because:
- Lambda functions have __name__ = "<lambda>" which contains angle brackets
- Python identifiers and LLVM/NVVM global names don't allow special characters
Args:
name: The name to sanitize (e.g., function __name__)
Returns:
A sanitized name safe for use as a Python identifier or LLVM symbol
"""
return re.sub(r"[^a-zA-Z0-9_]", "_", name)

View File

@@ -0,0 +1,168 @@
# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from typing import TYPE_CHECKING, List, Optional, Tuple
import numpy as np
"""
Utilities for extracting information from protocols such as `__cuda_array_interface__` and `__cuda_stream__`.
"""
if TYPE_CHECKING:
from ..typing import DeviceArrayLike, GpuStruct
def is_device_array(obj: object) -> bool:
"""Check if an object implements the `__cuda_array_interface__` protocol."""
return hasattr(obj, "__cuda_array_interface__")
def get_data_pointer(arr: DeviceArrayLike) -> int:
# TODO: these are fast paths for CuPy and PyTorch until
# we have a more general solution.
# Fast path for PyTorch (arr.data_ptr())
try:
return arr.data_ptr() # type: ignore
except AttributeError:
pass
# Fast path for CuPy (arr.data.ptr)
try:
return arr.data.ptr # type: ignore
except AttributeError:
pass
# Fall back to __cuda_array_interface__
return arr.__cuda_array_interface__["data"][0]
def get_dtype(arr: DeviceArrayLike | GpuStruct | np.ndarray) -> np.dtype:
# Try the fast path via .dtype attribute (works for np.ndarray, GpuStruct, and most device arrays)
try:
return np.dtype(arr.dtype) # type: ignore
except (AttributeError, TypeError):
pass
# Fall back to __cuda_array_interface__ for DeviceArrayLike
cai = arr.__cuda_array_interface__ # type: ignore
typestr = cai["typestr"]
if typestr.startswith("|V"):
# it's a structured dtype, use the descr field:
return np.dtype(cai["descr"])
else:
# a simple dtype, use the typestr field:
return np.dtype(typestr)
def get_shape(arr: DeviceArrayLike) -> Tuple[int]:
try:
# TODO: this is a fast path for CuPy until
# we have a more general solution.
return arr.shape # type: ignore
except AttributeError:
return arr.__cuda_array_interface__["shape"]
def get_size(arr: DeviceArrayLike) -> int:
"""Get the total number of elements in an array."""
# Try fast path via .size attribute
try:
return int(arr.size) # type: ignore
except AttributeError:
pass
# Fall back to computing from shape
shape = get_shape(arr)
import math
return math.prod(shape)
def is_contiguous(arr: DeviceArrayLike) -> bool:
cai = arr.__cuda_array_interface__
strides = cai["strides"]
if strides is None:
return True
shape = cai["shape"]
if any(dim == 0 for dim in shape):
# array has no elements
return True
if all(dim == 1 for dim in shape):
# there is a single element:
return True
itemsize = get_dtype(arr).itemsize
if strides[-1] == itemsize:
# assume C-contiguity
expected_stride = itemsize
for dim, stride in zip(reversed(shape), reversed(strides)):
if stride != expected_stride:
return False
expected_stride *= dim
return True
elif strides[0] == itemsize:
# assume F-contiguity
expected_stride = itemsize
for dim, stride in zip(shape, strides):
if stride != expected_stride:
return False
expected_stride *= dim
return True
else:
# not contiguous
return False
def compute_c_contiguous_strides_in_bytes(
shape: Tuple[int], itemsize: int
) -> Tuple[int, ...]:
"""Return C-contiguous strides in bytes for a given shape and itemsize (compatible with NumPy .strides)."""
strides: List[int] = []
acc = itemsize
for dim in reversed(shape):
strides.insert(0, acc)
acc *= dim
return tuple(strides)
def validate_and_get_stream(stream) -> Optional[int]:
# null stream is allowed
if stream is None:
return None
try:
stream_property = stream.__cuda_stream__()
except AttributeError as e:
raise TypeError(
f"stream argument {stream} does not implement the '__cuda_stream__' protocol"
) from e
try:
version, handle, *_ = stream_property
except (TypeError, ValueError) as e:
raise TypeError(
f"could not obtain __cuda_stream__ protocol version and handle from {stream_property}"
) from e
if version == 0:
if not isinstance(handle, int):
raise TypeError(f"invalid stream handle {handle}")
return handle
raise TypeError(f"unsupported __cuda_stream__ version {version}")

View File

@@ -0,0 +1,94 @@
from __future__ import annotations
import functools
import weakref
from types import SimpleNamespace
from typing import Optional
from cuda.bindings import driver, runtime
try:
from cuda.core import Device
from cuda.core._utils.cuda_utils import handle_return
except ImportError:
from cuda.core.experimental import Device
from cuda.core.experimental._utils.cuda_utils import handle_return
from ..typing import StreamLike
@functools.cache
def _set_default_mempool_threshold(device_id: int):
"""
Set the release threshold for the default memory pool on this device,
if we haven't already done so. This prevents the driver from attempting
to shrink the pool after every sync, which can be slow.
"""
default_pool = handle_return(driver.cuDeviceGetDefaultMemPool(device_id))
threshold = handle_return(
driver.cuMemPoolGetAttribute(
default_pool, driver.CUmemPool_attribute.CU_MEMPOOL_ATTR_RELEASE_THRESHOLD
)
)
if int(threshold) == 0:
handle_return(
driver.cuMemPoolSetAttribute(
default_pool,
driver.CUmemPool_attribute.CU_MEMPOOL_ATTR_RELEASE_THRESHOLD,
driver.cuuint64_t(0xFFFFFFFFFFFFFFFF),
)
)
def _finalize_buffer(ptr: int, stream_handle: Optional[int] = None):
"""Cleanup function for weakref finalizer."""
if ptr != 0:
try:
handle_return(runtime.cudaFreeAsync(ptr, stream_handle))
except Exception as e:
# Don't raise in finalizer, just print warning
print(f"Warning: Failed to free CUDA memory: {e}")
class TempStorageBuffer:
"""
Simple wrapper type around the memory allocation used for temporary storage,
exposing __cuda_array_interface__ and some other attributes for fast access.
This implementation uses cuda.bindings.runtime.cudaMallocAsync and
cudaFreeAsync for allocation and deallocation.
"""
def __init__(self, size: int, stream: Optional[StreamLike] = None):
# Get the current device
dev = Device()
stream_handle = stream.__cuda_stream__()[1] if stream is not None else None
# Set the release threshold for the default memory pool on this device
_set_default_mempool_threshold(dev.device_id)
# Allocate memory using cudaMallocAsync
device_ptr_int = handle_return(runtime.cudaMallocAsync(size, stream_handle))
self._ptr = int(device_ptr_int)
self._stream_handle = stream_handle
self._size = size
# attributes for fast path access in protocols.py
self.nbytes = size
self.data = SimpleNamespace(ptr=self._ptr)
# Set up weakref finalizer for cleanup
self._finalizer = weakref.finalize(
self, _finalize_buffer, self._ptr, self._stream_handle
)
@property
def __cuda_array_interface__(self):
return {
"data": (self._ptr, False),
"shape": (self._size,),
"strides": (1,),
"typestr": "|u1",
"version": 3,
}

View File

@@ -0,0 +1,76 @@
# Copyright (c) 2024, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from .._serialization import deserialize as deserialize
from .._serialization import serialize as serialize
from ._binary_search import lower_bound as lower_bound
from ._binary_search import make_lower_bound as make_lower_bound
from ._binary_search import make_upper_bound as make_upper_bound
from ._binary_search import upper_bound as upper_bound
from ._histogram import histogram_even as histogram_even
from ._histogram import make_histogram_even as make_histogram_even
from ._reduce import make_reduce_into as make_reduce_into
from ._reduce import reduce_into as reduce_into
from ._scan import exclusive_scan as exclusive_scan
from ._scan import inclusive_scan as inclusive_scan
from ._scan import make_exclusive_scan as make_exclusive_scan
from ._scan import make_inclusive_scan as make_inclusive_scan
from ._segmented_reduce import make_segmented_reduce as make_segmented_reduce
from ._segmented_reduce import segmented_reduce
from ._select import make_select as make_select
from ._select import select as select
from ._sort import DoubleBuffer, SortOrder
from ._sort import make_merge_sort as make_merge_sort
from ._sort import make_radix_sort as make_radix_sort
from ._sort import make_segmented_sort as make_segmented_sort
from ._sort import merge_sort as merge_sort
from ._sort import radix_sort as radix_sort
from ._sort import segmented_sort as segmented_sort
from ._three_way_partition import make_three_way_partition as make_three_way_partition
from ._three_way_partition import three_way_partition as three_way_partition
from ._transform import binary_transform, unary_transform
from ._transform import make_binary_transform as make_binary_transform
from ._transform import make_unary_transform as make_unary_transform
from ._unique_by_key import make_unique_by_key as make_unique_by_key
from ._unique_by_key import unique_by_key as unique_by_key
__all__ = [
"serialize",
"deserialize",
"reduce_into",
"make_reduce_into",
"lower_bound",
"make_lower_bound",
"upper_bound",
"make_upper_bound",
"inclusive_scan",
"make_inclusive_scan",
"exclusive_scan",
"make_exclusive_scan",
"unary_transform",
"make_unary_transform",
"binary_transform",
"make_binary_transform",
"histogram_even",
"make_histogram_even",
"merge_sort",
"make_merge_sort",
"radix_sort",
"make_radix_sort",
"segmented_reduce",
"make_segmented_reduce",
"unique_by_key",
"make_unique_by_key",
"segmented_sort",
"make_segmented_sort",
"three_way_partition",
"make_three_way_partition",
"select",
"make_select",
"DoubleBuffer",
"SortOrder",
]

View File

@@ -0,0 +1,345 @@
# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from typing import ClassVar
import numpy as np
from .. import _bindings, types
from .. import _cccl_interop as cccl
from .._caching import cache_build_results, cache_with_registered_key_functions
from .._cccl_interop import set_cccl_iterator_state
from .._serialization import (
BUILD_RESULTS,
ITER,
OP,
Serializable,
)
from .._utils import protocols
from ..op import OpAdapter, OpKind, make_op_adapter
from ..typing import DeviceArrayLike, IteratorT, Operator
def _data_pointer_or_none(array) -> int | None:
# A ProxyArray is a build-time placeholder with no GPU allocation and thus no
# data pointer; return None for it (these pointers are only cache-key
# discriminators) so binary_search can be built without a GPU.
from .._proxy import is_proxy
return None if is_proxy(array) else protocols.get_data_pointer(array)
class _BinarySearch:
# Shared implementation for the lower/upper bound searchers.
_MODE: ClassVar[_bindings.BinarySearchMode]
__slots__ = [
"_bound_build_result",
"build_results",
"loaded_build_result",
"d_data_cccl",
"d_values_cccl",
"d_out_cccl",
"op_cccl",
"data_ptr",
"out_ptr",
]
__serialization_schema__ = (
("d_data_cccl", ITER),
("d_values_cccl", ITER),
("d_out_cccl", ITER),
("op_cccl", OP),
("build_results", BUILD_RESULTS(_bindings.DeviceBinarySearchBuildResult)),
)
def __init__(
self,
d_data: DeviceArrayLike,
d_values: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike,
comp: OpAdapter,
compute_capability=None,
):
if not protocols.is_device_array(d_data):
raise ValueError("d_data must be a device array for index outputs.")
if not protocols.is_device_array(d_out):
raise ValueError("d_out must be a device array for index outputs.")
out_dtype = protocols.get_dtype(d_out)
if out_dtype.kind != "u":
raise TypeError("d_out must use an unsigned integer dtype for indices.")
if out_dtype.itemsize != np.dtype(np.uintp).itemsize:
raise ValueError(
"d_out must use a pointer-sized unsigned integer dtype (np.uintp)."
)
self.data_ptr = _data_pointer_or_none(d_data)
self.out_ptr = _data_pointer_or_none(d_out)
self.d_data_cccl = cccl.to_cccl_input_iter(d_data)
self.d_values_cccl = cccl.to_cccl_input_iter(d_values)
data_value_type = cccl.get_value_type(d_data)
self.d_out_cccl = cccl.to_cccl_output_iter(d_out)
self.op_cccl = comp.compile((data_value_type, data_value_type), types.uint8)
self.build_results, self._bound_build_result = cache_build_results(
_bindings.DeviceBinarySearchBuildResult,
d_data,
d_values,
d_out,
comp,
self._MODE,
compute_capability=compute_capability,
builder=lambda: cccl.build_for_ccs(
_bindings.DeviceBinarySearchBuildResult,
self._MODE,
self.d_data_cccl,
self.d_values_cccl,
self.d_out_cccl,
self.op_cccl,
compute_capability=compute_capability,
),
)
def __call__(
self,
*,
d_data,
num_items: int,
d_values,
num_values: int,
d_out,
comp: Operator | None,
stream=None,
):
# Select (and lazily load) the build result for the current device.
self.loaded_build_result = cccl.resolve_build_result(
self.build_results, self._bound_build_result
)
set_cccl_iterator_state(self.d_data_cccl, d_data)
set_cccl_iterator_state(self.d_values_cccl, d_values)
set_cccl_iterator_state(self.d_out_cccl, d_out)
# Update op state for stateful ops
comp_adapter = make_op_adapter(OpKind.LESS if comp is None else comp)
self.op_cccl.state = comp_adapter.get_state()
stream_handle = protocols.validate_and_get_stream(stream)
self.loaded_build_result.compute(
self.d_data_cccl,
num_items,
self.d_values_cccl,
num_values,
self.d_out_cccl,
self.op_cccl,
stream_handle,
)
class _LowerBound(_BinarySearch, Serializable):
__slots__ = ()
_MODE = _bindings.BinarySearchMode.LOWER_BOUND
class _UpperBound(_BinarySearch, Serializable):
__slots__ = ()
_MODE = _bindings.BinarySearchMode.UPPER_BOUND
@cache_with_registered_key_functions
def _make_binary_search(
d_data: DeviceArrayLike,
d_values: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike,
comp: OpAdapter,
mode: _bindings.BinarySearchMode,
data_ptr: int,
out_ptr: int,
compute_capability=None,
):
"""Cached factory for the binary_search searchers."""
cls = _LowerBound if mode == _bindings.BinarySearchMode.LOWER_BOUND else _UpperBound
return cls(d_data, d_values, d_out, comp, compute_capability=compute_capability)
def make_lower_bound(
*,
d_data: DeviceArrayLike,
d_values: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike,
comp: Operator | None = None,
compute_capability=None,
):
"""
Create a lower_bound object that can be called to find insertion positions.
Example:
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/binary_search/lower_bound_object.py
:language: python
:start-after: # example-begin
Args:
d_data: Device array containing the sorted input range.
d_values: Device array or iterator containing the search values.
d_out: Device array to store the index results.
comp: Optional comparison operator (default: ``OpKind.LESS``).
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that performs lower_bound.
See Also:
:func:`lower_bound`
"""
comp_adapter = make_op_adapter(OpKind.LESS if comp is None else comp)
return _make_binary_search(
d_data,
d_values,
d_out,
comp_adapter,
_bindings.BinarySearchMode.LOWER_BOUND,
_data_pointer_or_none(d_data),
_data_pointer_or_none(d_out),
compute_capability=compute_capability,
)
def make_upper_bound(
*,
d_data: DeviceArrayLike,
d_values: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike,
comp: Operator | None = None,
compute_capability=None,
):
"""
Create an upper_bound object that can be called to find insertion positions.
Example:
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/binary_search/upper_bound_object.py
:language: python
:start-after: # example-begin
Args:
d_data: Device array containing the sorted input range.
d_values: Device array or iterator containing the search values.
d_out: Device array to store the index results.
comp: Optional comparison operator (default: ``OpKind.LESS``).
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that performs upper_bound.
See Also:
:func:`upper_bound`
"""
comp_adapter = make_op_adapter(OpKind.LESS if comp is None else comp)
return _make_binary_search(
d_data,
d_values,
d_out,
comp_adapter,
_bindings.BinarySearchMode.UPPER_BOUND,
_data_pointer_or_none(d_data),
_data_pointer_or_none(d_out),
compute_capability=compute_capability,
)
def lower_bound(
*,
d_data: DeviceArrayLike,
num_items: int,
d_values: DeviceArrayLike | IteratorT,
num_values: int,
d_out: DeviceArrayLike,
comp: Operator | None = None,
stream=None,
):
"""
Find the *first* position that each value in ``d_values`` would be inserted into
``d_data`` to maintain sorted order.
Example:
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/binary_search/lower_bound_basic.py
:language: python
:start-after: # example-begin
Args:
d_data: Device array containing the sorted input range.
num_items: Number of items in ``d_data``.
d_values: Device array or iterator containing the search values.
num_values: Number of items in ``d_values``.
d_out: Device array to store the index results.
comp: Optional comparison operator (default: ``OpKind.LESS``).
stream: CUDA stream for the operation (optional).
"""
searcher = make_lower_bound(
d_data=d_data, d_values=d_values, d_out=d_out, comp=comp
)
searcher(
d_data=d_data,
num_items=num_items,
d_values=d_values,
num_values=num_values,
d_out=d_out,
comp=comp,
stream=stream,
)
def upper_bound(
*,
d_data: DeviceArrayLike,
num_items: int,
d_values: DeviceArrayLike | IteratorT,
num_values: int,
d_out: DeviceArrayLike,
comp: Operator | None = None,
stream=None,
):
"""
Find the *last* position that each value in ``d_values`` would be inserted into
``d_data`` to maintain sorted order.
Example:
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/binary_search/upper_bound_basic.py
:language: python
:start-after: # example-begin
Args:
d_data: Device array containing the sorted input range.
num_items: Number of items in ``d_data``.
d_values: Device array or iterator containing the search values.
num_values: Number of items in ``d_values``.
d_out: Device array to store the index results.
comp: Optional comparison operator (default: ``OpKind.LESS``).
stream: CUDA stream for the operation (optional).
"""
searcher = make_upper_bound(
d_data=d_data, d_values=d_values, d_out=d_out, comp=comp
)
searcher(
d_data=d_data,
num_items=num_items,
d_values=d_values,
num_values=num_values,
d_out=d_out,
comp=comp,
stream=stream,
)

View File

@@ -0,0 +1,330 @@
# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
import math
from typing import Union
import numpy as np
from .. import _bindings
from .. import _cccl_interop as cccl
from .._caching import cache_build_results, cache_with_registered_key_functions
from .._cccl_interop import set_cccl_iterator_state, to_cccl_value_state
from .._serialization import BUILD_RESULTS, ITER, U64, VALUE, Serializable
from .._utils.protocols import get_data_pointer, validate_and_get_stream
from .._utils.temp_storage_buffer import TempStorageBuffer
from ..typing import DeviceArrayLike, IteratorT
class _Histogram(Serializable):
__slots__ = [
"_bound_build_result",
"num_rows",
"d_samples_cccl",
"d_histogram_cccl",
"h_num_output_levels_cccl",
"h_lower_level_cccl",
"h_upper_level_cccl",
"build_results",
"loaded_build_result",
]
__serialization_schema__ = (
("num_rows", U64),
("d_samples_cccl", ITER),
("d_histogram_cccl", ITER),
("h_num_output_levels_cccl", VALUE),
("h_lower_level_cccl", VALUE),
("h_upper_level_cccl", VALUE),
("build_results", BUILD_RESULTS(_bindings.DeviceHistogramBuildResult)),
)
def __init__(
self,
d_samples: DeviceArrayLike | IteratorT,
d_histogram: DeviceArrayLike,
h_num_output_levels: np.ndarray,
h_lower_level: np.ndarray,
h_upper_level: np.ndarray,
num_samples: int,
compute_capability=None,
):
num_channels = 1
num_active_channels = 1
is_evenly_segmented = True
self.num_rows = 1
num_levels = h_num_output_levels[0]
row_stride_samples = num_samples
self.d_samples_cccl = cccl.to_cccl_input_iter(d_samples)
self.d_histogram_cccl = cccl.to_cccl_output_iter(d_histogram)
self.h_num_output_levels_cccl = cccl.to_cccl_value(h_num_output_levels)
self.h_lower_level_cccl = cccl.to_cccl_value(h_lower_level)
self.h_upper_level_cccl = cccl.to_cccl_value(h_upper_level)
self.build_results, self._bound_build_result = cache_build_results(
_bindings.DeviceHistogramBuildResult,
d_samples,
d_histogram,
int(num_levels),
h_lower_level.dtype,
num_samples,
is_evenly_segmented,
compute_capability=compute_capability,
builder=lambda: cccl.build_for_ccs(
_bindings.DeviceHistogramBuildResult,
num_channels,
num_active_channels,
self.d_samples_cccl,
num_levels,
self.d_histogram_cccl,
self.h_lower_level_cccl.type,
self.num_rows,
row_stride_samples,
is_evenly_segmented,
compute_capability=compute_capability,
),
)
def __call__(
self,
*,
temp_storage,
d_samples: DeviceArrayLike | IteratorT,
d_histogram: DeviceArrayLike,
h_num_output_levels: np.ndarray,
h_lower_level: np.ndarray,
h_upper_level: np.ndarray,
num_samples: int,
stream=None,
):
# Select (and lazily load) the build result for the current device.
self.loaded_build_result = cccl.resolve_build_result(
self.build_results, self._bound_build_result
)
set_cccl_iterator_state(self.d_samples_cccl, d_samples)
set_cccl_iterator_state(self.d_histogram_cccl, d_histogram)
self.h_num_output_levels_cccl.state = to_cccl_value_state(h_num_output_levels)
self.h_lower_level_cccl.state = to_cccl_value_state(h_lower_level)
self.h_upper_level_cccl.state = to_cccl_value_state(h_upper_level)
stream_handle = validate_and_get_stream(stream)
if temp_storage is None:
temp_storage_bytes = 0
d_temp_storage = 0
else:
temp_storage_bytes = temp_storage.nbytes
# Note: this is slightly slower, but supports all ndarray-like objects as long as they support CAI
# TODO: switch to use gpumemoryview once it's ready
d_temp_storage = get_data_pointer(temp_storage)
temp_storage_bytes = self.loaded_build_result.compute_even(
d_temp_storage,
temp_storage_bytes,
self.d_samples_cccl,
self.d_histogram_cccl,
self.h_num_output_levels_cccl,
self.h_lower_level_cccl,
self.h_upper_level_cccl,
num_samples,
self.num_rows,
num_samples,
stream_handle,
)
return temp_storage_bytes
@cache_with_registered_key_functions
def _make_histogram_even_impl(
d_samples: DeviceArrayLike | IteratorT,
d_histogram: DeviceArrayLike,
num_output_levels_val: int,
level_dtype,
uses_64bit_offset: bool,
uses_privatized_smem: bool,
compute_capability=None,
):
"""Internal cached implementation of make_histogram_even.
The uses_64bit_offset and uses_privatized_smem parameters ensure
kernels compiled for different offset and bin count regimes aren't reused.
"""
# Reconstruct the numpy arrays expected by _Histogram
h_num_output_levels = np.array([num_output_levels_val], dtype=np.int32)
# Bounds are runtime values. These placeholders only provide storage for
# cccl_value_t wrappers; build receives only the level type.
h_lower_level = np.zeros(1, dtype=level_dtype)
h_upper_level = np.ones(1, dtype=level_dtype)
# v1 only needs num_samples to select the generated offset type, so use a
# representative value for the requested offset-width regime.
if uses_64bit_offset:
sample_size = cccl.get_value_type(d_samples).size
int_max = np.iinfo(np.int32).max
# Smallest representative sample count that still selects long long
# offsets in v1's build-time offset type check.
build_num_samples = math.ceil(int_max / sample_size)
else:
build_num_samples = 1
return _Histogram(
d_samples,
d_histogram,
h_num_output_levels,
h_lower_level,
h_upper_level,
build_num_samples,
compute_capability=compute_capability,
)
def make_histogram_even(
*,
d_samples: DeviceArrayLike | IteratorT,
d_histogram: DeviceArrayLike,
h_num_output_levels: np.ndarray,
h_lower_level: np.ndarray,
h_upper_level: np.ndarray,
num_samples: int,
compute_capability=None,
):
"""Implements a device-wide histogram that places ``d_samples`` into evenly-spaced bins.
Example:
Below, ``make_histogram_even`` is used to create a histogram object that can be reused.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/histogram/histogram_object.py
:language: python
:start-after: # example-begin
Args:
d_samples: Device array or iterator containing the input samples to be histogrammed
d_histogram: Device array to store the histogram
h_num_output_levels: Host array containing the number of output levels
h_lower_level: Host array containing the lower level
h_upper_level: Host array containing the upper level
num_samples: Number of samples to be histogrammed
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that can be used to perform the histogram
"""
# Extract compile-relevant cache inputs from arrays.
num_output_levels_val = int(h_num_output_levels[0])
if h_lower_level.dtype != h_upper_level.dtype:
raise TypeError(
"h_lower_level and h_upper_level must have the same dtype; "
f"got {h_lower_level.dtype} and {h_upper_level.dtype}"
)
level_dtype = h_lower_level.dtype
# Mirrors v1 c/parallel/src/histogram.cu offset_cpp selection:
# (num_rows * row_stride_samples * sample_size) < INT_MAX selects int,
# otherwise long long. cuda.compute currently builds one-row histograms,
# so row_stride_samples is num_samples.
sample_size = cccl.get_value_type(d_samples).size
int_max = np.iinfo(np.int32).max
uses_64bit_offset = num_samples * sample_size >= int_max
# Mirrors CUB's even-histogram dispatch:
# detail::histogram::max_privatized_smem_bins is 256, and
# dispatch_histogram.cuh uses PRIVATIZED_SMEM_BINS=256 for <=256 bins
# and 0 for >256 bins.
num_bins = num_output_levels_val - 1
uses_privatized_smem = num_bins <= 256
# TODO: Once v2 is the default, remove uses_64bit_offset,
# num_output_levels_val, and uses_privatized_smem from this cache key;
# v2 passes row sizing and num_output_levels at runtime.
return _make_histogram_even_impl(
d_samples,
d_histogram,
num_output_levels_val,
level_dtype,
uses_64bit_offset,
uses_privatized_smem,
compute_capability=compute_capability,
)
def histogram_even(
*,
d_samples: DeviceArrayLike | IteratorT,
d_histogram: DeviceArrayLike,
num_output_levels: int,
lower_level: Union[np.floating, np.integer],
upper_level: Union[np.floating, np.integer],
num_samples: int,
stream=None,
):
"""
Performs device-wide histogram computation with evenly-spaced bins.
This function automatically handles temporary storage allocation and execution.
Example:
Below, ``histogram_even`` is used to compute a histogram with evenly-spaced bins.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/histogram/histogram_even_basic.py
:language: python
:start-after: # example-begin
:caption: Basic histogram example.
Args:
d_samples: Device array or iterator containing the input sequence of data samples
d_histogram: Device array to store the computed histogram
num_output_levels: Number of histogram bin levels (num_bins = num_output_levels - 1)
lower_level: Lower sample value bound (inclusive)
upper_level: Upper sample value bound (exclusive)
num_samples: Number of input samples
stream: CUDA stream for the operation (optional)
"""
# Histogram can accept multiple channels, with one value per channel for
# each of these parameters. The API only supports one channel for now but we
# pass arrays to make_histogram_even to support multiple channels in the
# future.
h_num_output_levels = np.array([num_output_levels], dtype=np.int32)
h_lower_level = np.array([lower_level], dtype=type(lower_level))
h_upper_level = np.array([upper_level], dtype=type(upper_level))
histogram = make_histogram_even(
d_samples=d_samples,
d_histogram=d_histogram,
h_num_output_levels=h_num_output_levels,
h_lower_level=h_lower_level,
h_upper_level=h_upper_level,
num_samples=num_samples,
)
temp_storage_bytes = histogram(
temp_storage=None,
d_samples=d_samples,
d_histogram=d_histogram,
h_num_output_levels=h_num_output_levels,
h_lower_level=h_lower_level,
h_upper_level=h_upper_level,
num_samples=num_samples,
stream=stream,
)
temp_storage = TempStorageBuffer(temp_storage_bytes, stream)
histogram(
temp_storage=temp_storage,
d_samples=d_samples,
d_histogram=d_histogram,
h_num_output_levels=h_num_output_levels,
h_lower_level=h_lower_level,
h_upper_level=h_upper_level,
num_samples=num_samples,
stream=stream,
)

View File

@@ -0,0 +1,277 @@
# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from typing import Callable
import numpy as np
from .. import _bindings
from .. import _cccl_interop as cccl
from .._caching import cache_build_results, cache_with_registered_key_functions
from .._cccl_interop import (
get_value_type,
set_cccl_iterator_state,
to_cccl_value_state,
)
from .._serialization import BUILD_RESULTS, ITER, OP, VALUE, Serializable
from .._utils.protocols import get_data_pointer, get_dtype, validate_and_get_stream
from .._utils.temp_storage_buffer import TempStorageBuffer
from ..determinism import Determinism
from ..op import OpAdapter, make_op_adapter
from ..typing import (
DeviceArrayLike,
GpuStruct,
IteratorBase,
IteratorT,
Operator,
_Struct,
)
class _Reduce(Serializable):
__slots__ = [
"_bound_build_result",
"d_in_cccl",
"d_out_cccl",
"h_init_cccl",
"op_cccl",
"build_results",
"loaded_build_result",
"device_reduce_fn",
]
__serialization_schema__ = (
("d_in_cccl", ITER),
("d_out_cccl", ITER),
("op_cccl", OP),
("h_init_cccl", VALUE),
("build_results", BUILD_RESULTS(_bindings.DeviceReduceBuildResult)),
)
# TODO: constructor shouldn't require concrete `d_in`, `d_out`:
def __init__(
self,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
op: OpAdapter,
h_init: np.ndarray | GpuStruct,
determinism: Determinism,
compute_capability=None,
):
self.d_in_cccl = cccl.to_cccl_input_iter(d_in)
self.d_out_cccl = cccl.to_cccl_output_iter(d_out)
self.h_init_cccl = cccl.to_cccl_value(h_init)
# Compile the op with value types
value_type = get_value_type(h_init)
self.op_cccl = op.compile((value_type, value_type), value_type)
# loaded_build_result / device_reduce_fn are bound lazily on the first
# __call__ (see _bind_device_reduce_fn).
self.build_results, self._bound_build_result = cache_build_results(
_bindings.DeviceReduceBuildResult,
d_in,
d_out,
op,
h_init,
determinism,
compute_capability=compute_capability,
builder=lambda: cccl.build_for_ccs(
_bindings.DeviceReduceBuildResult,
self.d_in_cccl,
self.d_out_cccl,
self.op_cccl,
self.h_init_cccl,
determinism,
compute_capability=compute_capability,
),
)
def _bind_device_reduce_fn(self) -> None:
# Derived from the loaded build result (not serialized); bound at __call__
# once resolve_build_result picks + loads the current device's build result.
if (
Determinism(self.loaded_build_result.determinism)
is Determinism.NOT_GUARANTEED
):
self.device_reduce_fn = self.loaded_build_result.compute_nondeterministic
else:
self.device_reduce_fn = self.loaded_build_result.compute
def __call__(
self,
*,
temp_storage,
d_in,
d_out,
num_items: int,
op: Callable | OpAdapter,
h_init: np.ndarray | GpuStruct,
stream=None,
):
# Select (and lazily load) the current device's build result, then bind the
# derived compute fn from it.
self.loaded_build_result = cccl.resolve_build_result(
self.build_results, self._bound_build_result
)
self._bind_device_reduce_fn()
set_cccl_iterator_state(self.d_in_cccl, d_in)
set_cccl_iterator_state(self.d_out_cccl, d_out)
# Update op state for stateful ops
op_adapter = make_op_adapter(op)
self.op_cccl.state = op_adapter.get_state()
self.h_init_cccl.state = to_cccl_value_state(h_init)
stream_handle = validate_and_get_stream(stream)
if temp_storage is None:
temp_storage_bytes = 0
d_temp_storage = 0
else:
temp_storage_bytes = temp_storage.nbytes
d_temp_storage = get_data_pointer(temp_storage)
temp_storage_bytes = self.device_reduce_fn(
d_temp_storage,
temp_storage_bytes,
self.d_in_cccl,
self.d_out_cccl,
num_items,
self.op_cccl,
self.h_init_cccl,
stream_handle,
)
return temp_storage_bytes
@cache_with_registered_key_functions
def make_reduce_into(
*,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
op: Operator,
h_init: np.ndarray | GpuStruct,
**kwargs,
):
"""Computes a device-wide reduction using the specified binary ``op`` and initial value ``init``.
Example:
Below, ``make_reduce_into`` is used to create a reduction object that can be reused.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/reduction/reduce_object.py
:language: python
:start-after: # example-begin
Args:
d_in: Device array or iterator containing the input sequence of data items
d_out: Device array (of size 1) or iterator that will store the result of the reduction
op: Binary operator to apply.
The signature is ``(T, T) -> T``, where ``T`` is
the data type of the initial value ``h_init``.
init: Numpy array storing initial value of the reduction
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that can be used to perform the reduction
"""
try:
accum_dtype = get_dtype(h_init)
except (AttributeError, TypeError) as e:
raise TypeError(
"Could not determine accumulator dtype from h_init; "
"expected numpy array or object with .dtype"
) from e
# Validate d_in and d_out if they are device arrays (iterators may not expose
# dtype reliably here). Additionally, only require equality of dtypes for
# struct objects; mixed scalar dtypes (e.g. int8 input with int64 output)
# is acceptable
if isinstance(h_init, _Struct):
for arr, name in ((d_in, "input"), (d_out, "output")):
if isinstance(arr, IteratorBase):
continue
dtype = get_dtype(arr)
if dtype != accum_dtype:
raise TypeError(
f"reduce_into dtype mismatch: {name} dtype {dtype} != "
f"accumulator dtype {accum_dtype}. "
f"Ensure {name} elements and h_init have identical dtype to "
"avoid truncation or misinterpretation."
)
op_adapter = make_op_adapter(op)
return _Reduce(
d_in,
d_out,
op_adapter,
h_init,
kwargs.get("determinism", Determinism.RUN_TO_RUN),
compute_capability=kwargs.get("compute_capability"),
)
def reduce_into(
*,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
num_items: int,
op: Operator,
h_init: np.ndarray | GpuStruct,
stream=None,
**kwargs,
):
"""
Performs device-wide reduction.
This function automatically handles temporary storage allocation and execution.
Example:
Below, ``reduce_into`` is used to compute the sum of a sequence of integers.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/reduction/sum_reduction.py
:language: python
:start-after: # example-begin
Args:
d_in: Device array or iterator containing the input sequence of data items
d_out: Device array or iterator to store the result of the reduction
num_items: Number of items to reduce
op: Binary operator to apply.
The signature is ``(T, T) -> T``, where ``T`` is
the data type of the initial value ``h_init``.
h_init: Initial value for the reduction
stream: CUDA stream for the operation (optional)
"""
reducer = make_reduce_into(d_in=d_in, d_out=d_out, op=op, h_init=h_init, **kwargs)
tmp_storage_bytes = reducer(
temp_storage=None,
d_in=d_in,
d_out=d_out,
num_items=num_items,
op=op,
h_init=h_init,
stream=stream,
)
tmp_storage = TempStorageBuffer(tmp_storage_bytes, stream)
reducer(
temp_storage=tmp_storage,
d_in=d_in,
d_out=d_out,
num_items=num_items,
op=op,
h_init=h_init,
stream=stream,
)

View File

@@ -0,0 +1,436 @@
# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from typing import Callable, cast
import numpy as np
from .. import _bindings
from .. import _cccl_interop as cccl
from .._caching import cache_build_results, cache_with_registered_key_functions
from .._cccl_interop import (
get_value_type,
set_cccl_iterator_state,
to_cccl_value_state,
)
from .._serialization import (
BOOL,
BUILD_RESULTS,
CONDITIONAL,
ENUM,
ITER,
OP,
VALUE,
Serializable,
)
from .._utils.protocols import (
get_data_pointer,
is_device_array,
validate_and_get_stream,
)
from .._utils.temp_storage_buffer import TempStorageBuffer
from ..op import OpAdapter, make_op_adapter
from ..typing import DeviceArrayLike, GpuStruct, IteratorT, Operator
def get_init_kind(
init_value: np.ndarray | DeviceArrayLike | GpuStruct | None,
) -> _bindings.InitKind:
match init_value:
case None:
return _bindings.InitKind.NO_INIT
case _ if is_device_array(init_value):
return _bindings.InitKind.FUTURE_VALUE_INIT
case _:
return _bindings.InitKind.VALUE_INIT
class _Scan(Serializable):
__slots__ = [
"_bound_build_result",
"build_results",
"loaded_build_result",
"d_in_cccl",
"d_out_cccl",
"init_value_cccl",
"op_cccl",
"init_kind",
"force_inclusive",
"device_scan_fn",
]
__serialization_schema__ = (
("init_kind", ENUM(_bindings.InitKind)),
("force_inclusive", BOOL),
("d_in_cccl", ITER),
("d_out_cccl", ITER),
("op_cccl", OP),
(
"init_value_cccl",
CONDITIONAL(
"init_kind",
{
_bindings.InitKind.NO_INIT: None,
_bindings.InitKind.FUTURE_VALUE_INIT: ITER,
_bindings.InitKind.VALUE_INIT: VALUE,
},
),
),
("build_results", BUILD_RESULTS(_bindings.DeviceScanBuildResult)),
)
# TODO: constructor shouldn't require concrete `d_in`, `d_out`:
def __init__(
self,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
op: OpAdapter,
init_value: np.ndarray | DeviceArrayLike | GpuStruct | None,
force_inclusive: bool,
compute_capability=None,
):
self.d_in_cccl = cccl.to_cccl_input_iter(d_in)
self.d_out_cccl = cccl.to_cccl_output_iter(d_out)
self.init_kind = get_init_kind(init_value)
self.init_value_cccl: _bindings.Iterator | _bindings.Value | None
match self.init_kind:
case _bindings.InitKind.NO_INIT:
self.init_value_cccl = None
value_type = get_value_type(d_in)
init_value_type_info = self.d_in_cccl.value_type
case _bindings.InitKind.FUTURE_VALUE_INIT:
self.init_value_cccl = cccl.to_cccl_input_iter(
cast(DeviceArrayLike, init_value)
)
value_type = get_value_type(cast(DeviceArrayLike, init_value))
init_value_type_info = self.init_value_cccl.value_type
case _bindings.InitKind.VALUE_INIT:
init_value_typed = cast(np.ndarray | GpuStruct, init_value)
self.init_value_cccl = cccl.to_cccl_value(init_value_typed)
value_type = get_value_type(init_value_typed)
init_value_type_info = self.init_value_cccl.type
self.force_inclusive = force_inclusive
# Compile the op with value types
self.op_cccl = op.compile((value_type, value_type), value_type)
# loaded_build_result / device_scan_fn are bound lazily on the first
# __call__ (see _bind_device_scan_fn).
self.build_results, self._bound_build_result = cache_build_results(
_bindings.DeviceScanBuildResult,
d_in,
d_out,
op,
init_value,
force_inclusive,
self.init_kind,
compute_capability=compute_capability,
builder=lambda: cccl.build_for_ccs(
_bindings.DeviceScanBuildResult,
self.d_in_cccl,
self.d_out_cccl,
self.op_cccl,
init_value_type_info,
force_inclusive,
self.init_kind,
compute_capability=compute_capability,
),
)
def _bind_device_scan_fn(self) -> None:
# Derived from force_inclusive + init_kind, from the loaded build result (not
# serialized as a function); bound at __call__ once resolve_build_result picks
# + loads the current device's build result.
match (self.force_inclusive, self.init_kind):
case (True, _bindings.InitKind.FUTURE_VALUE_INIT):
self.device_scan_fn = (
self.loaded_build_result.compute_inclusive_future_value
)
case (True, _bindings.InitKind.VALUE_INIT):
self.device_scan_fn = self.loaded_build_result.compute_inclusive
case (True, _bindings.InitKind.NO_INIT):
self.device_scan_fn = self.loaded_build_result.compute_inclusive_no_init
case (False, _bindings.InitKind.FUTURE_VALUE_INIT):
self.device_scan_fn = (
self.loaded_build_result.compute_exclusive_future_value
)
case (False, _bindings.InitKind.VALUE_INIT):
self.device_scan_fn = self.loaded_build_result.compute_exclusive
case (False, _bindings.InitKind.NO_INIT):
raise ValueError("Exclusive scan with No init value is not supported")
def __call__(
self,
*,
temp_storage,
d_in,
d_out,
op: Callable | OpAdapter,
init_value: np.ndarray | DeviceArrayLike | GpuStruct | None,
num_items: int,
stream=None,
):
# Select (and lazily load) the current device's build result, then bind the
# derived compute fn from it.
self.loaded_build_result = cccl.resolve_build_result(
self.build_results, self._bound_build_result
)
self._bind_device_scan_fn()
set_cccl_iterator_state(self.d_in_cccl, d_in)
set_cccl_iterator_state(self.d_out_cccl, d_out)
# Update op state for stateful ops
op_adapter = make_op_adapter(op)
self.op_cccl.state = op_adapter.get_state()
match self.init_kind:
case _bindings.InitKind.FUTURE_VALUE_INIT:
# We know that the init_value_cccl is an Iterator, so this cast
# tells MyPy what the actual type is. cast() is a no-op at runtime,
# which makes it better than isinstance() since this is a hot path
# and we have to minimize the work we do prior to calling the
# kernel.
self.init_value_cccl = cast(_bindings.Iterator, self.init_value_cccl)
set_cccl_iterator_state(self.init_value_cccl, init_value)
case _bindings.InitKind.VALUE_INIT:
self.init_value_cccl = cast(_bindings.Value, self.init_value_cccl)
self.init_value_cccl.state = to_cccl_value_state(
cast(np.ndarray | GpuStruct, init_value)
)
stream_handle = validate_and_get_stream(stream)
if temp_storage is None:
temp_storage_bytes = 0
d_temp_storage = 0
else:
temp_storage_bytes = temp_storage.nbytes
d_temp_storage = get_data_pointer(temp_storage)
temp_storage_bytes = self.device_scan_fn(
d_temp_storage,
temp_storage_bytes,
self.d_in_cccl,
self.d_out_cccl,
num_items,
self.op_cccl,
self.init_value_cccl,
stream_handle,
)
return temp_storage_bytes
# TODO Figure out `sum` without operator and initial value
# TODO Accept stream
@cache_with_registered_key_functions
def make_exclusive_scan(
*,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
op: Operator,
init_value: np.ndarray | DeviceArrayLike | GpuStruct,
compute_capability=None,
):
"""Computes a device-wide scan using the specified binary ``op`` and initial value ``init``.
Example:
Below, ``make_exclusive_scan`` is used to create an exclusive scan object that can be reused.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/scan/exclusive_scan_object.py
:language: python
:start-after: # example-begin
Args:
d_in: Device array or iterator containing the input sequence of data items
d_out: Device array that will store the result of the scan
op: Binary scan operator.
The signature is ``(T, T) -> T``, where ``T`` is the data type of
the initial value ``init_value``.
init_value: Numpy array, device array, or GPU struct storing initial value of the scan
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that can be used to perform the scan
"""
op_adapter = make_op_adapter(op)
return _Scan(
d_in,
d_out,
op_adapter,
init_value,
False,
compute_capability=compute_capability,
)
def exclusive_scan(
*,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
op: Operator,
init_value: np.ndarray | DeviceArrayLike | GpuStruct,
num_items: int,
stream=None,
):
"""
Performs device-wide exclusive scan.
This function automatically handles temporary storage allocation and execution.
Example:
Below, ``exclusive_scan`` is used to compute an exclusive scan with max operation.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/scan/exclusive_scan_max.py
:language: python
:start-after: # example-begin
Args:
d_in: Device array or iterator containing the input sequence of data items
d_out: Device array or iterator to store the result of the scan
op: Binary scan operator.
The signature is ``(T, T) -> T``, where ``T`` is the data type of
the initial value ``init_value``.
init_value: Initial value for the scan
num_items: Number of items to scan
stream: CUDA stream for the operation (optional)
"""
scanner = make_exclusive_scan(d_in=d_in, d_out=d_out, op=op, init_value=init_value)
tmp_storage_bytes = scanner(
temp_storage=None,
d_in=d_in,
d_out=d_out,
op=op,
init_value=init_value,
num_items=num_items,
stream=stream,
)
tmp_storage = TempStorageBuffer(tmp_storage_bytes, stream)
scanner(
temp_storage=tmp_storage,
d_in=d_in,
d_out=d_out,
op=op,
init_value=init_value,
num_items=num_items,
stream=stream,
)
# TODO Figure out `sum` without operator and initial value
# TODO Accept stream
@cache_with_registered_key_functions
def make_inclusive_scan(
*,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
op: Operator,
init_value: np.ndarray | DeviceArrayLike | GpuStruct | None = None,
compute_capability=None,
):
"""Computes a device-wide scan using the specified binary ``op`` and initial value ``init``.
Example:
Below, ``make_inclusive_scan`` is used to create an inclusive scan object that can be reused.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/scan/inclusive_scan_object.py
:language: python
:start-after: # example-begin
Args:
d_in: Device array or iterator containing the input sequence of data items
d_out: Device array that will store the result of the scan
op: Binary scan operator.
The signature is ``(T, T) -> T``, where ``T`` is the data type of
the initial value ``init_value``.
init_value: Numpy array, device array, or GPU struct storing initial value of the scan, or None for no initial value
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that can be used to perform the scan
"""
op_adapter = make_op_adapter(op)
return _Scan(
d_in,
d_out,
op_adapter,
init_value,
True,
compute_capability=compute_capability,
)
def inclusive_scan(
*,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
op: Operator,
init_value: np.ndarray | DeviceArrayLike | GpuStruct | None = None,
num_items: int,
stream=None,
):
"""
Performs device-wide inclusive scan.
This function automatically handles temporary storage allocation and execution.
Example:
Below, ``inclusive_scan`` is used to compute an inclusive scan (prefix sum).
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/scan/inclusive_scan_custom.py
:language: python
:start-after: # example-begin
Args:
d_in: Device array or iterator containing the input sequence of data items
d_out: Device array or iterator to store the result of the scan
op: Binary scan operator.
The signature is ``(T, T) -> T``, where ``T`` is the data type of
the initial value ``init_value``.
init_value: Initial value for the scan
num_items: Number of items to scan
stream: CUDA stream for the operation (optional)
"""
scanner = make_inclusive_scan(d_in=d_in, d_out=d_out, op=op, init_value=init_value)
tmp_storage_bytes = scanner(
temp_storage=None,
d_in=d_in,
d_out=d_out,
op=op,
init_value=init_value,
num_items=num_items,
stream=stream,
)
tmp_storage = TempStorageBuffer(tmp_storage_bytes, stream)
scanner(
temp_storage=tmp_storage,
d_in=d_in,
d_out=d_out,
op=op,
init_value=init_value,
num_items=num_items,
stream=stream,
)

View File

@@ -0,0 +1,293 @@
# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from typing import Callable
import numpy as np
from .. import _bindings
from .. import _cccl_interop as cccl
from .._caching import cache_build_results, cache_with_registered_key_functions
from .._cccl_interop import (
get_value_type,
set_cccl_iterator_state,
to_cccl_value_state,
)
from .._serialization import BUILD_RESULTS, ITER, OP, VALUE, Serializable
from .._utils.protocols import (
get_data_pointer,
validate_and_get_stream,
)
from .._utils.temp_storage_buffer import TempStorageBuffer
from ..op import OpAdapter, make_op_adapter
from ..typing import DeviceArrayLike, GpuStruct, IteratorT, Operator
class _SegmentedReduce(Serializable):
__slots__ = [
"_bound_build_result",
"build_results",
"loaded_build_result",
"d_in_cccl",
"d_out_cccl",
"start_offsets_in_cccl",
"end_offsets_in_cccl",
"h_init_cccl",
"op_cccl",
]
__serialization_schema__ = (
("d_in_cccl", ITER),
("d_out_cccl", ITER),
("start_offsets_in_cccl", ITER),
("end_offsets_in_cccl", ITER),
("h_init_cccl", VALUE),
("op_cccl", OP),
("build_results", BUILD_RESULTS(_bindings.DeviceSegmentedReduceBuildResult)),
)
def __init__(
self,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
start_offsets_in: DeviceArrayLike | IteratorT,
end_offsets_in: DeviceArrayLike | IteratorT,
op: OpAdapter,
h_init: np.ndarray | GpuStruct,
compute_capability=None,
):
self.d_in_cccl = cccl.to_cccl_input_iter(d_in)
self.d_out_cccl = cccl.to_cccl_output_iter(d_out)
self.start_offsets_in_cccl = cccl.to_cccl_input_iter(start_offsets_in)
self.end_offsets_in_cccl = cccl.to_cccl_input_iter(end_offsets_in)
self.h_init_cccl = cccl.to_cccl_value(h_init)
# Compile the op with value types
value_type = get_value_type(h_init)
self.op_cccl = op.compile((value_type, value_type), value_type)
self.build_results, self._bound_build_result = cache_build_results(
_bindings.DeviceSegmentedReduceBuildResult,
d_in,
d_out,
start_offsets_in,
end_offsets_in,
op,
h_init,
compute_capability=compute_capability,
builder=lambda: cccl.build_for_ccs(
_bindings.DeviceSegmentedReduceBuildResult,
self.d_in_cccl,
self.d_out_cccl,
self.start_offsets_in_cccl,
self.end_offsets_in_cccl,
self.op_cccl,
self.h_init_cccl,
compute_capability=compute_capability,
),
)
def __call__(
self,
*,
temp_storage,
d_in,
d_out,
num_segments: int,
start_offsets_in,
end_offsets_in,
op: Callable | OpAdapter,
h_init,
max_segment_size: int | None = None,
stream=None,
):
# Select (and lazily load) the build result for the current device.
self.loaded_build_result = cccl.resolve_build_result(
self.build_results, self._bound_build_result
)
if num_segments > np.iinfo(np.int32).max:
raise RuntimeError(
"Segmented sort does not currently support more than 2^31-1 segments."
)
if max_segment_size is None:
max_segment_size = 0 # CCCL.c treats 0 as "not specified"
if max_segment_size > 0:
try:
from .._build_info import USING_V2 # type: ignore[import-not-found]
except ImportError:
USING_V2 = False
if USING_V2:
import warnings
warnings.warn(
"max_segment_size is not used by the v2 backend and will be ignored",
stacklevel=4,
)
set_cccl_iterator_state(self.d_in_cccl, d_in)
set_cccl_iterator_state(self.d_out_cccl, d_out)
set_cccl_iterator_state(self.start_offsets_in_cccl, start_offsets_in)
set_cccl_iterator_state(self.end_offsets_in_cccl, end_offsets_in)
op_adapter = make_op_adapter(op)
self.op_cccl.state = op_adapter.get_state()
self.h_init_cccl.state = to_cccl_value_state(h_init)
stream_handle = validate_and_get_stream(stream)
if temp_storage is None:
temp_storage_bytes = 0
d_temp_storage = 0
else:
temp_storage_bytes = temp_storage.nbytes
d_temp_storage = get_data_pointer(temp_storage)
temp_storage_bytes = self.loaded_build_result.compute(
d_temp_storage,
temp_storage_bytes,
self.d_in_cccl,
self.d_out_cccl,
num_segments,
self.start_offsets_in_cccl,
self.end_offsets_in_cccl,
self.op_cccl,
self.h_init_cccl,
max_segment_size,
stream_handle,
)
return temp_storage_bytes
@cache_with_registered_key_functions
def make_segmented_reduce(
*,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
start_offsets_in: DeviceArrayLike | IteratorT,
end_offsets_in: DeviceArrayLike | IteratorT,
op: Operator,
h_init: np.ndarray | GpuStruct,
compute_capability=None,
):
"""Computes a device-wide segmented reduction using the specified binary ``op`` and initial value ``init``.
Example:
Below, ``make_segmented_reduce`` is used to create a segmented reduction object that can be reused.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/segmented/segmented_reduce_object.py
:language: python
:start-after: # example-begin
Args:
d_in: Device array or iterator containing the input sequence of data items
d_out: Device array that will store the result of the reduction
start_offsets_in: Device array or iterator containing offsets to start of segments
end_offsets_in: Device array or iterator containing offsets to end of segments
op: Binary operator to apply.
The signature is ``(T, T) -> T``, where ``T`` is
the data type of the initial value ``h_init``.
init: Numpy array storing initial value of the reduction
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that can be used to perform the reduction
"""
op_adapter = make_op_adapter(op)
return _SegmentedReduce(
d_in,
d_out,
start_offsets_in,
end_offsets_in,
op_adapter,
h_init,
compute_capability=compute_capability,
)
def segmented_reduce(
*,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
num_segments: int,
start_offsets_in: DeviceArrayLike | IteratorT,
end_offsets_in: DeviceArrayLike | IteratorT,
op: Operator,
h_init: np.ndarray | GpuStruct,
max_segment_size: int | None = None,
stream=None,
):
"""
Performs device-wide segmented reduction.
This function automatically handles temporary storage allocation and execution.
Example:
Below, ``segmented_reduce`` is used to compute the minimum value of segments in a sequence of integers.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/segmented/segmented_reduce_basic.py
:language: python
:start-after: # example-begin
Args:
d_in: Device array or iterator containing the input sequence of data items
d_out: Device array to store the result of the reduction for each segment
num_segments: Number of segments to reduce
start_offsets_in: Device array or iterator containing the sequence of beginning offsets
end_offsets_in: Device array or iterator containing the sequence of ending offsets
op: Binary operator to apply.
The signature is ``(T, T) -> T``, where ``T`` is
the data type of the initial value ``h_init``.
h_init: Initial value for the reduction
max_segment_size: The number of elements in the largest segment (optional)
If provided, this information is used to dispatch to the
optimal kernel for best performance.
stream: CUDA stream for the operation (optional)
"""
reducer = make_segmented_reduce(
d_in=d_in,
d_out=d_out,
start_offsets_in=start_offsets_in,
end_offsets_in=end_offsets_in,
op=op,
h_init=h_init,
)
tmp_storage_bytes = reducer(
temp_storage=None,
d_in=d_in,
d_out=d_out,
num_segments=num_segments,
start_offsets_in=start_offsets_in,
end_offsets_in=end_offsets_in,
op=op,
h_init=h_init,
max_segment_size=max_segment_size,
stream=stream,
)
tmp_storage = TempStorageBuffer(tmp_storage_bytes, stream)
reducer(
temp_storage=tmp_storage,
d_in=d_in,
d_out=d_out,
num_segments=num_segments,
start_offsets_in=start_offsets_in,
end_offsets_in=end_offsets_in,
op=op,
h_init=h_init,
max_segment_size=max_segment_size,
stream=stream,
)

View File

@@ -0,0 +1,239 @@
# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from functools import cache
from .._caching import cache_with_registered_key_functions
from .._cpp_compile import compile_cpp_op_code
from .._serialization import NESTED, Serializable
from .._utils.temp_storage_buffer import TempStorageBuffer
from ..iterators import DiscardIterator
from ..op import OpAdapter, RawOp, make_op_adapter
from ..typing import DeviceArrayLike, IteratorT, Operator
from ._three_way_partition import _ThreeWayPartition, make_three_way_partition
@cache
def _always_false_op(_target_cc):
# ``_target_cc`` (the build's get_target_cc()) is part of the cache key so the
# predicate's LTO-IR is recompiled per target arch: this RawOp is linked into
# the three-way-partition build, and nvJitLink rejects a newer-arch input in
# an older-arch result. Without the key, the first build's arch would leak
# into every later build (module-global cache). compile_cpp_op_code() reads
# the same target internally; the arg only distinguishes cache entries.
source = """
extern "C" __device__ void always_false(void*, void* result) {{
*static_cast<bool*>(result) = false;
}}
"""
code = compile_cpp_op_code(source)
return RawOp(ltoir=code, name="always_false")
def _get_always_false_op():
"""The always-false predicate compiled for the current build's target cc."""
from .._target_cc import get_target_cc
return _always_false_op(get_target_cc())
class _Select(Serializable):
__slots__ = ["_bound_build_result", "partitioner", "always_false_op", "_discards"]
__serialization_schema__ = (("partitioner", NESTED(_ThreeWayPartition)),)
def __init__(
self,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
d_num_selected_out: DeviceArrayLike,
cond: OpAdapter,
compute_capability=None,
):
self.always_false_op = _get_always_false_op()
d_second, d_unselected = self._discard_iterators(d_out)
self.partitioner = make_three_way_partition(
d_in=d_in,
d_first_part_out=d_out,
d_second_part_out=d_second,
d_unselected_out=d_unselected,
d_num_selected_out=d_num_selected_out,
select_first_part_op=cond,
select_second_part_op=self.always_false_op,
compute_capability=compute_capability,
)
def _discard_iterators(self, d_out):
# The second/unselected outputs are discarded; their iterators depend
# only on d_out's type, so build the pair once and cache it. Bound
# lazily (on first construction or first call) so a deserialized
# _Select, which has no construction d_out, builds them on first use.
try:
return self._discards
except AttributeError:
self._discards = (DiscardIterator(d_out), DiscardIterator(d_out))
return self._discards
def _after_deserialize(self) -> None:
# always_false_op (the always-false second predicate) is not serialized.
# Its compiled LTO-IR is already baked into the (serialized) three-way
# partition build result, and __call__ reads only this op's runtime state
# (which is empty — the predicate is stateless). So reconstruct an
# empty-state stand-in WITHOUT compiling: deserialize() must neither
# recompile nor require a GPU, and calling _get_always_false_op() here
# would do both (cold cache -> compile_cpp_op_code -> Device() fallback).
self.always_false_op = RawOp(ltoir=b"", name="always_false")
def __call__(
self,
*,
temp_storage,
d_in,
d_out,
d_num_selected_out,
cond,
num_items: int,
stream=None,
):
d_second, d_unselected = self._discard_iterators(d_out)
return self.partitioner(
temp_storage=temp_storage,
d_in=d_in,
d_first_part_out=d_out,
d_second_part_out=d_second,
d_unselected_out=d_unselected,
d_num_selected_out=d_num_selected_out,
select_first_part_op=make_op_adapter(cond),
select_second_part_op=self.always_false_op,
num_items=num_items,
stream=stream,
)
@cache_with_registered_key_functions
def make_select(
*,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
d_num_selected_out: DeviceArrayLike,
cond: Operator,
compute_capability=None,
):
"""
Create a select object that can be called to select elements matching a condition.
This is the object-oriented API that allows explicit control over temporary
storage allocation. For simpler usage, consider using :func:`select`.
Example:
Below, ``make_select`` is used to create a select object that can be reused.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/select/select_object.py
:language: python
:start-after: # example-begin
Args:
d_in: Device array or iterator containing the input sequence of data items.
d_out: Device array or iterator to store the selected output items.
d_num_selected_out: Device array to store the number of items that passed the selection.
The count is stored in ``d_num_selected_out[0]``.
cond: Selection condition (predicate).
The signature is ``(T) -> uint8``, where ``T`` is the input data type.
Returns 1 (selected) or 0 (not selected).
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that performs the selection operation.
"""
cond_adapter = make_op_adapter(cond)
# Note: _Select internally calls make_three_way_partition which will
# normalize the cond. But we've already normalized it, so the Op
# will be passed through make_op unchanged.
return _Select(
d_in,
d_out,
d_num_selected_out,
cond_adapter,
compute_capability=compute_capability,
)
def select(
*,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
d_num_selected_out: DeviceArrayLike,
cond: Operator,
num_items: int,
stream=None,
):
"""
Performs device-wide selection of elements based on a condition.
Given an input sequence, this function selects all elements for which the condition
function ``cond`` returns true (non-zero) and writes them to the output in a
compacted form. The number of selected elements is written to ``d_num_selected_out[0]``.
This function automatically handles temporary storage allocation and execution.
The ``cond`` function can reference device arrays as globals or closures - they will
be automatically captured as state arrays, enabling stateful operations like counting.
Example:
Below, ``select`` is used to select even numbers from an input array:
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/select/select_basic.py
:language: python
:start-after: # example-begin
You can also use iterators for more complex selection patterns:
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/select/select_with_iterator.py
:language: python
:start-after: # example-begin
Args:
d_in: Device array or iterator containing the input sequence of data items.
d_out: Device array or iterator to store the selected output items.
d_num_selected_out: Device array to store the number of items that passed the selection.
The count is stored in ``d_num_selected_out[0]``.
cond: Selection condition (predicate).
The signature is ``(T) -> uint8``, where ``T`` is the input data type.
Returns 1 (selected) or 0 (not selected).
Can reference device arrays as globals/closures - they will be automatically captured.
num_items: Number of items in the input sequence.
stream: CUDA stream to use for the operation (optional).
"""
# Create adapter to support stateful ops
cond_adapter = make_op_adapter(cond)
selector = make_select(
d_in=d_in, d_out=d_out, d_num_selected_out=d_num_selected_out, cond=cond_adapter
)
tmp_storage_bytes = selector(
temp_storage=None,
d_in=d_in,
d_out=d_out,
d_num_selected_out=d_num_selected_out,
cond=cond_adapter,
num_items=num_items,
stream=stream,
)
tmp_storage = TempStorageBuffer(tmp_storage_bytes, stream)
selector(
temp_storage=tmp_storage,
d_in=d_in,
d_out=d_out,
d_num_selected_out=d_num_selected_out,
cond=cond_adapter,
num_items=num_items,
stream=stream,
)

View File

@@ -0,0 +1,25 @@
# Copyright (c) 2024, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from ._merge_sort import make_merge_sort as make_merge_sort
from ._merge_sort import merge_sort as merge_sort
from ._radix_sort import make_radix_sort as make_radix_sort
from ._radix_sort import radix_sort as radix_sort
from ._segmented_sort import make_segmented_sort as make_segmented_sort
from ._segmented_sort import segmented_sort as segmented_sort
from ._sort_common import DoubleBuffer, SortOrder
__all__ = [
"make_merge_sort",
"merge_sort",
"make_radix_sort",
"radix_sort",
"make_segmented_sort",
"segmented_sort",
"DoubleBuffer",
"SortOrder",
]

View File

@@ -0,0 +1,261 @@
# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from ... import _bindings, types
from ... import _cccl_interop as cccl
from ..._caching import cache_build_results, cache_with_registered_key_functions
from ..._cccl_interop import set_cccl_iterator_state
from ..._serialization import BUILD_RESULTS, ITER, OP, Serializable
from ..._utils.protocols import (
get_data_pointer,
validate_and_get_stream,
)
from ..._utils.temp_storage_buffer import TempStorageBuffer
from ...op import OpAdapter, make_op_adapter
from ...typing import DeviceArrayLike, IteratorT, Operator
class _MergeSort(Serializable):
__slots__ = [
"_bound_build_result",
"d_in_keys_cccl",
"d_in_values_cccl",
"d_out_keys_cccl",
"d_out_values_cccl",
"op_adapter",
"op_cccl",
"build_results",
"loaded_build_result",
]
__serialization_schema__ = (
("d_in_keys_cccl", ITER),
("d_in_values_cccl", ITER),
("d_out_keys_cccl", ITER),
("d_out_values_cccl", ITER),
("op_cccl", OP),
("build_results", BUILD_RESULTS(_bindings.DeviceMergeSortBuildResult)),
)
def __init__(
self,
d_in_keys: DeviceArrayLike | IteratorT,
d_in_values: DeviceArrayLike | IteratorT | None,
d_out_keys: DeviceArrayLike,
d_out_values: DeviceArrayLike | None,
op: OpAdapter,
compute_capability=None,
):
present_in_values = d_in_values is not None
present_out_values = d_out_values is not None
assert present_in_values == present_out_values
self.d_in_keys_cccl = cccl.to_cccl_input_iter(d_in_keys)
self.d_in_values_cccl = cccl.to_cccl_input_iter(d_in_values)
self.d_out_keys_cccl = cccl.to_cccl_output_iter(d_out_keys)
self.d_out_values_cccl = cccl.to_cccl_output_iter(d_out_values)
self.op_adapter = op
# Compile the op - merge_sort expects int8 return (comparison)
value_type = cccl.get_value_type(d_in_keys)
self.op_cccl = op.compile((value_type, value_type), types.int8)
self.build_results, self._bound_build_result = cache_build_results(
_bindings.DeviceMergeSortBuildResult,
d_in_keys,
d_in_values,
d_out_keys,
d_out_values,
op,
compute_capability=compute_capability,
builder=lambda: cccl.build_for_ccs(
_bindings.DeviceMergeSortBuildResult,
self.d_in_keys_cccl,
self.d_in_values_cccl,
self.d_out_keys_cccl,
self.d_out_values_cccl,
self.op_cccl,
compute_capability=compute_capability,
),
)
def __call__(
self,
*,
temp_storage,
d_in_keys: DeviceArrayLike | IteratorT,
d_in_values: DeviceArrayLike | IteratorT | None,
d_out_keys: DeviceArrayLike,
d_out_values: DeviceArrayLike | None,
num_items: int,
op: Operator,
stream=None,
):
# Select (and lazily load) the build result for the current device.
self.loaded_build_result = cccl.resolve_build_result(
self.build_results, self._bound_build_result
)
present_in_values = d_in_values is not None
present_out_values = d_out_values is not None
assert present_in_values == present_out_values
set_cccl_iterator_state(self.d_in_keys_cccl, d_in_keys)
if present_in_values:
set_cccl_iterator_state(self.d_in_values_cccl, d_in_values)
set_cccl_iterator_state(self.d_out_keys_cccl, d_out_keys)
if present_out_values:
set_cccl_iterator_state(self.d_out_values_cccl, d_out_values)
op_adapter = make_op_adapter(op)
self.op_cccl.state = op_adapter.get_state()
stream_handle = validate_and_get_stream(stream)
if temp_storage is None:
temp_storage_bytes = 0
d_temp_storage = 0
else:
temp_storage_bytes = temp_storage.nbytes
# Note: this is slightly slower, but supports all ndarray-like objects as long as they support CAI
# TODO: switch to use gpumemoryview once it's ready
d_temp_storage = get_data_pointer(temp_storage)
temp_storage_bytes = self.loaded_build_result.compute(
d_temp_storage,
temp_storage_bytes,
self.d_in_keys_cccl,
self.d_in_values_cccl,
self.d_out_keys_cccl,
self.d_out_values_cccl,
num_items,
self.op_cccl,
stream_handle,
)
return temp_storage_bytes
@cache_with_registered_key_functions
def make_merge_sort(
*,
d_in_keys: DeviceArrayLike | IteratorT,
d_in_values: DeviceArrayLike | IteratorT | None = None,
d_out_keys: DeviceArrayLike,
d_out_values: DeviceArrayLike | None = None,
op: Operator,
compute_capability=None,
):
"""Implements a device-wide merge sort using ``d_in_keys`` and the comparison operator ``op``.
Example:
Below, ``make_merge_sort`` is used to create a merge sort object that can be reused.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/sort/merge_sort_object.py
:language: python
:start-after: # example-begin
Args:
d_in_keys: Device array or iterator containing the input keys to be sorted
d_in_values: Optional device array or iterator that contains each key's corresponding value
d_out_keys: Device array to store the sorted keys
d_out_values: Device array to store the sorted values
op: The comparison operator for sorting. The signature is ``(T, T) -> int8``, where ``T`` is the input data type. See notes below.
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that can be used to perform the merge sort
.. important::
The provided comparison operator must follow `strict weak ordering <https://en.cppreference.com/w/cpp/concepts/strict_weak_order.html>`_
semantics. For example, the comparator ``lambda lhs, rhs: lhs < rhs`` follows strict weak ordering, but the comparator
``lambda lhs, rhs: rhs >= lhs`` does not, because it is reflexive: ``r(x, x) == True``. Providing a comparator that does not
follow the required semantics can lead to incorrect results, silent memory corruption, or crashes.
"""
op_adapter = make_op_adapter(op)
return _MergeSort(
d_in_keys,
d_in_values,
d_out_keys,
d_out_values,
op_adapter,
compute_capability=compute_capability,
)
def merge_sort(
*,
d_in_keys: DeviceArrayLike | IteratorT,
d_in_values: DeviceArrayLike | IteratorT | None = None,
d_out_keys: DeviceArrayLike,
d_out_values: DeviceArrayLike | None = None,
num_items: int,
op: Operator,
stream=None,
):
"""
Performs device-wide merge sort.
This function automatically handles temporary storage allocation and execution.
Example:
Below, ``merge_sort`` is used to sort a sequence of keys inplace. It also rearranges the values according to the keys' order.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/sort/merge_sort_basic.py
:language: python
:start-after: # example-begin
Args:
d_in_keys: Device array or iterator containing the input sequence of keys
d_in_values: Device array or iterator containing the input sequence of values (optional)
d_out_keys: Device array to store the sorted keys
d_out_values: Device array to store the sorted values (optional)
num_items: Number of items to sort
op: The comparison operator for sorting. The signature is ``(T, T) -> int8``, where ``T`` is the input data type. See notes below.
stream: CUDA stream for the operation (optional)
.. important::
The provided comparison operator must follow `strict weak ordering <https://en.cppreference.com/w/cpp/concepts/strict_weak_order.html>`_
semantics. For example, the comparator ``lambda lhs, rhs: lhs < rhs`` follows strict weak ordering, but the comparator
``lambda lhs, rhs: rhs >= lhs`` does not, because it is reflexive: ``r(x, x) == True``. Providing a comparator that does not
follow the required semantics can lead to incorrect results, silent memory corruption, or crashes.
"""
sorter = make_merge_sort(
d_in_keys=d_in_keys,
d_in_values=d_in_values,
d_out_keys=d_out_keys,
d_out_values=d_out_values,
op=op,
)
tmp_storage_bytes = sorter(
temp_storage=None,
d_in_keys=d_in_keys,
d_in_values=d_in_values,
d_out_keys=d_out_keys,
d_out_values=d_out_values,
num_items=num_items,
op=op,
stream=stream,
)
tmp_storage = TempStorageBuffer(tmp_storage_bytes, stream)
sorter(
temp_storage=tmp_storage,
d_in_keys=d_in_keys,
d_in_values=d_in_values,
d_out_keys=d_out_keys,
d_out_values=d_out_values,
num_items=num_items,
op=op,
stream=stream,
)

View File

@@ -0,0 +1,289 @@
# Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from ... import _bindings
from ... import _cccl_interop as cccl
from ..._caching import cache_build_results, cache_with_registered_key_functions
from ..._cccl_interop import set_cccl_iterator_state
from ..._serialization import BUILD_RESULTS, ITER, OP, Serializable
from ..._utils.protocols import (
get_data_pointer,
get_dtype,
validate_and_get_stream,
)
from ..._utils.temp_storage_buffer import TempStorageBuffer
from ...typing import DeviceArrayLike
from ._sort_common import DoubleBuffer, SortOrder, _get_arrays
class _RadixSort(Serializable):
__slots__ = [
"_bound_build_result",
"d_in_keys_cccl",
"d_out_keys_cccl",
"d_in_values_cccl",
"d_out_values_cccl",
"decomposer_op",
"build_results",
"loaded_build_result",
]
__serialization_schema__ = (
("d_in_keys_cccl", ITER),
("d_out_keys_cccl", ITER),
("d_in_values_cccl", ITER),
("d_out_values_cccl", ITER),
("decomposer_op", OP),
("build_results", BUILD_RESULTS(_bindings.DeviceRadixSortBuildResult)),
)
def __init__(
self,
d_in_keys: DeviceArrayLike | DoubleBuffer,
d_out_keys: DeviceArrayLike | None,
d_in_values: DeviceArrayLike | DoubleBuffer | None,
d_out_values: DeviceArrayLike | None,
order: SortOrder,
compute_capability=None,
):
d_in_keys_array, d_out_keys_array, d_in_values_array, d_out_values_array = (
_get_arrays(d_in_keys, d_out_keys, d_in_values, d_out_values)
)
self.d_in_keys_cccl = cccl.to_cccl_input_iter(d_in_keys_array)
self.d_out_keys_cccl = cccl.to_cccl_output_iter(d_out_keys_array)
self.d_in_values_cccl = cccl.to_cccl_input_iter(d_in_values_array)
self.d_out_values_cccl = cccl.to_cccl_output_iter(d_out_values_array)
# TODO: decomposer op is not supported for now
self.decomposer_op = cccl.Op(
name="",
operator_type=cccl.OpKind.STATELESS,
ltoir=b"",
state_alignment=1,
state=b"", # explicit empty bytes so the serialize path is byte-safe
)
decomposer_return_type = "".encode("utf-8")
build_order = (
_bindings.SortOrder.ASCENDING
if order is SortOrder.ASCENDING
else _bindings.SortOrder.DESCENDING
)
self.build_results, self._bound_build_result = cache_build_results(
_bindings.DeviceRadixSortBuildResult,
d_in_keys,
d_out_keys,
d_in_values,
d_out_values,
order,
compute_capability=compute_capability,
builder=lambda: cccl.build_for_ccs(
_bindings.DeviceRadixSortBuildResult,
build_order,
self.d_in_keys_cccl,
self.d_in_values_cccl,
self.decomposer_op,
decomposer_return_type,
compute_capability=compute_capability,
),
)
def __call__(
self,
*,
temp_storage,
d_in_keys: DeviceArrayLike | DoubleBuffer,
d_out_keys: DeviceArrayLike | None,
d_in_values: DeviceArrayLike | DoubleBuffer | None,
d_out_values: DeviceArrayLike | None,
num_items: int,
begin_bit: int | None = None,
end_bit: int | None = None,
stream=None,
):
# Select (and lazily load) the build result for the current device.
self.loaded_build_result = cccl.resolve_build_result(
self.build_results, self._bound_build_result
)
d_in_keys_array, d_out_keys_array, d_in_values_array, d_out_values_array = (
_get_arrays(d_in_keys, d_out_keys, d_in_values, d_out_values)
)
set_cccl_iterator_state(self.d_in_keys_cccl, d_in_keys_array)
if d_in_values_array is not None:
set_cccl_iterator_state(self.d_in_values_cccl, d_in_values_array)
set_cccl_iterator_state(self.d_out_keys_cccl, d_out_keys_array)
if d_out_values_array is not None:
set_cccl_iterator_state(self.d_out_values_cccl, d_out_values_array)
is_overwrite_okay = isinstance(d_in_keys, DoubleBuffer)
stream_handle = validate_and_get_stream(stream)
if temp_storage is None:
temp_storage_bytes = 0
d_temp_storage = 0
else:
temp_storage_bytes = temp_storage.nbytes
# Note: this is slightly slower, but supports all ndarray-like objects as long as they support CAI
# TODO: switch to use gpumemoryview once it's ready
d_temp_storage = get_data_pointer(temp_storage)
if begin_bit is None:
begin_bit = 0
if end_bit is None:
key_type = get_dtype(d_in_keys_array)
end_bit = key_type.itemsize * 8
selector = -1
temp_storage_bytes, selector = self.loaded_build_result.compute(
d_temp_storage,
temp_storage_bytes,
self.d_in_keys_cccl,
self.d_out_keys_cccl,
self.d_in_values_cccl,
self.d_out_values_cccl,
self.decomposer_op,
num_items,
begin_bit,
end_bit,
is_overwrite_okay,
selector,
stream_handle,
)
if is_overwrite_okay and temp_storage is not None:
assert selector in (0, 1)
assert isinstance(d_in_keys, DoubleBuffer)
d_in_keys.selector = selector
if d_in_values is not None:
assert isinstance(d_in_values, DoubleBuffer)
d_in_values.selector = selector
return temp_storage_bytes
@cache_with_registered_key_functions
def make_radix_sort(
*,
d_in_keys: DeviceArrayLike | DoubleBuffer,
d_out_keys: DeviceArrayLike | None,
d_in_values: DeviceArrayLike | DoubleBuffer | None,
d_out_values: DeviceArrayLike | None,
order: SortOrder,
compute_capability=None,
):
"""Implements a device-wide radix sort using ``d_in_keys`` in the requested order.
Example:
Below, ``make_radix_sort`` is used to create a radix sort object that can be reused.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/sort/radix_sort_object.py
:language: python
:start-after: # example-begin
Args:
d_in_keys: Device array or DoubleBuffer containing the input keys to be sorted
d_out_keys: Device array to store the sorted keys
d_in_values: Optional Device array or DoubleBuffer containing the input keys to be sorted
d_out_values: Device array to store the sorted values
op: Callable representing the comparison operator
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that can be used to perform the radix sort
"""
return _RadixSort(
d_in_keys,
d_out_keys,
d_in_values,
d_out_values,
order,
compute_capability=compute_capability,
)
def radix_sort(
*,
d_in_keys: DeviceArrayLike | DoubleBuffer,
d_out_keys: DeviceArrayLike | None,
d_in_values: DeviceArrayLike | DoubleBuffer | None = None,
d_out_values: DeviceArrayLike | None = None,
num_items: int,
order: SortOrder,
begin_bit: int | None = None,
end_bit: int | None = None,
stream=None,
):
"""
Performs device-wide radix sort.
This function automatically handles temporary storage allocation and execution.
Example:
Below, ``radix_sort`` is used to sort a sequence of keys. It also rearranges the values according to the keys' order.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/sort/radix_sort_basic.py
:language: python
:start-after: # example-begin
In the following example, ``radix_sort`` is used to sort a sequence of keys with a ``DoubleBuffer`` for reduced temporary storage.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/sort/radix_sort_buffer.py
:language: python
:start-after: # example-begin
Args:
d_in_keys: Device array or DoubleBuffer containing the input sequence of keys
d_out_keys: Device array to store the sorted keys (optional)
d_in_values: Device array or DoubleBuffer containing the input sequence of values (optional)
d_out_values: Device array to store the sorted values (optional)
num_items: Number of items to sort
order: Sort order (ascending or descending)
begin_bit: Beginning bit position for comparison (optional)
end_bit: Ending bit position for comparison (optional)
stream: CUDA stream for the operation (optional)
"""
sorter = make_radix_sort(
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=d_in_values,
d_out_values=d_out_values,
order=order,
)
tmp_storage_bytes = sorter(
temp_storage=None,
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=d_in_values,
d_out_values=d_out_values,
num_items=num_items,
begin_bit=begin_bit,
end_bit=end_bit,
stream=stream,
)
tmp_storage = TempStorageBuffer(tmp_storage_bytes, stream)
sorter(
temp_storage=tmp_storage,
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=d_in_values,
d_out_values=d_out_values,
num_items=num_items,
begin_bit=begin_bit,
end_bit=end_bit,
stream=stream,
)

View File

@@ -0,0 +1,298 @@
# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
import numpy as np
from ... import _bindings
from ... import _cccl_interop as cccl
from ..._caching import cache_build_results, cache_with_registered_key_functions
from ..._cccl_interop import set_cccl_iterator_state
from ..._serialization import BUILD_RESULTS, ITER, Serializable
from ..._utils.protocols import (
get_data_pointer,
validate_and_get_stream,
)
from ..._utils.temp_storage_buffer import TempStorageBuffer
from ...typing import DeviceArrayLike
from ._sort_common import DoubleBuffer, SortOrder, _get_arrays
class _SegmentedSort(Serializable):
__slots__ = [
"_bound_build_result",
"build_results",
"loaded_build_result",
"d_in_keys_cccl",
"d_out_keys_cccl",
"d_in_values_cccl",
"d_out_values_cccl",
"start_offsets_in_cccl",
"end_offsets_in_cccl",
]
__serialization_schema__ = (
("d_in_keys_cccl", ITER),
("d_out_keys_cccl", ITER),
("d_in_values_cccl", ITER),
("d_out_values_cccl", ITER),
("start_offsets_in_cccl", ITER),
("end_offsets_in_cccl", ITER),
("build_results", BUILD_RESULTS(_bindings.DeviceSegmentedSortBuildResult)),
)
def __init__(
self,
d_in_keys: DeviceArrayLike | DoubleBuffer,
d_out_keys: DeviceArrayLike | None,
d_in_values: DeviceArrayLike | DoubleBuffer | None,
d_out_values: DeviceArrayLike | None,
start_offsets_in: DeviceArrayLike,
end_offsets_in: DeviceArrayLike,
order: SortOrder,
compute_capability=None,
):
d_in_keys_array, d_out_keys_array, d_in_values_array, d_out_values_array = (
_get_arrays(d_in_keys, d_out_keys, d_in_values, d_out_values)
)
self.d_in_keys_cccl = cccl.to_cccl_input_iter(d_in_keys_array)
self.d_out_keys_cccl = cccl.to_cccl_output_iter(d_out_keys_array)
self.d_in_values_cccl = cccl.to_cccl_input_iter(d_in_values_array)
self.d_out_values_cccl = cccl.to_cccl_output_iter(d_out_values_array)
self.start_offsets_in_cccl = cccl.to_cccl_input_iter(start_offsets_in)
self.end_offsets_in_cccl = cccl.to_cccl_input_iter(end_offsets_in)
build_order = (
_bindings.SortOrder.ASCENDING
if order is SortOrder.ASCENDING
else _bindings.SortOrder.DESCENDING
)
self.build_results, self._bound_build_result = cache_build_results(
_bindings.DeviceSegmentedSortBuildResult,
d_in_keys,
d_out_keys,
d_in_values,
d_out_values,
start_offsets_in,
end_offsets_in,
order,
compute_capability=compute_capability,
builder=lambda: cccl.build_for_ccs(
_bindings.DeviceSegmentedSortBuildResult,
build_order,
self.d_in_keys_cccl,
self.d_in_values_cccl,
self.start_offsets_in_cccl,
self.end_offsets_in_cccl,
compute_capability=compute_capability,
),
)
def __call__(
self,
*,
temp_storage,
d_in_keys,
d_out_keys,
d_in_values,
d_out_values,
num_items,
num_segments,
start_offsets_in,
end_offsets_in,
stream=None,
):
# Select (and lazily load) the build result for the current device.
self.loaded_build_result = cccl.resolve_build_result(
self.build_results, self._bound_build_result
)
if num_segments > np.iinfo(np.int32).max:
raise RuntimeError(
"Segmented sort does not currently support more than 2^31-1 segments."
)
d_in_keys_array, d_out_keys_array, d_in_values_array, d_out_values_array = (
_get_arrays(d_in_keys, d_out_keys, d_in_values, d_out_values)
)
set_cccl_iterator_state(self.d_in_keys_cccl, d_in_keys_array)
set_cccl_iterator_state(self.d_out_keys_cccl, d_out_keys_array)
if d_in_values_array is not None:
set_cccl_iterator_state(self.d_in_values_cccl, d_in_values_array)
if d_out_values_array is not None:
set_cccl_iterator_state(self.d_out_values_cccl, d_out_values_array)
set_cccl_iterator_state(self.start_offsets_in_cccl, start_offsets_in)
set_cccl_iterator_state(self.end_offsets_in_cccl, end_offsets_in)
stream_handle = validate_and_get_stream(stream)
if temp_storage is None:
temp_storage_bytes = 0
d_temp_storage = 0
else:
temp_storage_bytes = temp_storage.nbytes
d_temp_storage = get_data_pointer(temp_storage)
# Detect overwrite mode and selector, similar to radix sort
is_overwrite_okay = isinstance(d_in_keys, DoubleBuffer)
selector = -1
temp_storage_bytes, selector = self.loaded_build_result.compute(
d_temp_storage,
temp_storage_bytes,
self.d_in_keys_cccl,
self.d_out_keys_cccl,
self.d_in_values_cccl,
self.d_out_values_cccl,
num_items,
num_segments,
self.start_offsets_in_cccl,
self.end_offsets_in_cccl,
is_overwrite_okay,
selector,
stream_handle,
)
if is_overwrite_okay and temp_storage is not None:
assert selector in (0, 1)
assert isinstance(d_in_keys, DoubleBuffer)
d_in_keys.selector = selector
if d_in_values is not None:
assert isinstance(d_in_values, DoubleBuffer)
d_in_values.selector = selector
return temp_storage_bytes
@cache_with_registered_key_functions
def make_segmented_sort(
*,
d_in_keys: DeviceArrayLike | DoubleBuffer,
d_out_keys: DeviceArrayLike | None = None,
d_in_values: DeviceArrayLike | DoubleBuffer | None = None,
d_out_values: DeviceArrayLike | None = None,
start_offsets_in: DeviceArrayLike,
end_offsets_in: DeviceArrayLike,
order: SortOrder,
compute_capability=None,
):
"""
Performs a device-wide segmented sort using the specified keys and values.
Example:
Below, ``make_segmented_sort`` is used to create a segmented sort object that can be reused.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/sort/segmented_sort_object.py
:language: python
:start-after: # example-begin
Args:
d_in_keys: Device array or DoubleBuffer containing the input keys to be sorted
d_out_keys: Device array to store the sorted keys
d_in_values: Optional Device array or DoubleBuffer containing the input values to be sorted
d_out_values: Device array to store the sorted values
start_offsets_in: Device array or iterator containing the sequence of beginning offsets
end_offsets_in: Device array or iterator containing the sequence of ending offsets
order: SortOrder specifying the order of the sort
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that can be used to perform the segmented sort
"""
return _SegmentedSort(
d_in_keys,
d_out_keys,
d_in_values,
d_out_values,
start_offsets_in,
end_offsets_in,
order,
compute_capability=compute_capability,
)
def segmented_sort(
*,
d_in_keys: DeviceArrayLike | DoubleBuffer,
d_out_keys: DeviceArrayLike | None = None,
d_in_values: DeviceArrayLike | DoubleBuffer | None = None,
d_out_values: DeviceArrayLike | None = None,
num_items: int,
num_segments: int,
start_offsets_in: DeviceArrayLike,
end_offsets_in: DeviceArrayLike,
order: SortOrder,
stream=None,
):
"""
Performs device-wide segmented sort.
This function automatically handles temporary storage allocation and execution.
Example:
Below, ``segmented_sort`` is used to perform a segmented sort. It also rearranges the values according to the keys' order.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/sort/segmented_sort_basic.py
:language: python
:start-after: # example-begin
In the following example, ``segmented_sort`` is used to perform a segmented sort with a ``DoubleBuffer`` for reduced temporary storage.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/sort/segmented_sort_buffer.py
:language: python
:start-after: # example-begin
Args:
d_in_keys: Device array or DoubleBuffer containing the input keys to be sorted
d_out_keys: Device array to store the sorted keys (optional)
d_in_values: Device array or DoubleBuffer containing the input values to be sorted (optional)
d_out_values: Device array to store the sorted values (optional)
num_items: Total number of items to sort
num_segments: Number of segments to sort
start_offsets_in: Device array or iterator containing the sequence of beginning offsets
end_offsets_in: Device array or iterator containing the sequence of ending offsets
order: Sort order (ascending or descending)
stream: CUDA stream for the operation (optional)
"""
sorter = make_segmented_sort(
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=d_in_values,
d_out_values=d_out_values,
start_offsets_in=start_offsets_in,
end_offsets_in=end_offsets_in,
order=order,
)
tmp_storage_bytes = sorter(
temp_storage=None,
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=d_in_values,
d_out_values=d_out_values,
num_items=num_items,
num_segments=num_segments,
start_offsets_in=start_offsets_in,
end_offsets_in=end_offsets_in,
stream=stream,
)
tmp_storage = TempStorageBuffer(tmp_storage_bytes, stream)
sorter(
temp_storage=tmp_storage,
d_in_keys=d_in_keys,
d_out_keys=d_out_keys,
d_in_values=d_in_values,
d_out_values=d_out_values,
num_items=num_items,
num_segments=num_segments,
start_offsets_in=start_offsets_in,
end_offsets_in=end_offsets_in,
stream=stream,
)

View File

@@ -0,0 +1,62 @@
# Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from enum import Enum
from typing import Tuple
from ..._caching import cache_with_registered_key_functions
from ..._utils.protocols import get_dtype
from ...typing import DeviceArrayLike
class SortOrder(Enum):
ASCENDING = 0
DESCENDING = 1
class DoubleBuffer:
def __init__(self, d_current: DeviceArrayLike, d_alternate: DeviceArrayLike):
self.d_buffers = [d_current, d_alternate]
self.selector = 0
def current(self):
return self.d_buffers[self.selector]
def alternate(self):
return self.d_buffers[1 - self.selector]
def _get_arrays(
d_in_keys: DeviceArrayLike | DoubleBuffer,
d_out_keys: DeviceArrayLike | None,
d_in_values: DeviceArrayLike | DoubleBuffer | None,
d_out_values: DeviceArrayLike | None,
) -> Tuple[DeviceArrayLike, DeviceArrayLike, DeviceArrayLike, DeviceArrayLike]:
if isinstance(d_in_keys, DoubleBuffer):
d_in_keys_array = d_in_keys.current()
d_out_keys_array = d_in_keys.alternate()
if d_in_values is not None:
assert isinstance(d_in_values, DoubleBuffer)
d_in_values_array = d_in_values.current()
d_out_values_array = d_in_values.alternate()
else:
d_in_values_array = None
d_out_values_array = None
else:
d_in_keys_array = d_in_keys
d_in_values_array = d_in_values
d_out_keys_array = d_out_keys
d_out_values_array = d_out_values
return d_in_keys_array, d_out_keys_array, d_in_values_array, d_out_values_array
# DoubleBuffer: extract dtype from current buffer
cache_with_registered_key_functions.register(
DoubleBuffer, lambda buf: get_dtype(buf.current())
)

View File

@@ -0,0 +1,289 @@
# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from typing import Callable
from .. import _bindings, types
from .. import _cccl_interop as cccl
from .._caching import cache_build_results, cache_with_registered_key_functions
from .._cccl_interop import set_cccl_iterator_state
from .._serialization import BUILD_RESULTS, ITER, OP, Serializable
from .._utils import protocols
from .._utils.temp_storage_buffer import TempStorageBuffer
from ..op import OpAdapter, make_op_adapter
from ..typing import DeviceArrayLike, IteratorT, Operator
class _ThreeWayPartition(Serializable):
__slots__ = [
"_bound_build_result",
"build_results",
"loaded_build_result",
"d_in_cccl",
"d_first_part_out_cccl",
"d_second_part_out_cccl",
"d_unselected_out_cccl",
"d_num_selected_out_cccl",
"select_first_part_op_cccl",
"select_second_part_op_cccl",
]
__serialization_schema__ = (
("d_in_cccl", ITER),
("d_first_part_out_cccl", ITER),
("d_second_part_out_cccl", ITER),
("d_unselected_out_cccl", ITER),
("d_num_selected_out_cccl", ITER),
("select_first_part_op_cccl", OP),
("select_second_part_op_cccl", OP),
("build_results", BUILD_RESULTS(_bindings.DeviceThreeWayPartitionBuildResult)),
)
def __init__(
self,
d_in: DeviceArrayLike | IteratorT,
d_first_part_out: DeviceArrayLike | IteratorT,
d_second_part_out: DeviceArrayLike | IteratorT,
d_unselected_out: DeviceArrayLike | IteratorT,
d_num_selected_out: DeviceArrayLike | IteratorT,
select_first_part_op: OpAdapter,
select_second_part_op: OpAdapter,
compute_capability=None,
):
self.d_in_cccl = cccl.to_cccl_input_iter(d_in)
self.d_first_part_out_cccl = cccl.to_cccl_output_iter(d_first_part_out)
self.d_second_part_out_cccl = cccl.to_cccl_output_iter(d_second_part_out)
self.d_unselected_out_cccl = cccl.to_cccl_output_iter(d_unselected_out)
self.d_num_selected_out_cccl = cccl.to_cccl_output_iter(d_num_selected_out)
# Compile ops - partition predicates return uint8 (boolean)
value_type = cccl.get_value_type(d_in)
self.select_first_part_op_cccl = select_first_part_op.compile(
(value_type,), types.uint8
)
self.select_second_part_op_cccl = select_second_part_op.compile(
(value_type,), types.uint8
)
self.build_results, self._bound_build_result = cache_build_results(
_bindings.DeviceThreeWayPartitionBuildResult,
d_in,
d_first_part_out,
d_second_part_out,
d_unselected_out,
d_num_selected_out,
select_first_part_op,
select_second_part_op,
compute_capability=compute_capability,
builder=lambda: cccl.build_for_ccs(
_bindings.DeviceThreeWayPartitionBuildResult,
self.d_in_cccl,
self.d_first_part_out_cccl,
self.d_second_part_out_cccl,
self.d_unselected_out_cccl,
self.d_num_selected_out_cccl,
self.select_first_part_op_cccl,
self.select_second_part_op_cccl,
compute_capability=compute_capability,
),
)
def __call__(
self,
*,
temp_storage,
d_in,
d_first_part_out,
d_second_part_out,
d_unselected_out,
d_num_selected_out,
select_first_part_op: Callable | OpAdapter,
select_second_part_op: Callable | OpAdapter,
num_items: int,
stream=None,
):
# Select (and lazily load) the build result for the current device.
self.loaded_build_result = cccl.resolve_build_result(
self.build_results, self._bound_build_result
)
set_cccl_iterator_state(self.d_in_cccl, d_in)
set_cccl_iterator_state(self.d_first_part_out_cccl, d_first_part_out)
set_cccl_iterator_state(self.d_second_part_out_cccl, d_second_part_out)
set_cccl_iterator_state(self.d_unselected_out_cccl, d_unselected_out)
set_cccl_iterator_state(self.d_num_selected_out_cccl, d_num_selected_out)
first_op_adapter = make_op_adapter(select_first_part_op)
second_op_adapter = make_op_adapter(select_second_part_op)
self.select_first_part_op_cccl.state = first_op_adapter.get_state()
self.select_second_part_op_cccl.state = second_op_adapter.get_state()
stream_handle = protocols.validate_and_get_stream(stream)
if temp_storage is None:
temp_storage_bytes = 0
d_temp_storage = 0
else:
temp_storage_bytes = temp_storage.nbytes
d_temp_storage = protocols.get_data_pointer(temp_storage)
temp_storage_bytes = self.loaded_build_result.compute(
d_temp_storage,
temp_storage_bytes,
self.d_in_cccl,
self.d_first_part_out_cccl,
self.d_second_part_out_cccl,
self.d_unselected_out_cccl,
self.d_num_selected_out_cccl,
self.select_first_part_op_cccl,
self.select_second_part_op_cccl,
num_items,
stream_handle,
)
return temp_storage_bytes
@cache_with_registered_key_functions
def make_three_way_partition(
*,
d_in: DeviceArrayLike | IteratorT,
d_first_part_out: DeviceArrayLike | IteratorT,
d_second_part_out: DeviceArrayLike | IteratorT,
d_unselected_out: DeviceArrayLike | IteratorT,
d_num_selected_out: DeviceArrayLike | IteratorT,
select_first_part_op: Operator,
select_second_part_op: Operator,
compute_capability=None,
):
"""
Computes a device-wide three-way partition using the specified unary ``select_first_part_op`` and ``select_second_part_op`` operators.
Example:
Below, ``make_three_way_partition`` is used to create a three-way partition object that can be reused.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/partition/three_way_partition_object.py
:language: python
:start-after: # example-begin
Args:
d_in: Device array or iterator containing the input sequence of data items
d_first_part_out: Device array or iterator to store the first part of the output
d_second_part_out: Device array or iterator to store the second part of the output
d_unselected_out: Device array or iterator to store the unselected items
d_num_selected_out: Device array to store the number of items selected. The total number of items selected by ``select_first_part_op`` and ``select_second_part_op`` is stored in ``d_num_selected_out[0]`` and ``d_num_selected_out[1]``, respectively.
select_first_part_op: Unary operator to select the first part.
The signature is ``(T) -> uint8``, where ``T`` is the input data type.
Returns 1 (selected) or 0 (not selected).
Can reference device arrays as globals/closures - they will be automatically captured.
select_second_part_op: Unary operator to select the second part.
The signature is ``(T) -> uint8``, where ``T`` is the input data type.
Returns 1 (selected) or 0 (not selected).
Can reference device arrays as globals/closures - they will be automatically captured.
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that can be used to perform the three-way partition
"""
first_op_adapter = make_op_adapter(select_first_part_op)
second_op_adapter = make_op_adapter(select_second_part_op)
return _ThreeWayPartition(
d_in,
d_first_part_out,
d_second_part_out,
d_unselected_out,
d_num_selected_out,
first_op_adapter,
second_op_adapter,
compute_capability=compute_capability,
)
def three_way_partition(
*,
d_in: DeviceArrayLike | IteratorT,
d_first_part_out: DeviceArrayLike | IteratorT,
d_second_part_out: DeviceArrayLike | IteratorT,
d_unselected_out: DeviceArrayLike | IteratorT,
d_num_selected_out: DeviceArrayLike | IteratorT,
select_first_part_op: Operator,
select_second_part_op: Operator,
num_items: int,
stream=None,
):
"""
Performs device-wide three-way partition. Given an input sequence of data items, it partitions the items into three parts:
- The first part is selected by the ``select_first_part_op`` operator.
- The second part is selected by the ``select_second_part_op`` operator.
- The unselected items are not selected by either operator.
This function automatically handles temporary storage allocation and execution.
Example:
Below, ``three_way_partition`` is used to partition a sequence of integers into three parts.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/partition/three_way_partition_basic.py
:language: python
:start-after: # example-begin
Args:
d_in: Device array or iterator containing the input sequence of data items
d_first_part_out: Device array or iterator to store the first part of the output
d_second_part_out: Device array or iterator to store the second part of the output
d_unselected_out: Device array or iterator to store the unselected items
d_num_selected_out: Device array to store the number of items selected. The total number of items selected by ``select_first_part_op`` and ``select_second_part_op`` is stored in ``d_num_selected_out[0]`` and ``d_num_selected_out[1]``, respectively.
select_first_part_op: Unary operator to select the first part.
The signature is ``(T) -> uint8``, where ``T`` is the input data type.
Returns 1 (selected) or 0 (not selected).
select_second_part_op: Unary operator to select the second part.
The signature is ``(T) -> uint8``, where ``T`` is the input data type.
Returns 1 (selected) or 0 (not selected).
num_items: Number of items to partition
stream: CUDA stream for the operation (optional)
"""
# Create adapters to support stateful ops
first_op_adapter = make_op_adapter(select_first_part_op)
second_op_adapter = make_op_adapter(select_second_part_op)
partitioner = make_three_way_partition(
d_in=d_in,
d_first_part_out=d_first_part_out,
d_second_part_out=d_second_part_out,
d_unselected_out=d_unselected_out,
d_num_selected_out=d_num_selected_out,
select_first_part_op=first_op_adapter,
select_second_part_op=second_op_adapter,
)
tmp_storage_bytes = partitioner(
temp_storage=None,
d_in=d_in,
d_first_part_out=d_first_part_out,
d_second_part_out=d_second_part_out,
d_unselected_out=d_unselected_out,
d_num_selected_out=d_num_selected_out,
select_first_part_op=first_op_adapter,
select_second_part_op=second_op_adapter,
num_items=num_items,
stream=stream,
)
tmp_storage = TempStorageBuffer(tmp_storage_bytes, stream)
partitioner(
temp_storage=tmp_storage,
d_in=d_in,
d_first_part_out=d_first_part_out,
d_second_part_out=d_second_part_out,
d_unselected_out=d_unselected_out,
d_num_selected_out=d_num_selected_out,
select_first_part_op=first_op_adapter,
select_second_part_op=second_op_adapter,
num_items=num_items,
stream=stream,
)

View File

@@ -0,0 +1,371 @@
# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from typing import Callable
from .. import _bindings
from .. import _cccl_interop as cccl
from .._caching import cache_build_results, cache_with_registered_key_functions
from .._cccl_interop import set_cccl_iterator_state
from .._serialization import BUILD_RESULTS, ITER, OP, Serializable
from .._utils import protocols
from ..op import OpAdapter, make_op_adapter
from ..typing import DeviceArrayLike, IteratorT, Operator
class _UnaryTransform(Serializable):
__slots__ = [
"_bound_build_result",
"d_in_cccl",
"d_out_cccl",
"op_cccl",
"build_results",
"loaded_build_result",
]
__serialization_schema__ = (
("d_in_cccl", ITER),
("d_out_cccl", ITER),
("op_cccl", OP),
("build_results", BUILD_RESULTS(_bindings.DeviceUnaryTransform)),
)
def __init__(
self,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
op: OpAdapter,
compute_capability=None,
):
self.d_in_cccl = cccl.to_cccl_input_iter(d_in)
self.d_out_cccl = cccl.to_cccl_output_iter(d_out)
# Compile the op with input/output types
in_type = cccl.get_value_type(d_in)
out_type = cccl.get_value_type(d_out)
self.op_cccl = op.compile((in_type,), out_type)
self.build_results, self._bound_build_result = cache_build_results(
_bindings.DeviceUnaryTransform,
d_in,
d_out,
op,
compute_capability=compute_capability,
builder=lambda: cccl.build_for_ccs(
_bindings.DeviceUnaryTransform,
self.d_in_cccl,
self.d_out_cccl,
self.op_cccl,
compute_capability=compute_capability,
),
)
def __call__(
self,
*,
d_in,
d_out,
op: Callable | OpAdapter,
num_items: int,
stream=None,
):
# Select (and lazily load) the build result for the current device.
self.loaded_build_result = cccl.resolve_build_result(
self.build_results, self._bound_build_result
)
op_adapter = make_op_adapter(op)
set_cccl_iterator_state(self.d_in_cccl, d_in)
set_cccl_iterator_state(self.d_out_cccl, d_out)
self.op_cccl.state = op_adapter.get_state()
stream_handle = protocols.validate_and_get_stream(stream)
self.loaded_build_result.compute(
self.d_in_cccl,
self.d_out_cccl,
num_items,
self.op_cccl,
stream_handle,
)
return None
class _BinaryTransform(Serializable):
__slots__ = [
"_bound_build_result",
"d_in1_cccl",
"d_in2_cccl",
"d_out_cccl",
"op_cccl",
"build_results",
"loaded_build_result",
]
__serialization_schema__ = (
("d_in1_cccl", ITER),
("d_in2_cccl", ITER),
("d_out_cccl", ITER),
("op_cccl", OP),
("build_results", BUILD_RESULTS(_bindings.DeviceBinaryTransform)),
)
def __init__(
self,
d_in1: DeviceArrayLike | IteratorT,
d_in2: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
op: OpAdapter,
compute_capability=None,
):
self.d_in1_cccl = cccl.to_cccl_input_iter(d_in1)
self.d_in2_cccl = cccl.to_cccl_input_iter(d_in2)
self.d_out_cccl = cccl.to_cccl_output_iter(d_out)
# Compile the op with input/output types
in1_type = cccl.get_value_type(d_in1)
in2_type = cccl.get_value_type(d_in2)
out_type = cccl.get_value_type(d_out)
self.op_cccl = op.compile((in1_type, in2_type), out_type)
self.build_results, self._bound_build_result = cache_build_results(
_bindings.DeviceBinaryTransform,
d_in1,
d_in2,
d_out,
op,
compute_capability=compute_capability,
builder=lambda: cccl.build_for_ccs(
_bindings.DeviceBinaryTransform,
self.d_in1_cccl,
self.d_in2_cccl,
self.d_out_cccl,
self.op_cccl,
compute_capability=compute_capability,
),
)
def __call__(
self,
*,
d_in1,
d_in2,
d_out,
op: Callable | OpAdapter,
num_items: int,
stream=None,
):
# Select (and lazily load) the build result for the current device.
self.loaded_build_result = cccl.resolve_build_result(
self.build_results, self._bound_build_result
)
set_cccl_iterator_state(self.d_in1_cccl, d_in1)
set_cccl_iterator_state(self.d_in2_cccl, d_in2)
set_cccl_iterator_state(self.d_out_cccl, d_out)
op_adapter = make_op_adapter(op)
self.op_cccl.state = op_adapter.get_state()
stream_handle = protocols.validate_and_get_stream(stream)
self.loaded_build_result.compute(
self.d_in1_cccl,
self.d_in2_cccl,
self.d_out_cccl,
num_items,
self.op_cccl,
stream_handle,
)
return None
@cache_with_registered_key_functions
def make_unary_transform(
*,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
op: Operator,
compute_capability=None,
):
"""
Create a unary transform object that can be called to apply a transformation
to each element of the input according to the unary operation ``op``.
This is the object-oriented API that allows explicit control over temporary
storage allocation. For simpler usage, consider using :func:`unary_transform`.
Example:
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/transform/unary_transform_object.py
:language: python
:start-after: # example-begin
Args:
d_in: Device array or iterator containing the input sequence of data items.
d_out: Device array or iterator to store the result of the transformation.
op: Unary operation to apply to each element.
The signature is ``(T) -> U``, where ``T`` is
the input data type and ``U`` is the output data type.
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that performs the transformation.
"""
op_adapter = make_op_adapter(op)
return _UnaryTransform(
d_in, d_out, op_adapter, compute_capability=compute_capability
)
@cache_with_registered_key_functions
def make_binary_transform(
*,
d_in1: DeviceArrayLike | IteratorT,
d_in2: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
op: Operator,
compute_capability=None,
):
"""
Create a binary transform object that can be called to apply a transformation
to the given pair of input sequences according to the binary operation ``op``.
This is the object-oriented API that allows explicit control over temporary
storage allocation. For simpler usage, consider using :func:`binary_transform`.
Example:
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/transform/binary_transform_object.py
:language: python
:start-after: # example-begin
Args:
d_in1: Device array or iterator containing the first input sequence of data items.
d_in2: Device array or iterator containing the second input sequence of data items.
d_out: Device array or iterator to store the result of the transformation.
op: Binary operation.
The signature is ``(T1, T2) -> U``, where ``T1`` and ``T2`` are the input data types and
``U`` is the output data type.
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that performs the transformation.
"""
op_adapter = make_op_adapter(op)
return _BinaryTransform(
d_in1, d_in2, d_out, op_adapter, compute_capability=compute_capability
)
def unary_transform(
*,
d_in: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
op: Operator,
num_items: int,
stream=None,
):
"""
Performs device-wide unary transform.
This function automatically handles temporary storage allocation and execution.
The ``op`` function can reference device arrays as globals or closures - they will
be automatically captured as state arrays, enabling stateful operations like counting.
Example:
Below, ``unary_transform`` is used to apply a transformation to each element of the input.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/transform/unary_transform_basic.py
:language: python
:start-after: # example-begin
When working with custom struct types, you need to provide type annotations
to help with type inference. See the binary transform struct example for reference:
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/struct/struct_transform.py
:language: python
:start-after: # example-begin
Args:
d_in: Device array or iterator containing the input sequence of data items.
d_out: Device array or iterator to store the result of the transformation.
op: Unary operation to apply to each element.
The signature is ``(T) -> U``, where ``T`` is
the input data type and ``U`` is the output data type.
Can reference device arrays as globals/closures - they will be automatically captured.
num_items: Number of items to transform.
stream: CUDA stream to use for the operation.
"""
op_adapter = make_op_adapter(op)
transformer = make_unary_transform(d_in=d_in, d_out=d_out, op=op_adapter)
transformer(
d_in=d_in, d_out=d_out, op=op_adapter, num_items=num_items, stream=stream
)
def binary_transform(
*,
d_in1: DeviceArrayLike | IteratorT,
d_in2: DeviceArrayLike | IteratorT,
d_out: DeviceArrayLike | IteratorT,
op: Operator,
num_items: int,
stream=None,
):
"""
Performs device-wide binary transform.
This function automatically handles temporary storage allocation and execution.
Example:
Below, ``binary_transform`` is used to apply a transformation to pairs of elements from two input sequences.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/transform/binary_transform_basic.py
:language: python
:start-after: # example-begin
When working with custom struct types, you need to provide type annotations
to help with type inference. See the following example:
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/struct/struct_transform.py
:language: python
:start-after: # example-begin
Args:
d_in1: Device array or iterator containing the first input sequence of data items.
d_in2: Device array or iterator containing the second input sequence of data items.
d_out: Device array or iterator to store the result of the transformation.
op: Binary operation.
The signature is ``(T1, T2) -> U``, where ``T1`` and ``T2`` are the input data types and
``U`` is the output data type.
Can reference device arrays as globals/closures - they will be automatically captured.
num_items: Number of items to transform.
stream: CUDA stream to use for the operation.
"""
op_adapter = make_op_adapter(op)
transformer = make_binary_transform(
d_in1=d_in1, d_in2=d_in2, d_out=d_out, op=op_adapter
)
transformer(
d_in1=d_in1,
d_in2=d_in2,
d_out=d_out,
op=op_adapter,
num_items=num_items,
stream=stream,
)

View File

@@ -0,0 +1,253 @@
# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from .. import _bindings, types
from .. import _cccl_interop as cccl
from .._caching import cache_build_results, cache_with_registered_key_functions
from .._cccl_interop import set_cccl_iterator_state
from .._serialization import BUILD_RESULTS, ITER, OP, Serializable
from .._utils.protocols import (
get_data_pointer,
validate_and_get_stream,
)
from .._utils.temp_storage_buffer import TempStorageBuffer
from ..op import OpAdapter, make_op_adapter
from ..typing import DeviceArrayLike, IteratorT, Operator
class _UniqueByKey(Serializable):
__slots__ = [
"_bound_build_result",
"build_results",
"loaded_build_result",
"d_in_keys_cccl",
"d_in_items_cccl",
"d_out_keys_cccl",
"d_out_items_cccl",
"d_out_num_selected_cccl",
"op_cccl",
]
__serialization_schema__ = (
("d_in_keys_cccl", ITER),
("d_in_items_cccl", ITER),
("d_out_keys_cccl", ITER),
("d_out_items_cccl", ITER),
("d_out_num_selected_cccl", ITER),
("op_cccl", OP),
("build_results", BUILD_RESULTS(_bindings.DeviceUniqueByKeyBuildResult)),
)
def __init__(
self,
d_in_keys: DeviceArrayLike | IteratorT,
d_in_items: DeviceArrayLike | IteratorT,
d_out_keys: DeviceArrayLike | IteratorT,
d_out_items: DeviceArrayLike | IteratorT,
d_out_num_selected: DeviceArrayLike,
op: OpAdapter,
compute_capability=None,
):
self.d_in_keys_cccl = cccl.to_cccl_input_iter(d_in_keys)
self.d_in_items_cccl = cccl.to_cccl_input_iter(d_in_items)
self.d_out_keys_cccl = cccl.to_cccl_output_iter(d_out_keys)
self.d_out_items_cccl = cccl.to_cccl_output_iter(d_out_items)
self.d_out_num_selected_cccl = cccl.to_cccl_output_iter(d_out_num_selected)
# Compile the op - unique_by_key expects bool return (comparison)
value_type = cccl.get_value_type(d_in_keys)
self.op_cccl = op.compile((value_type, value_type), types.uint8)
self.build_results, self._bound_build_result = cache_build_results(
_bindings.DeviceUniqueByKeyBuildResult,
d_in_keys,
d_in_items,
d_out_keys,
d_out_items,
d_out_num_selected,
op,
compute_capability=compute_capability,
builder=lambda: cccl.build_for_ccs(
_bindings.DeviceUniqueByKeyBuildResult,
self.d_in_keys_cccl,
self.d_in_items_cccl,
self.d_out_keys_cccl,
self.d_out_items_cccl,
self.d_out_num_selected_cccl,
self.op_cccl,
compute_capability=compute_capability,
),
)
def __call__(
self,
*,
temp_storage,
d_in_keys: DeviceArrayLike | IteratorT,
d_in_items: DeviceArrayLike | IteratorT,
d_out_keys: DeviceArrayLike | IteratorT,
d_out_items: DeviceArrayLike | IteratorT,
d_out_num_selected: DeviceArrayLike,
op: Operator,
num_items: int,
stream=None,
):
# Select (and lazily load) the build result for the current device.
self.loaded_build_result = cccl.resolve_build_result(
self.build_results, self._bound_build_result
)
set_cccl_iterator_state(self.d_in_keys_cccl, d_in_keys)
set_cccl_iterator_state(self.d_in_items_cccl, d_in_items)
set_cccl_iterator_state(self.d_out_keys_cccl, d_out_keys)
set_cccl_iterator_state(self.d_out_items_cccl, d_out_items)
set_cccl_iterator_state(self.d_out_num_selected_cccl, d_out_num_selected)
# Update op state for stateful ops
op_adapter = make_op_adapter(op)
self.op_cccl.state = op_adapter.get_state()
stream_handle = validate_and_get_stream(stream)
if temp_storage is None:
temp_storage_bytes = 0
d_temp_storage = 0
else:
temp_storage_bytes = temp_storage.nbytes
# Note: this is slightly slower, but supports all ndarray-like objects as long as they support CAI
# TODO: switch to use gpumemoryview once it's ready
d_temp_storage = get_data_pointer(temp_storage)
temp_storage_bytes = self.loaded_build_result.compute(
d_temp_storage,
temp_storage_bytes,
self.d_in_keys_cccl,
self.d_in_items_cccl,
self.d_out_keys_cccl,
self.d_out_items_cccl,
self.d_out_num_selected_cccl,
self.op_cccl,
num_items,
stream_handle,
)
return temp_storage_bytes
@cache_with_registered_key_functions
def make_unique_by_key(
*,
d_in_keys: DeviceArrayLike | IteratorT,
d_in_items: DeviceArrayLike | IteratorT,
d_out_keys: DeviceArrayLike | IteratorT,
d_out_items: DeviceArrayLike | IteratorT,
d_out_num_selected: DeviceArrayLike,
op: Operator,
compute_capability=None,
):
"""Implements a device-wide unique by key operation using ``d_in_keys`` and the comparison operator ``op``. Only the first key and its value from each run is selected and the total number of items selected is also reported.
Example:
Below, ``make_unique_by_key`` is used to create a unique by key object that can be reused.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/unique/unique_by_key_object.py
:language: python
:start-after: # example-begin
Args:
d_in_keys: Device array or iterator containing the input sequence of keys
d_in_items: Device array or iterator that contains each key's corresponding item
d_out_keys: Device array or iterator to store the outputted keys
d_out_items: Device array or iterator to store each outputted key's item
d_out_num_selected: Device array to store how many items were selected
op: Callable or OpKind representing the equality operator
compute_capability: Compute capability, or list of capabilities, to
build for ahead of time. Accepts a packed int (e.g. ``90``), a
``(major, minor)`` pair, a string (e.g. ``"9.0"``), or a list
thereof. When ``None`` (the default), the current device's
architecture is used.
Returns:
A callable object that can be used to perform unique by key
"""
op_adapter = make_op_adapter(op)
return _UniqueByKey(
d_in_keys,
d_in_items,
d_out_keys,
d_out_items,
d_out_num_selected,
op_adapter,
compute_capability=compute_capability,
)
def unique_by_key(
*,
d_in_keys: DeviceArrayLike | IteratorT,
d_in_items: DeviceArrayLike | IteratorT,
d_out_keys: DeviceArrayLike | IteratorT,
d_out_items: DeviceArrayLike | IteratorT,
d_out_num_selected: DeviceArrayLike,
op: Operator,
num_items: int,
stream=None,
):
"""
Performs device-wide unique by key operation using the single-phase API.
This function automatically handles temporary storage allocation and execution.
Example:
Below, ``unique_by_key`` is used to populate the arrays of output keys and items with the first key and its corresponding item from each sequence of equal keys. It also outputs the number of items selected.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/unique/unique_by_key_basic.py
:language: python
:start-after: # example-begin
Args:
d_in_keys: Device array or iterator containing the input sequence of keys
d_in_items: Device array or iterator that contains each key's corresponding item
d_out_keys: Device array or iterator to store the outputted keys
d_out_items: Device array or iterator to store each outputted key's item
d_out_num_selected: Device array to store how many items were selected
op: Callable or OpKind representing the equality operator
num_items: Number of items to process
stream: CUDA stream for the operation (optional)
"""
uniquer = make_unique_by_key(
d_in_keys=d_in_keys,
d_in_items=d_in_items,
d_out_keys=d_out_keys,
d_out_items=d_out_items,
d_out_num_selected=d_out_num_selected,
op=op,
)
tmp_storage_bytes = uniquer(
temp_storage=None,
d_in_keys=d_in_keys,
d_in_items=d_in_items,
d_out_keys=d_out_keys,
d_out_items=d_out_items,
d_out_num_selected=d_out_num_selected,
op=op,
num_items=num_items,
stream=stream,
)
tmp_storage = TempStorageBuffer(tmp_storage_bytes, stream)
uniquer(
temp_storage=tmp_storage,
d_in_keys=d_in_keys,
d_in_items=d_in_items,
d_out_keys=d_out_keys,
d_out_items=d_out_items,
d_out_num_selected=d_out_num_selected,
op=op,
num_items=num_items,
stream=stream,
)

View File

@@ -0,0 +1,5 @@
from __future__ import annotations
from ._bindings import Determinism
__all__ = ["Determinism"]

View File

@@ -0,0 +1,31 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from __future__ import annotations
from ._base import IteratorBase
from ._cache_modified import CacheModifiedInputIterator
from ._constant import ConstantIterator
from ._counting import CountingIterator
from ._discard import DiscardIterator
from ._permutation import PermutationIterator
from ._reverse import ReverseIterator
from ._shuffle import ShuffleIterator
from ._transform import TransformIterator, TransformOutputIterator
from ._zip import ZipIterator
__all__ = [
"CacheModifiedInputIterator",
"ConstantIterator",
"CountingIterator",
"DiscardIterator",
"IteratorBase",
"PermutationIterator",
"ReverseIterator",
"ShuffleIterator",
"TransformIterator",
"TransformOutputIterator",
"ZipIterator",
]

View File

@@ -0,0 +1,293 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""
Base classes for iterators.
"""
from __future__ import annotations
import hashlib
import threading
from typing import Hashable
from .._bindings import Iterator, IteratorKind, IteratorState, Op
from .._caching import cache_with_registered_key_functions
from ..types import TypeDescriptor
class IteratorBase:
"""
Iterators represent streams of data computed on the fly.
See :py:mod:`cuda.compute.iterators` for available iterators.
"""
# Subclassing
# -----------
#
# Subclasses must implement the following methods that return
# Op objects.
#
# - _make_advance_op() -> Op
# - _make_input_deref_op() -> Op | None
# - _make_output_deref_op() -> Op | None
#
# Iterators composed of other iterators must also implement:
#
# - children property to return tuple of child iterators for dependency tracking
#
# Examples of such "compound" iterators include TransformIterator,
# PermutationIterator, ReverseIterator and ZipIterator.
#
# The base class provides public cached accessors:
#
# - get_advance_op() -> Op (cached)
# - get_input_deref_op() -> Op | None (cached)
# - get_output_deref_op() -> Op | None (cached)
__slots__ = [
"_state_bytes",
"_state_alignment",
"_value_type",
"_advance_op",
"_input_deref_op",
"_output_deref_op",
"_uid_cached",
"_op_lock",
]
def __init__(
self,
state_bytes: bytes,
state_alignment: int,
value_type: TypeDescriptor,
):
"""
Args:
state_bytes: bytes object representing iterator's state
state_alignment: Alignment of the state
value_type: Type of dereferenced values
"""
self._state_bytes = state_bytes
self._state_alignment = state_alignment
self._value_type = value_type
# Per-cc caches: the compiled Op is arch-specific (its LTO-IR is
# built for the current build's target compute capability), so the memo
# must be keyed on that cc. Reusing one iterator instance across builds
# targeting different arches otherwise leaks the first arch's LTO-IR into
# the others, which nvJitLink rejects. Keyed on get_target_cc() (None ==
# current device); see get_advance_op() below.
self._advance_op: dict[Hashable, Op] = {}
self._input_deref_op: dict[Hashable, Op | None] = {}
self._output_deref_op: dict[Hashable, Op | None] = {}
self._uid_cached: str | None = None
# Free-threaded Python can let multiple threads share a read-only
# iterator object and race during the first lazy Op construction.
# The lock only protects that cache miss path; cached access stays
# lock-free and iterator mutation remains the caller's responsibility.
self._op_lock = threading.Lock()
@property
def state(self) -> IteratorState:
"""Return the iterator state for CCCL interop."""
return IteratorState(self._state_bytes)
@property
def state_alignment(self) -> int:
"""Return the alignment of the iterator state."""
return self._state_alignment
@property
def value_type(self) -> TypeDescriptor:
"""Return the TypeDescriptor for dereferenced values."""
return self._value_type
@property
def children(self) -> tuple["IteratorBase", ...]:
"""Return child iterators for automatic dependency tracking. Override in subclasses."""
return ()
def _get_uid(self) -> str:
"""Return a deterministic unique identifier for this iterator type."""
if self._uid_cached is None:
self._uid_cached = _deterministic_suffix(self.kind)
return self._uid_cached
def _make_advance_symbol(self) -> str:
"""Generate symbol name for advance operation."""
return f"{self.__class__.__name__}_advance_{self._get_uid()}"
def _make_input_deref_symbol(self) -> str:
"""Generate symbol name for input dereference operation."""
return f"{self.__class__.__name__}_input_deref_{self._get_uid()}"
def _make_output_deref_symbol(self) -> str:
"""Generate symbol name for output dereference operation."""
return f"{self.__class__.__name__}_output_deref_{self._get_uid()}"
def get_advance_op(self) -> Op:
"""Get the cached Op for the advance operation."""
from .._target_cc import get_target_cc
key = get_target_cc()
if key not in self._advance_op:
with self._op_lock:
if key not in self._advance_op:
self._advance_op[key] = self._make_advance_op()
return self._advance_op[key]
def get_input_deref_op(self) -> Op | None:
"""Get the cached Op for input dereference operation, or None if not supported."""
from .._target_cc import get_target_cc
key = get_target_cc()
if key not in self._input_deref_op:
with self._op_lock:
if key not in self._input_deref_op:
self._input_deref_op[key] = self._make_input_deref_op()
return self._input_deref_op[key]
def get_output_deref_op(self) -> Op | None:
"""Get the cached Op for output dereference operation, or None if not supported."""
from .._target_cc import get_target_cc
key = get_target_cc()
if key not in self._output_deref_op:
with self._op_lock:
if key not in self._output_deref_op:
self._output_deref_op[key] = self._make_output_deref_op()
return self._output_deref_op[key]
@property
def is_input_iterator(self) -> bool:
"""Return True if this iterator supports input dereference."""
return self.get_input_deref_op() is not None
@property
def is_output_iterator(self) -> bool:
"""Return True if this iterator supports output dereference."""
return self.get_output_deref_op() is not None
def to_cccl_iter(self, is_output: bool = False) -> Iterator:
"""
Convert this iterator to a CCCL Iterator for algorithm interop.
Args:
is_output: If True, use output_dereference; otherwise use input_dereference
Returns:
CCCL Iterator object
"""
# Get advance op
advance_op = self.get_advance_op()
# Get dereference op based on direction
if is_output:
deref_op = self.get_output_deref_op()
if deref_op is None:
raise ValueError("This iterator does not support output operations")
else:
deref_op = self.get_input_deref_op()
if deref_op is None:
raise ValueError("This iterator does not support input operations")
# Create the CCCL Iterator
return Iterator(
self._state_alignment,
IteratorKind.ITERATOR,
advance_op,
deref_op,
self._value_type.info,
state=self.state,
)
@property
def kind(self) -> Hashable:
"""Return a hashable kind for caching purposes.
Note: state_bytes is intentionally excluded - iterators with the same
type structure but different runtime state should share cached reducers.
"""
return (type(self).__name__, self._value_type)
# Abstract methods for subclasses
def _make_advance_op(self) -> Op:
"""
Create Op object for advance operation.
Returns:
Op object with compiled LTOIR
"""
raise NotImplementedError
def _make_input_deref_op(self) -> Op | None:
"""
Create Op object for input dereference operation.
Returns:
Op object with compiled LTOIR, or None if not supported
"""
raise NotImplementedError
def _make_output_deref_op(self) -> Op | None:
"""
Create Op object for output dereference operation.
Returns:
Op object with compiled LTOIR, or None if not supported
"""
raise NotImplementedError
def _deterministic_suffix(kind: Hashable) -> str:
kind_str = str(kind)
return hashlib.sha256(kind_str.encode()).hexdigest()[:16]
def compose_iterator_states(
iterators: list[IteratorBase],
) -> tuple[bytes, int, list[int]]:
"""
Concatenate multiple iterator states with proper alignment.
This is used by composite iterators (like ZipIterator and PermutationIterator)
that need to store multiple child iterator states in their own state.
Args:
iterators: List of child iterators whose states should be composed
Returns:
Tuple of:
- combined_state_bytes: Concatenated state bytes with padding
- combined_alignment: Maximum alignment requirement
- offsets: List of byte offsets for each iterator's state
"""
if not iterators:
return (b"", 1, [])
states = [bytes(memoryview(it.state)) for it in iterators]
alignments = [it.state_alignment for it in iterators]
offsets = []
current_offset = 0
combined = b""
for state, align in zip(states, alignments):
# Add padding to meet alignment requirement
padding = (align - (current_offset % align)) % align
combined += b"\x00" * padding
current_offset += padding
offsets.append(current_offset)
combined += state
current_offset += len(state)
max_alignment = max(alignments)
return (combined, max_alignment, offsets)
cache_with_registered_key_functions.register(IteratorBase, lambda it: it.kind)
__all__ = ["IteratorBase"]

View File

@@ -0,0 +1,156 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""CacheModifiedInputIterator implementation."""
from __future__ import annotations
import struct
from textwrap import dedent
from typing import Literal
from .._bindings import Op, OpKind
from .._cpp_compile import compile_cpp_op_code, cpp_type_from_descriptor
from .._utils.protocols import get_data_pointer, get_dtype
from ..types import from_numpy_dtype
from ._base import IteratorBase
from ._common import CUDA_PREAMBLE
# Map modifier names to PTX cache operators and C++ intrinsics
_CACHE_MODIFIERS = {
"stream": ("cs", "__ldcs"), # Cache streaming (evict first)
"global": ("cg", "__ldcg"), # Cache at L2 only
"volatile": ("cv", "__ldcv"), # Don't cache, always fetch
}
class CacheModifiedInputIterator(IteratorBase):
"""
Iterator that wraps a device pointer with cache-modified loads.
This iterator uses PTX cache modifiers to control how data is loaded:
- "stream": Uses streaming loads (ld.global.cs) - hints that data will not be reused
- "global": Uses global cache loads (ld.global.cg) - caches only at L2
- "volatile": Uses volatile loads (ld.global.cv) - always fetches from memory
Supports element types of size 1, 2, 4, 8, or 16 bytes.
"""
__slots__ = [
"_modifier",
"_array",
"_ptr",
]
def __init__(
self,
array,
modifier: Literal["stream", "global", "volatile"] = "stream",
):
"""
Create a cache-modified input iterator.
Args:
array: Device array to wrap (must support __cuda_array_interface__)
modifier: Cache modifier - "stream", "global", or "volatile"
"""
if modifier not in _CACHE_MODIFIERS:
raise ValueError(
f"Unknown modifier: {modifier}. Must be one of {list(_CACHE_MODIFIERS.keys())}"
)
self._modifier = modifier
self._array = array # Keep reference to prevent GC
ptr = get_data_pointer(array)
dtype = get_dtype(array)
self._ptr = ptr
value_type = from_numpy_dtype(dtype)
# Cache-modified loads only supported for power-of-two sizes up to 16 bytes
# These correspond to PTX instructions: ld.global.{modifier}.b{8,16,32,64,128}
if value_type.size not in (1, 2, 4, 8, 16):
raise ValueError(
f"CacheModifiedInputIterator only supports types of size 1, 2, 4, 8, or 16 bytes. "
f"Got type with size {value_type.size} bytes. "
f"This matches PTX cache-modified load instruction limitations."
)
# State is just the pointer (8 bytes on 64-bit)
state_bytes = struct.pack("Q", ptr)
super().__init__(
state_bytes=state_bytes,
state_alignment=8, # Pointer alignment
value_type=value_type,
)
def _make_advance_op(self) -> Op:
symbol = self._make_advance_symbol()
cpp_type = cpp_type_from_descriptor(self._value_type)
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void* state, void* offset) {{
auto* s = static_cast<{cpp_type}**>(state);
auto dist = *static_cast<uint64_t*>(offset);
*s += dist;
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[],
)
def _make_input_deref_op(self) -> Op | None:
symbol = self._make_input_deref_symbol()
cpp_type = cpp_type_from_descriptor(self._value_type)
_, intrinsic = _CACHE_MODIFIERS[self._modifier]
# Use cache-modified intrinsic for all supported sizes (1, 2, 4, 8, 16 bytes)
# These correspond to PTX instructions: ld.global.{modifier}.b{8,16,32,64,128}
# Note: __ldcs, __ldcg, __ldcv intrinsics work for all these sizes
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void* state, void* result) {{
auto* ptr = *static_cast<{cpp_type}**>(state);
*static_cast<{cpp_type}*>(result) = {intrinsic}(ptr);
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[],
)
def _make_output_deref_op(self) -> Op | None:
# Cache-modified iterator is input-only
return None
def __add__(self, offset: int) -> "CacheModifiedInputIterator":
"""Advance the iterator by offset elements."""
out = CacheModifiedInputIterator(self._array, self._modifier)
offset_ptr = self._ptr + offset * get_dtype(out._array).itemsize
out._ptr = offset_ptr
out._state_bytes = struct.pack("Q", offset_ptr)
out._uid_cached = None
return out
@property
def kind(self):
"""Return a hashable kind for caching purposes."""
return (
"CacheModifiedInputIterator",
self._modifier,
self._value_type,
)

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
from .._utils.protocols import is_device_array
from ._base import IteratorBase
CUDA_PREAMBLE = """#include <cuda/std/cstdint>
#include <cuda_fp16.h>
#include <cuda/std/cstring>
using namespace cuda::std;
"""
def ensure_iterator(obj):
"""Wrap array in PointerIterator if needed."""
from ._pointer import PointerIterator
if isinstance(obj, IteratorBase):
return obj
if is_device_array(obj):
return PointerIterator(obj)
raise TypeError("Expected an iterator or a device array")

View File

@@ -0,0 +1,103 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""ConstantIterator implementation."""
from __future__ import annotations
from textwrap import dedent
import numpy as np
from .._bindings import Op, OpKind
from .._cpp_compile import compile_cpp_op_code, cpp_type_from_descriptor
from ..types import from_numpy_dtype
from ._base import IteratorBase
from ._common import CUDA_PREAMBLE
class ConstantIterator(IteratorBase):
"""
Iterator representing a sequence of constant values.
Similar to `thrust::constant_iterator <https://nvidia.github.io/cccl/thrust/api/classthrust_1_1constant__iterator.html>`_.
Every dereference returns the same constant value.
Example:
The code snippet below demonstrates the usage of a ``ConstantIterator``
representing a sequence of constant values:
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/iterator/constant_iterator_basic.py
:language: python
:start-after: # example-begin
Args:
value: The value of every item in the sequence
"""
def __init__(self, value: np.number):
"""
Create a constant iterator with the given value.
Args:
value: The constant value (must be a numpy scalar)
"""
if not isinstance(value, np.generic):
value = np.array(value).flatten()[0]
self._constant_value = value
value_type = from_numpy_dtype(value.dtype)
state_bytes = value.tobytes()
super().__init__(
state_bytes=state_bytes,
state_alignment=value_type.alignment,
value_type=value_type,
)
def _make_advance_op(self) -> Op:
symbol = self._make_advance_symbol()
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void*, void*) {{
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[],
)
def _make_input_deref_op(self) -> Op | None:
symbol = self._make_input_deref_symbol()
cpp_type = cpp_type_from_descriptor(self._value_type)
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void* state, void* result) {{
*static_cast<{cpp_type}*>(result) = *static_cast<{cpp_type}*>(state);
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[],
)
def _make_output_deref_op(self) -> Op | None:
return None
def __add__(self, offset: int) -> "ConstantIterator":
"""Return a new ConstantIterator (value doesn't change with position)."""
return ConstantIterator(self._constant_value)

View File

@@ -0,0 +1,108 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""CountingIterator implementation."""
from __future__ import annotations
from textwrap import dedent
import numpy as np
from .._bindings import Op, OpKind
from .._cpp_compile import compile_cpp_op_code, cpp_type_from_descriptor
from ..types import from_numpy_dtype
from ._base import IteratorBase
from ._common import CUDA_PREAMBLE
class CountingIterator(IteratorBase):
"""
Iterator representing a sequence of incrementing values.
Similar to `thrust::counting_iterator <https://nvidia.github.io/cccl/thrust/api/classthrust_1_1counting__iterator.html>`_.
The iterator starts at `start` and increments by 1 for each advance.
Example:
The code snippet below demonstrates the usage of a ``CountingIterator``
representing the sequence ``[10, 11, 12]``:
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/iterator/counting_iterator_basic.py
:language: python
:start-after: # example-begin
Args:
start: The initial value of the sequence
"""
def __init__(self, start: np.number):
"""
Create a counting iterator starting at `start`.
Args:
start: The initial value (must be a numpy scalar)
"""
if not isinstance(start, np.generic):
start = np.array(start).flatten()[0]
self._start_value = start
value_type = from_numpy_dtype(start.dtype)
state_bytes = start.tobytes()
super().__init__(
state_bytes=state_bytes,
state_alignment=value_type.alignment,
value_type=value_type,
)
def _make_advance_op(self) -> Op:
symbol = self._make_advance_symbol()
cpp_type = cpp_type_from_descriptor(self._value_type)
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void* state, void* offset) {{
auto* s = static_cast<{cpp_type}*>(state);
auto dist = *static_cast<uint64_t*>(offset);
*s += static_cast<{cpp_type}>(dist);
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[],
)
def _make_input_deref_op(self) -> Op | None:
symbol = self._make_input_deref_symbol()
cpp_type = cpp_type_from_descriptor(self._value_type)
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void* state, void* result) {{
*static_cast<{cpp_type}*>(result) = *static_cast<{cpp_type}*>(state);
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[],
)
def _make_output_deref_op(self) -> Op | None:
return None
def __add__(self, offset: int) -> "CountingIterator":
"""Return a new CountingIterator advanced by offset elements."""
new_start = self._start_value + offset
return CountingIterator(new_start)

View File

@@ -0,0 +1,103 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""DiscardIterator implementation."""
from __future__ import annotations
from .._bindings import Op, OpKind
from .._cpp_compile import compile_cpp_op_code
from .._utils.protocols import get_dtype
from .._utils.temp_storage_buffer import TempStorageBuffer
from ..types import TypeDescriptor, from_numpy_dtype
from ._base import IteratorBase
from ._common import CUDA_PREAMBLE
class DiscardIterator(IteratorBase):
"""
Iterator that discards all reads and writes.
"""
def __init__(self, reference_iterator=None):
"""
Create a discard iterator.
Args:
reference_iterator: Optional iterator or device array used to infer
value_type/state_type. Defaults to a temporary byte buffer.
"""
if reference_iterator is None:
reference_iterator = TempStorageBuffer(1)
self._reference_iterator = reference_iterator
if hasattr(reference_iterator, "__cuda_array_interface__"):
value_type = from_numpy_dtype(get_dtype(reference_iterator))
state_bytes = bytes(value_type.dtype.itemsize)
elif isinstance(reference_iterator, IteratorBase):
value_type = reference_iterator.value_type
if isinstance(value_type, TypeDescriptor):
state_bytes = bytes(value_type.dtype.itemsize)
else:
state_bytes = bytes(value_type.info.size)
else:
raise TypeError("reference_iterator must be a device array or iterator")
super().__init__(
state_bytes=state_bytes,
state_alignment=value_type.alignment,
value_type=value_type,
)
def _make_advance_op(self) -> Op:
symbol = self._make_advance_symbol()
source = f"""{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void*, void*) {{
}}
"""
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[],
)
def _make_input_deref_op(self) -> Op | None:
symbol = self._make_input_deref_symbol()
source = f"""{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void*, void*) {{
}}
"""
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[],
)
def _make_output_deref_op(self) -> Op | None:
symbol = self._make_output_deref_symbol()
source = f"""{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void*, void*) {{
}}
"""
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[],
)
def __add__(self, offset: int) -> "DiscardIterator":
return DiscardIterator(self._reference_iterator)

View File

@@ -0,0 +1,220 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""PermutationIterator implementation."""
from __future__ import annotations
from textwrap import dedent
from .._bindings import Op, OpKind
from .._cpp_compile import compile_cpp_op_code, make_variable_declaration
from ._base import IteratorBase, compose_iterator_states
from ._common import CUDA_PREAMBLE, ensure_iterator
class PermutationIterator(IteratorBase):
"""
Iterator that accesses values through an index mapping.
At position i, yields values[indices[i]].
Similar to `thrust::permutation_iterator <https://nvidia.github.io/cccl/thrust/api/classthrust_1_1permutation__iterator.html>`_.
Example:
The code snippet below demonstrates accessing values through an index mapping.
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/iterator/permutation_iterator_basic.py
:language: python
:start-after: # example-begin
"""
__slots__ = [
"_values",
"_indices",
"_values_offset",
"_indices_offset",
]
def __init__(
self,
values,
indices,
):
"""
Create a permutation iterator.
Args:
values: Iterator or array providing the values to be permuted
indices: Iterator or array providing the indices for permutation
"""
# Wrap arrays in PointerIterator
self._values = ensure_iterator(values)
self._indices = ensure_iterator(indices)
# Compose states from both iterators
state_bytes, state_alignment, offsets = compose_iterator_states(
[self._values, self._indices]
)
self._values_offset = offsets[0]
self._indices_offset = offsets[1]
super().__init__(
state_bytes=state_bytes,
state_alignment=state_alignment,
value_type=self._values.value_type,
)
def _make_advance_op(self) -> Op:
"""Provide Op for advance that only advances indices iterator."""
child_op = self._indices.get_advance_op()
symbol = self._make_advance_symbol()
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {child_op.name}(void* state, void* offset);
extern "C" __device__ void {symbol}(void* state, void* offset) {{
char* indices_state = static_cast<char*>(state) + {self._indices_offset};
{child_op.name}(indices_state, offset);
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[child_op.code, *child_op.extra_code],
)
def _make_input_deref_op(self) -> Op | None:
"""Provide Op for input deref that reads index then accesses values."""
indices_deref_op = self._indices.get_input_deref_op()
if indices_deref_op is None:
raise ValueError("Indices iterator must support input dereference")
values_deref_op = self._values.get_input_deref_op()
if values_deref_op is None:
return None
# Also need values advance for random access
values_advance_op = self._values.get_advance_op()
symbol = self._make_input_deref_symbol()
idx_decl = make_variable_declaration(self._indices.value_type, "idx")
values_state_size = len(bytes(memoryview(self._values.state)))
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {indices_deref_op.name}(void* state, void* result);
extern "C" __device__ void {values_advance_op.name}(void* state, void* offset);
extern "C" __device__ void {values_deref_op.name}(void* state, void* result);
extern "C" __device__ void {symbol}(void* state, void* result) {{
char* values_state = static_cast<char*>(state) + {self._values_offset};
char* indices_state = static_cast<char*>(state) + {self._indices_offset};
{idx_decl}
{indices_deref_op.name}(indices_state, &idx);
alignas({self._values.state_alignment}) char temp_values[{values_state_size}];
memcpy(temp_values, values_state, {values_state_size});
uint64_t offset = static_cast<uint64_t>(idx);
{values_advance_op.name}(temp_values, &offset);
{values_deref_op.name}(temp_values, result);
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[
values_advance_op.code,
*values_advance_op.extra_code,
indices_deref_op.code,
*indices_deref_op.extra_code,
values_deref_op.code,
*values_deref_op.extra_code,
],
)
def _make_output_deref_op(self) -> Op | None:
"""Provide Op for output deref that reads index then writes to values."""
indices_deref_op = self._indices.get_input_deref_op()
if indices_deref_op is None:
raise ValueError("Indices iterator must support input dereference")
values_deref_op = self._values.get_output_deref_op()
if values_deref_op is None:
return None
# Also need values advance for random access
values_advance_op = self._values.get_advance_op()
symbol = self._make_output_deref_symbol()
idx_decl = make_variable_declaration(self._indices.value_type, "idx")
values_state_size = len(bytes(memoryview(self._values.state)))
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {indices_deref_op.name}(void* state, void* result);
extern "C" __device__ void {values_advance_op.name}(void* state, void* offset);
extern "C" __device__ void {values_deref_op.name}(void* state, void* value);
extern "C" __device__ void {symbol}(void* state, void* value) {{
char* values_state = static_cast<char*>(state) + {self._values_offset};
char* indices_state = static_cast<char*>(state) + {self._indices_offset};
{idx_decl}
{indices_deref_op.name}(indices_state, &idx);
alignas({self._values.state_alignment}) char temp_values[{values_state_size}];
memcpy(temp_values, values_state, {values_state_size});
uint64_t offset = static_cast<uint64_t>(idx);
{values_advance_op.name}(temp_values, &offset);
{values_deref_op.name}(temp_values, value);
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[
values_advance_op.code,
*values_advance_op.extra_code,
indices_deref_op.code,
*indices_deref_op.extra_code,
values_deref_op.code,
*values_deref_op.extra_code,
],
)
@property
def children(self):
return (self._values, self._indices)
def __add__(self, offset: int) -> "PermutationIterator":
"""Advance the indices iterator by offset, keeping values at base."""
return PermutationIterator(
self._values, # values stays at base for random access
self._indices + offset, # only indices advances # type: ignore[operator]
)
@property
def kind(self):
"""Return a hashable kind for caching purposes."""
return ("PermutationIterator", self._values.kind, self._indices.kind)

View File

@@ -0,0 +1,191 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""PointerIterator implementation - simple bidirectional iterator for device arrays."""
from __future__ import annotations
import ctypes
import sys
from textwrap import dedent
from .._bindings import Op, OpKind
from .._cpp_compile import compile_cpp_op_code, cpp_type_from_descriptor
from .._utils.protocols import get_data_pointer, get_dtype
from ..types import from_numpy_dtype
from ._base import IteratorBase
from ._common import CUDA_PREAMBLE
class PointerIterator(IteratorBase):
"""
Simple iterator wrapping a device array pointer.
Supports both input (reading) and output (writing) operations.
Handles both scalar types (using typed C++ code) and struct types
(using byte-level memcpy).
"""
def __init__(self, array):
"""
Create a pointer iterator from a device array.
Args:
array: Device array with __cuda_array_interface__
"""
# Get pointer and dtype from array
ptr = get_data_pointer(array)
dtype = get_dtype(array)
value_type = from_numpy_dtype(dtype)
# State is just the pointer
state_bytes = ctypes.c_void_p(ptr)
state_bytes_buffer = (ctypes.c_char * 8)()
ctypes.memmove(state_bytes_buffer, ctypes.byref(state_bytes), 8)
state_bytes = bytes(state_bytes_buffer)
self._cpp_type = cpp_type_from_descriptor(value_type) # None for struct types
self._element_size = value_type.info.size
self._array = array # Keep reference to prevent GC
super().__init__(
state_bytes=state_bytes,
state_alignment=8, # pointer alignment
value_type=value_type,
)
@property
def array(self):
return self._array
def _make_advance_op(self) -> Op:
symbol = self._make_advance_symbol()
if self._cpp_type:
# Scalar type - use typed pointer arithmetic
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void* state, void* offset) {{
auto* ptr_state = static_cast<{self._cpp_type}**>(state);
auto dist = *static_cast<int64_t*>(offset);
*ptr_state += dist;
}}
""").strip()
else:
# Struct type - use byte-level pointer arithmetic
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void* state, void* offset) {{
auto* ptr_state = static_cast<char**>(state);
auto dist = *static_cast<int64_t*>(offset);
*ptr_state += dist * {self._element_size};
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[],
)
def _make_input_deref_op(self) -> Op | None:
symbol = self._make_input_deref_symbol()
if self._cpp_type:
# Scalar type - use typed dereference
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void* state, void* result) {{
auto* ptr_state = static_cast<{self._cpp_type}**>(state);
*static_cast<{self._cpp_type}*>(result) = **ptr_state;
}}
""").strip()
else:
# Struct type - use memcpy
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void* state, void* result) {{
auto* ptr_state = static_cast<char**>(state);
memcpy(result, *ptr_state, {self._element_size});
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[],
)
def _make_output_deref_op(self) -> Op | None:
symbol = self._make_output_deref_symbol()
if self._cpp_type:
# Scalar type - use typed dereference
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void* state, void* value) {{
auto* ptr_state = static_cast<{self._cpp_type}**>(state);
**ptr_state = *static_cast<{self._cpp_type}*>(value);
}}
""").strip()
else:
# Struct type - use memcpy
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {symbol}(void* state, void* value) {{
auto* ptr_state = static_cast<char**>(state);
memcpy(*ptr_state, value, {self._element_size});
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[],
)
def __add__(self, offset: int):
dtype = get_dtype(self._array)
offset_ptr = self._current_pointer() + offset * dtype.itemsize
return self._clone_with_pointer(offset_ptr)
def _current_pointer(self) -> int:
return int.from_bytes(self._state_bytes, sys.byteorder, signed=False)
def _clone_with_pointer(self, pointer_value: int):
"""Clone this iterator with a different pointer value."""
clone = PointerIterator(self._array)
state_bytes_buffer = (ctypes.c_char * 8)()
ptr_obj = ctypes.c_void_p(pointer_value)
ctypes.memmove(state_bytes_buffer, ctypes.byref(ptr_obj), 8)
clone._state_bytes = bytes(state_bytes_buffer)
clone._uid_cached = None
return clone
@property
def kind(self):
"""
Return a hashable kind for caching purposes.
Include _cpp_type and _element_size since they affect generated code.
Different code paths are taken for scalar vs struct types.
"""
return (
type(self).__name__,
self._value_type,
self._cpp_type,
self._element_size,
)

View File

@@ -0,0 +1,140 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""ReverseIterator implementation."""
from __future__ import annotations
from textwrap import dedent
from .._bindings import Op, OpKind
from .._cpp_compile import compile_cpp_op_code
from .._utils.protocols import get_size, is_device_array
from ._base import IteratorBase
from ._common import CUDA_PREAMBLE, ensure_iterator
class ReverseIterator(IteratorBase):
"""
Iterator that reverses the direction of an underlying iterator.
Advance with positive offset moves backward in the underlying iterator.
"""
__slots__ = [
"_underlying",
]
def __init__(self, underlying):
"""
Create a reverse iterator.
Args:
underlying: The underlying iterator or array to reverse
"""
if is_device_array(underlying):
# TODO: this is probably incorrect behaviour. In C++, initializing
# with a pointer to the end of the array is left to be done explicitly
# by the user.
self._underlying = ensure_iterator(underlying) + (get_size(underlying) - 1)
else:
self._underlying = ensure_iterator(underlying)
super().__init__(
state_bytes=bytes(self._underlying.state),
state_alignment=self._underlying.state_alignment,
value_type=self._underlying.value_type,
)
def _make_advance_op(self) -> Op:
"""Provide Op for advance that negates offset direction."""
child_op = self._underlying.get_advance_op()
symbol = self._make_advance_symbol()
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {child_op.name}(void* state, void* offset);
extern "C" __device__ void {symbol}(void* state, void* offset) {{
int64_t neg_offset = -static_cast<int64_t>(*static_cast<uint64_t*>(offset));
{child_op.name}(state, &neg_offset);
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[child_op.code, *child_op.extra_code],
)
def _make_input_deref_op(self) -> Op | None:
"""Provide Op for input dereference that delegates to underlying."""
child_op = self._underlying.get_input_deref_op()
if child_op is None:
return None
symbol = self._make_input_deref_symbol()
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {child_op.name}(void* state, void* result);
extern "C" __device__ void {symbol}(void* state, void* result) {{
{child_op.name}(state, result);
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[child_op.code, *child_op.extra_code],
)
def _make_output_deref_op(self) -> Op | None:
"""Provide Op for output dereference that delegates to underlying."""
child_op = self._underlying.get_output_deref_op()
if child_op is None:
return None
symbol = self._make_output_deref_symbol()
source = dedent(f"""
{CUDA_PREAMBLE}
extern "C" __device__ void {child_op.name}(void* state, void* value);
extern "C" __device__ void {symbol}(void* state, void* value) {{
{child_op.name}(state, value);
}}
""").strip()
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[child_op.code, *child_op.extra_code],
)
@property
def children(self):
return (self._underlying,)
@property
def kind(self):
"""Return a hashable kind for caching purposes."""
return ("ReverseIterator", self._underlying.kind)
def __add__(self, offset: int):
return ReverseIterator(self._underlying + offset)

View File

@@ -0,0 +1,148 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""ShuffleIterator implementation."""
from __future__ import annotations
import struct
from textwrap import dedent
import numpy as np
from .._bindings import Op, OpKind
from .._cpp_compile import compile_cpp_op_code
from ..types import from_numpy_dtype
from ._base import IteratorBase
from ._common import CUDA_PREAMBLE
_SHUFFLE_STATE_STRUCT = """\
struct ShuffleState {
int64_t current_index;
uint64_t num_items;
uint64_t seed;
};"""
class ShuffleIterator(IteratorBase):
"""
Iterator that produces a deterministic random permutation of indices.
At position ``i``, yields ``bijection(i)`` where the bijection is a random
permutation of ``[0, num_items)`` parameterized by ``seed``.
Example:
The code snippet below demonstrates the usage of a ``ShuffleIterator``
to randomly permute indices:
.. literalinclude:: ../../python/cuda_cccl/tests/compute/examples/iterator/shuffle_iterator_basic.py
:language: python
:start-after: # example-begin
Args:
num_items: Number of elements in the domain to permute. Must be > 0.
seed: Seed for the random permutation. Different seeds produce
different (deterministic) permutations. Defaults to 0.
"""
__slots__ = ["_num_items", "_seed", "_current_index"]
def __init__(self, num_items: int, seed: int = 0, *, _current_index: int = 0):
if num_items <= 0:
raise ValueError("num_items must be > 0")
self._num_items = int(num_items)
self._seed = int(seed)
self._current_index = int(_current_index)
# State layout matches C++ ShuffleState:
# int64_t current_index (offset 0, size 8)
# uint64_t num_items (offset 8, size 8)
# uint64_t seed (offset 16, size 8)
state_bytes = struct.pack(
"<qQQ", self._current_index, self._num_items, self._seed
)
super().__init__(
state_bytes=state_bytes,
state_alignment=8,
value_type=from_numpy_dtype(np.dtype("int64")),
)
def _make_advance_op(self) -> Op:
symbol = self._make_advance_symbol()
source = dedent(f"""
{CUDA_PREAMBLE}
{_SHUFFLE_STATE_STRUCT}
extern "C" __device__ void {symbol}(void* state, void* offset) {{
auto s = static_cast<ShuffleState*>(state);
auto dist = *static_cast<int64_t*>(offset);
s->current_index += dist;
}}
""")
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[],
)
def _make_input_deref_op(self) -> Op | None:
symbol = self._make_input_deref_symbol()
# Note: a potential optimization is to avoid constructing
# `cuda::random_bijection` objects upon every dereference,
# instead constructing it once and using it as the state
# object. The tradeoff is that it would require a C++
# extension providing a constructor for
# `cuda::random_bijection` objects, since we would now be
# doing it on the host. See discussion in #7721.
source = dedent(f"""
#include <cuda/__random/random_bijection.h>
#include <cuda/__random/pcg_engine.h>
{CUDA_PREAMBLE}
{_SHUFFLE_STATE_STRUCT}
// __noinline__ is required to prevent the compiler from merging
// this function's register usage into the calling kernel during LTO
// inlining. feistel_bijection constructs 24 round keys with
// UNROLL_FULL, which exhausts the kernel's register budget and
// causes spilling to local memory (LDL/STL instructions).
// Keeping it non-inlined gives it an isolated register frame.
__device__ __noinline__ int64_t __shuffle_apply(uint64_t num_items, uint64_t seed, uint64_t idx) {{
cuda::pcg64 rng(seed);
cuda::random_bijection<uint64_t> bijection(num_items, rng);
return static_cast<int64_t>(bijection(idx));
}}
extern "C" __device__ void {symbol}(void* state, void* result) {{
const auto* s = static_cast<const ShuffleState*>(state);
*static_cast<int64_t*>(result) = __shuffle_apply(
s->num_items, s->seed, static_cast<uint64_t>(s->current_index));
}}
""")
code = compile_cpp_op_code(source)
return Op(
operator_type=OpKind.STATELESS,
name=symbol,
ltoir=code,
extra_ltoirs=[],
)
def _make_output_deref_op(self) -> Op | None:
return None
def __add__(self, offset: int) -> "ShuffleIterator":
return ShuffleIterator(
self._num_items,
self._seed,
_current_index=self._current_index + offset,
)

Some files were not shown because too many files have changed in this diff Show More