mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
backend: add GPU substrate selector with fallback policy + ABI guards
Adds the canonical runtime substrate selector for luxfi/crypto and decomplects
all per-algorithm GPU dispatchers behind it.
New `backend` API:
- Default()/SetDefault/Resolved()/IsGPU/IsCGo/IsVanilla — runtime selection
- CGoAvailable()/GPUAvailable() — real probes (was stubbed)
- Probe() returning Snapshot{Default, Resolved, CGo, GPU, Disabled,
GPUBackend, GPUDeviceCount, AccelVersion, Fallbacks}
- GPUDisabled() reads LUX_GPU_DISABLE operator kill switch
- RecordFallback(reason, where) atomic counter + one-shot log per reason,
low-cardinality FallbackReason enum (disabled / unsupported / probe_failed
/ backend_unavailable / abi_mismatch)
Dispatcher cleanup (one-and-one-way):
- All Resolve(gpuhost.Available(), false) call sites replaced with IsGPU()
- hqc switched to IsVanilla() (its accel batch wins for any non-vanilla pick)
- gpu/gpu.go now delegates entirely to backend (no separate session)
- internal/gpuhost dropped Snapshot()/Provenance — backend.Probe() canonical
Build tag policy: CGo is the only gate. There is no `gpu` build tag.
LLM.md documents the canonical surface.
This commit is contained in:
@@ -1664,18 +1664,19 @@ gpuCalls, cpuCalls := ctx.Stats()
|
||||
|
||||
### Build Tags
|
||||
|
||||
| Build | Tags | GPU Support |
|
||||
|-------|------|-------------|
|
||||
| Default | (none) | CPU only (gnark-crypto) |
|
||||
| Metal | `darwin,arm64,cgo,gpu` | Apple Silicon GPU |
|
||||
| CUDA | `linux,cgo,gpu` | NVIDIA GPU (future) |
|
||||
CGO is the only gate. There is no `gpu` build tag.
|
||||
|
||||
| Build | GPU Support |
|
||||
|-------|-------------|
|
||||
| `CGO_ENABLED=0` | Pure Go fallback (gnark-crypto). |
|
||||
| `CGO_ENABLED=1` | Runtime probe via `luxfi/accel`: Metal on darwin, CUDA on linux/NVIDIA, CPU oracle otherwise. |
|
||||
|
||||
```bash
|
||||
# CPU-only build
|
||||
go build ./...
|
||||
CGO_ENABLED=0 go build ./...
|
||||
|
||||
# Metal GPU build
|
||||
CGO_ENABLED=1 go build -tags "gpu" ./...
|
||||
# GPU-capable build (runtime decides)
|
||||
go build ./...
|
||||
```
|
||||
|
||||
### Files
|
||||
@@ -1721,4 +1722,37 @@ CGO_ENABLED=1 go test ./gpu/...
|
||||
|
||||
---
|
||||
|
||||
## Backend Selector
|
||||
|
||||
One-and-one-way runtime substrate selection in `github.com/luxfi/crypto/backend`:
|
||||
|
||||
| Surface | Purpose |
|
||||
|---------|---------|
|
||||
| `Default() / SetDefault(b)` | Active selection (Auto/Vanilla/CGo/GPU). Env `CRYPTO_BACKEND` overrides at init. |
|
||||
| `CGoAvailable()` | Compile-time constant: true iff built with `CGO_ENABLED=1`. |
|
||||
| `GPUAvailable()` | Lazy probe via `internal/gpuhost` → `luxfi/accel.Init()`. |
|
||||
| `Available(b)` | Per-backend availability. |
|
||||
| `Resolved()` | `Resolve(GPUAvailable(), CGoAvailable())` — one-call shortcut. |
|
||||
| `IsGPU() / IsCGo() / IsVanilla()` | Dispatcher gates. |
|
||||
| `Probe() → Snapshot` | Diagnostic line: `backend{default=auto resolved=cgo cgo=true gpu=false accel=...}`. |
|
||||
|
||||
### Dispatcher pattern
|
||||
|
||||
```go
|
||||
func batchGPU(...) (bool, error) {
|
||||
if !backend.IsGPU() { return false, nil }
|
||||
sess := gpuhost.Session()
|
||||
// pack tensors, call sess.Crypto().Keccak256(...), copy out
|
||||
}
|
||||
```
|
||||
|
||||
Wired in: `keccak256 sha256 blake3 bls ed25519 secp256k1 mldsa mlkem slhdsa hqc`.
|
||||
`hqc` uses `!backend.IsVanilla()` (accel batch beats per-item PQClean for any
|
||||
non-Vanilla selection).
|
||||
|
||||
### `gpu/` package
|
||||
|
||||
Public diagnostic surface — delegates entirely to `backend`. `Available()`,
|
||||
`Backend()`, `Devices()`, `Version()` are thin wrappers. No separate session.
|
||||
|
||||
**Single source of truth for AI assistants on this project.**
|
||||
|
||||
+52
-12
@@ -94,30 +94,70 @@ func SetDefault(b Backend) {
|
||||
atomic.StoreUint32(¤t, uint32(b))
|
||||
}
|
||||
|
||||
// Available is reserved for backend probing. Today, Auto and Vanilla are
|
||||
// always available; CGo and GPU are reported by build-tagged shims and the
|
||||
// runtime resolver.
|
||||
// CGoAvailable reports whether the binary was compiled with CGO_ENABLED=1.
|
||||
// The answer is a compile-time constant: cgoLinked is set by cgo_yes.go
|
||||
// when cgo is on and cgo_no.go when cgo is off.
|
||||
func CGoAvailable() bool { return cgoLinked }
|
||||
|
||||
// GPUAvailable reports whether the luxfi/accel GPU substrate is reachable
|
||||
// in this process. First call lazily initialises accel via gpuhost; the
|
||||
// answer is cached afterwards. Returns false unconditionally when the
|
||||
// LUX_GPU_DISABLE kill switch is set (see disable.go).
|
||||
func GPUAvailable() bool {
|
||||
if gpuDisabled {
|
||||
return false
|
||||
}
|
||||
return gpuLinked()
|
||||
}
|
||||
|
||||
// Available reports whether backend b is currently usable. Auto and Vanilla
|
||||
// are always available; CGo requires CGO_ENABLED=1; GPU requires a live
|
||||
// accel session with at least one device.
|
||||
func Available(b Backend) bool {
|
||||
switch b {
|
||||
case Auto, Vanilla:
|
||||
return true
|
||||
case CGo:
|
||||
return CGoAvailable()
|
||||
case GPU:
|
||||
return GPUAvailable()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Resolved picks the concrete backend for the caller using the real
|
||||
// CGo / GPU probes. This is the one-call shortcut for the common pattern:
|
||||
//
|
||||
// if backend.Resolved() == backend.GPU {
|
||||
// // dispatch GPU path
|
||||
// }
|
||||
//
|
||||
// Equivalent to Resolve(GPUAvailable(), CGoAvailable()).
|
||||
func Resolved() Backend { return Resolve(GPUAvailable(), CGoAvailable()) }
|
||||
|
||||
// IsGPU reports whether Resolved() picks GPU. Algorithm dispatchers gate
|
||||
// their GPU path with this:
|
||||
//
|
||||
// if !backend.IsGPU() { return false, nil }
|
||||
func IsGPU() bool { return Resolved() == GPU }
|
||||
|
||||
// IsCGo reports whether Resolved() picks CGo.
|
||||
func IsCGo() bool { return Resolved() == CGo }
|
||||
|
||||
// IsVanilla reports whether Resolved() picks Vanilla. Useful for dispatchers
|
||||
// whose accelerated path covers both GPU and CGo and only needs to bail out
|
||||
// on explicit Vanilla selection (e.g. crypto/hqc batch entrypoints).
|
||||
func IsVanilla() bool { return Resolved() == Vanilla }
|
||||
|
||||
// Resolve picks a concrete backend for the caller. If Default() is Auto the
|
||||
// resolution falls back through GPU → CGo → Vanilla, choosing the first
|
||||
// backend reported as available by the supplied probes. probes may be nil;
|
||||
// in that case Resolve returns Vanilla for Auto.
|
||||
// backend reported as available by the supplied probes.
|
||||
//
|
||||
// This is the primary entry point used inside algorithm packages:
|
||||
//
|
||||
// switch backend.Resolve(gpuOK, cgoOK) {
|
||||
// case backend.GPU: return keccak256GPU(in)
|
||||
// case backend.CGo: return keccak256CGo(in)
|
||||
// default: return keccak256Vanilla(in)
|
||||
// }
|
||||
// Prefer Resolved() / IsGPU() / IsCGo() / IsVanilla() — they call this
|
||||
// function with the real probes from CGoAvailable() and GPUAvailable().
|
||||
// The four-arg form remains exported for callers that already know the
|
||||
// answer (tests, custom dispatchers with their own probes).
|
||||
func Resolve(gpuAvailable, cgoAvailable bool) Backend {
|
||||
switch d := Default(); d {
|
||||
case Vanilla:
|
||||
|
||||
@@ -85,6 +85,92 @@ func TestStringRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCGoAvailableMatchesBuildTag(t *testing.T) {
|
||||
// cgoLinked is a compile-time constant; CGoAvailable is its accessor.
|
||||
// On a cgo build the value is true, on a no-cgo build it is false.
|
||||
// Either way, repeated calls must return the same value.
|
||||
v1 := CGoAvailable()
|
||||
v2 := CGoAvailable()
|
||||
if v1 != v2 {
|
||||
t.Fatalf("CGoAvailable() not stable: %v then %v", v1, v2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGPUAvailableStable(t *testing.T) {
|
||||
// GPUAvailable goes through gpuhost.Available which caches the
|
||||
// result of a single accel.Init. Two consecutive calls must agree.
|
||||
v1 := GPUAvailable()
|
||||
v2 := GPUAvailable()
|
||||
if v1 != v2 {
|
||||
t.Fatalf("GPUAvailable() not stable: %v then %v", v1, v2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailableMatchesProbes(t *testing.T) {
|
||||
if !Available(Auto) || !Available(Vanilla) {
|
||||
t.Fatal("Auto and Vanilla must always be available")
|
||||
}
|
||||
if Available(CGo) != CGoAvailable() {
|
||||
t.Errorf("Available(CGo)=%v differs from CGoAvailable()=%v",
|
||||
Available(CGo), CGoAvailable())
|
||||
}
|
||||
if Available(GPU) != GPUAvailable() {
|
||||
t.Errorf("Available(GPU)=%v differs from GPUAvailable()=%v",
|
||||
Available(GPU), GPUAvailable())
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvedMatchesResolve(t *testing.T) {
|
||||
t.Cleanup(func() { SetDefault(Auto) })
|
||||
for _, d := range []Backend{Auto, Vanilla, CGo, GPU} {
|
||||
SetDefault(d)
|
||||
want := Resolve(GPUAvailable(), CGoAvailable())
|
||||
got := Resolved()
|
||||
if got != want {
|
||||
t.Errorf("Resolved() with Default=%s = %s; want %s", d, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsHelpersMatchResolved(t *testing.T) {
|
||||
t.Cleanup(func() { SetDefault(Auto) })
|
||||
for _, d := range []Backend{Auto, Vanilla, CGo, GPU} {
|
||||
SetDefault(d)
|
||||
r := Resolved()
|
||||
if IsGPU() != (r == GPU) {
|
||||
t.Errorf("IsGPU mismatch under default=%s: IsGPU=%v Resolved=%s", d, IsGPU(), r)
|
||||
}
|
||||
if IsCGo() != (r == CGo) {
|
||||
t.Errorf("IsCGo mismatch under default=%s: IsCGo=%v Resolved=%s", d, IsCGo(), r)
|
||||
}
|
||||
if IsVanilla() != (r == Vanilla) {
|
||||
t.Errorf("IsVanilla mismatch under default=%s: IsVanilla=%v Resolved=%s", d, IsVanilla(), r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeIsConsistentWithHelpers(t *testing.T) {
|
||||
t.Cleanup(func() { SetDefault(Auto) })
|
||||
SetDefault(Vanilla)
|
||||
p := Probe()
|
||||
if p.Default != Default() {
|
||||
t.Errorf("Probe.Default=%s != Default()=%s", p.Default, Default())
|
||||
}
|
||||
if p.Resolved != Resolved() {
|
||||
t.Errorf("Probe.Resolved=%s != Resolved()=%s", p.Resolved, Resolved())
|
||||
}
|
||||
if p.CGo != CGoAvailable() {
|
||||
t.Errorf("Probe.CGo=%v != CGoAvailable()=%v", p.CGo, CGoAvailable())
|
||||
}
|
||||
if p.GPU != GPUAvailable() {
|
||||
t.Errorf("Probe.GPU=%v != GPUAvailable()=%v", p.GPU, GPUAvailable())
|
||||
}
|
||||
s := p.String()
|
||||
if len(s) == 0 || s[0] != 'b' {
|
||||
t.Errorf("Snapshot.String() = %q; want prefix 'backend{...}'", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvOverride(t *testing.T) {
|
||||
// Saved current state.
|
||||
prev := Default()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build !cgo
|
||||
|
||||
package backend
|
||||
|
||||
// cgoLinked is false: CGO_ENABLED=0 at build time so no C-backed dispatch
|
||||
// path is reachable in this binary. Resolve() will never return CGo.
|
||||
const cgoLinked = false
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build cgo
|
||||
|
||||
package backend
|
||||
|
||||
// cgoLinked reports whether this package was built with CGO_ENABLED=1.
|
||||
// When true, the C-backed dispatch paths in luxfi/crypto (libluxcrypto,
|
||||
// libsecp256k1, blst, ckzg, ...) can be linked at runtime.
|
||||
const cgoLinked = true
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LUX_GPU_DISABLE is the process-wide operator kill switch for GPU
|
||||
// dispatch. Read exactly once at init; the value is then constant for
|
||||
// the lifetime of the process. Set to a truthy value
|
||||
// (1 / true / yes / on) to force every algorithm dispatcher to its CPU
|
||||
// path regardless of whether a device is present.
|
||||
//
|
||||
// This is orthogonal to GPUAvailable() — that probe answers "is there a
|
||||
// device", this knob answers "should we use it even if there is". Use
|
||||
// cases: GPU driver regression in prod, A/B rollout where some racks
|
||||
// stay on CPU, validators sharing a host with another tenant.
|
||||
const envGPUDisable = "LUX_GPU_DISABLE"
|
||||
|
||||
var gpuDisabled = parseTruthy(os.Getenv(envGPUDisable))
|
||||
|
||||
// GPUDisabled reports whether the LUX_GPU_DISABLE kill switch is set.
|
||||
// When true, GPUAvailable() returns false and IsGPU() therefore returns
|
||||
// false, forcing every dispatcher onto its CPU path.
|
||||
func GPUDisabled() bool { return gpuDisabled }
|
||||
|
||||
// parseTruthy mirrors the convention used by strconv.ParseBool but
|
||||
// accepts a few extra human-friendly spellings. Empty / 0 / false / no /
|
||||
// off → false; anything else → true.
|
||||
func parseTruthy(s string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(s)) {
|
||||
case "", "0", "false", "no", "off":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package backend
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseTruthy(t *testing.T) {
|
||||
cases := map[string]bool{
|
||||
"": false,
|
||||
"0": false,
|
||||
"false": false,
|
||||
"FALSE": false,
|
||||
"no": false,
|
||||
"NO": false,
|
||||
"off": false,
|
||||
" 0 ": false,
|
||||
"1": true,
|
||||
"true": true,
|
||||
"yes": true,
|
||||
"on": true,
|
||||
"YES": true,
|
||||
"foo": true, // any non-falsy spelling counts as truthy
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := parseTruthy(in); got != want {
|
||||
t.Errorf("parseTruthy(%q) = %v; want %v", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGPUDisabledStable(t *testing.T) {
|
||||
// gpuDisabled is parsed once at init; repeated calls must agree.
|
||||
v1 := GPUDisabled()
|
||||
v2 := GPUDisabled()
|
||||
if v1 != v2 {
|
||||
t.Fatalf("GPUDisabled() not stable: %v then %v", v1, v2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGPUAvailableHonorsDisabled(t *testing.T) {
|
||||
// When disabled, GPUAvailable must return false regardless of probe.
|
||||
if GPUDisabled() && GPUAvailable() {
|
||||
t.Fatal("GPUAvailable=true while GPUDisabled=true — kill switch ignored")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// FallbackReason is the low-cardinality enum recorded by RecordFallback.
|
||||
// Keeping the set small and closed is deliberate — these values land in
|
||||
// counter labels and log lines, and dynamic strings there are an
|
||||
// observability anti-pattern (label explosion in Prometheus, log noise).
|
||||
type FallbackReason uint32
|
||||
|
||||
const (
|
||||
// FallbackDisabled — LUX_GPU_DISABLE is set; dispatcher routed to CPU.
|
||||
FallbackDisabled FallbackReason = iota
|
||||
// FallbackUnsupported — host driver reported no usable device
|
||||
// (cudaGetDeviceCount == 0, Metal device init failed, etc.).
|
||||
FallbackUnsupported
|
||||
// FallbackProbeFailed — first-use byte-equality probe rejected
|
||||
// the GPU path (e.g. FHE NTT (N,Q) convention mismatch).
|
||||
FallbackProbeFailed
|
||||
// FallbackBackendUnavailable — accel session itself failed to load
|
||||
// or the linked plugin returned LUX_NOT_SUPPORTED.
|
||||
FallbackBackendUnavailable
|
||||
// FallbackABIMismatch — a Go init() ABI-size/offset check would have
|
||||
// panicked, but a dispatcher recovered and downgraded to CPU. Should
|
||||
// never fire in a released build; included for completeness.
|
||||
FallbackABIMismatch
|
||||
|
||||
numFallbackReasons
|
||||
)
|
||||
|
||||
// String returns the canonical low-cardinality reason name. These are
|
||||
// the strings that appear in metrics labels and log lines.
|
||||
func (r FallbackReason) String() string {
|
||||
switch r {
|
||||
case FallbackDisabled:
|
||||
return "disabled"
|
||||
case FallbackUnsupported:
|
||||
return "unsupported"
|
||||
case FallbackProbeFailed:
|
||||
return "probe_failed"
|
||||
case FallbackBackendUnavailable:
|
||||
return "backend_unavailable"
|
||||
case FallbackABIMismatch:
|
||||
return "abi_mismatch"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
fallbackCounters [numFallbackReasons]uint64
|
||||
fallbackLogOnce [numFallbackReasons]sync.Once
|
||||
)
|
||||
|
||||
// RecordFallback increments the per-reason counter and emits exactly one
|
||||
// log line per distinct (reason) over the lifetime of the process. The
|
||||
// `where` string identifies the dispatcher site ("amm", "clob",
|
||||
// "fhe_ntt") so an operator skimming the log can locate the surface
|
||||
// that fell back without per-call spam.
|
||||
func RecordFallback(reason FallbackReason, where string) {
|
||||
if reason >= numFallbackReasons {
|
||||
return
|
||||
}
|
||||
atomic.AddUint64(&fallbackCounters[reason], 1)
|
||||
fallbackLogOnce[reason].Do(func() {
|
||||
log.Printf("[crypto/backend] GPU fallback: reason=%s where=%s", reason, where)
|
||||
})
|
||||
}
|
||||
|
||||
// FallbackCounters returns a snapshot of the per-reason counters keyed
|
||||
// by the canonical reason name. Suitable for Prometheus / Grafana with
|
||||
// `reason` as a label.
|
||||
func FallbackCounters() map[string]uint64 {
|
||||
out := make(map[string]uint64, int(numFallbackReasons))
|
||||
for i := FallbackReason(0); i < numFallbackReasons; i++ {
|
||||
out[i.String()] = atomic.LoadUint64(&fallbackCounters[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// resetFallbackForTest zeros every counter and reinitialises the
|
||||
// once-per-reason log gate. Test-only; package-private.
|
||||
func resetFallbackForTest() {
|
||||
for i := range fallbackCounters {
|
||||
atomic.StoreUint64(&fallbackCounters[i], 0)
|
||||
fallbackLogOnce[i] = sync.Once{}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFallbackReasonString(t *testing.T) {
|
||||
cases := map[FallbackReason]string{
|
||||
FallbackDisabled: "disabled",
|
||||
FallbackUnsupported: "unsupported",
|
||||
FallbackProbeFailed: "probe_failed",
|
||||
FallbackBackendUnavailable: "backend_unavailable",
|
||||
FallbackABIMismatch: "abi_mismatch",
|
||||
FallbackReason(99): "unknown",
|
||||
}
|
||||
for r, want := range cases {
|
||||
if got := r.String(); got != want {
|
||||
t.Errorf("FallbackReason(%d).String() = %q; want %q", r, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordFallbackIncrements(t *testing.T) {
|
||||
resetFallbackForTest()
|
||||
t.Cleanup(resetFallbackForTest)
|
||||
|
||||
RecordFallback(FallbackDisabled, "amm")
|
||||
RecordFallback(FallbackDisabled, "amm")
|
||||
RecordFallback(FallbackUnsupported, "clob")
|
||||
|
||||
c := FallbackCounters()
|
||||
if c["disabled"] != 2 {
|
||||
t.Errorf("disabled counter = %d; want 2", c["disabled"])
|
||||
}
|
||||
if c["unsupported"] != 1 {
|
||||
t.Errorf("unsupported counter = %d; want 1", c["unsupported"])
|
||||
}
|
||||
if c["probe_failed"] != 0 {
|
||||
t.Errorf("probe_failed counter = %d; want 0", c["probe_failed"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordFallbackKeysAreLowCardinality(t *testing.T) {
|
||||
resetFallbackForTest()
|
||||
t.Cleanup(resetFallbackForTest)
|
||||
|
||||
// Even a million RecordFallback calls only ever land in five keys.
|
||||
for i := 0; i < 1000; i++ {
|
||||
RecordFallback(FallbackDisabled, "amm")
|
||||
RecordFallback(FallbackUnsupported, "clob")
|
||||
RecordFallback(FallbackProbeFailed, "fhe_ntt")
|
||||
RecordFallback(FallbackBackendUnavailable, "amm")
|
||||
RecordFallback(FallbackABIMismatch, "clob")
|
||||
}
|
||||
c := FallbackCounters()
|
||||
if len(c) != int(numFallbackReasons) {
|
||||
t.Fatalf("counter key set drifted: %d keys; want %d", len(c), numFallbackReasons)
|
||||
}
|
||||
for _, want := range []string{"disabled", "unsupported", "probe_failed", "backend_unavailable", "abi_mismatch"} {
|
||||
if c[want] == 0 {
|
||||
t.Errorf("expected non-zero count for %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordFallbackOutOfRangeIsNoop(t *testing.T) {
|
||||
resetFallbackForTest()
|
||||
t.Cleanup(resetFallbackForTest)
|
||||
|
||||
RecordFallback(FallbackReason(99), "amm")
|
||||
for k, v := range FallbackCounters() {
|
||||
if v != 0 {
|
||||
t.Errorf("counter %q got %d after out-of-range call; want 0", k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeSurfacesDisabledAndFallbacks(t *testing.T) {
|
||||
resetFallbackForTest()
|
||||
t.Cleanup(resetFallbackForTest)
|
||||
|
||||
RecordFallback(FallbackUnsupported, "amm")
|
||||
|
||||
p := Probe()
|
||||
if p.Disabled != GPUDisabled() {
|
||||
t.Errorf("Probe.Disabled=%v != GPUDisabled()=%v", p.Disabled, GPUDisabled())
|
||||
}
|
||||
if p.Fallbacks["unsupported"] != 1 {
|
||||
t.Errorf("Probe.Fallbacks[unsupported]=%d; want 1", p.Fallbacks["unsupported"])
|
||||
}
|
||||
s := p.String()
|
||||
if len(s) == 0 || s[0] != 'b' {
|
||||
t.Errorf("Probe.String prefix wrong: %q", s)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package backend
|
||||
|
||||
import "github.com/luxfi/crypto/internal/gpuhost"
|
||||
|
||||
// gpuLinked reports whether the GPU substrate is reachable right now.
|
||||
// The first call triggers a one-time accel.Init() inside gpuhost; the
|
||||
// answer is then cached for the lifetime of the process.
|
||||
//
|
||||
// Returns false when:
|
||||
// - the binary was built without cgo (accel.Init returns ErrNoBackends)
|
||||
// - accel initialised but found no Metal / CUDA / WebGPU device
|
||||
// - the accel.Session allocation failed (driver / permission / OOM)
|
||||
func gpuLinked() bool { return gpuhost.Available() }
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
func TestKeccak256BatchAcrossBackends(t *testing.T) {
|
||||
inputs := make([][]byte, keccak.BatchThreshold+8)
|
||||
inputs := make([][]byte, keccak256.BatchThreshold+8)
|
||||
for i := range inputs {
|
||||
buf := make([]byte, 32)
|
||||
rand.Read(buf)
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/luxfi/accel"
|
||||
"github.com/luxfi/crypto/internal/gpuhost"
|
||||
)
|
||||
|
||||
// Snapshot is the auditable, side-effect-free view of the backend layer at
|
||||
// a single moment. Callers print one of these to make "GPU accelerated"
|
||||
// claims falsifiable — the snapshot says exactly which substrate the
|
||||
// dispatchers will reach for the next batch call.
|
||||
type Snapshot struct {
|
||||
// Default is the user-selected backend (Auto / Vanilla / CGo / GPU).
|
||||
Default Backend
|
||||
// Resolved is the concrete backend Resolved() would return right now,
|
||||
// given the live CGo / GPU probes.
|
||||
Resolved Backend
|
||||
// CGo reports whether CGO_ENABLED was on at build time.
|
||||
CGo bool
|
||||
// GPU reports whether luxfi/accel found at least one device. Always
|
||||
// false when Disabled is true, regardless of the underlying probe.
|
||||
GPU bool
|
||||
// Disabled reflects the LUX_GPU_DISABLE operator kill switch.
|
||||
Disabled bool
|
||||
// GPUBackend names the active accel backend ("metal" / "cuda" /
|
||||
// "webgpu") or "" when no GPU is available.
|
||||
GPUBackend string
|
||||
// GPUDeviceCount is the number of devices visible to accel.
|
||||
GPUDeviceCount int
|
||||
// AccelVersion is the underlying accel library version string.
|
||||
AccelVersion string
|
||||
// Fallbacks is the per-reason fallback counter snapshot. Keys are
|
||||
// the low-cardinality strings returned by FallbackReason.String().
|
||||
Fallbacks map[string]uint64
|
||||
}
|
||||
|
||||
// Probe returns the current backend Snapshot. Side-effect free except for
|
||||
// the lazy accel.Init() inside the GPU probe — calling it before any
|
||||
// algorithm dispatch is the canonical way to print "what would happen if
|
||||
// I called Batch right now."
|
||||
func Probe() Snapshot {
|
||||
s := Snapshot{
|
||||
Default: Default(),
|
||||
Resolved: Resolved(),
|
||||
CGo: CGoAvailable(),
|
||||
GPU: GPUAvailable(),
|
||||
Disabled: GPUDisabled(),
|
||||
AccelVersion: accel.GetVersion(),
|
||||
Fallbacks: FallbackCounters(),
|
||||
}
|
||||
if s.GPU {
|
||||
if sess := gpuhost.Session(); sess != nil {
|
||||
s.GPUBackend = sess.Backend().String()
|
||||
}
|
||||
s.GPUDeviceCount = len(accel.Devices())
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// String formats the snapshot as a single human-readable line suitable
|
||||
// for startup banners and audit logs.
|
||||
func (s Snapshot) String() string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "backend{default=%s resolved=%s cgo=%t gpu=%t",
|
||||
s.Default, s.Resolved, s.CGo, s.GPU)
|
||||
if s.Disabled {
|
||||
b.WriteString(" disabled=true")
|
||||
}
|
||||
if s.GPU {
|
||||
fmt.Fprintf(&b, " gpu_backend=%s devices=%d", s.GPUBackend, s.GPUDeviceCount)
|
||||
}
|
||||
if s.AccelVersion != "" {
|
||||
fmt.Fprintf(&b, " accel=%s", s.AccelVersion)
|
||||
}
|
||||
// Only emit the fallbacks block if at least one counter is non-zero
|
||||
// so the common case stays one short line.
|
||||
any := false
|
||||
for _, v := range s.Fallbacks {
|
||||
if v > 0 {
|
||||
any = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if any {
|
||||
b.WriteString(" fallbacks=[")
|
||||
first := true
|
||||
for _, name := range []string{"disabled", "unsupported", "probe_failed", "backend_unavailable", "abi_mismatch"} {
|
||||
v := s.Fallbacks[name]
|
||||
if v == 0 {
|
||||
continue
|
||||
}
|
||||
if !first {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
fmt.Fprintf(&b, "%s=%d", name, v)
|
||||
first = false
|
||||
}
|
||||
b.WriteByte(']')
|
||||
}
|
||||
b.WriteByte('}')
|
||||
return b.String()
|
||||
}
|
||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func batchVerifyGPU(pks []*PublicKey, msgs [][]byte, sigs []*Signature, out []bool) (bool, error) {
|
||||
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
|
||||
if !backend.IsGPU() {
|
||||
return false, nil
|
||||
}
|
||||
sess := gpuhost.Session()
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func batchGPU(pubs []PublicKey, msgs [][]byte, sigs [][]byte, out []bool) (bool, error) {
|
||||
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
|
||||
if !backend.IsGPU() {
|
||||
return false, nil
|
||||
}
|
||||
sess := gpuhost.Session()
|
||||
|
||||
+17
-21
@@ -1,35 +1,30 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package gpu exposes the runtime status of the GPU acceleration layer used
|
||||
// by luxfi/crypto.
|
||||
// Package gpu is the public probe surface for the GPU acceleration layer
|
||||
// used by luxfi/crypto.
|
||||
//
|
||||
// In normal use, callers should NOT call this package directly. Each
|
||||
// algorithm package (keccak, sha256, bls, mldsa, ...) decides at the call
|
||||
// site whether to dispatch to the GPU based on backend.Default() and the
|
||||
// batch threshold. This package is a single, narrow surface so consumers
|
||||
// can probe the status of the layer for diagnostics and monitoring.
|
||||
// It delegates every call to github.com/luxfi/crypto/backend — the canonical
|
||||
// runtime substrate selector. Callers writing new code should prefer
|
||||
// backend.Probe() / backend.GPUAvailable(); this package stays for back-compat
|
||||
// with consumers that already import "github.com/luxfi/crypto/gpu".
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"github.com/luxfi/accel"
|
||||
"github.com/luxfi/crypto/internal/gpuhost"
|
||||
"github.com/luxfi/crypto/backend"
|
||||
)
|
||||
|
||||
// Available returns true when an accel session was successfully created
|
||||
// and at least one backend is reporting available devices.
|
||||
func Available() bool { return gpuhost.Available() }
|
||||
// Available reports whether the GPU substrate is reachable. Equivalent to
|
||||
// backend.GPUAvailable().
|
||||
func Available() bool { return backend.GPUAvailable() }
|
||||
|
||||
// Backend returns the name of the active backend ("metal", "cuda", "webgpu",
|
||||
// or "" when no GPU is available).
|
||||
func Backend() string {
|
||||
if !Available() {
|
||||
return ""
|
||||
}
|
||||
return gpuhost.Session().Backend().String()
|
||||
}
|
||||
// Backend names the active accel backend ("metal" | "cuda" | "webgpu") or
|
||||
// "" when no GPU is available. Equivalent to backend.Probe().GPUBackend.
|
||||
func Backend() string { return backend.Probe().GPUBackend }
|
||||
|
||||
// Devices returns the list of devices visible to the accel layer.
|
||||
// Devices returns the list of devices visible to the accel layer, or nil
|
||||
// when no GPU is available.
|
||||
func Devices() []accel.DeviceInfo {
|
||||
if !Available() {
|
||||
return nil
|
||||
@@ -37,5 +32,6 @@ func Devices() []accel.DeviceInfo {
|
||||
return accel.Devices()
|
||||
}
|
||||
|
||||
// Version returns the underlying accel library version string.
|
||||
// Version returns the underlying accel library version string. Equivalent
|
||||
// to backend.Probe().AccelVersion.
|
||||
func Version() string { return accel.GetVersion() }
|
||||
|
||||
+7
-13
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/luxfi/accel/ops/code"
|
||||
"github.com/luxfi/crypto/backend"
|
||||
"github.com/luxfi/crypto/internal/gpuhost"
|
||||
)
|
||||
|
||||
// HQC is code-based (Hamming Quasi-Cyclic): the hot path is GF(2)^N
|
||||
@@ -54,20 +53,15 @@ const batchSizeThreshold = 4
|
||||
// OpenMP at the C++ level. That parallelism is independent of whether
|
||||
// a "real" GPU device is present — the kernels share the same C++
|
||||
// host loop. We therefore key dispatch on:
|
||||
// 1. backend.Resolve() does not explicitly request Vanilla (pure-Go).
|
||||
// 1. backend.IsVanilla() is false — i.e. the caller did NOT explicitly
|
||||
// pick the pure-Go fallback.
|
||||
// 2. The batch is large enough to amortise the cgo crossover.
|
||||
//
|
||||
// The gpuhost.Session() call is left for symmetry with crypto/mlkem's
|
||||
// dispatcher (and for future Metal/CUDA backend overrides) but its
|
||||
// nil-ness is not treated as a hard skip.
|
||||
// We do NOT consult a session handle here — accel/ops/code parallelises via
|
||||
// OpenMP at the C++ level whether or not a Metal/CUDA device exists, so the
|
||||
// presence of an accel session is irrelevant to this dispatcher.
|
||||
func batchEncapsulateGPU(pubs []*PublicKey, cts []*Ciphertext, sss [][]byte) (bool, error) {
|
||||
// `cgo` is always true here — this file only compiles under cgo
|
||||
// (the GPU batch surface requires the C++ host library). The
|
||||
// `gpuAvailable` argument tells backend.Resolve whether to
|
||||
// prefer GPU over CGo; we treat either as a green light because
|
||||
// the accel batch surface is faster than per-item PQClean even
|
||||
// without a Metal/CUDA device.
|
||||
if backend.Resolve(gpuhost.Available(), true) == backend.Vanilla {
|
||||
if backend.IsVanilla() {
|
||||
// Caller explicitly requested pure-Go (no PQClean) — no
|
||||
// batch path available, fall through to CPU per-item.
|
||||
return false, nil
|
||||
@@ -148,7 +142,7 @@ func batchEncapsulateGPU(pubs []*PublicKey, cts []*Ciphertext, sss [][]byte) (bo
|
||||
// batchDecapsulateGPU attempts GPU-batched HQC decapsulation.
|
||||
// See batchEncapsulateGPU for the dispatch-gating rationale.
|
||||
func batchDecapsulateGPU(sks []*PrivateKey, cts []*Ciphertext, sss [][]byte) (bool, error) {
|
||||
if backend.Resolve(gpuhost.Available(), true) == backend.Vanilla {
|
||||
if backend.IsVanilla() {
|
||||
return false, nil
|
||||
}
|
||||
if len(sks) < batchSizeThreshold {
|
||||
|
||||
@@ -66,36 +66,7 @@ func InitError() error {
|
||||
return initErr
|
||||
}
|
||||
|
||||
// Provenance captures, at the moment of the call, exactly which
|
||||
// substrate the algorithm dispatchers will reach when they ask gpuhost
|
||||
// for a session. This is the auditable evidence a reviewer asks for
|
||||
// when they want to confirm a "GPU accelerated" claim is not vapor.
|
||||
//
|
||||
// The fields are intentionally narrow and observable:
|
||||
//
|
||||
// - AccelInitialised: did accel.Init() return without error.
|
||||
// - DeviceAvailable: did accel report any device after init.
|
||||
// - SessionLive: did we actually allocate a session.
|
||||
//
|
||||
// Algorithm packages publish their own thin layer on top of this that
|
||||
// also reports whether the plugin's strong symbol for that algorithm
|
||||
// is resolved (e.g. whether lux_slhdsa_verify_batch is the LUX_NOT_SUPPORTED
|
||||
// stub or a strong override from a loaded plugin).
|
||||
type Provenance struct {
|
||||
AccelInitialised bool
|
||||
DeviceAvailable bool
|
||||
SessionLive bool
|
||||
}
|
||||
|
||||
// Snapshot returns the current Provenance. Side-effect-free except for
|
||||
// the lazy accel.Init() inside the underlying Init() — calling it before
|
||||
// any algorithm dispatch is the canonical way to print "what would
|
||||
// happen if I called Batch right now."
|
||||
func Snapshot() Provenance {
|
||||
Init()
|
||||
return Provenance{
|
||||
AccelInitialised: initErr == nil,
|
||||
DeviceAvailable: available && sess != nil,
|
||||
SessionLive: sess != nil,
|
||||
}
|
||||
}
|
||||
// Diagnostic snapshots live in github.com/luxfi/crypto/backend (Probe()).
|
||||
// gpuhost stays narrow: Init / Available / Session / InitError — the
|
||||
// substrate primitives. Anything richer is a diagnostic question and
|
||||
// belongs in backend, which is the canonical runtime substrate selector.
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import (
|
||||
// ErrNoBackends in that mode — the caller treats both shapes as "fall
|
||||
// back to vanilla".
|
||||
func batchGPU(inputs [][]byte, out [][Size]byte) (handled bool, err error) {
|
||||
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
|
||||
if !backend.IsGPU() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -5,7 +5,8 @@
|
||||
//
|
||||
// Dispatch path:
|
||||
//
|
||||
// backend.Resolve(gpuhost.Available(), false) → backend.GPU
|
||||
// backend.IsGPU()
|
||||
// ⇒ Resolved() picks the GPU substrate after probing CGo + GPU availability
|
||||
// ⇒ accel.LatticeOps.MLDSAVerifyBatch / MLDSASignBatch on the shared session
|
||||
// ⇒ luxcpp/lux-accel C API lux_mldsa_{verify,sign}_batch (under development)
|
||||
// ⇒ when a backend plugin registers a strong substrate:
|
||||
@@ -83,7 +84,7 @@ func batchVerifyGPU(pubs []*PublicKey, msgs [][]byte, sigs [][]byte, out []bool)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
|
||||
if !backend.IsGPU() {
|
||||
return false, nil
|
||||
}
|
||||
sess := gpuhost.Session()
|
||||
@@ -195,7 +196,7 @@ func batchSignGPU(privs []*PrivateKey, msgs [][]byte, sigs [][]byte) (bool, erro
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
|
||||
if !backend.IsGPU() {
|
||||
return false, nil
|
||||
}
|
||||
sess := gpuhost.Session()
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// circl reference, so we know the underlying library is intact.
|
||||
// 2. Batch verify equivalence — feed a batch through BatchVerify and
|
||||
// confirm it matches the per-element CPU oracle byte-for-byte. When a
|
||||
// GPU plugin is registered (CRYPTO_BACKEND=gpu + gpuhost detects a
|
||||
// GPU plugin is registered (CRYPTO_BACKEND=gpu + backend.GPUAvailable
|
||||
// device) this exercises the real GPU substrate; otherwise it
|
||||
// validates the goroutine-parallel CPU fallback.
|
||||
// 3. Tamper detection — flip a bit and confirm both paths reject.
|
||||
@@ -29,7 +29,8 @@ import (
|
||||
|
||||
"github.com/cloudflare/circl/sign/mldsa/mldsa44"
|
||||
"github.com/luxfi/crypto/backend"
|
||||
"github.com/luxfi/crypto/internal/gpuhost"
|
||||
|
||||
|
||||
)
|
||||
|
||||
// genBatch44 produces n ML-DSA-44 keypairs + signs distinct messages with
|
||||
@@ -137,7 +138,7 @@ func TestMLDSA44_BatchEquivalence_CPU_GPU(t *testing.T) {
|
||||
t.Fatalf("batchVerifyGPU: %v", err)
|
||||
}
|
||||
if dispatched {
|
||||
t.Logf("GPU dispatch active (gpuhost.Available=%v)", gpuhost.Available())
|
||||
t.Logf("GPU dispatch active (gpu=%v)", backend.GPUAvailable())
|
||||
for i := range cpu {
|
||||
if cpu[i] != gpu[i] {
|
||||
t.Fatalf("CPU/GPU mismatch at[%d]: cpu=%v gpu=%v", i, cpu[i], gpu[i])
|
||||
|
||||
@@ -21,7 +21,8 @@ import (
|
||||
|
||||
"github.com/cloudflare/circl/sign/mldsa/mldsa87"
|
||||
"github.com/luxfi/crypto/backend"
|
||||
"github.com/luxfi/crypto/internal/gpuhost"
|
||||
|
||||
|
||||
)
|
||||
|
||||
func genBatch87(t testing.TB, n int) (pubs []*PublicKey, msgs [][]byte, sigs [][]byte) {
|
||||
@@ -116,7 +117,7 @@ func TestMLDSA87_BatchEquivalence_CPU_GPU(t *testing.T) {
|
||||
t.Fatalf("batchVerifyGPU: %v", err)
|
||||
}
|
||||
if dispatched {
|
||||
t.Logf("GPU dispatch active (gpuhost.Available=%v)", gpuhost.Available())
|
||||
t.Logf("GPU dispatch active (gpu=%v)", backend.GPUAvailable())
|
||||
for i := range cpu {
|
||||
if cpu[i] != gpu[i] {
|
||||
t.Fatalf("CPU/GPU mismatch at[%d]: cpu=%v gpu=%v", i, cpu[i], gpu[i])
|
||||
|
||||
+8
-13
@@ -7,7 +7,6 @@ import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/luxfi/crypto/backend"
|
||||
"github.com/luxfi/crypto/internal/gpuhost"
|
||||
)
|
||||
|
||||
// DispatchTier identifies which path the ML-DSA batch dispatchers will
|
||||
@@ -67,22 +66,18 @@ type Provenance struct {
|
||||
// at runtime. Once a real batch dispatch lands a strong-symbol
|
||||
// observation, follow-up calls report TierGPUSubstrate.
|
||||
func GetProvenance() Provenance {
|
||||
snap := gpuhost.Snapshot()
|
||||
bp := backend.Probe()
|
||||
p := Provenance{
|
||||
AccelInitialised: snap.AccelInitialised,
|
||||
DeviceAvailable: snap.DeviceAvailable,
|
||||
// backend.Probe.GPU is the conjunction the prior gpuhost.Snapshot
|
||||
// tracked as (AccelInitialised && DeviceAvailable && SessionLive).
|
||||
// Surface it under both legacy field names for consumers that
|
||||
// already pattern-match on AccelInitialised / DeviceAvailable.
|
||||
AccelInitialised: bp.GPU,
|
||||
DeviceAvailable: bp.GPU,
|
||||
BatchThresholdN: BatchThreshold,
|
||||
ConcurrentThresholdN: concurrentBatchThreshold,
|
||||
}
|
||||
if !snap.AccelInitialised {
|
||||
p.Tier = TierGoroutineParallelCPU
|
||||
return p
|
||||
}
|
||||
if backend.Resolve(snap.DeviceAvailable, false) != backend.GPU {
|
||||
p.Tier = TierGoroutineParallelCPU
|
||||
return p
|
||||
}
|
||||
if !snap.SessionLive {
|
||||
if bp.Resolved != backend.GPU {
|
||||
p.Tier = TierGoroutineParallelCPU
|
||||
return p
|
||||
}
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ import (
|
||||
// Other modes fall through.
|
||||
|
||||
func batchEncapsulateGPU(pubs []*PublicKey, cts [][]byte, sss [][]byte) (bool, error) {
|
||||
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
|
||||
if !backend.IsGPU() {
|
||||
return false, nil
|
||||
}
|
||||
sess := gpuhost.Session()
|
||||
@@ -74,7 +74,7 @@ func batchEncapsulateGPU(pubs []*PublicKey, cts [][]byte, sss [][]byte) (bool, e
|
||||
}
|
||||
|
||||
func batchDecapsulateGPU(sks []*PrivateKey, cts [][]byte, sss [][]byte) (bool, error) {
|
||||
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
|
||||
if !backend.IsGPU() {
|
||||
return false, nil
|
||||
}
|
||||
sess := gpuhost.Session()
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func batchVerifyGPU(pubkeys, msgHashes, sigs [][]byte, out []bool) (bool, error) {
|
||||
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
|
||||
if !backend.IsGPU() {
|
||||
return false, nil
|
||||
}
|
||||
sess := gpuhost.Session()
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func batchGPU(inputs [][]byte, out [][Size]byte) (bool, error) {
|
||||
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
|
||||
if !backend.IsGPU() {
|
||||
return false, nil
|
||||
}
|
||||
sess := gpuhost.Session()
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@
|
||||
// Reaches a Metal/CUDA plugin when one is registered, otherwise falls
|
||||
// through to (2). The interface is the same wire as ML-DSA dispatch:
|
||||
//
|
||||
// backend.Resolve(gpuhost.Available(), false) → backend.GPU
|
||||
// backend.IsGPU()
|
||||
// ⇒ accel.LatticeOps.SLHDSA{Sign,Verify}Batch on the shared session
|
||||
// ⇒ luxcpp/lux-accel C API lux_slhdsa_{sign,verify}_batch
|
||||
// ⇒ when a backend plugin registers a strong override:
|
||||
@@ -175,7 +175,7 @@ func VerifyBatchGPU(pubs []*PublicKey, msgs, sigs [][]byte, out []bool) (bool, e
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
|
||||
if !backend.IsGPU() {
|
||||
return false, nil
|
||||
}
|
||||
sess := gpuhost.Session()
|
||||
@@ -368,7 +368,7 @@ func SignBatchGPU(privs []*PrivateKey, msgs, sigs [][]byte) (bool, error) {
|
||||
if len(msgs) != len(privs) || len(sigs) != len(privs) {
|
||||
return false, nil
|
||||
}
|
||||
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
|
||||
if !backend.IsGPU() {
|
||||
return false, nil
|
||||
}
|
||||
sess := gpuhost.Session()
|
||||
|
||||
@@ -33,7 +33,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/backend"
|
||||
"github.com/luxfi/crypto/internal/gpuhost"
|
||||
|
||||
|
||||
)
|
||||
|
||||
// genSignBatch192f produces n SLH-DSA-192f keypairs ready for SignBatch.
|
||||
@@ -97,7 +98,7 @@ func TestSLHDSASignBatch_GPU_Path(t *testing.T) {
|
||||
t.Fatalf("SignBatchGPU: %v", err)
|
||||
}
|
||||
if dispatched {
|
||||
t.Logf("GPU dispatch active (gpuhost.Available=%v)", gpuhost.Available())
|
||||
t.Logf("GPU dispatch active (gpu=%v)", backend.GPUAvailable())
|
||||
// GPU-produced signatures MUST verify under the CPU oracle.
|
||||
// This is the round-trip that catches GPU-output corruption.
|
||||
for i := 0; i < n; i++ {
|
||||
|
||||
+5
-4
@@ -9,7 +9,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/backend"
|
||||
"github.com/luxfi/crypto/internal/gpuhost"
|
||||
|
||||
|
||||
)
|
||||
|
||||
// genBatch produces n SLH-DSA-192f keypairs + signs distinct messages with
|
||||
@@ -70,8 +71,8 @@ func TestSLHDSABatchEquivalence_CPU_GPU(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Force GPU resolution to attempt the GPU path. backend.Resolve
|
||||
// gates on gpuhost.Available() so this only actually goes to GPU
|
||||
// Force GPU resolution to attempt the GPU path. backend.IsGPU()
|
||||
// gates on backend.GPUAvailable() so this only actually goes to GPU
|
||||
// when a backend plugin is loaded; otherwise it falls through and
|
||||
// VerifyBatchGPU returns (false, nil).
|
||||
prevBackend := backend.Default()
|
||||
@@ -85,7 +86,7 @@ func TestSLHDSABatchEquivalence_CPU_GPU(t *testing.T) {
|
||||
}
|
||||
|
||||
if dispatched {
|
||||
t.Logf("GPU dispatch executed (backend=%s, gpuhost.Available=%v)", backend.Default(), gpuhost.Available())
|
||||
t.Logf("GPU dispatch executed (backend=%s, gpu=%v)", backend.Default(), backend.GPUAvailable())
|
||||
for i := range cpu {
|
||||
if cpu[i] != gpu[i] {
|
||||
t.Fatalf("CPU/GPU mismatch at[%d]: cpu=%v gpu=%v", i, cpu[i], gpu[i])
|
||||
|
||||
+13
-15
@@ -7,7 +7,6 @@ import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/luxfi/crypto/backend"
|
||||
"github.com/luxfi/crypto/internal/gpuhost"
|
||||
)
|
||||
|
||||
// DispatchTier identifies which path the SLH-DSA batch dispatchers will
|
||||
@@ -115,27 +114,26 @@ type Provenance struct {
|
||||
// a stock keypair (no allocation cost on the caller side) and caches
|
||||
// the result for the lifetime of the process.
|
||||
func GetProvenance() Provenance {
|
||||
snap := gpuhost.Snapshot()
|
||||
bp := backend.Probe()
|
||||
|
||||
p := Provenance{
|
||||
AccelInitialised: snap.AccelInitialised,
|
||||
DeviceAvailable: snap.DeviceAvailable,
|
||||
// backend.Probe.GPU is true only when accel initialised AND a
|
||||
// session is live AND at least one device is visible — the same
|
||||
// three-way conjunction the prior gpuhost.Snapshot() tracked
|
||||
// individually. We surface the conjunction here as one bit per
|
||||
// historical field for backward compatibility with consumers
|
||||
// that already pattern-match on AccelInitialised/DeviceAvailable.
|
||||
AccelInitialised: bp.GPU,
|
||||
DeviceAvailable: bp.GPU,
|
||||
SignBatchThresholdN: SignBatchThreshold,
|
||||
ConcurrentSignThresholdN: concurrentSignThreshold,
|
||||
}
|
||||
|
||||
// Walk the dispatch tiers from the strongest evidence downward,
|
||||
// mirroring the order in SignBatch / VerifyBatch.
|
||||
if !snap.AccelInitialised {
|
||||
p.Tier = TierGoroutineParallelCPU
|
||||
return p
|
||||
}
|
||||
if backend.Resolve(snap.DeviceAvailable, false) != backend.GPU {
|
||||
// Vanilla profile or no device — CPU path is the only option.
|
||||
p.Tier = TierGoroutineParallelCPU
|
||||
return p
|
||||
}
|
||||
if !snap.SessionLive {
|
||||
// mirroring the order in SignBatch / VerifyBatch. Resolved() returns
|
||||
// the same answer regardless of whether the user explicitly set
|
||||
// CRYPTO_BACKEND or left it at Auto.
|
||||
if bp.Resolved != backend.GPU {
|
||||
p.Tier = TierGoroutineParallelCPU
|
||||
return p
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user