fix(diagnostic): Karten kommen im Browser an, lokales Modell springt nicht zurueck
1. "Die LLM-Box meldet noch keine Karten": die Box schickte die Karten-
Belegung, aber workerList() im Diagnostic-Server gibt nur eine feste
Feldliste an den Browser — slots stand nicht drauf. Meine UI-Tests haben
die Flotte direkt im Browser gesetzt und sind an genau dieser Stelle
vorbeigelaufen. Jetzt Ende-zu-Ende getestet: echter Diagnostic-Server +
RVS + Box-Meldung → Browser (per DevTools-Protokoll gelesen).
2. Lokales Modell sprang auf "Qwen3 8B" zurueck: die Auswahl wurde gespeichert,
aber die Seite behielt intern das alte Modell und stellte das Dropdown beim
naechsten Lebenszeichen der Box (10 s) darauf zurueck. Dazu wie gewuenscht
umgebaut: nur Modelle, die im Cluster geladen sind, jedes einmal, ohne
Karte. Ist das gewaehlte gerade nirgends geladen: "(offline)" — geht
automatisch weiter, sobald eine Box es hat. Nichts da: "kein Modell
verfuegbar". Waehrend die Auswahl offen ist, wird nicht umgebaut.
3. "Bereit", aber Karte leer: der llm-adapter meldet pro Karte den belegten
VRAM. Laeuft ein Modell "ready" und die Karte bleibt unter 400 MB, rechnet
llama.cpp auf der CPU (Treiber zu alt fuer das CUDA im Image) — das sagt
llama.cpp nicht nach aussen. Diagnostic zeigt es jetzt in Flotte,
LLM-Karten und Modell-Auswahl ("laeuft auf der CPU").
4. bootstrap.sh: der normale Lauf meldete "✓ Treiber aktiv", auch wenn er fuer
das LLM zu alt ist. Jetzt Warnung mit Version und dem Befehl
(--upgrade-driver), wenn das LLM-Profil aktiv ist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+22
-16
@@ -8886,24 +8886,24 @@
|
|||||||
// Baut das Modell-Dropdown aus der Vereinigung von live-Flotte + kuratierter
|
// Baut das Modell-Dropdown aus der Vereinigung von live-Flotte + kuratierter
|
||||||
// Liste; zeigt pro Modell "· N Box(en)" bzw. "· offline". Auswahl bleibt
|
// Liste; zeigt pro Modell "· N Box(en)" bzw. "· offline". Auswahl bleibt
|
||||||
// erhalten (auch wenn das gewaehlte Modell gerade keine online Box hat).
|
// erhalten (auch wenn das gewaehlte Modell gerade keine online Box hat).
|
||||||
|
// ARIAs lokales Modell: nur was im Cluster GELADEN ist, jedes einmal, ohne
|
||||||
|
// Karte — wo es laeuft, entscheidet der Cluster. Ist das gewaehlte gerade
|
||||||
|
// nirgends geladen, bleibt es mit "(offline)" stehen; sobald eine Box es
|
||||||
|
// wieder hat, geht es von selbst weiter (bis dahin antwortet Claude).
|
||||||
function refreshLocalLlmModelChoices() {
|
function refreshLocalLlmModelChoices() {
|
||||||
const sel = document.getElementById('local-llm-model');
|
const sel = document.getElementById('local-llm-model');
|
||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
|
if (document.activeElement === sel) return; // nicht unter der offenen Auswahl umbauen
|
||||||
const counts = fleetLlmModelCounts();
|
const counts = fleetLlmModelCounts();
|
||||||
const nameOf = id => { const m = _localModelsCache.find(x => x.id === id); return (m && m.display_name) || id; };
|
const nameOf = id => { const m = _localModelsCache.find(x => x.id === id); return (m && m.display_name) || id; };
|
||||||
const ids = new Set();
|
const cpu = id => workers.some(w => w.online && (w.slots || []).some(sl => sl.model === id && sl.onCpu));
|
||||||
Object.keys(counts).forEach(id => ids.add(id));
|
const ids = Object.keys(counts).sort();
|
||||||
_localModelsCache.forEach(m => ids.add(m.id));
|
const opts = ids.map(id => `<option value="${escapeHtml(id)}">${escapeHtml(nameOf(id))}${cpu(id) ? ' · läuft auf der CPU' : ''}</option>`);
|
||||||
if (_currentLocalModel) ids.add(_currentLocalModel);
|
if (_currentLocalModel && !ids.includes(_currentLocalModel)) {
|
||||||
const order = Array.from(ids).sort();
|
opts.unshift(`<option value="${escapeHtml(_currentLocalModel)}">${escapeHtml(nameOf(_currentLocalModel))} (offline)</option>`);
|
||||||
sel.innerHTML = order.map(id => {
|
}
|
||||||
const n = counts[id] || 0;
|
sel.innerHTML = opts.join('') || '<option value="">kein Modell verfügbar</option>';
|
||||||
// Nur geladene Modelle zaehlen (Karten-Belegung) — alles andere kann
|
sel.value = _currentLocalModel && (ids.includes(_currentLocalModel) || opts.length) ? _currentLocalModel : (ids[0] || '');
|
||||||
// ARIA nicht nutzen; laden geht im Modell-Katalog.
|
|
||||||
const avail = n > 0 ? ` · geladen auf ${n} Box${n > 1 ? 'en' : ''}` : ' · nicht geladen';
|
|
||||||
return `<option value="${id}">${escapeHtml(nameOf(id))}${avail}</option>`;
|
|
||||||
}).join('') || '<option value="">(keine)</option>';
|
|
||||||
if (order.includes(_currentLocalModel)) sel.value = _currentLocalModel;
|
|
||||||
updateLocalModelDesc();
|
updateLocalModelDesc();
|
||||||
renderLocalLlmFleet();
|
renderLocalLlmFleet();
|
||||||
}
|
}
|
||||||
@@ -8952,6 +8952,9 @@
|
|||||||
toolVariant: document.getElementById('local-llm-toolvariant').value,
|
toolVariant: document.getElementById('local-llm-toolvariant').value,
|
||||||
localLlmModel: (modelSel && modelSel.value) || '',
|
localLlmModel: (modelSel && modelSel.value) || '',
|
||||||
};
|
};
|
||||||
|
// Sofort merken — sonst stellt das naechste Flotten-Update (alle 10 s) das
|
||||||
|
// Dropdown auf das alte Modell zurueck, obwohl das neue gespeichert ist.
|
||||||
|
if (body.localLlmModel) _currentLocalModel = body.localLlmModel;
|
||||||
updateLocalModelDesc();
|
updateLocalModelDesc();
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/local-llm-config', {
|
const r = await fetch('/api/local-llm-config', {
|
||||||
@@ -9167,13 +9170,15 @@
|
|||||||
+ (tooBig ? ' · passt nicht' : '');
|
+ (tooBig ? ' · passt nicht' : '');
|
||||||
opts.push(`<option value="${escapeHtml(m.id)}"${m.id === sl.model ? ' selected' : ''}${tooBig ? ' style="color:#FF9F0A;"' : ''}>${escapeHtml(label)}</option>`);
|
opts.push(`<option value="${escapeHtml(m.id)}"${m.id === sl.model ? ' selected' : ''}${tooBig ? ' style="color:#FF9F0A;"' : ''}>${escapeHtml(label)}</option>`);
|
||||||
}
|
}
|
||||||
const [c, st] = sl.model ? (SLOT_STATE[sl.state] || ['#8888AA', escapeHtml(sl.state || '?')]) : ['#555570', 'leer'];
|
let [c, st] = sl.model ? (SLOT_STATE[sl.state] || ['#8888AA', escapeHtml(sl.state || '?')]) : ['#555570', 'leer'];
|
||||||
|
if (sl.onCpu) { c = '#FF9F0A'; st = '⚠ auf der CPU'; }
|
||||||
const job = Object.values(_llmJobs || {}).find(j => j.instanceId === b.instanceId && String(j.gpu) === String(sl.gpu)
|
const job = Object.values(_llmJobs || {}).find(j => j.instanceId === b.instanceId && String(j.gpu) === String(sl.gpu)
|
||||||
&& !['ready', 'error'].includes(j.phase));
|
&& !['ready', 'error'].includes(j.phase));
|
||||||
const jobTxt = job ? (job.phase === 'download' && job.percent != null ? `lädt ${job.percent.toFixed(0)} %` : 'lädt …') : '';
|
const jobTxt = job ? (job.phase === 'download' && job.percent != null ? `lädt ${job.percent.toFixed(0)} %` : 'lädt …') : '';
|
||||||
rows.push(`<div style="display:grid;grid-template-columns:minmax(220px,1fr) minmax(260px,2fr) 160px;gap:10px;align-items:center;">
|
rows.push(`<div style="display:grid;grid-template-columns:minmax(220px,1fr) minmax(260px,2fr) 160px;gap:10px;align-items:center;">
|
||||||
<div><b style="color:#E0E0F0;">${escapeHtml(b.node)}</b> · Karte ${escapeHtml(sl.gpu)}
|
<div><b style="color:#E0E0F0;">${escapeHtml(b.node)}</b> · Karte ${escapeHtml(sl.gpu)}
|
||||||
<div style="color:#555570;font-size:11px;">${escapeHtml(sl.gpuName || '')}${vramGB ? ' · ' + vramGB.toFixed(0) + ' GB' : ''}${sl.ctx ? ' · Kontext ' + sl.ctx : ''}</div></div>
|
<div style="color:#555570;font-size:11px;">${escapeHtml(sl.gpuName || '')}${vramGB ? ' · ' + vramGB.toFixed(0) + ' GB' : ''}${sl.vramUsedMB >= 0 ? ' · belegt ' + (sl.vramUsedMB / 1024).toFixed(1) + ' GB' : ''}${sl.ctx ? ' · Kontext ' + sl.ctx : ''}</div>
|
||||||
|
${sl.onCpu ? '<div style="color:#FF9F0A;font-size:11px;" title="llama.cpp findet die Karte nicht und rechnet langsam auf der CPU. Ursache meist: Treiber zu alt fuer das CUDA im llama-swap-Image. Abhilfe: auf der Box sudo ./bootstrap.sh --upgrade-driver">⚠ Modell läuft auf der CPU, Karte leer — Treiber zu alt? (bootstrap.sh --upgrade-driver)</div>' : ''}</div>
|
||||||
<select data-box="${escapeHtml(b.instanceId)}" data-gpu="${escapeHtml(sl.gpu)}" data-current="${escapeHtml(sl.model || '')}"
|
<select data-box="${escapeHtml(b.instanceId)}" data-gpu="${escapeHtml(sl.gpu)}" data-current="${escapeHtml(sl.model || '')}"
|
||||||
onchange="onLlmCardSelect(this)" ${job ? 'disabled' : ''}
|
onchange="onLlmCardSelect(this)" ${job ? 'disabled' : ''}
|
||||||
style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:5px 8px;color:#E0E0F0;font-size:12px;">${opts.join('')}</select>
|
style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:5px 8px;color:#E0E0F0;font-size:12px;">${opts.join('')}</select>
|
||||||
@@ -9220,7 +9225,8 @@
|
|||||||
<span style="min-width:200px;color:#8888AA;">${card}</span>
|
<span style="min-width:200px;color:#8888AA;">${card}</span>
|
||||||
<span style="color:#555570;">— leer · im Modell-Katalog „Laden" → diese Karte</span></div>`;
|
<span style="color:#555570;">— leer · im Modell-Katalog „Laden" → diese Karte</span></div>`;
|
||||||
}
|
}
|
||||||
const [c, label] = SLOT_STATE[sl.state] || ['#8888AA', escapeHtml(sl.state || '?')];
|
let [c, label] = SLOT_STATE[sl.state] || ['#8888AA', escapeHtml(sl.state || '?')];
|
||||||
|
if (sl.onCpu) { c = '#FF9F0A'; label = '⚠ läuft auf der CPU — Karte leer (Treiber zu alt?)'; }
|
||||||
return `<div style="display:flex;gap:10px;align-items:center;padding:2px 0;">
|
return `<div style="display:flex;gap:10px;align-items:center;padding:2px 0;">
|
||||||
<span style="min-width:200px;color:#8888AA;">${card}</span>
|
<span style="min-width:200px;color:#8888AA;">${card}</span>
|
||||||
<b style="color:#E0E0F0;">${escapeHtml(sl.model)}</b>
|
<b style="color:#E0E0F0;">${escapeHtml(sl.model)}</b>
|
||||||
|
|||||||
@@ -762,6 +762,10 @@ function workerList() {
|
|||||||
gpus: w.gpus, model: w.model, models: w.models || [], busy: !!w.busy,
|
gpus: w.gpus, model: w.model, models: w.models || [], busy: !!w.busy,
|
||||||
openaiPort: w.openaiPort || "", openaiBind: w.openaiBind || "",
|
openaiPort: w.openaiPort || "", openaiBind: w.openaiBind || "",
|
||||||
openaiKeySet: !!w.openaiKeySet, openaiEnvLocked: !!w.openaiEnvLocked,
|
openaiKeySet: !!w.openaiKeySet, openaiEnvLocked: !!w.openaiEnvLocked,
|
||||||
|
// LLM-Karten (Belegung, Zustand, VRAM). Fehlte hier — die Box schickte sie,
|
||||||
|
// aber diese Feldliste liess sie nie zum Browser durch ("meldet keine Karten").
|
||||||
|
slots: Array.isArray(w.slots) ? w.slots : null,
|
||||||
|
knownModels: w.knownModels || [],
|
||||||
online: (now - (w.last_seen || 0)) < WORKER_OFFLINE_MS,
|
online: (now - (w.last_seen || 0)) < WORKER_OFFLINE_MS,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -189,8 +189,17 @@ fi
|
|||||||
# ── 3. NVIDIA-Treiber ──
|
# ── 3. NVIDIA-Treiber ──
|
||||||
step "NVIDIA-Treiber"
|
step "NVIDIA-Treiber"
|
||||||
if nvidia-smi >/dev/null 2>&1; then
|
if nvidia-smi >/dev/null 2>&1; then
|
||||||
ok "Treiber aktiv: $(nvidia-smi --query-gpu=name --format=csv,noheader | paste -sd', ')"
|
DRV="$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -1 || true)"
|
||||||
|
ok "Treiber ${DRV} aktiv: $(nvidia-smi --query-gpu=name --format=csv,noheader | paste -sd', ')"
|
||||||
DRIVER_ACTIVE=1
|
DRIVER_ACTIVE=1
|
||||||
|
# Fuer das lokale LLM zu alt? Dann laeuft es still auf der CPU — das soll man
|
||||||
|
# hier sehen, nicht erst an einer leeren Karte in nvidia-smi.
|
||||||
|
PROFILES="$(grep -E '^COMPOSE_PROFILES=' "$XTTS_DIR/.env" 2>/dev/null | cut -d= -f2- || true)"
|
||||||
|
if [[ "${DRV%%.*}" =~ ^[0-9]+$ && "${DRV%%.*}" -lt "$MIN_DRIVER_MAJOR" && ( -z "$PROFILES" || "$PROFILES" == *llm* ) ]]; then
|
||||||
|
warn "Treiber ${DRV} ist zu alt fuer das lokale LLM (braucht >= ${MIN_DRIVER_MAJOR})."
|
||||||
|
warn " Folge: llama.cpp findet keine Karte und rechnet auf der CPU."
|
||||||
|
warn " Abhilfe: sudo ./bootstrap.sh --upgrade-driver (zeigt erst den Plan, fragt nach)"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
if dpkg -l | grep -q '^ii nvidia-driver '; then
|
if dpkg -l | grep -q '^ii nvidia-driver '; then
|
||||||
warn "Treiber installiert, aber nvidia-smi antwortet nicht → REBOOT noetig."
|
warn "Treiber installiert, aber nvidia-smi antwortet nicht → REBOOT noetig."
|
||||||
|
|||||||
@@ -444,6 +444,26 @@ def _model_ctx(key: str) -> int:
|
|||||||
return int(m.group(1)) if m else 0
|
return int(m.group(1)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
|
async def _vram_used() -> dict:
|
||||||
|
"""{index: belegte MB} — frisch bei jedem Aufruf (aendert sich laufend)."""
|
||||||
|
try:
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
"nvidia-smi", "--query-gpu=index,memory.used", "--format=csv,noheader,nounits",
|
||||||
|
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL)
|
||||||
|
out, _ = await asyncio.wait_for(proc.communicate(), 10)
|
||||||
|
return {a.strip(): int(float(b)) for a, b in
|
||||||
|
(l.split(",", 1) for l in out.decode().splitlines() if "," in l)}
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
# Ein geladenes Modell belegt GB auf seiner Karte. Bleibt die Karte trotz
|
||||||
|
# "ready" praktisch leer, rechnet llama.cpp auf der CPU — typisch, wenn der
|
||||||
|
# Treiber zu alt fuer das CUDA im Image ist ("forward compatibility …").
|
||||||
|
# llama.cpp meldet das nicht nach aussen; wir sehen es nur am Speicher.
|
||||||
|
ON_CPU_BELOW_MB = 400
|
||||||
|
|
||||||
|
|
||||||
async def _slots_info() -> list:
|
async def _slots_info() -> list:
|
||||||
"""Pro LLM-Karte: [{gpu, gpuName, model, state}]. state aus llama-swap
|
"""Pro LLM-Karte: [{gpu, gpuName, model, state}]. state aus llama-swap
|
||||||
/running: ready | starting | stopping | stopped; "unknown", wenn llama-swap
|
/running: ready | starting | stopping | stopped; "unknown", wenn llama-swap
|
||||||
@@ -452,12 +472,16 @@ async def _slots_info() -> list:
|
|||||||
running = await _fetch_loaded()
|
running = await _fetch_loaded()
|
||||||
rmap = {r["model"]: r["state"] for r in running} if running is not None else None
|
rmap = {r["model"]: r["state"] for r in running} if running is not None else None
|
||||||
gpus = await _gpu_names()
|
gpus = await _gpu_names()
|
||||||
|
used = await _vram_used()
|
||||||
out = []
|
out = []
|
||||||
for card in _llm_cards():
|
for card in _llm_cards():
|
||||||
m = slots.get(card, "")
|
m = slots.get(card, "")
|
||||||
state = "" if not m else ("unknown" if rmap is None else rmap.get(m, "stopped"))
|
state = "" if not m else ("unknown" if rmap is None else rmap.get(m, "stopped"))
|
||||||
g = gpus.get(card) or {}
|
g = gpus.get(card) or {}
|
||||||
|
u = used.get(card)
|
||||||
|
on_cpu = bool(m and state == "ready" and u is not None and u < ON_CPU_BELOW_MB)
|
||||||
out.append({"gpu": card, "gpuName": g.get("name", ""), "vramMB": g.get("vramMB", 0),
|
out.append({"gpu": card, "gpuName": g.get("name", ""), "vramMB": g.get("vramMB", 0),
|
||||||
|
"vramUsedMB": u if u is not None else -1, "onCpu": on_cpu,
|
||||||
"model": m, "state": state, "ctx": _model_ctx(m) if m else 0})
|
"model": m, "state": state, "ctx": _model_ctx(m) if m else 0})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -504,7 +528,8 @@ async def _announce(ws) -> None:
|
|||||||
"openaiKeySet": bool(_openai_key()),
|
"openaiKeySet": bool(_openai_key()),
|
||||||
"openaiEnvLocked": bool(_OPENAI_KEY_ENV),
|
"openaiEnvLocked": bool(_OPENAI_KEY_ENV),
|
||||||
})
|
})
|
||||||
logger.info("worker_hello: %s", ", ".join(f"Karte {x['gpu']}={x['model'] or '-'}({x['state'] or 'leer'})" for x in info))
|
logger.info("worker_hello: %s", ", ".join(
|
||||||
|
f"Karte {x['gpu']}={x['model'] or '-'}({x['state'] or 'leer'}{', CPU!' if x.get('onCpu') else ''})" for x in info))
|
||||||
|
|
||||||
|
|
||||||
async def _worker_register(ws) -> None:
|
async def _worker_register(ws) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user