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: