Sparse-checkout from NVIDIA/cccl main branch to complete cccl_upstream: Added: - python/cuda_cccl/ (226 files) — Python bindings for device-level algorithms Critical for muh toolchain: cuda.compute.reduce_into, scan, radix_sort, etc. Includes 204 .py files with full test coverage for all 27 algorithms - ci/ (163 files) — Build/test infrastructure build_cub.sh, test_cub.sh, build_and_test_targets.sh, matrix.yaml Directly maps to our [INFRA-CI] and [INFRA-BUILD] items - .agent/skills/ (7 files) — NVIDIA's own agent skills for CCCL cccl-style/SKILL.md, cccl-test/SKILL.md, sass-diff/SKILL.md - docs/ (491 files) — Official CCCL documentation CI references, CMake guides, Python compute docs, libcudacxx PTX docs - test/ (12 files) — Top-level integration tests (cuda_smoke, stdpar) - Root configs: .clang-format, .clang-tidy, CONTRIBUTING.md, pyproject.toml - CLAUDE.md symlink → AGENTS.md (NVIDIA's standard) cccl_upstream now mirrors full NVIDIA/cccl structure: Before: 42M (cub + thrust + libcudacxx + cudax + c + examples + benchmarks) After: 53M (+python +ci +docs +.agent +test +configs) This completes the CCCL base needed for: - [muh-bench] items: ci/util/build_and_test_targets.sh for targeted builds - [CCCL-verify] items: python/cuda_cccl/tests/ as reference implementations - [CCCL-test] items: ci/test_cub.sh, ci/test_thrust.sh - Agent workflow: .agent/skills/ for consistent style and test patterns
253 lines
8.4 KiB
Python
253 lines
8.4 KiB
Python
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
|