Repository navigation
AArch64 ML-KEM: NEON compress_encode and decode_decompress - #2772
Merged
Merged
Conversation
… 10) The codecs of ML-KEM-768 move from a scalar loop over groups to NEON, four u32 coefficients per register: Compress_d and Decompress_d per lane with the scalar code's formulas, pairs of lanes combined by UZP/SHL/ORR (and USHR/SLI into doublewords for d = 10), narrowed by UZP1 or picked by TBL, and the reverse with AND/USHR/ZIP, DUP/AND/CMEQ/BIC and TBL/TRN1/ZIP. The contracts, CE.correct and DD.correct are unchanged; proofs in Proof/MlKem/AArch64/CodecV. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ma2fA9rUTPXEEQjYyCMCRw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ma2fA9rUTPXEEQjYyCMCRw
Member
Author
|
What's the perf here? |
Member
Author
|
N2 run: https://github.com/pyca/verified-garbage/actions/runs/39130771921 (all four aarch64 configurations, best of 8 copies, against
The gain comes from compress/decompress: they went from ~1.8–2.7k scalar instructions per polynomial to ~0.3–0.7k NEON. Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #991.
What
vg_mlkem_compress_encodeandvg_mlkem_decode_decompresson AArch64 (Impl/MlKem/AArch64/Compress.lean), the codecs of ML-KEM-768's ciphertexts and messages (d= 1, 4 and 10), move from a scalar loop over groups to NEON. They keep theirApis, contracts and register usage. Coefficients stayu32, four to a register.Compression.
Compress_d(x)of a lane is still the scalar code's((x · M_d + 262080) >> 19) mod 2ᵈ, as MUL, ADD, USHR and AND (cmpV), so its proof reusescompress_eq.x₂ₑ + 2ᵇ x₂ₑ₊₁(UZP1, UZP2, SHL, ORR;pairV). This repeats until a lane holds what one or more bytes encode:d = 4: one level, then UZP1 narrows words to bytes (16 bytes per 32 coefficients);d = 1: three levels (4 bytes per 32 coefficients);d = 10: one level, then USHR and SLI put four values in a doubleword (dpairV), and TBL picks each group's five bytes (20 bytes per 16 coefficients).Decompression.
Decompress_d(y)of a lane is still(q · y + 2ᵈ⁻¹) >> d(MUL, ADD, USHR;decV).d = 4: AND and USHR split the nibbles of 16 bytes, ZIP1 and ZIP2 interleave them, and ZIPs with zero widen them to words.d = 1: each byte is broadcast (DUP), and its bitiis masked in wordi(AND). CMEQ with zero and BIC turn the word into0or1665, which isDecompress₁of the bit.d = 10: TBL puts each group of five bytes in a doubleword. USHR, TRN1, the ANDs and the ZIPs split it into its four values (dd10Half).Dynamic instructions per polynomial (counted on the model,
Progrun on random and edge inputs, all matchingSpec.MlKem.compressEncode/decodeDecompress):compress_encode,d = 1compress_encode,d = 4compress_encode,d = 10decode_decompress,d = 1decode_decompress,d = 4decode_decompress,d = 10The code uses
v0–v7andv16–v23, none of the registers the procedure call standard preserves, so the KEM callers (which useCE.correct/DD.correct, whose statements are unchanged) need no change.ML-KEM-1024's
d = 5andd = 11codecs are separate functions (Impl/MlKem1024/AArch64/Compress.lean) and stay as they are: the 16-bit K-PKE cores (#2763) will replace them with NEON codecs of 16-bit coefficients.No
TCB/orSpec/changes.Constant time
Both functions are proven constant time with the usual
taint_decide(CE.ct,DD.ct): every address and branch depends only on the pointers andlen, and the coefficients and bytes (secret in decapsulation:Compress₁ofm′,Decompress₁of the message) only flow through vector arithmetic.Every instruction used is on Arm's DIT list (DDI 0487, "About PSTATE.DIT", FEAT_DIT): ADD (vector), AND, BIC, CMEQ (register), DUP (element and general), INS (general), MOVI, MUL (vector), ORR, SHL, SLI, TBL, TRN1, UMOV, USHR, UZP1, UZP2, ZIP1, ZIP2, the loads and stores LDR/STR (Q, W), and the scalar MOVZ/MOVK and ADD/SUB (immediate). The only branches are CBZ/CBNZ on the public length and the loop counters. The same vector instructions already carry secrets in the existing NEON code (the 16-bit NTT,
vg_mlkem_encode12/decode12, CBD), under the same assumption that PSTATE.DIT is 1 (README, "AArch64 needs PSTATE.DIT set").Proof
Proof/MlKem/AArch64/CodecV/:Lane,Narrow,Dword: the lanes of each operation, as numbers (cmpV_ok,pairV_ok,decV_ok,dpairV_ok,narrow4, …).Blocks: loads of coefficients (ld4_ok) and four compressions (cmp4_ok).Ce1,Ce4,Ce10: each width's arithmetic;Ce1Step,Ce4Step,Ce10Step: an iteration, againstCE.L_eq.Dd4,Dd10: the decompression ofd = 4and the halves ofd = 10;Dd1: a byte ofd = 1and its iteration;Dd4Step,Dd10Step: the other iterations, againstDD.G_eq.CeLoop/CeRun,DdLoop/DdStore: the loops' invariants and constants, once for every width.CeSpec,DdSpec: the byte and coefficient lemmas of the scalar proofs, kept.CompressEncode.leanandDecodeDecompress.leankeep the contracts,correct,ctand_verified, now dispatching to the vector loops.Performance
One scoped run on the N2 runners (run 39130771921):
arches=aarch64, the four configurationsbench_archeslists,modules=mlkem(ML-KEM-768 and ML-KEM-1024), against0bac9044. Fastest of 8 copies of each binary; every spread is 0.3% or less, but one (mlkem768_encaps_from_bytes, head,neon: 2.4%).sha3neonnonemlkem768_decapsmlkem768_encapsmlkem768_encaps_from_bytesmlkem768_keygenmlkem1024_decapsmlkem1024_encapsmlkem1024_encaps_from_bytesmlkem1024_keygen(default: 21.43 → 20.48 µs for decaps, 16.29 → 15.79 µs for encaps.) Keygen uses none of these codecs, and ML-KEM-1024 only the message's (
d = 1; itsd = 5andd = 11are separate functions), hence its small change. Nothing is slower by 0.1% or more.Tests
lake build(full),Emit.lean(onlysrc/asm/aarch64/mlkem.rschanges),check_lean_modules,check_lean_imports,check_lean_speed,check_registrations,check_variants,check_mcdt,check_arch_gates,check_vectors,cpu_tests lintandalgorithms_table.py(no change) pass.cargo test --release --target aarch64-unknown-linux-gnu -- mlkem, 19 tests.🤖 Generated with Claude Code
https://claude.ai/code/session_01Ma2fA9rUTPXEEQjYyCMCRw