Skip to content

Make permute deterministic given a seed#3075

Open
says1117 wants to merge 4 commits into
NVIDIA:mainfrom
says1117:bug-make-regression-deterministic-shuffle
Open

Make permute deterministic given a seed#3075
says1117 wants to merge 4 commits into
NVIDIA:mainfrom
says1117:bug-make-regression-deterministic-shuffle

Conversation

@says1117

@says1117 says1117 commented Jul 6, 2026

Copy link
Copy Markdown

Summary

raft::random::permute drew the coefficients of its affine permutation
from the unseeded global C rand(), so results were not reproducible
across calls. This surfaced in cuML as rapidsai/cuml#7871:
make_regression(..., shuffle=True) returned different data on repeated
calls with the same random_state.

Changes

  • Add a seed parameter to permute that derives the permutation
    coefficients from a local std::mt19937_64, replacing the global rand().
  • Thread the existing make_regression seed through both the sample and
    feature shuffles (a distinct derived seed each, so the two shuffles stay
    independent).
  • The public permute overloads default seed = 0, so existing callers
    are unaffected.
  • Add PermSeedTest verifying the same seed yields identical permutations
    and different seeds yield different ones.

Testing

All 667 tests in RANDOM_TEST pass locally (RTX 2060, CUDA arch 75).

Relates to rapidsai/cuml#7871

permute() drew its affine permutation coefficients from the unseeded global rand(), so make_regression produced different output on repeated calls with the same random_state (cuML issue #7871). Derive the coefficients from a local std::mt19937_64 seeded per call, thread the seed through make_regression's row and feature shuffles, and add a determinism test. Public wrappers default the seed so existing callers are unaffected.
@copy-pr-bot

copy-pr-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

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.

@says1117

says1117 commented Jul 6, 2026

Copy link
Copy Markdown
Author

This is a non-breaking bug fix (the new permute seed parameter is defaulted, so the public API stays backward-compatible). Could a maintainer add the bug and non-breaking labels? Thanks!

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added an optional seed parameter to permutation operations to enable deterministic, repeatable results for the same inputs.
  • Bug Fixes
    • Updated permutation generation to use caller-controlled RNG (and a seed-based engine), improving consistency and reproducibility across runs.
  • Tests
    • Added coverage to verify permutations match exactly with the same seed and change when the seed differs.

Walkthrough

The random permute APIs now accept optional seeds and use standard RNG engines for deterministic permutation generation. Regression shuffling shares an engine across permutations, and parameterized tests validate reproducibility and seed variation.

Changes

Seeded deterministic permutation

Layer / File(s) Summary
Deterministic RNG in detail::permute
cpp/include/raft/random/detail/permute.cuh
Accepts a caller-provided std::mt19937_64 and uses uniform draws to generate permutation coefficients while preserving the coprimality adjustment.
Public permute overloads forward seed
cpp/include/raft/random/permute.cuh
The mdspan and legacy raw-pointer overloads gain optional seeds, construct generators with deterministic or random-device-derived seeds, and forward them to the detail implementation.
Regression shuffle integration and determinism tests
cpp/include/raft/random/detail/make_regression.cuh, cpp/tests/random/permute.cu
Regression shuffling shares a seeded generator across sample and feature permutations; parameterized tests check same-seed equality and different-seed divergence for larger inputs.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the core change: making permute reproducible when a seed is provided.
Description check ✅ Passed The description matches the changeset and correctly explains the seed parameter, make_regression threading, and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cpp/include/raft/random/permute.cuh (1)

139-165: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Convenience permute overload doesn't forward seed.

This overload (used when passing std::nullopt for permsOut/out) never accepts or forwards a seed, so any caller through this entry point is silently locked to seed = 0 and cannot opt into deterministic seeding, unlike the other two updated overloads.

🐛 Proposed fix
 template <typename InputOutputValueType,
           typename IdxType,
           typename Layout,
           typename PermsOutType,
           typename OutType>
 void permute(raft::resources const& handle,
              raft::device_matrix_view<const InputOutputValueType, IdxType, Layout> in,
              PermsOutType&& permsOut,
-             OutType&& out)
+             OutType&& out,
+             uint64_t seed = 0ULL)
 {
   ...
   std::optional<perms_out_view_type> permsOut_arg = std::forward<PermsOutType>(permsOut);
   std::optional<out_view_type> out_arg            = std::forward<OutType>(out);
-  permute(handle, in, permsOut_arg, out_arg);
+  permute(handle, in, permsOut_arg, out_arg, seed);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/include/raft/random/permute.cuh` around lines 139 - 165, The convenience
permute overload in permute.cuh is missing seed support, so callers using the
std::nullopt path are forced to the default seed. Update this template overload
of permute to accept a seed parameter and forward it into the inner permute
call, matching the behavior of the other permute overloads; use the permute
function signature and the permsOut/out forwarding logic to locate the change.
🧹 Nitpick comments (2)
cpp/include/raft/random/permute.cuh (1)

57-94: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Doxygen not updated for new seed parameter.

Both public overloads gained a seed param (lines 94 and 196) but their doc comments (lines 57-88 and 169-187) were not updated with an @param[in] seed entry describing default/determinism semantics.

As per path instructions, "For public headers under cpp/include/raft ... also require Doxygen on new APIs and deprecation warnings on breaking changes."

Also applies to: 169-199

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/include/raft/random/permute.cuh` around lines 57 - 94, Update the Doxygen
for the public permute overloads so the new seed argument is documented in both
declarations of permute. Add an `@param`[in] seed entry alongside the existing
handle/in/permsOut/out docs, and describe its default behavior and how it
affects determinism so the public API comment stays in sync with the signature.

Source: Path instructions

cpp/tests/random/permute.cu (1)

345-411: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test_data_type alias instead of hardcoding float in the TEST_P body.

PermSeedTest<T>::SetUp() correctly uses T for the permute<T, int, int>(...) calls (lines 371-374), but the TEST_P(PermSeedTestF, SameSeedIsDeterministic) body hardcodes permute<float, int, int>(...) (line 401) instead of following the using test_data_type = T; pattern already used by sibling fixtures PermTest/PermMdspanTest (lines 37, 85). This works today only because PermSeedTest is solely instantiated for float; extending coverage to double (matching the existing PermTestD/PermMdspanTestD symmetry) would require copy-pasting this class and updating the hardcoded type, which is easy to overlook until it fails to compile.

♻️ Proposed refactor
 template <typename T>
 class PermSeedTest : public ::testing::TestWithParam<PermInputs<T>> {
+ public:
+  using test_data_type = T;
+
  protected:
   ...
 };

 using PermSeedTestF = PermSeedTest<float>;
 TEST_P(PermSeedTestF, SameSeedIsDeterministic)
 {
+  using test_data_type = PermSeedTestF::test_data_type;
   auto stream = resource::get_cuda_stream(handle);
   ...
   if (N >= 1024) {
     rmm::device_uvector<int> perms3(N, stream);
-    permute<float, int, int>(
+    permute<test_data_type, int, int>(
       perms3.data(), out.data(), in.data(), params.D, N, params.rowMajor, stream, params.seed + 1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/tests/random/permute.cu` around lines 345 - 411, Replace the hardcoded
float in the PermSeedTest fixture with a test_data_type alias, matching the
pattern used by PermTest and PermMdspanTest, and use that alias in the TEST_P
body. Update PermSeedTest<T> to define test_data_type = T, then change the
permute<float, int, int>(...) call in SameSeedIsDeterministic to
permute<test_data_type, int, int>(...) so the fixture stays generic and can be
reused for double without copy-pasting.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cpp/include/raft/random/permute.cuh`:
- Around line 139-165: The convenience permute overload in permute.cuh is
missing seed support, so callers using the std::nullopt path are forced to the
default seed. Update this template overload of permute to accept a seed
parameter and forward it into the inner permute call, matching the behavior of
the other permute overloads; use the permute function signature and the
permsOut/out forwarding logic to locate the change.

---

Nitpick comments:
In `@cpp/include/raft/random/permute.cuh`:
- Around line 57-94: Update the Doxygen for the public permute overloads so the
new seed argument is documented in both declarations of permute. Add an
`@param`[in] seed entry alongside the existing handle/in/permsOut/out docs, and
describe its default behavior and how it affects determinism so the public API
comment stays in sync with the signature.

In `@cpp/tests/random/permute.cu`:
- Around line 345-411: Replace the hardcoded float in the PermSeedTest fixture
with a test_data_type alias, matching the pattern used by PermTest and
PermMdspanTest, and use that alias in the TEST_P body. Update PermSeedTest<T> to
define test_data_type = T, then change the permute<float, int, int>(...) call in
SameSeedIsDeterministic to permute<test_data_type, int, int>(...) so the fixture
stays generic and can be reused for double without copy-pasting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 870a805f-b734-4504-92c6-79a4fe214946

📥 Commits

Reviewing files that changed from the base of the PR and between e90dcc3 and e2b3ce7.

📒 Files selected for processing (4)
  • cpp/include/raft/random/detail/make_regression.cuh
  • cpp/include/raft/random/detail/permute.cuh
  • cpp/include/raft/random/permute.cuh
  • cpp/tests/random/permute.cu

@viclafargue

Copy link
Copy Markdown
Contributor

cc @vinaydes

@achirkin achirkin 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.

Thanks for the PR! It's a clear improvement indeed, yet I have couple minor suggestions.

n_cols,
false,
stream,
seed + 1); // different derived seed for feature shuffle keeps sample and feature independent

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.

Maybe it would be a bit cleaner to pass a single generator by reference to the two functions here in place of the integer seed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the suggestion! I'll build one std::mt19937_64 in make_regression_caller and pass it by reference to both permute calls instead of deriving seed/seed + 1.

Comment thread cpp/include/raft/random/permute.cuh Outdated
std::optional<raft::device_vector_view<IntType, IdxType>> permsOut,
std::optional<raft::device_matrix_view<InputOutputValueType, IdxType, Layout>> out)
std::optional<raft::device_matrix_view<InputOutputValueType, IdxType, Layout>> out,
uint64_t seed = 0ULL)

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.

I think, this is a "slightly breaking" change: the previous behavior was non-deterministic and the new behavior is deterministic. Shall we pass the seed as std::optional<> instead (uint64_t or an optional generator type by value) and default to rand() producing the seed non-deterministically?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'll change it to std::optional<uint64_t> seed = std::nullopt, falling back to rand() when unset so existing callers keep their current non-deterministic behavior. Will push an update soon.

@achirkin achirkin added enhancement New feature or request non-breaking Non-breaking change labels Jul 9, 2026
…llback. Corrected function parameters from seed to gen.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
cpp/include/raft/random/permute.cuh (1)

90-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the new seed parameter (Doxygen).

The public mdspan overload gained std::optional<uint64_t> seed, but the Doxygen block (Lines 58-89) has no @param[in] seed. Public headers require Doxygen for new API surface. The legacy overload (Lines 173-200) is missing it too.

As per path instructions: "require Doxygen on new APIs and deprecation warnings on breaking changes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/include/raft/random/permute.cuh` around lines 90 - 95, The public permute
API in permute.cuh now accepts an optional seed, but the Doxygen for the mdspan
overload and the legacy overload still omits documentation for this new
parameter. Update the comments above the permute overloads to add an `@param`[in]
entry for seed, using the same symbol names (permute, seed) so the new API
surface is fully documented in the public header.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/include/raft/random/permute.cuh`:
- Around line 125-126: The fallback seed in the permute initialization currently
relies on rand(), which uses shared global state and can race across concurrent
calls. Update the seed generation in the permute path to use a per-call
thread-safe random source instead of rand(), and make sure the same change is
applied consistently in both permute overloads so the unseeded behavior is
stable and thread-safe. Use the permute function and its seed initialization
block as the place to fix this.

---

Nitpick comments:
In `@cpp/include/raft/random/permute.cuh`:
- Around line 90-95: The public permute API in permute.cuh now accepts an
optional seed, but the Doxygen for the mdspan overload and the legacy overload
still omits documentation for this new parameter. Update the comments above the
permute overloads to add an `@param`[in] entry for seed, using the same symbol
names (permute, seed) so the new API surface is fully documented in the public
header.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 941e2100-285a-4a03-807c-84ca8411fd79

📥 Commits

Reviewing files that changed from the base of the PR and between e2b3ce7 and 5021723.

📒 Files selected for processing (3)
  • cpp/include/raft/random/detail/make_regression.cuh
  • cpp/include/raft/random/detail/permute.cuh
  • cpp/include/raft/random/permute.cuh

Comment thread cpp/include/raft/random/permute.cuh Outdated
@says1117

says1117 commented Jul 9, 2026

Copy link
Copy Markdown
Author

Pushed an update addressing the review feedback:

  • make_regression_caller now builds a single std::mt19937_64 generator
    and passes it by reference to both the row-shuffle and column-shuffle
    permute calls, rather than deriving seed/seed + 1 separately.
  • seed is now std::optional<uint64_t> (default std::nullopt) across all
    three permute overloads. When unset it falls back to rand(), so
    existing callers keep their previous non-deterministic behavior; passing a
    seed opts into reproducible shuffles.
  • Also addressed the CodeRabbit findings: the std::nullopt-convenience
    overload now accepts and forwards seed too (previously it was stuck on
    the default with no way to opt in), added missing @param[in] seed
    Doxygen to all three overloads, and replaced a hardcoded float in
    PermSeedTest's TEST_P body with the test_data_type alias to match
    the sibling fixtures.
    Verified: RANDOM_TEST builds clean and all 667 tests pass.

@vinaydes

vinaydes commented Jul 9, 2026

Copy link
Copy Markdown
Member

Thanks @viclafargue for tagging me.

Thanks @says1117 for this PR. I also have been working on overhauling the RAFT permute. Unfortunately, the affine transformation (ax + b) used by permute is not a good strategy. It suffers from biasing. A permute can be viewed as a bijective (thus invertible) function that maps index i to F(i). The bijectivity ensures no collision. Ideally we would want this function to behave as a keyed random function. We can measure the "randomness" of this mapping function by looking at the avalanche property, much like hash functions. In short it measures the probability of changing j th output bit when i th input bit is flipped for all is and js. An ideal function would have each probability close to 0.5. The affine transform performs very poorly on avalanche metric. Following is the heat-map of avalanche probabilities for N = 2^24

bias_lcg64_n24_heatmap

I used Feistel network based hashing to get near perfect avalanche. See more in my PR #3079

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cpp/tests/random/permute.cu (1)

400-411: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

HIGH: Avoid the probabilistic different-seed assertion.

ASSERT_NE(h1, h3) can fail when distinct seeds produce the same affine permutation; for N == 1024, there are only N * φ(N) = 524,288 possible outputs. Have you considered using a deterministic golden-output check or dropping this assertion while retaining same-seed reproducibility and permutation-validity checks?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/tests/random/permute.cu` around lines 400 - 411, Replace the
probabilistic ASSERT_NE(h1, h3) check in the N >= 1024 branch with a
deterministic golden-output assertion, or remove the different-seed comparison
entirely. Retain the existing same-seed reproducibility and permutation-validity
checks, and remove any now-unused perms3/h3 setup if the assertion is dropped.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cpp/tests/random/permute.cu`:
- Around line 400-411: Replace the probabilistic ASSERT_NE(h1, h3) check in the
N >= 1024 branch with a deterministic golden-output assertion, or remove the
different-seed comparison entirely. Retain the existing same-seed
reproducibility and permutation-validity checks, and remove any now-unused
perms3/h3 setup if the assertion is dropped.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 399ac9b7-212d-4366-8611-0c73c3b52f16

📥 Commits

Reviewing files that changed from the base of the PR and between c479663 and 9f84b8b.

📒 Files selected for processing (4)
  • cpp/include/raft/random/detail/make_regression.cuh
  • cpp/include/raft/random/detail/permute.cuh
  • cpp/include/raft/random/permute.cuh
  • cpp/tests/random/permute.cu
🚧 Files skipped from review as they are similar to previous changes (2)
  • cpp/include/raft/random/detail/permute.cuh
  • cpp/include/raft/random/detail/make_regression.cuh

@says1117

Copy link
Copy Markdown
Author

Thanks for the detailed writeup and the heatmap @vinaydes, that's a much deeper fix than what I was going for here. This PR was purely aimed at closing the determinism gap flagged in rapidsai/cuml#7871 (same seed to same shuffle), not at fixing the affine transform's statistical quality, so it looks like #3079 supersedes what I'm doing here once it lands. Happy to either: (a) keep this as a narrow, low-risk fix to unblock cuml now while #3079 is still in progress, or (b) close this in favor of #3079 if you'd rather consolidate. No strong preference either way. Let me know which you'd prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request non-breaking Non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants