Unify benchmark exports and drop ASCII charts
This commit is contained in:
@@ -125,6 +125,8 @@ func (s *System) RunNvidiaBenchmark(ctx context.Context, baseDir string, opts Nv
|
||||
}
|
||||
|
||||
logFunc(fmt.Sprintf("NVIDIA benchmark profile=%s gpus=%s", spec.Name, joinIndexList(selected)))
|
||||
var metricRows []GPUMetricRow
|
||||
gpuBurnLog := filepath.Join(runDir, "gpu-burn.log")
|
||||
|
||||
// Server power characterization state — populated during per-GPU phases.
|
||||
var serverIdleW, serverLoadedWSum float64
|
||||
@@ -171,199 +173,202 @@ func (s *System) RunNvidiaBenchmark(ctx context.Context, baseDir string, opts Nv
|
||||
cpuSamplesCh := startCPULoadSampler(cpuStopCh, 10)
|
||||
|
||||
if opts.ParallelGPUs {
|
||||
runNvidiaBenchmarkParallel(ctx, verboseLog, runDir, selected, infoByIndex, opts, spec, logFunc, &result, calibPowerByIndex, &serverIdleW, &serverLoadedWSum, &serverIdleOK, &serverLoadedOK, &serverLoadedSamples)
|
||||
runNvidiaBenchmarkParallel(ctx, verboseLog, runDir, selected, infoByIndex, opts, spec, logFunc, &result, calibPowerByIndex, &serverIdleW, &serverLoadedWSum, &serverIdleOK, &serverLoadedOK, &serverLoadedSamples, &metricRows, gpuBurnLog)
|
||||
} else {
|
||||
|
||||
for _, idx := range selected {
|
||||
gpuResult := BenchmarkGPUResult{
|
||||
Index: idx,
|
||||
Status: "FAILED",
|
||||
}
|
||||
if info, ok := infoByIndex[idx]; ok {
|
||||
gpuResult.UUID = info.UUID
|
||||
gpuResult.Name = info.Name
|
||||
gpuResult.BusID = info.BusID
|
||||
gpuResult.VBIOS = info.VBIOS
|
||||
gpuResult.PowerLimitW = info.PowerLimitW
|
||||
gpuResult.MultiprocessorCount = info.MultiprocessorCount
|
||||
gpuResult.DefaultPowerLimitW = info.DefaultPowerLimitW
|
||||
gpuResult.MaxGraphicsClockMHz = info.MaxGraphicsClockMHz
|
||||
gpuResult.BaseGraphicsClockMHz = info.BaseGraphicsClockMHz
|
||||
gpuResult.MaxMemoryClockMHz = info.MaxMemoryClockMHz
|
||||
}
|
||||
if w, ok := calibPowerByIndex[idx]; ok && w > 0 {
|
||||
gpuResult.CalibratedPeakPowerW = w
|
||||
}
|
||||
if norm := findBenchmarkNormalization(result.Normalization.GPUs, idx); norm != nil {
|
||||
gpuResult.LockedGraphicsClockMHz = norm.GPUClockLockMHz
|
||||
gpuResult.LockedMemoryClockMHz = norm.MemoryClockLockMHz
|
||||
}
|
||||
|
||||
baselineRows, err := collectBenchmarkSamples(ctx, spec.BaselineSec, []int{idx})
|
||||
if err != nil && err != context.Canceled {
|
||||
gpuResult.Notes = append(gpuResult.Notes, "baseline sampling failed: "+err.Error())
|
||||
}
|
||||
gpuResult.Baseline = summarizeBenchmarkTelemetry(baselineRows)
|
||||
writeBenchmarkMetricsFiles(runDir, fmt.Sprintf("gpu-%d-baseline", idx), baselineRows)
|
||||
|
||||
// Sample server idle power once (first GPU only — server state is global).
|
||||
if !serverIdleOK {
|
||||
if w, ok := sampleIPMIPowerSeries(ctx, maxInt(spec.BaselineSec, 10)); ok {
|
||||
serverIdleW = w
|
||||
serverIdleOK = true
|
||||
logFunc(fmt.Sprintf("server idle power (IPMI): %.0f W", w))
|
||||
for _, idx := range selected {
|
||||
gpuResult := BenchmarkGPUResult{
|
||||
Index: idx,
|
||||
Status: "FAILED",
|
||||
}
|
||||
if info, ok := infoByIndex[idx]; ok {
|
||||
gpuResult.UUID = info.UUID
|
||||
gpuResult.Name = info.Name
|
||||
gpuResult.BusID = info.BusID
|
||||
gpuResult.VBIOS = info.VBIOS
|
||||
gpuResult.PowerLimitW = info.PowerLimitW
|
||||
gpuResult.MultiprocessorCount = info.MultiprocessorCount
|
||||
gpuResult.DefaultPowerLimitW = info.DefaultPowerLimitW
|
||||
gpuResult.MaxGraphicsClockMHz = info.MaxGraphicsClockMHz
|
||||
gpuResult.BaseGraphicsClockMHz = info.BaseGraphicsClockMHz
|
||||
gpuResult.MaxMemoryClockMHz = info.MaxMemoryClockMHz
|
||||
}
|
||||
if w, ok := calibPowerByIndex[idx]; ok && w > 0 {
|
||||
gpuResult.CalibratedPeakPowerW = w
|
||||
}
|
||||
if norm := findBenchmarkNormalization(result.Normalization.GPUs, idx); norm != nil {
|
||||
gpuResult.LockedGraphicsClockMHz = norm.GPUClockLockMHz
|
||||
gpuResult.LockedMemoryClockMHz = norm.MemoryClockLockMHz
|
||||
}
|
||||
}
|
||||
|
||||
warmupCmd := []string{
|
||||
"bee-gpu-burn",
|
||||
"--seconds", strconv.Itoa(spec.WarmupSec),
|
||||
"--size-mb", strconv.Itoa(opts.SizeMB),
|
||||
"--devices", strconv.Itoa(idx),
|
||||
}
|
||||
logFunc(fmt.Sprintf("GPU %d: warmup (%ds)", idx, spec.WarmupSec))
|
||||
warmupOut, _, warmupErr := runBenchmarkCommandWithMetrics(ctx, verboseLog, fmt.Sprintf("gpu-%d-warmup.log", idx), warmupCmd, nil, []int{idx}, runDir, fmt.Sprintf("gpu-%d-warmup", idx), logFunc)
|
||||
_ = os.WriteFile(filepath.Join(runDir, fmt.Sprintf("gpu-%d-warmup.log", idx)), warmupOut, 0644)
|
||||
if warmupErr != nil {
|
||||
gpuResult.Notes = append(gpuResult.Notes, "warmup failed: "+warmupErr.Error())
|
||||
result.GPUs = append(result.GPUs, finalizeBenchmarkGPUResult(gpuResult))
|
||||
continue
|
||||
}
|
||||
baselineRows, err := collectBenchmarkSamples(ctx, spec.BaselineSec, []int{idx})
|
||||
if err != nil && err != context.Canceled {
|
||||
gpuResult.Notes = append(gpuResult.Notes, "baseline sampling failed: "+err.Error())
|
||||
}
|
||||
gpuResult.Baseline = summarizeBenchmarkTelemetry(baselineRows)
|
||||
appendBenchmarkMetrics(&metricRows, baselineRows, fmt.Sprintf("gpu-%d-baseline", idx))
|
||||
|
||||
// ── Per-precision stability phases ────────────────────────────────────────
|
||||
// Run each precision category alone so PowerCVPct reflects genuine GPU
|
||||
// power stability, not kernel-mix variance.
|
||||
// Time budget: each phase gets steadySec/numPhases, minimum 60 s.
|
||||
// SteadySec is split equally across all precision phases + 1 combined slot.
|
||||
// Skipped phases (unsupported precision) are simply omitted; combined is fixed.
|
||||
totalSlots := len(benchmarkPrecisionPhases) + 1
|
||||
perPhaseSec := spec.SteadySec / totalSlots
|
||||
if perPhaseSec < 60 {
|
||||
perPhaseSec = 60
|
||||
}
|
||||
eccBase, _ := queryECCCounters(idx)
|
||||
for _, prec := range benchmarkPrecisionPhases {
|
||||
phaseCmd := []string{
|
||||
// Sample server idle power once (first GPU only — server state is global).
|
||||
if !serverIdleOK {
|
||||
if w, ok := sampleIPMIPowerSeries(ctx, maxInt(spec.BaselineSec, 10)); ok {
|
||||
serverIdleW = w
|
||||
serverIdleOK = true
|
||||
logFunc(fmt.Sprintf("server idle power (IPMI): %.0f W", w))
|
||||
}
|
||||
}
|
||||
|
||||
warmupCmd := []string{
|
||||
"bee-gpu-burn",
|
||||
"--seconds", strconv.Itoa(spec.WarmupSec),
|
||||
"--size-mb", strconv.Itoa(opts.SizeMB),
|
||||
"--devices", strconv.Itoa(idx),
|
||||
}
|
||||
logFunc(fmt.Sprintf("GPU %d: warmup (%ds)", idx, spec.WarmupSec))
|
||||
warmupOut, warmupRows, warmupErr := runBenchmarkCommandWithMetrics(ctx, verboseLog, fmt.Sprintf("gpu-%d-warmup.log", idx), warmupCmd, nil, []int{idx}, logFunc)
|
||||
appendBenchmarkMetrics(&metricRows, warmupRows, fmt.Sprintf("gpu-%d-warmup", idx))
|
||||
appendBenchmarkStageLog(gpuBurnLog, "bee-gpu-burn", fmt.Sprintf("gpu-%d-warmup", idx), warmupOut)
|
||||
if warmupErr != nil {
|
||||
gpuResult.Notes = append(gpuResult.Notes, "warmup failed: "+warmupErr.Error())
|
||||
result.GPUs = append(result.GPUs, finalizeBenchmarkGPUResult(gpuResult))
|
||||
continue
|
||||
}
|
||||
|
||||
// ── Per-precision stability phases ────────────────────────────────────────
|
||||
// Run each precision category alone so PowerCVPct reflects genuine GPU
|
||||
// power stability, not kernel-mix variance.
|
||||
// Time budget: each phase gets steadySec/numPhases, minimum 60 s.
|
||||
// SteadySec is split equally across all precision phases + 1 combined slot.
|
||||
// Skipped phases (unsupported precision) are simply omitted; combined is fixed.
|
||||
totalSlots := len(benchmarkPrecisionPhases) + 1
|
||||
perPhaseSec := spec.SteadySec / totalSlots
|
||||
if perPhaseSec < 60 {
|
||||
perPhaseSec = 60
|
||||
}
|
||||
eccBase, _ := queryECCCounters(idx)
|
||||
for _, prec := range benchmarkPrecisionPhases {
|
||||
phaseCmd := []string{
|
||||
"bee-gpu-burn",
|
||||
"--seconds", strconv.Itoa(perPhaseSec),
|
||||
"--size-mb", strconv.Itoa(opts.SizeMB),
|
||||
"--devices", strconv.Itoa(idx),
|
||||
"--precision", prec,
|
||||
}
|
||||
logFunc(fmt.Sprintf("GPU %d: %s stability phase (%ds)", idx, prec, perPhaseSec))
|
||||
phaseLogName := fmt.Sprintf("gpu-%d-steady-%s", idx, prec)
|
||||
eccBefore, _ := queryECCCounters(idx)
|
||||
phaseOut, phaseRows, phaseErr := runBenchmarkCommandWithMetrics(ctx, verboseLog, phaseLogName+".log", phaseCmd, nil, []int{idx}, logFunc)
|
||||
appendBenchmarkMetrics(&metricRows, phaseRows, phaseLogName)
|
||||
appendBenchmarkStageLog(gpuBurnLog, "bee-gpu-burn", phaseLogName, phaseOut)
|
||||
eccAfter, _ := queryECCCounters(idx)
|
||||
if phaseErr != nil || len(phaseRows) == 0 {
|
||||
continue
|
||||
}
|
||||
phase := BenchmarkPrecisionSteadyPhase{
|
||||
Precision: prec,
|
||||
Steady: summarizeBenchmarkTelemetry(phaseRows),
|
||||
ECC: diffECCCounters(eccBefore, eccAfter),
|
||||
}
|
||||
for _, p := range parseBenchmarkBurnLog(string(phaseOut)).Profiles {
|
||||
if p.Supported {
|
||||
phase.TeraOpsPerSec += p.TeraOpsPerSec
|
||||
phase.WeightedTeraOpsPerSec += p.WeightedTeraOpsPerSec
|
||||
}
|
||||
}
|
||||
gpuResult.PrecisionSteady = append(gpuResult.PrecisionSteady, phase)
|
||||
}
|
||||
|
||||
beforeThrottle, _ := queryThrottleCounters(idx)
|
||||
steadyCmd := []string{
|
||||
"bee-gpu-burn",
|
||||
"--seconds", strconv.Itoa(perPhaseSec),
|
||||
"--size-mb", strconv.Itoa(opts.SizeMB),
|
||||
"--devices", strconv.Itoa(idx),
|
||||
"--precision", prec,
|
||||
}
|
||||
logFunc(fmt.Sprintf("GPU %d: %s stability phase (%ds)", idx, prec, perPhaseSec))
|
||||
phaseLogName := fmt.Sprintf("gpu-%d-steady-%s", idx, prec)
|
||||
eccBefore, _ := queryECCCounters(idx)
|
||||
phaseOut, phaseRows, phaseErr := runBenchmarkCommandWithMetrics(ctx, verboseLog, phaseLogName+".log", phaseCmd, nil, []int{idx}, runDir, phaseLogName, logFunc)
|
||||
eccAfter, _ := queryECCCounters(idx)
|
||||
if phaseErr != nil || len(phaseRows) == 0 {
|
||||
continue
|
||||
}
|
||||
phase := BenchmarkPrecisionSteadyPhase{
|
||||
Precision: prec,
|
||||
Steady: summarizeBenchmarkTelemetry(phaseRows),
|
||||
ECC: diffECCCounters(eccBefore, eccAfter),
|
||||
}
|
||||
for _, p := range parseBenchmarkBurnLog(string(phaseOut)).Profiles {
|
||||
if p.Supported {
|
||||
phase.TeraOpsPerSec += p.TeraOpsPerSec
|
||||
phase.WeightedTeraOpsPerSec += p.WeightedTeraOpsPerSec
|
||||
}
|
||||
}
|
||||
gpuResult.PrecisionSteady = append(gpuResult.PrecisionSteady, phase)
|
||||
}
|
||||
logFunc(fmt.Sprintf("GPU %d: steady compute (combined, %ds)", idx, perPhaseSec))
|
||||
|
||||
beforeThrottle, _ := queryThrottleCounters(idx)
|
||||
steadyCmd := []string{
|
||||
"bee-gpu-burn",
|
||||
"--seconds", strconv.Itoa(perPhaseSec),
|
||||
"--size-mb", strconv.Itoa(opts.SizeMB),
|
||||
"--devices", strconv.Itoa(idx),
|
||||
}
|
||||
logFunc(fmt.Sprintf("GPU %d: steady compute (combined, %ds)", idx, perPhaseSec))
|
||||
|
||||
// Sample server power via IPMI in parallel with the steady phase.
|
||||
// We collect readings every 5s and average them.
|
||||
ipmiStopCh := make(chan struct{})
|
||||
ipmiResultCh := make(chan float64, 1)
|
||||
go func() {
|
||||
defer close(ipmiResultCh)
|
||||
var samples []float64
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
// First sample after a short warmup delay.
|
||||
select {
|
||||
case <-ipmiStopCh:
|
||||
return
|
||||
case <-time.After(15 * time.Second):
|
||||
}
|
||||
for {
|
||||
if w, err := queryIPMIServerPowerW(); err == nil {
|
||||
samples = append(samples, w)
|
||||
}
|
||||
// Sample server power via IPMI in parallel with the steady phase.
|
||||
// We collect readings every 5s and average them.
|
||||
ipmiStopCh := make(chan struct{})
|
||||
ipmiResultCh := make(chan float64, 1)
|
||||
go func() {
|
||||
defer close(ipmiResultCh)
|
||||
var samples []float64
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
// First sample after a short warmup delay.
|
||||
select {
|
||||
case <-ipmiStopCh:
|
||||
if len(samples) > 0 {
|
||||
var sum float64
|
||||
for _, w := range samples {
|
||||
sum += w
|
||||
}
|
||||
ipmiResultCh <- sum / float64(len(samples))
|
||||
}
|
||||
return
|
||||
case <-ticker.C:
|
||||
case <-time.After(15 * time.Second):
|
||||
}
|
||||
for {
|
||||
if w, err := queryIPMIServerPowerW(); err == nil {
|
||||
samples = append(samples, w)
|
||||
}
|
||||
select {
|
||||
case <-ipmiStopCh:
|
||||
if len(samples) > 0 {
|
||||
var sum float64
|
||||
for _, w := range samples {
|
||||
sum += w
|
||||
}
|
||||
ipmiResultCh <- sum / float64(len(samples))
|
||||
}
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
steadyOut, steadyRows, steadyErr := runBenchmarkCommandWithMetrics(ctx, verboseLog, fmt.Sprintf("gpu-%d-steady.log", idx), steadyCmd, nil, []int{idx}, logFunc)
|
||||
appendBenchmarkMetrics(&metricRows, steadyRows, fmt.Sprintf("gpu-%d-steady", idx))
|
||||
appendBenchmarkStageLog(gpuBurnLog, "bee-gpu-burn", fmt.Sprintf("gpu-%d-steady", idx), steadyOut)
|
||||
close(ipmiStopCh)
|
||||
if loadedW, ok := <-ipmiResultCh; ok {
|
||||
serverLoadedWSum += loadedW
|
||||
serverLoadedSamples++
|
||||
serverLoadedOK = true
|
||||
logFunc(fmt.Sprintf("GPU %d: server loaded power (IPMI): %.0f W", idx, loadedW))
|
||||
}
|
||||
afterThrottle, _ := queryThrottleCounters(idx)
|
||||
if steadyErr != nil {
|
||||
gpuResult.Notes = append(gpuResult.Notes, "steady compute failed: "+steadyErr.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
steadyOut, steadyRows, steadyErr := runBenchmarkCommandWithMetrics(ctx, verboseLog, fmt.Sprintf("gpu-%d-steady.log", idx), steadyCmd, nil, []int{idx}, runDir, fmt.Sprintf("gpu-%d-steady", idx), logFunc)
|
||||
close(ipmiStopCh)
|
||||
if loadedW, ok := <-ipmiResultCh; ok {
|
||||
serverLoadedWSum += loadedW
|
||||
serverLoadedSamples++
|
||||
serverLoadedOK = true
|
||||
logFunc(fmt.Sprintf("GPU %d: server loaded power (IPMI): %.0f W", idx, loadedW))
|
||||
parseResult := parseBenchmarkBurnLog(string(steadyOut))
|
||||
gpuResult.ComputeCapability = parseResult.ComputeCapability
|
||||
gpuResult.Backend = parseResult.Backend
|
||||
gpuResult.PrecisionResults = parseResult.Profiles
|
||||
if parseResult.Fallback {
|
||||
gpuResult.Notes = append(gpuResult.Notes, "benchmark used driver PTX fallback; tensor throughput score is not comparable")
|
||||
}
|
||||
|
||||
gpuResult.Steady = summarizeBenchmarkTelemetry(steadyRows)
|
||||
gpuResult.Throttle = diffThrottleCounters(beforeThrottle, afterThrottle)
|
||||
if eccFinal, err := queryECCCounters(idx); err == nil {
|
||||
gpuResult.ECC = diffECCCounters(eccBase, eccFinal)
|
||||
}
|
||||
|
||||
cooldownRows, err := collectBenchmarkSamples(ctx, spec.CooldownSec, []int{idx})
|
||||
if err != nil && err != context.Canceled {
|
||||
gpuResult.Notes = append(gpuResult.Notes, "cooldown sampling failed: "+err.Error())
|
||||
}
|
||||
gpuResult.Cooldown = summarizeBenchmarkTelemetry(cooldownRows)
|
||||
appendBenchmarkMetrics(&metricRows, cooldownRows, fmt.Sprintf("gpu-%d-cooldown", idx))
|
||||
|
||||
gpuResult.Scores = scoreBenchmarkGPUResult(gpuResult)
|
||||
gpuResult.DegradationReasons = detectBenchmarkDegradationReasons(gpuResult, result.Normalization.Status)
|
||||
if steadyErr != nil {
|
||||
gpuResult.Status = classifySATErrorStatus(steadyOut, steadyErr)
|
||||
} else if parseResult.Fallback {
|
||||
gpuResult.Status = "PARTIAL"
|
||||
} else {
|
||||
gpuResult.Status = "OK"
|
||||
}
|
||||
|
||||
result.GPUs = append(result.GPUs, finalizeBenchmarkGPUResult(gpuResult))
|
||||
}
|
||||
|
||||
_ = os.WriteFile(filepath.Join(runDir, fmt.Sprintf("gpu-%d-steady.log", idx)), steadyOut, 0644)
|
||||
afterThrottle, _ := queryThrottleCounters(idx)
|
||||
if steadyErr != nil {
|
||||
gpuResult.Notes = append(gpuResult.Notes, "steady compute failed: "+steadyErr.Error())
|
||||
}
|
||||
|
||||
parseResult := parseBenchmarkBurnLog(string(steadyOut))
|
||||
gpuResult.ComputeCapability = parseResult.ComputeCapability
|
||||
gpuResult.Backend = parseResult.Backend
|
||||
gpuResult.PrecisionResults = parseResult.Profiles
|
||||
if parseResult.Fallback {
|
||||
gpuResult.Notes = append(gpuResult.Notes, "benchmark used driver PTX fallback; tensor throughput score is not comparable")
|
||||
}
|
||||
|
||||
gpuResult.Steady = summarizeBenchmarkTelemetry(steadyRows)
|
||||
gpuResult.Throttle = diffThrottleCounters(beforeThrottle, afterThrottle)
|
||||
if eccFinal, err := queryECCCounters(idx); err == nil {
|
||||
gpuResult.ECC = diffECCCounters(eccBase, eccFinal)
|
||||
}
|
||||
|
||||
cooldownRows, err := collectBenchmarkSamples(ctx, spec.CooldownSec, []int{idx})
|
||||
if err != nil && err != context.Canceled {
|
||||
gpuResult.Notes = append(gpuResult.Notes, "cooldown sampling failed: "+err.Error())
|
||||
}
|
||||
gpuResult.Cooldown = summarizeBenchmarkTelemetry(cooldownRows)
|
||||
writeBenchmarkMetricsFiles(runDir, fmt.Sprintf("gpu-%d-cooldown", idx), cooldownRows)
|
||||
|
||||
gpuResult.Scores = scoreBenchmarkGPUResult(gpuResult)
|
||||
gpuResult.DegradationReasons = detectBenchmarkDegradationReasons(gpuResult, result.Normalization.Status)
|
||||
if steadyErr != nil {
|
||||
gpuResult.Status = classifySATErrorStatus(steadyOut, steadyErr)
|
||||
} else if parseResult.Fallback {
|
||||
gpuResult.Status = "PARTIAL"
|
||||
} else {
|
||||
gpuResult.Status = "OK"
|
||||
}
|
||||
|
||||
result.GPUs = append(result.GPUs, finalizeBenchmarkGPUResult(gpuResult))
|
||||
}
|
||||
|
||||
} // end sequential path
|
||||
|
||||
if len(selected) > 1 && opts.RunNCCL {
|
||||
@@ -413,6 +418,7 @@ func (s *System) RunNvidiaBenchmark(ctx context.Context, baseDir string, opts Nv
|
||||
|
||||
result.Findings = buildBenchmarkFindings(result)
|
||||
result.OverallStatus = benchmarkOverallStatus(result)
|
||||
writeBenchmarkMetricsFiles(runDir, metricRows)
|
||||
|
||||
resultJSON, err := json.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
@@ -422,7 +428,7 @@ func (s *System) RunNvidiaBenchmark(ctx context.Context, baseDir string, opts Nv
|
||||
return "", fmt.Errorf("write result.json: %w", err)
|
||||
}
|
||||
|
||||
report := renderBenchmarkReportWithCharts(result, loadBenchmarkReportCharts(runDir, selected))
|
||||
report := renderBenchmarkReportWithCharts(result)
|
||||
if err := os.WriteFile(filepath.Join(runDir, "report.md"), []byte(report), 0644); err != nil {
|
||||
return "", fmt.Errorf("write report.md: %w", err)
|
||||
}
|
||||
@@ -511,11 +517,11 @@ func enrichGPUInfoWithMaxClocks(infoByIndex map[int]benchmarkGPUInfo, nvsmiQ []b
|
||||
|
||||
// Split the verbose output into per-GPU sections on "^GPU " lines.
|
||||
gpuSectionRe := regexp.MustCompile(`(?m)^GPU\s+([\dA-Fa-f:\.]+)`)
|
||||
maxGfxRe := regexp.MustCompile(`(?i)Max Clocks[\s\S]*?Graphics\s*:\s*(\d+)\s*MHz`)
|
||||
maxMemRe := regexp.MustCompile(`(?i)Max Clocks[\s\S]*?Memory\s*:\s*(\d+)\s*MHz`)
|
||||
defaultPwrRe := regexp.MustCompile(`(?i)Default Power Limit\s*:\s*([0-9.]+)\s*W`)
|
||||
currentPwrRe := regexp.MustCompile(`(?i)Current Power Limit\s*:\s*([0-9.]+)\s*W`)
|
||||
smCountRe := regexp.MustCompile(`(?i)Multiprocessor Count\s*:\s*(\d+)`)
|
||||
maxGfxRe := regexp.MustCompile(`(?i)Max Clocks[\s\S]*?Graphics\s*:\s*(\d+)\s*MHz`)
|
||||
maxMemRe := regexp.MustCompile(`(?i)Max Clocks[\s\S]*?Memory\s*:\s*(\d+)\s*MHz`)
|
||||
defaultPwrRe := regexp.MustCompile(`(?i)Default Power Limit\s*:\s*([0-9.]+)\s*W`)
|
||||
currentPwrRe := regexp.MustCompile(`(?i)Current Power Limit\s*:\s*([0-9.]+)\s*W`)
|
||||
smCountRe := regexp.MustCompile(`(?i)Multiprocessor Count\s*:\s*(\d+)`)
|
||||
|
||||
sectionStarts := gpuSectionRe.FindAllSubmatchIndex(nvsmiQ, -1)
|
||||
for i, loc := range sectionStarts {
|
||||
@@ -651,7 +657,6 @@ func queryBenchmarkGPUInfo(gpuIndices []int) (map[int]benchmarkGPUInfo, error) {
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
|
||||
func applyBenchmarkNormalization(ctx context.Context, verboseLog string, gpuIndices []int, infoByIndex map[int]benchmarkGPUInfo, result *NvidiaBenchmarkResult) []benchmarkRestoreAction {
|
||||
if os.Geteuid() != 0 {
|
||||
result.Normalization.Status = "partial"
|
||||
@@ -754,7 +759,7 @@ func collectBenchmarkSamples(ctx context.Context, durationSec int, gpuIndices []
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func runBenchmarkCommandWithMetrics(ctx context.Context, verboseLog, name string, cmd []string, env []string, gpuIndices []int, runDir, baseName string, logFunc func(string)) ([]byte, []GPUMetricRow, error) {
|
||||
func runBenchmarkCommandWithMetrics(ctx context.Context, verboseLog, name string, cmd []string, env []string, gpuIndices []int, logFunc func(string)) ([]byte, []GPUMetricRow, error) {
|
||||
stopCh := make(chan struct{})
|
||||
doneCh := make(chan struct{})
|
||||
var metricRows []GPUMetricRow
|
||||
@@ -786,18 +791,65 @@ func runBenchmarkCommandWithMetrics(ctx context.Context, verboseLog, name string
|
||||
close(stopCh)
|
||||
<-doneCh
|
||||
|
||||
writeBenchmarkMetricsFiles(runDir, baseName, metricRows)
|
||||
return out, metricRows, err
|
||||
}
|
||||
|
||||
func writeBenchmarkMetricsFiles(runDir, baseName string, rows []GPUMetricRow) {
|
||||
func annotateBenchmarkMetricRows(rows []GPUMetricRow, stage string, offset float64) []GPUMetricRow {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]GPUMetricRow, len(rows))
|
||||
for i, row := range rows {
|
||||
row.Stage = stage
|
||||
row.ElapsedSec += offset
|
||||
out[i] = row
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func benchmarkMetricOffset(rows []GPUMetricRow) float64 {
|
||||
if len(rows) == 0 {
|
||||
return 0
|
||||
}
|
||||
var maxElapsed float64
|
||||
for _, row := range rows {
|
||||
if row.ElapsedSec > maxElapsed {
|
||||
maxElapsed = row.ElapsedSec
|
||||
}
|
||||
}
|
||||
return maxElapsed
|
||||
}
|
||||
|
||||
func appendBenchmarkMetrics(allRows *[]GPUMetricRow, rows []GPUMetricRow, stage string) {
|
||||
annotated := annotateBenchmarkMetricRows(rows, stage, benchmarkMetricOffset(*allRows))
|
||||
*allRows = append(*allRows, annotated...)
|
||||
}
|
||||
|
||||
func writeBenchmarkMetricsFiles(runDir string, rows []GPUMetricRow) {
|
||||
if len(rows) == 0 {
|
||||
return
|
||||
}
|
||||
_ = WriteGPUMetricsCSV(filepath.Join(runDir, baseName+"-metrics.csv"), rows)
|
||||
_ = WriteGPUMetricsHTML(filepath.Join(runDir, baseName+"-metrics.html"), rows)
|
||||
chart := RenderGPUTerminalChart(rows)
|
||||
_ = os.WriteFile(filepath.Join(runDir, baseName+"-metrics-term.txt"), []byte(chart), 0644)
|
||||
_ = WriteGPUMetricsCSV(filepath.Join(runDir, "gpu-metrics.csv"), rows)
|
||||
_ = WriteGPUMetricsHTML(filepath.Join(runDir, "gpu-metrics.html"), rows)
|
||||
}
|
||||
|
||||
func appendBenchmarkStageLog(path, source, stage string, raw []byte) {
|
||||
if path == "" || len(raw) == 0 {
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
header := fmt.Sprintf("\n========== %s | stage=%s ==========\n", source, stage)
|
||||
_, _ = f.WriteString(header)
|
||||
if len(raw) > 0 {
|
||||
_, _ = f.Write(raw)
|
||||
if raw[len(raw)-1] != '\n' {
|
||||
_, _ = f.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseBenchmarkBurnLog(raw string) benchmarkBurnParseResult {
|
||||
@@ -897,11 +949,13 @@ func ensureBenchmarkProfile(profiles map[string]*benchmarkBurnProfile, name stri
|
||||
// precisionWeight returns the fp32-equivalence factor for a precision category.
|
||||
// Each factor represents how much "real" numeric work one operation of that
|
||||
// type performs relative to fp32 (single precision = 1.0 baseline):
|
||||
// fp64 = 2.0 — double precision, 2× more bits per operand
|
||||
// fp32 = 1.0 — single precision baseline
|
||||
// fp16 = 0.5 — half precision
|
||||
// fp8 = 0.25 — quarter precision
|
||||
// fp4 = 0.125 — eighth precision
|
||||
//
|
||||
// fp64 = 2.0 — double precision, 2× more bits per operand
|
||||
// fp32 = 1.0 — single precision baseline
|
||||
// fp16 = 0.5 — half precision
|
||||
// fp8 = 0.25 — quarter precision
|
||||
// fp4 = 0.125 — eighth precision
|
||||
//
|
||||
// Multiplying raw TOPS by the weight gives fp32-equivalent TOPS, enabling
|
||||
// cross-precision comparison on the same numeric scale.
|
||||
func precisionWeight(category string) float64 {
|
||||
@@ -1670,6 +1724,8 @@ func runNvidiaBenchmarkParallel(
|
||||
calibPowerByIndex map[int]float64,
|
||||
serverIdleW *float64, serverLoadedWSum *float64,
|
||||
serverIdleOK *bool, serverLoadedOK *bool, serverLoadedSamples *int,
|
||||
allMetricRows *[]GPUMetricRow,
|
||||
gpuBurnLog string,
|
||||
) {
|
||||
allDevices := joinIndexList(selected)
|
||||
|
||||
@@ -1709,8 +1765,8 @@ func runNvidiaBenchmarkParallel(
|
||||
for _, idx := range selected {
|
||||
perGPU := filterRowsByGPU(baselineRows, idx)
|
||||
gpuResults[idx].Baseline = summarizeBenchmarkTelemetry(perGPU)
|
||||
writeBenchmarkMetricsFiles(runDir, fmt.Sprintf("gpu-%d-baseline", idx), perGPU)
|
||||
}
|
||||
appendBenchmarkMetrics(allMetricRows, baselineRows, "baseline")
|
||||
|
||||
// Sample server idle power once.
|
||||
if !*serverIdleOK {
|
||||
@@ -1729,11 +1785,9 @@ func runNvidiaBenchmarkParallel(
|
||||
"--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, runDir, "gpu-all-warmup", logFunc)
|
||||
_ = os.WriteFile(filepath.Join(runDir, "gpu-all-warmup.log"), warmupOut, 0644)
|
||||
for _, idx := range selected {
|
||||
writeBenchmarkMetricsFiles(runDir, fmt.Sprintf("gpu-%d-warmup", idx), filterRowsByGPU(warmupRows, idx))
|
||||
}
|
||||
warmupOut, warmupRows, warmupErr := runBenchmarkCommandWithMetrics(ctx, verboseLog, "gpu-all-warmup.log", warmupCmd, nil, selected, logFunc)
|
||||
appendBenchmarkMetrics(allMetricRows, warmupRows, "warmup")
|
||||
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())
|
||||
@@ -1764,7 +1818,9 @@ func runNvidiaBenchmarkParallel(
|
||||
for _, idx := range selected {
|
||||
eccBeforePhase[idx], _ = queryECCCounters(idx)
|
||||
}
|
||||
phaseOut, phaseRows, phaseErr := runBenchmarkCommandWithMetrics(ctx, verboseLog, phaseLogName+".log", phaseCmd, nil, selected, runDir, phaseLogName, logFunc)
|
||||
phaseOut, phaseRows, phaseErr := runBenchmarkCommandWithMetrics(ctx, verboseLog, phaseLogName+".log", phaseCmd, nil, selected, logFunc)
|
||||
appendBenchmarkMetrics(allMetricRows, phaseRows, phaseLogName)
|
||||
appendBenchmarkStageLog(gpuBurnLog, "bee-gpu-burn", phaseLogName, phaseOut)
|
||||
eccAfterPhase := make(map[int]BenchmarkECCCounters, len(selected))
|
||||
for _, idx := range selected {
|
||||
eccAfterPhase[idx], _ = queryECCCounters(idx)
|
||||
@@ -1842,7 +1898,9 @@ func runNvidiaBenchmarkParallel(
|
||||
}
|
||||
}()
|
||||
|
||||
steadyOut, steadyRows, steadyErr := runBenchmarkCommandWithMetrics(ctx, verboseLog, "gpu-all-steady.log", steadyCmd, nil, selected, runDir, "gpu-all-steady", logFunc)
|
||||
steadyOut, steadyRows, steadyErr := runBenchmarkCommandWithMetrics(ctx, verboseLog, "gpu-all-steady.log", steadyCmd, nil, selected, logFunc)
|
||||
appendBenchmarkMetrics(allMetricRows, steadyRows, "steady")
|
||||
appendBenchmarkStageLog(gpuBurnLog, "bee-gpu-burn", "steady", steadyOut)
|
||||
close(ipmiStopCh)
|
||||
if loadedW, ok := <-ipmiResultCh; ok {
|
||||
*serverLoadedWSum += loadedW
|
||||
@@ -1850,8 +1908,6 @@ func runNvidiaBenchmarkParallel(
|
||||
*serverLoadedOK = true
|
||||
logFunc(fmt.Sprintf("GPUs %s: server loaded power (IPMI): %.0f W", allDevices, loadedW))
|
||||
}
|
||||
_ = os.WriteFile(filepath.Join(runDir, "gpu-all-steady.log"), steadyOut, 0644)
|
||||
|
||||
afterThrottle := make(map[int]BenchmarkThrottleCounters, len(selected))
|
||||
for _, idx := range selected {
|
||||
afterThrottle[idx], _ = queryThrottleCounters(idx)
|
||||
@@ -1861,7 +1917,6 @@ func runNvidiaBenchmarkParallel(
|
||||
|
||||
for _, idx := range selected {
|
||||
perGPU := filterRowsByGPU(steadyRows, idx)
|
||||
writeBenchmarkMetricsFiles(runDir, fmt.Sprintf("gpu-%d-steady", idx), perGPU)
|
||||
gpuResults[idx].Steady = summarizeBenchmarkTelemetry(perGPU)
|
||||
gpuResults[idx].Throttle = diffThrottleCounters(beforeThrottle[idx], afterThrottle[idx])
|
||||
if eccFinal, err := queryECCCounters(idx); err == nil {
|
||||
@@ -1891,8 +1946,8 @@ func runNvidiaBenchmarkParallel(
|
||||
for _, idx := range selected {
|
||||
perGPU := filterRowsByGPU(cooldownRows, idx)
|
||||
gpuResults[idx].Cooldown = summarizeBenchmarkTelemetry(perGPU)
|
||||
writeBenchmarkMetricsFiles(runDir, fmt.Sprintf("gpu-%d-cooldown", idx), perGPU)
|
||||
}
|
||||
appendBenchmarkMetrics(allMetricRows, cooldownRows, "cooldown")
|
||||
|
||||
// Score and finalize each GPU.
|
||||
for _, idx := range selected {
|
||||
@@ -2102,7 +2157,7 @@ func runBenchmarkPowerCalibration(
|
||||
logFunc(fmt.Sprintf("power calibration: running dcgmi targeted_power for %ds on GPUs %s", calibDurationSec, joinIndexList(gpuIndices)))
|
||||
|
||||
cmd := nvidiaDCGMNamedDiagCommand("targeted_power", calibDurationSec, gpuIndices)
|
||||
out, rows, err := runBenchmarkCommandWithMetrics(ctx, verboseLog, "power-calibration.log", cmd, nil, gpuIndices, runDir, "power-calibration", logFunc)
|
||||
out, rows, err := runBenchmarkCommandWithMetrics(ctx, verboseLog, "power-calibration.log", cmd, nil, gpuIndices, logFunc)
|
||||
_ = os.WriteFile(filepath.Join(runDir, "power-calibration.log"), out, 0644)
|
||||
if err != nil {
|
||||
logFunc(fmt.Sprintf("power calibration: dcgmi targeted_power failed (%v), skipping", err))
|
||||
|
||||
Reference in New Issue
Block a user