Files
bee/audit/internal/platform/nvidia_topology_groups_test.go
mchusandClaude Sonnet 5 8a91f0f783 fix(webui): repair broken scenario Run button onclick, dedupe build.sh overlay staging
- page_scenario.go: onclick built via JSON.stringify() embedded raw double
  quotes inside a double-quoted HTML attribute, truncating the attribute so
  the click handler never compiled; pass the name through an escaped
  data-scenario-name attribute instead.
- build.sh: overlay staging rsyncs (OVERLAY_DIR->stage, stage->includes.chroot)
  ran without --delete, so a scenario removed from the repo (a9924b0) stayed
  baked into every ISO built from the persistent stage cache since — the
  "second script" in the Scenario page's list.
- blackbox: rewritten around a deterministic local zip + incremental
  patch-the-changed-suffix onto removable media, instead of walking/copying
  ~90 files through a synchronous ntfs-3g FUSE mount every cycle. journalctl
  captures are now "--since last sync" (were "--since boot", growing with
  uptime) and metrics.db is excluded (was copied whole every cycle).
- scenario: nvbandwidth-acs-ab now escalates GPU count (same-socket pair,
  other socket's pair, one cross-socket pair, all GPUs) under each ACS state
  instead of always running all 6 GPUs at once, using a new `bee
  gpu-bandwidth-groups` subcommand that discovers socket layout from
  `nvidia-smi topo -m` at runtime — gpu_indices is host-specific, so this
  can't be baked into the scenario file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 18:11:24 +03:00

86 lines
3.4 KiB
Go

package platform
import (
"reflect"
"testing"
)
// realTopoTwoSocketSixGPU is a real "nvidia-smi topo -m" capture (CG480-S6053,
// 6 GPUs across 2 sockets: GPU0-3 on one, GPU4-5 on the other), ANSI escapes
// included as nvidia-smi actually emits them in the header row.
const realTopoTwoSocketSixGPU = "\t\x1b[4mGPU0\tGPU1\tGPU2\tGPU3\tGPU4\tGPU5\tNIC0\tNIC1\tNIC2\tNIC3\tCPU Affinity\tNUMA Affinity\tGPU NUMA ID\x1b[0m\n" +
"GPU0\t X \tPIX\tNODE\tNODE\tSYS\tSYS\tNODE\tNODE\tPIX\tPIX\t0-95,192-287\t0\t\tN/A\n" +
"GPU1\tPIX\t X \tNODE\tNODE\tSYS\tSYS\tNODE\tNODE\tPIX\tPIX\t0-95,192-287\t0\t\tN/A\n" +
"GPU2\tNODE\tNODE\t X \tPIX\tSYS\tSYS\tPIX\tPIX\tNODE\tNODE\t0-95,192-287\t0\t\tN/A\n" +
"GPU3\tNODE\tNODE\tPIX\t X \tSYS\tSYS\tPIX\tPIX\tNODE\tNODE\t0-95,192-287\t0\t\tN/A\n" +
"GPU4\tSYS\tSYS\tSYS\tSYS\t X \tPIX\tSYS\tSYS\tSYS\tSYS\t96-191,288-383\t1\t\tN/A\n" +
"GPU5\tSYS\tSYS\tSYS\tSYS\tPIX\t X \tSYS\tSYS\tSYS\tSYS\t96-191,288-383\t1\t\tN/A\n"
// realTopoSingleSocketFourGPU simulates a single-socket host (all GPUs share
// one CPU Affinity range) — no cross-socket stage should be produced.
const realTopoSingleSocketFourGPU = "GPU0\tGPU1\tGPU2\tGPU3\tCPU Affinity\tNUMA Affinity\n" +
"GPU0\t X \tPIX\tNODE\tNODE\t0-31\t0\n" +
"GPU1\tPIX\t X \tNODE\tNODE\t0-31\t0\n" +
"GPU2\tNODE\tNODE\t X \tPIX\t0-31\t0\n" +
"GPU3\tNODE\tNODE\tPIX\t X \t0-31\t0\n"
func TestParseNvidiaSocketGroupsTwoSockets(t *testing.T) {
groups := ParseNvidiaSocketGroups(realTopoTwoSocketSixGPU)
if len(groups) != 2 {
t.Fatalf("groups=%+v, want 2", groups)
}
if !reflect.DeepEqual(groups[0].GPUIndices, []int{0, 1, 2, 3}) {
t.Fatalf("group0=%v, want [0 1 2 3]", groups[0].GPUIndices)
}
if !reflect.DeepEqual(groups[1].GPUIndices, []int{4, 5}) {
t.Fatalf("group1=%v, want [4 5]", groups[1].GPUIndices)
}
if groups[0].CPUAffinity == groups[1].CPUAffinity {
t.Fatalf("expected distinct CPU affinities, got %q for both", groups[0].CPUAffinity)
}
}
func TestNvidiaProgressiveBandwidthGroupsTwoSockets(t *testing.T) {
socketGroups := ParseNvidiaSocketGroups(realTopoTwoSocketSixGPU)
stages := NvidiaProgressiveBandwidthGroups(socketGroups)
want := []NvidiaBandwidthTestGroup{
{Label: "same-socket-1", GPUIndices: []int{0, 1}},
{Label: "same-socket-2", GPUIndices: []int{4, 5}},
{Label: "cross-socket", GPUIndices: []int{0, 4}},
{Label: "all", GPUIndices: []int{0, 1, 2, 3, 4, 5}},
}
if len(stages) != len(want) {
t.Fatalf("stages=%+v, want %+v", stages, want)
}
for i, w := range want {
if stages[i].Label != w.Label || !reflect.DeepEqual(stages[i].GPUIndices, w.GPUIndices) {
t.Fatalf("stage %d = %+v, want %+v", i, stages[i], w)
}
}
}
func TestNvidiaProgressiveBandwidthGroupsSingleSocket(t *testing.T) {
socketGroups := ParseNvidiaSocketGroups(realTopoSingleSocketFourGPU)
stages := NvidiaProgressiveBandwidthGroups(socketGroups)
want := []NvidiaBandwidthTestGroup{
{Label: "same-socket-1", GPUIndices: []int{0, 1}},
{Label: "all", GPUIndices: []int{0, 1, 2, 3}},
}
if len(stages) != len(want) {
t.Fatalf("stages=%+v, want %+v (no cross-socket stage on a single-socket host)", stages, want)
}
for i, w := range want {
if stages[i].Label != w.Label || !reflect.DeepEqual(stages[i].GPUIndices, w.GPUIndices) {
t.Fatalf("stage %d = %+v, want %+v", i, stages[i], w)
}
}
}
func TestParseNvidiaSocketGroupsEmptyOnGarbage(t *testing.T) {
if got := ParseNvidiaSocketGroups("not a topology matrix"); len(got) != 0 {
t.Fatalf("got %+v, want empty", got)
}
}