Files
llmgw/internal/web/explore_fit_test.go
T
Ilker ManapandClaude Opus 5.5 6a870a8c15
CI / test (push) Successful in 9m13s
The explorer's card figures: MIG, NVLink, GH200, FP8 and FP4
From review. A MIG instance is given its share of the card's bandwidth,
not the whole card's. NVLink between a layout's cards means all of them
joined to each other, the largest such set on one ready node, not a count
of cards with any peer: two bridged pairs are not four on NVLink. GH200 is
matched before H200. An FP8 checkpoint is listed for cards of 8.0 and up,
which vLLM runs with Marlin; 8.9 is the agent's bound for --quantization
fp8. NVFP4 repositories tagged fp4 are sized at four bits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:38:38 +01:00

240 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package web
import (
"html"
"strings"
"testing"
"time"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
"gitea.mynodes.xyz/ilker/llmgw/internal/hf"
)
// nodesFleet is a cluster of the given nodes, all ready.
type nodesFleet struct {
fakeFleet
nodes []adminapi.NodeView
}
func (f nodesFleet) Nodes() []adminapi.NodeView { return f.nodes }
func gpuNode(id, class string, cards int, cc string, ramGB int) adminapi.NodeView {
return adminapi.NodeView{ID: id, State: "ready", GPUTotal: cards, GPUFree: cards, MemoryGB: ramGB,
Classes: []string{class}, ClassGPUs: []adminapi.ClassGPUs{{Class: class, Total: cards, Free: cards}},
ComputeCapabilities: []string{cc}}
}
func cpuNode(id string, ramGB int) adminapi.NodeView {
return adminapi.NodeView{ID: id, State: "ready", MemoryGB: ramGB}
}
func stModel(id string, params map[string]int64, tags ...string) hf.Model {
var total int64
for dt, n := range params {
if dt != "I32" {
total += n
}
}
return hf.Model{ID: id, CreatedAt: time.Now(), LastModified: time.Now(), PipelineTag: "text-generation",
Tags: append([]string{"safetensors"}, tags...), Safetensors: &hf.SafetensorsInfo{Total: total, Parameters: params}}
}
// listing is what the explorer shows of these repositories on this cluster.
func listing(t *testing.T, nodes []adminapi.NodeView, ms ...hf.Model) (string, *fakeRegistry) {
t.Helper()
reg := &fakeRegistry{results: ms}
mux := mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg, Fleet: nodesFleet{nodes: nodes}})
return mirrorGet(t, mux, "/admin/mirror?tab=explore").Body.String(), reg
}
func TestTheExplorerListsWhatThisClustersHardwareHolds(t *testing.T) {
// The PoC: one Turing card of 24 GB, CPU nodes of up to 31 GB.
poc := []adminapi.NodeView{gpuNode("cuda01", "6000-24g", 1, "7.5", 251), cpuNode("node05", 31)}
body, reg := listing(t, poc,
ggufModel("a/Small-GGUF", "qwen3", 8e9, time.Now()),
ggufModel("a/Huge-GGUF", "qwen3moe", 671e9, time.Now()),
stModel("a/Eight-BF16", map[string]int64{"BF16": 8e9}),
stModel("a/ThirtyTwo-BF16", map[string]int64{"BF16": 32e9}),
stModel("a/Eight-FP8", map[string]int64{"F8_E4M3": 8e9, "BF16": 1e8}),
stModel("a/ThirtyTwo-AWQ", map[string]int64{"BF16": 1e9, "I32": 31e9}, "awq", "4-bit"),
stModel("a/Eight-MLX", map[string]int64{"U32": 8e9}, "mlx", "8-bit"),
)
if reg.query.GGUF || reg.query.Limit != 100 {
t.Errorf("with a card, safetensors are asked for too, more of them: %+v", reg.query)
}
for id, want := range map[string]bool{
"a/Small-GGUF": true, "a/Huge-GGUF": false,
"a/Eight-BF16": true, "a/ThirtyTwo-BF16": false,
"a/Eight-FP8": false, "a/ThirtyTwo-AWQ": true, "a/Eight-MLX": false,
} {
if got := strings.Contains(body, id+"<") || strings.Contains(body, id+"\""); got != want {
t.Errorf("%s listed = %v, want %v", id, got, want)
}
}
if !strings.Contains(body, "largest card has 24 GB (compute capability 7.5)") ||
!strings.Contains(body, "4 more are hidden") {
t.Errorf("the page should say what it judged by and how many it left out: %s", body)
}
}
func TestANewerClusterSeesTheModelsItCanRun(t *testing.T) {
// A customer's: eight H100s. What the PoC cannot run, this one lists.
h100 := []adminapi.NodeView{gpuNode("gpu01", "h100-80g", 8, "9.0", 1024)}
body, _ := listing(t, h100,
stModel("a/ThirtyTwo-BF16", map[string]int64{"BF16": 32e9}),
stModel("a/Eight-FP8", map[string]int64{"F8_E4M3": 8e9, "BF16": 1e8}),
ggufModel("a/Huge-GGUF", "qwen3moe", 671e9, time.Now()),
)
for _, id := range []string{"a/ThirtyTwo-BF16", "a/Eight-FP8", "a/Huge-GGUF"} {
if !strings.Contains(body, id) {
t.Errorf("%s should be listed on eight H100s", id)
}
}
}
func TestACPUOnlyClusterIsShownGGUFOnly(t *testing.T) {
body, reg := listing(t, []adminapi.NodeView{cpuNode("c1", 32)},
ggufModel("a/Small-GGUF", "qwen3", 8e9, time.Now()),
ggufModel("a/Big-GGUF", "qwen3", 70e9, time.Now()),
stModel("a/Eight-BF16", map[string]int64{"BF16": 8e9}),
)
if !reg.query.GGUF {
t.Errorf("without cards only GGUF runs, and should be asked for: %+v", reg.query)
}
if !strings.Contains(body, "a/Small-GGUF") || strings.Contains(body, "a/Big-GGUF") || strings.Contains(body, "a/Eight-BF16") {
t.Errorf("a 32 GB CPU node holds the 8B GGUF only: %s", body)
}
if !strings.Contains(body, "no GPUs, so GGUF only") {
t.Error("the page should say it judged by a cluster without cards")
}
}
func TestARepositorysFileTooBigForThisClusterIsNotOffered(t *testing.T) {
poc := []adminapi.NodeView{gpuNode("cuda01", "6000-24g", 1, "7.5", 251), cpuNode("node05", 31)}
reg := &fakeRegistry{repo: hf.Repo{ID: "a/M", Revision: "abc123def456", Params: 32e9,
Files: []hf.File{
{Path: "M-Q4_K_M.gguf", Bytes: 19 << 30},
{Path: "model-00001-of-00002.safetensors", Bytes: 32 << 30},
{Path: "model-00002-of-00002.safetensors", Bytes: 32 << 30},
}}}
mux := mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg, Fleet: nodesFleet{nodes: poc}})
body := mirrorGet(t, mux, "/admin/mirror?repo=a/M").Body.String()
if !strings.Contains(body, "/resolve/abc123def456/M-Q4_K_M.gguf") {
t.Errorf("the GGUF fits cuda01's RAM and should be offered: %s", body)
}
if strings.Contains(body, "/resolve/abc123def456/model-00001-of-00002.safetensors") ||
!strings.Contains(body, "too big for this cluster") {
t.Errorf("64 GB of safetensors on one 24 GB card should be marked, not offered: %s", body)
}
}
func TestPackedWeightsAreSizedAsTheyAreStored(t *testing.T) {
// gpt-oss-20b: MXFP4 in U8, counted by the registry at the weights it
// unpacks to. At a byte each it read as 22 GB and missed a 24 GB card.
oss := stModel("openai/gpt-oss-20b", map[string]int64{"BF16": 1804459584, "U8": 19110297600}, "8-bit", "mxfp4")
if gb := float64(oss.WeightBytes()) / (1 << 30); gb < 11 || gb > 13 {
t.Errorf("gpt-oss-20b sized %.1f GB, want about 12", gb)
}
// Tagged mlx, but with a GGUF too: the GGUF runs.
both := ggufModel("a/Both", "qwen3", 27e9, time.Now())
both.Tags = []string{"mlx", "safetensors", "gguf"}
poc := []adminapi.NodeView{gpuNode("cuda01", "6000-24g", 1, "7.5", 251)}
body, _ := listing(t, poc, oss, both)
if !strings.Contains(body, "openai/gpt-oss-20b") || !strings.Contains(body, "a/Both") {
t.Errorf("both run on cuda01: %s", body)
}
}
func TestTheExplorerSaysHowFastAModelAnswersOnACPU(t *testing.T) {
slow, fast := cpuNode("node01", 7), cpuNode("node05", 31)
slow.MemBandwidthGBs, fast.MemBandwidthGBs = 24.9, 41.9
body, _ := listing(t, []adminapi.NodeView{slow, fast},
ggufModel("Qwen/Qwen3-8B-GGUF", "qwen3", 8e9, time.Now()),
ggufModel("Qwen/Qwen3-30B-A3B-GGUF", "qwen3moe", 30e9, time.Now()),
)
// 8B at Q4_K_M is 4.8 GB, on node05's 41.9 GB/s: about 5.8 a second.
// The 30B model of experts reads 1.6 times its 3B active part: about 9.4.
for _, want := range []string{">~5.8<", ">~9.4<", "on node05&#39;s CPU"} {
if !strings.Contains(body, want) {
t.Errorf("the listing lacks %q: %s", want, body)
}
}
reg := &fakeRegistry{repo: hf.Repo{ID: "Qwen/Qwen3-8B-GGUF", Revision: "abc123def456", Params: 8e9,
Files: []hf.File{{Path: "Qwen3-8B-Q4_K_M.gguf", Bytes: 5 << 30}, {Path: "Qwen3-8B-Q8_0.gguf", Bytes: 8 << 30}}}}
mux := mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg,
Fleet: nodesFleet{nodes: []adminapi.NodeView{slow, fast}}})
page := html.UnescapeString(mirrorGet(t, mux, "/admin/mirror?repo=Qwen/Qwen3-8B-GGUF").Body.String())
if !strings.Contains(page, "~5.2 tok/s on node05's CPU") || !strings.Contains(page, "~3.3 tok/s on node05's CPU") {
t.Errorf("each file should say its speed on the CPU, by its own size: %s", page)
}
}
// cards is a node of n cards of a class, named as the driver names them,
// joined by NVLink or not, on a fabric or none.
func cards(id, class, name string, n int, cc string, nvlink bool, fabric string) adminapi.NodeView {
v := gpuNode(id, class, n, cc, 1024)
for i := 0; i < n; i++ {
g := adminapi.GPUView{Index: i, Name: name, Class: class}
if nvlink {
for j := 0; j < n; j++ {
if j != i {
g.NVLinkPeers = append(g.NVLinkPeers, j)
}
}
}
v.GPUs = append(v.GPUs, g)
}
if fabric != "" {
v.Fabrics = []string{fabric}
v.IBPorts = []adminapi.IBPortView{{HCA: "mlx5_0", Port: 1, Fabric: fabric, LinkLayer: "InfiniBand",
RateGbps: 200, Active: true}}
}
return v
}
func TestTheExplorerSaysHowFastAModelAnswersOnTheCards(t *testing.T) {
// The PoC's one Quadro RTX 6000: an 8B GGUF at Q4_K_M, 4.8 GB, on
// 672 GB/s.
poc := []adminapi.NodeView{cards("cuda01", "6000-24g", "Quadro RTX 6000", 1, "7.5", false, "")}
body, _ := listing(t, poc, ggufModel("Qwen/Qwen3-8B-GGUF", "qwen3", 8e9, time.Now()))
if !strings.Contains(body, `title="1× 6000-24g, llama.cpp">~103<`) {
t.Errorf("an 8B on cuda01's card should be about 103 a second: %s", body)
}
// A mixed site: eight H100s on NVLink, four L40S on PCIe. 70B in BF16
// fits two H100s or four L40S, and the planner takes the fewest cards:
// the H100s are the faster.
mixed := []adminapi.NodeView{
cards("h", "h100-80g", "NVIDIA H100 80GB HBM3", 8, "9.0", true, ""),
cards("l", "l40s-48g", "NVIDIA L40S", 4, "8.9", false, ""),
}
body, _ = listing(t, mixed, stModel("meta-llama/Llama-3.3-70B-Instruct", map[string]int64{"BF16": 70e9}))
if !strings.Contains(body, "2× h100-80g, NVLink, vLLM") {
t.Errorf("70B should run fastest on two NVLinked H100s: %s", body)
}
// Two nodes of one A100 each on InfiniBand: 70B fits neither, and the
// pipeline across them is what the planner offers.
ib := []adminapi.NodeView{
cards("a", "a100-80g", "NVIDIA A100-SXM4-80GB", 1, "8.0", false, "ib0"),
cards("b", "a100-80g", "NVIDIA A100-SXM4-80GB", 1, "8.0", false, "ib0"),
}
body, _ = listing(t, ib, stModel("meta-llama/Llama-3.3-70B-Instruct", map[string]int64{"BF16": 70e9}))
if !strings.Contains(body, "2 nodes × 1× a100-80g over ib0 (InfiniBand 200 Gb/s), vLLM") {
t.Errorf("70B should be laid out across both nodes over ib0: %s", body)
}
}
func TestTwoBridgedPairsAreNotFourCardsOnNVLink(t *testing.T) {
pairs := []adminapi.GPUView{{Index: 0, NVLinkPeers: []int{1}}, {Index: 1, NVLinkPeers: []int{0}},
{Index: 2, NVLinkPeers: []int{3}}, {Index: 3, NVLinkPeers: []int{2}}}
if got := nvlinkClique(pairs); got != 2 {
t.Errorf("bridged pairs: %d cards together, want 2", got)
}
full := cards("h", "h100-80g", "NVIDIA H100 80GB HBM3", 4, "9.0", true, "").GPUs
if got := nvlinkClique(full); got != 4 {
t.Errorf("four on one fabric: %d", got)
}
}