mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
Merge feature/thresholdvm-cgo-bridge: thresholdvm GPU plugin bridge (luxd)
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user