Skip to content

[FEA] permute: replace LCG with keyed Feistel permutation, add deterministic key API#3079

Open
vinaydes wants to merge 23 commits into
NVIDIA:mainfrom
vinaydes:vd/permute-fixes
Open

[FEA] permute: replace LCG with keyed Feistel permutation, add deterministic key API#3079
vinaydes wants to merge 23 commits into
NVIDIA:mainfrom
vinaydes:vd/permute-fixes

Conversation

@vinaydes

@vinaydes vinaydes commented Jul 9, 2026

Copy link
Copy Markdown
Member

The previous permute() implementation used a linear-congruential generator (LCG) -- (a * tid + b) % N -- seeded from rand() to map output indices to input indices. This had two significant problems:

  • Non-reproducible: rand() is seeded by global state, so results varied between runs and between callers. There was no way to request the same permutation twice.
  • LCG is not a very good randomizer causing biases

This PR replaces the LCG with a keyed Feistel permutation built from a 4-round network whose round function is a reduced version of MurmurHash3 32-bit finalizer (fmix32). The domain is extended to the next power of two and cycle walking is used to map results back into [0, N) without any modulo bias.

API Changes

Both the legacy pointer API and the mdspan API gain a required uint64_t key parameter. The same (key, N) pair always produces the same permutation, giving full reproducibility.

// legacy pointer API
raft::random::permute(perms, out, in, D, N, rowMajor, stream, key);

// mdspan API
raft::random::permute(handle, in, permsOut, out, key);

Callers that only want the permutation index array (no data shuffle) can pass out = nullptr; this now dispatches to a dedicated permsOnlyKernel with ITEMS_PER_THREAD = 8 for higher throughput on large index arrays.

Implementation Details

detail/permute.cuh

  • fmix32(h): Reudced version of MurmurHash3 finalizer; used as the Feistel round function.
  • kperm_params: host-precomputed schedule (split widths a_bits/b_bits, per-round key prefixes). Computed once per permute() call on the host and passed as a kernel argument, so all threads share it without extra loads.
  • make_kperm_params(N, key): builds the key schedule.
  • kperm_mix_nbits<WordType>(m, p): 4-round unrolled Feistel step.
  • kperm_index<IdxType>(idx, p): cycle-walking wrapper
  • permsOnlyKernel: new kernel invoked when out == nullptr; processes 8 indices per thread for better occupancy on pure index-generation workloads.

detail/make_regression.cuh

Derives two distinct keys from the caller-supplied seed so the sample shuffle and the feature shuffle get independent permutations:

const uint64_t samples_key  = seed;
const uint64_t features_key = seed ^ 0x9e3779b97f4a7c15ULL;

Tests

  • All existing PermTest and PermMdspanTest cases updated to pass params.seed as the key
  • New SeedDiversity test: 16384 threads each generate a permutation keyed by base_seed + tid + 1 and count index coincidences with the reference permutation. The test asserts that the total match rate is below 5%, verifying that adjacent keys produce statistically independent permutations.

Benchmarks

  • Existing permute<float/double> benchmarks pass a fixed key (123456ULL).
  • New permute_perms_only<int/uint32_t> benchmarks measure the pure index-generation path (out = nullptr) at sizes up to 1 GiB.

Avalanche Property
A permutation of a finite set is by definition a bijection: a function that is both injective (no two inputs map to the same output) and surjective (every output is hit). A keyed hash function that is itself a bijection for every key is therefore directly usable as a permutation generator: apply it to every index in [0, N) and the results are a rearrangement with no duplicates or gaps. The Feistel construction guarantees bijectivity structurally -- each round is an invertible XOR-and-mask, so the full network is invertible regardless of what the round function does internally. The quality of the resulting permutation is measured by the avalanche property: flipping any single input bit should flip each output bit with probability close to 1/2, ensuring that nearby keys produce statistically independent permutations. I evaluated the avalanche behavior of this Feistel construction across all domain widths from 2 to 32 bits and found it satisfactory throughout.

@vinaydes
vinaydes requested a review from a team as a code owner July 9, 2026 13:44
@vinaydes
vinaydes marked this pull request as draft July 9, 2026 13:49
@copy-pr-bot

copy-pr-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jul 9, 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: 75aa9db3-805b-4045-8a2d-c738250f5ab2

📥 Commits

Reviewing files that changed from the base of the PR and between 24dcaa2 and 80a3471.

📒 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/include/raft/random/permute.cuh
  • cpp/tests/random/permute.cu

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added an explicit key parameter to permutation so results are reproducible for the same input size and key.
    • Added benchmark coverage for generating permutation indices without copying data, including “perms only” scenarios.
    • Permute behavior now uses deterministic keyed shuffles for consistent sample/feature ordering.
  • Bug Fixes

    • Improved correctness and consistency of permutation across varying input sizes.
  • Tests

    • Expanded unit tests for small sizes, row-major layouts, and multiple shuffle configurations.
    • Added coverage to verify seed diversity and prevent overly similar permutations.

Walkthrough

The permutation implementation changes from random a/b indexing to a deterministic keyed Feistel permutation. Explicit keys are added across the API and detail implementation, propagated to regression shuffles, and covered by updated benchmarks and tests.

Changes

Keyed permutation implementation and propagation

Layer / File(s) Summary
Feistel permutation core implementation
cpp/include/raft/random/detail/permute.cuh
Adds keyed Feistel mixing, cycle-walking, and scheduled kernel execution using kperm_params instead of random a/b values.
Public API key parameter
cpp/include/raft/random/permute.cuh
Adds uint64_t key to active overloads, forwards it to the detail implementation, documents deterministic selection, and preserves deprecated no-key forwarding with key zero.
Regression shuffle key derivation
cpp/include/raft/random/detail/make_regression.cuh
Derives separate keys from seed for sample and feature shuffles.
Benchmarks and permutation validation
cpp/bench/prims/random/permute.cu, cpp/tests/random/permute.cu
Passes explicit seeds, adds perms-only benchmarks, expands small-size cases, and adds seed-diversity validation.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% 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 main change: replacing LCG with a keyed Feistel permutation and adding a deterministic key API.
Description check ✅ Passed The description is clearly related to the changeset and explains the API, implementation, tests, and benchmark updates.
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.

Actionable comments posted: 1

Caution

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

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

91-96: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

HIGH: Preserve source compatibility or add deprecation coverage for the new key parameter.

Issue: Adding required uint64_t key parameters to public overloads breaks existing callers that use the previous signatures.
Why: This is a public API change in cpp/include/raft/random/permute.cuh; consider a deprecated compatibility overload or a defaulted key while callers migrate.

Suggested compatibility pattern
 void permute(raft::resources const& handle,
              raft::device_matrix_view<const InputOutputValueType, IdxType, Layout> in,
              std::optional<raft::device_vector_view<IntType, IdxType>> permsOut,
              std::optional<raft::device_matrix_view<InputOutputValueType, IdxType, Layout>> out,
              uint64_t key)
// Add a deprecated overload with the old signature that forwards to a documented default key.

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: 193-203

🤖 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 91 - 96, The new required
key parameter on the public permute overloads breaks existing callers, so
preserve source compatibility in permute by adding a deprecated compatibility
overload with the old signature that forwards to a documented default key, or
otherwise default the new parameter where appropriate. Update the affected
permute declarations/definitions in raft::random::permute to include Doxygen for
the new API and emit deprecation warnings on the old overloads so downstream
code can migrate without a hard break.

Source: Path instructions

cpp/include/raft/random/detail/permute.cuh (1)

243-265: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

CRITICAL: Avoid launching permute kernels with zero blocks.

Issue: N == 0 makes nblks == 0, but the row-major and column-major paths still call permuteImpl, which launches <<<0, TPB, 0, stream>>>.
Why: CUDA treats zero-block launches as invalid configuration, so empty inputs fail instead of behaving as an empty permutation.

Suggested fix
 {
+  if (N == 0) { return; }
+
   auto nblks = raft::ceildiv(N, (IntType)TPB);

As per coding guidelines, “Do not launch kernels with zero blocks/threads or invalid grid/block dimensions.”

🤖 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/detail/permute.cuh` around lines 243 - 265, Guard the
permute launch path in permute.cuh so empty inputs do not reach
permute_impl_t::permuteImpl with a zero grid size. In the host-side setup where
nblks is computed and the rowMajor/column-major branches call permuteImpl, add
an early return or equivalent no-op when N == 0 (or nblks == 0) before
building/using feistel_permute_params fp and launching the kernel. Keep the
existing permuteImpl specializations unchanged; the fix should only prevent
<<<0, TPB, 0, stream>>> launches from the permute wrapper.

Source: Coding guidelines

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

340-341: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add small-N permutation cases back to the test matrix. Every current PermInputs entry starts at N=32, so the N <= 1 identity path and the small non-power-of-two cycle-walk cases in the keyed Feistel permute aren’t exercised.

🤖 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 340 - 341, The PermMdspanTestD test
matrix currently only instantiates cases from inputsd starting at N=32, so it
misses the N<=1 identity path and the small non-power-of-two cycle-walk behavior
in the keyed Feistel permute. Update the test setup around
INSTANTIATE_TEST_CASE_P and the PermInputs/inputsd definitions to add back
small-N permutation cases, including N=0/1 and a few small non-power-of-two
sizes, so the existing permute coverage exercises those code paths.
🤖 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/detail/permute.cuh`:
- Around line 78-83: Guard the Feistel width selection loop in the permutation
setup so it cannot overflow `uint64_t` when computing the next power of two for
large N. In the logic that updates pow2 and n, add a termination/saturation
check before shifting so the loop exits or clamps once pow2 would exceed the
representable range, and make sure any invalid or negative N values are rejected
or handled before entering this path. Keep the bijection/cycle-walking behavior
intact for supported N by preserving the current width computation semantics in
the permutation routine.

---

Outside diff comments:
In `@cpp/include/raft/random/detail/permute.cuh`:
- Around line 243-265: Guard the permute launch path in permute.cuh so empty
inputs do not reach permute_impl_t::permuteImpl with a zero grid size. In the
host-side setup where nblks is computed and the rowMajor/column-major branches
call permuteImpl, add an early return or equivalent no-op when N == 0 (or nblks
== 0) before building/using feistel_permute_params fp and launching the kernel.
Keep the existing permuteImpl specializations unchanged; the fix should only
prevent <<<0, TPB, 0, stream>>> launches from the permute wrapper.

In `@cpp/include/raft/random/permute.cuh`:
- Around line 91-96: The new required key parameter on the public permute
overloads breaks existing callers, so preserve source compatibility in permute
by adding a deprecated compatibility overload with the old signature that
forwards to a documented default key, or otherwise default the new parameter
where appropriate. Update the affected permute declarations/definitions in
raft::random::permute to include Doxygen for the new API and emit deprecation
warnings on the old overloads so downstream code can migrate without a hard
break.

In `@cpp/tests/random/permute.cu`:
- Around line 340-341: The PermMdspanTestD test matrix currently only
instantiates cases from inputsd starting at N=32, so it misses the N<=1 identity
path and the small non-power-of-two cycle-walk behavior in the keyed Feistel
permute. Update the test setup around INSTANTIATE_TEST_CASE_P and the
PermInputs/inputsd definitions to add back small-N permutation cases, including
N=0/1 and a few small non-power-of-two sizes, so the existing permute coverage
exercises those code paths.
🪄 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: 94b3576c-a4cd-4edd-8180-45105614b8bc

📥 Commits

Reviewing files that changed from the base of the PR and between c515501 and b57d2b6.

📒 Files selected for processing (5)
  • cpp/bench/prims/random/permute.cu
  • 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

Comment thread cpp/include/raft/random/detail/permute.cuh
@vinaydes

Copy link
Copy Markdown
Member Author

Addressed all the @coderabbitai review comments.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@vinaydes Thanks for addressing the review feedback. 🐇

@vinaydes vinaydes changed the title Permute overhaul [FEA] permute: replace LCG with keyed Feistel permutation, add deterministic key API Jul 21, 2026
@vinaydes
vinaydes marked this pull request as ready for review July 21, 2026 19:53

@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

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)

206-251: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Deprecated overloads now return the same shuffle every call. These wrappers always forward key=0, so any legacy caller that omitted a key loses per-call randomness and will get the same permutation for a given N on every invocation. Either keep a temporary varying fallback key or make the deprecation text/docs explicitly warn that the no-key path is now deterministic.

🤖 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 206 - 251, The deprecated
no-key permute overloads always use key=0, making legacy calls deterministic.
Update the three deprecated permute wrappers to generate or reuse a varying
fallback key for each invocation, while preserving forwarding behavior and
deprecation annotations.
🧹 Nitpick comments (2)
cpp/tests/random/permute.cu (1)

378-407: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

New test calls detail:: functions directly instead of the public permute() API.

kperm_seed_diversity_kernel/SeedDiversity reach into detail::kperm_params, detail::make_kperm_params, and detail::kperm_index rather than the exported raft::random::permute overloads used by the rest of this file. As per path instructions, "permutation tests should call the exported raft::random::permute overloads (not detail::permute) to match what users consume." Given the test needs to cheaply compare 16384 distinct keys, going through the full public API would likely be impractical, so this may be a reasonable, deliberate exception — but worth a short comment explaining why, so it isn't mistaken for drift from the guideline on future edits.

🤖 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 378 - 407, Add a brief comment in
the SeedDiversity test explaining that it intentionally uses the detail-level
permutation helpers to compare 16384 distinct seeds efficiently, rather than
invoking the public raft::random::permute API for every key. Leave the existing
kperm_seed_diversity_kernel and SeedDiversity behavior unchanged.

Source: Path instructions

cpp/include/raft/random/permute.cuh (1)

206-251: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test coverage for the deprecated no-key permute() overloads. The deprecated overloads in permute.cuh (which forward with key=0, silently dropping prior per-call randomness — see separate comment on that range) are never exercised by cpp/tests/random/permute.cu; every call site there was updated to pass an explicit key/seed, so the backward-compat forwarding path has zero regression coverage.

  • cpp/include/raft/random/permute.cuh#L206-L251: this is the contract that needs guarding — behavior for no-key callers changed from randomized-per-call to fixed key=0.
  • cpp/tests/random/permute.cu#L1-L433: add at least one test (wrapped with a deprecation-warning suppression pragma if -Werror is enforced) that calls the deprecated 4-arg/7-arg overloads and asserts they still produce a valid permutation equivalent to explicit key=0.
🤖 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 206 - 251,
cpp/include/raft/random/permute.cuh:206-251 requires no direct change; add
regression coverage in cpp/tests/random/permute.cu:1-433 for the deprecated
no-key permute overloads, suppressing deprecation warnings if required. Exercise
at least the 4-argument/7-argument APIs and verify their outputs are valid
permutations matching the corresponding explicit-key calls with key=0.

Source: Coding guidelines

🤖 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/tests/random/permute.cu`:
- Around line 416-421: Immediately after the kperm_seed_diversity_kernel launch,
add the standard RAFT CUDA launch-error check using
RAFT_CUDA_TRY(cudaPeekAtLastError()) before copying results or synchronizing the
stream. Keep the existing raft::update_host and resource::sync_stream flow
unchanged.

---

Outside diff comments:
In `@cpp/include/raft/random/permute.cuh`:
- Around line 206-251: The deprecated no-key permute overloads always use key=0,
making legacy calls deterministic. Update the three deprecated permute wrappers
to generate or reuse a varying fallback key for each invocation, while
preserving forwarding behavior and deprecation annotations.

---

Nitpick comments:
In `@cpp/include/raft/random/permute.cuh`:
- Around line 206-251: cpp/include/raft/random/permute.cuh:206-251 requires no
direct change; add regression coverage in cpp/tests/random/permute.cu:1-433 for
the deprecated no-key permute overloads, suppressing deprecation warnings if
required. Exercise at least the 4-argument/7-argument APIs and verify their
outputs are valid permutations matching the corresponding explicit-key calls
with key=0.

In `@cpp/tests/random/permute.cu`:
- Around line 378-407: Add a brief comment in the SeedDiversity test explaining
that it intentionally uses the detail-level permutation helpers to compare 16384
distinct seeds efficiently, rather than invoking the public
raft::random::permute API for every key. Leave the existing
kperm_seed_diversity_kernel and SeedDiversity behavior unchanged.
🪄 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: b0c3dda5-4cf4-462c-ba51-23d522d91aa0

📥 Commits

Reviewing files that changed from the base of the PR and between b57d2b6 and 24dcaa2.

📒 Files selected for processing (5)
  • cpp/bench/prims/random/permute.cu
  • 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 (1)
  • cpp/include/raft/random/detail/permute.cuh

Comment thread cpp/tests/random/permute.cu
Comment on lines 265 to 278
@@ -192,7 +274,7 @@ void permute(IntType* perms,
bool rowMajor,
cudaStream_t stream)
{
detail::permute<Type, IntType, IdxType, TPB>(perms, out, in, D, N, rowMajor, stream);
detail::permute<Type, IntType, IdxType, TPB>(perms, out, in, D, N, rowMajor, stream, uint64_t{0});
}

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 we'd just pass rand() as the explicit key to the new overload that requires it to preserve the expected behavior?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Okay, are you suggesting something like this?

detail::permute<Type, IntType, IdxType, TPB>(perms, out, in, D, N, rowMajor, stream, uint64_t{rand()});

@aamijar aamijar added non-breaking Non-breaking change improvement Improvement / enhancement to an existing function labels Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improvement / enhancement to an existing function non-breaking Non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants