models: live "serving a request" dot on the engine/pin tag

The front already tracks which models are in-flight (engine.active); model-loaded-
status now returns a front-native `running` list. The models page shows a pulsing
green dot inside each model's engine/GPU tag while that model is serving a request,
toggled in place via a 2s poll of the fast front endpoint (no list re-render, so it
never re-hits the slow cache scan). Dot styling lives in base.html.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
parent 6ace8670
...@@ -10,6 +10,12 @@ ...@@ -10,6 +10,12 @@
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ root_path }}/static/admin/style.css"> <link rel="stylesheet" href="{{ root_path }}/static/admin/style.css">
<script>const ROOT_PATH = "{{ root_path }}";</script> <script>const ROOT_PATH = "{{ root_path }}";</script>
<style>
/* Pulsing dot shown on a model's engine tag while it is serving a request. */
@keyframes craiPulse { 0%,100% { opacity: 1 } 50% { opacity: .2 } }
.crai-run-dot { color: #3fb950; animation: craiPulse 1s ease-in-out infinite;
margin-right: .25rem; font-size: 1.1em; line-height: 0 }
</style>
<script> <script>
// ── Batched page bootstrap ──────────────────────────────────────────────── // ── Batched page bootstrap ────────────────────────────────────────────────
// A page's first paint often needs ~10 /admin/api reads. Fired as separate // A page's first paint often needs ~10 /admin/api reads. Fired as separate
......
...@@ -2017,15 +2017,21 @@ function _engineTagHtml(m, s){ ...@@ -2017,15 +2017,21 @@ function _engineTagHtml(m, s){
// Cross-GPU split: the model runs on `eng` (its lead card) but spills onto the // Cross-GPU split: the model runs on `eng` (its lead card) but spills onto the
// other GPU(s) for extra VRAM. Show it in the tag so the list makes the split // other GPU(s) for extra VRAM. Show it in the tag so the list makes the split
// visible, e.g. "nvidia ⇄split 📌". // visible, e.g. "nvidia ⇄split 📌".
// Live "serving a request now" indicator inside the engine tag. The dot is also
// toggled in-place by updateRunningDots() on a fast poll (no list re-render).
const mkey = esc(((m && (m.id || m.path || m.filename)) || '') + '');
const running = _isModelRunning(m);
const dot = running ? '<span class="crai-run-dot" title="Serving a request now">●</span>' : '';
const runTitle = running ? ' — serving a request now' : '';
const split = !!(s && s.gpu_split); const split = !!(s && s.gpu_split);
if(split){ if(split){
const ratio = ((s && s.tensor_split) || '').trim(); const ratio = ((s && s.tensor_split) || '').trim();
const title = 'Loads on ' + eng + ', split across all GPUs (' const title = 'Loads on ' + eng + ', split across all GPUs ('
+ (ratio ? ('ratio ' + ratio) : 'auto by free VRAM') + ')'; + (ratio ? ('ratio ' + ratio) : 'auto by free VRAM') + ')' + runTitle;
return `<span class="badge" title="${esc(title)}" style="font-size:9px;padding:.05rem .3rem;margin:.1rem .1rem 0 0;vertical-align:middle;border:1px solid ${color};color:${color};background:transparent">${esc(eng)} ⇄split${pinned?' 📌':''}</span>`; return `<span class="badge eng-tag" data-model="${mkey}" title="${esc(title)}" style="font-size:9px;padding:.05rem .3rem;margin:.1rem .1rem 0 0;vertical-align:middle;border:1px solid ${color};color:${color};background:transparent">${dot}${esc(eng)} ⇄split${pinned?' 📌':''}</span>`;
} }
const title = pinned ? ('Pinned to engine: ' + eng) : ('Runs on: ' + eng + ' (auto)'); const title = (pinned ? ('Pinned to engine: ' + eng) : ('Runs on: ' + eng + ' (auto)')) + runTitle;
return `<span class="badge" title="${esc(title)}" style="font-size:9px;padding:.05rem .3rem;margin:.1rem .1rem 0 0;vertical-align:middle;border:1px solid ${color};color:${color};background:transparent">${esc(eng)}${pinned?' 📌':''}</span>`; return `<span class="badge eng-tag" data-model="${mkey}" title="${esc(title)}" style="font-size:9px;padding:.05rem .3rem;margin:.1rem .1rem 0 0;vertical-align:middle;border:1px solid ${color};color:${color};background:transparent">${dot}${esc(eng)}${pinned?' 📌':''}</span>`;
} }
function _renderConfigPills(idx, m) { function _renderConfigPills(idx, m) {
...@@ -2661,6 +2667,7 @@ function getWhisperServerModelPath(){ ...@@ -2661,6 +2667,7 @@ function getWhisperServerModelPath(){
let _loadedKeys = new Set(); let _loadedKeys = new Set();
let _instanceInfo = {}; // loaded_key -> {loaded: N, max: N} let _instanceInfo = {}; // loaded_key -> {loaded: N, max: N}
let _runningKeys = new Set(); // model ids currently SERVING a request (front-tracked)
async function refreshLoadedStatus(){ async function refreshLoadedStatus(){
try{ try{
...@@ -2669,10 +2676,49 @@ async function refreshLoadedStatus(){ ...@@ -2669,10 +2676,49 @@ async function refreshLoadedStatus(){
const d = await r.json(); const d = await r.json();
_loadedKeys = new Set(d.loaded||[]); _loadedKeys = new Set(d.loaded||[]);
_instanceInfo = d.instances || {}; _instanceInfo = d.instances || {};
_runningKeys = new Set(d.running||[]);
} }
}catch{} }catch{}
} }
// True when a model is actively serving a request right now (any engine).
function _runKeyMatch(key){
if(!key) return false;
key = '' + key;
return _runningKeys.has(key) || _runningKeys.has(key.split('/').pop());
}
function _isModelRunning(m){
if(!m) return false;
for(const c of [m.id, m.path, m.filename, m.alias].filter(Boolean).map(x=>''+x)){
if(_runKeyMatch(c)) return true;
}
return false;
}
// Toggle the pulsing "serving now" dot on every engine tag in place — no list
// re-render (which would re-hit the slow cache scan), so it stays cheap to poll.
function updateRunningDots(){
document.querySelectorAll('.eng-tag[data-model]').forEach(el => {
const on = _runKeyMatch(el.getAttribute('data-model'));
let dot = el.querySelector('.crai-run-dot');
if(on && !dot){
dot = document.createElement('span');
dot.className = 'crai-run-dot'; dot.title = 'Serving a request now';
dot.textContent = '●';
el.insertBefore(dot, el.firstChild);
} else if(!on && dot){
dot.remove();
}
});
}
// Poll the front's (fast, native) loaded-status for the live running set and
// reflect it on the tags. model-loaded-status carries `running`.
setInterval(async () => {
if(document.visibilityState === 'hidden') return;
await refreshLoadedStatus();
updateRunningDots();
}, 2000);
function _findLoadedKey(paths){ function _findLoadedKey(paths){
for(const p of paths){ for(const p of paths){
if(_instanceInfo[p]) return p; if(_instanceInfo[p]) return p;
......
...@@ -638,14 +638,29 @@ class FrontProxy: ...@@ -638,14 +638,29 @@ class FrontProxy:
"pid": pid}) "pid": pid})
return out return out
def _running_models(self) -> list:
"""Canonical ids of models currently SERVING a request, from the front's
own in-flight tracking (engine.active). Front-native, so it's accurate even
while the engine is too busy to answer its own status."""
running = set()
for e in self.registry.all():
for _rid, m in list((e.active or {}).items()):
mid = m.get("model")
if mid:
running.add(self._model_info(mid).get("model_id") or mid)
return sorted(running)
async def model_loaded_status(self, request: Request): async def model_loaded_status(self, request: Request):
"""Proxy /admin/api/model-loaded-status to the primary, then union in the """Proxy /admin/api/model-loaded-status to the primary, then union in the
models loaded on every *other* engine. Otherwise the models page only sees models loaded on every *other* engine. Otherwise the models page only sees
the primary engine's pool and shows models loaded on a secondary (e.g. the the primary engine's pool and shows models loaded on a secondary (e.g. the
radeon engine) as idle.""" radeon engine) as idle. Also adds ``running`` — models actively serving a
request — from the front's own in-flight tracking."""
prim = self.registry.primary() prim = self.registry.primary()
running = self._running_models()
if prim is None: if prim is None:
return JSONResponse({"loaded": [], "instances": {}, "configured_max": {}}) return JSONResponse({"loaded": [], "instances": {}, "configured_max": {},
"running": running})
try: try:
headers = self._filter_headers(request.headers, _DROP_REQ) headers = self._filter_headers(request.headers, _DROP_REQ)
r = await self._short.get(prim.url + request.url.path, headers=headers, r = await self._short.get(prim.url + request.url.path, headers=headers,
...@@ -656,7 +671,8 @@ class FrontProxy: ...@@ -656,7 +671,8 @@ class FrontProxy:
media_type=r.headers.get("content-type")) media_type=r.headers.get("content-type"))
data = r.json() data = r.json()
except Exception: except Exception:
return JSONResponse({"loaded": [], "instances": {}, "configured_max": {}}) return JSONResponse({"loaded": [], "instances": {}, "configured_max": {},
"running": running})
if isinstance(data, dict): if isinstance(data, dict):
loaded = set(data.get("loaded") or []) loaded = set(data.get("loaded") or [])
for e in self.registry.all(): for e in self.registry.all():
...@@ -664,6 +680,7 @@ class FrontProxy: ...@@ -664,6 +680,7 @@ class FrontProxy:
continue continue
loaded |= e.loaded_models loaded |= e.loaded_models
data["loaded"] = sorted(loaded) data["loaded"] = sorted(loaded)
data["running"] = running
return JSONResponse(data) return JSONResponse(data)
async def _forward_to_engine(self, request: Request, engine, body: bytes): async def _forward_to_engine(self, request: Request, engine, body: bytes):
......
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