Files
crypto/hqc/gpu_bench_test.go
Hanzo AI 7ae131e995 hqc: pure-Go bit-sliced GF(2)^N polymul + CGO single-shot path
Adds GPU/CPU split for HQC's hot inner-loop primitives:

  gpu_nocgo.go  Pure-Go bit-sliced Karatsuba mod (X^n - 1), byte-equal
                to PQClean's PQCLEAN_HQC{128,192,256}_CLEAN_vect_mul.
                Constant-time on secret operands (masked nibble table
                lookups; no data-dependent branches or indices).
                Modern Go compiler emits SIMD-friendly XOR loops on
                amd64 / arm64.

  gpu_cgo.go    Same surface, routed through luxfi/accel/hqc -> the
                C kernel in libluxgpu_hqc.a (also byte-equal to PQClean).

  gpu_kat_test.go         KAT cross-check across both builds — vectors
                          captured from the cgo path's PQClean kernel,
                          verified bit-identical from the pure-Go path
                          (100-iter checksum + single-vector KAT).
  gpu_polymul_test.go     Algebraic properties (commutativity, identity,
                          zero, distributivity, associativity fuzz x600).
  gpu_bench_test.go       Single-shot + batched benchmarks per mode.

Updated gpu.go to route the existing batchEncapsulateGPU /
batchDecapsulateGPU through accel/ops/code's batch surface (replaces
the old "not implemented" stubs). Updated randombytes_shim.c to be
the single canonical PQClean entropy bridge.

Determinism contract: validators reach consensus on HQC encapsulation
ciphertexts, so any byte-level divergence between the two paths is a
consensus fork. The KAT test pins this contract.

Test status (all three configs green):
  CGO_ENABLED=0 go test ./hqc/...                  18.1s
  CGO_ENABLED=1 go test ./hqc/...                  44.2s
  CGO_ENABLED=1 go test -tags=hqc_pqclean ./hqc/.. 5.5s

NIST KATs (HQC128/192/256 encap/decap byte-vectors): all PASS.

Bench numbers on Apple M1 Max (single-shot GF2 polymul, ns/op):
  HQC-128: pure-Go 2.03 ms / cgo 4.10 ms
  HQC-192: pure-Go 6.28 ms / cgo (n.m.)
  HQC-256: pure-Go 11.20 ms / cgo (n.m.)

The cgo path is currently slower than pure-Go because libluxgpu_hqc.a
ships from luxcpp/gpu/build with CMAKE_BUILD_TYPE empty (no -O3). With
the CMake build switched to Release, cgo will return to its expected
~30us / poly margin.

Wired via:
  crypto/hqc -> accel/hqc -> accel/ops/code -> luxcpp/gpu/libluxgpu_hqc
2026-05-21 13:34:58 -07:00

92 lines
2.4 KiB
Go

// Copyright (C) 2024-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package hqc
import (
"math/rand/v2"
"testing"
)
// BenchmarkGF2Polymul measures single-shot GF(2)^N polynomial
// multiplication across all three parameter sets. The numbers
// captured here are the headline figure for HQC's hot path — every
// IND-CPA encryption/decryption issues at least one polymul.
//
// Run on each build mode to compare:
//
// CGO_ENABLED=0 go test -bench BenchmarkGF2Polymul -run '^$' ./hqc/...
// CGO_ENABLED=1 go test -bench BenchmarkGF2Polymul -run '^$' ./hqc/...
func BenchmarkGF2Polymul(b *testing.B) {
cases := []struct {
mode Mode
name string
}{
{HQC128, "HQC-128"},
{HQC192, "HQC-192"},
{HQC256, "HQC-256"},
}
for _, tc := range cases {
b.Run(tc.name, func(b *testing.B) {
n, _ := vecNSize64(tc.mode)
a := make([]uint64, n)
bb := make([]uint64, n)
c := make([]uint64, n)
r := rand.New(rand.NewPCG(7, 13))
for i := 0; i < n; i++ {
a[i] = r.Uint64()
bb[i] = r.Uint64()
}
b.SetBytes(int64(n * 8))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = GF2Polymul(tc.mode, c, a, bb)
}
})
}
}
// BenchmarkGF2PolymulBatch measures batched polymul throughput at
// various batch sizes, matching the user-requested test matrix:
//
// n in {2048, 4096, 8192} bits (vector-length surrogate is HQC mode)
// batch in {1, 32}
//
// HQC vector sizes (PARAM_N in bits): 17669, 35851, 57637.
// We use those directly rather than synthetic n — the production
// API never sees synthetic vector sizes.
func BenchmarkGF2PolymulBatch(b *testing.B) {
cases := []struct {
mode Mode
name string
batch int
}{
{HQC128, "HQC-128/batch=1", 1},
{HQC128, "HQC-128/batch=32", 32},
{HQC192, "HQC-192/batch=1", 1},
{HQC192, "HQC-192/batch=32", 32},
{HQC256, "HQC-256/batch=1", 1},
{HQC256, "HQC-256/batch=32", 32},
}
for _, tc := range cases {
b.Run(tc.name, func(b *testing.B) {
n, _ := vecNSize64(tc.mode)
a := make([]uint64, n)
bb := make([]uint64, n)
c := make([]uint64, n)
r := rand.New(rand.NewPCG(uint64(tc.mode)+1, uint64(tc.batch)*17))
for i := 0; i < n; i++ {
a[i] = r.Uint64()
bb[i] = r.Uint64()
}
b.SetBytes(int64(tc.batch * n * 8))
b.ResetTimer()
for iter := 0; iter < b.N; iter++ {
for j := 0; j < tc.batch; j++ {
_ = GF2Polymul(tc.mode, c, a, bb)
}
}
})
}
}