front: serve /admin/api/gpu-stats from the front (keep temps live when an engine is busy)

gpu-stats fell through to the generic proxy, which forwards via the long-timeout
client to the primary engine. When that engine is saturated generating (sync
llama.cpp holding its event loop), the request blocked → the dashboard's temp/GPU
polling hung → the whole web UI went unresponsive and the task page stopped showing
temperatures.

Serve it from the front instead, using the already torch-free gpu_detect.gpu_stats()
(nvidia-smi + AMD sysfs, reports every card regardless of CUDA_VISIBLE_DEVICES), run
in a thread so the subprocess never blocks the front's event loop, and registered
before the catch-all so it's not proxied to a busy engine. Light auth (session
cookie / bearer presence) since GPU telemetry is low-sensitivity and full session
validation lives on the engine — which is the component we're decoupling from.

Note: the thermal THROTTLE (pausing a generation when hot) stays in the engine —
it must, to pause that engine's own forward pass — but its stats are now front-served.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 6e111277
......@@ -375,6 +375,28 @@ class FrontProxy:
except Exception:
return False
async def gpu_stats(self, request: Request) -> Response:
"""Serve per-card GPU stats (util/VRAM/temperature) FROM THE FRONT, using the
torch-free gpu_detect enumeration (nvidia-smi + AMD sysfs). This keeps the
dashboard's temps/stats live even when an engine is saturated generating —
previously /admin/api/gpu-stats fell through to the generic proxy and blocked
on the busy engine, hanging the whole UI. Run in a thread so the subprocess
(nvidia-smi) never blocks the front's event loop. Auth is light on purpose:
GPU telemetry is low-sensitivity, and full session validation lives on the
(possibly busy) engine, which would defeat the point — so we only require a
session cookie or bearer token to be present."""
has_cred = bool(request.cookies.get("session")) or \
request.headers.get("authorization", "").lower().startswith("bearer ")
if not has_cred:
return JSONResponse({"detail": "Unauthorized"}, status_code=401)
import asyncio
from codai.frontproxy.gpu_detect import gpu_stats as _gpu_stats
try:
cards = await asyncio.to_thread(_gpu_stats)
except Exception as exc:
return JSONResponse({"cards": [], "error": str(exc)})
return JSONResponse({"cards": cards})
def _canonical_loaded(self, keys) -> list:
"""Map an engine's loaded-model keys to canonical model ids, deduped.
......@@ -801,6 +823,13 @@ def build_app(config, config_dir=None) -> FastAPI:
async def _system_stats(request: Request):
return await front.poll(request)
# GPU stats are served by the FRONT (torch-free gpu_detect) so temps/util stay
# live even when an engine is busy generating — registered before the catch-all
# so it isn't proxied to a (possibly blocked) engine.
@app.get("/admin/api/gpu-stats", include_in_schema=False)
async def _gpu_stats(request: Request):
return await front.gpu_stats(request)
# /v1/models is the union across engines (each engine registers only the models
# the front assigned to it). Registered before the catch-all so it's aggregated.
@app.get("/v1/models", include_in_schema=False)
......
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