Skip to content

Make permute deterministic given a seed#3075

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

Make permute deterministic given a seed#3075
says1117 wants to merge 3 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

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: 739b814a-c1f3-42d5-beb9-ca56ac865976

📥 Commits

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

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

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added an optional seed parameter to permutation operations to support deterministic, repeatable permutations for the same inputs.
  • Bug Fixes
    • Updated permutation generation to use a caller-controlled RNG (instead of non-deterministic randomness), improving consistency across runs.
  • Tests
    • Added tests to confirm permutations are identical when using the same seed and differ when the seed changes.

Walkthrough

The random permute APIs now accept an optional seed and use it to drive deterministic permutation generation. Callers forward the seed through the detail implementation, regression shuffling uses explicit engines, and tests cover same-seed determinism.

Changes

Seeded deterministic permutation

Layer / File(s) Summary
Deterministic RNG in detail::permute
cpp/include/raft/random/detail/permute.cuh
Adds <random> support and switches permutation parameter generation to a caller-provided std::mt19937_64.
Public permute overloads forward seed
cpp/include/raft/random/permute.cuh
Both permute overloads gain an optional seed parameter, build a local generator, and pass it to the detail implementation.
make_regression passes explicit seeds
cpp/include/raft/random/detail/make_regression.cuh
The shuffle path creates an RNG from seed and uses it for both sample and feature permutation calls.
Determinism test for seeded permute
cpp/tests/random/permute.cu
Adds a parameterized test that checks identical seeds produce identical permutations and different seeds diverge for larger inputs, plus a copyright year update.

Estimated code review effort: 2 (Simple) | ~12 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: making permute deterministic when a seed is provided.
Description check ✅ Passed The description is directly related to the changeset and accurately discusses seeded permutation and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 on lines +125 to +126
std::mt19937_64 gen(seed.has_value() ? *seed
: rand()); // use the given seed, else a random one

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Is C rand() guaranteed thread-safe by the C++ standard?

💡 Result:

No, the C++ standard does not guarantee that rand is thread-safe [1][2]. It is implementation-defined whether a call to rand introduces data races [2]. Because rand typically relies on hidden global state (a seed) to generate its sequence, concurrent calls from multiple threads can result in data races if the implementation does not provide internal synchronization [3][4]. Developers should not assume thread safety and are generally advised to use the C++11 library facilities instead [1][2]. If is used, PRNG engines and distributions are not inherently thread-safe and should be managed (for example, by using thread_local storage) to avoid race conditions [5][6].

Citations:


🏁 Script executed:

ast-grep outline cpp/include/raft/random/permute.cuh --view expanded
sed -n '100,230p' cpp/include/raft/random/permute.cuh | cat -n

Repository: NVIDIA/raft

Length of output: 5648


Avoid rand() for the fallback seed. The unseeded path still uses shared global state, so concurrent permute calls can race and produce cross-call variability. Use a per-call thread-safe source instead, and apply the same fix to both overloads.

🤖 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 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.

Source: Path instructions

@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

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