Files
zeekay cb2714a652 accel: implement lux_lattice_ntt_mldsa_batch + lux_mldsa_verify_batch (was ErrNotSupported stub)
Pulsar audit (commit 2dd1383) identified these as the missing wiring blocking
GPU acceleration for ML-DSA threshold signing. This change ships the C ABI
plumbing end-to-end:

  1. internal/capi/include/lux/accel/c_api.h
     - declares lux_mldsa_sign_batch, lux_mldsa_verify_batch (FIPS 204; modes
       2/3/5 = ML-DSA-44/65/87)
     - declares lux_lattice_ntt_mldsa_batch (in-place forward/inverse NTT
       over Z_q[X]/(X^256+1), q=8380417)

  2. internal/capi/stub.go — adds weak-symbol stubs returning LUX_NO_BACKEND
     so the build succeeds without libluxaccel.

  3. internal/capi/capi.go — replaces the placeholder _ = stubs that returned
     ErrNotSupported with real cgo calls. Reorders the header search path so
     the vendored c_api.h takes precedence over older installed snapshots
     (single source of truth: ${SRCDIR}/include/lux/accel/c_api.h).

  4. ops_lattice.go — adds LatticeNTTMLDSABatch to the LatticeOps interface,
     consumed by both cgoLatticeOps (ops_c.go) and stubLatticeOps (ops.go).

  5. accel_c.go — top-level Go API:
       MLDSAVerifyBatch(mode, msgs, sigs, pks, msgWidth)
       MLDSASignBatch(mode, msgs, sks, msgWidth)
       LatticeNTTMLDSABatch(polys, inverse)
     Below MLDSABatchThreshold=8 each returns ErrNotSupported so callers
     route to their per-element CPU oracle (pulsar's existing fallback).

  6. errors.go — adds MLDSAMode{44,65,87}, per-mode pk/sk/sig widths,
     MLDSANTTPolyLen=256, MLDSABatchThreshold=8.

  7. ops/lattice/mldsa_ntt.go — Go CPU oracle byte-equal to
     PQCLEAN_MLDSA65_CLEAN_ntt and PQCLEAN_MLDSA65_CLEAN_invntt_tomont.
     Same 256-entry zeta table the lux-private/gpu-kernels/ops/lattice/
     ntt_mldsa kernels embed across every backend (CUDA/HIP/Metal/WGSL).

Tests:
  - ops/lattice/mldsa_ntt_test.go: round-trip identity, zero-input, constant-
    poly evaluation, zeta-table endpoint pins, Montgomery-reduce edge case,
    batch=22 (Pulsar Round-2 shape) byte-equality to per-poly oracle.
  - accel_test.go: MLDSAVerifyBatch/MLDSASignBatch/LatticeNTTMLDSABatch
    surface checks, FIPS 204 width constants.

Verified:
  - go test ./... PASS on cgo + CGO_ENABLED=0 + -tags=accel.
  - pulsar TestPulsar_GPU_ByteEqual PASS (~/work/lux/pulsar regression).
  - go vet clean, gofmt clean.
2026-06-03 12:23:15 -07:00

605 lines
19 KiB
Go

//go:build cgo
// Package capi provides CGO bindings to the lux-accel C library.
//
// Library resolution order:
// 1. System paths (/usr/local/lib, /usr/lib)
// 2. Homebrew paths (/opt/homebrew/lib)
// 3. Local luxcpp install (../../../luxcpp/install/lib)
//
// Install the library using one of:
// - make install-system (from lux/accel)
// - make build-deps && make install-system (build from luxcpp source)
// - scripts/fetch-luxcpp.sh accel (download pre-built)
package capi
/*
// Header search order — VENDORED FIRST.
//
// The vendored c_api.h shipped under ${SRCDIR}/include is the single
// source of truth for the C ABI. Putting it first guarantees that every
// declaration (including ones added since the last luxcpp install) is
// visible to cgo regardless of what version of the public luxcpp header
// is sitting in /usr/local/include or /opt/homebrew/include. The stub.go
// file provides matching weak symbols so builds succeed even when
// libluxaccel is not linked.
#cgo CFLAGS: -I${SRCDIR}/include
// Optional installed headers (older snapshots are tolerated — they
// supply nothing the vendored header doesn't already declare).
#cgo CFLAGS: -I/usr/local/include -I/opt/homebrew/include -I${SRCDIR}/../../include
// Fallback to local luxcpp install (relative to this file)
#cgo CFLAGS: -I${SRCDIR}/../../../../luxcpp/install/include
// macOS: system paths, homebrew, and local luxcpp with rpaths
#cgo darwin LDFLAGS: -L/usr/local/lib -L/opt/homebrew/lib
#cgo darwin LDFLAGS: -L${SRCDIR}/../../../../luxcpp/install/lib
#cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -Wl,-rpath,/opt/homebrew/lib
#cgo darwin LDFLAGS: -Wl,-rpath,${SRCDIR}/../../../../luxcpp/install/lib
// Real library: set CGO_LDFLAGS=-lluxaccel when libluxaccel is installed
// Linux: system paths and local luxcpp with rpaths
#cgo linux LDFLAGS: -L/usr/local/lib -L/usr/lib
#cgo linux LDFLAGS: -L${SRCDIR}/../../../../luxcpp/install/lib
#cgo linux LDFLAGS: -Wl,-rpath,/usr/local/lib
#cgo linux LDFLAGS: -Wl,-rpath,${SRCDIR}/../../../../luxcpp/install/lib
// Real library: set CGO_LDFLAGS=-lluxaccel when libluxaccel is installed
#include <lux/accel/c_api.h>
#include <stdlib.h>
*/
import "C"
import (
"errors"
"fmt"
"sync"
"unsafe"
)
var (
initOnce sync.Once
initErr error
)
// Error types matching C library status codes.
var (
ErrOutOfMemory = errors.New("out of memory")
ErrInvalidArgument = errors.New("invalid argument")
ErrNotSupported = errors.New("not supported")
ErrNoBackends = errors.New("no backends")
ErrKernelFailed = errors.New("kernel failed")
)
// Init initializes the lux-accel library.
func Init() error {
initOnce.Do(func() {
status := C.lux_init()
if status != C.LUX_OK {
initErr = statusToError(status)
}
})
return initErr
}
// Shutdown releases library resources.
func Shutdown() {
C.lux_shutdown()
}
// Version returns the library version.
func Version() string {
return C.GoString(C.lux_version())
}
// GetError returns the last error message.
func GetError() string {
return C.GoString(C.lux_get_error())
}
// LoadBackend loads a backend plugin from path.
func LoadBackend(path string) error {
cpath := C.CString(path)
defer C.free(unsafe.Pointer(cpath))
status := C.lux_load_backend(cpath)
return statusToError(status)
}
// BackendCount returns the number of available backends.
func BackendCount() int {
return int(C.lux_backend_count())
}
// BackendTypeAt returns the backend type at index.
func BackendTypeAt(index int) int {
return int(C.lux_backend_type_at(C.int(index)))
}
// DeviceCount returns the number of devices for a backend.
func DeviceCount(backend int) int {
return int(C.lux_device_count(C.lux_backend_type(backend)))
}
// DeviceInfo contains device information.
type DeviceInfo struct {
Name string
Vendor string
Backend int
IsDiscrete bool
IsUnifiedMemory bool
TotalMemory uint64
MaxWorkgroupSize uint32
SIMDWidth uint32
}
// GetDeviceInfo retrieves device information.
func GetDeviceInfo(backend, index int) (*DeviceInfo, error) {
var info C.lux_device_info
status := C.lux_get_device_info(C.lux_backend_type(backend), C.int(index), &info)
if status != C.LUX_OK {
return nil, statusToError(status)
}
return &DeviceInfo{
Name: C.GoString(info.name),
Vendor: C.GoString(info.vendor),
Backend: int(info.backend),
IsDiscrete: info.is_discrete != 0,
IsUnifiedMemory: info.is_unified_memory != 0,
TotalMemory: uint64(info.total_memory),
MaxWorkgroupSize: uint32(info.max_workgroup_size),
SIMDWidth: uint32(info.simd_width),
}, nil
}
// Session wraps a C session handle.
type Session struct {
handle C.lux_session
}
// CreateSession creates a new session.
func CreateSession() (*Session, error) {
var handle C.lux_session
status := C.lux_session_create(&handle)
if status != C.LUX_OK {
return nil, statusToError(status)
}
return &Session{handle: handle}, nil
}
// CreateSessionWithBackend creates a session with specific backend.
func CreateSessionWithBackend(backend int) (*Session, error) {
var handle C.lux_session
status := C.lux_session_create_with_backend(C.lux_backend_type(backend), &handle)
if status != C.LUX_OK {
return nil, statusToError(status)
}
return &Session{handle: handle}, nil
}
// CreateSessionWithDevice creates a session with specific device.
func CreateSessionWithDevice(backend, deviceIndex int) (*Session, error) {
var handle C.lux_session
status := C.lux_session_create_with_device(
C.lux_backend_type(backend),
C.int(deviceIndex),
&handle,
)
if status != C.LUX_OK {
return nil, statusToError(status)
}
return &Session{handle: handle}, nil
}
// Destroy releases session resources.
func (s *Session) Destroy() {
if s.handle != nil {
C.lux_session_destroy(s.handle)
s.handle = nil
}
}
// Sync synchronizes all pending operations.
func (s *Session) Sync() error {
status := C.lux_session_sync(s.handle)
return statusToError(status)
}
// Handle returns the raw C handle.
func (s *Session) Handle() C.lux_session {
return s.handle
}
// Tensor wraps a C tensor handle.
type Tensor struct {
handle C.lux_tensor
}
// CreateTensor creates a new tensor.
func CreateTensor(session *Session, dtype int, shape []int) (*Tensor, error) {
if len(shape) == 0 {
return nil, ErrInvalidArgument
}
cshape := make([]C.size_t, len(shape))
for i, s := range shape {
cshape[i] = C.size_t(s)
}
var handle C.lux_tensor
status := C.lux_tensor_create(
session.handle,
C.lux_dtype(dtype),
&cshape[0],
C.size_t(len(shape)),
&handle,
)
if status != C.LUX_OK {
return nil, statusToError(status)
}
return &Tensor{handle: handle}, nil
}
// CreateTensorWithData creates a tensor with initial data.
func CreateTensorWithData(session *Session, dtype int, shape []int, data []byte) (*Tensor, error) {
if len(shape) == 0 || len(data) == 0 {
return nil, ErrInvalidArgument
}
cshape := make([]C.size_t, len(shape))
for i, s := range shape {
cshape[i] = C.size_t(s)
}
var handle C.lux_tensor
status := C.lux_tensor_create_with_data(
session.handle,
C.lux_dtype(dtype),
&cshape[0],
C.size_t(len(shape)),
unsafe.Pointer(&data[0]),
C.size_t(len(data)),
&handle,
)
if status != C.LUX_OK {
return nil, statusToError(status)
}
return &Tensor{handle: handle}, nil
}
// Destroy releases tensor resources.
func (t *Tensor) Destroy() {
if t.handle != nil {
C.lux_tensor_destroy(t.handle)
t.handle = nil
}
}
// NDim returns the number of dimensions.
func (t *Tensor) NDim() int {
return int(C.lux_tensor_ndim(t.handle))
}
// Shape returns the size of a dimension.
func (t *Tensor) Shape(dim int) int {
return int(C.lux_tensor_shape(t.handle, C.size_t(dim)))
}
// NumEl returns the total number of elements.
func (t *Tensor) NumEl() int {
return int(C.lux_tensor_numel(t.handle))
}
// Bytes returns the total byte size.
func (t *Tensor) Bytes() int {
return int(C.lux_tensor_bytes(t.handle))
}
// DType returns the data type.
func (t *Tensor) DType() int {
return int(C.lux_tensor_dtype(t.handle))
}
// ToHost copies data to host memory.
func (t *Tensor) ToHost(dst []byte) error {
status := C.lux_tensor_to_host(t.handle, unsafe.Pointer(&dst[0]), C.size_t(len(dst)))
return statusToError(status)
}
// FromHost copies data from host memory.
func (t *Tensor) FromHost(src []byte) error {
status := C.lux_tensor_from_host(t.handle, unsafe.Pointer(&src[0]), C.size_t(len(src)))
return statusToError(status)
}
// Handle returns the raw C handle.
func (t *Tensor) Handle() C.lux_tensor {
return t.handle
}
// HandlePtr returns the handle as uintptr.
func (t *Tensor) HandlePtr() uintptr {
return uintptr(unsafe.Pointer(t.handle))
}
// statusToError converts C status to Go error.
func statusToError(status C.lux_status) error {
switch status {
case C.LUX_OK:
return nil
case C.LUX_ERROR:
return fmt.Errorf("lux error: %s", GetError())
case C.LUX_OUT_OF_MEMORY:
return ErrOutOfMemory
case C.LUX_INVALID_ARGUMENT:
return ErrInvalidArgument
case C.LUX_NOT_SUPPORTED:
return ErrNotSupported
case C.LUX_NO_BACKEND:
return ErrNoBackends
case C.LUX_KERNEL_ERROR:
return ErrKernelFailed
default:
return fmt.Errorf("unknown lux error: %d", status)
}
}
// ML operations
// MatMul performs matrix multiplication.
func MatMul(session *Session, a, b, c *Tensor) error {
status := C.lux_matmul(session.handle, a.handle, b.handle, c.handle)
return statusToError(status)
}
// ReLU applies ReLU activation.
func ReLU(session *Session, input, output *Tensor) error {
status := C.lux_relu(session.handle, input.handle, output.handle)
return statusToError(status)
}
// GELU applies GELU activation.
func GELU(session *Session, input, output *Tensor) error {
status := C.lux_gelu(session.handle, input.handle, output.handle)
return statusToError(status)
}
// Softmax applies softmax.
func Softmax(session *Session, input, output *Tensor, axis int) error {
status := C.lux_softmax(session.handle, input.handle, output.handle, C.int(axis))
return statusToError(status)
}
// LayerNorm applies layer normalization.
func LayerNorm(session *Session, input, gamma, beta, output *Tensor, eps float32) error {
status := C.lux_layer_norm(session.handle, input.handle, gamma.handle, beta.handle, output.handle, C.float(eps))
return statusToError(status)
}
// Attention computes attention.
func Attention(session *Session, q, k, v, output *Tensor, scale float32) error {
status := C.lux_attention(session.handle, q.handle, k.handle, v.handle, output.handle, C.float(scale))
return statusToError(status)
}
// Crypto operations
// SHA256 computes SHA-256.
func SHA256(session *Session, input, output *Tensor) error {
status := C.lux_sha256(session.handle, input.handle, output.handle)
return statusToError(status)
}
// Keccak256 computes Keccak-256.
func Keccak256(session *Session, input, output *Tensor) error {
status := C.lux_keccak256(session.handle, input.handle, output.handle)
return statusToError(status)
}
// Poseidon computes Poseidon hash.
func Poseidon(session *Session, input, output *Tensor) error {
status := C.lux_poseidon(session.handle, input.handle, output.handle)
return statusToError(status)
}
// ECDSAVerifyBatch verifies ECDSA signatures.
func ECDSAVerifyBatch(session *Session, messages, signatures, pubkeys, results *Tensor) error {
status := C.lux_ecdsa_verify_batch(session.handle, messages.handle, signatures.handle, pubkeys.handle, results.handle)
return statusToError(status)
}
// Ed25519VerifyBatch verifies Ed25519 signatures.
func Ed25519VerifyBatch(session *Session, messages, signatures, pubkeys, results *Tensor) error {
status := C.lux_ed25519_verify_batch(session.handle, messages.handle, signatures.handle, pubkeys.handle, results.handle)
return statusToError(status)
}
// BLSVerifyBatch verifies BLS signatures.
func BLSVerifyBatch(session *Session, messages, signatures, pubkeys, results *Tensor) error {
status := C.lux_bls_verify_batch(session.handle, messages.handle, signatures.handle, pubkeys.handle, results.handle)
return statusToError(status)
}
// MerkleRoot computes Merkle root.
func MerkleRoot(session *Session, leaves, root *Tensor) error {
status := C.lux_merkle_root(session.handle, leaves.handle, root.handle)
return statusToError(status)
}
// ZK operations
// NTT performs Number Theoretic Transform.
func NTT(session *Session, input, output, roots *Tensor, modulus uint64) error {
status := C.lux_ntt(session.handle, input.handle, output.handle, roots.handle, C.uint64_t(modulus))
return statusToError(status)
}
// INTT performs inverse NTT.
func INTT(session *Session, input, output, invRoots *Tensor, modulus uint64) error {
status := C.lux_intt(session.handle, input.handle, output.handle, invRoots.handle, C.uint64_t(modulus))
return statusToError(status)
}
// MSM performs multi-scalar multiplication.
func MSM(session *Session, scalars, bases, result *Tensor) error {
status := C.lux_msm(session.handle, scalars.handle, bases.handle, result.handle)
return statusToError(status)
}
// PolyMul multiplies polynomials.
func PolyMul(session *Session, a, b, c *Tensor, modulus uint64) error {
status := C.lux_poly_mul(session.handle, a.handle, b.handle, c.handle, C.uint64_t(modulus))
return statusToError(status)
}
// Lattice operations
// KyberKeyGen generates Kyber key pair.
func KyberKeyGen(session *Session, pk, sk *Tensor) error {
status := C.lux_kyber_keygen(session.handle, pk.handle, sk.handle)
return statusToError(status)
}
// KyberEncaps encapsulates shared secret.
func KyberEncaps(session *Session, pk, ct, ss *Tensor) error {
status := C.lux_kyber_encaps(session.handle, pk.handle, ct.handle, ss.handle)
return statusToError(status)
}
// KyberDecaps decapsulates shared secret.
func KyberDecaps(session *Session, ct, sk, ss *Tensor) error {
status := C.lux_kyber_decaps(session.handle, ct.handle, sk.handle, ss.handle)
return statusToError(status)
}
// DilithiumSign signs a message.
func DilithiumSign(session *Session, msg, sk, sig *Tensor) error {
status := C.lux_dilithium_sign(session.handle, msg.handle, sk.handle, sig.handle)
return statusToError(status)
}
// DilithiumVerify verifies a signature.
func DilithiumVerify(session *Session, msg, sig, pk *Tensor) (bool, error) {
var valid C.int
status := C.lux_dilithium_verify(session.handle, msg.handle, sig.handle, pk.handle, &valid)
if err := statusToError(status); err != nil {
return false, err
}
return valid != 0, nil
}
// SLHDSASignBatch batch-signs SLH-DSA (FIPS 205 / Magnetar) messages.
// mode encodes the parameter set per c_api.h: 2=SHA2-128f, 3=SHA2-192f,
// 5=SHA2-256f, 12=SHAKE-128f, 13=SHAKE-192f, 15=SHAKE-256f.
func SLHDSASignBatch(session *Session, mode int, msgs, sks, sigs *Tensor) error {
status := C.lux_slhdsa_sign_batch(session.handle, C.int(mode), msgs.handle, sks.handle, sigs.handle)
return statusToError(status)
}
// SLHDSAVerifyBatch batch-verifies SLH-DSA (FIPS 205 / Magnetar) signatures.
// mode as for SLHDSASignBatch. The results tensor is populated with one
// uint8 per signature (1 = valid, 0 = invalid).
func SLHDSAVerifyBatch(session *Session, mode int, msgs, sigs, pks, results *Tensor) error {
status := C.lux_slhdsa_verify_batch(session.handle, C.int(mode), msgs.handle, sigs.handle, pks.handle, results.handle)
return statusToError(status)
}
// MLDSAVerifyBatch batch-verifies ML-DSA / Dilithium (FIPS 204) signatures.
// mode encodes the parameter set: 2=ML-DSA-44, 3=ML-DSA-65, 5=ML-DSA-87.
// The results tensor is populated with one uint8 per signature (1 = valid,
// 0 = invalid).
//
// The C ABI symbol `lux_mldsa_verify_batch` is declared in c_api.h and
// resolved at link time. When libluxaccel ships an implementation it
// runs FIPS 204 batched verify on the active backend; when no native
// library is linked the weak-symbol stub in stub.go returns
// LUX_NO_BACKEND, which surfaces here as ErrNoBackends. Either way,
// callers see the Go-level error and can fall through to the per-
// element CPU verify path — the substrate boundary is the wire, not a
// stub.
func MLDSAVerifyBatch(session *Session, mode int, msgs, sigs, pks, results *Tensor) error {
status := C.lux_mldsa_verify_batch(session.handle, C.int(mode), msgs.handle, sigs.handle, pks.handle, results.handle)
return statusToError(status)
}
// MLDSASignBatch batch-signs ML-DSA / Dilithium (FIPS 204) messages.
// mode as for MLDSAVerifyBatch. See doc on MLDSAVerifyBatch for ABI status.
func MLDSASignBatch(session *Session, mode int, msgs, sks, sigs *Tensor) error {
status := C.lux_mldsa_sign_batch(session.handle, C.int(mode), msgs.handle, sks.handle, sigs.handle)
return statusToError(status)
}
// LatticeNTTMLDSABatch performs the in-place forward (inverse=false) or
// inverse (inverse=true) Number-Theoretic Transform over Z_q[X]/(X^256 + 1)
// with q = 8380417 (the ML-DSA prime), batched across n polynomials.
//
// The polys tensor MUST be LUX_DTYPE_I32 with shape [n, 256]; the transform
// is in-place. Byte-equal to PQCLEAN_MLDSA65_CLEAN_ntt for the forward
// direction; the inverse direction (when implemented by the backend) is
// the matching PQCLEAN_MLDSA65_CLEAN_invntt_tomont.
//
// When libluxaccel has not linked an implementation, the weak stub
// returns LUX_NO_BACKEND which surfaces as ErrNoBackends — callers
// then route to their per-poly CPU NTT (Pulsar does this via the
// luxfi/lattice/v7 dispatcher).
func LatticeNTTMLDSABatch(session *Session, polys *Tensor, inverse bool) error {
var inv C.int
if inverse {
inv = 1
}
status := C.lux_lattice_ntt_mldsa_batch(session.handle, polys.handle, inv)
return statusToError(status)
}
// FHE operations
// BFVEncrypt encrypts with BFV.
func BFVEncrypt(session *Session, plaintext, pk, ciphertext *Tensor) error {
status := C.lux_bfv_encrypt(session.handle, plaintext.handle, pk.handle, ciphertext.handle)
return statusToError(status)
}
// BFVDecrypt decrypts BFV.
func BFVDecrypt(session *Session, ciphertext, sk, plaintext *Tensor) error {
status := C.lux_bfv_decrypt(session.handle, ciphertext.handle, sk.handle, plaintext.handle)
return statusToError(status)
}
// BFVAdd adds BFV ciphertexts.
func BFVAdd(session *Session, ct1, ct2, result *Tensor) error {
status := C.lux_bfv_add(session.handle, ct1.handle, ct2.handle, result.handle)
return statusToError(status)
}
// BFVMultiply multiplies BFV ciphertexts.
func BFVMultiply(session *Session, ct1, ct2, relinKey, result *Tensor) error {
status := C.lux_bfv_multiply(session.handle, ct1.handle, ct2.handle, relinKey.handle, result.handle)
return statusToError(status)
}
// DEX operations
// ConstantProductSwap computes AMM swap.
func ConstantProductSwap(session *Session, reserveX, reserveY, amountIn, amountOut *Tensor, xToY bool, fee float32) error {
var dir C.int
if xToY {
dir = 1
}
status := C.lux_constant_product_swap(session.handle, reserveX.handle, reserveY.handle, amountIn.handle, dir, amountOut.handle, C.float(fee))
return statusToError(status)
}
// ComputeTWAP computes time-weighted average price.
func ComputeTWAP(session *Session, prices, timestamps, twap *Tensor, start, end uint64) error {
status := C.lux_compute_twap(session.handle, prices.handle, timestamps.handle, C.uint64_t(start), C.uint64_t(end), twap.handle)
return statusToError(status)
}
// MatchOrders matches orders.
func MatchOrders(session *Session, bids, asks, matches, prices, amounts *Tensor) error {
status := C.lux_match_orders(session.handle, bids.handle, asks.handle, matches.handle, prices.handle, amounts.handle)
return statusToError(status)
}