Repository navigation
Description
Is this a duplicate?
- I confirmed there appear to be no duplicate issues for this bug and that I agree to the Code of Conduct
Type of Bug
Something else
Component
CUB
Describe the bug
Summary
cub::DeviceReduce::ReduceByKey documents:
- Provides "run-to-run" determinism for pseudo-associative reduction
(e.g., addition of floating point types) on the same GPU device.
However, results for pseudo-associative reduction may be inconsistent
from one device to a another device of a different compute-capability
because CUB can employ different tile-sizing for different architectures.
(v3.1.4,
main @ 0d919ed
— same sentence in the ReduceByKey doc block.)
Empirically this does not hold when a run of equal keys spans several
tiles: repeated invocations of the same ReduceByKey call on the
same device with byte-identical device input and the same
pre-allocated temp storage return the long run's float64 aggregate
with 5–6 distinct bit patterns across 200 repetitions, spread over the
last ~3 ulps. Short (single-tile) runs are bit-stable, and the same
experiment with integer-valued doubles (whose sums are exact under any
association order) is bit-stable — so the variation is a run-to-run
change in the floating-point combination order (presumably the
cross-tile partial combination in the decoupled look-back), not memory
corruption or a harness artifact.
Reproduced on two architectures with the same CUB version, so it is a
property of the implementation, not of a particular GPU:
| GPU | SM | Driver | Runtime | CUB_VERSION |
Result (experiment A) |
|---|---|---|---|---|---|
| NVIDIA T600 Laptop GPU | sm_75 | 13010 | 13010 | 300104 (3.1.4) | 6 distinct bit patterns / 200 reps |
| NVIDIA H100 80GB HBM3 | sm_90 | 13020 | 12080¹ | 300104 (3.1.4) | 5 distinct bit patterns / 200 reps |
¹ the H100 host links a CUDA 12.8 runtime while compiling against a
CCCL 3.1.4 include tree (NVHPC toolchain); the T600 datapoint is a pure
CUDA 13.1 stack, so the finding does not depend on the mixed toolkit.
How to Reproduce
Steps to reproduce
Compile the self-contained program below (no dependencies beyond the
toolkit's bundled CCCL) and run it:
nvcc -O2 -arch=native \
-I$CUDA_HOME/targets/x86_64-linux/include/cccl \
repro.cu -o repro
./reproThe program runs three experiments over 65536 (uint64 key, double value)
pairs, 200 repetitions each, and prints a census of the bit patterns
observed for the first run's aggregate:
- A — one 50 000-item run of key
0(values: mixed fp64 and zeros),
followed by 64-item runs. Expected: 1 bit pattern. Actual: 5–6. - B — identical keys, values replaced by small integers stored as
fp64. Integer sums below 2^53 are exact under any association order, so
any flip here would indicate corruption rather than reordering.
Actual: 1 bit pattern, equal to the sequential host sum. - C — the same fp64 values with every run 64 items (single tile).
Actual: 1 bit pattern.
Input buffers are uploaded once and checksummed before/after (reported
UNMODIFIED); the output buffer is poisoned before every repetition;
temp storage is allocated once and its size printed.
repro.cu
// Reproducer: cub::DeviceReduce::ReduceByKey is not run-to-run
// deterministic for float64 addition over a key run spanning many tiles,
// despite the documented guarantee ("Provides 'run-to-run' determinism for
// pseudo-associative reduction (e.g., addition of floating point types) on
// the same GPU device" -- cub/device/device_reduce.cuh, ReduceByKey doc
// block; present in v3.1.4 and on main).
//
// Observed (NVIDIA T600 Laptop GPU, sm_75, CUDA 13.1, CUB 3.1.4 / 300104):
// experiment A returns 5 distinct bit patterns for the long run's aggregate
// across 200 identical invocations, spread over the last ~3 ulps.
//
// Protocol:
// - keys/values are uploaded to the device ONCE and never touched again
// (verified by checksumming the input buffers before and after);
// - temp storage is allocated ONCE (constant size, printed);
// - the SAME ReduceByKey call runs REPS times on the default stream; the
// first (long) run's aggregate is compared bit-for-bit across reps.
//
// Controls (same code path, only the data changes):
// A) fp64 values, one 50k-item key run -> flips if the internal
// combination order varies between invocations;
// B) same keys, values = small integers stored as fp64. Integer sums
// below 2^53 are EXACT in any association order, so if A's flips came
// from memory corruption or a harness bug, B would flip too; if they
// come from reordered floating-point combination, B is bit-stable;
// C) fp64 values but every run fits one tile (64 items) -> bit-stable,
// isolating the nondeterminism to the cross-tile combination.
//
// Build (no dependencies beyond the CUDA toolkit's bundled CCCL):
// nvcc -O2 -arch=native \
// -I$CUDA_HOME/targets/x86_64-linux/include/cccl \
// cub_reducebykey_determinism.cu -o cub_reducebykey_determinism
// ./cub_reducebykey_determinism
#include <cub/device/device_reduce.cuh>
#include <cub/version.cuh>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <map>
#include <random>
#include <vector>
#define CK(x) \
if ((x) != cudaSuccess) { \
printf("cuda error %s @%d\n", cudaGetErrorString(x), __LINE__); \
return 1; \
}
struct Sum {
__host__ __device__ double operator()(double a, double b) const {
return a + b;
}
};
static uint64_t fnv1a(const void* p, size_t n) {
const auto* b = static_cast<const unsigned char*>(p);
uint64_t h = 1469598103934665603ULL;
for (size_t i = 0; i < n; ++i) h = (h ^ b[i]) * 1099511628211ULL;
return h;
}
static int experiment(const char* name, const std::vector<uint64_t>& keys,
const std::vector<double>& vals, int reps) {
const size_t n = keys.size();
uint64_t *d_keys, *d_ukeys;
double *d_vals, *d_aggs;
size_t* d_nruns;
CK(cudaMalloc(&d_keys, n * 8));
CK(cudaMalloc(&d_ukeys, n * 8));
CK(cudaMalloc(&d_vals, n * 8));
CK(cudaMalloc(&d_aggs, n * 8));
CK(cudaMalloc(&d_nruns, 8));
CK(cudaMemcpy(d_keys, keys.data(), n * 8, cudaMemcpyHostToDevice));
CK(cudaMemcpy(d_vals, vals.data(), n * 8, cudaMemcpyHostToDevice));
const uint64_t in_hash =
fnv1a(keys.data(), n * 8) ^ fnv1a(vals.data(), n * 8);
size_t temp_bytes = 0;
cub::DeviceReduce::ReduceByKey(nullptr, temp_bytes, d_keys, d_ukeys,
d_vals, d_aggs, d_nruns, Sum{}, n);
void* d_temp;
CK(cudaMalloc(&d_temp, temp_bytes));
std::map<uint64_t, int> observed; // aggregate[0] bit pattern -> count
size_t nruns_first = 0;
for (int rep = 0; rep < reps; ++rep) {
CK(cudaMemset(d_aggs, 0xEE, n * 8)); // poison: must be overwritten
cub::DeviceReduce::ReduceByKey(d_temp, temp_bytes, d_keys, d_ukeys,
d_vals, d_aggs, d_nruns, Sum{}, n);
uint64_t bits = 0;
size_t nruns = 0;
CK(cudaMemcpy(&bits, d_aggs, 8, cudaMemcpyDeviceToHost));
CK(cudaMemcpy(&nruns, d_nruns, 8, cudaMemcpyDeviceToHost));
if (rep == 0) nruns_first = nruns;
if (nruns != nruns_first) printf(" !! run count changed\n");
++observed[bits];
}
// Re-download the input to prove it was never modified.
std::vector<uint64_t> keys2(n);
std::vector<double> vals2(n);
CK(cudaMemcpy(keys2.data(), d_keys, n * 8, cudaMemcpyDeviceToHost));
CK(cudaMemcpy(vals2.data(), d_vals, n * 8, cudaMemcpyDeviceToHost));
const uint64_t out_hash =
fnv1a(keys2.data(), n * 8) ^ fnv1a(vals2.data(), n * 8);
// Host reference: strict left-to-right sum of run 0.
double seq = 0;
for (size_t i = 0; i < n && keys[i] == keys[0]; ++i) seq += vals[i];
printf("%s: n=%zu runs=%zu temp=%zu B, input %s, %d reps -> %zu "
"distinct bit patterns for aggregate[0]\n",
name, n, nruns_first, temp_bytes,
in_hash == out_hash ? "UNMODIFIED" : "MODIFIED(!)", reps,
observed.size());
for (const auto& [bits, count] : observed) {
double v;
memcpy(&v, &bits, 8);
printf(" %.17g (0x%016llx) x%d%s\n", v,
static_cast<unsigned long long>(bits), count,
memcmp(&v, &seq, 8) == 0 ? " == sequential host sum" : "");
}
cudaFree(d_keys);
cudaFree(d_ukeys);
cudaFree(d_vals);
cudaFree(d_aggs);
cudaFree(d_nruns);
cudaFree(d_temp);
return 0;
}
int main() {
int dev = 0;
cudaDeviceProp prop{};
CK(cudaGetDeviceProperties(&prop, dev));
int drv = 0, rt = 0;
cudaDriverGetVersion(&drv);
cudaRuntimeGetVersion(&rt);
printf("GPU: %s (sm_%d%d), driver %d, runtime %d, CUB %d\n\n", prop.name,
prop.major, prop.minor, drv, rt, CUB_VERSION);
const size_t n = 65536;
std::mt19937_64 rng(42);
std::uniform_real_distribution<double> dist(-1.0, 1.0);
// A: one 50k-item run of key 0 (fp64 values), then short runs.
std::vector<uint64_t> keys(n);
std::vector<double> vals_fp(n), vals_int(n);
for (size_t i = 0; i < n; ++i) {
keys[i] = (i < 50000) ? 0 : 1 + (i - 50000) / 64;
vals_fp[i] = (i % 7 == 0) ? dist(rng) * 1e-2 : 0.0;
vals_int[i] = static_cast<double>(static_cast<int>(rng() % 1000));
}
if (experiment("A) fp64, 50k-item run ", keys, vals_fp, 200)) return 1;
// B: identical keys, integer-valued doubles (any order sums exactly).
if (experiment("B) int-as-fp64, same run", keys, vals_int, 200)) return 1;
// C: fp64 values, all runs short (64 items, single-tile).
std::vector<uint64_t> keys_short(n);
for (size_t i = 0; i < n; ++i) keys_short[i] = i / 64;
if (experiment("C) fp64, 64-item runs ", keys_short, vals_fp, 200))
return 1;
return 0;
}Expected behavior
Observed output
T600 (sm_75, CUDA 13.1):
GPU: NVIDIA T600 Laptop GPU (sm_75), driver 13010, runtime 13010, CUB 300104
A) fp64, 50k-item run : n=65536 runs=244 temp=5119 B, input UNMODIFIED, 200 reps -> 6 distinct bit patterns for aggregate[0]
-0.042204912555673282 (0xbfa59be1de5076d2) x10
-0.042204912555673296 (0xbfa59be1de5076d4) x2
-0.04220491255567331 (0xbfa59be1de5076d6) x53
-0.042204912555673324 (0xbfa59be1de5076d8) x84
-0.042204912555673338 (0xbfa59be1de5076da) x5
-0.042204912555673352 (0xbfa59be1de5076dc) x46
B) int-as-fp64, same run: n=65536 runs=244 temp=5119 B, input UNMODIFIED, 200 reps -> 1 distinct bit patterns for aggregate[0]
24857026 (0x4177b49c20000000) x200 == sequential host sum
C) fp64, 64-item runs : n=65536 runs=1024 temp=5119 B, input UNMODIFIED, 200 reps -> 1 distinct bit patterns for aggregate[0]
0.024264975624164437 (0x3f98d8eaf2f9e148) x200
H100 (sm_90):
GPU: NVIDIA H100 80GB HBM3 (sm_90), driver 13020, runtime 12080, CUB 300104
A) fp64, 50k-item run : n=65536 runs=244 temp=5119 B, input UNMODIFIED, 200 reps -> 5 distinct bit patterns for aggregate[0]
-0.042204912555673296 (0xbfa59be1de5076d4) x6
-0.04220491255567331 (0xbfa59be1de5076d6) x85
-0.042204912555673324 (0xbfa59be1de5076d8) x26
-0.042204912555673338 (0xbfa59be1de5076da) x11
-0.042204912555673352 (0xbfa59be1de5076dc) x72
B) int-as-fp64, same run: n=65536 runs=244 temp=5119 B, input UNMODIFIED, 200 reps -> 1 distinct bit patterns for aggregate[0]
24857026 (0x4177b49c20000000) x200 == sequential host sum
C) fp64, 64-item runs : n=65536 runs=1024 temp=5119 B, input UNMODIFIED, 200 reps -> 1 distinct bit patterns for aggregate[0]
0.024264975624164437 (0x3f98d8eaf2f9e148) x200
Expected behavior
Per the documentation, identical invocations on the same device should
return bit-identical aggregates, including for floating-point addition
("pseudo-associative reduction").
Reproduction link
No response
Operating System
No response
nvidia-smi output
No response
NVCC version
No response
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsIn Review
Activity
github-actions commentedon Jul 17, 2026
Hi @guillaume-michel!
Thanks for submitting this issue - the CCCL team has been notified and we'll get back to you as soon as we can!
In the mean time, feel free to add any relevant information to this issue.
gevtushenko commentedon Jul 17, 2026
Hello @guillaume-michel and thank you for filing the issue!
It was identified before NVIDIA/cub#441. I forgot to change reduce docs when fixing this in NVIDIA/cub#432. Since then, @srinivasyadav18 implemented deterministic scan by using new scan technique (lookahead) that performs similarly to non-deterministic one. Instead of fixing docs, I'd like us to try and make reduce by key deterministic to evaluate the performance impact. If performance difference is tolerable, we could keep the doc.
@nanan-nvidia is currently integrating lookahead into run length encode. It's very similar to reduce by key, so we should be able to re-use a lot of code there.
ahmedtaha100 commentedon Aug 26, 2026
Is anyone currently adapting the Run Length Encode lookahead implementation to ReduceByKey, or would an external contribution addressing this issue be welcome? @gevtushenko
gevtushenko commentedon Aug 27, 2026
@nanan-nvidia imlemented initial PoC in #10680. We haven't planned productization of his lookahead implementation yet.
External contributions are always welcome! The issue covers a couple of limitations. If you are interested in trying to address them, please, comment on #10680 and I'll assign to you.
ahmedtaha100 commentedon Aug 27, 2026
Just left a comment, thank you!