feat: a GPU agent on the CPU shows as degraded, in the System view and on its own page (4410)
CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 3s
CI and images / frontend-build (push) Successful in 27s
CI and images / backend-lint-and-test (push) Successful in 34s
CI and images / integration (push) Successful in 2m22s
CI and images / sign-extension (push) Successful in 4s
CI and images / build-web (push) Successful in 2m46s
CI and images / smoke-web (push) Successful in 50s
CI and images / build-agent (push) Successful in 6m35s
CI and images / promote (push) Successful in 2s

torch and onnxruntime both fall back to the CPU without raising, so the agent
that ran CPU-bound for weeks after a driver update leased and checked in like
a healthy one.

- The agent sends its startup accel report on every lease and heartbeat.
- The server keeps a bounded copy on the roster row. A running agent with a
  runtime off the GPU becomes `degraded`, with a sentence naming the runtime
  and the reason.
- The top nav shows it amber.
- The agent page carries a banner, and its pill reads "CPU only".

Also: the bandwidth field gets the page's − / + stepper, and both number
fields drop the browser's spin arrows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-24 19:09:07 -04:00
co-authored by Claude Opus 5.5
parent e39ec1c550
commit cc53d8db7b
10 changed files with 210 additions and 10 deletions
+43 -3
View File
@@ -72,9 +72,14 @@ assert STALE_AFTER_SECONDS >= SWEEP_PERIOD_SECONDS * _SWEEPS_BEFORE_STALE, (
PROBE_TIMEOUT_SECONDS = 2.0
_OK, _STALE, _DOWN, _UNKNOWN = "ok", "stale", "down", "unknown"
# Checking in, but working at a fraction of its speed: a GPU agent whose
# runtimes fell back to the CPU (#4410). Below stale — a part that may have
# stopped is the more urgent question — and above unknown, because this one
# IS known to be wrong.
_DEGRADED = "degraded"
# Worst-first, so an overall verdict is just the max.
_SEVERITY = {_OK: 0, _UNKNOWN: 1, _STALE: 2, _DOWN: 3}
_SEVERITY = {_OK: 0, _UNKNOWN: 1, _DEGRADED: 2, _STALE: 3, _DOWN: 4}
def _age_state(age_seconds: float) -> str:
@@ -100,6 +105,39 @@ def _describe_learned(name: str, state: str, age: float, details: dict) -> str:
return f"{name} has not checked in for {ago} — treat it as stopped"
def _cpu_runtimes(details: dict) -> list[str]:
"""The runtimes an agent reported as NOT on the GPU, with why.
Both torch and onnxruntime fall back to the CPU without raising, so an
agent in that state leases, works and checks in exactly like a healthy
one. On 2026-09-24 one had been doing so since a driver update left a
stale CDI spec; the only sign was a line in the agent's own log.
"""
accel = details.get("accel")
if not isinstance(accel, dict):
return []
out = []
for name, entry in sorted(accel.items()):
if not isinstance(entry, dict) or entry.get("device") == "cuda":
continue
why = entry.get("error") or entry.get("device") or "unknown"
out.append(f"{name} ({why})")
return out
def _learned_state(name: str, state: str, age: float, details: dict) -> tuple[str, str]:
"""A roster row's state and its sentence, degraded included."""
if state == _OK:
cpu = _cpu_runtimes(details)
if cpu:
return _DEGRADED, (
f"{name} is running on the CPU — not on the GPU: {'; '.join(cpu)}. "
"After a driver update, regenerate the agent host's CDI spec "
"(agent README)."
)
return state, _describe_learned(name, state, age, details)
async def _probe_postgres(session) -> dict:
started = time.monotonic()
try:
@@ -175,13 +213,15 @@ async def system_health():
).scalars().all()
for row in rows:
age = (now - row.last_seen_at).total_seconds()
state = _age_state(age)
state, detail = _learned_state(
row.display_name, _age_state(age), age, row.details or {},
)
parts.append({
"key": row.key,
"kind": row.kind,
"name": row.display_name,
"state": state,
"detail": _describe_learned(row.display_name, state, age, row.details or {}),
"detail": detail,
"last_seen_at": row.last_seen_at.isoformat(),
"first_seen_at": row.first_seen_at.isoformat(),
**{k: v for k, v in (row.details or {}).items() if k != "agent_id"},