Files
lattice/gpu/montgomery_debug_test.go
zeekay 5d4473dfb8 feat(gpu): Lattigo-byte-equal Montgomery NTT dispatch path
Go-side surface for the Montgomery Metal NTT shipped in
luxcpp/lattice. New constructors:

- gpu.NewMontgomeryNTTContext(s *ring.SubRing): binds a SubRing to a
  GPU-resident NTT context, capturing RootsForward/RootsBackward/
  NInv/MRedConstant. Forward/Backward output is byte-equal to
  ring.SubRing.NTT/INTT for the same inputs.
- gpu.RegisterSubRing / LookupSubRing / UnregisterSubRing: per-SubRing
  registry so the Ring.NTT fast-path can route calls to GPU.
- gpu.SetNTTThreshold / NTTThreshold: tunable per-N crossover.

ring.SubRing now consults pluggable NTT dispatchers via
ring.SetGPUDispatchers (called from gpu/init). Default behaviour is
unchanged -- dispatcher returns false unless a SubRing was explicitly
registered AND the threshold is non-zero. Pure-Go path remains the
default.

Tests:
- TestMontgomery_ByteEqual_Forward: 1024 random vectors x N in
  {1024, 2048, 4096} byte-equal to ring.SubRing.NTT
- TestMontgomery_ByteEqual_Inverse: same for INTT
- TestMontgomery_RoundTrip: at N up to 8192
- TestBatchNTT_NoSegfault: previously-segfaulting (N=4096, B=128) now
  runs and produces byte-equal output to per-poly Go pure-Go.

Bench (M1 Max, benchtime=10x):
- N=4096 B=128 batched: 12.6 us per NTT (4.57x faster than
  Go pure-Go)
- N=4096 B=64 batched:  18.2 us per NTT (3.18x faster)
- Single-poly Metal remains slower than Go at all measured N
  (8-100x slower); single-poly dispatch is opt-in via threshold.

Also fixes a pre-existing bug in gpu.go pure-Go NewNTTContext:
ring.NewSubRing alone does not generate NTT constants; switched to
ring.NewRing which calls generateNTTConstants under the hood.
2026-04-27 13:00:23 -07:00

87 lines
1.9 KiB
Go

//go:build cgo && gpu
package gpu
import (
"testing"
"github.com/luxfi/lattice/v7/ring"
)
// TestDebug isolates the N=4096 divergence with minimal input.
func TestDebug_N4096_OneHot(t *testing.T) {
r, err := ring.NewRing(4096, []uint64{mtQ})
if err != nil {
t.Fatalf("NewRing: %v", err)
}
s := r.SubRings[0]
ctx, err := NewMontgomeryNTTContext(s)
if err != nil {
t.Fatalf("NewMontgomeryNTTContext: %v", err)
}
defer ctx.Close()
for _, hotIdx := range []int{0, 1, 2047, 2048, 4095} {
t.Run("", func(t *testing.T) {
input := make([]uint64, 4096)
input[hotIdx] = 1
goOut := make([]uint64, 4096)
gpuOut := make([]uint64, 4096)
copy(goOut, input)
copy(gpuOut, input)
s.NTT(goOut, goOut)
if err := ctx.Forward(gpuOut, 1); err != nil {
t.Fatalf("Forward: %v", err)
}
diff := 0
firstDiff := -1
for i := 0; i < 4096; i++ {
if goOut[i] != gpuOut[i] {
if firstDiff < 0 {
firstDiff = i
}
diff++
}
}
if diff > 0 {
t.Errorf("hot=%d differ at %d coefficients (first at i=%d): go=%016x gpu=%016x",
hotIdx, diff, firstDiff, goOut[firstDiff], gpuOut[firstDiff])
} else {
t.Logf("hot=%d byte-equal", hotIdx)
}
})
}
}
// Also test N=2048 to confirm sanity.
func TestDebug_N2048_OneHot(t *testing.T) {
r, err := ring.NewRing(2048, []uint64{mtQ})
if err != nil {
t.Fatalf("NewRing: %v", err)
}
s := r.SubRings[0]
ctx, err := NewMontgomeryNTTContext(s)
if err != nil {
t.Fatalf("NewMontgomeryNTTContext: %v", err)
}
defer ctx.Close()
input := make([]uint64, 2048)
input[0] = 1
goOut := make([]uint64, 2048)
gpuOut := make([]uint64, 2048)
copy(goOut, input)
copy(gpuOut, input)
s.NTT(goOut, goOut)
if err := ctx.Forward(gpuOut, 1); err != nil {
t.Fatalf("Forward: %v", err)
}
for i := 0; i < 2048; i++ {
if goOut[i] != gpuOut[i] {
t.Errorf("differ at i=%d: go=%016x gpu=%016x", i, goOut[i], gpuOut[i])
return
}
}
}