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