Skip to content

ML-DSA on x86-64: polynomial arithmetic in SSE2 (sign −19%, verify −11% instructions) - #467

Merged
alex merged 5 commits into
mainfrom
claude/fervent-einstein-ukl7t7-mul
Oct 1, 2026
Merged

alex merged 5 commits into
mainfrom
claude/fervent-einstein-ukl7t7-mul

Conversation

@alex

@alex alex commented Oct 1, 2026

Copy link
Copy Markdown
Member

Stacked on #464 (the SSE2 NTT). Its base is that PR's branch, so the diff here shows only this change. Once #464 merges, I'll retarget this PR to main.

What

vg_mldsa_multiply_ntt, vg_mldsa_multiply_add_ntt, vg_mldsa_add and vg_mldsa_sub on x86-64 now process four coefficients at a time in SSE2 registers. They reuse the vector helpers from #464 (Impl/MlDsa/X86_64/Arith/Vec.lean).

  • Products: two Montgomery multiplications, with pmuludq on even lanes and pshufd 0xF5 for odd lanes. The first gives f·g·2⁻³²; multiplying that by 2⁶⁴ mod q gives f·g in [0, 2q), which vcsub reduces. multiply_add then adds h and reduces again.
  • add / sub: paddd/psubd, then vcsub/vcadd.

MXCSR without stack

Because of pmuludq, the multiplications have to run inside ML-KEM's withMxcsr, so Intel's MCDT mitigation holds (ci/check_mcdt.py passes). These functions have no scratch buffer, and I didn't want to give them a stack frame either. On x86-64, Exec.frameSp and the taint analysis don't support frames yet, and every ML-DSA caller relies on NoSp. So MXCSR goes through the last 8 bytes of h:

  1. Set r8 = h and load the last four coefficients of h into xmm6.
  2. Run withMxcsr r8 1016 (prologue; 63-iteration loop over the first 252 coefficients; the last four from registers).
  3. Store the last four coefficients after MXCSR is restored, overwriting the slot.

The contracts are unchanged (no stack), and so are the proofs of sign, keygen and verify.

Proofs (untrusted)

  • mul_lane / mulAdd_lane: one lane of the product, from the existing mont_mont_R2.
  • Mul.step, Mul.loop_ok, Mul.last, Mul.fn_ok: the loop, the last block, and the whole function.
  • withMxcsrH_ok (Proof/MlDsa/X86_64/Arith/Mxcsr.lean): withMxcsr through the end of a writable polynomial, adapted from ML-KEM's withMxcsr_ok.
  • AddSub is reproven the same way.
  • Constant time uses taint_decide, as before.

There are no changes to TCB or Spec.

Performance

ML-DSA-65 instructions executed (callgrind, 20 operations, #464 → this PR):

before after
keygen 47.3M 46.2M −2.4%
sign 174.1M 141.2M −18.9%
verify 52.6M 46.7M −11.2%

Criterion timings on this shared VM were within noise (±10%), so I'm reporting instruction counts.

docs/algorithms/ml-dsa-*.toml now say "SSE2 polynomial arithmetic", and the README tables are regenerated.

Checks run

  • lake build
  • lake env lean --run Emit.lean --check
  • check_lean_imports, check_lean_speed, check_vectors, check_arch_gates, check_variants, check_mcdt
  • cargo fmt --check, cargo clippy --all-targets -D warnings
  • cargo test --release with Wycheproof: all pass

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ddof3szoTi7HB8iCsM2MCr


Generated by Claude Code

claude added 4 commits October 1, 2026 13:33
vg_mldsa_ntt and vg_mldsa_inv_ntt now compute on four coefficients at a
time in SSE2 registers, as ML-KEM's x86-64 NTT does: a Montgomery
multiplication with pmuludq (the even doublewords, then the odd ones moved
down by pshufd), conditional additions of q with psrad masks, and for the
layers with len 2 and 1 the coefficients of two or four blocks gathered with
punpck{l,h}qdq (and pshufd) and interleaved back. The zetas are a table in
Montgomery form that the prologue stores in scratch; the multiplications run
inside ML-KEM's withMxcsr, so Intel's MCDT mitigation holds.

Proofs: the lanes' arithmetic (VArith), the butterflies on registers
(VLanes), the loads and stores of four coefficients and the zetas (VMem),
the layers (VLay, VLay21), and the functions (Ntt, NttInv). Signing and
verification now check that their code loads MXCSR only to restore it
(ctlOk, through a compositional ctlC for verification) rather than never,
since their primitives now do.

ML-DSA-65 on this machine: sign 1.53 ms -> 0.81 ms, verify 281 us ->
181 us, keygen 276 us -> 248 us.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ddof3szoTi7HB8iCsM2MCr
vg_mldsa_multiply_ntt, vg_mldsa_multiply_add_ntt, vg_mldsa_add and
vg_mldsa_sub now compute on four coefficients at a time in SSE2 registers,
with the vector helpers of the NTT (Vec.lean).

The products are two Montgomery multiplications: f·g·2⁻³², then by
2⁶⁴ mod q, which is f·g in [0, 2q), reduced with vcsub (for
multiply_add, h is then added and the sum reduced). They use pmuludq, so
they run inside ML-KEM's withMxcsr. These functions have no working space
and use no stack, so MXCSR goes through the last 8 bytes of h, addressed
through r8 = h. The last four coefficients of h are loaded into xmm6
first. The loop stores the first 252 coefficients. The last four are
computed from registers before MXCSR is loaded back, and stored after it.
Their callers' proofs are unchanged: the functions still need no stack
and never write rsp.

add and sub use paddd/psubd and vcsub/vcadd.

Proofs: the lanes of a product (mul_lane, mulAdd_lane), the loop
(Mul.step, Mul.loop_ok), the last block (Mul.last), the function
(Mul.fn_ok), and withMxcsr through the end of a polynomial
(withMxcsrH_ok). AddSub is reproven the same way.

ML-DSA-65 instructions executed (callgrind, PR #464 -> this): sign
-19%, verify -11%, keygen -2%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ddof3szoTi7HB8iCsM2MCr
Base automatically changed from claude/fervent-einstein-ukl7t7 to main October 1, 2026 14:50
@alex
alex added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 47ee6b1 Oct 1, 2026
35 checks passed
@alex
alex deleted the claude/fervent-einstein-ukl7t7-mul branch October 1, 2026 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants