From 126e33c4c09dd2c97ad5d83b31330c765d2be3fd Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 31 Oct 2026 15:49:10 +0100 Subject: [PATCH] =?UTF-8?q?fix(llm):=20Kontext-Auswahl=20zeigt=20wieder=20?= =?UTF-8?q?Maximum=20+=20GB=20=E2=80=94=20auch=20fuer=20Modelle=20auf=20de?= =?UTF-8?q?r=20Platte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seit "erst waehlen, dann Laden" zeigte die Kontext-Liste fuer ein noch nicht geladenes Modell nur feste Stufen — ohne Maximum und ohne Speicherbedarf. Jetzt: - llm-adapter: disk.models[].profiles — pro heruntergeladener Quantisierung Maximum (context_length) und GB je Stufe aus dem Kopf der GGUF (gleiche Rechnung wie fuer die geladene Karte, Annahme: alles auf der Karte). Gesplittete Modelle ausgenommen. - Diagnostic (Karte + Katalog-Spalte): Stufen bis zum Maximum, "(Maximum)" markiert, GB je Stufe, "⚠ passt nicht" bzw. bei MoE "⚠ nur mit Aufteilen". Nicht heruntergeladen: feste Stufen + Hinweis im Tooltip. Katalog rechnet mit der gewaehlten Ziel-Karte. Co-Authored-By: Claude Opus 5.5 --- diagnostic/index.html | 55 +++++++++++++++++++++++++++---------- xtts/llm-adapter/adapter.py | 31 +++++++++++++++++++-- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/diagnostic/index.html b/diagnostic/index.html index 5fe7dec..9d82718 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -9353,13 +9353,45 @@ } // Kontext-Stufen fuer Modelle, deren Speicherbedarf die Box noch nicht kennt const _CTX_STEPS = [4096, 8192, 16384, 32768, 65536, 131072]; - function _ctxSteps(cur) { return cur && !_CTX_STEPS.includes(cur) ? [cur, ..._CTX_STEPS].sort((a, b) => a - b) : _CTX_STEPS; } function _ctxLabel(n) { return n >= 1024 ? (n % 1024 ? (n / 1024).toFixed(1) : n / 1024) + 'k' : String(n); } + // Kontext-Auswahl fuer ein Modell auf einer Box: was es maximal kann und was + // jede Stufe an Grafikspeicher kostet. Quelle: das geladene Modell der Karte + // (inkl. MoE-Aufteilung) oder — liegt es nur auf der Platte — die Datei + // dort (Annahme: alles auf der Karte). Nicht heruntergeladen: feste Stufen. + function _ctxChoices(modelId, box, sl) { + if (sl && modelId && modelId === sl.model && Array.isArray(sl.options) && sl.options.length) + return { list: sl.options.map(o => ({ ctx: o.ctx, est: o.estMB })), ctxMax: sl.ctxMax || 0, moe: !!sl.moe, known: true, loaded: true }; + const m = _llmCatalog.find(x => x.id === modelId); + const d = m && box && box.disk && (box.disk.models || []).find(x => x.repo === m.hfRepo); + const profs = (d && d.profiles) || []; + const p = profs.find(x => String(x.quant || '').toUpperCase() === String((m && m.quant) || '').toUpperCase()) || profs[0]; + if (p && Array.isArray(p.options) && p.options.length) + return { list: p.options.map(o => ({ ctx: o.ctx, est: o.estMB })), ctxMax: p.ctxMax || 0, moe: !!p.moe, known: true }; + return { list: _CTX_STEPS.map(n => ({ ctx: n, est: 0 })), ctxMax: 0, moe: false, known: false }; + } + function _ctxOptionsHtml(ch, selCtx, vramMB) { + let list = ch.list; + if (selCtx && !list.some(o => o.ctx === selCtx)) list = [{ ctx: selCtx, est: 0 }, ...list]; + return list.map(o => { + const gb = o.est ? ` · ca. ${(o.est / 1024).toFixed(1).replace('.', ',')} GB` : ''; + const over = o.est && vramMB && o.est > vramMB * 0.97; + const warn = over ? (ch.moe && !ch.loaded ? ' ⚠ nur mit Aufteilen' : ' ⚠ passt nicht') : ''; + const max = ch.ctxMax && o.ctx === ch.ctxMax ? ' (Maximum)' : ''; + return ``; + }).join(''); + } + function _ctxTitle(ch) { + return 'Kontextfenster (Eingabe + Ausgabe).' + (ch.known + ? (ch.ctxMax ? ` Das Modell kann höchstens ${ch.ctxMax} Tokens.` : '') + ' GB = geschätzter Grafikspeicher für Modell + Kontext' + + (ch.loaded ? ' auf dieser Karte.' : (ch.moe ? ' (alles auf der Karte — mit Aufteilen der Experten passt mehr).' : '.')) + : ' Maximum und Speicherbedarf kennt die Box erst, wenn das Modell heruntergeladen ist.'); + } // Auswahl im Katalog (Ziel-Karte, Kontext) je Modell — ueberlebt das Neuzeichnen const _llmCatSel = {}; function llmCatPick(i, field, value) { const m = _llmCatalog[i]; if (m) _llmCatSel[m.id] = { ...(_llmCatSel[m.id] || {}), [field]: value }; + if (field === 'target') renderLlmCatalog(); // Kontext-Liste gilt pro Box (wartet, bis das Dropdown zu ist) } function renderLlmCatalog() { const box = document.getElementById('llm-catalog-list'); @@ -9394,6 +9426,8 @@ const pre = targets.some(t => t.value === pick.target) ? pick.target : (targets.find(t => t.slot && !t.slot.model) || targets[0] || {}).value; const ctxPick = pick.ctx || m.ctx || 8192; + const tgtObj = targets.find(t => t.value === pre) || {}; + const chCat = _ctxChoices(m.id, tgtObj.box, tgtObj.slot); const opts = targets.length ? targets.map(t => ``).join('') : ''; @@ -9407,8 +9441,8 @@ '' + vram + '' + '' + haveTxt + '' + '' + - '' + + '' + '' + ' ' + ''; @@ -9593,20 +9627,11 @@ // Speicherbedarf nicht → feste Stufen ohne GB-Angabe. const sameModel = shown && shown === sl.model; const selCtx = pend ? pend.ctx : curCtx; - let ctxList = sameModel && Array.isArray(sl.options) && sl.options.length - ? sl.options.map(o => ({ ctx: o.ctx, est: o.estMB })) - : _CTX_STEPS.map(n => ({ ctx: n, est: 0 })); - if (selCtx && !ctxList.some(o => o.ctx === selCtx)) ctxList = [{ ctx: selCtx, est: 0 }, ...ctxList]; - const vramMB = sl.vramMB || 0; - const ctxOpts = ctxList.map(o => { - const label = o.ctx >= 1024 ? (o.ctx % 1024 ? (o.ctx / 1024).toFixed(1) : o.ctx / 1024) + 'k' : o.ctx; - const gb = o.est ? ` · ca. ${(o.est / 1024).toFixed(1).replace('.', ',')} GB` : ''; - const tooBig = o.est && vramMB && o.est > vramMB * 0.97 ? ' ⚠ passt nicht' : ''; - return ``; - }).join(''); + const ch = _ctxChoices(shown, b, sl); + const ctxOpts = _ctxOptionsHtml(ch, selCtx, sl.vramMB || 0); const ctxSel = shown ? `` : ''; const ctxCell = `
${ctxSel} diff --git a/xtts/llm-adapter/adapter.py b/xtts/llm-adapter/adapter.py index 47e1d9b..5dc6cf3 100644 --- a/xtts/llm-adapter/adapter.py +++ b/xtts/llm-adapter/adapter.py @@ -771,6 +771,13 @@ def _ctx_profile(key: str) -> dict: weights, mmproj = _model_files(key) if not weights: return {} + return _profile_of_files(weights, mmproj, lambda: _model_cpu_moe(key)) + + +def _profile_of_files(weights: str, mmproj, cpu_moe_of=lambda: 0) -> dict: + """Wie _ctx_profile, aber fuer beliebige Dateien — auch heruntergeladene + Modelle, die (noch) auf keiner Karte liegen. cpu_moe_of: Aufteilung der + Experten (0 = Schaetzung "alles auf der Karte").""" meta = _gguf_meta(weights) arch = meta.get("general.architecture") if not arch: @@ -784,7 +791,7 @@ def _ctx_profile(key: str) -> dict: ctx_max = int(g("context_length", 0) or 0) exp_by_layer = meta.get("_expert_bytes_by_layer") or {} moe = bool(exp_by_layer) or int(g("expert_count", 0) or 0) > 0 - cpu_moe = _model_cpu_moe(key) if moe else 0 + cpu_moe = cpu_moe_of() if moe else 0 if cpu_moe: layers = sorted(exp_by_layer) if cpu_moe < 0 else sorted(exp_by_layer)[:cpu_moe] ram_mb = sum(exp_by_layer[l] for l in layers) / 1048576 @@ -1364,6 +1371,25 @@ async def _prefetch(ws, key: str, repo: str, files: list, card: str, cancel: thr return err +def _disk_profiles(repo_dir: str) -> list: + """Kontext-Profil (Maximum + GB je Stufe, Annahme: alles auf der Karte) je + heruntergeladener Quantisierung — damit die Diagnostic auch fuer Modelle, + die auf keiner Karte liegen, zeigen kann, was sie koennen und kosten.""" + import glob + files = [p for p in glob.glob(os.path.join(repo_dir, "snapshots", "*", "**", "*.gguf"), recursive=True) + if os.path.isfile(p)] + mmproj = next((p for p in files if "mmproj" in os.path.basename(p).lower()), None) + out = [] + for w in files: + if not _is_main_gguf(w) or _SPLIT_RE.search(w): + continue # gesplittete Modelle: Groesse waere nur die des ersten Teils + prof = _profile_of_files(w, mmproj) + if prof: + out.append({"quant": _quant_of(w), **{k: prof[k] for k in ("ctxMax", "options", "moe") if k in prof}, + **({"expertsMB": prof["expertsMB"]} if "expertsMB" in prof else {})}) + return out + + def _disk_info() -> dict: """Freier Platz + fertig heruntergeladene Modelle (je Repo) fuer die Diagnostic.""" out = {"freeBytes": 0, "models": []} @@ -1381,7 +1407,8 @@ def _disk_info() -> dict: except OSError: pass if size: - out["models"].append({"repo": name[len("models--"):].replace("--", "/", 1), "bytes": size}) + out["models"].append({"repo": name[len("models--"):].replace("--", "/", 1), "bytes": size, + "profiles": _disk_profiles(os.path.join(MODELS_DIR, name))}) except OSError as e: logger.info("Plattenbelegung nicht lesbar: %s", e) return out