admin: make per-engine GPU power lock configurable in the web UI

The per-engine concurrency-overrides table on the Settings page already
exposes max_parallel_requests per engine; add a "GPU power lock" column
(dpm level select) so dpm_force_performance_level_overrides is editable
too. Backend: GET returns the map, POST saves it via a level-validating
sanitizer. Takes effect on engine restart.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
parent 91ee14d0
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API # Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here. # metadata and the admin web UI read from here.
__version__ = "0.1.53" __version__ = "0.1.54"
# Configure the CUDA caching allocator BEFORE torch is imported anywhere. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
# expandable_segments lets the allocator return freed pages to the driver even # expandable_segments lets the allocator return freed pages to the driver even
......
...@@ -2320,6 +2320,22 @@ def _sanitize_engine_int_overrides(raw) -> dict: ...@@ -2320,6 +2320,22 @@ def _sanitize_engine_int_overrides(raw) -> dict:
return out return out
_DPM_LEVELS = ("auto", "low", "high", "manual", "profile_standard",
"profile_min_sclk", "profile_min_mclk", "profile_peak")
def _sanitize_engine_dpm_overrides(raw) -> dict:
"""Clean a {engine_name: dpm_level} map: keep valid AMD power levels, drop the
rest (and blank/'default' → not set, so the card keeps its OS default)."""
out = {}
if isinstance(raw, dict):
for name, val in raw.items():
lvl = str(val or "").strip().lower()
if lvl and lvl != "default" and lvl in _DPM_LEVELS:
out[str(name)] = lvl
return out
def _resolve_engine_spec(engine_name: str, engine_specs): def _resolve_engine_spec(engine_name: str, engine_specs):
"""Find the declared engine matching ``engine_name`` (by name or backend).""" """Find the declared engine matching ``engine_name`` (by name or backend)."""
for s in (engine_specs or []): for s in (engine_specs or []):
...@@ -3165,6 +3181,8 @@ def build_settings_dict(c, gpu_cards): ...@@ -3165,6 +3181,8 @@ def build_settings_dict(c, gpu_cards):
"queue_max_size": c.server.queue_max_size, "queue_max_size": c.server.queue_max_size,
"max_parallel_requests": c.server.max_parallel_requests, "max_parallel_requests": c.server.max_parallel_requests,
"max_parallel_requests_overrides": c.server.max_parallel_requests_overrides, "max_parallel_requests_overrides": c.server.max_parallel_requests_overrides,
"dpm_force_performance_level_overrides": getattr(
c.server, "dpm_force_performance_level_overrides", {}) or {},
"internal_port_base": c.server.internal_port_base, "internal_port_base": c.server.internal_port_base,
"default_engine": c.server.default_engine, "default_engine": c.server.default_engine,
# Engine names available to pick as the default (for the settings UI). # Engine names available to pick as the default (for the settings UI).
...@@ -3320,6 +3338,10 @@ async def api_save_settings(request: Request, username: str = Depends(require_ad ...@@ -3320,6 +3338,10 @@ async def api_save_settings(request: Request, username: str = Depends(require_ad
if "max_parallel_requests_overrides" in srv: if "max_parallel_requests_overrides" in srv:
c.server.max_parallel_requests_overrides = _sanitize_engine_int_overrides( c.server.max_parallel_requests_overrides = _sanitize_engine_int_overrides(
srv["max_parallel_requests_overrides"]) srv["max_parallel_requests_overrides"])
if "dpm_force_performance_level_overrides" in srv:
c.server.dpm_force_performance_level_overrides = (
_sanitize_engine_dpm_overrides(
srv["dpm_force_performance_level_overrides"]))
if "internal_port_base" in srv: if "internal_port_base" in srv:
try: try:
c.server.internal_port_base = max(1, min(65535, int(srv["internal_port_base"]))) c.server.internal_port_base = max(1, min(65535, int(srv["internal_port_base"])))
......
...@@ -82,7 +82,8 @@ ...@@ -82,7 +82,8 @@
<div id="concurrency-overrides" style="margin-top:.7rem;display:none"> <div id="concurrency-overrides" style="margin-top:.7rem;display:none">
<label class="form-label" style="margin-bottom:.3rem">Per-engine overrides <span class="muted">(blank = use the defaults above)</span></label> <label class="form-label" style="margin-bottom:.3rem">Per-engine overrides <span class="muted">(blank = use the defaults above)</span></label>
<span class="form-hint" style="margin-bottom:.4rem">Give a bigger card more concurrency than a smaller one — e.g. more parallel requests on a 24 GB NVIDIA than an 8 GB Radeon.</span> <span class="form-hint" style="margin-bottom:.4rem">Give a bigger card more concurrency than a smaller one — e.g. more parallel requests on a 24 GB NVIDIA than an 8 GB Radeon.</span>
<div id="concurrency-override-rows" style="display:grid;grid-template-columns:110px 1fr 1fr;gap:.5rem;align-items:center"></div> <div id="concurrency-override-rows" style="display:grid;grid-template-columns:110px 1fr 1fr 1.3fr;gap:.5rem;align-items:center"></div>
<span class="form-hint" style="margin-top:.4rem"><b>GPU power lock</b> pins an AMD card to a fixed power/clock state, avoiding the DPM transitions that hang Polaris/GCN cards under sustained Vulkan compute. <em>high</em> = max clocks (runs hotter). Applied at engine start; in an unprivileged container it logs the host command instead.</span>
</div> </div>
</div> </div>
...@@ -622,19 +623,26 @@ function _collectGpuOverrides(){ ...@@ -622,19 +623,26 @@ function _collectGpuOverrides(){
return ov; return ov;
} }
// Per-engine concurrency overrides — one row per running engine (by name). // Per-engine concurrency overrides — one row per running engine (by name).
function _buildConcurrencyOverrides(engNames, parOv, instOv){ function _buildConcurrencyOverrides(engNames, parOv, instOv, dpmOv){
const wrap = document.getElementById('concurrency-overrides'); const wrap = document.getElementById('concurrency-overrides');
const rows = document.getElementById('concurrency-override-rows'); const rows = document.getElementById('concurrency-override-rows');
if(!engNames || engNames.length < 2){ wrap.style.display='none'; rows.innerHTML=''; return; } if(!engNames || engNames.length < 2){ wrap.style.display='none'; rows.innerHTML=''; return; }
wrap.style.display = ''; wrap.style.display = '';
dpmOv = dpmOv || {};
const DPM = ['','auto','low','high','profile_standard','profile_peak'];
const dpmSel = (n) => `<select class="form-input conc-dpm" data-engine="${n}">` +
DPM.map(l => `<option value="${l}"${(dpmOv[n]||'')===l?' selected':''}>${l||'default'}</option>`).join('') +
`</select>`;
rows.innerHTML = rows.innerHTML =
`<span class="muted" style="font-size:11px">Engine</span> `<span class="muted" style="font-size:11px">Engine</span>
<span class="muted" style="font-size:11px">Max parallel</span> <span class="muted" style="font-size:11px">Max parallel</span>
<span class="muted" style="font-size:11px">Max instances</span>` + <span class="muted" style="font-size:11px">Max instances</span>
<span class="muted" style="font-size:11px">GPU power lock</span>` +
engNames.map(n => ` engNames.map(n => `
<span style="font-size:12px">${n}</span> <span style="font-size:12px">${n}</span>
<input type="number" class="form-input conc-par" data-engine="${n}" min="1" max="64" placeholder="default" value="${parOv[n] ?? ''}"> <input type="number" class="form-input conc-par" data-engine="${n}" min="1" max="64" placeholder="default" value="${parOv[n] ?? ''}">
<input type="number" class="form-input conc-inst" data-engine="${n}" min="1" max="16" placeholder="default" value="${instOv[n] ?? ''}">`).join(''); <input type="number" class="form-input conc-inst" data-engine="${n}" min="1" max="16" placeholder="default" value="${instOv[n] ?? ''}">
${dpmSel(n)}`).join('');
} }
function _collectConcOverrides(cls){ function _collectConcOverrides(cls){
const o = {}; const o = {};
...@@ -644,6 +652,14 @@ function _collectConcOverrides(cls){ ...@@ -644,6 +652,14 @@ function _collectConcOverrides(cls){
}); });
return o; return o;
} }
function _collectDpmOverrides(){
const o = {};
document.querySelectorAll('#concurrency-override-rows .conc-dpm').forEach(sel => {
const v = (sel.value || '').trim();
if(v) o[sel.dataset.engine] = v;
});
return o;
}
function toggleDs4Fields(){ function toggleDs4Fields(){
document.getElementById('ds4-fields').style.display = document.getElementById('ds4-fields').style.display =
document.getElementById('s-ds4-enabled').checked ? 'block' : 'none'; document.getElementById('s-ds4-enabled').checked ? 'block' : 'none';
...@@ -818,7 +834,8 @@ async function loadSettings(){ ...@@ -818,7 +834,8 @@ async function loadSettings(){
document.getElementById('s-max-instances').value = d.models?.max_model_instances ?? 1; document.getElementById('s-max-instances').value = d.models?.max_model_instances ?? 1;
_buildConcurrencyOverrides(engNames, _buildConcurrencyOverrides(engNames,
d.server?.max_parallel_requests_overrides || {}, d.server?.max_parallel_requests_overrides || {},
d.models?.max_model_instances_overrides || {}); d.models?.max_model_instances_overrides || {},
d.server?.dpm_force_performance_level_overrides || {});
document.getElementById('s-hf-cache').value = d.models?.hf_cache_dir ?? ''; document.getElementById('s-hf-cache').value = d.models?.hf_cache_dir ?? '';
document.getElementById('s-gguf-cache').value = d.models?.gguf_cache_dir ?? ''; document.getElementById('s-gguf-cache').value = d.models?.gguf_cache_dir ?? '';
document.getElementById('s-offload-dir').value = d.offload?.directory ?? './offload'; document.getElementById('s-offload-dir').value = d.offload?.directory ?? './offload';
...@@ -929,6 +946,7 @@ async function saveSettings(){ ...@@ -929,6 +946,7 @@ async function saveSettings(){
internal_port_base: parseInt(document.getElementById('s-internal-port-base').value) || 8780, internal_port_base: parseInt(document.getElementById('s-internal-port-base').value) || 8780,
max_parallel_requests: parseInt(document.getElementById('s-max-parallel').value) || 2, max_parallel_requests: parseInt(document.getElementById('s-max-parallel').value) || 2,
max_parallel_requests_overrides: _collectConcOverrides('conc-par'), max_parallel_requests_overrides: _collectConcOverrides('conc-par'),
dpm_force_performance_level_overrides: _collectDpmOverrides(),
default_engine: document.getElementById('s-default-engine').value || null, default_engine: document.getElementById('s-default-engine').value || null,
}, },
models:{ models:{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment