Files
chains/bridgevm/bridgevm_gpu.go
T
2026-09-01 02:38:03 -07:00

588 lines
19 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build cgo
package bridgevm
// CGo-backed BridgeVM GPU substrate. Loads the per-backend plugin
// libluxgpu_backend_<bk>.{so,dylib} at process start via dlopen(3) and
// resolves the five lux_<bk>_bridgevm_* host launcher symbols via dlsym(3).
//
// Direct dlopen — NOT pkg-config. The plugin lives in
// the GPU plugin install tree (a private repo); the public chains module
// must build without it present. Linking via pkg-config would couple the
// public build to a private dep; dlopen + dlsym lets us probe at runtime
// and degrade cleanly to ErrGPUNotAvailable when the plugin is absent.
//
// Search path for the plugin — every entry is somewhere an operator named:
//
// 1. $LUX_GPU_PLUGIN_DIR/ and $LUX_GPU_PLUGIN_DIR/<bk>/ (explicit override).
// 2. $LUXCPP_PREFIX/lib/lux-gpu/ (the install tree).
// 3. $LUXCPP_PREFIX/lib/ (legacy install tree).
//
// Neither variable set means no plugin, which is the configuration every box
// without a GPU is already in. There is deliberately no working-directory
// entry and no bare name: a plugin replaces bridgevm's state transitions
// wholesale, so a library the loader found would be deciding consensus state.
//
// Probe order — cuda → hip → metal → vulkan → webgpu. The first plugin that
// resolves all five symbols wins and sets activeBackend. Subsequent probes
// are skipped (one and only one plugin loaded per process).
//
// Thread safety: the init() probe runs once before main(). Post-init, the
// pluginHandle and the five fnPtr_* package vars are read-only — concurrent
// SignerApply / LiquidityApply / etc. calls from many goroutines are safe.
// Each launcher allocates its own scratch buffers; there are no shared
// mutables on the Go side. The C launchers themselves are documented as
// concurrent-safe (per backends/<bk>/src/bridgevm_launchers.{mm,cpp}).
/*
#cgo LDFLAGS: -ldl
#cgo CFLAGS: -I${SRCDIR}/../internal/luxgpu/include
#include <dlfcn.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// Trampolines — we cannot call function pointers from Go directly through cgo.
// The trampoline takes the function pointer (resolved via dlsym from Go) and
// the launcher args and calls the launcher. One trampoline per launcher
// signature.
typedef int (*bvm_signer_fn)(
const void* desc, const void* ops, void* signers, void* applied_out,
uint32_t signer_count, void* stream);
typedef int (*bvm_liquidity_fn)(
const void* desc, const void* ops, void* liquidity, void* applied_out,
void* total_fees_lo_out, void* total_fees_hi_out,
uint32_t liquidity_count, void* stream);
typedef int (*bvm_inbox_fn)(
const void* desc, const void* in_msgs, void* signers, void* daily,
void* inbox, void* applied_out, void* total_in_lo_out, void* total_in_hi_out,
uint32_t signer_count, uint32_t daily_count, uint32_t inbox_count,
void* stream);
typedef int (*bvm_outbox_fn)(
const void* desc, const void* reqs, void* daily, void* outbox, void* epoch,
void* applied_out, void* total_out_lo_out, void* total_out_hi_out,
uint32_t daily_count, uint32_t outbox_count, void* stream);
typedef int (*bvm_transition_fn)(
const void* desc, void* signers, void* liquidity, void* daily,
void* inbox, void* outbox, void* epoch, void* result,
uint32_t signer_count, uint32_t liquidity_count, uint32_t daily_count,
uint32_t inbox_count, uint32_t outbox_count,
void* stream);
static int call_signer(void* fn,
const void* desc, const void* ops, void* signers, void* applied_out,
uint32_t signer_count) {
return ((bvm_signer_fn)fn)(desc, ops, signers, applied_out,
signer_count, (void*)0);
}
static int call_liquidity(void* fn,
const void* desc, const void* ops, void* liquidity, void* applied_out,
void* total_fees_lo_out, void* total_fees_hi_out,
uint32_t liquidity_count) {
return ((bvm_liquidity_fn)fn)(desc, ops, liquidity, applied_out,
total_fees_lo_out, total_fees_hi_out,
liquidity_count, (void*)0);
}
static int call_inbox(void* fn,
const void* desc, const void* in_msgs, void* signers, void* daily,
void* inbox, void* applied_out, void* total_in_lo_out, void* total_in_hi_out,
uint32_t signer_count, uint32_t daily_count, uint32_t inbox_count) {
return ((bvm_inbox_fn)fn)(desc, in_msgs, signers, daily, inbox,
applied_out, total_in_lo_out, total_in_hi_out,
signer_count, daily_count, inbox_count,
(void*)0);
}
static int call_outbox(void* fn,
const void* desc, const void* reqs, void* daily, void* outbox, void* epoch,
void* applied_out, void* total_out_lo_out, void* total_out_hi_out,
uint32_t daily_count, uint32_t outbox_count) {
return ((bvm_outbox_fn)fn)(desc, reqs, daily, outbox, epoch,
applied_out, total_out_lo_out, total_out_hi_out,
daily_count, outbox_count, (void*)0);
}
static int call_transition(void* fn,
const void* desc, void* signers, void* liquidity, void* daily,
void* inbox, void* outbox, void* epoch, void* result,
uint32_t signer_count, uint32_t liquidity_count, uint32_t daily_count,
uint32_t inbox_count, uint32_t outbox_count) {
return ((bvm_transition_fn)fn)(desc, signers, liquidity, daily,
inbox, outbox, epoch, result,
signer_count, liquidity_count,
daily_count, inbox_count, outbox_count,
(void*)0);
}
#include "lux/gpu/backend_plugin.h"
// bvm_plugin_trusted resolves lux_gpu_backend_init in an already-dlopen'd
// library, runs it, and checks the two cookies the plugin header requires a
// loader to check: abi_version must be the one this build was compiled
// against, and vtbl_size must be our sizeof. Without this, dlsym'ing a
// launcher by name trusts a library nothing has identified — the name is the
// only thing being matched, and a name is not a contract.
//
// quantumvm already does exactly this; bridgevm, aivm and mpcvm dlsym'd
// blind. One way, and this is it.
//
// 0 — trusted
// 1 — no lux_gpu_backend_init (not one of our plugins)
// 2 — init returned false
// 3 — abi_version mismatch
// 4 — vtbl_size mismatch
static int bvm_plugin_trusted(void* h) {
void* sym = dlsym(h, LUX_GPU_BACKEND_INIT_SYMBOL);
if (sym == NULL) return 1;
lux_gpu_backend_desc desc;
memset(&desc, 0, sizeof desc);
if (!((lux_gpu_backend_init_fn)sym)(&desc)) return 2;
if (desc.abi_version != LUX_GPU_BACKEND_ABI_VERSION) return 3;
if (desc.vtbl_size != (uint32_t)sizeof(lux_gpu_backend_vtbl)) return 4;
return 0;
}
// dl helpers — wrap dlopen / dlsym / dlerror so the Go side doesn't have to
// deal with the C string lifetimes.
static void* dl_open(const char* path) {
return dlopen(path, RTLD_NOW | RTLD_GLOBAL);
}
static void* dl_sym(void* h, const char* name) {
return dlsym(h, name);
}
static const char* dl_err(void) {
const char* e = dlerror();
return e ? e : "";
}
*/
import "C"
import (
"fmt"
"os"
"path/filepath"
"runtime"
"sync"
"unsafe"
)
// pluginHandle is the dlopen()'d handle to libluxgpu_backend_<bk>.{so,dylib}.
// Held for the lifetime of the process (we never dlclose — the plugin's
// runtime state, GPU contexts, kernel caches, etc., would all be invalidated
// and any in-flight goroutine call would crash).
var pluginHandle unsafe.Pointer
// fnPtr_<launcher> are the dlsym'd entry points. Read-only after init(); a
// nil entry means the loaded plugin doesn't export that symbol (which fails
// the probe — we require all five).
var (
fnSignerApply unsafe.Pointer
fnLiquidityApply unsafe.Pointer
fnMessageInbox unsafe.Pointer
fnMessageOutbox unsafe.Pointer
fnBridgeTransit unsafe.Pointer
)
// initOnce guards the probe — even though init() runs at most once, this
// belt-and-braces guard makes the contract explicit and lets the test files
// re-trigger the probe deterministically (via a debug entry, not yet wired).
var initOnce sync.Once
func init() {
initOnce.Do(probePlugin)
}
// probePlugin walks the dlopen search path × backend probe order looking
// for the first libluxgpu_backend_<bk>.{so,dylib} that exposes all five
// lux_<bk>_bridgevm_* launchers. First match wins; activeBackend is set
// and pluginHandle + fnPtr_* are populated.
//
// On no match: activeBackend stays at BackendNone, every GPUBackend method
// returns ErrGPUNotAvailable. We do NOT log here — process start in the
// chains binary is noisy enough; vm.go (the consumer) can log AutoBackend()
// at the level it wants.
func probePlugin() {
for _, bk := range []Backend{
BackendCUDA, BackendHIP, BackendMetal, BackendVulkan, BackendWebGPU,
} {
for _, path := range candidatePluginPaths(bk) {
if !plausiblePath(path) {
continue
}
if !tryLoad(bk, path) {
continue
}
setActiveBackend(bk)
return
}
}
}
// candidatePluginPaths returns the search list for one backend, in priority
// order. The list is small (≤6) so we don't bother deduping — a duplicate
// dlopen on a path that doesn't exist is cheap (immediate ENOENT).
func candidatePluginPaths(bk Backend) []string {
name := dsoBareName(bk)
var paths []string
// 1) Explicit override — useful in tests / CI to pin a specific build.
if env := os.Getenv("LUX_GPU_PLUGIN_DIR"); env != "" {
paths = append(paths, filepath.Join(env, name))
paths = append(paths, filepath.Join(env, bk.String(), name))
}
// 2/3) Install tree (set by `cmake --install`).
if prefix := os.Getenv("LUXCPP_PREFIX"); prefix != "" {
paths = append(paths, filepath.Join(prefix, "lib", "lux-gpu", name))
paths = append(paths, filepath.Join(prefix, "lib", name))
}
// Deliberately no CWD entry and no bare-name fallback. A GPU plugin
// replaces bridgevm's state transitions wholesale, so where it comes from
// has to be something an operator said, not something the loader found:
// with a bare name the dynamic loader searches its default path, and with
// CWD the working directory of whoever started the node decides which
// code computes consensus state. Both are named above or nothing loads.
return paths
}
// dsoBareName returns the platform-correct shared object name for a backend.
// macOS uses .dylib; everything else (linux, *bsd, windows) uses .so —
// Windows builds are produced from the WSL/MinGW toolchain in the gpu-kernels
// CI matrix and ship .so as well per the gpu-kernels CMake convention.
func dsoBareName(bk Backend) string {
ext := ".so"
if runtime.GOOS == "darwin" {
ext = ".dylib"
}
return "libluxgpu_backend_" + bk.String() + ext
}
// plausiblePath returns false for an absolute path that doesn't exist, so we
// skip wasted dlopen calls (each one allocates inside libc's dlerror buffer).
// Bare names (without a slash) are always "plausible" — the loader handles
// the search.
func plausiblePath(p string) bool {
if filepath.IsAbs(p) {
_, err := os.Stat(p)
return err == nil
}
return true
}
// tryLoad dlopens `path`, then dlsyms the five lux_<bk>_bridgevm_* symbols.
// All-or-nothing — if any symbol is missing the handle is left open (it's
// global and the next backend's dlsym calls may benefit) but pluginHandle
// stays nil. Returns true iff every symbol resolved and we've committed to
// this backend.
func tryLoad(bk Backend, path string) bool {
cpath := C.CString(path)
defer C.free(unsafe.Pointer(cpath))
// Clear any prior dlerror state so dl_err() reports THIS call's error.
C.dl_err()
h := C.dl_open(cpath)
if h == nil {
return false
}
// Identify the library before trusting any symbol in it.
if rc := C.bvm_plugin_trusted(h); rc != 0 {
return false
}
prefix := "lux_" + bk.String() + "_bridgevm_"
syms := map[string]*unsafe.Pointer{
prefix + "signer_apply": &fnSignerApply,
prefix + "liquidity_apply": &fnLiquidityApply,
prefix + "message_inbox": &fnMessageInbox,
prefix + "message_outbox": &fnMessageOutbox,
prefix + "transition": &fnBridgeTransit,
}
staging := make(map[string]unsafe.Pointer, 5)
for name := range syms {
cname := C.CString(name)
p := C.dl_sym(h, cname)
C.free(unsafe.Pointer(cname))
if p == nil {
// Missing symbol — backend's plugin doesn't expose bridgevm.
// Don't dlclose: the same .so may carry useful symbols for the
// next probe. Yes this leaks a handle on miss, but probes are
// bounded (5 max) and run once at init.
return false
}
staging[name] = unsafe.Pointer(p)
}
// All five resolved — commit.
pluginHandle = unsafe.Pointer(h)
for name, slot := range syms {
*slot = staging[name]
}
return true
}
// =============================================================================
// GPUBackend implementation — one struct, one method per launcher. The struct
// is stateless (just a Backend tag); the real state lives in the package-level
// fnPtr_* and pluginHandle variables. We expose it as an interface (not a
// concrete struct) so the !cgo stub can satisfy the same surface.
// =============================================================================
// ActiveGPUBackend returns an invocable GPUBackend. When no plugin is loaded
// the returned backend's methods all return ErrGPUNotAvailable; the caller
// can route to the CPU oracle. Always non-nil — there is no error path here.
func ActiveGPUBackend() GPUBackend {
return cgoBackend{tag: AutoBackend()}
}
type cgoBackend struct {
tag Backend
}
func (b cgoBackend) Backend() Backend { return b.tag }
// errFromCode wraps a non-zero launcher return as a Go error tagged with the
// op name + numeric status. The launcher contract is `0 = success`,
// non-zero = error code with meanings documented per-launcher (1 = null
// pointer, 2 = device unavailable, 3 = pipeline compile failure, etc.).
func errFromCode(op string, code C.int) error {
if code == 0 {
return nil
}
return fmt.Errorf("bridgevm: %s launcher returned code %d", op, int(code))
}
// guardOrErr returns ErrGPUNotAvailable when the plugin isn't loaded.
// Centralised so each method has the same gate without scattering branches.
func (b cgoBackend) guard(fn unsafe.Pointer) error {
if pluginHandle == nil || fn == nil || b.tag == BackendNone {
return ErrGPUNotAvailable
}
return nil
}
func (b cgoBackend) SignerApply(
desc *BridgeVMRoundDescriptor,
ops []SignerOp,
signers []Signer,
) (uint32, error) {
if err := b.guard(fnSignerApply); err != nil {
return 0, err
}
if desc == nil || len(signers) == 0 {
return 0, fmt.Errorf("bridgevm: SignerApply requires non-nil desc + non-empty signers")
}
var applied uint32
descP := unsafe.Pointer(desc)
var opsP unsafe.Pointer
if len(ops) > 0 {
opsP = unsafe.Pointer(&ops[0])
}
signersP := unsafe.Pointer(&signers[0])
appliedP := unsafe.Pointer(&applied)
code := C.call_signer(
fnSignerApply,
descP, opsP, signersP, appliedP,
C.uint32_t(len(signers)),
)
runtime.KeepAlive(desc)
runtime.KeepAlive(ops)
runtime.KeepAlive(signers)
return applied, errFromCode("signer_apply", code)
}
func (b cgoBackend) LiquidityApply(
desc *BridgeVMRoundDescriptor,
ops []LiquidityOp,
liquidity []LiquidityEntry,
) (uint32, uint64, uint64, error) {
if err := b.guard(fnLiquidityApply); err != nil {
return 0, 0, 0, err
}
if desc == nil || len(liquidity) == 0 {
return 0, 0, 0, fmt.Errorf(
"bridgevm: LiquidityApply requires non-nil desc + non-empty liquidity")
}
var (
applied uint32
totalFeesLo uint64
totalFeesHi uint64
)
var opsP unsafe.Pointer
if len(ops) > 0 {
opsP = unsafe.Pointer(&ops[0])
}
code := C.call_liquidity(
fnLiquidityApply,
unsafe.Pointer(desc),
opsP,
unsafe.Pointer(&liquidity[0]),
unsafe.Pointer(&applied),
unsafe.Pointer(&totalFeesLo),
unsafe.Pointer(&totalFeesHi),
C.uint32_t(len(liquidity)),
)
runtime.KeepAlive(desc)
runtime.KeepAlive(ops)
runtime.KeepAlive(liquidity)
return applied, totalFeesLo, totalFeesHi, errFromCode("liquidity_apply", code)
}
func (b cgoBackend) MessageInbox(
desc *BridgeVMRoundDescriptor,
inMsgs []Message,
signers []Signer,
daily []DailyLimit,
inbox []Message,
) (uint32, uint64, uint64, error) {
if err := b.guard(fnMessageInbox); err != nil {
return 0, 0, 0, err
}
if desc == nil || len(signers) == 0 || len(daily) == 0 || len(inbox) == 0 {
return 0, 0, 0, fmt.Errorf(
"bridgevm: MessageInbox requires non-nil desc + non-empty signers/daily/inbox")
}
var (
applied uint32
totalInLo uint64
totalInHi uint64
)
var inMsgsP unsafe.Pointer
if len(inMsgs) > 0 {
inMsgsP = unsafe.Pointer(&inMsgs[0])
}
code := C.call_inbox(
fnMessageInbox,
unsafe.Pointer(desc),
inMsgsP,
unsafe.Pointer(&signers[0]),
unsafe.Pointer(&daily[0]),
unsafe.Pointer(&inbox[0]),
unsafe.Pointer(&applied),
unsafe.Pointer(&totalInLo),
unsafe.Pointer(&totalInHi),
C.uint32_t(len(signers)),
C.uint32_t(len(daily)),
C.uint32_t(len(inbox)),
)
runtime.KeepAlive(desc)
runtime.KeepAlive(inMsgs)
runtime.KeepAlive(signers)
runtime.KeepAlive(daily)
runtime.KeepAlive(inbox)
return applied, totalInLo, totalInHi, errFromCode("message_inbox", code)
}
func (b cgoBackend) MessageOutbox(
desc *BridgeVMRoundDescriptor,
reqs []OutboundReq,
daily []DailyLimit,
outbox []Message,
epoch *BridgeVMEpochState,
) (uint32, uint64, uint64, error) {
if err := b.guard(fnMessageOutbox); err != nil {
return 0, 0, 0, err
}
if desc == nil || epoch == nil || len(daily) == 0 || len(outbox) == 0 {
return 0, 0, 0, fmt.Errorf(
"bridgevm: MessageOutbox requires non-nil desc/epoch + non-empty daily/outbox")
}
var (
applied uint32
totalOutLo uint64
totalOutHi uint64
)
var reqsP unsafe.Pointer
if len(reqs) > 0 {
reqsP = unsafe.Pointer(&reqs[0])
}
code := C.call_outbox(
fnMessageOutbox,
unsafe.Pointer(desc),
reqsP,
unsafe.Pointer(&daily[0]),
unsafe.Pointer(&outbox[0]),
unsafe.Pointer(epoch),
unsafe.Pointer(&applied),
unsafe.Pointer(&totalOutLo),
unsafe.Pointer(&totalOutHi),
C.uint32_t(len(daily)),
C.uint32_t(len(outbox)),
)
runtime.KeepAlive(desc)
runtime.KeepAlive(reqs)
runtime.KeepAlive(daily)
runtime.KeepAlive(outbox)
runtime.KeepAlive(epoch)
return applied, totalOutLo, totalOutHi, errFromCode("message_outbox", code)
}
func (b cgoBackend) BridgeTransition(
desc *BridgeVMRoundDescriptor,
signers []Signer,
liquidity []LiquidityEntry,
daily []DailyLimit,
inbox []Message,
outbox []Message,
epoch *BridgeVMEpochState,
result *BridgeVMTransitionResult,
) error {
if err := b.guard(fnBridgeTransit); err != nil {
return err
}
if desc == nil || epoch == nil || result == nil ||
len(signers) == 0 || len(liquidity) == 0 || len(daily) == 0 ||
len(inbox) == 0 || len(outbox) == 0 {
return fmt.Errorf(
"bridgevm: BridgeTransition requires non-nil desc/epoch/result + non-empty arrays")
}
code := C.call_transition(
fnBridgeTransit,
unsafe.Pointer(desc),
unsafe.Pointer(&signers[0]),
unsafe.Pointer(&liquidity[0]),
unsafe.Pointer(&daily[0]),
unsafe.Pointer(&inbox[0]),
unsafe.Pointer(&outbox[0]),
unsafe.Pointer(epoch),
unsafe.Pointer(result),
C.uint32_t(len(signers)),
C.uint32_t(len(liquidity)),
C.uint32_t(len(daily)),
C.uint32_t(len(inbox)),
C.uint32_t(len(outbox)),
)
runtime.KeepAlive(desc)
runtime.KeepAlive(signers)
runtime.KeepAlive(liquidity)
runtime.KeepAlive(daily)
runtime.KeepAlive(inbox)
runtime.KeepAlive(outbox)
runtime.KeepAlive(epoch)
runtime.KeepAlive(result)
return errFromCode("transition", code)
}