front: serve settings (GET) locally; engine-freeze root-cause = GIL contention

Root-cause finding (Path B): the engine event loop is genuinely freed during
GGUF generation (work runs via asyncio.to_thread; llama_decode releases the GIL),
so it's not a hard-blocked loop. But under CONTINUOUS generation the per-token
Python work in llama-cpp-python keeps re-grabbing the GIL and starves the
engine's own API handlers (sync ones especially) — an intrinsic ceiling that
can't be cleanly removed while generating. So per "B first, A fallback", falling
back to A: serve page data from the front (a separate process the GIL can't
touch).

This commit: /admin/api/settings GET is now served by the front. api_get_settings
was refactored into a pure build_settings_dict(config, gpu_cards) shared by the
engine handler and the front (front holds the same Config), so both return an
identical payload. Front uses its torch-free gpu_detect.gpu_cards(). POST (save)
still falls through to the engine, which persists + applies it.

Verified: build_settings_dict standalone + front /admin/api/settings via TestClient.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
parent a31b7863
......@@ -3417,7 +3417,13 @@ async def api_get_settings(username: str = Depends(require_admin)):
"""Return current config.json as JSON."""
if config_manager is None or config_manager.config is None:
raise HTTPException(status_code=503, detail="Config manager not initialized")
c = config_manager.config
return build_settings_dict(config_manager.config, _detect_gpu_cards())
def build_settings_dict(c, gpu_cards):
"""Pure ``Config`` → settings dict. Shared by the engine handler and the front
proxy (which holds the same Config) so both serve an identical
/admin/api/settings without the front having to round-trip to the engine."""
return {
"server": {
"host": c.server.host,
......@@ -3469,7 +3475,7 @@ async def api_get_settings(username: str = Depends(require_admin)):
"split_card_caps_gb": c.offload.split_card_caps_gb,
# Every physical card on the machine, so the UI can render a per-card cap
# row (key + name) and the user can cap each independently.
"gpu_cards": _detect_gpu_cards(),
"gpu_cards": gpu_cards,
},
"vulkan": {
"n_gpu_layers": c.vulkan.n_gpu_layers,
......
......@@ -22,7 +22,7 @@ from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
def register_admin_data(app: FastAPI, config_dir) -> bool:
def register_admin_data(app: FastAPI, config_dir, config=None) -> bool:
if not config_dir:
return False
try:
......@@ -159,6 +159,28 @@ def register_admin_data(app: FastAPI, config_dir) -> bool:
return JSONResponse({"detail": "Cannot delete user"}, status_code=400)
return JSONResponse({"success": True})
print("[front] serving tokens/users data locally (no engine round-trip)",
# ---------------------------------------------------------------- settings
# GET is a pure function of the Config the front already holds, so serve it
# locally (identical to the engine via the shared build_settings_dict). The
# POST (save) still falls through to the engine, which persists + applies it.
if config is not None:
@app.get("/admin/api/settings", include_in_schema=False)
async def _get_settings(request: Request):
_u, err = _admin(request)
if err:
return err
try:
from codai.admin.routes import build_settings_dict
from codai.frontproxy.gpu_detect import gpu_cards as _gpu_cards
try:
cards = _gpu_cards()
except Exception:
cards = []
return JSONResponse(build_settings_dict(config, cards))
except Exception as exc:
return JSONResponse({"detail": f"settings unavailable: {exc}"},
status_code=500)
print("[front] serving tokens/users/settings data locally (no engine round-trip)",
flush=True)
return True
......@@ -1194,7 +1194,7 @@ def build_app(config, config_dir=None) -> FastAPI:
# — answered from disk so they stay live while the engine is busy generating.
try:
from codai.frontproxy.admin_data import register_admin_data
register_admin_data(app, config_dir)
register_admin_data(app, config_dir, config=config)
except Exception as _exc:
print(f"[front] could not register local admin-data endpoints ({_exc}); "
f"they will be proxied to the engine", flush=True)
......
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