Skip to content

simd: simd_stride.rs (lut_u8, lut9) + FACET STRIDE rule and guard - #382

Merged
AdaWorldAPI merged 12 commits into
masterfrom
ccr-45a41fb4-0j3i0l-odd-lut
Oct 27, 2026
Merged

AdaWorldAPI merged 12 commits into
masterfrom
ccr-45a41fb4-0j3i0l-odd-lut

Conversation

@AdaWorldAPI

@AdaWorldAPI AdaWorldAPI commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Two reusable FACET STRIDE primitives in a new src/simd_stride.rs: lut_u8 (a byte table) and lut9 (an odd function over the sign-magnitude 9-bit lane from #381). Also the FACET STRIDE rule that their development produced, enforced from now on by a guard.

███ FACET STRIDE ███

'{0}..{15}' -f $128.split()

A 128-bit value is sixteen u8 positions. One mov reads all sixteen; nothing has to move to read one. The precedent is ternary_match_strided16_to_mask: it loads 16-byte registers with plain moves and reads every byte position in place.

  1. Never compute with byte shuffles (shuffle_bytes, permute_bytes, *_shuffle_epi8, permutexvar_epi8, swizzle_dyn, vqtbl*). That rules out lookups or folds assembled from shuffles plus masks. A shuffle is only for a genuine byte move that a one-mov cast cannot do.
  2. Scalar loops only in simd_scalar.rs. They are forbidden in the AVX2, AVX-512, AVX, NEON, wasm, AMX and nightly backends when a split realization exists (on AVX2: two U8x32).
  3. u8 has no sign. Never mix u8 and i8.

Where it lives:

  • CLAUDE.md: the rule, at the top of the file.
  • .claude/blackboard.md: the decision record.
  • .claude/hooks/facet-stride-guard.sh: a PostToolUse hook on Edit/Write/MultiEdit, wired in .claude/settings.json.

What the guard checks: lines a change adds, judged with the whole file as context.

  • R1: a shuffle used to compute. A shuffle wrapper's own definition is not compute.
  • R2: a scalar lane loop in a non-scalar backend.
  • R3: a u8/i8 mix: a cast to i8 inside a U8 type, to u8 inside an I8 type, or a u8/i8 conversion on one line.

Test modules are skipped, including multi-line #[cfg(all(test, ..))] and mod *tests. A hit blocks the edit (exit 2). The waiver is // facet-stride: ok <reason>, and a reason is required. --diff <rev> checks a range, --all audits the tree, and --root <dir> audits another checkout.

Exclusions (.claude/hooks/facet-stride-exclude.txt, each with a mandatory reason) are only for code whose byte shuffle is the defined operation:

  • vendor/chacha20: cipher rotations by 8 and 16 bits.
  • src/hpc/perm.rs: Perm64 is a permutation map.
  • examples/ternlog_popcnt_gap_probe.rs: it measures the anti-pattern on purpose.

Verified:

  • It fires on the U8x64::lut256 code this branch used to carry.
  • It fires on a probe line for each rule.
  • It stays silent on clean added lines.
  • It honours a waiver with a reason and rejects one without.
  • --diff origin/master on this branch: 0 hits.

The existing trees are not clean (burn-down lists, not precedents):

  • ndarray: 166 hits outside tests (.claude/knowledge/facet-stride-audit-2026-10-27.txt).
    • R1 7: all nibble-table popcount (bitwise.rs ×4, simd_avx.rs, simd_avx2.rs ×2).
    • R2 157: simd_avx2.rs 81, mostly the array-backed AVX2 types; NEON 26; nightly 28; AVX-512 12; wasm 10.
    • R3 2: the u8 splats that cast to the i8 intrinsic parameter (AVX2, AVX-512).
  • lance-graph: 10 hits, all R1 nibble-table popcount with raw intrinsics (holograph/src/hamming.rs ×2, blasgraph/ndarray_bridge.rs ×8). It has no cipher shuffles, so nothing there is excluded.

simd_stride.rs

pub fn lut_u8(values: &[u8], table: &[u8; 256], out: &mut [u8])
pub fn lut9(magnitudes: &[u8], signs: &[u64], table: &[u8; 256],
            out_magnitudes: &mut [u8], out_signs: &mut [u64])

Both are re-exported through ndarray::simd, together with FACET_BYTES = 16.

lut_u8 computes out[i] = table[values[i]]. Each 16-byte facet's positions are read where they sit and address the table directly.

lut9 works on the SM9 lane from #381: a u8 magnitude per row, a u64 sign plane, and a separate presence plane. An odd function (f(-x) = -f(x)) needs one table over the 256 magnitudes, and the sign passes through.

  • The magnitude lookup is lut_u8.
  • A negative value whose result rounds to zero is stored as +0.
  • An input -0 stays -0, the way IEEE arithmetic carries NaN through.
  • table[0] must be 0, or the call panics. A missing row is presence clear over 0, so it stays 0, and the presence plane is never read or written.
  • The sign plane is 64 rows per U8x64::cmpeq_mask. The tail is padded with 0 and masked off the sign plane.

How it got here:

  1. Four 64-entry permute_bytes. This was 5.6× slower than a scalar loop, because without AVX-512 VBMI permute_bytes is a scalar loop.
  2. A 16-row in-lane vpshufb plus a mask fold (U8x64::lut256 on all six backends). This is the shape the rule forbids, and its nightly arm did not compile.

Both are gone, and lut256 is removed from every backend. lut9 was briefly named odd_lut_u8 on this branch.

Measured

examples/lut9_bench.rs, AVX-512 host without VBMI, 1 Mi elements. The outputs are checked against the scalar form before timing.

path ns/elem
i8 table, no sign plane 0.595
u8 table + sign plane, scalar 0.608
lut9 0.503

Tests

  • Unit tests (7):

    • lut_u8 at 14 lengths around the 16-byte facet;
    • lut_u8 reading every table entry;
    • lut_u8 refusing a short output;
    • lut9 at every magnitude, at every lane position and at 12 lengths around the 64-row seams, with junk sign bits past the end and pre-filled outputs;
    • lut9 rounding to +0 while keeping an input -0;
    • lut9 refusing a table with table[0] != 0;
    • lut9 refusing a short sign plane.
  • Parity groups:

  • Disable runs. Each was red in both the unit tests and the parity group.

    guard removed parity
    lut9: input -0 kept 0x14a3
    lut9: tail rows masked off the sign plane 0x1421
    lut9: lookup reads the right position (table[m ^ 1]) 0x1401
    lut_u8: the tail is looked up 0x1501
  • Lint: clippy -D warnings is clean on the lib, the tests, the examples and the parity crate.

  • Backend arms, at e47d456 (before the merge with master): 9/9 pass. Every arm that prints its group count reports all 21 groups: native AVX-512, AVX2 (config-v3), NEON qemu, AVX qemu, v2 qemu and nightly. wasm, wasm-scalar and wasm-relaxed pass their self-check.

  • After merging master (simd: NaN/presence bijection and SQL NULL=NaN predicates #381): the parity crate conflicted on the import list and the group list; both now carry both PRs' entries. Native parity passes 23/23 groups. Clippy, rustfmt and the guard (--diff origin/master, 0 hits) are clean, and the lib simd_ tests (471) and the sm9_presence_probe tests (7) pass.

Open

  • The polyfill shuffle_bytes gives different results across backends when an index has bit 7 set. AVX2 U8x64::shuffle_bytes and permute_bytes are scalar loops. Both are entries in the audit list.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XAqKMPh2Seh2WLccCksAdY

…e lane

Looks up each u8 magnitude in a 256-entry table as four 64-entry
U8x64::permute_bytes lookups chosen by the magnitude's top two bits.
The sign plane passes through, cleared where the result is 0 so a zero
result is stored as +0. table[0] must be 0, so a missing row (+0)
stays +0 and the presence plane is not touched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMPh2Seh2WLccCksAdY
Clippy 1.99 rejects chunks_exact with a constant size. The probe
examples/odd_lut_bench.rs times odd_lut_u8 against a scalar i8 table
and the scalar sign-magnitude form, after checking the outputs agree.

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

coderabbitai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 09ec46b0-d421-4ed5-9491-c8f151bb3785
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@AdaWorldAPI
AdaWorldAPI marked this pull request as ready for review October 27, 2026 16:27
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-27T16:28:39.777728Z e5aa2c5 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

A negative value whose result rounds to zero is still stored as +0,
but an input -0 now comes out as -0. A writer that only stores +0
never produces it, so the plain reading is unchanged; a class reading
that gives -0 its own meaning (observed NaN beside a clear presence bit
for not observed) keeps it, the way IEEE carries NaN. The tail needs
its live mask again: padding lanes are magnitude 0 and would keep
their junk sign bits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMPh2Seh2WLccCksAdY
The four-permute lookup ran 5.6x slower than a scalar loop on AVX-512
without VBMI, because permute_bytes is a scalar loop on every backend
but AVX-512+VBMI and nightly. lut256 reads the 256-entry table as 16
rows of 16 bytes: the low nibble picks the byte (in-lane vpshufb), the
high nibble picks the row. Native on AVX-512 (BW), AVX2 (U8x64 as two
U8x32 halves) and AVX-without-AVX2 (two 128-bit halves); the plain loop
only in the scalar backend; swizzle_dyn on nightly.

examples/odd_lut_bench.rs on AVX-512 without VBMI, 1 Mi elements:
scalar u8 table + sign 0.606 ns/elem, odd_lut_u8 0.176 ns/elem.
The probe's scalar reference now keeps an input -0, as odd_lut_u8 does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMPh2Seh2WLccCksAdY
'{0}..{15}' -f $128.split(): a 128-bit value is sixteen u8 positions,
one mov reads them all. No byte shuffles to compute; scalar loops only
in simd_scalar.rs; u8 has no sign, never mixed with i8.

.claude/hooks/facet-stride-guard.sh runs after every Edit/Write and
blocks added lines that use a shuffle to compute (R1), a scalar lane
loop in a non-scalar backend (R2) or a u8/i8 mix (R3). Waiver:
'// facet-stride: ok <reason>'. --diff and --all for ranges and audits.
Baseline audit of the existing tree: 248 hits outside tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMPh2Seh2WLccCksAdY
Sixteen magnitudes are one 128-bit value read in place as positions
{0}..{15}; each position addresses the table directly. No shuffles,
no result assembled from masks. lut256 (16 in-lane vpshufb + mask
fold) is removed from all six backends; it was the forbidden shape and
its nightly arm did not compile.

examples/odd_lut_bench.rs, AVX-512 without VBMI, 1 Mi elements:
scalar u8 table + sign 0.606 ns/elem, odd_lut_u8 0.496 ns/elem.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMPh2Seh2WLccCksAdY
@AdaWorldAPI AdaWorldAPI changed the title simd: odd_lut_u8, an odd-function table over a sign-magnitude lane simd: odd_lut_u8 (facet stride) + FACET STRIDE rule and guard Oct 27, 2026
The checker reads the whole file and reports only the lines in scope, so
hook mode judges added lines with context: test modules (including
multi-line cfg(all(test, ..)) and mod *tests) are skipped, a shuffle
wrapper's own definition is not compute, and R3 is typed (cast to i8 in
a U8 type, to u8 in an I8 type, or a u8<->i8 line). Exclusions with a
mandatory reason for cipher rounds, permutation maps and a benchmark of
the anti-pattern. --root audits another checkout.

Audit: ndarray 166 hits (R1 7, R2 157, R3 2); lance-graph 10 (all R1
nibble-table popcount).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMPh2Seh2WLccCksAdY
out[i] = table[values[i]], reading each 16-byte facet's positions where
they sit; odd_lut_u8 now uses it for the magnitude lookup. Re-exported
through ndarray::simd with FACET_BYTES. Unit tests at every length around
the facet, plus parity group 0x15 (21 groups).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMPh2Seh2WLccCksAdY
Moves the #382 pattern out of simd_masking_ops into simd_stride beside
lut_u8 and names it lut9: a 256-entry magnitude table over a
sign-magnitude 9-bit lane (u8 magnitude + sign plane), sign cleared where
a nonzero input rounds to zero, input -0 kept. The magnitude lookup is
lut_u8. Behaviour unchanged; odd_lut_u8 is renamed, its tests, parity
group 0x14 and bench (examples/lut9_bench.rs) follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMPh2Seh2WLccCksAdY
@AdaWorldAPI AdaWorldAPI changed the title simd: odd_lut_u8 (facet stride) + FACET STRIDE rule and guard simd: simd_stride.rs (lut_u8, lut9) + FACET STRIDE rule and guard Oct 27, 2026
…-odd-lut

# Conflicts:
#	.claude/blackboard.md
#	crates/simd-masking-parity/src/lib.rs
@AdaWorldAPI
AdaWorldAPI merged commit 38dd52e into master Oct 27, 2026
30 checks passed
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