Files
lattice/gpu/montgomery_bench_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

102 lines
2.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build cgo && gpu
package gpu
import (
"testing"
"github.com/luxfi/lattice/v7/ring"
)
// BenchmarkMontGPU_Forward measures the Montgomery-form Metal NTT in
// single-poly mode. Compare against BenchmarkRingNTT_Forward (pure Go
// in the ring package) for the per-N crossover analysis.
func BenchmarkMontGPU_Forward(b *testing.B) {
for _, N := range []int{1024, 2048, 4096, 8192, 16384} {
b.Run("", func(b *testing.B) {
r, err := ring.NewRing(N, []uint64{mtQ})
if err != nil {
b.Fatalf("NewRing: %v", err)
}
s := r.SubRings[0]
ctx, err := NewMontgomeryNTTContext(s)
if err != nil {
b.Fatalf("NewMontgomeryNTTContext: %v", err)
}
defer ctx.Close()
data := genVec(0xCAFE, N, mtQ)
work := make([]uint64, N)
b.ResetTimer()
for i := 0; i < b.N; i++ {
copy(work, data)
if err := ctx.Forward(work, 1); err != nil {
b.Fatalf("Forward: %v", err)
}
}
})
}
}
// BenchmarkMontGPU_BatchForward measures the per-poly amortised cost
// when batching B polynomials in a single GPU dispatch. This is the
// "14× speedup" claim in #88.
func BenchmarkMontGPU_BatchForward(b *testing.B) {
configs := []struct {
N int
B int
}{
{1024, 16}, {1024, 64},
{2048, 16}, {2048, 64},
{4096, 16}, {4096, 64}, {4096, 128},
{8192, 16}, {8192, 64}, {8192, 128},
}
for _, cfg := range configs {
b.Run("", func(b *testing.B) {
r, err := ring.NewRing(cfg.N, []uint64{mtQ})
if err != nil {
b.Fatalf("NewRing: %v", err)
}
s := r.SubRings[0]
ctx, err := NewMontgomeryNTTContext(s)
if err != nil {
b.Fatalf("NewMontgomeryNTTContext: %v", err)
}
defer ctx.Close()
seed := genVec(0xCAFE, cfg.N, mtQ)
work := make([]uint64, cfg.N*cfg.B)
for j := 0; j < cfg.B; j++ {
copy(work[j*cfg.N:(j+1)*cfg.N], seed)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := ctx.Forward(work, uint32(cfg.B)); err != nil {
b.Fatalf("Forward batched: %v", err)
}
}
b.ReportMetric(float64(b.N)*float64(cfg.B)/b.Elapsed().Seconds(), "ntts/sec")
})
}
}
// BenchmarkRingNTT_Forward is the pure-Go baseline.
func BenchmarkRingNTT_Forward(b *testing.B) {
for _, N := range []int{1024, 2048, 4096, 8192, 16384} {
b.Run("", func(b *testing.B) {
r, err := ring.NewRing(N, []uint64{mtQ})
if err != nil {
b.Fatalf("NewRing: %v", err)
}
s := r.SubRings[0]
data := genVec(0xCAFE, N, mtQ)
work := make([]uint64, N)
b.ResetTimer()
for i := 0; i < b.N; i++ {
copy(work, data)
s.NTT(work, work)
}
})
}
}