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:
co-authored by
Claude Sonnet 5
parent
2599d9c5e3
commit
53c46465d2
@@ -404,11 +404,170 @@ loadNvidiaSelfHeal();
|
||||
func renderTools() string {
|
||||
return renderNVMeFormatCard() + `
|
||||
|
||||
` + renderGPUSettingsCard() + `
|
||||
|
||||
` + renderFRUEditorCard() + `
|
||||
|
||||
` + 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()">↻ 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 {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user