Skip to content

Commit 729599e

Browse files
authored
Merge pull request #240 from AdaWorldAPI/claude/medcare-ruff-codebook-handover-5ulx0i
simd: ChaCha20 hardware-accel + U32x16 ARX lane + wasm parity CI (crypto-SIMD arc)
2 parents 9c41b3a + 5a914c3 commit 729599e

21 files changed

Lines changed: 1432 additions & 10 deletions

File tree

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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).

.claude/blackboard.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,3 +609,24 @@ index was transiently stale this session (returned empty for
609609
`simd_int_ops.rs`, `vnni_gemm.rs`, `bf16_gemm_f32`); Bash ground-truth
610610
confirmed all present. Orchestrator should `cargo fmt`/`clippy`/`test`
611611
centrally (edits were edit-only, no compile performed here).
612+
613+
---
614+
615+
## 2026-07-11 — U32x16 ARX lane + ChaCha20 matryoshka (see .claude/CHACHA20_MATRYOSHKA_PLAN.md)
616+
617+
**DONE:** `ndarray::simd::U32x16` full ARX triple (Add/BitXor/**rotate_left**) on
618+
every tier — avx512 native `_mm512_rolv_epi32`, native wasm `[U32x4;4]`, avx2/
619+
scalar/nightly. Node-run wasm parity CI gate (`wasm_simd` job + `scripts/wasm-parity.sh`
620+
+ `crates/wasm-simd-parity/`, workspace-excluded, tests the real types, no drift).
621+
Interim `src/simd_crypto.rs` (chacha20 scalar+avx512+wasm128, RustCrypto-parity-proven)
622+
is superseded by the matryoshka once the fork lands.
623+
624+
**TO-DO (deferred, token limit):**
625+
1. Native neon `U32x16 = [U32x4;4]` (extend `U32x4(uint32x4_t)` w/ xor+rotl, compose,
626+
fix F32x16 to_bits, swap simd.rs aarch64 arm).
627+
2. aarch64 cross (qemu) parity CI job — generalize `wasm-simd-parity` to a shared
628+
`simd-parity` harness; closes NEON's x86-suite blind spot.
629+
3. avx2 native `U32x16` (2×__m256i, TD-SIMD-3) — optional.
630+
4. **Matryoshka execution:** fork `chacha20`, clone `avx2.rs` backend → rewire over
631+
`ndarray::simd::U32x16`, `[patch]` into the encryption stack (transitive accel),
632+
gate vs RustCrypto `soft`, retire `simd_crypto.rs`. Full plan in the doc above.

.github/workflows/ci.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,29 @@ jobs:
112112
# crates that have no business existing in the nostd build.
113113
cargo rustc -p ndarray "--target=${{ matrix.target }}" --no-default-features --features portable-atomic-critical-section
114114
115+
wasm_simd:
116+
# The x86 `cargo test` suite never touches `simd_wasm.rs` (it is cfg'd to
117+
# wasm32), so wasm SIMD correctness is otherwise unverified. Build the real
118+
# `ndarray::simd` wasm types (+simd128) via the excluded `wasm-simd-parity`
119+
# cdylib and run its numeric selfcheck under node — both integration (the
120+
# lib compiles for wasm32+simd128) and parity (each lane == scalar). The
121+
# standing version of the one-off node check from commit 500d57e6.
122+
runs-on: ubuntu-latest
123+
name: wasm-simd/parity-node
124+
steps:
125+
- uses: actions/checkout@v4
126+
- uses: dtolnay/rust-toolchain@stable
127+
with:
128+
targets: wasm32-unknown-unknown
129+
# rust-toolchain.toml pins 1.95.0 — install the wasm target for the pinned
130+
# toolchain too (dtolnay installs for `stable`, which may differ).
131+
- run: rustup target add wasm32-unknown-unknown
132+
- uses: actions/setup-node@v4
133+
with:
134+
node-version: "22"
135+
- name: wasm SIMD parity (build + node run)
136+
run: ./scripts/wasm-parity.sh
137+
115138
tests:
116139
runs-on: ubuntu-latest
117140
needs: pass-msrv
@@ -335,6 +358,7 @@ jobs:
335358
- clippy
336359
- format
337360
- nostd
361+
- wasm_simd
338362
- tests
339363
- native-backend
340364
- hpc-stream-parallel

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@ approx = { workspace = true, default-features = true }
217217
itertools = { workspace = true }
218218
ndarray-gen = { workspace = true }
219219
criterion = { version = "0.5", features = ["html_reports"] }
220+
# The vetted RustCrypto ChaCha20 stream cipher — used ONLY as the independent
221+
# oracle for the `chacha20_rustcrypto_parity` integration test that pins
222+
# `ndarray::simd::chacha20_keystream` byte-for-byte against a trusted reference.
223+
# Same version chacha20poly1305 0.10 pulls transitively (see Cargo.lock).
224+
chacha20 = "0.9"
220225

221226
[[bench]]
222227
name = "append"
@@ -402,7 +407,7 @@ members = [
402407
"ndarray-rand",
403408
"crates/*",
404409
]
405-
exclude = ["crates/burn"]
410+
exclude = ["crates/burn", "crates/wasm-simd-parity"]
406411
default-members = [
407412
".",
408413
"ndarray-rand",

crates/encryption/src/aead.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@
55
//! is no counter state to persist or synchronise between a browser tab
66
//! and a server. Callers normally use [`crate::envelope`], which manages
77
//! nonce generation and layout; this module is the raw primitive.
8+
//!
9+
//! ## Deliberately NOT hand-composed from `ndarray::simd::chacha20_keystream`
10+
//!
11+
//! `ndarray` now ships a hardware-accelerated ChaCha20 keystream
12+
//! (`ndarray::simd::chacha20_keystream`, AVX-512 / wasm128, byte-parity-proven
13+
//! against RustCrypto). It is tempting to swap this AEAD's keystream to that
14+
//! primitive for speed — **do not.** XChaCha20-Poly1305 is not just a keystream:
15+
//! it is HChaCha20 subkey derivation + the Poly1305 one-time-key + MAC framing +
16+
//! the AAD/length encoding. Re-wiring only the keystream means re-implementing
17+
//! that authenticated composition by hand, which is exactly the "roll your own
18+
//! AEAD" footgun the stack forbids. The vetted RustCrypto `XChaCha20Poly1305`
19+
//! construction stays here. The accelerated primitive is for *raw-stream* use
20+
//! sites whose caller already owns a vetted MAC/framing — not this module.
821
922
use chacha20poly1305::aead::{Aead, KeyInit, Payload};
1023
use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};

0 commit comments

Comments
 (0)