|
| 1 | +# ChaCha20 SIMD via the `U32x16` ARX lane — matryoshka usage + open work |
| 2 | + |
| 3 | +> Handoff doc. What this branch shipped, what's deferred, and the **matryoshka** |
| 4 | +> plan for *using* the `U32x16` lane to accelerate ChaCha20 without owning any |
| 5 | +> crypto algorithm. Operator-ratified design (2026-07-11 session). |
| 6 | +
|
| 7 | +## The doctrine (why this shape) |
| 8 | + |
| 9 | +- **RustCrypto owns the algorithm; ndarray owns the SIMD.** We do NOT hand-roll |
| 10 | + ChaCha20/Poly1305/the XChaCha20-Poly1305 AEAD. Rolling your own AEAD (HChaCha20 |
| 11 | + + Poly1305 + framing) is the footgun; it is forbidden. |
| 12 | +- The `encryption` crate (`crates/encryption`, RustCrypto wrappers) stays |
| 13 | + **unchanged**. See its `aead.rs` module doc for why the AEAD is not re-wired. |
| 14 | +- The only crypto-algorithm SIMD we accelerate is the **ChaCha20 keystream ARX |
| 15 | + core**, and even that is done by feeding RustCrypto's *own* backend a new |
| 16 | + SIMD lane — never by re-implementing the cipher. |
| 17 | + |
| 18 | +## DONE on this branch (the lane is ready) |
| 19 | + |
| 20 | +`ndarray::simd::U32x16` now carries the full ARX triple — `Add` + `BitXor` + |
| 21 | +`rotate_left` — on every tier, so a ChaCha20 backend can ride it: |
| 22 | + |
| 23 | +| tier | `U32x16` backing | `rotate_left` | verified | |
| 24 | +|---|---|---|---| |
| 25 | +| avx512 (server) | `__m512i` | `_mm512_rolv_epi32` (VPROLVD) | executed, parity-green | |
| 26 | +| wasm128 (browser) | `[U32x4; 4]` (`v128`) | `v128_or(u32x4_shl, u32x4_shr)` | full-lib wasm build + **node parity** | |
| 27 | +| avx2 | `[u32; 16]` polyfill (native 2×`__m256i` = deferred TD-SIMD-3) | `u32::rotate_left` loop | compiles; == reference | |
| 28 | +| scalar / nightly | `[u32; 16]` / `core::simd` | `u32::rotate_left` / shift-or | completeness tier + reference | |
| 29 | + |
| 30 | +CI: `wasm_simd` job (`.github/workflows/ci.yaml` + `scripts/wasm-parity.sh` + |
| 31 | +`crates/wasm-simd-parity/`) builds the real `ndarray::simd` wasm types and runs a |
| 32 | +lane-by-lane parity selfcheck under **node** — the standing guard for the wasm |
| 33 | +tier (invisible to the x86 `cargo test`). Extend its per-lane blocks as lanes land. |
| 34 | + |
| 35 | +The stand-alone `src/simd_crypto.rs` (`chacha20_block`/`chacha20_keystream`, |
| 36 | +scalar+AVX-512+wasm128, RustCrypto-parity-proven) is the *interim* primitive; the |
| 37 | +matryoshka below **supersedes it** (RustCrypto supplies the rounds, so the |
| 38 | +from-scratch cipher retires once the fork lands). |
| 39 | + |
| 40 | +## DEFERRED — TO-DO (next session) |
| 41 | + |
| 42 | +1. **Native neon `U32x16 = [U32x4; 4]`.** `simd_neon.rs` has native |
| 43 | + `U32x4(uint32x4_t)` with add/sub/min/max; add `bitxor` (`veorq_u32`) + |
| 44 | + `rotate_left` (shift-or via `vshlq_u32` with variable count vectors, `n%32` |
| 45 | + guard), then compose `U32x16([U32x4; 4])` with `impl Add`/`impl BitXor`/ |
| 46 | + `rotate_left` (fan over 4 lanes, mirror the wasm shape in `simd_wasm.rs`), fix |
| 47 | + `F32x16::to_bits/from_bits` to `from_array`/`to_array`, and swap the `simd.rs` |
| 48 | + aarch64 arm's scalar `U32x16` re-export for the native one. |
| 49 | +2. **aarch64 cross parity CI.** Generalize `wasm-simd-parity` into a shared |
| 50 | + `simd-parity` harness (or add a sibling) run under **qemu** via `cross` in a |
| 51 | + new `neon_simd` CI job — same selfcheck, closing NEON's identical x86-suite |
| 52 | + blind spot (its `F32x16`/`I8x16` are unverified in CI today too). |
| 53 | +3. **avx2 native `U32x16`** (2×`__m256i`) — the TD-SIMD-3 lowering; optional, the |
| 54 | + scalar polyfill is correct meanwhile. |
| 55 | + |
| 56 | +## The MATRYOSHKA — how the lane gets USED (the finalization) |
| 57 | + |
| 58 | +Goal: ChaCha20 (and thus the whole `encryption` AEAD stack) accelerated on |
| 59 | +server (AVX-512) + browser (wasm128), with **essentially zero owned crypto code** |
| 60 | +and a **one-file** delta over RustCrypto that is trivial to re-sync on security |
| 61 | +updates. |
| 62 | + |
| 63 | +**Structure (nesting):** |
| 64 | + |
| 65 | +``` |
| 66 | +RustCrypto chacha20 (rounds / constants / counter / StreamBackend / TESTS — VERBATIM, vetted) |
| 67 | + └─ one backend's round body expressed over → ndarray::simd::U32x16 (Add / BitXor / rotate_left — generic, NOT crypto) |
| 68 | + └─ dispatched by ndarray's polyfill → avx512 / wasm128 / neon / scalar |
| 69 | +``` |
| 70 | + |
| 71 | +**Steps:** |
| 72 | + |
| 73 | +1. **Fork `chacha20`** (the RustCrypto stream-cipher crate; `AdaWorldAPI/chacha20`, |
| 74 | + or vendor). Its backends live in `src/backends/{soft,sse2,avx2,neon}.rs`, each a |
| 75 | + `struct Backend<R>` impl of `StreamBackend` with `gen_ks_block` / |
| 76 | + `gen_par_ks_blocks`. **Clone `avx2.rs` once**; keep soft/sse2/avx2/neon |
| 77 | + untouched. |
| 78 | +2. **Rewire the clone over `ndarray::simd::U32x16`** — the word-sliced ChaCha |
| 79 | + round (`state[a] = state[a] + state[b]; state[d] = (state[d] ^ state[a]).rotate_left(16);` |
| 80 | + …) written against `U32x16`, `ParBlocksSize = U16`. `ndarray` lowers it to |
| 81 | + AVX-512 (server) / wasm128 (browser) / NEON / scalar automatically. The |
| 82 | + carried file has **no `unsafe`, no intrinsics** — all of that lives once in |
| 83 | + `ndarray::simd` (audited once; only AMX+F16 are byte-asm, neither on this path). |
| 84 | +3. **Dispatch:** add the generic backend to the fork's `lib.rs` selection. |
| 85 | + Per the ndarray model this is **compile-time** (`cfg(target_feature)`), not |
| 86 | + `is_x86_feature_detected!` — server built `x86-64-v4`, browser built `+simd128`. |
| 87 | + Non-portable per-target binaries by design (SIGILL/validation-fail on a CPU |
| 88 | + built-for-but-absent) — matches how the servers/browsers are actually built. |
| 89 | +4. **`[patch]` the fork in** (the pattern already used in MedCare-rs for |
| 90 | + `encryption`): `chacha20poly1305 → encryption → ogar-encryption → consumers` |
| 91 | + all accelerate **transitively**, with **zero** change to the AEAD or any |
| 92 | + consumer, because the AEAD just calls `apply_keystream` which now dispatches |
| 93 | + to the new backend. HChaCha20 + Poly1305 + framing stay 100% RustCrypto. |
| 94 | +5. **Gate bit-exact vs RustCrypto's own `soft` backend** + run its stock test |
| 95 | + vectors — the same "reference + KAT, then vectorize with parity" discipline. |
| 96 | +6. **Retire `src/simd_crypto.rs`** — the rounds now come from RustCrypto; only |
| 97 | + the `U32x16` lane (not crypto) is ours. |
| 98 | + |
| 99 | +**Maintenance win:** a RustCrypto security advisory almost never touches the ARX |
| 100 | +lane ops (fixes land in framing/counter/AEAD — all pristine upstream), so the |
| 101 | +one-file delta re-applies with ~zero conflict: bump the vendored `chacha20`, |
| 102 | +re-apply the one backend file, run the stock vectors + the soft parity. And if |
| 103 | +the backend is ever **upstreamed** (RustCrypto would plausibly take an avx512 / |
| 104 | +wasm128 backend), the fork evaporates → maintenance goes to zero. AMX has no |
| 105 | +ChaCha/Poly backend (it is a matrix engine); the upstream targets are avx512 + |
| 106 | +wasm128 (NEON already exists upstream). |
0 commit comments