Files
llmgw/internal/agent/provision/python_test.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

185 lines
6.7 KiB
Go

package provision
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"testing"
"gitea.mynodes.xyz/ilker/llmgw/internal/agent/discovery"
)
var gpuNode = discovery.Host{OSID: "ubuntu", OSVersion: "24.04", Arch: "amd64",
NVIDIADriver: "580.65.06", MinComputeCapability: 8.0}
// fakePython is uv and the environments it builds.
type fakePython struct {
fakeSystem
pmu sync.Mutex
commands []string
failPip bool
}
func (f *fakePython) run(ctx context.Context, env []string, name string, args ...string) ([]byte, error) {
line := name + " " + strings.Join(args, " ")
f.pmu.Lock()
f.commands = append(f.commands, line)
f.pmu.Unlock()
switch {
case strings.HasSuffix(name, "/uv") && len(args) == 1 && args[0] == "--version":
return []byte("uv 0.12.20\n"), nil
case strings.HasSuffix(name, "/uv") && args[0] == "venv":
return nil, os.MkdirAll(filepath.Join(args[len(args)-1], "bin"), 0o755)
case strings.HasSuffix(name, "/uv") && args[0] == "pip" && args[1] == "install":
if f.failPip {
return []byte("error: Hash mismatch for torch\n"), errExit
}
venv := filepath.Dir(filepath.Dir(args[slices.Index(args, "--python")+1]))
return nil, os.WriteFile(filepath.Join(venv, "bin", "vllm"), []byte("#!/bin/sh\n"), 0o755)
case strings.HasSuffix(name, "/bin/vllm") && args[0] == "--version":
return []byte("0.30.0\n"), nil
}
return f.fakeSystem.run(ctx, env, name, args...)
}
func (f *fakePython) runStdin(_ context.Context, _ []string, stdin, name string, args ...string) ([]byte, error) {
f.pmu.Lock()
f.commands = append(f.commands, name+" "+strings.Join(args, " ")+" <<< "+strings.TrimSpace(stdin))
f.pmu.Unlock()
out := args[slices.Index(args, "--output-file")+1]
return nil, os.WriteFile(out, []byte("vllm==0.30.0 --hash=sha256:ab\n"), 0o644)
}
func pythonInstaller(t *testing.T, host discovery.Host, sys *fakePython) (*Installer, *release) {
uv := newRelease(map[string][]byte{
"uv-x86_64-unknown-linux-gnu.tar.gz": tarGz(t,
map[string]string{"uv-x86_64-unknown-linux-gnu/uv": "#!/bin/sh\n"}, nil),
})
gh := uv.serve(t)
mux := http.NewServeMux()
mux.HandleFunc("/pypi/vllm/0.30.0/json", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"urls":[{"upload_time_iso_8601":"2026-09-22T05:22:21.647189Z"},
{"upload_time_iso_8601":"2026-09-22T05:23:10.039243Z"}]}`))
})
pypi := httptest.NewServer(mux)
t.Cleanup(pypi.Close)
return &Installer{
Root: t.TempDir(), Host: host, Run: sys.run, RunStdin: sys.runStdin,
APIBase: gh.URL, DownloadBase: gh.URL, PyPIBase: pypi.URL, HTTP: gh.Client(),
FreeSpace: func(string) (float64, error) { return 100, nil },
}, uv
}
func TestVLLMIsInstalledOnAGPUNode(t *testing.T) {
sys := &fakePython{fakeSystem: fakeSystem{installed: map[string]bool{}}}
in, _ := pythonInstaller(t, gpuNode, sys)
got := in.Converge(t.Context(), []Want{{Engine: "vllm", Version: "0.30.0"}})
if got[0].State != Ready {
t.Fatalf("not ready: %+v", got[0])
}
dir := filepath.Join(in.Root, "vllm", "0.30.0-cu13")
if got[0].Binary != filepath.Join(dir, "bin", "vllm") {
t.Errorf("binary %s, want the environment's vllm", got[0].Binary)
}
if !sys.installed["gcc"] {
t.Error("gcc, which Triton compiles its launcher with, was not installed")
}
all := strings.Join(sys.commands, "\n")
for _, want := range []string{
// PyPI as it stood a day after vLLM's last file, so the dependencies
// are the ones it was released against.
"--exclude-newer 2026-09-23T05:23:10Z",
"--generate-hashes",
"--require-hashes",
"venv --relocatable --python 3.12",
} {
if !strings.Contains(all, want) {
t.Errorf("no %q in:\n%s", want, all)
}
}
if _, err := os.Stat(filepath.Join(dir, lockName)); err != nil {
t.Errorf("the resolved requirements were not kept: %v", err)
}
if _, err := os.Stat(filepath.Join(in.Root, ".uv-cache")); err == nil {
t.Error("the download cache was left behind after a successful install")
}
// Checked again on the next pass, not installed again.
before := len(sys.commands)
if again := in.Converge(t.Context(), []Want{{Engine: "vllm", Version: "0.30.0"}}); again[0].State != Ready {
t.Fatalf("second pass: %+v", again[0])
}
for _, c := range sys.commands[before:] {
if strings.Contains(c, "pip install") {
t.Errorf("a ready environment was installed again: %s", c)
}
}
}
func TestVLLMIsNotForEveryNode(t *testing.T) {
cases := []struct {
name string
host discovery.Host
state string
says string
}{
{"no GPU", ubuntu, Skipped, "has none"},
{"old driver", discovery.Host{Arch: "amd64", NVIDIADriver: "550.54.15"}, Unsupported, "driver 580"},
{"V100", discovery.Host{Arch: "amd64", NVIDIADriver: "580.65.06", MinComputeCapability: 7.0}, Unsupported, "7.5"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
sys := &fakePython{fakeSystem: fakeSystem{installed: map[string]bool{}}}
in, uv := pythonInstaller(t, c.host, sys)
got := in.Converge(t.Context(), []Want{{Engine: "vllm", Version: "0.30.0"}})
if got[0].State != c.state || !strings.Contains(got[0].Detail, c.says) {
t.Errorf("got %s %q, want %s saying %q", got[0].State, got[0].Detail, c.state, c.says)
}
if uv.gets != 0 || len(sys.commands) != 0 {
t.Errorf("a node that cannot run it still started installing: %v", sys.commands)
}
})
}
}
func TestAPythonEngineIsRefusedWithoutTheDiskForIt(t *testing.T) {
sys := &fakePython{fakeSystem: fakeSystem{installed: map[string]bool{"gcc": true}}}
in, uv := pythonInstaller(t, gpuNode, sys)
in.FreeSpace = func(string) (float64, error) { return 12.5, nil }
got := in.Converge(t.Context(), []Want{{Engine: "vllm", Version: "0.30.0"}})
if got[0].State != Failed || !strings.Contains(got[0].Detail, "12.5 GB") {
t.Errorf("got %s %q", got[0].State, got[0].Detail)
}
if uv.gets != 0 {
t.Error("it began downloading anyway")
}
}
func TestAFailedPythonInstallLeavesNothingThatLooksInstalled(t *testing.T) {
sys := &fakePython{fakeSystem: fakeSystem{installed: map[string]bool{"gcc": true}}, failPip: true}
in, _ := pythonInstaller(t, gpuNode, sys)
got := in.Converge(t.Context(), []Want{{Engine: "vllm", Version: "0.30.0"}})
if got[0].State != Failed || !strings.Contains(got[0].Detail, "Hash mismatch") {
t.Fatalf("got %s %q", got[0].State, got[0].Detail)
}
entries, _ := os.ReadDir(filepath.Join(in.Root, "vllm"))
for _, e := range entries {
t.Errorf("left behind: %s", e.Name())
}
}
func TestAVersionPyPIDoesNotHaveIsSaidSo(t *testing.T) {
sys := &fakePython{fakeSystem: fakeSystem{installed: map[string]bool{"gcc": true}}}
in, _ := pythonInstaller(t, gpuNode, sys)
got := in.Converge(t.Context(), []Want{{Engine: "vllm", Version: "9.9.9"}})
if got[0].State != Failed || !strings.Contains(got[0].Detail, "PyPI has no vllm 9.9.9") {
t.Errorf("got %s %q", got[0].State, got[0].Detail)
}
}