feat(webui): show GPU serial in NVIDIA selection pickers

Every GPU-selection picker (Load/SAT, Burn, Benchmark) now renders the
card serial: "GPU N — <model> · <mem> MiB · sn: <serial>". The serial is
monospace and the digits that differ across the listed GPUs (common
prefix/suffix stripped) are emphasised so operators can tell cards apart.

Row markup was duplicated across three pages (and twice within
page_validate.go). Consolidated into a single module,
internal/webui/gpu_picker.go: beeGpuPicker.render({...}) builds every row;
gpuPickerCSS/gpuPickerJS are injected once by layoutHead/renderPage. Pages
keep their own selection-note text and multi-GPU toggles but no longer
hand-build <label> markup.

ListNvidiaGPUs() adds the serial via nvidia-smi --query-gpu=...,serial;
N/A is normalised to empty. Serial flows to the client unchanged through
the existing /api/gpu/nvidia JSON response.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AudE3Q2nd9kxxVuxPKcTho
This commit is contained in:
Mikhail Chusavitin
2026-09-04 10:04:47 +03:00
co-authored by Claude Sonnet 5
parent f31971f440
commit e4f7519ef3
8 changed files with 112 additions and 68 deletions
+12 -3
View File
@@ -14,6 +14,7 @@ type NvidiaGPU struct {
Index int `json:"index"`
Name string `json:"name"`
MemoryMB int `json:"memory_mb"`
Serial string `json:"serial,omitempty"`
}
type NvidiaGPUStatus struct {
@@ -226,7 +227,7 @@ func amdStressJobs(seconds int, cfgFile string) []satJob {
// ListNvidiaGPUs returns GPUs visible to nvidia-smi.
func (s *System) ListNvidiaGPUs() ([]NvidiaGPU, error) {
out, err := exec.Command("nvidia-smi",
"--query-gpu=index,name,memory.total",
"--query-gpu=index,name,memory.total,serial",
"--format=csv,noheader,nounits").Output()
if err != nil {
return nil, fmt.Errorf("nvidia-smi: %w", err)
@@ -237,8 +238,8 @@ func (s *System) ListNvidiaGPUs() ([]NvidiaGPU, error) {
if line == "" {
continue
}
parts := strings.SplitN(line, ", ", 3)
if len(parts) != 3 {
parts := strings.SplitN(line, ", ", 4)
if len(parts) < 3 {
continue
}
idx, err := strconv.Atoi(strings.TrimSpace(parts[0]))
@@ -246,10 +247,18 @@ func (s *System) ListNvidiaGPUs() ([]NvidiaGPU, error) {
continue
}
memMB, _ := strconv.Atoi(strings.TrimSpace(parts[2]))
serial := ""
if len(parts) == 4 {
serial = strings.TrimSpace(parts[3])
if strings.EqualFold(serial, "N/A") || strings.EqualFold(serial, "[N/A]") {
serial = ""
}
}
gpus = append(gpus, NvidiaGPU{
Index: idx,
Name: strings.TrimSpace(parts[1]),
MemoryMB: memMB,
Serial: serial,
})
}
sort.Slice(gpus, func(i, j int) bool {