config: per-engine AMD GPU power-state lock (Polaris compute-hang mitigation)

New server.dpm_force_performance_level_overrides (engine name → level,
e.g. {"radeon":"high"}). At engine startup the engine writes the level
to every amdgpu card's power_dpm_force_performance_level — locking a
Polaris/GCN card to fixed top clocks avoids the DPM power-state
transitions that hang these cards under sustained Vulkan compute
(current default is 'auto', the hang-prone mode). Card is matched by PCI
vendor id (0x1002), robust to DRM card renumbering across resets.
Best-effort: an unprivileged container logs the exact host command on
PermissionError.

Pairs with two config-only stability levers applied for the radeon:
n_ctx reduction (qwen3 1024→512, gme 1536→1024) to keep all three
embedders in real VRAM with headroom (no GTT-over-PCIe spill during
compute — another Polaris hang trigger), and max_parallel_requests
override {"radeon":1} to serialize Vulkan submissions.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
parent 0c69e4bc
...@@ -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.52" __version__ = "0.1.53"
# 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
......
...@@ -40,6 +40,13 @@ class ServerConfig: ...@@ -40,6 +40,13 @@ class ServerConfig:
# override lets a bigger card run more concurrently than a smaller one. Blank = # override lets a bigger card run more concurrently than a smaller one. Blank =
# use the default above. # use the default above.
max_parallel_requests_overrides: dict = field(default_factory=dict) max_parallel_requests_overrides: dict = field(default_factory=dict)
# Per-engine AMD GPU power/clock lock (engine name → level, e.g.
# {"radeon": "high"}). Applied at engine startup: writes the level to each
# amdgpu card's power_dpm_force_performance_level. Stabilises Polaris/GCN
# cards that hang under sustained Vulkan compute due to DPM clock switching.
# Levels: auto | low | high | manual | profile_standard | profile_peak.
# Best-effort — an unprivileged container logs the manual host command.
dpm_force_performance_level_overrides: dict = field(default_factory=dict)
# ─── Frontend/engine split ─────────────────────────────────────────────── # ─── Frontend/engine split ───────────────────────────────────────────────
# coderai boots a thin, always-responsive *front* reverse proxy on the public # coderai boots a thin, always-responsive *front* reverse proxy on the public
# host/port and supervises one or more *engine* subprocesses (which do all # host/port and supervises one or more *engine* subprocesses (which do all
...@@ -659,6 +666,7 @@ class ConfigManager: ...@@ -659,6 +666,7 @@ class ConfigManager:
"queue_max_size": self.config.server.queue_max_size, "queue_max_size": self.config.server.queue_max_size,
"max_parallel_requests": self.config.server.max_parallel_requests, "max_parallel_requests": self.config.server.max_parallel_requests,
"max_parallel_requests_overrides": self.config.server.max_parallel_requests_overrides, "max_parallel_requests_overrides": self.config.server.max_parallel_requests_overrides,
"dpm_force_performance_level_overrides": self.config.server.dpm_force_performance_level_overrides,
"internal_port_base": self.config.server.internal_port_base, "internal_port_base": self.config.server.internal_port_base,
"engines": self.config.server.engines, "engines": self.config.server.engines,
"engine_gpus": self.config.server.engine_gpus, "engine_gpus": self.config.server.engine_gpus,
......
...@@ -372,6 +372,13 @@ class EngineSupervisor: ...@@ -372,6 +372,13 @@ class EngineSupervisor:
env["CODERAI_MAX_PARALLEL"] = str(int(par)) env["CODERAI_MAX_PARALLEL"] = str(int(par))
if inst is not None: if inst is not None:
env["CODERAI_MAX_MODEL_INSTANCES"] = str(int(inst)) env["CODERAI_MAX_MODEL_INSTANCES"] = str(int(inst))
# Per-engine AMD GPU power-state lock (e.g. {"radeon": "high"}) — the
# engine applies it to its amdgpu card(s) at startup to avoid Polaris
# DPM-transition hangs under sustained Vulkan compute.
_dpm = (getattr(srv, "dpm_force_performance_level_overrides", None)
or {}).get(engine.name)
if _dpm:
env["CODERAI_DPM_FORCE"] = str(_dpm)
# Force this engine's backend (the engine reads this in --engine-only mode # Force this engine's backend (the engine reads this in --engine-only mode
# and overrides config.backend.type) so a Vulkan/Radeon engine doesn't # and overrides config.backend.type) so a Vulkan/Radeon engine doesn't
# auto-pick CUDA, and vice-versa. # auto-pick CUDA, and vice-versa.
......
...@@ -401,6 +401,52 @@ def engine_gpu_stats() -> list: ...@@ -401,6 +401,52 @@ def engine_gpu_stats() -> list:
if c.get("vendor") in sels or (c.get("uuid") and c["uuid"] in sels)] if c.get("vendor") in sels or (c.get("uuid") and c["uuid"] in sels)]
_VALID_DPM_LEVELS = ("auto", "low", "high", "manual",
"profile_standard", "profile_min_sclk", "profile_min_mclk",
"profile_peak")
def apply_amd_dpm_force_level(level: str) -> None:
"""Write ``level`` to every amdgpu card's power_dpm_force_performance_level.
Locking a Polaris/GCN card to 'high' (fixed top clocks) avoids the DPM
power-state transitions that hang these cards under sustained Vulkan
compute. Best-effort: an unprivileged container can't write root-owned
sysfs, so on PermissionError we log the exact host command to run instead."""
import glob
level = (level or "").strip()
if level not in _VALID_DPM_LEVELS:
print(f"[dpm] ignoring invalid level '{level}' "
f"(valid: {', '.join(_VALID_DPM_LEVELS)})", flush=True)
return
wrote, denied = [], []
for card in sorted(glob.glob("/sys/class/drm/card*")):
dev = os.path.join(card, "device")
try:
with open(os.path.join(dev, "vendor")) as f:
if f.read().strip().lower() != "0x1002": # AMD PCI vendor id
continue
except OSError:
continue
node = os.path.join(dev, "power_dpm_force_performance_level")
if not os.path.exists(node):
continue
try:
with open(node, "w") as f:
f.write(level + "\n")
wrote.append(os.path.basename(card))
except PermissionError:
denied.append(node)
except OSError as e:
print(f"[dpm] failed to set {node}: {e}", flush=True)
if wrote:
print(f"[dpm] locked AMD power level '{level}' on {', '.join(wrote)}",
flush=True)
for node in denied:
print(f"[dpm] cannot set power level (unprivileged container). Run once "
f"on the HOST: echo {level} | sudo tee {node}", flush=True)
def card_key(vendor: str, uuid: str = "", pci: str = "") -> str: def card_key(vendor: str, uuid: str = "", pci: str = "") -> str:
"""Stable identifier for one physical GPU, computable identically by the front """Stable identifier for one physical GPU, computable identically by the front
(which sees every card) and by an engine (which sees only its own, in llama.cpp (which sees every card) and by an engine (which sees only its own, in llama.cpp
......
...@@ -635,6 +635,18 @@ def main(): ...@@ -635,6 +635,18 @@ def main():
if _forced_backend: if _forced_backend:
config.backend.type = _forced_backend config.backend.type = _forced_backend
print(f"[engine] backend forced to '{_forced_backend}' by the front") print(f"[engine] backend forced to '{_forced_backend}' by the front")
# Optional: pin this engine's AMD GPU(s) to a fixed power/clock state.
# Polaris (and other GCN) cards frequently HANG under sustained Vulkan
# compute when the driver switches DPM power states mid-kernel; locking
# to 'high' avoids those transitions. Configured per-engine via
# server.dpm_force_performance_level_overrides (env CODERAI_DPM_FORCE).
_dpm = os.environ.get("CODERAI_DPM_FORCE")
if _dpm:
try:
from codai.frontproxy.gpu_detect import apply_amd_dpm_force_level
apply_amd_dpm_force_level(_dpm.strip())
except Exception as _de:
print(f"[dpm] apply failed: {_de}", flush=True)
# The front owns the AISBF broker (always-responsive, one registration for # The front owns the AISBF broker (always-responsive, one registration for
# the whole node, routes to engines). So no engine runs its own broker # the whole node, routes to engines). So no engine runs its own broker
# client — that would double-register and stall when the engine loads. # client — that would double-register and stall when the engine loads.
......
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