From 9be14eef5ed4ee4fef19317afd099ed703359ae5 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Mon, 19 Oct 2026 02:11:02 +0200 Subject: [PATCH] fix(diagnostic): Karten kommen im Browser an, lokales Modell springt nicht zurueck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- diagnostic/index.html | 38 +++++++++++++++++++++---------------- diagnostic/server.js | 4 ++++ xtts/bootstrap.sh | 11 ++++++++++- xtts/llm-adapter/adapter.py | 27 +++++++++++++++++++++++++- 4 files changed, 62 insertions(+), 18 deletions(-) diff --git a/diagnostic/index.html b/diagnostic/index.html index 00800a6..b591ea3 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -8886,24 +8886,24 @@ // Baut das Modell-Dropdown aus der Vereinigung von live-Flotte + kuratierter // Liste; zeigt pro Modell "· N Box(en)" bzw. "· offline". Auswahl bleibt // 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() { const sel = document.getElementById('local-llm-model'); if (!sel) return; + if (document.activeElement === sel) return; // nicht unter der offenen Auswahl umbauen const counts = fleetLlmModelCounts(); const nameOf = id => { const m = _localModelsCache.find(x => x.id === id); return (m && m.display_name) || id; }; - const ids = new Set(); - Object.keys(counts).forEach(id => ids.add(id)); - _localModelsCache.forEach(m => ids.add(m.id)); - if (_currentLocalModel) ids.add(_currentLocalModel); - const order = Array.from(ids).sort(); - sel.innerHTML = order.map(id => { - const n = counts[id] || 0; - // Nur geladene Modelle zaehlen (Karten-Belegung) — alles andere kann - // ARIA nicht nutzen; laden geht im Modell-Katalog. - const avail = n > 0 ? ` · geladen auf ${n} Box${n > 1 ? 'en' : ''}` : ' · nicht geladen'; - return ``; - }).join('') || ''; - if (order.includes(_currentLocalModel)) sel.value = _currentLocalModel; + const cpu = id => workers.some(w => w.online && (w.slots || []).some(sl => sl.model === id && sl.onCpu)); + const ids = Object.keys(counts).sort(); + const opts = ids.map(id => ``); + if (_currentLocalModel && !ids.includes(_currentLocalModel)) { + opts.unshift(``); + } + sel.innerHTML = opts.join('') || ''; + sel.value = _currentLocalModel && (ids.includes(_currentLocalModel) || opts.length) ? _currentLocalModel : (ids[0] || ''); updateLocalModelDesc(); renderLocalLlmFleet(); } @@ -8952,6 +8952,9 @@ toolVariant: document.getElementById('local-llm-toolvariant').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(); try { const r = await fetch('/api/local-llm-config', { @@ -9167,13 +9170,15 @@ + (tooBig ? ' · passt nicht' : ''); opts.push(``); } - 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) && !['ready', 'error'].includes(j.phase)); const jobTxt = job ? (job.phase === 'download' && job.percent != null ? `lädt ${job.percent.toFixed(0)} %` : 'lädt …') : ''; rows.push(`
${escapeHtml(b.node)} · Karte ${escapeHtml(sl.gpu)} -
${escapeHtml(sl.gpuName || '')}${vramGB ? ' · ' + vramGB.toFixed(0) + ' GB' : ''}${sl.ctx ? ' · Kontext ' + sl.ctx : ''}
+
${escapeHtml(sl.gpuName || '')}${vramGB ? ' · ' + vramGB.toFixed(0) + ' GB' : ''}${sl.vramUsedMB >= 0 ? ' · belegt ' + (sl.vramUsedMB / 1024).toFixed(1) + ' GB' : ''}${sl.ctx ? ' · Kontext ' + sl.ctx : ''}
+ ${sl.onCpu ? '
⚠ Modell läuft auf der CPU, Karte leer — Treiber zu alt? (bootstrap.sh --upgrade-driver)
' : ''}
@@ -9220,7 +9225,8 @@ ${card} — leer · im Modell-Katalog „Laden" → diese Karte`; } - 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 `
${card} ${escapeHtml(sl.model)} diff --git a/diagnostic/server.js b/diagnostic/server.js index d6bef07..aed388f 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -762,6 +762,10 @@ function workerList() { gpus: w.gpus, model: w.model, models: w.models || [], busy: !!w.busy, openaiPort: w.openaiPort || "", openaiBind: w.openaiBind || "", 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, })); } diff --git a/xtts/bootstrap.sh b/xtts/bootstrap.sh index d2dadae..f20915f 100755 --- a/xtts/bootstrap.sh +++ b/xtts/bootstrap.sh @@ -189,8 +189,17 @@ fi # ── 3. NVIDIA-Treiber ── step "NVIDIA-Treiber" 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 + # 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 if dpkg -l | grep -q '^ii nvidia-driver '; then warn "Treiber installiert, aber nvidia-smi antwortet nicht → REBOOT noetig." diff --git a/xtts/llm-adapter/adapter.py b/xtts/llm-adapter/adapter.py index d6f92f9..6fb9719 100644 --- a/xtts/llm-adapter/adapter.py +++ b/xtts/llm-adapter/adapter.py @@ -444,6 +444,26 @@ def _model_ctx(key: str) -> int: 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: """Pro LLM-Karte: [{gpu, gpuName, model, state}]. state aus llama-swap /running: ready | starting | stopping | stopped; "unknown", wenn llama-swap @@ -452,12 +472,16 @@ async def _slots_info() -> list: running = await _fetch_loaded() rmap = {r["model"]: r["state"] for r in running} if running is not None else None gpus = await _gpu_names() + used = await _vram_used() out = [] for card in _llm_cards(): m = slots.get(card, "") state = "" if not m else ("unknown" if rmap is None else rmap.get(m, "stopped")) 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), + "vramUsedMB": u if u is not None else -1, "onCpu": on_cpu, "model": m, "state": state, "ctx": _model_ctx(m) if m else 0}) return out @@ -504,7 +528,8 @@ async def _announce(ws) -> None: "openaiKeySet": bool(_openai_key()), "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: