diff --git a/diagnostic/index.html b/diagnostic/index.html index d117148..90b3271 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -9740,7 +9740,7 @@ openLlmDownloads(); } - function onLlmCardSelect(sel) { + async function onLlmCardSelect(sel) { const target = sel.dataset.box, gpu = sel.dataset.gpu, current = sel.dataset.current, next = sel.value; sel.blur(); // Fokus weg → aufgeschobenes Neuzeichnen holt den Stand nach const node = _boxName(target); @@ -9755,7 +9755,8 @@ if (!m || !m.hfRepo) { alert(`${next} steht nicht im Modell-Katalog — Herkunft unbekannt.`); sel.value = current; return; } const sl = (workers.find(w => w.instanceId === target)?.slots || []).find(x => String(x.gpu) === String(gpu)) || {}; const vramGB = sl.vramMB ? sl.vramMB / 1024 : 0; - const cpuMoe = askCpuMoe(m, sl.vramMB || 0); + const cpuMoe = await askCpuMoe(m, sl.vramMB || 0); + if (cpuMoe === null) { sel.value = current; return; } const warn = (!cpuMoe && vramGB && m.sizeGB && m.sizeGB > vramGB * 0.92) ? `\n\n⚠ ${next} ist etwa ${m.sizeGB} GB groß, die Karte hat ${vramGB.toFixed(0)} GB — wahrscheinlich passt es nicht.` : ''; const repl = current ? `\n\nErsetzt ${current} — Programme, die es nutzen, verlieren es.` : ''; @@ -9829,17 +9830,39 @@ // langsam wird. Erkennung am Namen (A3B, A4B, MoE …); sicher weiss es die Box // erst nach dem Laden (slot.moe). function looksMoe(m) { return /(\bA\d+(\.\d+)?B\b|moe|mixtral)/i.test((m.id || '') + ' ' + (m.hfRepo || '')); } - /** 0 = normal laden, -2 = Experten automatisch aufteilen, null = abbrechen */ + /** Promise: 0 = normal laden, -2 = Experten automatisch aufteilen, null = abbrechen. + * Eigener Dialog mit drei Knoepfen — bei confirm() hiess "Abbrechen" frueher + * "normal laden", und das liest jeder als "gar nicht laden". */ function askCpuMoe(m, vramMB) { - if (!looksMoe(m) || !vramMB || !m.sizeGB || m.sizeGB * 1024 <= vramMB * 0.85) return 0; - return confirm(`${m.id} ist ein MoE-Modell (pro Wort rechnet nur ein kleiner Teil) und mit ~${m.sizeGB} GB ` - + `größer als die Karte (${(vramMB / 1024).toFixed(0)} GB).\n\n` - + `Die Experten automatisch zwischen Karte und Arbeitsspeicher aufteilen? So viele wie passen bleiben ` - + `auf der Karte, der Rest geht in den RAM der Box. Langsamer als ganz auf der Karte, aber es läuft.\n\n` - + `OK = aufteilen · Abbrechen = normal laden (passt dann vermutlich nicht)`) ? -2 : 0; + if (!looksMoe(m) || !vramMB || !m.sizeGB || m.sizeGB * 1024 <= vramMB * 0.85) return Promise.resolve(0); + return new Promise(resolve => { + const bg = document.createElement('div'); + bg.style.cssText = 'position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;padding:16px;'; + const btn = (val, label, sub, primary) => ``; + bg.innerHTML = `
+
${escapeHtml(m.id)} ist größer als die Karte
+
Das Modell braucht ~${m.sizeGB} GB, die Karte hat ${(vramMB / 1024).toFixed(0)} GB. + Es ist ein MoE-Modell — pro Wort rechnet nur ein kleiner Teil, deshalb dürfen die „Experten" in den Arbeitsspeicher der Box.
+ ${btn(-2, 'Aufteilen (empfohlen)', 'So viele Experten wie passen auf die Karte, der Rest in den RAM. Langsamer, aber es läuft.', true)} + ${btn(0, 'Ganz auf die Karte', 'Passt vermutlich nicht — die Box setzt die Karte dann von selbst zurück.', false)} + ${btn('x', 'Abbrechen', 'Nichts laden.', false)} +
`; + const done = v => { document.removeEventListener('keydown', onKey, true); bg.remove(); resolve(v); }; + const onKey = e => { if (e.key === 'Escape') { e.preventDefault(); done(null); } }; + bg.addEventListener('click', e => { + const b = e.target.closest('[data-v]'); + if (b) done(b.dataset.v === 'x' ? null : Number(b.dataset.v)); + else if (e.target === bg) done(null); + }); + document.addEventListener('keydown', onKey, true); + document.body.appendChild(bg); + bg.querySelector('[data-v="-2"]').focus(); + }); } - function provisionModel(i) { + async function provisionModel(i) { const m = _llmCatalog[i]; if (!m) return; const sel = document.getElementById('llm-cat-box-' + i); @@ -9853,7 +9876,8 @@ !confirm(`Karte ${gpu} auf ${tgt.box.node} hält gerade ${tgt.slot.model}.\n\n` + `${m.id} ersetzt es. Clients, die ${tgt.slot.model} nutzen, verlieren es dann.\n` + `Die anderen Karten dieser Box starten dabei kurz neu.\n\nErsetzen?`)) return; - const cpuMoe = askCpuMoe(m, (tgt && tgt.slot && tgt.slot.vramMB) || 0); + const cpuMoe = await askCpuMoe(m, (tgt && tgt.slot && tgt.slot.vramMB) || 0); + if (cpuMoe === null) return; const st = document.getElementById('llm-catalog-status'); if (st) { st.textContent = `Lade '${m.id}' auf ${target}… (GGUF-Download kann dauern)`; st.style.color = '#FFD60A'; } send({ action: 'llm_provision_model', targetInstance: target, gpu: gpu || '', key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: m.ctx, cpuMoe }); diff --git a/xtts/llm-adapter/adapter.py b/xtts/llm-adapter/adapter.py index e6bdd96..34bd765 100644 --- a/xtts/llm-adapter/adapter.py +++ b/xtts/llm-adapter/adapter.py @@ -29,6 +29,7 @@ import logging import os import re import shlex +import shutil import time import httpx @@ -1141,6 +1142,63 @@ def _scan_download(oids: set) -> tuple: return done, active, found +# Freier Platz vor dem Download: llama.cpp bricht bei voller Platte mitten +# im Download ab, und der Grund kommt ueber llama-swap nicht an. Vorher +# rechnen: was fehlt noch (fertige Blobs zaehlen nicht) gegen den freien +# Platz minus Puffer. Halbe Downloads, an denen seit STALE_PARTIAL_S niemand +# mehr schreibt, sind Reste abgebrochener Versuche — die duerfen weg. +DISK_RESERVE_BYTES = int(float(os.getenv("LLM_DISK_RESERVE_GB", "2")) * 1024**3) +STALE_PARTIAL_S = 30 * 60 + + +def _blob_state(oids: set) -> tuple: + """({oid: Bytes fertig}, [(pfad, bytes, mtime)] aller halben Downloads).""" + done, partial = {}, [] + for root, _dirs, fns in os.walk(MODELS_DIR): + for fn in fns: + path = os.path.join(root, fn) + try: + st = os.stat(path) + except OSError: + continue + if fn.endswith(_INPROGRESS): + partial.append((path, st.st_size, st.st_mtime)) + elif fn in oids: + done[fn] = st.st_size + return done, partial + + +def _check_disk(files: list) -> tuple: + """(ok, Meldung). Ohne HF-Dateiliste (files leer) wird nicht geprueft.""" + if not files: + return True, "" + done, partial = _blob_state({f["oid"] for f in files if f.get("oid")}) + need = sum(f["size"] for f in files if f.get("oid") not in done) + if not need: + return True, "" + free = shutil.disk_usage(MODELS_DIR).free + note = "" + if need + DISK_RESERVE_BYTES > free: + stale = [(p, b) for p, b, mt in partial if time.time() - mt > STALE_PARTIAL_S] + for path, _b in stale: + try: + os.remove(path) + except OSError: + pass + if stale: + freed = sum(b for _p, b in stale) + note = f"Reste abgebrochener Downloads geloescht ({len(stale)} Datei(en), {freed / 1024**3:.1f} GB)" + logger.info("Speicherplatz: %s", note) + free = shutil.disk_usage(MODELS_DIR).free + if need + DISK_RESERVE_BYTES > free: + gb = lambda n: f"{n / 1024**3:.1f}".replace(".", ",") + return False, (f"Zu wenig Speicherplatz auf {NODE_NAME}: das Modell braucht ca. {gb(need)} GB, " + f"frei sind {gb(free)} GB (dazu {gb(DISK_RESERVE_BYTES)} GB Puffer). " + f"Nicht mehr gebrauchte Modelle im Katalog entfernen oder Platz auf der Box schaffen." + + (f" ({note})" if note else "")) + return True, note + + async def _gpu_info(card: str) -> dict: """Name + VRAM einer Karte nach dem Laden (fuer 'in Karte N geladen').""" if not card: @@ -1258,6 +1316,13 @@ async def _handle_provision(ws, payload: dict) -> None: if quant != wanted.upper(): quant_note = f"{wanted} gibt es in {repo} nicht — nehme {quant}" logger.info("Karte %s: %s", card, quant_note) + # Vor jeder Aenderung an Registry/Belegung: passt es auf die Platte? + disk_ok, disk_note = await asyncio.to_thread(_check_disk, _files_for_quant(repo_files, quant)) + if not disk_ok: + await _emit_progress(ws, key, "error", gpu=card, error=disk_note) + return await _provision_result(ws, key, False, gpu=card, error=disk_note) + if disk_note: + quant_note = "; ".join(x for x in (quant_note, disk_note) if x) ctx_req = int(payload.get("ctx") or 8192) try: cpu_moe = int(payload.get("cpuMoe") or 0)