package platform import ( "bee/audit/internal/collector" "context" "fmt" "math" "os" "os/exec" "sort" "strconv" "strings" "time" ) func detectDCMIPartialCoverage(sp *BenchmarkServerPower) bool { if sp == nil || !sp.Available { return false } if sp.PSUInputIdleW > 0 && sp.IdleW > 0 { return sp.IdleW/sp.PSUInputIdleW < 0.7 } if sp.PSUInputLoadedW > 0 && sp.LoadedW > 0 { return sp.LoadedW/sp.PSUInputLoadedW < 0.7 } return false } // detectIPMISaturationFallback is the heuristic used when SDR PSU sensors are // absent. It analyses the power ramp: if 2+ of the last 3 per-step incremental // DCMI/GPU ratios fall below 25% of the first-step ratio, DCMI has likely // plateaued while GPU load continued to grow (saturation proxy). // Prefer detectDCMIPartialCoverage when SDR data is available. func detectIPMISaturationFallback(steps []NvidiaPowerBenchStep) bool { type pt struct{ incIPMI, incGPU float64 } var pts []pt for i := 1; i < len(steps); i++ { if steps[i].ServerDeltaW <= 0 || steps[i-1].ServerDeltaW <= 0 { continue } incIPMI := steps[i].ServerDeltaW - steps[i-1].ServerDeltaW incGPU := steps[i].TotalObservedPowerW - steps[i-1].TotalObservedPowerW if incGPU <= 0 { continue } pts = append(pts, pt{incIPMI, incGPU}) } if len(pts) < 3 { return false } refRatio := pts[0].incIPMI / pts[0].incGPU if refRatio <= 0 { return false } saturated := 0 for _, p := range pts[len(pts)-3:] { if p.incIPMI/p.incGPU < refRatio*0.25 { saturated++ } } return saturated >= 2 } // psuStatusSnapshot samples PSU health sensor states via // `ipmitool sdr type "Power Supply"`. Returns a map of sensor name → reading // string (e.g. "Presence detected", "Failure detected"). Returns nil when IPMI // is unavailable or no Power Supply entity sensors are present. func psuStatusSnapshot() map[string]string { out, err := exec.Command("ipmitool", "sdr", "type", "Power Supply").Output() if err != nil || len(out) == 0 { return nil } result := make(map[string]string) for _, line := range strings.Split(string(out), "\n") { parts := strings.Split(line, "|") if len(parts) < 5 { continue } name := strings.TrimSpace(parts[0]) reading := strings.TrimSpace(parts[4]) if name == "" { continue } result[name] = reading } return result } // diffPSUStatus compares PSU sensor snapshots taken before and after a test. // Returns human-readable fault strings for sensors that entered a fault state // during the test. Pre-existing faults (present in both snapshots) are excluded // so that only new anomalies caused by the test are reported. func diffPSUStatus(before, after map[string]string) []string { if len(after) == 0 { return nil } isFault := func(s string) bool { lower := strings.ToLower(s) return strings.Contains(lower, "failure") || strings.Contains(lower, "fault") || strings.Contains(lower, "warning") || strings.Contains(lower, "predictive") || strings.Contains(lower, "absent") || strings.Contains(lower, "ac lost") } var issues []string for name, afterReading := range after { if !isFault(afterReading) { continue } if beforeReading, had := before[name]; had && isFault(beforeReading) { continue // pre-existing fault, not caused by this test } if prev, had := before[name]; had { issues = append(issues, fmt.Sprintf("%s: changed from %q to %q during test", name, prev, afterReading)) } else { issues = append(issues, fmt.Sprintf("%s: %s (appeared after test start)", name, afterReading)) } } sort.Strings(issues) return issues } // sdrPowerSnapshot holds per-source power sums from a single `ipmitool sdr` read. type sdrPowerSnapshot struct { PSUInW float64 // sum of PSU AC input across all slots PSUOutW float64 // sum of PSU DC output across all slots GPUSlotW float64 // sum of GPU slot/GPU power sensors // Per-slot PSU data from collector.PSUSlotsFromSDR — same slot keys as // audit HardwarePowerSupply.Slot (0-based strings). PSUSlots map[string]BenchmarkPSUSlotPower SkippedSensors []string // sensors rejected during self-healing } type benchmarkSDRSeriesSummary struct { PSUInW float64 PSUOutW float64 GPUSlotW float64 PSUSlots map[string]BenchmarkPSUSlotPower Samples int SkippedSensors []string } // sdrSensor is a name+watts pair used for GPU slot self-healing filtering. type sdrSensor struct { name string watts float64 } // filterSensorGroup removes physically implausible readings from a group. // Hard bounds: 0 < watts ≤ maxPerSensorW. Within groups of 2+ sensors, // values more than 5× the group median are rejected as stuck/fault sensors. func filterSensorGroup(sensors []sdrSensor, maxPerSensorW float64) (valid []sdrSensor, skipped []string) { var inBounds []sdrSensor for _, s := range sensors { if s.watts <= 0 || s.watts > maxPerSensorW { skipped = append(skipped, fmt.Sprintf("%s (%.0f W: out of range 0–%.0f W)", s.name, s.watts, maxPerSensorW)) } else { inBounds = append(inBounds, s) } } if len(inBounds) < 2 { return inBounds, skipped } vals := make([]float64, len(inBounds)) for i, s := range inBounds { vals[i] = s.watts } sort.Float64s(vals) mid := len(vals) / 2 var median float64 if len(vals)%2 == 0 { median = (vals[mid-1] + vals[mid]) / 2 } else { median = vals[mid] } for _, s := range inBounds { if median > 0 && s.watts > median*5 { skipped = append(skipped, fmt.Sprintf("%s (%.0f W: >5× median %.0f W, likely sensor fault)", s.name, s.watts, median)) } else { valid = append(valid, s) } } return valid, skipped } // sampleIPMISDRPowerSensors reads power sensors from `ipmitool sdr` in a single // invocation and returns self-healed grouped sums. // // PSU identification delegates to collector.PSUSlotsFromSDR which uses the same // slot-detection regexes as the hardware audit (PSU1_POWER_IN, PSU1_PIN, PS1 POut, // Power1…). Self-healing: bounds checking + 5× median outlier rejection. // // GPU slot sensors (GPU_POWER_SLOTx, GPU1 Power, …) are classified separately // since the audit collector does not track GPU PCIe slot power. func sampleIPMISDRPowerSensors() sdrPowerSnapshot { raw, err := exec.Command("ipmitool", "sdr").Output() if err != nil || len(raw) == 0 { return sdrPowerSnapshot{} } sdrStr := string(raw) var snap sdrPowerSnapshot // ── PSU data via audit collector ───────────────────────────────────────── // collector.PSUSlotsFromSDR handles all vendor naming variants and applies // bounds checking inside parseBoundedFloat (0 < w ≤ 6000 W). collectorSlots := collector.PSUSlotsFromSDR(sdrStr) // Convert to benchmark type and apply cross-slot median filtering. var psuInSensors, psuOutSensors []sdrSensor for slotKey, sp := range collectorSlots { bsp := BenchmarkPSUSlotPower{Status: sp.Status} if sp.InputW != nil { bsp.InputW = sp.InputW psuInSensors = append(psuInSensors, sdrSensor{name: "PSU-slot-" + slotKey, watts: *sp.InputW}) } if sp.OutputW != nil { bsp.OutputW = sp.OutputW psuOutSensors = append(psuOutSensors, sdrSensor{name: "PSU-slot-" + slotKey + "-out", watts: *sp.OutputW}) } if snap.PSUSlots == nil { snap.PSUSlots = make(map[string]BenchmarkPSUSlotPower) } snap.PSUSlots[slotKey] = bsp } // Apply cross-slot outlier filter and sum. validIn, skIn := filterSensorGroup(psuInSensors, 6000) for _, s := range validIn { snap.PSUInW += s.watts } snap.SkippedSensors = append(snap.SkippedSensors, skIn...) validOut, skOut := filterSensorGroup(psuOutSensors, 6000) for _, s := range validOut { snap.PSUOutW += s.watts } snap.SkippedSensors = append(snap.SkippedSensors, skOut...) // ── GPU slot sensors ───────────────────────────────────────────────────── // collector does not track GPU PCIe slot power; classify here. // Matches: GPU_POWER_SLOTx (MSI), GPU1 Power (xFusion), GPU_PWR_x (generic). var gpuSensors []sdrSensor for _, line := range strings.Split(sdrStr, "\n") { parts := strings.Split(line, "|") if len(parts) < 2 { continue } name := strings.TrimSpace(parts[0]) nameLower := strings.ToLower(name) if !strings.Contains(nameLower, "gpu") { continue } if !strings.Contains(nameLower, "slot") && !strings.Contains(nameLower, "power") && !strings.Contains(nameLower, "pwr") { continue } var w float64 if n, _ := fmt.Sscanf(strings.TrimSpace(parts[1]), "%f Watts", &w); n != 1 { continue } gpuSensors = append(gpuSensors, sdrSensor{name: name, watts: w}) } validGPU, skGPU := filterSensorGroup(gpuSensors, 2000) for _, s := range validGPU { snap.GPUSlotW += s.watts } snap.SkippedSensors = append(snap.SkippedSensors, skGPU...) return snap } func startIPMISDRSampler(stopCh <-chan struct{}, intervalSec int) <-chan []sdrPowerSnapshot { if intervalSec <= 0 { intervalSec = benchmarkPowerAutotuneSampleInterval } ch := make(chan []sdrPowerSnapshot, 1) go func() { defer close(ch) var samples []sdrPowerSnapshot record := func() { snap := sampleIPMISDRPowerSensors() if snap.PSUInW <= 0 && snap.PSUOutW <= 0 && snap.GPUSlotW <= 0 && len(snap.PSUSlots) == 0 { return } samples = append(samples, snap) } record() ticker := time.NewTicker(time.Duration(intervalSec) * time.Second) defer ticker.Stop() for { select { case <-stopCh: ch <- samples return case <-ticker.C: record() } } }() return ch } func summarizeSDRPowerSeries(samples []sdrPowerSnapshot) benchmarkSDRSeriesSummary { var summary benchmarkSDRSeriesSummary if len(samples) == 0 { return summary } type slotAggregate struct { inputs []float64 outputs []float64 status string } slotAgg := make(map[string]*slotAggregate) skippedSet := make(map[string]struct{}) var inputTotals []float64 var outputTotals []float64 var gpuSlotTotals []float64 for _, sample := range samples { if sample.PSUInW > 0 { inputTotals = append(inputTotals, sample.PSUInW) } if sample.PSUOutW > 0 { outputTotals = append(outputTotals, sample.PSUOutW) } if sample.GPUSlotW > 0 { gpuSlotTotals = append(gpuSlotTotals, sample.GPUSlotW) } for _, skipped := range sample.SkippedSensors { if skipped != "" { skippedSet[skipped] = struct{}{} } } for slot, reading := range sample.PSUSlots { agg := slotAgg[slot] if agg == nil { agg = &slotAggregate{} slotAgg[slot] = agg } if reading.InputW != nil && *reading.InputW > 0 { agg.inputs = append(agg.inputs, *reading.InputW) } if reading.OutputW != nil && *reading.OutputW > 0 { agg.outputs = append(agg.outputs, *reading.OutputW) } switch { case reading.Status == "": case agg.status == "": agg.status = reading.Status case agg.status == "OK" && reading.Status != "OK": agg.status = reading.Status } } } summary.PSUInW = benchmarkMean(inputTotals) summary.PSUOutW = benchmarkMean(outputTotals) summary.GPUSlotW = benchmarkMean(gpuSlotTotals) summary.Samples = len(samples) if len(slotAgg) > 0 { summary.PSUSlots = make(map[string]BenchmarkPSUSlotPower, len(slotAgg)) for slot, agg := range slotAgg { reading := BenchmarkPSUSlotPower{Status: agg.status} if mean := benchmarkMean(agg.inputs); mean > 0 { v := mean reading.InputW = &v } if mean := benchmarkMean(agg.outputs); mean > 0 { v := mean reading.OutputW = &v } summary.PSUSlots[slot] = reading } } if len(skippedSet) > 0 { summary.SkippedSensors = make([]string, 0, len(skippedSet)) for skipped := range skippedSet { summary.SkippedSensors = append(summary.SkippedSensors, skipped) } sort.Strings(summary.SkippedSensors) } return summary } // queryIPMIServerPowerW reads the current server power draw via ipmitool dcmi. // Returns 0 and an error if IPMI is unavailable or the output cannot be parsed. func queryIPMIServerPowerW() (float64, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "ipmitool", "dcmi", "power", "reading") out, err := cmd.Output() if err != nil { return 0, fmt.Errorf("ipmitool dcmi power reading: %w", err) } if w := parseDCMIPowerReading(string(out)); w > 0 { return w, nil } return 0, fmt.Errorf("could not parse ipmitool dcmi power reading output") } // characterizeServerPower computes BenchmarkServerPower from idle and loaded // samples plus the GPU-reported average power during steady state. func characterizeServerPower(idleW, loadedW, gpuReportedSumW float64, source string, available bool) *BenchmarkServerPower { sp := &BenchmarkServerPower{ Available: available, Source: normalizeBenchmarkPowerSource(source), SampleIntervalSec: benchmarkPowerAutotuneSampleInterval, } if !available { sp.Notes = append(sp.Notes, "IPMI power reading unavailable; server-side power characterization skipped") return sp } sp.IdleW = idleW sp.LoadedW = loadedW sp.DeltaW = loadedW - idleW sp.GPUReportedSumW = gpuReportedSumW if gpuReportedSumW > 0 && sp.DeltaW > 0 { sp.ReportingRatio = sp.DeltaW / gpuReportedSumW } return sp } // readServerModel returns the DMI system product name (e.g. "SuperMicro SYS-421GE-TNRT"). // Returns empty string if unavailable (non-Linux or missing DMI entry). func readServerModel() string { data, err := os.ReadFile("/sys/class/dmi/id/product_name") if err != nil { return "" } return strings.TrimSpace(string(data)) } // filterRowsByGPU returns only the metric rows for a specific GPU index. func filterRowsByGPU(rows []GPUMetricRow, gpuIndex int) []GPUMetricRow { var out []GPUMetricRow for _, r := range rows { if r.GPUIndex == gpuIndex { out = append(out, r) } } return out } // parseBenchmarkBurnLogByGPU splits a multi-GPU bee-gpu-burn output by [gpu N] prefix // and returns a per-GPU parse result map. func parseBenchmarkBurnLogByGPU(raw string) map[int]benchmarkBurnParseResult { gpuLines := make(map[int][]string) for _, line := range strings.Split(strings.ReplaceAll(raw, "\r\n", "\n"), "\n") { line = strings.TrimSpace(line) if !strings.HasPrefix(line, "[gpu ") { continue } end := strings.Index(line, "] ") if end < 0 { continue } gpuIdx, err := strconv.Atoi(strings.TrimSpace(line[5:end])) if err != nil { continue } gpuLines[gpuIdx] = append(gpuLines[gpuIdx], line[end+2:]) } results := make(map[int]benchmarkBurnParseResult, len(gpuLines)) for gpuIdx, lines := range gpuLines { // Lines are already stripped of the [gpu N] prefix; parseBenchmarkBurnLog // calls stripBenchmarkPrefix which is a no-op on already-stripped lines. results[gpuIdx] = parseBenchmarkBurnLog(strings.Join(lines, "\n")) } return results } // runNvidiaBenchmarkParallel runs warmup and steady compute on all selected GPUs // simultaneously using a single bee-gpu-burn invocation per phase. func runNvidiaBenchmarkParallel( ctx context.Context, verboseLog, runDir string, selected []int, infoByIndex map[int]benchmarkGPUInfo, opts NvidiaBenchmarkOptions, spec benchmarkProfileSpec, logFunc func(string), result *NvidiaBenchmarkResult, calibByIndex map[int]benchmarkPowerCalibrationResult, serverIdleW *float64, serverLoadedWSum *float64, serverIdleOK *bool, serverLoadedOK *bool, serverLoadedSamples *int, allMetricRows *[]GPUMetricRow, metricTimelineSec *float64, gpuBurnLog string, ) { allDevices := joinIndexList(selected) // Build per-GPU result stubs. gpuResults := make(map[int]*BenchmarkGPUResult, len(selected)) for _, idx := range selected { r := &BenchmarkGPUResult{Index: idx, Status: "FAILED"} if info, ok := infoByIndex[idx]; ok { populateBenchmarkGPUInfo(r, info) } if calib, ok := calibByIndex[idx]; ok { r.CalibratedPeakPowerW = calib.Summary.P95PowerW r.CalibratedPeakTempC = calib.Summary.P95TempC r.PowerCalibrationTries = calib.Attempts r.PowerLimitDerated = calib.Derated r.Notes = append(r.Notes, calib.Notes...) if calib.CoolingWarning != "" { r.CoolingWarning = calib.CoolingWarning } } if norm := findBenchmarkNormalization(result.Normalization.GPUs, idx); norm != nil { r.LockedGraphicsClockMHz = norm.GPUClockLockMHz r.LockedMemoryClockMHz = norm.MemoryClockLockMHz } gpuResults[idx] = r } // Baseline: sample all GPUs together. baselineRows, err := collectBenchmarkSamples(ctx, spec.BaselineSec, selected) if err != nil && err != context.Canceled { for _, idx := range selected { gpuResults[idx].Notes = append(gpuResults[idx].Notes, "baseline sampling failed: "+err.Error()) } } for _, idx := range selected { perGPU := filterRowsByGPU(baselineRows, idx) gpuResults[idx].Baseline = summarizeBenchmarkTelemetry(perGPU) } appendBenchmarkMetrics(allMetricRows, baselineRows, "baseline", metricTimelineSec, float64(spec.BaselineSec)) // Sample server idle power once. if !*serverIdleOK { if w, ok := sampleBenchmarkPowerSourceSeries(ctx, opts.ServerPowerSource, maxInt(spec.BaselineSec, 10), benchmarkPowerAutotuneSampleInterval); ok { *serverIdleW = w *serverIdleOK = true logFunc(fmt.Sprintf("server idle power (%s): %.0f W", opts.ServerPowerSource, w)) } } // Warmup: all GPUs simultaneously. warmupCmd := []string{ "bee-gpu-burn", "--seconds", strconv.Itoa(spec.WarmupSec), "--size-mb", strconv.Itoa(opts.SizeMB), "--devices", allDevices, } logFunc(fmt.Sprintf("GPUs %s: parallel warmup (%ds)", allDevices, spec.WarmupSec)) warmupOut, warmupRows, warmupErr := runBenchmarkCommandWithMetrics(ctx, verboseLog, "gpu-all-warmup.log", warmupCmd, nil, selected, logFunc) appendBenchmarkMetrics(allMetricRows, warmupRows, "warmup", metricTimelineSec, float64(spec.WarmupSec)) appendBenchmarkStageLog(gpuBurnLog, "bee-gpu-burn", "warmup", warmupOut) if warmupErr != nil { for _, idx := range selected { gpuResults[idx].Notes = append(gpuResults[idx].Notes, "parallel warmup failed: "+warmupErr.Error()) } } warmupParseByGPU := parseBenchmarkBurnLogByGPU(string(warmupOut)) supportedPrecisions := append([]string(nil), benchmarkPrecisionPhases...) for _, idx := range selected { if pr, ok := warmupParseByGPU[idx]; ok && pr.ComputeCapability != "" { if gpuResults[idx].ComputeCapability == "" { gpuResults[idx].ComputeCapability = pr.ComputeCapability } if ccPrecisions := benchmarkSupportedPrecisions(pr.ComputeCapability); len(ccPrecisions) < len(supportedPrecisions) { supportedPrecisions = ccPrecisions } } } // Run synthetic precision phases and the combined steady phase as one // uninterrupted command so the GPUs stay hot between windows. eccBase := make(map[int]BenchmarkECCCounters, len(selected)) for _, idx := range selected { eccBase[idx], _ = queryECCCounters(idx) } planLabels, planPhases, basePhaseSec, mixedPhaseSec := buildBenchmarkSteadyPlan(spec, supportedPrecisions, func(label string) string { if label == "mixed" { return "steady" } return "gpu-all-steady-" + label }) planCmd := []string{ "bee-gpu-burn", "--seconds", strconv.Itoa(basePhaseSec), "--size-mb", strconv.Itoa(opts.SizeMB), "--devices", allDevices, "--precision-plan", strings.Join(planLabels, ","), "--precision-plan-seconds", benchmarkPlanDurationsCSV(planPhases), } logFunc(fmt.Sprintf("GPUs %s: uninterrupted precision plan (%d precision phases x %ds, mixed %ds)", allDevices, len(supportedPrecisions), basePhaseSec, mixedPhaseSec)) serverPowerStopCh := make(chan struct{}) serverPowerCh := startSelectedPowerSourceSampler(serverPowerStopCh, opts.ServerPowerSource, benchmarkPowerAutotuneSampleInterval) _, phaseRowsByStage, phaseLogs, planErr := runBenchmarkPlannedCommandWithMetrics(ctx, verboseLog, "gpu-all-precision-plan.log", planCmd, nil, selected, planPhases, logFunc) close(serverPowerStopCh) if serverPowerSamples := <-serverPowerCh; len(serverPowerSamples) > 0 { *serverLoadedWSum += benchmarkMean(serverPowerSamples) (*serverLoadedSamples)++ *serverLoadedOK = true logFunc(fmt.Sprintf("GPUs %s: server loaded power (%s avg): %.0f W", allDevices, opts.ServerPowerSource, benchmarkMean(serverPowerSamples))) } for _, phaseSpec := range planPhases { if rows := phaseRowsByStage[phaseSpec.MetricStage]; len(rows) > 0 { appendBenchmarkMetrics(allMetricRows, rows, phaseSpec.MetricStage, metricTimelineSec, float64(phaseSpec.DurationSec)) } appendBenchmarkStageLog(gpuBurnLog, "bee-gpu-burn", phaseSpec.MetricStage, phaseLogs[phaseSpec.PlanLabel]) } for _, prec := range supportedPrecisions { phaseLogName := "gpu-all-steady-" + prec phaseRows := phaseRowsByStage[phaseLogName] parseByGPU := parseBenchmarkBurnLogByGPU(string(phaseLogs[prec])) for _, idx := range selected { perGPU := filterRowsByGPU(phaseRows, idx) phase := BenchmarkPrecisionSteadyPhase{ Precision: prec, Status: "OK", Steady: summarizeBenchmarkTelemetry(perGPU), } if status, note := benchmarkPlannedPhaseStatus(phaseLogs[prec]); status != "OK" { phase.Status = status phase.Notes = note gpuResults[idx].PrecisionFailures = append(gpuResults[idx].PrecisionFailures, prec+":"+status) } if pr, ok := parseByGPU[idx]; ok { for _, p := range pr.Profiles { if p.Supported { phase.TeraOpsPerSec += p.TeraOpsPerSec phase.WeightedTeraOpsPerSec += p.WeightedTeraOpsPerSec } } } gpuResults[idx].PrecisionSteady = append(gpuResults[idx].PrecisionSteady, phase) } } // Snapshot throttle counters before steady. beforeThrottle := make(map[int]BenchmarkThrottleCounters, len(selected)) for _, idx := range selected { beforeThrottle[idx], _ = queryThrottleCounters(idx) } logFunc(fmt.Sprintf("GPUs %s: parallel steady compute (combined, %ds)", allDevices, mixedPhaseSec)) afterThrottle := make(map[int]BenchmarkThrottleCounters, len(selected)) for _, idx := range selected { afterThrottle[idx], _ = queryThrottleCounters(idx) } steadyRows := phaseRowsByStage["steady"] parseResults := parseBenchmarkBurnLogByGPU(string(phaseLogs["mixed"])) for _, idx := range selected { perGPU := filterRowsByGPU(steadyRows, idx) gpuResults[idx].Steady = summarizeBenchmarkTelemetry(perGPU) gpuResults[idx].Throttle = diffThrottleCounters(beforeThrottle[idx], afterThrottle[idx]) if eccFinal, err := queryECCCounters(idx); err == nil { gpuResults[idx].ECC = diffECCCounters(eccBase[idx], eccFinal) } if pr, ok := parseResults[idx]; ok { gpuResults[idx].ComputeCapability = pr.ComputeCapability gpuResults[idx].Backend = pr.Backend gpuResults[idx].PrecisionResults = pr.Profiles if pr.Fallback { gpuResults[idx].Notes = append(gpuResults[idx].Notes, "benchmark used driver PTX fallback; tensor throughput score is not comparable") } } if planErr != nil { gpuResults[idx].Notes = append(gpuResults[idx].Notes, "precision plan failed: "+planErr.Error()) } } // Cooldown: all GPUs together. if spec.CooldownSec > 0 { cooldownRows, err := collectBenchmarkSamples(ctx, spec.CooldownSec, selected) if err != nil && err != context.Canceled { for _, idx := range selected { gpuResults[idx].Notes = append(gpuResults[idx].Notes, "cooldown sampling failed: "+err.Error()) } } for _, idx := range selected { perGPU := filterRowsByGPU(cooldownRows, idx) gpuResults[idx].Cooldown = summarizeBenchmarkTelemetry(perGPU) } appendBenchmarkMetrics(allMetricRows, cooldownRows, "cooldown", metricTimelineSec, float64(spec.CooldownSec)) } // Score and finalize each GPU. for _, idx := range selected { r := gpuResults[idx] applyBenchmarkSteadyFallback(r) r.Scores = scoreBenchmarkGPUResult(*r) r.DegradationReasons = detectBenchmarkDegradationReasons(*r, result.Normalization.Status) pr := parseResults[idx] switch { case planErr != nil: r.Status = classifySATErrorStatus(phaseLogs["mixed"], planErr) case len(r.PrecisionFailures) > 0: r.Status = "PARTIAL" case pr.Fallback: r.Status = "PARTIAL" default: r.Status = "OK" } result.GPUs = append(result.GPUs, finalizeBenchmarkGPUResult(*r)) } } func populateBenchmarkGPUInfo(result *BenchmarkGPUResult, info benchmarkGPUInfo) { result.UUID = info.UUID result.Name = info.Name result.BusID = info.BusID result.VBIOS = info.VBIOS result.PowerLimitW = info.PowerLimitW result.MultiprocessorCount = info.MultiprocessorCount result.DefaultPowerLimitW = info.DefaultPowerLimitW result.ShutdownTempC = info.ShutdownTempC result.SlowdownTempC = info.SlowdownTempC result.MaxGraphicsClockMHz = info.MaxGraphicsClockMHz result.BaseGraphicsClockMHz = info.BaseGraphicsClockMHz result.MaxMemoryClockMHz = info.MaxMemoryClockMHz } // readBenchmarkHostConfig reads static CPU and memory configuration from // /proc/cpuinfo and /proc/meminfo. Returns nil if neither source is readable. func readBenchmarkHostConfig() *BenchmarkHostConfig { cfg := &BenchmarkHostConfig{} populated := false // Parse /proc/cpuinfo for CPU model, sockets, cores, threads. if data, err := os.ReadFile("/proc/cpuinfo"); err == nil { socketIDs := map[string]struct{}{} coresPerSocket := map[string]int{} var modelName string threads := 0 for _, line := range strings.Split(string(data), "\n") { kv := strings.SplitN(line, ":", 2) if len(kv) != 2 { continue } key := strings.TrimSpace(kv[0]) val := strings.TrimSpace(kv[1]) switch key { case "processor": threads++ case "model name": if modelName == "" { modelName = val } case "physical id": socketIDs[val] = struct{}{} case "cpu cores": // Overwrite per-socket core count (last wins per socket, but all // entries for the same socket report the same value). if physLine := ""; physLine == "" { // We accumulate below by treating cpu cores as a per-thread // field; sum by socket requires a two-pass approach. Use the // simpler approximation: totalCores = threads / (threads per core). _ = val } } } // Second pass: per-socket core count. var curSocket string for _, line := range strings.Split(string(data), "\n") { kv := strings.SplitN(line, ":", 2) if len(kv) != 2 { continue } key := strings.TrimSpace(kv[0]) val := strings.TrimSpace(kv[1]) switch key { case "physical id": curSocket = val case "cpu cores": if curSocket != "" { if _, seen := coresPerSocket[curSocket]; !seen { v, _ := strconv.Atoi(val) coresPerSocket[curSocket] = v } } } } totalCores := 0 for _, c := range coresPerSocket { totalCores += c } cfg.CPUModel = modelName cfg.CPUSockets = len(socketIDs) if cfg.CPUSockets == 0 && threads > 0 { cfg.CPUSockets = 1 } cfg.CPUCores = totalCores cfg.CPUThreads = threads if modelName != "" || threads > 0 { populated = true } } // Parse /proc/meminfo for total physical RAM. if data, err := os.ReadFile("/proc/meminfo"); err == nil { for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, "MemTotal:") { fields := strings.Fields(line) if len(fields) >= 2 { kb, _ := strconv.ParseUint(fields[1], 10, 64) cfg.MemTotalGiB = float64(kb) / (1024 * 1024) populated = true } break } } } if !populated { return nil } return cfg } // startCPULoadSampler starts a goroutine that samples host CPU load every // intervalSec seconds until stopCh is closed, then sends the collected // samples on the returned channel. func startCPULoadSampler(stopCh <-chan struct{}, intervalSec int) <-chan []float64 { ch := make(chan []float64, 1) go func() { var samples []float64 ticker := time.NewTicker(time.Duration(intervalSec) * time.Second) defer ticker.Stop() for { select { case <-stopCh: ch <- samples return case <-ticker.C: if pct := sampleCPULoadPct(); pct > 0 { samples = append(samples, pct) } } } }() return ch } // summarizeCPULoad computes stats over sampled CPU load values and assigns // a health status. func summarizeCPULoad(samples []float64) *BenchmarkCPULoad { if len(samples) == 0 { return nil } sorted := append([]float64(nil), samples...) sort.Float64s(sorted) var sum float64 for _, v := range sorted { sum += v } avg := sum / float64(len(sorted)) p95 := sorted[int(float64(len(sorted))*0.95)] max := sorted[len(sorted)-1] cl := &BenchmarkCPULoad{ AvgPct: math.Round(avg*10) / 10, MaxPct: math.Round(max*10) / 10, P95Pct: math.Round(p95*10) / 10, Samples: len(sorted), } // Compute standard deviation to detect instability. var variance float64 for _, v := range sorted { d := v - avg variance += d * d } stdDev := math.Sqrt(variance / float64(len(sorted))) switch { case avg > 20 || max > 40: cl.Status = "high" cl.Note = fmt.Sprintf("avg %.1f%% max %.1f%% — elevated host CPU load may interfere with GPU benchmark results", avg, max) case stdDev > 12: cl.Status = "unstable" cl.Note = fmt.Sprintf("avg %.1f%% stddev %.1f%% — host CPU load was erratic during the benchmark", avg, stdDev) default: cl.Status = "ok" } return cl } // runBenchmarkPowerCalibration runs the configured power-fit load for the supplied // GPU set and actively watches throttle counters. seedLimits, when provided, are treated as // the starting point for this calibration pass rather than as immutable fixed // limits. This matters during cumulative ramp-up: once an additional GPU is // introduced, every already-active GPU must be revalidated under the new // thermal state instead of assuming its previous single-step limit is still // valid. The selected reduced power limits stay active for the main benchmark // and are restored by the caller afterwards.