mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
202 lines
7.0 KiB
Go
202 lines
7.0 KiB
Go
// 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()
|
|
}
|