Files
llmgw/internal/agent/discovery/host.go
T
Ilker ManapandClaude Opus 5.5 4c5a2fd6da Install vLLM on every GPU node that can run it, as llama.cpp is
vLLM was installed by hand and named in each agent's configuration. The
agent now installs it: a Python 3.12 environment built by uv, which is
itself a GitHub release checked against its checksum, from PyPI resolved as
it stood a day after that vLLM was published and installed with
--require-hashes. The resolved list is kept in the environment. The
environment is built relocatable under a scratch name and renamed into place
once it runs, like an archive.

The default is 0.30.0, whose flags were checked against the ones llmgw
builds. It is built for CUDA 13, so it needs driver 580 and a Turing GPU or
newer, and 25 GB free while it installs; a node lacking one says which, and a
node with no GPU skips it without being shown as a fault. gcc comes with it
because Triton compiles its launcher at the first model load. SGLang
installs the same way when the control plane names a version. llmgw doctor
reports whether vLLM can be installed on the node.

Checked by installing 0.30.0 this way on a test node: 7.7 GB, torch 2.13 for
CUDA 13.0, importing cleanly; the node has no GPU, so vLLM was not started.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 13:15:54 +02:00

175 lines
4.7 KiB
Go

package discovery
import (
"bufio"
"os"
"runtime"
"strconv"
"strings"
)
// Host is what a node is, as far as installing software on it is concerned.
type Host struct {
// OSID, OSVersion and OSCodename come from /etc/os-release: "ubuntu",
// "24.04", "noble".
OSID string `json:"os_id,omitempty"`
OSVersion string `json:"os_version,omitempty"`
OSCodename string `json:"os_codename,omitempty"`
// OSLike is ID_LIKE: the families this system says it follows, "debian"
// for Ubuntu, "rhel centos fedora" for Rocky. It is what decides the
// package manager, which the name alone does not for every derivative.
OSLike []string `json:"os_like,omitempty"`
// Arch is Debian's spelling, "amd64" or "arm64", because that is what
// package names and release archives are keyed by.
Arch string `json:"arch,omitempty"`
// CPUFlags are the vector extensions inference cares about, of those this
// processor has.
CPUFlags []string `json:"cpu_flags,omitempty"`
// NVIDIADriver is the loaded driver's version, empty when there is none.
NVIDIADriver string `json:"nvidia_driver,omitempty"`
// MinComputeCapability is the oldest GPU's, 0 when there are none or it
// is not known. Not read here but from the GPU inventory, by the agent.
MinComputeCapability float64 `json:"min_compute_capability,omitempty"`
}
// Family is the packaging family: "deb", "rpm", or "" when neither is known.
func (h Host) Family() string {
for _, id := range append([]string{h.OSID}, h.OSLike...) {
switch id {
case "debian", "ubuntu":
return "deb"
case "rhel", "fedora", "centos", "rocky", "almalinux", "suse", "opensuse":
return "rpm"
}
}
return ""
}
// HasCPUFlag reports whether the processor has an extension.
func (h Host) HasCPUFlag(f string) bool {
for _, have := range h.CPUFlags {
if have == f {
return true
}
}
return false
}
// interestingFlags are the extensions that decide which build of an engine
// runs well. A VM on QEMU's default CPU model has none of them — and still
// runs, five times slower, with nothing saying why.
var interestingFlags = []string{"avx", "avx2", "fma", "f16c", "avx512f", "avx512_bf16", "amx_tile", "asimd", "sve"}
// DiscoverHost reads what the machine is.
func DiscoverHost() Host {
h := hostFromOSRelease("/etc/os-release")
h.Arch = debianArch(runtime.GOARCH)
h.CPUFlags = cpuFlagsFrom("/proc/cpuinfo")
h.NVIDIADriver = nvidiaDriverFrom("/proc/driver/nvidia/version")
return h
}
func hostFromOSRelease(path string) Host {
var h Host
f, err := os.Open(path) //nolint:gosec // a fixed system path
if err != nil {
return h
}
defer f.Close()
sc := bufio.NewScanner(f)
for sc.Scan() {
k, v, ok := strings.Cut(sc.Text(), "=")
if !ok {
continue
}
v = strings.Trim(v, `"'`)
switch k {
case "ID":
h.OSID = v
case "VERSION_ID":
h.OSVersion = v
case "VERSION_CODENAME":
h.OSCodename = v
case "ID_LIKE":
h.OSLike = strings.Fields(v)
}
}
return h
}
func debianArch(goarch string) string {
switch goarch {
case "amd64", "arm64":
return goarch
case "386":
return "i386"
case "arm":
return "armhf"
}
return goarch
}
func cpuFlagsFrom(path string) []string {
b, err := os.ReadFile(path) //nolint:gosec // a fixed system path
if err != nil {
return nil
}
have := map[string]bool{}
for _, line := range strings.Split(string(b), "\n") {
k, v, ok := strings.Cut(line, ":")
if !ok {
continue
}
// "flags" on x86, "Features" on ARM. The first processor is enough:
// a machine does not mix instruction sets across its cores.
if k = strings.TrimSpace(k); k != "flags" && k != "Features" {
continue
}
for _, f := range strings.Fields(v) {
have[f] = true
}
break
}
var out []string
for _, f := range interestingFlags {
if have[f] {
out = append(out, f)
}
}
return out
}
// nvidiaDriverFrom reads the loaded kernel module's version, which exists
// exactly when a driver is loaded — unlike nvidia-smi, which can be installed
// on a machine whose module failed to load.
func nvidiaDriverFrom(path string) string {
b, err := os.ReadFile(path) //nolint:gosec // a fixed system path
if err != nil {
return ""
}
// "NVRM version: NVIDIA UNIX x86_64 Kernel Module 570.86.15 Thu ..."
fields := strings.Fields(string(b))
for i, f := range fields {
if f == "Module" && i+1 < len(fields) {
return fields[i+1]
}
}
return ""
}
// MinComputeCapability is the oldest GPU's compute capability, 0 when none
// is known: an engine built for newer GPUs is refused by the oldest.
func MinComputeCapability(gpus []GPU) float64 {
low := 0.0
for _, g := range gpus {
cc, err := strconv.ParseFloat(strings.TrimSpace(g.ComputeCapability), 64)
if err != nil || cc <= 0 {
continue
}
if low == 0 || cc < low {
low = cc
}
}
return low
}