webui: add persistent GPU settings management (ECC/MIG/CC/power limit)

Adds a GPU Settings card to /tools for the settings that actually
persist on NVIDIA data-center GPUs: ECC mode, MIG mode, and
Confidential Computing mode (all stored in the GPU's inforom/firmware,
take effect after a GPU reset or reboot) plus power limit (does not
persist — reapplied on demand). Includes a one-click "Reset All to
Defaults" that restores factory settings across every visible GPU,
touching only whatever has actually drifted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-07-07 15:25:25 +03:00
co-authored by Claude Sonnet 5
parent 2599d9c5e3
commit 53c46465d2
8 changed files with 761 additions and 0 deletions
+6
View File
@@ -137,6 +137,12 @@ type satRunner interface {
RunConfidentialComputingCheckPack(ctx context.Context, baseDir string, logFunc func(string)) (string, error) RunConfidentialComputingCheckPack(ctx context.Context, baseDir string, logFunc func(string)) (string, error)
RunCPUAcceptancePack(ctx context.Context, baseDir string, durationSec int, logFunc func(string)) (string, error) RunCPUAcceptancePack(ctx context.Context, baseDir string, durationSec int, logFunc func(string)) (string, error)
ListNvidiaGPUs() ([]platform.NvidiaGPU, error) ListNvidiaGPUs() ([]platform.NvidiaGPU, error)
ListNvidiaGPUSettings() ([]platform.NvidiaGPUSetting, error)
SetNvidiaGPUECC(index int, enabled bool) (string, error)
SetNvidiaGPUMIG(index int, enabled bool) (string, error)
SetNvidiaGPUCCMode(index int, enabled bool) (string, error)
SetNvidiaGPUPowerLimit(index int, watts float64) (string, error)
ResetNvidiaGPUDefaults() (string, error)
DetectGPUVendor() string DetectGPUVendor() string
ListAMDGPUs() ([]platform.AMDGPUInfo, error) ListAMDGPUs() ([]platform.AMDGPUInfo, error)
RunAMDAcceptancePack(ctx context.Context, baseDir string, logFunc func(string)) (string, error) RunAMDAcceptancePack(ctx context.Context, baseDir string, logFunc func(string)) (string, error)
+41
View File
@@ -39,6 +39,47 @@ func (a *App) ResetNvidiaGPU(index int) (ActionResult, error) {
return ActionResult{Title: fmt.Sprintf("Reset NVIDIA GPU %d", index), Body: strings.TrimSpace(out)}, err return ActionResult{Title: fmt.Sprintf("Reset NVIDIA GPU %d", index), Body: strings.TrimSpace(out)}, err
} }
func (a *App) ListNvidiaGPUSettings() ([]platform.NvidiaGPUSetting, error) {
return a.sat.ListNvidiaGPUSettings()
}
func (a *App) SetNvidiaGPUECC(index int, enabled bool) (ActionResult, error) {
out, err := a.sat.SetNvidiaGPUECC(index, enabled)
title := fmt.Sprintf("Enable ECC on GPU %d", index)
if !enabled {
title = fmt.Sprintf("Disable ECC on GPU %d", index)
}
return ActionResult{Title: title, Body: strings.TrimSpace(out)}, err
}
func (a *App) SetNvidiaGPUMIG(index int, enabled bool) (ActionResult, error) {
out, err := a.sat.SetNvidiaGPUMIG(index, enabled)
title := fmt.Sprintf("Enable MIG on GPU %d", index)
if !enabled {
title = fmt.Sprintf("Disable MIG on GPU %d", index)
}
return ActionResult{Title: title, Body: strings.TrimSpace(out)}, err
}
func (a *App) SetNvidiaGPUCCMode(index int, enabled bool) (ActionResult, error) {
out, err := a.sat.SetNvidiaGPUCCMode(index, enabled)
title := fmt.Sprintf("Enable Confidential Computing on GPU %d", index)
if !enabled {
title = fmt.Sprintf("Disable Confidential Computing on GPU %d", index)
}
return ActionResult{Title: title, Body: strings.TrimSpace(out)}, err
}
func (a *App) SetNvidiaGPUPowerLimit(index int, watts float64) (ActionResult, error) {
out, err := a.sat.SetNvidiaGPUPowerLimit(index, watts)
return ActionResult{Title: fmt.Sprintf("Set power limit on GPU %d", index), Body: strings.TrimSpace(out)}, err
}
func (a *App) ResetNvidiaGPUDefaults() (ActionResult, error) {
out, err := a.sat.ResetNvidiaGPUDefaults()
return ActionResult{Title: "Reset all GPUs to factory defaults", Body: strings.TrimSpace(out)}, err
}
func (a *App) RunNvidiaAcceptancePackWithOptions(ctx context.Context, baseDir string, diagLevel int, gpuIndices []int, logFunc func(string)) (ActionResult, error) { func (a *App) RunNvidiaAcceptancePackWithOptions(ctx context.Context, baseDir string, diagLevel int, gpuIndices []int, logFunc func(string)) (ActionResult, error) {
if strings.TrimSpace(baseDir) == "" { if strings.TrimSpace(baseDir) == "" {
baseDir = DefaultSATBaseDir baseDir = DefaultSATBaseDir
+24
View File
@@ -235,6 +235,30 @@ func (f fakeSAT) ResetNvidiaGPU(index int) (string, error) {
return "", nil return "", nil
} }
func (f fakeSAT) ListNvidiaGPUSettings() ([]platform.NvidiaGPUSetting, error) {
return nil, nil
}
func (f fakeSAT) SetNvidiaGPUECC(index int, enabled bool) (string, error) {
return "", nil
}
func (f fakeSAT) SetNvidiaGPUMIG(index int, enabled bool) (string, error) {
return "", nil
}
func (f fakeSAT) SetNvidiaGPUCCMode(index int, enabled bool) (string, error) {
return "", nil
}
func (f fakeSAT) SetNvidiaGPUPowerLimit(index int, watts float64) (string, error) {
return "", nil
}
func (f fakeSAT) ResetNvidiaGPUDefaults() (string, error) {
return "", nil
}
func (f fakeSAT) RunMemoryAcceptancePack(_ context.Context, baseDir string, _, _ int, _ func(string)) (string, error) { func (f fakeSAT) RunMemoryAcceptancePack(_ context.Context, baseDir string, _, _ int, _ func(string)) (string, error) {
return f.runMemoryFn(baseDir) return f.runMemoryFn(baseDir)
} }
+183
View File
@@ -0,0 +1,183 @@
package platform
import (
"fmt"
"strconv"
"strings"
)
// NvidiaGPUSetting reports the persistent GPU settings that nvidia-smi
// exposes: ECC mode, MIG mode, and Confidential Computing mode all persist
// via the GPU's inforom/firmware and survive a reboot, but only take effect
// after a GPU reset (nvidia-smi -r) or host reboot (hence separate
// current/pending fields for ECC and MIG). Power limit does NOT persist
// across reboot/driver reload — it is re-applied here on demand, same as
// nvidia-smi's own behavior.
type NvidiaGPUSetting struct {
Index int `json:"index"`
Name string `json:"name"`
ECCCurrent string `json:"ecc_current"` // "Enabled" / "Disabled" / "N/A"
ECCPending string `json:"ecc_pending"` // differs from current until a GPU reset/reboot
MIGCurrent string `json:"mig_current"` // "Enabled" / "Disabled" / "N/A" (N/A = not MIG-capable)
MIGPending string `json:"mig_pending"`
CCState string `json:"cc_state"` // "ON" / "OFF" / "" (unsupported or query failed)
PowerLimitW float64 `json:"power_limit_w"`
PowerMinLimitW float64 `json:"power_min_limit_w"`
PowerMaxLimitW float64 `json:"power_max_limit_w"`
PowerDefaultLimitW float64 `json:"power_default_limit_w"`
}
// ListNvidiaGPUSettings reports current ECC, MIG, Confidential Computing and
// power-limit configuration per GPU.
func (s *System) ListNvidiaGPUSettings() ([]NvidiaGPUSetting, error) {
out, err := satExecCommand("nvidia-smi",
"--query-gpu=index,name,ecc.mode.current,ecc.mode.pending,mig.mode.current,mig.mode.pending,power.limit,power.min_limit,power.max_limit,power.default_limit",
"--format=csv,noheader,nounits").Output()
if err != nil {
return nil, fmt.Errorf("nvidia-smi: %w", err)
}
var settings []NvidiaGPUSetting
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.Split(line, ",")
if len(parts) < 10 {
continue
}
idx, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
continue
}
g := NvidiaGPUSetting{
Index: idx,
Name: strings.TrimSpace(parts[1]),
ECCCurrent: strings.TrimSpace(parts[2]),
ECCPending: strings.TrimSpace(parts[3]),
MIGCurrent: strings.TrimSpace(parts[4]),
MIGPending: strings.TrimSpace(parts[5]),
PowerLimitW: parseNvidiaFloat(parts[6]),
PowerMinLimitW: parseNvidiaFloat(parts[7]),
PowerMaxLimitW: parseNvidiaFloat(parts[8]),
PowerDefaultLimitW: parseNvidiaFloat(parts[9]),
}
g.CCState = s.queryNvidiaGPUCCState(idx)
settings = append(settings, g)
}
return settings, nil
}
// queryNvidiaGPUCCState reads the per-GPU Confidential Computing state via
// `nvidia-smi conf-compute -i <index> -q`, reusing the same field-name
// convention as RunConfidentialComputingCheckPack's parseConfComputeFields.
// Returns "" if the driver/GPU doesn't support conf-compute at all.
func (s *System) queryNvidiaGPUCCState(index int) string {
out, err := satExecCommand("nvidia-smi", "conf-compute", "-i", strconv.Itoa(index), "-q").Output()
if err != nil {
return ""
}
return parseConfComputeFields(out)["CC State"]
}
func parseNvidiaFloat(v string) float64 {
f, err := strconv.ParseFloat(strings.TrimSpace(v), 64)
if err != nil {
return 0
}
return f
}
// SetNvidiaGPUECC sets pending ECC mode for one GPU. Takes effect after the
// GPU is reset (nvidia-smi -r) or the host is rebooted.
func (s *System) SetNvidiaGPUECC(index int, enabled bool) (string, error) {
val := "0"
if enabled {
val = "1"
}
out, err := satExecCommand("nvidia-smi", "-i", strconv.Itoa(index), "-e", val).CombinedOutput()
return string(out), err
}
// SetNvidiaGPUMIG sets pending MIG (Multi-Instance GPU) mode for one GPU.
// Persists via inforom but takes effect only after a GPU reset (nvidia-smi
// -r) or host reboot. Only supported on MIG-capable GPUs (Ampere/Hopper
// data-center parts); fails harmlessly on others.
func (s *System) SetNvidiaGPUMIG(index int, enabled bool) (string, error) {
val := "0"
if enabled {
val = "1"
}
out, err := satExecCommand("nvidia-smi", "-i", strconv.Itoa(index), "-mig", val).CombinedOutput()
return string(out), err
}
// SetNvidiaGPUCCMode sets Confidential Computing mode for one GPU
// (nvidia-smi conf-compute -scc). Persists via inforom, reversible, but
// takes effect only after a GPU reset (nvidia-smi -r) or host reboot.
func (s *System) SetNvidiaGPUCCMode(index int, enabled bool) (string, error) {
val := "0"
if enabled {
val = "1"
}
out, err := satExecCommand("nvidia-smi", "conf-compute", "-i", strconv.Itoa(index), "-scc", val).CombinedOutput()
return string(out), err
}
// SetNvidiaGPUPowerLimit sets the power limit (watts) for one GPU. Does not
// persist across reboot/driver reload — callers that want a durable change
// must re-apply it (e.g. at boot), same as nvidia-smi's own semantics.
func (s *System) SetNvidiaGPUPowerLimit(index int, watts float64) (string, error) {
out, err := satExecCommand("nvidia-smi", "-i", strconv.Itoa(index), "-pl", strconv.FormatFloat(watts, 'f', 0, 64)).CombinedOutput()
return string(out), err
}
// ResetNvidiaGPUDefaults restores factory-default settings on every visible
// NVIDIA GPU that has drifted from them: power limit back to
// power.default_limit (as reported by nvidia-smi), ECC re-enabled (NVIDIA
// data-center GPUs ship with ECC on; nvidia-smi has no queryable "ECC
// default" field, so this is the documented factory default, not a value
// read back from hardware), and MIG/Confidential-Computing mode disabled
// (both ship off from the factory). GPUs already at defaults are left
// alone.
func (s *System) ResetNvidiaGPUDefaults() (string, error) {
settings, err := s.ListNvidiaGPUSettings()
if err != nil {
return "", err
}
var out strings.Builder
for _, g := range settings {
if g.PowerDefaultLimitW > 0 && g.PowerLimitW != g.PowerDefaultLimitW {
res, err := s.SetNvidiaGPUPowerLimit(g.Index, g.PowerDefaultLimitW)
out.WriteString(res)
if err != nil {
fmt.Fprintf(&out, "GPU %d: power limit reset failed: %v\n", g.Index, err)
}
}
if strings.EqualFold(g.ECCCurrent, "disabled") {
res, err := s.SetNvidiaGPUECC(g.Index, true)
out.WriteString(res)
if err != nil {
fmt.Fprintf(&out, "GPU %d: ECC re-enable failed: %v\n", g.Index, err)
}
}
if strings.EqualFold(g.MIGCurrent, "enabled") {
res, err := s.SetNvidiaGPUMIG(g.Index, false)
out.WriteString(res)
if err != nil {
fmt.Fprintf(&out, "GPU %d: MIG disable failed: %v\n", g.Index, err)
}
}
if strings.EqualFold(g.CCState, "on") {
res, err := s.SetNvidiaGPUCCMode(g.Index, false)
out.WriteString(res)
if err != nil {
fmt.Fprintf(&out, "GPU %d: CC mode disable failed: %v\n", g.Index, err)
}
}
}
if out.Len() == 0 {
out.WriteString("All GPUs already at factory-default settings.\n")
}
return out.String(), nil
}
@@ -0,0 +1,223 @@
package platform
import (
"os/exec"
"strings"
"testing"
)
// fakeNvidiaSMI stubs satExecCommand for nvidia-smi calls made by the
// GPU-settings code: the CSV query (index,...,power.default_limit), the
// per-GPU "conf-compute -q" read, and write commands (-e/-mig/-pl/-scc).
// ccStateByIndex maps GPU index -> CC State text ("ON"/"OFF"); a missing
// entry simulates conf-compute being unsupported (query fails).
func fakeNvidiaSMI(t *testing.T, queryOut string, ccStateByIndex map[int]string, writeCalls *[]string) {
t.Helper()
old := satExecCommand
satExecCommand = func(name string, args ...string) *exec.Cmd {
if name != "nvidia-smi" {
return exec.Command(name, args...)
}
if len(args) > 0 && strings.HasPrefix(args[0], "--query-gpu=") {
return exec.Command("printf", "%s", queryOut)
}
if len(args) > 0 && args[0] == "conf-compute" {
// args: conf-compute -i <index> -q
idx := -1
for i, a := range args {
if a == "-i" && i+1 < len(args) {
var n int
fscanInt(args[i+1], &n)
idx = n
}
}
if len(args) > 0 && args[len(args)-1] == "-q" {
state, ok := ccStateByIndex[idx]
if !ok {
return exec.Command("false")
}
return exec.Command("printf", "CC State : %s\\n", state)
}
if writeCalls != nil {
*writeCalls = append(*writeCalls, strings.Join(args, " "))
}
return exec.Command("true")
}
if writeCalls != nil {
*writeCalls = append(*writeCalls, strings.Join(args, " "))
}
return exec.Command("true")
}
t.Cleanup(func() { satExecCommand = old })
}
func fscanInt(s string, out *int) {
n := 0
neg := false
for i, c := range s {
if i == 0 && c == '-' {
neg = true
continue
}
if c < '0' || c > '9' {
return
}
n = n*10 + int(c-'0')
}
if neg {
n = -n
}
*out = n
}
func TestListNvidiaGPUSettingsParsesCSV(t *testing.T) {
fakeNvidiaSMI(t,
"0, H100 80GB HBM3, Enabled, Enabled, Disabled, Disabled, 350.00, 100.00, 700.00, 700.00\n"+
"1, H100 80GB HBM3, Disabled, Enabled, N/A, N/A, 300.00, 100.00, 700.00, 700.00\n",
map[int]string{0: "OFF", 1: "OFF"}, nil)
s := &System{}
got, err := s.ListNvidiaGPUSettings()
if err != nil {
t.Fatalf("err=%v", err)
}
if len(got) != 2 {
t.Fatalf("len=%d want 2 (%#v)", len(got), got)
}
if got[0].ECCCurrent != "Enabled" || got[0].PowerLimitW != 350 || got[0].PowerDefaultLimitW != 700 {
t.Fatalf("gpu0=%#v", got[0])
}
if got[0].MIGCurrent != "Disabled" || got[0].CCState != "OFF" {
t.Fatalf("gpu0 mig/cc=%#v", got[0])
}
if got[1].ECCCurrent != "Disabled" || got[1].ECCPending != "Enabled" || got[1].MIGCurrent != "N/A" {
t.Fatalf("gpu1=%#v", got[1])
}
}
func TestListNvidiaGPUSettingsCCUnsupportedLeavesEmptyState(t *testing.T) {
fakeNvidiaSMI(t,
"0, H100, Enabled, Enabled, Disabled, Disabled, 350.00, 100.00, 700.00, 700.00\n",
map[int]string{}, nil)
s := &System{}
got, err := s.ListNvidiaGPUSettings()
if err != nil {
t.Fatalf("err=%v", err)
}
if len(got) != 1 || got[0].CCState != "" {
t.Fatalf("got=%#v want empty CCState when conf-compute query fails", got)
}
}
func TestSetNvidiaGPUECCPassesCorrectFlag(t *testing.T) {
var calls []string
fakeNvidiaSMI(t, "", nil, &calls)
s := &System{}
if _, err := s.SetNvidiaGPUECC(2, true); err != nil {
t.Fatalf("err=%v", err)
}
if len(calls) != 1 || calls[0] != "-i 2 -e 1" {
t.Fatalf("calls=%v want [-i 2 -e 1]", calls)
}
if _, err := s.SetNvidiaGPUECC(0, false); err != nil {
t.Fatalf("err=%v", err)
}
if calls[1] != "-i 0 -e 0" {
t.Fatalf("calls[1]=%q want -i 0 -e 0", calls[1])
}
}
func TestSetNvidiaGPUMIGPassesCorrectFlag(t *testing.T) {
var calls []string
fakeNvidiaSMI(t, "", nil, &calls)
s := &System{}
if _, err := s.SetNvidiaGPUMIG(3, true); err != nil {
t.Fatalf("err=%v", err)
}
if len(calls) != 1 || calls[0] != "-i 3 -mig 1" {
t.Fatalf("calls=%v want [-i 3 -mig 1]", calls)
}
}
func TestSetNvidiaGPUCCModePassesCorrectFlag(t *testing.T) {
var calls []string
fakeNvidiaSMI(t, "", nil, &calls)
s := &System{}
if _, err := s.SetNvidiaGPUCCMode(1, false); err != nil {
t.Fatalf("err=%v", err)
}
if len(calls) != 1 || calls[0] != "conf-compute -i 1 -scc 0" {
t.Fatalf("calls=%v want [conf-compute -i 1 -scc 0]", calls)
}
}
func TestSetNvidiaGPUPowerLimitFormatsWatts(t *testing.T) {
var calls []string
fakeNvidiaSMI(t, "", nil, &calls)
s := &System{}
if _, err := s.SetNvidiaGPUPowerLimit(1, 450); err != nil {
t.Fatalf("err=%v", err)
}
if len(calls) != 1 || calls[0] != "-i 1 -pl 450" {
t.Fatalf("calls=%v want [-i 1 -pl 450]", calls)
}
}
func TestResetNvidiaGPUDefaultsOnlyTouchesDriftedGPUs(t *testing.T) {
var calls []string
fakeNvidiaSMI(t,
"0, H100, Enabled, Enabled, Enabled, Enabled, 350.00, 100.00, 700.00, 700.00\n"+
"1, H100, Disabled, Disabled, Disabled, Disabled, 700.00, 100.00, 700.00, 700.00\n",
map[int]string{0: "ON", 1: "OFF"}, &calls)
s := &System{}
out, err := s.ResetNvidiaGPUDefaults()
if err != nil {
t.Fatalf("err=%v", err)
}
if out == "" {
t.Fatalf("expected non-empty output")
}
// GPU0: power limit drifted (350 vs 700) -> reset; MIG enabled -> disable; CC ON -> disable.
// GPU1: power limit already default, MIG already disabled, CC already off;
// only ECC disabled -> re-enable.
want := []string{
"-i 0 -pl 700",
"-i 0 -mig 0",
"conf-compute -i 0 -scc 0",
"-i 1 -e 1",
}
if len(calls) != len(want) {
t.Fatalf("calls=%v want %v", calls, want)
}
for i := range want {
if calls[i] != want[i] {
t.Fatalf("calls[%d]=%q want %q (all calls: %v)", i, calls[i], want[i], calls)
}
}
}
func TestResetNvidiaGPUDefaultsNoOpWhenAlreadyDefault(t *testing.T) {
var calls []string
fakeNvidiaSMI(t,
"0, H100, Enabled, Enabled, Disabled, Disabled, 700.00, 100.00, 700.00, 700.00\n",
map[int]string{0: "OFF"}, &calls)
s := &System{}
out, err := s.ResetNvidiaGPUDefaults()
if err != nil {
t.Fatalf("err=%v", err)
}
if len(calls) != 0 {
t.Fatalf("calls=%v want no calls (already at factory defaults)", calls)
}
if !strings.Contains(out, "already at factory-default") {
t.Fatalf("out=%q want no-op message", out)
}
}
+119
View File
@@ -1169,6 +1169,125 @@ func (h *handler) handleAPIGNVIDIAReset(w http.ResponseWriter, r *http.Request)
writeJSON(w, map[string]string{"status": status, "output": result.Body}) writeJSON(w, map[string]string{"status": status, "output": result.Body})
} }
// ── GPU settings (ECC / power limit) ──────────────────────────────────────────
func (h *handler) handleAPIGNVIDIAGPUSettings(w http.ResponseWriter, _ *http.Request) {
if h.opts.App == nil {
writeError(w, http.StatusServiceUnavailable, "app not configured")
return
}
settings, err := h.opts.App.ListNvidiaGPUSettings()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if settings == nil {
settings = []platform.NvidiaGPUSetting{}
}
writeJSON(w, settings)
}
func (h *handler) handleAPIGNVIDIASetECC(w http.ResponseWriter, r *http.Request) {
if h.opts.App == nil {
writeError(w, http.StatusServiceUnavailable, "app not configured")
return
}
var req struct {
Index int `json:"index"`
Enabled bool `json:"enabled"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
result, err := h.opts.App.SetNvidiaGPUECC(req.Index, req.Enabled)
status := "ok"
if err != nil {
status = "error"
}
writeJSON(w, map[string]string{"status": status, "output": result.Body})
}
func (h *handler) handleAPIGNVIDIASetMIG(w http.ResponseWriter, r *http.Request) {
if h.opts.App == nil {
writeError(w, http.StatusServiceUnavailable, "app not configured")
return
}
var req struct {
Index int `json:"index"`
Enabled bool `json:"enabled"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
result, err := h.opts.App.SetNvidiaGPUMIG(req.Index, req.Enabled)
status := "ok"
if err != nil {
status = "error"
}
writeJSON(w, map[string]string{"status": status, "output": result.Body})
}
func (h *handler) handleAPIGNVIDIASetCCMode(w http.ResponseWriter, r *http.Request) {
if h.opts.App == nil {
writeError(w, http.StatusServiceUnavailable, "app not configured")
return
}
var req struct {
Index int `json:"index"`
Enabled bool `json:"enabled"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
result, err := h.opts.App.SetNvidiaGPUCCMode(req.Index, req.Enabled)
status := "ok"
if err != nil {
status = "error"
}
writeJSON(w, map[string]string{"status": status, "output": result.Body})
}
func (h *handler) handleAPIGNVIDIASetPowerLimit(w http.ResponseWriter, r *http.Request) {
if h.opts.App == nil {
writeError(w, http.StatusServiceUnavailable, "app not configured")
return
}
var req struct {
Index int `json:"index"`
Watts float64 `json:"watts"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Watts <= 0 {
writeError(w, http.StatusBadRequest, "watts must be > 0")
return
}
result, err := h.opts.App.SetNvidiaGPUPowerLimit(req.Index, req.Watts)
status := "ok"
if err != nil {
status = "error"
}
writeJSON(w, map[string]string{"status": status, "output": result.Body})
}
func (h *handler) handleAPIGNVIDIAResetDefaults(w http.ResponseWriter, _ *http.Request) {
if h.opts.App == nil {
writeError(w, http.StatusServiceUnavailable, "app not configured")
return
}
result, err := h.opts.App.ResetNvidiaGPUDefaults()
status := "ok"
if err != nil {
status = "error"
}
writeJSON(w, map[string]string{"status": status, "output": result.Body})
}
func (h *handler) handleAPIGPUPresence(w http.ResponseWriter, r *http.Request) { func (h *handler) handleAPIGPUPresence(w http.ResponseWriter, r *http.Request) {
if h.opts.App == nil { if h.opts.App == nil {
writeError(w, http.StatusServiceUnavailable, "app not configured") writeError(w, http.StatusServiceUnavailable, "app not configured")
+159
View File
@@ -404,11 +404,170 @@ loadNvidiaSelfHeal();
func renderTools() string { func renderTools() string {
return renderNVMeFormatCard() + ` return renderNVMeFormatCard() + `
` + renderGPUSettingsCard() + `
` + renderFRUEditorCard() + ` ` + renderFRUEditorCard() + `
` + renderRAIDMgmtCard() ` + renderRAIDMgmtCard()
} }
func renderGPUSettingsCard() string {
return `<div class="card"><div class="card-head card-head-actions">GPU Settings<div class="card-head-buttons">
<button class="btn btn-sm btn-secondary" onclick="gpuSettingsRefresh()">&#8635; Refresh</button>
<button class="btn btn-sm btn-secondary" onclick="gpuSettingsResetAll()">Reset All to Defaults</button>
</div></div><div class="card-body">
<p style="font-size:13px;color:var(--muted);margin-bottom:12px">ECC, MIG, and Confidential Computing modes persist across reboot but only take effect after a GPU reset (or host reboot). Power limit does NOT persist across reboot — it is reapplied here on demand.</p>
<div id="gpu-settings-status" style="font-size:13px;color:var(--muted);margin-bottom:8px">Loading GPU settings...</div>
<div id="gpu-settings-table"></div>
<div id="gpu-settings-out" style="display:none;margin-top:12px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
<span id="gpu-settings-out-label" style="font-size:12px;font-weight:600;color:var(--muted)">Output</span>
<span id="gpu-settings-out-status" style="font-size:12px"></span>
</div>
<div id="gpu-settings-terminal" class="terminal" style="max-height:220px;width:100%;box-sizing:border-box"></div>
</div>
</div></div>
<script>
function gpuSettingsShowResult(label, status, output) {
var out = document.getElementById('gpu-settings-out');
var term = document.getElementById('gpu-settings-terminal');
var statusEl = document.getElementById('gpu-settings-out-status');
var labelEl = document.getElementById('gpu-settings-out-label');
out.style.display = 'block';
labelEl.textContent = label;
term.textContent = output || '(no output)';
term.scrollTop = term.scrollHeight;
if (status === 'ok') {
statusEl.textContent = '✓ done';
statusEl.style.color = 'var(--ok-fg, #2c662d)';
} else {
statusEl.textContent = '✗ failed';
statusEl.style.color = 'var(--crit-fg, #9f3a38)';
}
}
function gpuSettingsToggleMode(url, label, index, enable, btn) {
var original = btn.textContent;
btn.disabled = true;
btn.textContent = '...';
gpuSettingsShowResult(label + ' gpu ' + index, 'ok', 'Running...');
fetch(url, {
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({index:index, enabled:enable})
}).then(r=>r.json()).then(d => {
gpuSettingsShowResult(label + ' gpu ' + index, d.status || 'error', d.output || '(no output)');
setTimeout(gpuSettingsRefresh, 800);
}).catch(e => {
gpuSettingsShowResult(label + ' gpu ' + index, 'error', 'Request failed: ' + e);
}).finally(() => {
btn.disabled = false;
btn.textContent = original;
});
}
function gpuSettingsToggleECC(index, enable, btn) {
gpuSettingsToggleMode('/api/gpu/nvidia-ecc', 'ecc', index, enable, btn);
}
function gpuSettingsToggleMIG(index, enable, btn) {
gpuSettingsToggleMode('/api/gpu/nvidia-mig', 'mig', index, enable, btn);
}
function gpuSettingsToggleCC(index, enable, btn) {
gpuSettingsToggleMode('/api/gpu/nvidia-cc', 'cc', index, enable, btn);
}
function gpuSettingsApplyPowerLimit(index, inputId, btn) {
var input = document.getElementById(inputId);
var watts = parseFloat(input.value);
if (!watts || watts <= 0) {
gpuSettingsShowResult('power limit gpu ' + index, 'error', 'Enter a valid wattage.');
return;
}
var original = btn.textContent;
btn.disabled = true;
btn.textContent = 'Applying...';
gpuSettingsShowResult('power limit gpu ' + index, 'ok', 'Running...');
fetch('/api/gpu/nvidia-power-limit', {
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({index:index, watts:watts})
}).then(r=>r.json()).then(d => {
gpuSettingsShowResult('power limit gpu ' + index, d.status || 'error', d.output || '(no output)');
setTimeout(gpuSettingsRefresh, 800);
}).catch(e => {
gpuSettingsShowResult('power limit gpu ' + index, 'error', 'Request failed: ' + e);
}).finally(() => {
btn.disabled = false;
btn.textContent = original;
});
}
function gpuSettingsResetAll() {
if (!confirm('Reset ECC, MIG, Confidential Computing, and power limit to factory defaults on ALL GPUs?')) return;
gpuSettingsShowResult('reset all GPUs to defaults', 'ok', 'Running...');
fetch('/api/gpu/nvidia-reset-defaults', {method:'POST'}).then(r=>r.json()).then(d => {
gpuSettingsShowResult('reset all GPUs to defaults', d.status || 'error', d.output || '(no output)');
setTimeout(gpuSettingsRefresh, 800);
}).catch(e => {
gpuSettingsShowResult('reset all GPUs to defaults', 'error', 'Request failed: ' + e);
});
}
function gpuSettingsRefresh() {
var status = document.getElementById('gpu-settings-status');
var table = document.getElementById('gpu-settings-table');
status.textContent = 'Loading GPU settings...';
status.style.color = 'var(--muted)';
fetch('/api/gpu/nvidia-settings').then(r=>r.json()).then(gpus => {
if (!Array.isArray(gpus) || gpus.length === 0) {
status.textContent = 'No NVIDIA GPUs detected or nvidia-smi is unavailable.';
table.innerHTML = '';
return;
}
status.textContent = gpus.length + ' NVIDIA GPU(s) detected.';
const rows = gpus.map(g => {
const eccEnabled = /enabled/i.test(g.ecc_current);
const eccPendingDiffers = g.ecc_pending && g.ecc_current && g.ecc_pending.toLowerCase() !== g.ecc_current.toLowerCase();
const migSupported = g.mig_current && !/n\/a/i.test(g.mig_current);
const migEnabled = /enabled/i.test(g.mig_current);
const migPendingDiffers = g.mig_pending && g.mig_current && g.mig_pending.toLowerCase() !== g.mig_current.toLowerCase();
const ccSupported = !!g.cc_state;
const ccEnabled = /^on$/i.test(g.cc_state || '');
const inputId = 'gpu-pl-' + g.index;
return '<tr>'
+ '<td style="white-space:nowrap">' + g.index + '</td>'
+ '<td>' + (g.name || 'unknown') + '</td>'
+ '<td style="white-space:nowrap">'
+ (g.ecc_current || 'N/A')
+ (eccPendingDiffers ? '<div style="font-size:11px;color:var(--warn-fg,#573a08)">pending: ' + g.ecc_pending + ' (reset required)</div>' : '')
+ '<div style="margin-top:4px"><button class="btn btn-sm btn-secondary" onclick="gpuSettingsToggleECC(' + g.index + ', ' + (!eccEnabled) + ', this)">' + (eccEnabled ? 'Disable' : 'Enable') + '</button></div>'
+ '</td>'
+ '<td style="white-space:nowrap">'
+ (migSupported ? (g.mig_current +
(migPendingDiffers ? '<div style="font-size:11px;color:var(--warn-fg,#573a08)">pending: ' + g.mig_pending + ' (reset required)</div>' : '') +
'<div style="margin-top:4px"><button class="btn btn-sm btn-secondary" onclick="gpuSettingsToggleMIG(' + g.index + ', ' + (!migEnabled) + ', this)">' + (migEnabled ? 'Disable' : 'Enable') + '</button></div>')
: '<span style="color:var(--muted)">N/A</span>')
+ '</td>'
+ '<td style="white-space:nowrap">'
+ (ccSupported ? (g.cc_state +
'<div style="margin-top:4px"><button class="btn btn-sm btn-secondary" onclick="gpuSettingsToggleCC(' + g.index + ', ' + (!ccEnabled) + ', this)">' + (ccEnabled ? 'Disable' : 'Enable') + '</button></div>')
: '<span style="color:var(--muted)">N/A</span>')
+ '</td>'
+ '<td style="white-space:nowrap">' + (g.power_limit_w ? g.power_limit_w + ' W' : 'N/A')
+ (g.power_min_limit_w && g.power_max_limit_w ? '<div style="font-size:11px;color:var(--muted)">range ' + g.power_min_limit_w + '-' + g.power_max_limit_w + ' W, default ' + g.power_default_limit_w + ' W</div>' : '')
+ '</td>'
+ '<td style="white-space:nowrap">'
+ '<input id="' + inputId + '" type="number" min="' + (g.power_min_limit_w||0) + '" max="' + (g.power_max_limit_w||0) + '" value="' + (g.power_limit_w||'') + '" style="width:80px;padding:3px 6px;border:1.5px solid #888;border-radius:3px;font-size:13px" /> '
+ '<button class="btn btn-sm btn-secondary" onclick="gpuSettingsApplyPowerLimit(' + g.index + ', \'' + inputId + '\', this)">Apply</button>'
+ '</td>'
+ '</tr>';
}).join('');
table.innerHTML = '<table><tr><th>GPU</th><th>Model</th><th>ECC</th><th>MIG</th><th>Confidential Computing</th><th>Power Limit</th><th>Set Power Limit (W)</th></tr>' + rows + '</table>';
}).catch(e => {
status.textContent = 'Error loading GPU settings: ' + e;
status.style.color = 'var(--crit-fg, #9f3a38)';
table.innerHTML = '';
});
}
gpuSettingsRefresh();
</script>`
}
func renderFRUEditorCard() string { func renderFRUEditorCard() string {
return `<div class="card"><div class="card-head card-head-actions">FRU / Elabel<div class="card-head-buttons"><button class="btn btn-sm btn-secondary" onclick="fruAllRead()">Read All</button></div></div><div class="card-body"> return `<div class="card"><div class="card-head card-head-actions">FRU / Elabel<div class="card-head-buttons"><button class="btn btn-sm btn-secondary" onclick="fruAllRead()">Read All</button></div></div><div class="card-body">
<p style="font-size:13px;color:var(--muted);margin-bottom:12px">Reads and edits hardware identity fields from all available sources. Each field shows its source method.</p> <p style="font-size:13px;color:var(--muted);margin-bottom:12px">Reads and edits hardware identity fields from all available sources. Each field shows its source method.</p>
+6
View File
@@ -331,6 +331,12 @@ func NewHandler(opts HandlerOptions) http.Handler {
mux.HandleFunc("GET /api/gpu/nvidia", h.handleAPIGNVIDIAGPUs) mux.HandleFunc("GET /api/gpu/nvidia", h.handleAPIGNVIDIAGPUs)
mux.HandleFunc("GET /api/gpu/nvidia-status", h.handleAPIGNVIDIAGPUStatuses) mux.HandleFunc("GET /api/gpu/nvidia-status", h.handleAPIGNVIDIAGPUStatuses)
mux.HandleFunc("POST /api/gpu/nvidia-reset", h.handleAPIGNVIDIAReset) mux.HandleFunc("POST /api/gpu/nvidia-reset", h.handleAPIGNVIDIAReset)
mux.HandleFunc("GET /api/gpu/nvidia-settings", h.handleAPIGNVIDIAGPUSettings)
mux.HandleFunc("POST /api/gpu/nvidia-ecc", h.handleAPIGNVIDIASetECC)
mux.HandleFunc("POST /api/gpu/nvidia-mig", h.handleAPIGNVIDIASetMIG)
mux.HandleFunc("POST /api/gpu/nvidia-cc", h.handleAPIGNVIDIASetCCMode)
mux.HandleFunc("POST /api/gpu/nvidia-power-limit", h.handleAPIGNVIDIASetPowerLimit)
mux.HandleFunc("POST /api/gpu/nvidia-reset-defaults", h.handleAPIGNVIDIAResetDefaults)
mux.HandleFunc("GET /api/gpu/tools", h.handleAPIGPUTools) mux.HandleFunc("GET /api/gpu/tools", h.handleAPIGPUTools)
// System // System