package platform import ( "context" "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "time" ) // HPLOptions configures the HPL (LINPACK) benchmark run. type HPLOptions struct { MemFraction float64 // fraction of RAM to use (default 0.80) NB int // block size (default 256) } // HPLResult holds the parsed result of an HPL run. type HPLResult struct { N int // matrix dimension NB int // block size P int // process grid rows Q int // process grid cols TimeSec float64 // wall time in seconds GFlops float64 // achieved performance Residual float64 // backward error residual (from HPL verification line) Status string // "PASSED" or "FAILED" RawOutput string // full xhpl output } func applyHPLDefaults(opts *HPLOptions) { if opts.MemFraction <= 0 || opts.MemFraction > 1 { opts.MemFraction = 0.80 } if opts.NB <= 0 { opts.NB = 256 } } // RunHPL runs bee-hpl and returns parsed results plus a tar.gz artifact path. func (s *System) RunHPL(ctx context.Context, baseDir string, opts HPLOptions, logFunc func(string)) (string, *HPLResult, error) { applyHPLDefaults(&opts) if baseDir == "" { baseDir = "/var/log/bee-sat" } ts := time.Now().UTC().Format("20060102-150405") runDir := filepath.Join(baseDir, "hpl-"+ts) if err := os.MkdirAll(runDir, 0755); err != nil { return "", nil, fmt.Errorf("mkdir %s: %w", runDir, err) } logPath := filepath.Join(runDir, "hpl.log") cmd := []string{ "bee-hpl", "--mem-fraction", strconv.FormatFloat(opts.MemFraction, 'f', 2, 64), "--nb", strconv.Itoa(opts.NB), } if logFunc != nil { logFunc(fmt.Sprintf("HPL: N will be auto-sized to %.0f%% of RAM, NB=%d", opts.MemFraction*100, opts.NB)) } out, err := runSATCommandCtx(ctx, "", "hpl", cmd, nil, logFunc) _ = os.WriteFile(logPath, out, 0644) result := parseHPLOutput(string(out)) result.RawOutput = string(out) if err != nil && err != context.Canceled { return "", result, fmt.Errorf("bee-hpl failed: %w", err) } if err == nil && result.GFlops <= 0 { return "", result, fmt.Errorf("HPL completed but no Gflops result found in output") } // Write summary summary := fmt.Sprintf("N=%d NB=%d time=%.2fs gflops=%.3f status=%s\n", result.N, result.NB, result.TimeSec, result.GFlops, result.Status) _ = os.WriteFile(filepath.Join(runDir, "summary.txt"), []byte(summary), 0644) if logFunc != nil { logFunc(fmt.Sprintf("HPL result: N=%d NB=%d %.2fs %.3f Gflops %s", result.N, result.NB, result.TimeSec, result.GFlops, result.Status)) } ts2 := time.Now().UTC().Format("20060102-150405") archive := filepath.Join(baseDir, "hpl-"+ts2+".tar.gz") if archErr := createTarGz(archive, runDir); archErr != nil { return runDir, result, err } return archive, result, err } // parseHPLOutput extracts N, NB, time, and Gflops from standard HPL output. // // HPL prints a result line of the form: // // WR00L2L2 45312 256 1 1 1234.56 5.678e+01 // T/V N NB P Q Time Gflops func parseHPLOutput(output string) *HPLResult { result := &HPLResult{Status: "FAILED"} for _, line := range strings.Split(output, "\n") { line = strings.TrimSpace(line) // Result line starts with WR if strings.HasPrefix(line, "WR") { fields := strings.Fields(line) // WR00L2L2 N NB P Q Time Gflops if len(fields) >= 7 { result.N, _ = strconv.Atoi(fields[1]) result.NB, _ = strconv.Atoi(fields[2]) result.P, _ = strconv.Atoi(fields[3]) result.Q, _ = strconv.Atoi(fields[4]) result.TimeSec, _ = strconv.ParseFloat(fields[5], 64) result.GFlops, _ = strconv.ParseFloat(fields[6], 64) } } // Verification line: "||Ax-b||_oo/(eps*(||A||_oo*||x||_oo+||b||_oo)*N)= ... PASSED" if strings.Contains(line, "PASSED") { result.Status = "PASSED" fields := strings.Fields(line) for i, f := range fields { if f == "PASSED" && i > 0 { result.Residual, _ = strconv.ParseFloat(fields[i-1], 64) } } } } return result } // hplAvailable returns true if bee-hpl and xhpl are present and executable. func hplAvailable() bool { if _, err := exec.LookPath("bee-hpl"); err != nil { return false } _, err := os.Stat("/usr/local/lib/bee/xhpl") return err == nil }