[CCCL] 瘦身 + 补全: 移除 cudax/python/libcudacxx-tests 冗余文件, 新增 c2h 测试助手 + cmake 构建系统 + 8 个 CUDA thrust examples
变更摘要:
- 删除: cudax/ (783 files, 7.2M) — 实验性组件,竞赛不需要
- 删除: python/ (226 files, 2.0M) — Python 绑定,竞赛不需要
- 删除: libcudacxx/{test,benchmarks,codegen,cmake,share} (4432 files, 31M)
保留: libcudacxx/include/ (1463 headers, cuda::std 编译依赖)
- 新增: c2h/ (27 files) — CUB Catch2 测试辅助头文件,编译 243 个测试必需
- 新增: cmake/ (29 files) — CCCL 原生 CMake 构建系统
- 新增: thrust/examples/cuda/ (7 files) + cpp_integration/ (1 file)
async_reduce, custom_temporary_allocation, explicit_cuda_stream,
global_device_vector, range_view, unwrap_pointer, wrap_pointer, device
结果: cccl_upstream 从 74M→35M (瘦身 53%), 核心内容 100% 保留:
27/27 tuning headers, 78 benchmarks, 243 tests,
60 thrust examples, 18 CUB examples, 全部编译头文件
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
# Build artifacts
|
||||
build/
|
||||
bin/
|
||||
*.o
|
||||
*.so
|
||||
*.a
|
||||
compile_commands.json
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
# Benchmark results
|
||||
results*/
|
||||
|
||||
# Pixi
|
||||
.pixi/
|
||||
pixi.lock
|
||||
@@ -1,12 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,147 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,132 +0,0 @@
|
||||
# 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
@@ -1,95 +0,0 @@
|
||||
# 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,
|
||||
)
|
||||
@@ -1,97 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,110 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,138 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,89 +0,0 @@
|
||||
# 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"
|
||||
@@ -1,130 +0,0 @@
|
||||
# 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"
|
||||
@@ -1,97 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,109 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,82 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,85 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,87 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,80 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,528 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,83 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,84 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,165 +0,0 @@
|
||||
# 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 (1–16 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 (32–256 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)
|
||||
@@ -1,167 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,119 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,137 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,237 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,105 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,94 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,72 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,89 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,138 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,86 +0,0 @@
|
||||
#
|
||||
# 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)
|
||||
@@ -1,252 +0,0 @@
|
||||
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
|
||||
Reference in New Issue
Block a user