Files
crypto/backend/fallback.go
T
Hanzo AI ddff44bbee 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.
2026-05-24 14:16:55 -07:00

96 lines
3.1 KiB
Go

// 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{}
}
}