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>
This commit is contained in:
2026-07-29 18:11:24 +03:00
co-authored by Claude Sonnet 5
parent 1045fa9118
commit 8a91f0f783
12 changed files with 847 additions and 32 deletions
+72
View File
@@ -8,6 +8,7 @@ import (
"io"
"log/slog"
"os"
"os/exec"
"runtime/debug"
"strconv"
"strings"
@@ -88,6 +89,8 @@ func run(args []string, stdout, stderr io.Writer) (exitCode int) {
return runBenchmark(args[1:], stdout, stderr)
case "bee-worker":
return runBeeWorker(args[1:], stdout, stderr)
case "gpu-bandwidth-groups":
return runGPUBandwidthGroups(args[1:], stdout, stderr)
case "version", "--version", "-version":
fmt.Fprintln(stdout, Version)
return 0
@@ -111,6 +114,7 @@ func printRootUsage(w io.Writer) {
bee run <file.json|name> (bare name is looked up as scenarios/<name>.json on removable media)
bee benchmark nvidia [--profile standard|stability|overnight]
bee bee-worker --export-dir `+app.DefaultExportDir+` --task-id TASK-001
bee gpu-bandwidth-groups [--stage <label>]
bee version
bee help [command]`)
}
@@ -139,6 +143,8 @@ func runHelp(args []string, stdout, stderr io.Writer) int {
return runBenchmark([]string{"--help"}, stdout, stderr)
case "bee-worker":
return runBeeWorker([]string{"--help"}, stdout, stderr)
case "gpu-bandwidth-groups":
return runGPUBandwidthGroups([]string{"--help"}, stdout, stderr)
case "version":
fmt.Fprintln(stdout, "usage: bee version")
return 0
@@ -622,6 +628,72 @@ func runBenchmark(args []string, stdout, stderr io.Writer) int {
return 0
}
// runGPUBandwidthGroups discovers this host's GPU-to-socket layout from
// "nvidia-smi topo -m" and prints a progressive multi-GPU test plan: a pair
// within each socket, one cross-socket pair, then every GPU. gpu_indices in
// a scenario JSON file is host-specific (see scenarios/README.md) — this
// exists so a scenario can stage a GPU-count escalation ("same socket, other
// socket, cross-socket, all") without baking any host's specific GPU indices
// into the file at all.
//
// With --stage, prints just that stage's comma-joined GPU indices (for a
// scenario job's cmd to substitute directly, e.g.
// `dcgmi diag -r nvbandwidth -i "$(bee gpu-bandwidth-groups --stage all)"`)
// and exits nonzero with nothing on stdout if that stage doesn't apply on
// this host (e.g. only one socket present, so there's no "cross-socket"
// stage) — callers should treat that as "skip this stage here", not a
// hard failure.
// Without --stage, lists every applicable stage as "<label>\t<gpu,indices>".
func runGPUBandwidthGroups(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("gpu-bandwidth-groups", flag.ContinueOnError)
fs.SetOutput(stderr)
stage := fs.String("stage", "", "print only this stage's GPU indices (same-socket-1, same-socket-2, ..., cross-socket, all)")
fs.Usage = func() {
fmt.Fprintln(stderr, "usage: bee gpu-bandwidth-groups [--stage <label>]")
fs.PrintDefaults()
}
if err := fs.Parse(args); err != nil {
if err == flag.ErrHelp {
return 0
}
return 2
}
if fs.NArg() != 0 {
fs.Usage()
return 2
}
raw, err := exec.Command("nvidia-smi", "topo", "-m").CombinedOutput()
if err != nil {
fmt.Fprintf(stderr, "bee gpu-bandwidth-groups: nvidia-smi topo -m: %v\n", err)
return 1
}
groups := platform.NvidiaProgressiveBandwidthGroups(platform.ParseNvidiaSocketGroups(string(raw)))
if *stage == "" {
for _, g := range groups {
fmt.Fprintf(stdout, "%s\t%s\n", g.Label, joinInts(g.GPUIndices))
}
return 0
}
for _, g := range groups {
if g.Label == *stage {
fmt.Fprintln(stdout, joinInts(g.GPUIndices))
return 0
}
}
fmt.Fprintf(stderr, "bee gpu-bandwidth-groups: stage %q does not apply on this host\n", *stage)
return 1
}
func joinInts(vals []int) string {
parts := make([]string, len(vals))
for i, v := range vals {
parts[i] = strconv.Itoa(v)
}
return strings.Join(parts, ",")
}
func runBeeWorker(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("bee-worker", flag.ContinueOnError)
fs.SetOutput(stderr)