Files
zeekay 09eca3d159 pq/mldsa/gpu: GPU+CPU NTT accelerator covering ML-DSA-44/65/87
Replace the gpu.go one-liner stub with a real, full accelerator that
exposes the FIPS 204 NTT primitive plus batched sign/verify across all
three parameter sets via one shared NTT kernel.

The package decomposes into:

  params.go     FIPS 204 parameter table (modes 2/3/5 → k, ℓ, η, τ,
                β, γ₁, γ₂, ω, sizes). Single table; the NTT kernel
                is mode-independent because every set shares the same
                ring Z_q[X]/(X^256+1) with q=8380417.

  ntt_cpu.go    Pure-Go negacyclic NTT/INTT over the FIPS 204 ring.
                Canonical Cooley-Tukey forward + Gentleman-Sande
                inverse, FIPS 204 Appendix B zeta table. Constant-
                time helpers (reduce32, addModQ, subModQ, mulModQ).

  parallel.go   Worker-pool helpers (capWorkers, parallelDo) shared
                by both engines.

  gpu.go        Public dispatcher: Accelerator with threshold-routed
                NTTForward/Inverse/Mul Batch, SignBatch, VerifyBatch,
                Stats. Defaults: ≥16 polys for NTT GPU, ≥8 for sign,
                ≥16 for verify. Stats counters expose GPU vs CPU
                dispatch.

  gpu_cgo.go    (build tag cgo) — luxfi/accel-backed engine. NTT/Mul
                use accel.Lattice().PolynomialNTT/INTT/Mul; batched
                sign/verify use MLDSASignBatch / MLDSAVerifyBatch with
                mode ∈ {2, 3, 5}. Per-op GPU failures fall through to
                a parallel CPU path so the dispatcher contract
                ("every batch completes") holds.

  gpu_nocgo.go  (build tag !cgo) — pure-Go engine. NTT goes to
                ntt_cpu.go; sign/verify go to circl-backed
                mldsa{44,65,87} wrappers. parallelDo fan-out matches
                GOMAXPROCS so CPU-only hosts still get multi-core
                throughput. No stubs.

Tests cover: FIPS 204 parameter cross-check vs circl, NTT∘INTT
roundtrip, linearity, known vector, schoolbook PolyMul reference,
constant-time helper bounds, threshold routing, batch sign+verify
across all three modes, cross-wrapper equivalence (accelerator
signatures verify under plain mldsa{44,65,87}.Verify).

Both CGO_ENABLED={0,1} build and test green. KAT vectors in
pq/mldsa/mldsa{44,65,87} unchanged.

Apple M1 Max numbers (single-poly NTT, batched sign/verify):
  CPU NTTForward         ~2.6-3.3µs/op
  CPU PolyMul            ~7-9µs/op
  SignBatch  44 batch64  ~6.5ms  (CPU)  /  ~8.1ms (CGO+accel)
  SignBatch  65 batch64  ~6.8ms  (CPU)  /  ~12.3ms
  SignBatch  87 batch64  ~23ms   (CPU)  /  ~45ms
  VerifyBatch 44 batch64 ~5ms    (CPU)  /  ~6.1ms
  VerifyBatch 65 batch64 ~4.7ms  (CPU)  /  ~7.6ms
  VerifyBatch 87 batch64 ~7.5ms  (CPU)  /  ~10.1ms

(CGO numbers reflect accel session overhead; actual Metal kernels
for MLDSASignBatch/VerifyBatch land in luxfi/accel and are routed
to CPU until lux-gpu publishes the FIPS 204-aware kernel.)

Mode-44 and mode-87 sign/verify now share the same accelerator
surface as mode-65 — adding new parameter sets in the future is one
row in the params table, not a kernel rewrite.
2026-05-21 13:23:26 -07:00

249 lines
8.7 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package gpu
// FIPS 204 ML-DSA pure-Go NTT over R_q = Z_q[X]/(X^256+1).
//
// Implements the standard negacyclic Cooley-Tukey forward and
// Gentleman-Sande inverse described in FIPS 204 Algorithms 41 (NTT)
// and 42 (NTT^{-1}), with the canonical zeta-power table from FIPS 204
// Appendix B (rows match the reference C / Python implementation
// byte-for-byte).
//
// Constant-time discipline: all secret-dependent operations here are
// branch-free modular adds/subs/mults that operate uniformly across the
// input. The NTT itself is a fixed-shape butterfly network independent
// of input values, so it leaks nothing about its operand. mulQ, reduce,
// and the butterflies are written without secret-dependent branches.
//
// This file is *always* compiled (no build tags). The CGO and non-CGO
// dispatchers in gpu_{cgo,nocgo}.go both fall back to these helpers
// when the batch is below the GPU threshold or the GPU is unavailable.
// Zetas is the precomputed table of ζ^BitReverse(k) mod Q for k in [0,256),
// taken from FIPS 204 Appendix B (Table 1). This is the canonical NTT
// twiddle-factor table; deviating from it produces results that fail
// the FIPS 204 KAT vectors.
//
//nolint:gochecknoglobals // FIPS 204 Appendix B constants
var Zetas = [N]uint32{
0, 4808194, 3765607, 3761513, 5178923, 5496691, 5234739, 5178987,
7778734, 3542485, 2682288, 2129892, 3764867, 7375178, 557458, 7159240,
5010068, 4317364, 2663378, 6705802, 4855975, 7946292, 676590, 7044481,
5152541, 1714295, 2453983, 1460718, 7737789, 4795319, 2815639, 2283733,
3602218, 3182878, 2740543, 4793971, 5269599, 2101410, 3704823, 1159875,
394148, 928749, 1095468, 4874037, 2071829, 4361428, 3241972, 2156050,
3415069, 1759347, 7562881, 4805951, 3756790, 6444618, 6663429, 4430364,
5483103, 3192354, 556856, 3870317, 2917338, 1853806, 3345963, 1858416,
3073009, 1277625, 5744944, 3852015, 4183372, 5157610, 5258977, 8106357,
2508980, 2028118, 1937570, 4564692, 2811291, 5396636, 7270901, 4158088,
1528066, 482649, 1148858, 5418153, 7814814, 169688, 2462444, 5046034,
4213992, 4892034, 1987814, 5183169, 1736313, 235407, 5130263, 3258457,
5801164, 1787943, 5989328, 6125690, 3482206, 4197502, 7080401, 6018354,
7062739, 2461387, 3035980, 621164, 3901472, 7153756, 2925816, 3374250,
1356448, 5604662, 2683270, 5601629, 4912752, 2312838, 7727142, 7921254,
348812, 8052569, 1011223, 6026202, 4561790, 6458164, 6143691, 1744507,
1753, 6444997, 5720892, 6924527, 2660408, 6600190, 8321269, 2772600,
1182243, 87208, 636927, 4415111, 4423672, 6084020, 5095502, 4663471,
8352605, 822541, 1009365, 5926272, 6400920, 1596822, 4423473, 4620952,
6695264, 4969849, 2678278, 4611469, 4829411, 635956, 8129971, 5925040,
4234153, 6607829, 2192938, 6653329, 2387513, 4768667, 8111961, 5199961,
3747250, 2296099, 1239911, 4541938, 3195676, 2642980, 1254190, 8368000,
2998219, 141835, 8291116, 2513018, 7025525, 613238, 7070156, 6161950,
7921677, 6458423, 4040196, 4908348, 2039144, 6500539, 7561656, 6201452,
6757063, 2105286, 6006015, 6346610, 586241, 7200804, 527981, 5637006,
6903432, 1994046, 2491325, 6987258, 507927, 7192532, 7655613, 6545891,
5346675, 8041997, 2647994, 3009748, 5767564, 4148469, 749577, 4357667,
3980599, 2569011, 6764887, 1723229, 1665318, 2028038, 1163598, 5011144,
3994671, 8368538, 7009900, 3020393, 3363542, 214880, 545376, 7609976,
3105558, 7277073, 508145, 7826699, 860144, 3430436, 140244, 6866265,
6195333, 3123762, 2358373, 6187330, 5365997, 6663603, 2926054, 7987710,
8077412, 3531229, 4405932, 4606686, 1900052, 7598542, 1054478, 7648983,
}
// reduce32 computes a Barrett-style 32-bit reduction modulo Q.
// Input range: full uint32. Output range: [0, Q).
//
// Constant-time: no branches, no table lookups dependent on a.
func reduce32(a uint32) uint32 {
// Reduce a in [0, 2^32) to [0, 2Q) via Barrett:
// t = floor(a / Q)
// then a - t*Q in [0, 2Q).
// A final conditional subtract drops to [0, Q).
// The mulhi factor mu = floor(2^45 / Q) = 4193792.
// Implemented branch-free with the standard mask idiom.
// Use uint64 arithmetic to compute (a * mu) >> 45.
const mu uint64 = 4193792 // floor(2^45 / Q)
t := uint32((uint64(a) * mu) >> 45)
r := a - t*Q
// Branch-free conditional subtract: r -= Q & -(r >= Q).
mask := uint32(0) - uint32(boolToU32(r >= Q))
r -= Q & mask
return r
}
// boolToU32 converts a bool to 1 (true) or 0 (false) without a branch on
// secret data. Go guarantees this lowers to a setcc on amd64.
func boolToU32(b bool) uint32 {
if b {
return 1
}
return 0
}
// montReduce returns (a * R^{-1}) mod Q where R = 2^32 (Montgomery
// reduction). Input a is a 64-bit product of two reduced-form
// 32-bit values, so a < Q * 2^32 < 2^55.
//
// Constant-time: branch-free.
func montReduce(a uint64) uint32 {
// t = (a * QInv) mod 2^32
t := uint32(a) * QInv
// u = (a + t * Q) / 2^32
u := uint32((a + uint64(t)*uint64(Q)) >> 32)
// u in [0, 2Q); conditional subtract.
mask := uint32(0) - uint32(boolToU32(u >= Q))
u -= Q & mask
return u
}
// mulModQ returns a * b mod Q.
//
// Constant-time: branch-free.
func mulModQ(a, b uint32) uint32 {
return uint32((uint64(a) * uint64(b)) % uint64(Q))
}
// addModQ returns (a + b) mod Q, given a, b < Q.
//
// Constant-time: branch-free mask conditional subtract.
func addModQ(a, b uint32) uint32 {
r := a + b
mask := uint32(0) - uint32(boolToU32(r >= Q))
r -= Q & mask
return r
}
// subModQ returns (a - b) mod Q, given a, b < Q.
//
// Constant-time: branch-free mask conditional add.
func subModQ(a, b uint32) uint32 {
r := a - b
mask := uint32(0) - uint32(boolToU32(int32(r) < 0))
r += Q & mask
return r
}
// NTTForwardCPU performs the FIPS 204 §4.3 forward NTT in-place on a
// length-N coefficient slice over R_q. p is mutated to NTT-domain form.
//
// FIPS 204 Algorithm 41 (NTT) — Cooley-Tukey decimation-in-time.
// Twiddle factors come from Zetas[] in bit-reversed order, exactly
// matching the reference implementation. Output coefficients are in
// [0, Q).
//
// Constant-time: the iteration structure depends only on N, not on p.
func NTTForwardCPU(p *[N]uint32) {
k := 1
for length := 128; length >= 1; length >>= 1 {
for start := 0; start < N; start += 2 * length {
zeta := Zetas[k]
k++
for j := start; j < start+length; j++ {
// Standard CT butterfly:
// t = zeta * p[j+length]
// p[j+length] = p[j] - t
// p[j] = p[j] + t
t := mulModQ(zeta, p[j+length])
p[j+length] = subModQ(p[j], t)
p[j] = addModQ(p[j], t)
}
}
}
}
// NTTInverseCPU performs the FIPS 204 §4.3 inverse NTT in-place on a
// length-N evaluation-domain slice over R_q. p is mutated to coefficient
// form.
//
// FIPS 204 Algorithm 42 (NTT^{-1}) — Gentleman-Sande decimation-in-
// frequency, followed by the canonical scaling factor (1 - Q) (= N^{-1}
// mod Q packed into the same loop). Output coefficients are in [0, Q).
//
// Constant-time: structure depends only on N.
func NTTInverseCPU(p *[N]uint32) {
k := N - 1
for length := 1; length < N; length <<= 1 {
for start := 0; start < N; start += 2 * length {
// FIPS 204 reference uses -zeta in the inverse loop.
zeta := Q - Zetas[k]
k--
for j := start; j < start+length; j++ {
// Gentleman-Sande butterfly:
// t = p[j]
// p[j] = t + p[j+length]
// p[j+length] = zeta * (t - p[j+length])
t := p[j]
p[j] = addModQ(t, p[j+length])
p[j+length] = mulModQ(zeta, subModQ(t, p[j+length]))
}
}
}
// Final scaling: multiply by f = N^{-1} mod Q = 8347681 (FIPS 204
// reference constant; also equals (Q+1)/N reduced).
const fInv uint32 = 8347681
for i := 0; i < N; i++ {
p[i] = mulModQ(fInv, p[i])
}
}
// PolyMulCPU computes c = a * b in R_q via the standard NTT-domain
// pointwise multiplication: NTT(c) = NTT(a) ⊙ NTT(b), then INTT.
//
// All three slices must be length N. Inputs are not mutated.
func PolyMulCPU(a, b *[N]uint32) [N]uint32 {
var aN, bN [N]uint32
aN = *a
bN = *b
NTTForwardCPU(&aN)
NTTForwardCPU(&bN)
var c [N]uint32
for i := 0; i < N; i++ {
c[i] = mulModQ(aN[i], bN[i])
}
NTTInverseCPU(&c)
return c
}
// PolyMulNTTDomainCPU computes c = a ⊙ b pointwise modulo Q. Both
// inputs are assumed to already be in NTT (evaluation) domain. Useful
// when many multiplications share the same NTT of one operand.
func PolyMulNTTDomainCPU(a, b *[N]uint32) [N]uint32 {
var c [N]uint32
for i := 0; i < N; i++ {
c[i] = mulModQ(a[i], b[i])
}
return c
}
// PolyAddCPU returns a + b mod Q componentwise.
func PolyAddCPU(a, b *[N]uint32) [N]uint32 {
var c [N]uint32
for i := 0; i < N; i++ {
c[i] = addModQ(a[i], b[i])
}
return c
}
// PolySubCPU returns a - b mod Q componentwise.
func PolySubCPU(a, b *[N]uint32) [N]uint32 {
var c [N]uint32
for i := 0; i < N; i++ {
c[i] = subModQ(a[i], b[i])
}
return c
}