Skip to content

[CUB] Make ReduceByKey run-to-run deterministic - #11032

Open
ahmedtaha100 wants to merge 6 commits into
NVIDIA:mainfrom
ahmedtaha100:agent/issue-9995-reduce-by-key-determinism
Open

[CUB] Make ReduceByKey run-to-run deterministic#11032
ahmedtaha100 wants to merge 6 commits into
NVIDIA:mainfrom
ahmedtaha100:agent/issue-9995-reduce-by-key-determinism

Conversation

@ahmedtaha100

@ahmedtaha100 ahmedtaha100 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

@nanan-nvidia @guillaume-michel

Description

closes #9995

DeviceReduce::ReduceByKey can produce different FP64 result bits across identical calls when a segment spans multiple tiles. Cross-tile partial reductions are combined in an order that can depend on tile completion.

This change uses the stable ordering already supported by TilePrefixCallbackOp for floating-point cuda::std::plus. The stable-order requirement is passed separately from the tuning policy. The environment overload defaults to run_to_run, and not_guaranteed opts out. Integral operations with known CUDA operators and primitive min/max keep the existing path. Unsupported determinism requests fail at compile time. The legacy temporary-storage overload also uses stable ordering for floating-point cuda::std::plus. The shared kernel defaults to the previous ordering, so RLE and the deprecated dispatcher retain their current behavior. The virtual shared-memory sizing path uses the same flag as the kernel.

The regression reproduces the reported 65,536-item input with a 50,000-item first run and compares that aggregate bit-for-bit across 20 identical calls. It uses cuda::std::plus and runs in one generated test shard. Existing environment tests cover run_to_run and not_guaranteed, and one compile-fail test covers the two unsupported determinism cases. Existing parameterized tests cover the run count and complete output correctness.

Verification

  • The exact reproducer with cuda::std::plus produces one aggregate bit pattern across all 200 calls on an RTX 5090.
  • The C++20 matrix passes 4,697 assertions across 41 test cases, and the C++17 matrix passes 1,332 assertions across 33 test cases.
  • Both compile-fail cases pass under C++17 and C++20.
  • The reproducer compiles for sm75, sm90, and sm120. Runtime execution was available for sm120.
  • Repository pre-commit hooks pass for all seven changed files.

Performance

Measurements used CUDA 13.3 on the RTX 5090. The benchmark used one 2^24-item workload with 256-item runs and three order-balanced process pairs.

Case Base median (us) Patched median (us) Median paired change Maximum sample half-range
FP64 cuda::std::plus, default run_to_run 176.818 196.082 +10.90% 0.35%
FP64 cuda::std::plus, not_guaranteed 176.640 176.910 +0.05% 0.24%
int64 cuda::std::plus, default 171.256 171.202 +0.00% 0.24%
FP64 cuda::minimum, default 177.016 176.994 -0.07% 0.25%

The last three cases stay within their measured sample variation.

Checklist

  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

@ahmedtaha100
ahmedtaha100 requested a review from a team as a code owner August 27, 2026 03:10
@github-project-automation github-project-automation Bot moved this to Todo in CCCL Aug 27, 2026
@copy-pr-bot

copy-pr-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@cccl-authenticator-app cccl-authenticator-app Bot moved this from Todo to In Review in CCCL Aug 27, 2026
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 26e6cdb4-2e37-4158-af6a-ceab33905e64

📥 Commits

Reviewing files that changed from the base of the PR and between 892e0e4 and 83828fa.

📒 Files selected for processing (1)
  • cub/test/test_device_reduce_by_key_determinism_fail.cu
🚧 Files skipped from review as they are similar to previous changes (1)
  • cub/test/test_device_reduce_by_key_determinism_fail.cu

Included review availability: Your plan provides up to 12 included reviews per hour; 10 remain after this review.


📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added configurable stable reduction ordering for reduce-by-key operations.
    • Expanded deterministic reduction support for integral operations, min/max, and floating-point addition where supported.
    • Added clearer validation when requested determinism is unsupported by the operation or execution environment.
  • Bug Fixes

    • Improved consistency of reduce-by-key results across repeated runs and compatible GPU environments.
  • Tests

    • Added coverage for supported and rejected determinism scenarios.

Walkthrough

DeviceReduce::ReduceByKey now validates determinism requests and enables stable reduction ordering for supported floating-point additions. Dispatch propagates this setting through the kernel. Tests cover deterministic results and rejected operator combinations.

Changes

Reduce-by-key determinism

Layer / File(s) Summary
Determinism selection and validation
cub/cub/device/device_reduce.cuh
The API validates supported determinism and operator/type combinations. Floating-point cuda::std::plus uses stable reduction ordering. Documentation describes the supported guarantees.
Stable reduction-order propagation
cub/cub/agent/agent_reduce_by_key.cuh, cub/cub/device/dispatch/dispatch_reduce_by_key.cuh, cub/cub/device/dispatch/tuning/tuning_reduce_by_key.cuh
The selected stability setting flows from dispatch policy selection through shared-memory configuration, kernel instantiation, and TilePrefixCallbackOpT.
Determinism and diagnostic coverage
cub/test/catch2_test_device_reduce_by_key.cu, cub/test/catch2_test_device_reduce_env_api.cu, cub/test/test_device_reduce_by_key_determinism_fail.cu
Tests use standard reduction operators, cover deterministic and non-guaranteed floating-point reductions, and verify compile-time rejection of unsupported combinations.

Assessment against linked issues

Objective Addressed Explanation
[#9995] Ensure run-to-run deterministic fp64 reductions, including equal-key runs spanning multiple tiles.
[#9995] Preserve existing single-tile and exact integer-valued fp64 behavior across GPU architectures. The implementation preserves the default unstable policy and adds operator-based selection, but the provided tests do not explicitly cover single-tile behavior or multiple GPU architectures.

Suggested reviewers: srinivasyadav18

Merge Risk: ⚪ Minimal · up to 83828

The change makes floating-point ReduceByKey results deterministic by default while preserving an opt-out path and existing behavior for unaffected operations; the supplied verification shows the covered tests and benchmarks pass, so no actionable merge-blocking risk remains.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 69b6565e-8ae2-4392-844b-369f6c27dc16

📥 Commits

Reviewing files that changed from the base of the PR and between f047516 and 0720444.

📒 Files selected for processing (3)
  • cub/cub/agent/agent_reduce_by_key.cuh
  • cub/cub/device/dispatch/dispatch_reduce_by_key.cuh
  • cub/test/catch2_test_device_reduce_by_key.cu

Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review.

Comment thread cub/test/catch2_test_device_reduce_by_key.cu Outdated
Comment thread cub/test/catch2_test_device_reduce_by_key.cu Outdated
@ahmedtaha100
ahmedtaha100 force-pushed the agent/issue-9995-reduce-by-key-determinism branch from 0720444 to 009b23f Compare August 27, 2026 03:40

@gevtushenko gevtushenko left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for filing the PR! There are a couple of changes that have to be made before we merge:

Comment thread cub/cub/agent/agent_reduce_by_key.cuh Outdated
static constexpr BlockLoadAlgorithm LOAD_ALGORITHM = LoadAlgorithm;
static constexpr CacheLoadModifier LOAD_MODIFIER = LoadModifier;
static constexpr BlockScanAlgorithm SCAN_ALGORITHM = ScanAlgorithm;
static constexpr bool STABLE_REDUCTION_ORDER = StableReductionOrder;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

important: reduction order stability is not a tuning parameter. The only side-effect of changing a tuning parameter would be performance, while if user adjusts reduction order stability, results are observable in the function output.

Let's keep StableReductionOrder out of agent_reduce_by_key_policy. Instead, use DeviceScan for inspiration on how to pass it directly to dispatch:

return detail::scan::dispatch<EnforceInclusive, StableReductionOrder>(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

AccumT,
streaming_context_t>;
streaming_context_t,
/* StableReductionOrder */ true>;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical: performance impact is significant. We have to:

  • relax stability requirement when it's not needed: for certain combinations of data types and operators (say, reducing integers with plus operator, or finding min/max values on primitive types) existing implementation provides deterministic result. In this case, we can avoid performance regressions and avoid requirement on stability of reduction tree, see:

constexpr auto integral_fallback =
gpu_gpu_determinism && ::cuda::std::is_integral_v<AccumT> && (detail::is_cuda_binary_operator<ReductionOpT>);

  • give users opt out if determinism is not required: as written, determinism is always required. If user didn't need run-to-run determinism, performance regression wouldn't be welcome. We should connect this flag to environments API such that user can opt out of default run-to-run determinism and static assert if user requested gpu-to-gpu determinism and we couldn't provide it, see:

using requirements_t = ::cuda::std::execution::
__query_result_or_t<EnvT, ::cuda::execution::__get_requirements_t, ::cuda::std::execution::env<>>;
using default_determinism_t =
::cuda::std::execution::__query_result_or_t<requirements_t,
::cuda::execution::determinism::__get_determinism_t,
::cuda::execution::determinism::run_to_run_t>;

  • modify default reduce by key policy selector to enable tuning: we might want to re-tune the algorithm based on determinism requirement. Let's make a change analogous to how we treat this on scan side, see:

bool require_stable_reduction_order = false;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. updated results are in the PR description

d_values_in,
d_aggregates_out,
d_num_runs_out,
fp64_sum{},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

important: it should be cuda::std::plus, otherwise it won't compile when you introduce static assert on the interface side.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The regression now uses cuda::std::plus.

Comment on lines +180 to +193
c2h::host_vector<std::uint64_t> keys(num_items);
c2h::host_vector<double> values(num_items);
std::mt19937_64 rng{42};
std::uniform_real_distribution<double> distribution{-1.0, 1.0};

for (std::size_t i = 0; i < num_items; ++i)
{
keys[i] = i < long_run ? 0 : 1 + (i - long_run) / short_run;
values[i] = i % 7 == 0 ? distribution(rng) * 1e-2 : 0.0;
static_cast<void>(rng()); // Match the issue's integer-control RNG consumption.
}

c2h::device_vector<std::uint64_t> keys_in = keys;
c2h::device_vector<double> values_in = values;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: can we simplify this to just c2h::gen(C2H_SEED(2), values);? I assume the test will fail regarless of the pattern.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried the generator approach, but the usable variants either produced NaNs or did not reproduce the failure. I kept the smallest finite pattern that does.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6bf26672-f5de-49da-94cc-8307076a1198

📥 Commits

Reviewing files that changed from the base of the PR and between 009b23f and 892e0e4.

📒 Files selected for processing (7)
  • cub/cub/agent/agent_reduce_by_key.cuh
  • cub/cub/device/device_reduce.cuh
  • cub/cub/device/dispatch/dispatch_reduce_by_key.cuh
  • cub/cub/device/dispatch/tuning/tuning_reduce_by_key.cuh
  • cub/test/catch2_test_device_reduce_by_key.cu
  • cub/test/catch2_test_device_reduce_env_api.cu
  • cub/test/test_device_reduce_by_key_determinism_fail.cu

Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review.

Comment thread cub/test/test_device_reduce_by_key_determinism_fail.cu Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

[BUG]: DeviceReduce::ReduceByKey is not run-to-run deterministic for fp64 sums over multi-tile runs

2 participants