A GB200 NVL72 rack or an NVLink Switch system is a scale-up island: its GPUs reach each other over NVLink whatever node they are in. Each GPU now reports its fabric from nvidia-smi -q - state, status, ClusterUUID, CliqueId, bandwidth - at registration and once a minute after, since a fabric manager restarting drops every card out at once; and each node reports nvidia-imex and its channels, without which the domain's GPUs cannot share memory across nodes. A single machine's NVSwitch, which reports a cluster of zeros, is no domain. A replica spanning nodes is placed inside one domain first, on nodes whose every card is joined and whose IMEX is up, and its shards are told NCCL_MNNVL_ENABLE=1; InfiniBand is the fallback. /admin/nvlink draws each domain as a rack - the NVLink Switch band on top, light running down a backplane, trays of GPU chips that glow when joined, pulse red when not, turn amber on degraded bandwidth - and lists what keeps a tray out. A node's page shows its GPU-by-GPU NVLink matrix, each card's NUMA node and nearest adapter, and its domain and IMEX. doctor checks joining, bandwidth, IMEX and its channel; metrics llmgw_gpu_nvlink_joined and llmgw_imex_up; /api/v1/nvlink. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
177 lines
6.5 KiB
Go
177 lines
6.5 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/nvlink"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/topology"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/promq"
|
|
)
|
|
|
|
// nodeMux mounts the page where it lives, so the path carries the node.
|
|
func nodeMux(src Source, h History) http.Handler {
|
|
mux := http.NewServeMux()
|
|
if h == nil {
|
|
mux.HandleFunc("GET /admin/nodes/{id}", NodePage(src, nil, time.Hour, false, "v1", nil))
|
|
} else {
|
|
mux.HandleFunc("GET /admin/nodes/{id}", NodePage(src, h, time.Hour, false, "v1", nil))
|
|
}
|
|
return mux
|
|
}
|
|
|
|
// busySource is fakeSource with a node that has been measured.
|
|
type busySource struct{ fakeSource }
|
|
|
|
func (busySource) Nodes() []adminapi.NodeView {
|
|
n := fakeSource{}.Nodes()[0]
|
|
n.Load = &adminapi.LoadView{CPUPercent: 62, CPUs: 8, Load1: 3.25, Load5: 2, Load15: 1,
|
|
MemUsed: 6 << 30, MemTotal: 8 << 30, MemPercent: 75}
|
|
n.IBPorts = []adminapi.IBPortView{
|
|
{HCA: "mlx5_0", Port: 1, State: "ACTIVE", PhysState: "LinkUp", RateGbps: 400, Active: true,
|
|
Fabric: "ib-a", TxBytes: 4 << 30},
|
|
{HCA: "mlx5_1", Port: 1, State: "DOWN", PhysState: "Polling", Errors: map[string]uint64{"link_downed": 7}},
|
|
}
|
|
return []adminapi.NodeView{n}
|
|
}
|
|
|
|
func (busySource) Replicas(string) []adminapi.ReplicaView {
|
|
r := fakeSource{}.Replicas("")
|
|
r[0].Shards[0].CPUCores, r[0].Shards[0].MemoryBytes = 4, 5<<30
|
|
return r
|
|
}
|
|
|
|
func TestANodesPageShowsHowBusyItIsAndWhatIsUsingIt(t *testing.T) {
|
|
h := &fakeHistory{byExpr: map[string][]promq.Series{
|
|
`llmgw_node_cpu_utilization_percent{node="llmgw-node01"} / 100`: {ts("llmgw-node01", 0.2, 0.6)},
|
|
}}
|
|
code, body := get(t, nodeMux(busySource{}, h), "http://x/admin/nodes/llmgw-node01", nil)
|
|
if code != http.StatusOK {
|
|
t.Fatalf("status %d", code)
|
|
}
|
|
for _, want := range []string{
|
|
"62%", "CPU · 8 cores", "RAM in use", "3.25",
|
|
// The engine and its share: four of eight cores is half the machine.
|
|
"<td>served</td>", "4.0 cores", "width: 50%",
|
|
`hx-get="/admin/nodes/llmgw-node01"`,
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("missing %q", want)
|
|
}
|
|
}
|
|
// Every chart is about this node, and nothing else.
|
|
for _, q := range h.asked {
|
|
if !strings.Contains(q, `"llmgw-node01"`) && !strings.Contains(q, `served-1`) {
|
|
t.Errorf("a query not scoped to the node: %s", q)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAnUnknownNodeIsNotFound(t *testing.T) {
|
|
h := &fakeHistory{}
|
|
code, _ := get(t, nodeMux(busySource{}, h), "http://x/admin/nodes/nope", nil)
|
|
if code != http.StatusNotFound {
|
|
t.Errorf("status %d", code)
|
|
}
|
|
// Nothing was asked of the metrics store for a name nobody knows.
|
|
if len(h.asked) != 0 {
|
|
t.Errorf("queried for an unknown node: %v", h.asked)
|
|
}
|
|
}
|
|
|
|
func TestANodeNotMeasuredLatelySaysSoRatherThanZero(t *testing.T) {
|
|
_, body := get(t, nodeMux(fakeSource{}, nil), "http://x/admin/nodes/llmgw-node01", nil)
|
|
if !strings.Contains(body, "not measured lately") {
|
|
t.Error("an unmeasured node should say so")
|
|
}
|
|
if !strings.Contains(body, "No metrics store is configured") {
|
|
t.Error("without a metrics store the page should say where history comes from")
|
|
}
|
|
}
|
|
|
|
func TestANodesPageShowsItsFabricPorts(t *testing.T) {
|
|
_, body := get(t, nodeMux(busySource{}, nil), "http://x/admin/nodes/llmgw-node01", nil)
|
|
for _, want := range []string{
|
|
"<h2>Fabric</h2>", "On ib-a ·", "<code>mlx5_0/1</code>", "400 Gb/s", "4.0 GiB",
|
|
`<td class="bad">DOWN <span class="note">Polling</span></td>`, "link_downed 7",
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("missing %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// topoSource is busySource with a mapped fabric.
|
|
type topoSource struct{ busySource }
|
|
|
|
func (topoSource) Topologies() []adminapi.TopologyView {
|
|
return []adminapi.TopologyView{{ID: "sm-0x01", Key: "sm:0x01", Label: "fabric of subnet manager 0x01", Declared: []string{"ib-a"}}}
|
|
}
|
|
|
|
func (s topoSource) Topology(id string) (adminapi.TopologyView, bool) {
|
|
if id != "sm-0x01" {
|
|
return adminapi.TopologyView{}, false
|
|
}
|
|
t := s.Topologies()[0]
|
|
t.Map = &topology.View{Tiers: 2, Switches: []topology.SwitchView{{GUID: "0xs1", Name: "leaf-01</script><script>alert(1)", Tier: 1}}}
|
|
return t, true
|
|
}
|
|
|
|
func TestAFabricsPageCarriesItsMapSafely(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /admin/fabrics", FabricsPage(topoSource{}, false, "v1"))
|
|
mux.HandleFunc("GET /admin/fabrics/{id}", FabricsPage(topoSource{}, false, "v1"))
|
|
mux.HandleFunc("GET /admin/fabrics/{id}/topology.json", FabricTopologyJSON(topoSource{}))
|
|
|
|
code, body := get(t, mux, "http://x/admin/fabrics", nil)
|
|
if code != 200 {
|
|
t.Fatalf("status %d", code)
|
|
}
|
|
for _, want := range []string{`id="fabric-map"`, `data-src="/admin/fabrics/sm-0x01/topology.json"`, "fabric.js?v=", `id="fabric-data"`} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("missing %q", want)
|
|
}
|
|
}
|
|
// A switch named to break out of the script it is carried in cannot.
|
|
if strings.Contains(body, "</script><script>alert(1)") {
|
|
t.Error("a switch description closed the data script")
|
|
}
|
|
if code, _ := get(t, mux, "http://x/admin/fabrics/nope", nil); code != 404 {
|
|
t.Errorf("unknown fabric: %d", code)
|
|
}
|
|
if code, body := get(t, mux, "http://x/admin/fabrics/sm-0x01/topology.json", nil); code != 200 || !strings.Contains(body, `"tiers":2`) {
|
|
t.Errorf("json: %d %s", code, body)
|
|
}
|
|
}
|
|
|
|
type nvlSource struct{ busySource }
|
|
|
|
func (nvlSource) NVLink() nvlink.Report {
|
|
return nvlink.Report{
|
|
Domains: []nvlink.Domain{{Key: "76b1ad76-7c24/7", ClusterUUID: "76b1ad76-7c24", CliqueID: "7", GPUs: 8, Joined: 7, Usable: 1,
|
|
Members: []nvlink.Member{
|
|
{Node: "t1", Usable: true, IMEX: "active", IMEXChannels: 1, GPUs: []nvlink.GPU{{Index: 0, Joined: true, State: "Completed"}}},
|
|
{Node: "t2", IMEX: "inactive", GPUs: []nvlink.GPU{{Index: 3, State: "In Progress"}}},
|
|
}}},
|
|
Problems: []nvlink.Problem{{Level: nvlink.Fail, Domain: "76b1ad76-7c24/7", Message: "t2: nvidia-imex is inactive"}},
|
|
}
|
|
}
|
|
|
|
func TestTheNVLinkPageDrawsEachDomainAsARack(t *testing.T) {
|
|
_, body := get(t, NVLinkPage(nvlSource{}, false, "v1"), "http://x/admin/nvlink", nil)
|
|
for _, want := range []string{`class="rack troubled"`, "76b1ad76·7", "1/2</b><span>trays usable", `class="chip joined"`,
|
|
`class="chip lost"`, `class="tray out"`, "t2: nvidia-imex is inactive", `class="imex up"`} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("missing %q", want)
|
|
}
|
|
}
|
|
// And the cluster page points at it.
|
|
_, body = get(t, Dashboard(nvlSource{}, fakeUsage{}, "v1", time.Second, false, NavExtras{}, nil), "http://x/admin", nil)
|
|
if !strings.Contains(body, "see the racks") {
|
|
t.Error("the cluster page does not lead to the racks")
|
|
}
|
|
}
|