package cli import ( "strings" "testing" "gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi" "gitea.mynodes.xyz/ilker/llmgw/internal/modelplan" ) func TestBarStaysWithinItsWidth(t *testing.T) { // It is drawn from counts that can disagree briefly — a GPU freed between // two reads — and a bar that overruns its column wrecks the table it is in. for _, tc := range [][2]int{{0, 0}, {0, 8}, {8, 8}, {9, 8}, {100, 8}} { got := bar(tc[0], tc[1]) if tc[1] == 0 { if got != "" { t.Errorf("bar(%d,%d) = %q, want empty for an empty class", tc[0], tc[1], got) } continue } if n := strings.Count(got, "#") + strings.Count(got, "."); n != 20 { t.Errorf("bar(%d,%d) drew %d cells: %q", tc[0], tc[1], n, got) } } } func TestIdleColumnDistinguishesUnknownFromIdle(t *testing.T) { // Every replica looks unreported before its first request and after a // router restart. Showing that as a very large duration would make an // untouched fleet read as an abandoned one. if got := idleColumn(-1); got != "-" { t.Errorf("idleColumn(-1) = %q, want a dash", got) } if got := idleColumn(90); got != "1m30s" { t.Errorf("idleColumn(90) = %q", got) } } func TestModelStatusSeparatesWaitingFromBroken(t *testing.T) { // These used to differ by a parenthetical — "0/1 ready (1 total)" against // "0/1 ready" — and they need opposite responses: one means wait, the other // means look at explain-placement. On forty models that difference is // invisible. for _, tc := range []struct { name string view adminapi.ModelView want string }{ {"serving what it asked for", adminapi.ModelView{Min: 2, Ready: 2, Total: 2}, "ready"}, {"a replica is coming up", adminapi.ModelView{Min: 1, Ready: 0, Total: 1}, "starting"}, {"nothing could be placed", adminapi.ModelView{Min: 1, Ready: 0, Total: 0}, "not placed"}, {"deliberately not running", adminapi.ModelView{Min: 0, Ready: 0, Total: 0}, "stopped"}, {"short but rebuilding", adminapi.ModelView{Min: 2, Ready: 1, Total: 2}, "starting"}, {"woken by a request", adminapi.ModelView{Min: 0, Ready: 1, Total: 1}, "ready (on demand)"}, {"waking", adminapi.ModelView{Min: 0, Ready: 0, Total: 1}, "starting"}, } { t.Run(tc.name, func(t *testing.T) { if got := modelStatus(tc.view); got != tc.want { t.Errorf("modelStatus(%+v) = %q, want %q", tc.view, got, tc.want) } }) } } func TestHardwarePoolsNeverMergeAcrossFabrics(t *testing.T) { // A replica may never span two fabrics, so eight free cards split across // two of them are not eight cards for a model that needs eight. Merging // them would plan something that starts, passes health checks and hangs on // its first all-reduce. nodes := []adminapi.NodeView{ {ID: "a", State: "ready", Classes: []string{"h100-80g"}, Fabrics: []string{"ib-a"}, GPUFree: 4, GPUTotal: 4}, {ID: "b", State: "ready", Classes: []string{"h100-80g"}, Fabrics: []string{"ib-a"}, GPUFree: 4, GPUTotal: 4}, {ID: "c", State: "ready", Classes: []string{"h100-80g"}, Fabrics: []string{"ib-b"}, GPUFree: 4, GPUTotal: 4}, } hw := hardwareFrom(nodes) if len(hw) != 2 { t.Fatalf("expected one pool per fabric, got %d: %+v", len(hw), hw) } for _, p := range hw { switch p.Fabric { case "ib-a": if p.Nodes != 2 { t.Errorf("ib-a has %d nodes, want 2", p.Nodes) } case "ib-b": if p.Nodes != 1 { t.Errorf("ib-b has %d nodes, want 1", p.Nodes) } default: t.Errorf("unexpected fabric %q", p.Fabric) } } } func TestUnreadyNodesAreNotCountedAsCapacity(t *testing.T) { // Planning against a node that is draining or unreachable produces a fit // that cannot be placed, which is worse than saying nothing fits. nodes := []adminapi.NodeView{ {ID: "a", State: "ready", Classes: []string{"h100-80g"}, GPUFree: 8, GPUTotal: 8}, {ID: "b", State: "draining", Classes: []string{"h100-80g"}, GPUFree: 8, GPUTotal: 8}, {ID: "c", State: "unreachable", Classes: []string{"h100-80g"}, GPUFree: 8, GPUTotal: 8}, } hw := hardwareFrom(nodes) if len(hw) != 1 || hw[0].Nodes != 1 { t.Fatalf("expected only the ready node, got %+v", hw) } } func TestCardMemoryComesFromTheClassLabel(t *testing.T) { // The label carries it by construction — "h100-80g" is eighty gigabytes and // says so — which is why the planner needs no table of card models. for label, want := range map[string]int{ "h100-80g": 80, "a100-40g": 40, "l40s-48g": 48, "rtx4090-24g": 24, "nonsense": 0, "": 0, "h100-": 0, } { if got := memFromClass(label); got != want { t.Errorf("memFromClass(%q) = %d, want %d", label, got, want) } } } func TestACPUOnlyNodeBecomesAMemoryPool(t *testing.T) { // How this cluster began: a node with no cards is not an empty pool, it is // a pool measured in RAM. hw := hardwareFrom([]adminapi.NodeView{ {ID: "cpu1", State: "ready", MemoryGB: 31}, }) if len(hw) != 1 { t.Fatalf("the CPU node produced %d pools", len(hw)) } if hw[0].Class != "" || hw[0].HostMemGB != 31 { t.Errorf("pool = %+v, want a classless pool with 31 GB", hw[0]) } } func TestAGPUNodeWithNoFreeCardsIsNotAPool(t *testing.T) { // Fully allocated hardware is not capacity, and offering it would plan a // replica that cannot be placed. hw := hardwareFrom([]adminapi.NodeView{ {ID: "full", State: "ready", Classes: []string{"h100-80g"}, GPUFree: 0, GPUTotal: 8}, }) for _, p := range hw { if p.Class != "" { t.Errorf("a node with no free cards became a pool: %+v", p) } } } func TestTheEntryNamesAnEngineThatCanServeThePrecision(t *testing.T) { // The defect this prevents, found by reading the tool's own output: it // chose q8_0 — a GGUF quantisation — and emitted a vLLM profile with no // quantisation at all. Following that loads full-precision weights, which // the same plan had just shown do not fit on one card. The tool would have // caused the out-of-memory it exists to prevent. m := modelplan.Model{Name: "Qwen2.5-Coder-32B", Params: 32_800_000_000, Layers: 64, KVHeads: 8, HeadDim: 128} gguf := modelplan.Fit{Precision: modelplan.Q8GGUF, Class: "h100-80g", TP: 1, Nodes: 1} entry := catalogEntry(m, gguf, "Qwen/Qwen2.5-Coder-32B-Instruct", 32768) if !strings.Contains(entry, "engine: llamacpp") { t.Errorf("a GGUF precision produced a non-llamacpp profile:\n%s", entry) } if strings.Contains(entry, "source: Qwen/") { t.Errorf("a GGUF entry points at a repository rather than a file:\n%s", entry) } if !strings.Contains(entry, ".gguf") { t.Errorf("a GGUF entry has no file path:\n%s", entry) } fp8 := modelplan.Fit{Precision: modelplan.FP8, Class: "h100-80g", TP: 2, Nodes: 1} entry = catalogEntry(m, fp8, "Qwen/Qwen2.5-Coder-32B-Instruct", 32768) if !strings.Contains(entry, "engine: vllm") { t.Errorf("fp8 did not produce a vLLM profile:\n%s", entry) } // And the quantisation has to reach the engine, or vLLM loads bf16. if !strings.Contains(entry, "--quantization") || !strings.Contains(entry, "fp8") { t.Errorf("the fp8 quantisation is not passed to the engine:\n%s", entry) } if !strings.Contains(entry, "source: Qwen/") { t.Errorf("a vLLM entry should load from the repository:\n%s", entry) } }