Skip to content

hpc::perm: permutations as composable maps, and one fold over the whole 64×64 field - #336

Merged
AdaWorldAPI merged 3 commits into
masterfrom
claude/llvm-codegen-polyfill-gni3cw
Sep 25, 2026
Merged

AdaWorldAPI merged 3 commits into
masterfrom
claude/llvm-codegen-polyfill-gni3cw

Conversation

@AdaWorldAPI

@AdaWorldAPI AdaWorldAPI commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

A permutation here is a coordinate map. hpc::perm composes permutations on the index register, answers questions about the whole 64×64 field without building any cell, and moves data only through a call whose name starts with materialize.

1. Permutations as maps (a9a5d1a4)

  • Perm64: a bijection on 64 byte lanes, checked at construction.
    • then composes two maps with one U8x64::permute_bytes: the VPERMB instruction on AVX-512 VBMI, the facade's fallback elsewhere. It touches indices only.
    • inverse, rotate.
    • conjugate_mask carries a lane mask through the map instead of moving the data.
    • materialize_into / materialize_blocks_into are the only data moves.
  • PermTable12: twelve base steps selected by a 12-bit code, with the steps applied in a fixed order.
    • Stored as two 64-entry half tables (8 KiB) rather than 4096 flat compositions (256 KiB).
    • Each lookup is lo[l].then(hi[h]). This is exact even when the steps don't commute, because the order is fixed.
  • PermChain: a lazy accumulator.
    • It holds one Perm64 however many steps are pushed.
    • It has no method that takes a payload except materialize_*, so applying a step to data eagerly cannot be written through this type.

2. Batches and operation properties (d24e3fad)

  • PermField is the set of live codes: a 512-byte occupancy mask plus the distinct codes in first-appearance order.
  • PermBatch carries the request stream and a request→distinct remap, so multiplicity and order survive deduplication.
  • Schedule has no default, so the caller must say whether the codes are public:
    • Deduplicate is for public codes.
    • ConstantTime is for codes that may be secret. It builds no set and reorders nothing, and every table lookup reads all entries (for_code_constant_time, best effort).
  • PermInvariant / PermEquivariant are traits each operation implements, rather than a list the field owns.
    • Count, Any, Sum, Min and Max are invariant. The docs spell out that masked or position-reading reductions are not.
    • AND, OR, XOR and ternlog are equivariant.
    • PermBatch::fold_invariant takes no table, so it cannot compose a permutation.
  • Perm64::relative_to(basis) is defined by the law q.relative_to(p).then(p) == q.
    • combine_in_basis moves each non-basis operand once, by its relative map. The result stays in the basis until something needs actual coordinates.

3. The field as an exposure (ebb9fc42)

Every cell of the field is a gather: output lane i reads one source lane j. So summing, over the whole field, anything that depends only on the pair (i, j) gives the same answer as one weighted sum over the 64×64 pairs.

  • Exposure is that 64×64 weight table.
    • It is built from the two 64-entry tables in 64·64 + 64³ counter increments, independent of how much data is later folded against it.
    • count_where(pred) computes the field-wide Count from it.
  • PermTable12::relative_field(q) re-bases the field onto a fixed map q.
    • The relative map of cell (h, l) factors as (q · hi[h]⁻¹) · lo[l]⁻¹, so the re-based field is again two 64-entry tables. That is 128 compositions once, not one per cell.

Probe: examples/perm_field_probe.rs

Each question is asked of all 4096 cells and has one answer. The probe compares 4096 standalone per-cell folds with one fold that keeps the data and the running total in place. Setup: N = 64 blocks, release build, one run on an AVX-512 VBMI host.

question per-cell one fold
shared basis, Count 4096 compositions, 786,432 shuffles, 16.8M evals, 13.9 ms 0, 0, 4,096 evals, 3 µs
different bases (P per cell, fixed Q), Count 4096 compositions, 786,432 shuffles, 16.8M evals, 13.9 ms 128 compositions, 0 shuffles, 262,144 evals, 186 µs
mask fixed in output coordinates, Count 4096 compositions, 786,432 shuffles, 16.8M evals, 14.8 ms 0 compositions, 0 shuffles, 266,240 evals, 67 µs
shared basis, ordered Keep 19.0 ms, writes 67 MB 7.1 ms; still owes the 4096 × N output blocks
  • Every answer from the single fold is asserted equal to the per-cell answer.
  • The field-wide Any case (with early exit) stopped at the first cell on this data. It does not discriminate between the two approaches and is included only for completeness.
  • Negative finding: in a stream of 20,000 requests over 300 distinct codes, deduplicating compositions was slower than composing per request (594 µs vs 471 µs). A composition is a single byte permute, so the set bookkeeping costs more than it saves at this size.

Verification

  • 24 unit tests and 23 doctests pass; clippy reports nothing for perm.rs or the probe.
  • The tests pass on the native build and on the AVX2 build (config-v3).
  • The exposure and the relative field are checked by brute force against all 4096 cells.
  • Two negative controls show what the tests would catch:
    • a non-lane-wise operation breaks the relative-basis law;
    • an output mask ignored at field scale gives a different answer.
  • Disable runs: 14 deliberate one-line breaks, each one making its tests fail.
    • then operand order.
    • Half-table step order.
    • for_code composing the high half before the low half.
    • conjugate_mask using the inverse map.
    • from_indices skipping the duplicate check.
    • PermChain not resetting after materialize_into.
    • The request→distinct remap.
    • The constant-time select picking the wrong entry.
    • relative_to composing in the wrong order.
    • combine_in_basis skipping alignment.
    • combine_in_basis moving an operand that already shares the basis.
    • Deduplicate composing once per request instead of once per distinct code.
    • The exposure transposing its kernel.
    • The relative field using the wrong half table.
  • Not covered: the constant-time property itself. ConstantTime using the fast lookup would still pass, because the tests check values, not timing.

🤖 Generated with Claude Code

https://claude.ai/code/session_019HnekoM1EidTwQLS3oFVFm


Generated by Claude Code

Perm64 composes on the index register (one U8x64::permute_bytes, the
VPERMB word) and moves the payload only at a call named materialize_*.
PermTable12 stores twelve base steps as two 64-entry half tables (8 KiB)
instead of 4096 flat compositions (256 KiB), trading one compose per
lookup for tables that stay in L1; the split is exact because the step
order is fixed. PermChain holds one Perm64 whatever its length and has
no method that takes a payload except materialize_into, so eager
application is not expressible through the type.

All lane work goes through crate::simd::U8x64; no intrinsics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HnekoM1EidTwQLS3oFVFm
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Essentials

Run ID: c7ab8ba3-0a0c-418c-ac6e-d0c1ab734b30

📥 Commits

Reviewing files that changed from the base of the PR and between 239613e and ebb9fc4.

📒 Files selected for processing (4)
  • Cargo.toml
  • examples/perm_field_probe.rs
  • src/hpc/mod.rs
  • src/hpc/perm.rs
 ____________________________________________________________
< To infinity and beyond! Scouring your code for pesky bugs. >
 ------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR

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

@cursor

cursor Bot commented Sep 25, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_7ecd7914-cff4-4d0d-8b2c-91d90a45b1c9)

- PermField is the live-code SET (a 512-byte occupancy mask plus distinct
  codes in first-appearance order). PermBatch carries the request stream
  and a request->distinct remap, so multiplicity and order survive
  deduplication.
- Schedule has no default: Deduplicate for public codes, ConstantTime for
  possibly-secret codes (no field, no reordering, every table lookup reads
  all entries via for_code_constant_time).
- PermInvariant / PermEquivariant are traits the operation implements, not
  a list owned by the field. Count/Any/Sum/Min/Max are invariant;
  AND/OR/XOR/ternlog are equivariant. PermBatch::fold_invariant takes no
  table, so an invariant fold cannot compose a permutation.
- Perm64::relative_to(basis) names its direction by the law
  q.relative_to(p).then(p) == q; combine_in_basis moves each non-basis
  operand once by its relative map and returns the result still in basis.

Tests pin the relative-basis law for every equivariant op against
normalize-everything, invariance over all 4096 codes, the dedup remap, and
two negative controls (a non-lane-wise op and a position-reading fold)
that must break the laws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HnekoM1EidTwQLS3oFVFm
Every field cell is a gather, so a fold over the whole field of anything
that depends only on the (output lane, source lane) pair equals one
weighted fold over 64x64 pairs. Exposure is that kernel, built from the
two 64-entry tables in 64*64 + 64^3 increments, independent of how much
data is folded against it.

PermTable12::relative_field(q) re-bases the field onto a fixed map: the
relative map of cell (h,l) factors as (q . hi[h]^-1) . lo[l]^-1, so the
re-based field is again two 64-entry tables (128 compositions once).

examples/perm_field_probe asks one question of the whole field and
compares 4096 standalone per-cell folds with one hot fold (N = 64 blocks,
release, one run, AVX-512 VBMI host):

  shared basis Count     4096 compositions / 786432 shuffles -> 0 / 0
  different bases Count  16.8M evals -> 262144 evals, 0 shuffles
  output-mask Count      16.8M evals -> 266240 evals, 0 shuffles
  ordered Keep           owes 4096 x N output blocks either way

Dedup over 300 distinct cells in a 20000-request stream was slower than
composing per request (594 us vs 471 us): a composition is one byte
permute, so the set bookkeeping costs more than it saves here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HnekoM1EidTwQLS3oFVFm
@AdaWorldAPI AdaWorldAPI changed the title hpc::perm: 64-lane byte permutations as composable maps hpc::perm: permutations as composable maps, and one fold over the whole 64×64 field Sep 25, 2026
@AdaWorldAPI
AdaWorldAPI marked this pull request as ready for review September 25, 2026 13:27
@AdaWorldAPI
AdaWorldAPI merged commit b9f8e4e into master Sep 25, 2026
26 checks passed
@cursor

cursor Bot commented Sep 25, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_ca6af2f2-2cc9-4a3d-b643-8f887d7b0700)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants