Merge feature/thresholdvm-cgo-bridge: thresholdvm GPU plugin bridge (luxd)

This commit is contained in:
Hanzo AI
2026-06-05 15:36:33 -07:00
4 changed files with 266 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package thresholdvm — backend selection re-export.
//
// The canonical dlopen probe lives in github.com/luxfi/chains/thresholdvm.
// Its init() runs once at package load time and pins a process-wide
// GPUBackend handle (cuda → hip → metal → vulkan → webgpu probe order).
// This file is the node-side entry point for diagnostics and ops tooling.
//
// Note: the luxd VM manager registration for ThresholdVM (M-Chain) is
// NOT flipped here per the project memory note ("M-Chain code exists but
// NOT yet registered in running luxd"). This file provides the bridge
// surface only — flipping the manager hook-up is a separate ops PR.
package thresholdvm
// SelectGPUBackend returns the resolved GPU plugin (or nil) and a single
// human-readable diagnostic string. luxd startup logs use this to surface
// "thresholdvm-gpu backend=<name>" lines alongside the cevm backend
// selection, matching the cevm.go pattern at
// ~/work/lux/chains/evm/backend_cgo.go.
//
// Calling this multiple times is cheap — the underlying probe runs once
// at package init via sync.Once in chains/thresholdvm/backend.go.
func SelectGPUBackend() (*GPUBackend, string) {
g := GPUBackendInstance()
if g == nil || !g.IsAvailable() {
return nil, "thresholdvm-gpu: no plugin resolved (CPU-only)"
}
return g, "thresholdvm-gpu: backend=" + g.Kind.String() + " path=" + g.Path
}
+78
View File
@@ -0,0 +1,78 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build cgo
// Package thresholdvm re-exports the GPU bridge from
// github.com/luxfi/chains/thresholdvm so callers that import the legacy
// node path keep working without source changes.
//
// One and only one way to dlopen: the canonical bridge lives in
// chains/thresholdvm. This package is a typed alias layer — both the
// types and the GPUBackend singleton come straight from the upstream.
// There is exactly one dlopen handle, one symbol table, one init()
// probe across the entire luxd process. Consumers of either import path
// share the same plugin instance.
package thresholdvm
import "github.com/luxfi/chains/thresholdvm"
// =============================================================================
// Type re-exports — Go aliases preserve the public API so external callers
// (e.g. node/chains, node/main) can drop the import path without source
// changes. The GPU- prefix avoids collisions with the domain types
// already aliased above (Block, Client, Operation).
// =============================================================================
type (
GPUBackend = thresholdvm.GPUBackend
GPUBackendKind = thresholdvm.GPUBackendKind
GPUCeremony = thresholdvm.GPUCeremony
GPUKeyShare = thresholdvm.GPUKeyShare
GPUContribution = thresholdvm.GPUContribution
GPUMPCVMState = thresholdvm.GPUMPCVMState
GPUMPCVMRoundDescriptor = thresholdvm.GPUMPCVMRoundDescriptor
GPUCeremonyOp = thresholdvm.GPUCeremonyOp
GPUContributionOp = thresholdvm.GPUContributionOp
GPUMPCVMTransitionResult = thresholdvm.GPUMPCVMTransitionResult
)
// Backend constants re-exported. Matches the chains/thresholdvm dlopen
// probe order: cuda → hip → metal → vulkan → webgpu.
const (
GPUBackendNone = thresholdvm.GPUBackendNone
GPUBackendCUDA = thresholdvm.GPUBackendCUDA
GPUBackendHIP = thresholdvm.GPUBackendHIP
GPUBackendMetal = thresholdvm.GPUBackendMetal
GPUBackendVulkan = thresholdvm.GPUBackendVulkan
GPUBackendWebGPU = thresholdvm.GPUBackendWebGPU
)
// ErrGPUNotAvailable is the canonical "no plugin loaded" error. Callers
// `errors.Is(err, thresholdvm.ErrGPUNotAvailable)` to distinguish a
// fallback-to-CPU condition from a hard launcher failure.
var ErrGPUNotAvailable = thresholdvm.ErrGPUNotAvailable
// GPUBackendInstance returns the dlopen'd GPU plugin handle resolved at
// chains/thresholdvm package init. nil means no plugin was loaded
// (CPU-only mode). The handle is shared across the whole process — both
// the node and chains paths see the same instance.
//
// The name is GPUBackendInstance (not GPUBackend / Backend) because Go
// disallows a function named the same as a type alias in the same
// package, and `Backend` is already a domain term in the threshold
// state machine. Callers write:
//
// if g := thresholdvm.GPUBackendInstance(); g != nil && g.IsAvailable() {
// _, err := g.CeremonyApply(desc, ops, ceremonies)
// ...
// }
//
// This mirrors the cevm pattern `cevm.AvailableBackends()` /
// `cevm.LibraryABIVersion()` — discovery via package-scope function,
// not via a global variable.
func GPUBackendInstance() *GPUBackend {
return thresholdvm.Backend()
}
+49
View File
@@ -0,0 +1,49 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build !cgo
// nocgo re-export of the chains/thresholdvm GPU bridge. Under !cgo the
// upstream GPUBackend methods all return ErrGPUNotAvailable and
// Backend() returns nil; this layer transparently passes that through
// so callers see identical behaviour regardless of build mode.
//
// One and only one nocgo stub for the entire process: it lives in
// chains/thresholdvm. This package just re-exports the types and the
// sentinel error.
package thresholdvm
import "github.com/luxfi/chains/thresholdvm"
type (
GPUBackend = thresholdvm.GPUBackend
GPUBackendKind = thresholdvm.GPUBackendKind
GPUCeremony = thresholdvm.GPUCeremony
GPUKeyShare = thresholdvm.GPUKeyShare
GPUContribution = thresholdvm.GPUContribution
GPUMPCVMState = thresholdvm.GPUMPCVMState
GPUMPCVMRoundDescriptor = thresholdvm.GPUMPCVMRoundDescriptor
GPUCeremonyOp = thresholdvm.GPUCeremonyOp
GPUContributionOp = thresholdvm.GPUContributionOp
GPUMPCVMTransitionResult = thresholdvm.GPUMPCVMTransitionResult
)
const (
GPUBackendNone = thresholdvm.GPUBackendNone
GPUBackendCUDA = thresholdvm.GPUBackendCUDA
GPUBackendHIP = thresholdvm.GPUBackendHIP
GPUBackendMetal = thresholdvm.GPUBackendMetal
GPUBackendVulkan = thresholdvm.GPUBackendVulkan
GPUBackendWebGPU = thresholdvm.GPUBackendWebGPU
)
var ErrGPUNotAvailable = thresholdvm.ErrGPUNotAvailable
// GPUBackendInstance returns nil under !cgo — the upstream Backend()
// returns nil because no dlopen ever happens. Callers branch on the
// IsAvailable() check (or `g == nil`) and route to the CPU reference.
func GPUBackendInstance() *GPUBackend {
return thresholdvm.Backend()
}
+107
View File
@@ -0,0 +1,107 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package thresholdvm
import (
"errors"
"testing"
"unsafe"
)
// TestNodeGPULayoutSizes pins the re-exported wire structs to the same
// device-side __align__(16) values declared in
// ~/work/lux-private/gpu-kernels/ops/mpcvm/cuda/mpcvm_kernels_common.cuh.
//
// Even though the structs are type aliases to chains/thresholdvm, this
// test sits at the node import boundary — if upstream sizes drift the
// node-side surface MUST also fail loud rather than miscompile.
func TestNodeGPULayoutSizes(t *testing.T) {
cases := []struct {
name string
want uintptr
got uintptr
}{
{"GPUCeremony", 128, unsafe.Sizeof(GPUCeremony{})},
{"GPUKeyShare", 368, unsafe.Sizeof(GPUKeyShare{})},
{"GPUContribution", 432, unsafe.Sizeof(GPUContribution{})},
{"GPUMPCVMState", 160, unsafe.Sizeof(GPUMPCVMState{})},
{"GPUMPCVMRoundDescriptor", 96, unsafe.Sizeof(GPUMPCVMRoundDescriptor{})},
{"GPUCeremonyOp", 96, unsafe.Sizeof(GPUCeremonyOp{})},
{"GPUContributionOp", 416, unsafe.Sizeof(GPUContributionOp{})},
{"GPUMPCVMTransitionResult", 176, unsafe.Sizeof(GPUMPCVMTransitionResult{})},
}
for _, c := range cases {
if c.got != c.want {
t.Errorf("%s: sizeof=%d want=%d", c.name, c.got, c.want)
}
}
}
// TestNodeGPUBackendRoundTrip is the dlopen round-trip required by the
// task spec. Two acceptable outcomes:
//
// 1. Plugin resolved: GPUBackendInstance() != nil && IsAvailable(); a
// zero-fixture CeremonyApply against the best backend returns rc=0
// (no ops processed) with no error.
//
// 2. Plugin absent: GPUBackendInstance() == nil; nil-receiver methods
// return ErrGPUNotAvailable. SelectGPUBackend reports the CPU-only
// diagnostic string.
//
// Both outcomes count as passing — the bridge correctly surfaces GPU
// state through the public node API.
func TestNodeGPUBackendRoundTrip(t *testing.T) {
b, diag := SelectGPUBackend()
t.Logf("SelectGPUBackend: %s", diag)
if b == nil {
// Plugin-absent path. The nil receiver contract must hold —
// every method returns ErrGPUNotAvailable, no panic.
_, err := (*GPUBackend)(nil).CeremonyApply(nil, nil, nil)
if !errors.Is(err, ErrGPUNotAvailable) {
t.Fatalf("nil receiver CeremonyApply: want ErrGPUNotAvailable, got %v", err)
}
return
}
// Plugin-resolved path. Zero fixture: 1-slot ceremony arena, no ops.
// The kernel walks zero ops and returns applied=0 with rc=0. Any
// non-zero rc means a real launcher failure.
desc := &GPUMPCVMRoundDescriptor{
ChainID: 0xCAFEBABE,
Round: 1,
TimestampNs: 1700000000_000000000,
Epoch: 1,
}
ceremonies := make([]GPUCeremony, 1)
applied, err := b.CeremonyApply(desc, nil, ceremonies)
if err != nil {
t.Fatalf("CeremonyApply(zero): %v", err)
}
if applied != 0 {
t.Errorf("CeremonyApply(zero): applied=%d want=0", applied)
}
}
// TestNodeGPUStubContract verifies the nocgo-equivalent contract on a
// zero-value GPUBackend: IsAvailable()==false and every state-machine
// method returns ErrGPUNotAvailable. The same contract holds under cgo
// when no plugin is dlopened — making this test build-flavor-independent.
func TestNodeGPUStubContract(t *testing.T) {
var b GPUBackend
if b.IsAvailable() {
t.Fatal("zero GPUBackend.IsAvailable() must be false")
}
if _, err := b.CeremonyApply(nil, nil, nil); !errors.Is(err, ErrGPUNotAvailable) {
t.Errorf("CeremonyApply: want ErrGPUNotAvailable, got %v", err)
}
if _, _, _, err := b.KeyShareApply(nil, nil, nil, nil, 0); !errors.Is(err, ErrGPUNotAvailable) {
t.Errorf("KeyShareApply: want ErrGPUNotAvailable, got %v", err)
}
if _, err := b.ContributionApply(nil, nil, nil, nil, 0); !errors.Is(err, ErrGPUNotAvailable) {
t.Errorf("ContributionApply: want ErrGPUNotAvailable, got %v", err)
}
if _, err := b.MPCTransition(nil, nil, nil, nil, nil); !errors.Is(err, ErrGPUNotAvailable) {
t.Errorf("MPCTransition: want ErrGPUNotAvailable, got %v", err)
}
}