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

62 lines
1.2 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package gpu
import "sync"
// capWorkers clamps the worker count to a sane minimum/maximum. We never
// want to fan out to fewer than 1 goroutine, and beyond ~64 the
// scheduler contention dominates the work.
//
// Lives in a build-tag-free helper file so both the cgo and !cgo
// engines can share it without duplication.
func capWorkers(n int) int {
if n < 1 {
return 1
}
if n > 64 {
return 64
}
return n
}
// parallelDo dispatches f over indices [0, total) across at most
// workers goroutines. Returns when every index has completed.
//
// Lives in a build-tag-free helper file so both the cgo and !cgo
// engines can share it.
func parallelDo(total, workers int, f func(int)) {
if total <= 0 {
return
}
if workers < 1 {
workers = 1
}
if workers > total {
workers = total
}
if workers == 1 {
for i := 0; i < total; i++ {
f(i)
}
return
}
var wg sync.WaitGroup
ch := make(chan int, total)
for i := 0; i < total; i++ {
ch <- i
}
close(ch)
wg.Add(workers)
for w := 0; w < workers; w++ {
go func() {
defer wg.Done()
for i := range ch {
f(i)
}
}()
}
wg.Wait()
}