feat(llm): Diagnostic zeigt, welches Modell gerade in der Karte liegt
Die Flotte listete "qwen3-4b, qwen3-8b" — das sind die Modelle, die die Box
fahren KANN (llama-swap /v1/models), nicht die geladenen. Ohne groups-Config
liegt immer nur eins in der Karte; ein anderes anzufragen entlaedt es
(gegen llama-swap ausprobiert: A laden, B anfragen → A unloaded).
Der Adapter fragt jetzt llama-swap /running und schickt das Ergebnis in jedem
worker_ping (10 s) und im worker_hello mit; nach einem Modellwechsel sofort.
None = llama-swap nicht erreichbar, [] = nichts geladen — die Anzeige
unterscheidet das ("geladen: ?" vs. "keins geladen").
Diagnostic: das geladene Modell gruen mit "● geladen", die anderen grau mit
Hinweis, dass sie beim ersten Aufruf laden und das geladene verdraengen.
"GPU void": die LLM-Zeile zeigt jetzt LLM_GPU (die Karte von llama-swap) statt
der NVIDIA_VISIBLE_DEVICES des Adapters, die mit der Modell-Karte nichts zu
tun hat.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+19
-4
@@ -5156,13 +5156,28 @@
|
|||||||
const meta = WORKER_SVC_META[w.service] || { icon: '⚙️', label: w.service };
|
const meta = WORKER_SVC_META[w.service] || { icon: '⚙️', label: w.service };
|
||||||
const dot = !w.online ? '#666' : (w.busy ? '#FFB020' : '#3FFF3F');
|
const dot = !w.online ? '#666' : (w.busy ? '#FFB020' : '#3FFF3F');
|
||||||
const stat = !w.online ? 'offline' : (w.busy ? 'beschaeftigt' : 'frei – idle – keine Auslastung');
|
const stat = !w.online ? 'offline' : (w.busy ? 'beschaeftigt' : 'frei – idle – keine Auslastung');
|
||||||
// llm-Boxen koennen mehrere Modelle fahren (llama-swap) → Liste zeigen.
|
// llm-Boxen koennen mehrere Modelle FAHREN (llama-swap), aber nur
|
||||||
const modelText = (w.service === 'llm' && Array.isArray(w.models) && w.models.length)
|
// eins liegt gerade in der Karte — das ist gruen markiert. Die
|
||||||
? w.models.join(', ') : (w.model || '');
|
// anderen werden beim ersten Aufruf geladen (und verdraengen es).
|
||||||
|
let modelHtml;
|
||||||
|
if (w.service === 'llm' && Array.isArray(w.models) && w.models.length) {
|
||||||
|
const loaded = Array.isArray(w.loaded) ? w.loaded : null;
|
||||||
|
const isLoaded = id => loaded && loaded.some(l => l.model === id);
|
||||||
|
const state = id => (loaded.find(l => l.model === id) || {}).state;
|
||||||
|
modelHtml = w.models.map(id => isLoaded(id)
|
||||||
|
? '<span style="color:#3FFF3F;font-weight:600;" title="liegt gerade in der Karte">' + escapeHtml(id)
|
||||||
|
+ (state(id) && state(id) !== 'ready' ? ' (' + escapeHtml(state(id)) + ' …)' : ' ● geladen') + '</span>'
|
||||||
|
: '<span style="color:#8888AA;" title="lädt beim ersten Aufruf und verdrängt das geladene">' + escapeHtml(id) + '</span>'
|
||||||
|
).join('<span style="color:#555570;">, </span>');
|
||||||
|
if (loaded && !loaded.length) modelHtml += ' <span style="color:#6a6a88;">— keins geladen (lädt beim ersten Aufruf)</span>';
|
||||||
|
else if (!loaded && w.online) modelHtml += ' <span style="color:#6a6a88;" title="Box meldet es nicht — Adapter noch alt oder llama-swap nicht erreichbar">— geladen: ?</span>';
|
||||||
|
} else {
|
||||||
|
modelHtml = '<span style="color:#8888AA;">' + escapeHtml(w.model || '') + '</span>';
|
||||||
|
}
|
||||||
return '<div style="display:flex;align-items:center;gap:8px;padding:4px 0;">' +
|
return '<div style="display:flex;align-items:center;gap:8px;padding:4px 0;">' +
|
||||||
'<span style="width:8px;height:8px;border-radius:50%;background:' + dot + ';display:inline-block;"></span>' +
|
'<span style="width:8px;height:8px;border-radius:50%;background:' + dot + ';display:inline-block;"></span>' +
|
||||||
'<span>' + meta.icon + ' <b>' + escapeHtml(meta.label) + '</b></span>' +
|
'<span>' + meta.icon + ' <b>' + escapeHtml(meta.label) + '</b></span>' +
|
||||||
'<span style="color:#8888AA;">' + escapeHtml(modelText) + '</span>' +
|
'<span>' + modelHtml + '</span>' +
|
||||||
(w.gpus ? '<span style="color:#8888AA;">GPU ' + escapeHtml(w.gpus) + '</span>' : '') +
|
(w.gpus ? '<span style="color:#8888AA;">GPU ' + escapeHtml(w.gpus) + '</span>' : '') +
|
||||||
'<span style="margin-left:auto;color:' + dot + ';">' + stat + '</span>' +
|
'<span style="margin-left:auto;color:' + dot + ';">' + stat + '</span>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
|
|||||||
@@ -1260,6 +1260,9 @@ function connectRVS(forcePlain) {
|
|||||||
instanceId: p.instanceId, service: p.service || "",
|
instanceId: p.instanceId, service: p.service || "",
|
||||||
node: p.node || "", gpus: p.gpus || "", model: p.model || "",
|
node: p.node || "", gpus: p.gpus || "", model: p.model || "",
|
||||||
models: Array.isArray(p.models) ? p.models : (p.model ? [p.model] : []),
|
models: Array.isArray(p.models) ? p.models : (p.model ? [p.model] : []),
|
||||||
|
// Was GERADE in der Karte liegt (nur llm, aus llama-swap /running).
|
||||||
|
// null = Box konnte llama-swap nicht fragen, [] = nichts geladen.
|
||||||
|
loaded: Array.isArray(p.loaded) ? p.loaded : (prev.loaded ?? null),
|
||||||
// OpenAI-Endpunkt (nur llm-Worker): Port/Bind kommen aus der .env der
|
// OpenAI-Endpunkt (nur llm-Worker): Port/Bind kommen aus der .env der
|
||||||
// Box, der Key wird NIE uebertragen — nur ob einer gesetzt ist und ob
|
// Box, der Key wird NIE uebertragen — nur ob einer gesetzt ist und ob
|
||||||
// die .env ihn vorgibt (dann ist das Feld hier schreibgeschuetzt).
|
// die .env ihn vorgibt (dann ist das Feld hier schreibgeschuetzt).
|
||||||
@@ -1282,6 +1285,7 @@ function connectRVS(forcePlain) {
|
|||||||
workers.set(p.instanceId, w);
|
workers.set(p.instanceId, w);
|
||||||
}
|
}
|
||||||
w.busy = !!p.busy;
|
w.busy = !!p.busy;
|
||||||
|
if ("loaded" in p) w.loaded = Array.isArray(p.loaded) ? p.loaded : null;
|
||||||
w.last_seen = Date.now();
|
w.last_seen = Date.now();
|
||||||
broadcastWorkers();
|
broadcastWorkers();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,14 +315,43 @@ async def _fetch_available_models() -> list:
|
|||||||
return [LLM_MODEL]
|
return [LLM_MODEL]
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_loaded() -> list:
|
||||||
|
"""Welche Modelle liegen GERADE in der Karte? llama-swap /running →
|
||||||
|
[{"model", "state"}]. Ohne groups-Config ist das hoechstens eins — ein
|
||||||
|
anderes anzufragen entlaedt es. None, wenn llama-swap nicht antwortet
|
||||||
|
("unbekannt" ist etwas anderes als "nichts geladen")."""
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=5) as client:
|
||||||
|
r = await client.get(f"{LLAMA_URL}/running")
|
||||||
|
r.raise_for_status()
|
||||||
|
return [{"model": m.get("model", ""), "state": m.get("state", "")}
|
||||||
|
for m in (r.json().get("running") or []) if m.get("model")]
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("llama-swap /running nicht abfragbar: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _ping(ws) -> None:
|
||||||
|
"""Lebenszeichen mit busy + geladenem Modell. loaded=None heisst "konnte
|
||||||
|
llama-swap nicht fragen" — das ist etwas anderes als "nichts geladen"."""
|
||||||
|
await _send(ws, "worker_ping",
|
||||||
|
{"instanceId": INSTANCE_ID, "busy": _inflight > 0,
|
||||||
|
"loaded": await _fetch_loaded()})
|
||||||
|
|
||||||
|
|
||||||
async def _announce(ws) -> None:
|
async def _announce(ws) -> None:
|
||||||
"""Sendet ein frisches worker_hello mit der aktuellen Modell-Liste (nach
|
"""Sendet ein frisches worker_hello mit der aktuellen Modell-Liste (nach
|
||||||
Provision/Remove aufrufen, damit Bridge+Diagnostic das neue Modell lernen)."""
|
Provision/Remove aufrufen, damit Bridge+Diagnostic das neue Modell lernen)."""
|
||||||
models = await _fetch_available_models()
|
models = await _fetch_available_models()
|
||||||
await _send(ws, "worker_hello", {
|
await _send(ws, "worker_hello", {
|
||||||
"instanceId": INSTANCE_ID, "service": WORKER_SERVICE,
|
"instanceId": INSTANCE_ID, "service": WORKER_SERVICE,
|
||||||
"node": NODE_NAME, "gpus": GPU_IDS, "model": LLM_MODEL,
|
# Die LLM-Karte ist die von llama-swap (LLM_GPU). Die eigene
|
||||||
|
# NVIDIA_VISIBLE_DEVICES des Adapters ist "all" bzw. wird vom
|
||||||
|
# Container-Toolkit ersetzt (Anzeige "GPU void") — sagt nichts darueber,
|
||||||
|
# wo das Modell laeuft.
|
||||||
|
"node": NODE_NAME, "gpus": LLM_GPU or GPU_IDS, "model": LLM_MODEL,
|
||||||
"models": models, # welche Modelle diese Box fahren kann (llama-swap-Keys)
|
"models": models, # welche Modelle diese Box fahren kann (llama-swap-Keys)
|
||||||
|
"loaded": await _fetch_loaded(), # welches davon GERADE in der Karte liegt
|
||||||
# OpenAI-Endpunkt fuer Fremd-Tools (OpenHands & Co.). Der Key selbst wird
|
# OpenAI-Endpunkt fuer Fremd-Tools (OpenHands & Co.). Der Key selbst wird
|
||||||
# NICHT gesendet (Broadcast!) — nur ob einer gesetzt ist und ob die .env
|
# NICHT gesendet (Broadcast!) — nur ob einer gesetzt ist und ob die .env
|
||||||
# ihn vorgibt (dann ist das Feld in der Diagnostic schreibgeschuetzt).
|
# ihn vorgibt (dann ist das Feld in der Diagnostic schreibgeschuetzt).
|
||||||
@@ -343,8 +372,7 @@ async def _worker_register(ws) -> None:
|
|||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(WORKER_PING_INTERVAL_S)
|
await asyncio.sleep(WORKER_PING_INTERVAL_S)
|
||||||
n += 1
|
n += 1
|
||||||
await _send(ws, "worker_ping",
|
await _ping(ws)
|
||||||
{"instanceId": INSTANCE_ID, "busy": _inflight > 0})
|
|
||||||
if n % 3 == 0: # ~30s worker_hello wiederholen (wie der Satellit) →
|
if n % 3 == 0: # ~30s worker_hello wiederholen (wie der Satellit) →
|
||||||
await _announce(ws) # auch neu gestartetes Diagnostic/Bridge lernt uns
|
await _announce(ws) # auch neu gestartetes Diagnostic/Bridge lernt uns
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
@@ -396,6 +424,7 @@ async def _do_llm_request(ws, payload: dict) -> None:
|
|||||||
_last_model = eff_model
|
_last_model = eff_model
|
||||||
await _emit_llm_status(ws, "ready", eff_model,
|
await _emit_llm_status(ws, "ready", eff_model,
|
||||||
loadSeconds=round(dt, 1), freshlyDownloaded=fresh)
|
loadSeconds=round(dt, 1), freshlyDownloaded=fresh)
|
||||||
|
await _ping(ws) # Diagnostic zeigt den Wechsel sofort, nicht erst in 10 s
|
||||||
else:
|
else:
|
||||||
# bei Fehler _last_model NICHT setzen → naechster Versuch meldet erneut loading
|
# bei Fehler _last_model NICHT setzen → naechster Versuch meldet erneut loading
|
||||||
await _emit_llm_status(ws, "error", eff_model,
|
await _emit_llm_status(ws, "error", eff_model,
|
||||||
@@ -637,6 +666,7 @@ async def _handle_provision(ws, payload: dict) -> None:
|
|||||||
freshlyDownloaded=dt > 25)
|
freshlyDownloaded=dt > 25)
|
||||||
await _emit_progress(ws, key, "ready", percent=100, loadSeconds=round(dt, 1),
|
await _emit_progress(ws, key, "ready", percent=100, loadSeconds=round(dt, 1),
|
||||||
**(await _gpu_info()))
|
**(await _gpu_info()))
|
||||||
|
await _ping(ws)
|
||||||
else:
|
else:
|
||||||
await _emit_llm_status(ws, "error", key, error=(res.get("error") or "")[:160])
|
await _emit_llm_status(ws, "error", key, error=(res.get("error") or "")[:160])
|
||||||
await _emit_progress(ws, key, "error", error=(res.get("error") or "")[:200])
|
await _emit_progress(ws, key, "error", error=(res.get("error") or "")[:200])
|
||||||
|
|||||||
Reference in New Issue
Block a user