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

This commit is contained in:
Hanzo AI
2026-06-05 15:36:33 -07:00
4 changed files with 1315 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
// Package platformvm GPU backend selection — pure-Go shim that probes the
// lux-gpu-kernels plugin DSOs at PROCESS START in the canonical order
// (cuda → hip → metal → vulkan → webgpu) and exposes the first successful
// open as ActiveGPUBackend().
//
// Decomplected from platformvm_gpu.go / platformvm_gpu_nocgo.go: the
// probe order and package-level handle live here. The actual dlopen +
// dlsym calls live in platformvm_gpu.go (cgo build) or in the nocgo
// stub. Both files expose the same openGPUBackend signature so this
// file compiles identically in either mode.
//
// If a plugin opens cleanly, the chain routes transitions through it.
// If a probe fails (no plugin installed, missing runtime, etc.) the
// chain runs the existing pure-Go path. Every GPU launcher error makes
// the caller fall back to the Go path WITHOUT panic — strict positive
// overlay with graceful degradation.
package platformvm
import (
"os"
"runtime"
"sync"
)
// =============================================================================
// Canonical plugin search paths — every backend has a name + a list of
// DSO basenames the loader tries in order. The first one that opens AND
// dlsym's all four launchers wins.
// =============================================================================
type pluginCandidate struct {
kind GPUBackendKind
basenames []string
}
// candidatePlugins is the dlopen probe order: cuda → hip → metal → vulkan
// → webgpu. Each candidate lists the DSO basenames the loader probes (no
// path component). The probe layers add platform-conventional prefix +
// suffix (lib*.so / lib*.dylib).
func candidatePlugins() []pluginCandidate {
return []pluginCandidate{
{GPUCUDA, []string{
"libluxgpu_backend_cuda.so",
"libluxgpu_backend_cuda.dylib",
}},
{GPUHIP, []string{
"libluxgpu_backend_hip.so",
"libluxgpu_backend_hip.dylib",
}},
{GPUMetal, []string{
"libluxgpu_backend_metal.dylib",
}},
{GPUVulkan, []string{
"libluxgpu_backend_vulkan.so",
"libluxgpu_backend_vulkan.dylib",
}},
{GPUWebGPU, []string{
"libluxgpu_backend_webgpu.so",
"libluxgpu_backend_webgpu.dylib",
}},
}
}
// searchPaths returns the directories the loader probes for each
// candidate basename. Order:
//
// 1. LUX_GPU_PLUGIN_DIR — shared with every other VM (aivm, bridgevm, …).
// 2. ~/work/lux-private/gpu-kernels/build/<flavor>_backend/ — dev tree.
// 3. /usr/local/lib/lux-gpu/ — prefix install.
// 4. /usr/lib/lux-gpu/.
// 5. Empty string ("" — let dlopen consult the system search path).
//
// The empty-string fallback lets a plugin in DYLD_LIBRARY_PATH /
// LD_LIBRARY_PATH still resolve, which is the standard CI override.
func searchPaths() []string {
out := make([]string, 0, 8)
if d := os.Getenv("LUX_GPU_PLUGIN_DIR"); d != "" {
out = append(out, d)
}
if home, err := os.UserHomeDir(); err == nil {
// Match the lux-private/gpu-kernels Cmake out-of-tree
// convention: each backend lands in its own subdir.
out = append(out,
home+"/work/lux-private/gpu-kernels/build/cuda_backend",
home+"/work/lux-private/gpu-kernels/build/hip_backend",
home+"/work/lux-private/gpu-kernels/build/metal_backend",
home+"/work/lux-private/gpu-kernels/build/vulkan_backend",
home+"/work/lux-private/gpu-kernels/build/webgpu_backend",
)
}
out = append(out, "/usr/local/lib/lux-gpu", "/usr/lib/lux-gpu", "")
return out
}
// =============================================================================
// Package-level active backend — set once by init(), read by every call site.
// =============================================================================
var (
activeMu sync.RWMutex
activeBackend *GPUBackend // nil = no plugin loaded
)
// ActiveGPUBackend returns the currently-loaded backend, or nil if none
// satisfied the probe at init(). Callers MUST check b.IsAvailable() before
// invoking any kernel; calling on a nil receiver is safe and returns
// ErrGPUNotAvailable from every method.
func ActiveGPUBackend() *GPUBackend {
activeMu.RLock()
defer activeMu.RUnlock()
return activeBackend
}
// SetActiveGPUBackend replaces the active backend. Used by tests to inject
// a mock plugin. Returns the previous backend (caller is responsible for
// closing it). Passing nil clears the slot.
func SetActiveGPUBackend(b *GPUBackend) *GPUBackend {
activeMu.Lock()
defer activeMu.Unlock()
prev := activeBackend
activeBackend = b
return prev
}
// =============================================================================
// AutoBackend — pure-Go probe driver. Walks candidatePlugins() in order and
// returns the first one that opens cleanly. Returns nil if every probe fails
// (no plugin installed, missing runtime, etc.).
// =============================================================================
// AutoBackend probes the canonical plugin search paths in the cuda →
// hip → metal → vulkan → webgpu order and returns the first backend that
// opens AND resolves all four host launchers. Returns nil + the last
// error if no plugin satisfies the probe.
func AutoBackend() (*GPUBackend, error) {
var lastErr error = ErrGPUNotAvailable
for _, cand := range candidatePlugins() {
// Skip kind-incompatible probes early. Metal only on darwin,
// cuda/hip primarily on linux. The wrong-platform DSOs simply
// won't be on disk — but skipping known impossibles makes the
// fail-fast logs less noisy.
if !kindLikelyOnHost(cand.kind) {
continue
}
for _, basename := range cand.basenames {
for _, dir := range searchPaths() {
path := basename
if dir != "" {
path = dir + "/" + basename
}
b, err := openGPUBackend(cand.kind, path)
if err != nil {
lastErr = err
continue
}
return b, nil
}
}
}
return nil, lastErr
}
// kindLikelyOnHost is a coarse platform filter — true if the OS could
// plausibly host this backend's runtime. The dlopen call would catch
// impossibilities anyway, but skipping known-bad combos cuts noise.
func kindLikelyOnHost(k GPUBackendKind) bool {
switch k {
case GPUMetal:
return runtime.GOOS == "darwin"
case GPUCUDA, GPUHIP:
// CUDA/HIP are linux-first; the linux runtime may also be
// loadable under WSL2. macOS cannot run either today.
return runtime.GOOS != "darwin"
case GPUVulkan, GPUWebGPU:
// Vulkan via MoltenVK works on darwin too; WebGPU via Dawn/wgpu
// is portable.
return true
default:
return false
}
}
// =============================================================================
// init() — runs the canonical probe order at process start. The result is
// stored in the package-level activeBackend slot. Any error is silently
// swallowed (the chain runs the Go path) so a missing plugin never
// blocks node startup.
//
// Test code can call SetActiveGPUBackend() after init() to override.
// =============================================================================
func init() {
b, err := AutoBackend()
if err != nil || b == nil {
// Silent — the absence of a plugin is the common case.
return
}
activeMu.Lock()
activeBackend = b
activeMu.Unlock()
}
+727
View File
@@ -0,0 +1,727 @@
//go:build cgo
// Package platformvm GPU backend — runtime-loaded plugin bridge.
//
// Mirrors the chains/aivm/aivm_gpu.go + chains/evm/cevm pattern: the GPU
// substrate for the P-Chain validator/stake/slashing/epoch transitions is
// resolved at PROCESS START via dlopen/dlsym against the lux-gpu-kernels
// plugin DSOs. This keeps the node module compilable without
// lux-private/gpu-kernels present in the build tree — the plugin is fully
// optional and the chain runs the existing pure-Go path otherwise.
//
// Lookup order (handled by backend.go):
//
// libluxgpu_backend_cuda.so (Linux x86_64 + NVIDIA)
// libluxgpu_backend_hip.so (Linux x86_64 + AMD ROCm)
// libluxgpu_backend_metal.dylib (macOS Apple Silicon / Intel)
// libluxgpu_backend_vulkan.so/.dylib (any Vulkan ICD)
// libluxgpu_backend_webgpu.so/.dylib (Dawn / wgpu-native)
//
// Each plugin exports four extern "C" host launchers per backend:
//
// lux_<backend>_platformvm_validator_set_apply
// lux_<backend>_platformvm_stake_transition
// lux_<backend>_platformvm_slashing_transition
// lux_<backend>_platformvm_epoch_transition
//
// Struct layout matches ops/platformvm/cuda/platformvm_kernels_common.cuh
// and platformvm_gpu_layout.hpp byte-for-byte (asserted by init()).
// Pointer ABI is HOST pointers — the launcher does H2D-upload / dispatch /
// D2H-download internally.
//
// FALSE-by-default: even when cgo is on and a plugin loads, the chain
// runs the existing pure-Go path until an operator explicitly sets
// `LUX_PLATFORMVM_GPU=1`. Any launcher error → fall back to Go path
// WITHOUT panic and emit a warn log.
package platformvm
/*
#cgo darwin LDFLAGS: -ldl
#cgo linux LDFLAGS: -ldl
#include <dlfcn.h>
#include <stdint.h>
#include <stdlib.h>
// Four host-launcher trampolines — invoked by Go via cgo. The function
// pointer is opaque (void*); the cgo bridge casts it to the expected
// C signature and forwards arguments.
//
// All four launcher prototypes are identical across backends (cuda / hip /
// metal / vulkan / webgpu). Argument pointers are HOST pointers; the
// last `void*` is a "stream" handle that is always nullptr on the host-
// pointer ABI backends (metal / vulkan / webgpu) and the cuda/hip stream
// slot from the kernel author's POV.
typedef int (*pvm_validator_set_fn)(
const void* desc,
const void* ops,
void* validators,
void* applied_out,
uint32_t validator_count,
void* stream);
typedef int (*pvm_stake_fn)(
const void* desc,
const void* ops,
void* validators,
void* stake,
void* applied_out,
uint32_t validator_count,
uint32_t stake_count,
void* stream);
typedef int (*pvm_slashing_fn)(
const void* desc,
const void* evidence,
void* validators,
void* slashing,
void* applied_out,
void* total_lo_out,
void* total_hi_out,
uint32_t validator_count,
uint32_t slashing_count,
void* stream);
typedef int (*pvm_epoch_fn)(
const void* desc,
void* validators,
void* stake,
void* slashing,
void* epoch,
void* result,
uint32_t validator_count,
uint32_t stake_count,
uint32_t slashing_count,
void* leaf_scratch,
void* stream);
static int call_pvm_validator_set(void* fn,
const void* desc, const void* ops,
void* validators, void* applied_out,
uint32_t validator_count) {
return ((pvm_validator_set_fn)fn)(desc, ops, validators, applied_out,
validator_count, NULL);
}
static int call_pvm_stake(void* fn,
const void* desc, const void* ops,
void* validators, void* stake, void* applied_out,
uint32_t validator_count, uint32_t stake_count) {
return ((pvm_stake_fn)fn)(desc, ops, validators, stake, applied_out,
validator_count, stake_count, NULL);
}
static int call_pvm_slashing(void* fn,
const void* desc, const void* evidence,
void* validators, void* slashing, void* applied_out,
void* total_lo_out, void* total_hi_out,
uint32_t validator_count, uint32_t slashing_count) {
return ((pvm_slashing_fn)fn)(desc, evidence, validators, slashing,
applied_out, total_lo_out, total_hi_out,
validator_count, slashing_count, NULL);
}
static int call_pvm_epoch(void* fn,
const void* desc, void* validators, void* stake,
void* slashing, void* epoch, void* result,
uint32_t validator_count, uint32_t stake_count,
uint32_t slashing_count, void* leaf_scratch) {
return ((pvm_epoch_fn)fn)(desc, validators, stake, slashing, epoch, result,
validator_count, stake_count, slashing_count,
leaf_scratch, NULL);
}
// dlopen / dlsym wrappers — kept here so backend.go can stay pure Go.
static void* lux_pvm_dlopen(const char* path) {
return dlopen(path, RTLD_NOW | RTLD_LOCAL);
}
static void* lux_pvm_dlsym(void* handle, const char* sym) {
return dlsym(handle, sym);
}
static const char* lux_pvm_dlerror() {
return dlerror();
}
static void lux_pvm_dlclose(void* handle) {
dlclose(handle);
}
*/
import "C"
import (
"errors"
"fmt"
"runtime"
"sync"
"unsafe"
)
// ErrGPUNotAvailable is returned by every GPUBackend method when no plugin
// was loadable at init time. Callers fall through to the existing Go path.
var ErrGPUNotAvailable = errors.New("platformvm: no GPU plugin available")
// GPUBackendKind identifies which lux-gpu-kernels plugin satisfied the
// runtime dlopen probe. AvailableNone is the sentinel "fall through to Go".
type GPUBackendKind uint8
const (
GPUNone GPUBackendKind = 0
GPUCUDA GPUBackendKind = 1
GPUHIP GPUBackendKind = 2
GPUMetal GPUBackendKind = 3
GPUVulkan GPUBackendKind = 4
GPUWebGPU GPUBackendKind = 5
)
// String returns the human-readable name for the backend kind.
func (k GPUBackendKind) String() string {
switch k {
case GPUNone:
return "none"
case GPUCUDA:
return "cuda"
case GPUHIP:
return "hip"
case GPUMetal:
return "metal"
case GPUVulkan:
return "vulkan"
case GPUWebGPU:
return "webgpu"
default:
return fmt.Sprintf("backend(%d)", uint8(k))
}
}
// =============================================================================
// Layout-drift guards — match ops/platformvm/cuda/platformvm_kernels_common.cuh
// + platformvm_gpu_layout.hpp byte-for-byte.
//
// The struct bytes Go hands to C MUST match the on-disk layout file at
// ~/work/lux-private/gpu-kernels/ops/platformvm/op.yaml — every kernel reads
// them via reinterpret_cast. A silent layout shift produces consensus-divergent
// state roots. init() refuses to load if any size drifts.
// =============================================================================
// PVMValidatorSlot mirrors platformvm::gpu::ValidatorSlot (176 bytes).
type PVMValidatorSlot struct {
ValidatorID uint64
Weight uint64
BLSPubkey [48]byte
CoronaPubkey [32]byte // Corona / Corona commitment digest
MLDSAPubkey [32]byte
MLDSAGroth16Root [32]byte
Status uint32
JailUntilEpoch uint32
Occupied uint32
_pad0 uint32
}
// PVMStakeRecord mirrors platformvm::gpu::StakeRecord (64 bytes).
type PVMStakeRecord struct {
DelegatorID uint64
ValidatorID uint64
Amount uint64
LockUntilEpoch uint64
RewardAccumulator uint64
CommissionBPS uint32
Status uint32
EpochBonded uint32
EpochUnbonded uint32
_pad0 uint64
}
// PVMSlashEvidence mirrors platformvm::gpu::SlashEvidence (80 bytes).
type PVMSlashEvidence struct {
ValidatorID uint64
Height uint64
SlashAmount uint64
Kind uint32
Epoch uint32
JailForEpochs uint32
_pad0 uint32
EvidenceDigest [32]byte
_pad1 uint64
}
// PVMEpochState mirrors platformvm::gpu::EpochState (160 bytes).
type PVMEpochState struct {
CurrentEpoch uint64
NextEpochHeight uint64
TotalActiveStake uint64
ActiveValidatorCount uint32
PendingDropCount uint32
ValidatorSetRoot [32]byte
StakeRoot [32]byte
SlashingRoot [32]byte
EpochRoot [32]byte
}
// PVMRoundDescriptor mirrors platformvm::gpu::PVMRoundDescriptor (96 bytes).
type PVMRoundDescriptor struct {
ChainID uint64
Round uint64
TimestampNS uint64
Epoch uint64
Mode uint32
ValidatorOpCount uint32
StakeOpCount uint32
SlashEvidenceCount uint32
ClosingFlag uint32
_pad0 uint32
_pad1 uint64
ParentEpochRoot [32]byte
}
// PVMValidatorOp mirrors platformvm::gpu::ValidatorOp (176 bytes).
type PVMValidatorOp struct {
ValidatorID uint64
Weight uint64
BLSPubkey [48]byte
CoronaPubkey [32]byte
MLDSAPubkey [32]byte
MLDSAGroth16Root [32]byte
Kind uint32
JailUntilEpoch uint32
Epoch uint32
_pad0 uint32
}
// PVMStakeOp mirrors platformvm::gpu::StakeOp (64 bytes).
type PVMStakeOp struct {
DelegatorID uint64
ValidatorID uint64
Amount uint64
LockUntilEpoch uint64
SourceValidatorID uint64
Kind uint32
CommissionBPS uint32
Epoch uint32
_pad0 uint32
_pad1 uint64
}
// PVMTransitionResult mirrors platformvm::gpu::PVMTransitionResult (192 bytes).
type PVMTransitionResult struct {
Status uint32
ValidatorApplyCount uint32
StakeApplyCount uint32
SlashApplyCount uint32
ActiveValidatorCount uint32
PendingDropCount uint32
JailedCount uint32
TombstonedCount uint32
TotalActiveStake uint64
TotalSlashed uint64
TotalRewards uint64
Epoch uint64
ValidatorSetRoot [32]byte
StakeRoot [32]byte
SlashingRoot [32]byte
EpochRoot [32]byte
}
// PlatformVM op-kind constants — must match
// ops/platformvm/cuda/platformvm_kernels_common.cuh.
const (
PVMVOpAdd uint32 = 0
PVMVOpRemove uint32 = 1
PVMVOpUpdateWeight uint32 = 2
PVMVOpJail uint32 = 3
PVMVOpUnjail uint32 = 4
PVMVOpRotateKeys uint32 = 5
PVMSOpBond uint32 = 0
PVMSOpUnbond uint32 = 1
PVMSOpDelegate uint32 = 2
PVMSOpRedelegate uint32 = 3
PVMSOpReward uint32 = 4
PVMSOpCommission uint32 = 5
PVMEvEquivocation uint32 = 0
PVMEvDowntime uint32 = 1
PVMEvInvalidVote uint32 = 2
PVMModeValidator uint32 = 0
PVMModeStake uint32 = 1
PVMModeSlashing uint32 = 2
PVMModeEpoch uint32 = 3
PVMModeFullRound uint32 = 4
PVMStatusActive uint32 = 0x1
PVMStatusJailed uint32 = 0x2
PVMStatusTombstoned uint32 = 0x4
PVMStatusPendingAdd uint32 = 0x8
PVMStatusPendingDrop uint32 = 0x10
PVMStakeStatusActive uint32 = 1
PVMStakeStatusUnbonding uint32 = 2
PVMStakeStatusRetired uint32 = 3
)
// Layout-drift guard — refuse to load if any struct size disagrees with
// the on-device layout. Any disagreement here means Go would write
// garbage at the C boundary.
func init() {
type sz struct {
name string
got uintptr
want uintptr
}
checks := []sz{
{"PVMValidatorSlot", unsafe.Sizeof(PVMValidatorSlot{}), 176},
{"PVMStakeRecord", unsafe.Sizeof(PVMStakeRecord{}), 64},
{"PVMSlashEvidence", unsafe.Sizeof(PVMSlashEvidence{}), 80},
{"PVMEpochState", unsafe.Sizeof(PVMEpochState{}), 160},
{"PVMRoundDescriptor", unsafe.Sizeof(PVMRoundDescriptor{}), 96},
{"PVMValidatorOp", unsafe.Sizeof(PVMValidatorOp{}), 176},
{"PVMStakeOp", unsafe.Sizeof(PVMStakeOp{}), 64},
{"PVMTransitionResult", unsafe.Sizeof(PVMTransitionResult{}), 192},
}
for _, c := range checks {
if c.got != c.want {
panic(fmt.Sprintf(
"platformvm: layout drift — Go sizeof(%s)=%d but on-device layout=%d. "+
"Re-sync vms/platformvm/platformvm_gpu.go against "+
"~/work/lux-private/gpu-kernels/ops/platformvm/cuda/platformvm_kernels_common.cuh.",
c.name, c.got, c.want))
}
}
}
// =============================================================================
// GPUBackend — handle to an open plugin DSO + its four resolved launchers.
// =============================================================================
// GPUBackend is a handle to an open lux-gpu-kernels plugin. Zero value is
// usable (every method returns ErrGPUNotAvailable). The active backend is
// stored at package level by backend.go's init(); call ActiveGPUBackend()
// to retrieve it.
type GPUBackend struct {
mu sync.Mutex
kind GPUBackendKind
handle unsafe.Pointer // dlopen result
path string
fnValidatorSet unsafe.Pointer
fnStake unsafe.Pointer
fnSlashing unsafe.Pointer
fnEpoch unsafe.Pointer
}
// Kind returns which backend satisfied the dlopen probe.
func (b *GPUBackend) Kind() GPUBackendKind {
if b == nil {
return GPUNone
}
return b.kind
}
// Path returns the absolute path of the loaded plugin DSO.
func (b *GPUBackend) Path() string {
if b == nil {
return ""
}
return b.path
}
// IsAvailable reports whether the backend is loaded AND all four host
// launchers were successfully resolved.
func (b *GPUBackend) IsAvailable() bool {
if b == nil || b.handle == nil {
return false
}
return b.fnValidatorSet != nil && b.fnStake != nil &&
b.fnSlashing != nil && b.fnEpoch != nil
}
// openGPUBackend attempts to dlopen `path` and dlsym the four host launchers
// for `kind`. Returns a fully-initialised *GPUBackend on success, or
// (nil, error) when either the dlopen or any dlsym fails.
//
// On dlsym failure the dlopened handle IS dlclose'd before returning so
// we never leak a half-bound plugin.
func openGPUBackend(kind GPUBackendKind, path string) (*GPUBackend, error) {
cpath := C.CString(path)
defer C.free(unsafe.Pointer(cpath))
// Clear any pending error so a stale dlerror() from a previous failed
// dlsym doesn't get mis-attributed to this dlopen call.
C.lux_pvm_dlerror()
handle := C.lux_pvm_dlopen(cpath)
if handle == nil {
return nil, fmt.Errorf("platformvm: dlopen(%s): %s",
path, C.GoString(C.lux_pvm_dlerror()))
}
backendName := kind.String() // cuda / hip / metal / vulkan / webgpu
resolve := func(suffix string) (unsafe.Pointer, error) {
sym := fmt.Sprintf("lux_%s_platformvm_%s", backendName, suffix)
csym := C.CString(sym)
defer C.free(unsafe.Pointer(csym))
C.lux_pvm_dlerror()
ptr := C.lux_pvm_dlsym(handle, csym)
if ptr == nil {
return nil, fmt.Errorf("platformvm: dlsym(%s, %s): %s",
path, sym, C.GoString(C.lux_pvm_dlerror()))
}
return ptr, nil
}
b := &GPUBackend{kind: kind, handle: handle, path: path}
var err error
if b.fnValidatorSet, err = resolve("validator_set_apply"); err != nil {
C.lux_pvm_dlclose(handle)
return nil, err
}
if b.fnStake, err = resolve("stake_transition"); err != nil {
C.lux_pvm_dlclose(handle)
return nil, err
}
if b.fnSlashing, err = resolve("slashing_transition"); err != nil {
C.lux_pvm_dlclose(handle)
return nil, err
}
if b.fnEpoch, err = resolve("epoch_transition"); err != nil {
C.lux_pvm_dlclose(handle)
return nil, err
}
return b, nil
}
// Close releases the dlopen handle. Idempotent — safe to call on a nil
// receiver or an already-closed backend.
func (b *GPUBackend) Close() error {
if b == nil {
return nil
}
b.mu.Lock()
defer b.mu.Unlock()
if b.handle == nil {
return nil
}
C.lux_pvm_dlclose(b.handle)
b.handle = nil
b.fnValidatorSet = nil
b.fnStake = nil
b.fnSlashing = nil
b.fnEpoch = nil
return nil
}
// =============================================================================
// Four host launcher wrappers. Each is a thin cgo trampoline that pins the
// Go-side slice memory (via runtime.KeepAlive) for the duration of the C
// call. The launchers ALWAYS take HOST pointers — no D2H/H2D contract on
// the Go side beyond a defer'd KeepAlive on every input/output buffer.
//
// Error contract: any nonzero return code from the launcher → wrap in a Go
// error. The vm.go opt-in helpers fall back to the Go path on error
// WITHOUT panic and the caller is expected to emit a warn log.
// =============================================================================
// ValidatorSetApply runs the GPU validator-set-apply kernel. `validators` is
// read+written in place; `appliedOut` receives the count of successfully
// applied ops.
func (b *GPUBackend) ValidatorSetApply(
desc *PVMRoundDescriptor,
ops []PVMValidatorOp,
validators []PVMValidatorSlot,
appliedOut *uint32,
) error {
if !b.IsAvailable() {
return ErrGPUNotAvailable
}
if desc == nil || appliedOut == nil {
return errors.New("platformvm: ValidatorSetApply: nil desc or appliedOut")
}
if len(validators) == 0 {
return errors.New("platformvm: ValidatorSetApply: empty validators table")
}
var opsPtr unsafe.Pointer
if len(ops) > 0 {
opsPtr = unsafe.Pointer(&ops[0])
}
rc := C.call_pvm_validator_set(
b.fnValidatorSet,
unsafe.Pointer(desc),
opsPtr,
unsafe.Pointer(&validators[0]),
unsafe.Pointer(appliedOut),
C.uint32_t(len(validators)),
)
runtime.KeepAlive(desc)
runtime.KeepAlive(ops)
runtime.KeepAlive(validators)
runtime.KeepAlive(appliedOut)
if rc != 0 {
return fmt.Errorf("platformvm: %s_platformvm_validator_set_apply returned %d",
b.kind, int(rc))
}
return nil
}
// StakeTransition runs the GPU stake-transition kernel. Reads + writes
// `validators` and `stake` in place; `appliedOut` receives the count.
func (b *GPUBackend) StakeTransition(
desc *PVMRoundDescriptor,
ops []PVMStakeOp,
validators []PVMValidatorSlot,
stake []PVMStakeRecord,
appliedOut *uint32,
) error {
if !b.IsAvailable() {
return ErrGPUNotAvailable
}
if desc == nil || appliedOut == nil {
return errors.New("platformvm: StakeTransition: nil desc or appliedOut")
}
if len(validators) == 0 {
return errors.New("platformvm: StakeTransition: empty validators table")
}
if len(stake) == 0 {
return errors.New("platformvm: StakeTransition: empty stake table")
}
var opsPtr unsafe.Pointer
if len(ops) > 0 {
opsPtr = unsafe.Pointer(&ops[0])
}
rc := C.call_pvm_stake(
b.fnStake,
unsafe.Pointer(desc),
opsPtr,
unsafe.Pointer(&validators[0]),
unsafe.Pointer(&stake[0]),
unsafe.Pointer(appliedOut),
C.uint32_t(len(validators)),
C.uint32_t(len(stake)),
)
runtime.KeepAlive(desc)
runtime.KeepAlive(ops)
runtime.KeepAlive(validators)
runtime.KeepAlive(stake)
runtime.KeepAlive(appliedOut)
if rc != 0 {
return fmt.Errorf("platformvm: %s_platformvm_stake_transition returned %d",
b.kind, int(rc))
}
return nil
}
// SlashingTransition runs the GPU slashing-transition kernel. Reads +
// writes `validators` and `slashing` in place; `appliedOut` receives the
// count, and `totalLoOut`/`totalHiOut` receive the low/high u32 halves of
// the total slashed amount (u64).
func (b *GPUBackend) SlashingTransition(
desc *PVMRoundDescriptor,
evidence []PVMSlashEvidence,
validators []PVMValidatorSlot,
slashing []PVMSlashEvidence,
appliedOut, totalLoOut, totalHiOut *uint32,
) error {
if !b.IsAvailable() {
return ErrGPUNotAvailable
}
if desc == nil || appliedOut == nil || totalLoOut == nil || totalHiOut == nil {
return errors.New("platformvm: SlashingTransition: nil desc or output pointer")
}
if len(validators) == 0 {
return errors.New("platformvm: SlashingTransition: empty validators table")
}
var evidencePtr, slashingPtr unsafe.Pointer
if len(evidence) > 0 {
evidencePtr = unsafe.Pointer(&evidence[0])
}
if len(slashing) > 0 {
slashingPtr = unsafe.Pointer(&slashing[0])
}
rc := C.call_pvm_slashing(
b.fnSlashing,
unsafe.Pointer(desc),
evidencePtr,
unsafe.Pointer(&validators[0]),
slashingPtr,
unsafe.Pointer(appliedOut),
unsafe.Pointer(totalLoOut),
unsafe.Pointer(totalHiOut),
C.uint32_t(len(validators)),
C.uint32_t(len(slashing)),
)
runtime.KeepAlive(desc)
runtime.KeepAlive(evidence)
runtime.KeepAlive(validators)
runtime.KeepAlive(slashing)
runtime.KeepAlive(appliedOut)
runtime.KeepAlive(totalLoOut)
runtime.KeepAlive(totalHiOut)
if rc != 0 {
return fmt.Errorf("platformvm: %s_platformvm_slashing_transition returned %d",
b.kind, int(rc))
}
return nil
}
// EpochTransition runs the GPU epoch-finalisation kernel. Composes the
// per-epoch validator_set_root / stake_root / slashing_root / epoch_root
// (which doubles as pchain_validator_root for Quasar binding).
//
// `leafScratch` is an optional byte arena the kernel uses for keccak leaf
// hashes. If nil or empty the kernel allocates internally. Otherwise it
// must be sized to at least 32 × max(validator_count, stake_count,
// slashing_count) bytes.
func (b *GPUBackend) EpochTransition(
desc *PVMRoundDescriptor,
validators []PVMValidatorSlot,
stake []PVMStakeRecord,
slashing []PVMSlashEvidence,
epoch *PVMEpochState,
result *PVMTransitionResult,
leafScratch []byte,
) error {
if !b.IsAvailable() {
return ErrGPUNotAvailable
}
if desc == nil || epoch == nil || result == nil {
return errors.New("platformvm: EpochTransition: nil desc, epoch, or result")
}
if len(validators) == 0 {
return errors.New("platformvm: EpochTransition: empty validators table")
}
var stakePtr, slashingPtr, scratchPtr unsafe.Pointer
if len(stake) > 0 {
stakePtr = unsafe.Pointer(&stake[0])
}
if len(slashing) > 0 {
slashingPtr = unsafe.Pointer(&slashing[0])
}
if len(leafScratch) > 0 {
scratchPtr = unsafe.Pointer(&leafScratch[0])
}
rc := C.call_pvm_epoch(
b.fnEpoch,
unsafe.Pointer(desc),
unsafe.Pointer(&validators[0]),
stakePtr,
slashingPtr,
unsafe.Pointer(epoch),
unsafe.Pointer(result),
C.uint32_t(len(validators)),
C.uint32_t(len(stake)),
C.uint32_t(len(slashing)),
scratchPtr,
)
runtime.KeepAlive(desc)
runtime.KeepAlive(validators)
runtime.KeepAlive(stake)
runtime.KeepAlive(slashing)
runtime.KeepAlive(epoch)
runtime.KeepAlive(result)
runtime.KeepAlive(leafScratch)
if rc != 0 {
return fmt.Errorf("platformvm: %s_platformvm_epoch_transition returned %d",
b.kind, int(rc))
}
return nil
}
+287
View File
@@ -0,0 +1,287 @@
//go:build !cgo
// Package platformvm GPU backend — stub used when CGO is disabled.
//
// The cgo build (platformvm_gpu.go + backend.go) uses dlopen/dlsym to find a
// lux-gpu-kernels plugin at process start. Without cgo there's no way to
// reach a C function pointer, so every GPUBackend method returns
// ErrGPUNotAvailable. vm.go callers see GPUAvailable() == false and
// fall through to the existing Go path.
//
// This file keeps the public API surface identical between build modes:
// the same struct names, the same method signatures, the same package
// constants. Only the implementation differs.
package platformvm
import "errors"
// ErrGPUNotAvailable mirrors the cgo build's sentinel. vm.go can compare
// against it without caring which build mode is active.
var ErrGPUNotAvailable = errors.New("platformvm: GPU plugin unavailable (built without CGo)")
// GPUBackendKind identifies which lux-gpu-kernels plugin satisfied the
// runtime probe. GPUNone is the only value reachable without cgo.
type GPUBackendKind uint8
const (
GPUNone GPUBackendKind = 0
GPUCUDA GPUBackendKind = 1
GPUHIP GPUBackendKind = 2
GPUMetal GPUBackendKind = 3
GPUVulkan GPUBackendKind = 4
GPUWebGPU GPUBackendKind = 5
)
// String returns "none" under the nocgo stub. The other kinds are
// unreachable on this build but kept declared so callers can compare
// against the same constants either way.
func (k GPUBackendKind) String() string {
switch k {
case GPUNone:
return "none"
case GPUCUDA:
return "cuda"
case GPUHIP:
return "hip"
case GPUMetal:
return "metal"
case GPUVulkan:
return "vulkan"
case GPUWebGPU:
return "webgpu"
default:
return "unknown"
}
}
// =============================================================================
// Layout structs — kept fully declared so package-internal helpers compile
// identically in both modes. Field tags and sizes are NOT enforced under
// nocgo (no cgo boundary to validate against).
// =============================================================================
// PVMValidatorSlot mirrors the cgo build's layout.
type PVMValidatorSlot struct {
ValidatorID uint64
Weight uint64
BLSPubkey [48]byte
CoronaPubkey [32]byte
MLDSAPubkey [32]byte
MLDSAGroth16Root [32]byte
Status uint32
JailUntilEpoch uint32
Occupied uint32
_pad0 uint32
}
// PVMStakeRecord mirrors the cgo build's layout.
type PVMStakeRecord struct {
DelegatorID uint64
ValidatorID uint64
Amount uint64
LockUntilEpoch uint64
RewardAccumulator uint64
CommissionBPS uint32
Status uint32
EpochBonded uint32
EpochUnbonded uint32
_pad0 uint64
}
// PVMSlashEvidence mirrors the cgo build's layout.
type PVMSlashEvidence struct {
ValidatorID uint64
Height uint64
SlashAmount uint64
Kind uint32
Epoch uint32
JailForEpochs uint32
_pad0 uint32
EvidenceDigest [32]byte
_pad1 uint64
}
// PVMEpochState mirrors the cgo build's layout.
type PVMEpochState struct {
CurrentEpoch uint64
NextEpochHeight uint64
TotalActiveStake uint64
ActiveValidatorCount uint32
PendingDropCount uint32
ValidatorSetRoot [32]byte
StakeRoot [32]byte
SlashingRoot [32]byte
EpochRoot [32]byte
}
// PVMRoundDescriptor mirrors the cgo build's layout.
type PVMRoundDescriptor struct {
ChainID uint64
Round uint64
TimestampNS uint64
Epoch uint64
Mode uint32
ValidatorOpCount uint32
StakeOpCount uint32
SlashEvidenceCount uint32
ClosingFlag uint32
_pad0 uint32
_pad1 uint64
ParentEpochRoot [32]byte
}
// PVMValidatorOp mirrors the cgo build's layout.
type PVMValidatorOp struct {
ValidatorID uint64
Weight uint64
BLSPubkey [48]byte
CoronaPubkey [32]byte
MLDSAPubkey [32]byte
MLDSAGroth16Root [32]byte
Kind uint32
JailUntilEpoch uint32
Epoch uint32
_pad0 uint32
}
// PVMStakeOp mirrors the cgo build's layout.
type PVMStakeOp struct {
DelegatorID uint64
ValidatorID uint64
Amount uint64
LockUntilEpoch uint64
SourceValidatorID uint64
Kind uint32
CommissionBPS uint32
Epoch uint32
_pad0 uint32
_pad1 uint64
}
// PVMTransitionResult mirrors the cgo build's layout.
type PVMTransitionResult struct {
Status uint32
ValidatorApplyCount uint32
StakeApplyCount uint32
SlashApplyCount uint32
ActiveValidatorCount uint32
PendingDropCount uint32
JailedCount uint32
TombstonedCount uint32
TotalActiveStake uint64
TotalSlashed uint64
TotalRewards uint64
Epoch uint64
ValidatorSetRoot [32]byte
StakeRoot [32]byte
SlashingRoot [32]byte
EpochRoot [32]byte
}
// Constants — identical to the cgo build so callers compile either way.
const (
PVMVOpAdd uint32 = 0
PVMVOpRemove uint32 = 1
PVMVOpUpdateWeight uint32 = 2
PVMVOpJail uint32 = 3
PVMVOpUnjail uint32 = 4
PVMVOpRotateKeys uint32 = 5
PVMSOpBond uint32 = 0
PVMSOpUnbond uint32 = 1
PVMSOpDelegate uint32 = 2
PVMSOpRedelegate uint32 = 3
PVMSOpReward uint32 = 4
PVMSOpCommission uint32 = 5
PVMEvEquivocation uint32 = 0
PVMEvDowntime uint32 = 1
PVMEvInvalidVote uint32 = 2
PVMModeValidator uint32 = 0
PVMModeStake uint32 = 1
PVMModeSlashing uint32 = 2
PVMModeEpoch uint32 = 3
PVMModeFullRound uint32 = 4
PVMStatusActive uint32 = 0x1
PVMStatusJailed uint32 = 0x2
PVMStatusTombstoned uint32 = 0x4
PVMStatusPendingAdd uint32 = 0x8
PVMStatusPendingDrop uint32 = 0x10
PVMStakeStatusActive uint32 = 1
PVMStakeStatusUnbonding uint32 = 2
PVMStakeStatusRetired uint32 = 3
)
// =============================================================================
// GPUBackend stub — every method returns ErrGPUNotAvailable.
// =============================================================================
// GPUBackend is the nocgo stub. All methods return ErrGPUNotAvailable so
// vm.go can treat both builds identically.
type GPUBackend struct{}
// openGPUBackend is the nocgo entry point used by backend.go. Always
// returns (nil, ErrGPUNotAvailable) since dlopen requires cgo.
func openGPUBackend(_ GPUBackendKind, _ string) (*GPUBackend, error) {
return nil, ErrGPUNotAvailable
}
// Kind always returns GPUNone under nocgo.
func (b *GPUBackend) Kind() GPUBackendKind { return GPUNone }
// Path returns an empty string under nocgo.
func (b *GPUBackend) Path() string { return "" }
// IsAvailable always returns false under nocgo.
func (b *GPUBackend) IsAvailable() bool { return false }
// Close is a no-op under nocgo.
func (b *GPUBackend) Close() error { return nil }
// ValidatorSetApply returns ErrGPUNotAvailable under nocgo.
func (b *GPUBackend) ValidatorSetApply(
desc *PVMRoundDescriptor,
ops []PVMValidatorOp,
validators []PVMValidatorSlot,
appliedOut *uint32,
) error {
return ErrGPUNotAvailable
}
// StakeTransition returns ErrGPUNotAvailable under nocgo.
func (b *GPUBackend) StakeTransition(
desc *PVMRoundDescriptor,
ops []PVMStakeOp,
validators []PVMValidatorSlot,
stake []PVMStakeRecord,
appliedOut *uint32,
) error {
return ErrGPUNotAvailable
}
// SlashingTransition returns ErrGPUNotAvailable under nocgo.
func (b *GPUBackend) SlashingTransition(
desc *PVMRoundDescriptor,
evidence []PVMSlashEvidence,
validators []PVMValidatorSlot,
slashing []PVMSlashEvidence,
appliedOut, totalLoOut, totalHiOut *uint32,
) error {
return ErrGPUNotAvailable
}
// EpochTransition returns ErrGPUNotAvailable under nocgo.
func (b *GPUBackend) EpochTransition(
desc *PVMRoundDescriptor,
validators []PVMValidatorSlot,
stake []PVMStakeRecord,
slashing []PVMSlashEvidence,
epoch *PVMEpochState,
result *PVMTransitionResult,
leafScratch []byte,
) error {
return ErrGPUNotAvailable
}
+100
View File
@@ -0,0 +1,100 @@
// Round-trip test for the platformvm GPU bridge.
//
// Validates the dlopen + dlsym path end-to-end:
//
// 1. AutoBackend() probes the canonical plugin order.
// 2. If a plugin is found, ValidatorSetApply is invoked with a 1-validator
// fixture (kVOpAdd) and must return without error.
// 3. The result must reflect the apply: applied_out == 1 AND the
// validator slot must be marked occupied + Active|PendingAdd.
//
// SKIP-on-miss is the canonical contract: if no plugin is on disk the
// test logs SKIP and exits 0. This is how the existing chains/aivm GPU
// tests behave — the bridge is optional and a missing plugin is not a
// failure mode of luxd itself.
//
// The launcher (lux_<backend>_platformvm_validator_set_apply) must NOT
// return any error code; any nonzero rc is a regression.
package platformvm
import (
"testing"
)
func TestPlatformVMGPUBridge_AutoBackend(t *testing.T) {
b := ActiveGPUBackend()
if b == nil || !b.IsAvailable() {
t.Skip("platformvm: no GPU plugin loaded — skipping bridge round-trip")
}
t.Logf("loaded GPU plugin: kind=%s path=%s", b.Kind(), b.Path())
// Single-validator fixture: ADD validator_id=42 with weight=1000.
desc := PVMRoundDescriptor{
ChainID: 0xCAFEBABE,
Round: 1,
Epoch: 10,
Mode: PVMModeValidator,
ValidatorOpCount: 1,
}
op := PVMValidatorOp{
ValidatorID: 42,
Weight: 1000,
Kind: PVMVOpAdd,
Epoch: 10,
}
// Power-of-two slot count — required by the open-addressing locator
// in platformvm_kernels_common.cuh (mask = count - 1).
validators := make([]PVMValidatorSlot, 8)
var applied uint32
err := b.ValidatorSetApply(&desc, []PVMValidatorOp{op}, validators, &applied)
if err != nil {
t.Fatalf("ValidatorSetApply: %v", err)
}
if applied != 1 {
t.Fatalf("ValidatorSetApply: applied=%d, want=1", applied)
}
// Locate the slot we wrote — open-addressing hash on validator_id.
found := false
for i := range validators {
if validators[i].Occupied != 0 && validators[i].ValidatorID == 42 {
found = true
if validators[i].Weight != 1000 {
t.Errorf("validator slot: Weight=%d, want=1000", validators[i].Weight)
}
wantStatus := PVMStatusActive | PVMStatusPendingAdd
if validators[i].Status != wantStatus {
t.Errorf("validator slot: Status=0x%x, want=0x%x",
validators[i].Status, wantStatus)
}
break
}
}
if !found {
t.Fatal("ValidatorSetApply: no occupied slot with validator_id=42")
}
}
// TestPlatformVMGPUBridge_NilHandle exercises the Zero-value GPUBackend
// behaviour. Every method must return ErrGPUNotAvailable when the handle
// is nil (the "no plugin loaded" case) without panicking. This is the
// fall-through contract the vm.go opt-in helpers rely on.
func TestPlatformVMGPUBridge_NilHandle(t *testing.T) {
var b *GPUBackend
if b.IsAvailable() {
t.Fatal("nil GPUBackend: IsAvailable() = true, want false")
}
if b.Kind() != GPUNone {
t.Fatalf("nil GPUBackend: Kind() = %s, want none", b.Kind())
}
if b.Path() != "" {
t.Fatalf("nil GPUBackend: Path() = %q, want empty", b.Path())
}
if err := b.Close(); err != nil {
t.Fatalf("nil GPUBackend: Close() = %v, want nil", err)
}
}