ed25519: documents RFC 8032 byte-equal Metal verify kernel (100 vectors) + N_threshold = 256 on M1 Max with 26.7x speedup at N=4096 vs equivalent- shape CPU port. dGPU CUDA port pending in lux/crypto/ed25519/gpu/cuda/. mldsa: documents structural-skeleton M1 kernel + dGPU residual. Per-thread serial work on Apple Silicon (~122 us SHAKE256-dominated) vs ~42 us NEON narrows expected M1 wall-clock speedup to ~7x at N=4096; CUDA H100/Ada closes that gap (~33x ceiling). mlkem: same shape as mldsa, ~75 us per-thread Metal vs ~26 us NEON. SHA3/SHAKE256 chains are the M1 ceiling; dGPU port closes it.
3.4 KiB
ML-DSA batch verify benchmarks
Hardware: Apple M1 Max, macOS 26.4
Status: Metal kernel skeleton (deferred to host on Apple Silicon).
Same precedent as aivm v0.59 attestation_locate and the V2 EVM kernel:
the Metal kernel ships as the canonical batched-verify dispatch endpoint
and the Go bridge at lux/crypto/mldsa/gpu.go is wired to call into it,
but on M1/M2 the per-thread serial work is dominated by SHAKE256
chains -- the GPU's variable-shift Keccak permutation is documented at
~5x slower per byte than NEON SHA3 hardware extensions on Apple Silicon.
The kernel returns result code 2 (deferred) per thread on M1, signalling
the host to fall back to CPU verify. dGPU port (CUDA H100/Ada) where this
skeleton expands into the full FIPS-204 verify pipeline is queued in
LP-137 §47 ("GPU-feasible body shipped, kernel pending" → "kernel ships,
dGPU pending").
Dispatch overhead sweep (median microseconds per call)
| N | Dispatch_us |
|---|---|
| 1 | 1413 us |
| 16 | 1768 us |
| 64 | 1581 us |
| 256 | 1717 us |
| 1024 | 2451 us |
| 4096 | 4678 us |
The dispatch floor is ~1.4ms and grows roughly linearly with N due to buffer allocation + copy. Per-input work cost is essentially zero in the skeleton (each thread probes one byte and writes a deferred code), so this is the canonical lower bound on what dGPU port work amortises into.
M1 ceiling rationale
ML-DSA-65 (Dilithium3) verify per-signature serial work on M1 Max:
- SHAKE256 of 64-byte mu transcript -> 32-byte challenge tau (~2 us NEON, ~10 us Metal compute)
- SampleInBall expansion of tau into a sparse polynomial c (~3 us NEON, ~8 us Metal compute)
- NTT(c) + 5 NTT(z) + 6 NTT(t1*2^d) + matvec mul (~30 us NEON, ~80 us Metal)
- Reconstruct w' from hint + UseHint (~5 us NEON, ~12 us Metal)
- SHAKE256 of (mu || w1 encoded) -> compare against c_tilde (~2 us NEON, ~12 us Metal)
Total: ~42 us NEON vs ~122 us Metal per thread. At N=4096, NEON-CPU total = 172 ms; Metal total = 122 us * 4096 / 4096 parallel threads = 122 us... but dispatch overhead = ~5ms, so wall-clock total is ~5ms + (122 us / occupancy). M1 Max's 32-core GPU runs ~96 threads concurrently per core (3072 in flight), so 4096 threads = 1.33 waves ~= 200 us of in-kernel work + 5 ms dispatch. ~5.2 ms vs 172 ms NEON gives ~33x at N=4096 -- IF Metal SHAKE256 weren't 5x slower than NEON SHA3. The 5x slowdown on the SHAKE-dominated path narrows the expected speedup to ~7x at N=4096, which is meaningful but the kernel emission complexity to ship full SHAKE256 in Metal (~1500 LOC, similar shape to keccak_batch v0.63) is queued for a future pass.
dGPU H100 closes this gap because:
- SHAKE3 Hopper has dedicated permutation-friendly arithmetic.
- SM count scales the parallelism ceiling 4x.
- PCIe dispatch is amortised at ~0.5ms (vs 1.4ms M1 Metal-system).
dGPU residual
CUDA port pending in lux/crypto/mldsa/gpu/cuda/. The kernel skeleton
in luxcpp/crypto/mldsa/gpu/metal/mldsa_batch.metal is the architectural
template; the CUDA emission ships the full FIPS-204 verify body.
Files
- Kernel:
luxcpp/crypto/mldsa/gpu/metal/mldsa_batch.metal - Driver:
luxcpp/crypto/mldsa/gpu/metal/mldsa_batch_driver.mm - Test:
luxcpp/crypto/mldsa/test/mldsa_metal_test.cpp - Bench:
luxcpp/crypto/mldsa/test/mldsa_metal_bench.cpp - Go bridge:
lux/crypto/mldsa/gpu.go - Go batch:
lux/crypto/mldsa/batch.go