Each engine has a list of builds, best first, from what its project publishes and how each was compiled: llama.cpp CUDA 13.3, CUDA 12.8 (driver 570 for the GPUs it ships as PTX), then Vulkan, never the CPU build on an NVIDIA node; vLLM CUDA 13, then CUDA 12.9, and for GPUs older than every build of the version asked for, the newest built for them (V100: 0.19.1). The node takes the first its hardware meets and that installs and computes on the GPU, moving on when one does not, and reports the version it runs. GPU processes are read from nvidia-smi's full report, so Vulkan engines count as on their GPU. Refs #2. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
76 lines
2.9 KiB
Go
76 lines
2.9 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/inventory"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
|
|
)
|
|
|
|
// buildFleet is one GPU node whose vLLM build was chosen, and one CPU node.
|
|
type buildFleet struct{ fakeFleet }
|
|
|
|
func (buildFleet) Nodes() []adminapi.NodeView {
|
|
return []adminapi.NodeView{
|
|
{ID: "gpu07", State: "ready", GPUTotal: 8, GPUFree: 8,
|
|
EngineBuilds: map[string]string{"vllm": "cu129"},
|
|
Provision: []inventory.EngineInstall{{Engine: "vllm", Version: "0.30.0", Variant: "cu129", State: "installing"}}},
|
|
{ID: "cpu01", State: "ready"},
|
|
}
|
|
}
|
|
|
|
type recordedChoice struct{ node, engine, build, by string }
|
|
|
|
type fakeEngineAdmin struct{ got []recordedChoice }
|
|
|
|
func (f *fakeEngineAdmin) SetNodeEngine(_ context.Context, node, engine, build, by string) error {
|
|
f.got = append(f.got, recordedChoice{node, engine, build, by})
|
|
return nil
|
|
}
|
|
|
|
func TestTheVLLMBuildIsChosenOnTheClusterPage(t *testing.T) {
|
|
a := fakeAccounts{user: store.User{ID: "u1", Email: "a@example.com", IsAdmin: true}}
|
|
p := NewAccountPages(a, nil, nil, "v0", slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /admin", p.AdminPage(Dashboard(buildFleet{}, nil, "v1", time.Second, false, NavExtras{}, nil)))
|
|
fe := &fakeEngineAdmin{}
|
|
p.RegisterNodeEngines(mux, fe)
|
|
|
|
body := mirrorGet(t, mux, "/admin").Body.String()
|
|
if !strings.Contains(body, `action="/admin/nodes/gpu07/engines"`) ||
|
|
!strings.Contains(body, `<option value="cu129" selected>CUDA 12.9</option>`) {
|
|
t.Fatalf("no choice of build for the GPU node, with its current one selected: %s", body)
|
|
}
|
|
if strings.Contains(body, `action="/admin/nodes/cpu01/engines"`) {
|
|
t.Error("a node with no GPU was offered a vLLM build")
|
|
}
|
|
if !strings.Contains(body, "0.30.0 CUDA 12.9 installing") {
|
|
t.Error("what is installed is not shown beside the choice")
|
|
}
|
|
|
|
// The read-only page on the admin listener has no session, so no form.
|
|
_, ro := get(t, Dashboard(buildFleet{}, nil, "v1", time.Second, true, NavExtras{}, nil), "http://127.0.0.1:9090/", nil)
|
|
if strings.Contains(ro, "/engines\"") {
|
|
t.Error("the admin listener's page offers a change it cannot sign")
|
|
}
|
|
|
|
w := mirrorPost(t, mux, "/admin/nodes/gpu07/engines", url.Values{"csrf": {"tok"}, "engine": {"vllm"}, "build": {"none"}})
|
|
if w.Code != http.StatusSeeOther || len(fe.got) != 1 || fe.got[0] != (recordedChoice{"gpu07", "vllm", "none", "a@example.com"}) {
|
|
t.Fatalf("status %d, chose %+v", w.Code, fe.got)
|
|
}
|
|
if loc := w.Header().Get("Location"); !strings.Contains(loc, "notice=") {
|
|
t.Errorf("redirected to %s, saying nothing", loc)
|
|
}
|
|
if w := mirrorPost(t, mux, "/admin/nodes/gpu07/engines", url.Values{"csrf": {"bad"}, "engine": {"vllm"}, "build": {"none"}}); w.Code != http.StatusBadRequest || len(fe.got) != 1 {
|
|
t.Errorf("a forged form was accepted: %d", w.Code)
|
|
}
|
|
}
|