From d3f36d8292c477ae572e4b47006315bb45c2f25b Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sun, 25 Oct 2026 14:28:32 +0100 Subject: [PATCH] feat(llm): Kontext-Auswahl zeigt den Grafikspeicher und geht bis zum Modell-Maximum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Adapter liest den Kopf der GGUF-Datei (nur Metadaten): trainiertes Maximum (context_length) und KV-Cache pro Token — bei Sliding-Window-Modellen (Gemma) waechst nur der globale Teil mit dem Kontext. Daraus pro Stufe eine Schaetzung Modell + mmproj + KV (f16) + 1,4 GB Aufschlag (gemessen: Gemma 4 12B @32k geschaetzt 8,8 GB, belegt 8,77 GB). Diagnostic: Liste kommt von der Box (statt fest 4k..128k), bis zum Maximum des Modells (auch krumme wie 40960), mit "ca. X GB" und "passt nicht", wenn es die Karte sprengt. Alte Box ohne Angaben: feste Liste wie bisher. Co-Authored-By: Claude Opus 5.5 --- diagnostic/index.html | 20 +++-- xtts/llm-adapter/adapter.py | 141 +++++++++++++++++++++++++++++++++++- 2 files changed, 155 insertions(+), 6 deletions(-) diff --git a/diagnostic/index.html b/diagnostic/index.html index 224f2d1..4feaa66 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -9241,16 +9241,26 @@ const jobTxt = job ? (job.phase === 'download' && job.percent != null ? `lädt ${job.percent.toFixed(0)} %` : 'lädt …') : ''; // Kontextfenster dieser Karte. Mehr Kontext = mehr VRAM; passt es // nicht, setzt die Box die Karte von selbst zurueck. - const CTX = [4096, 8192, 16384, 32768, 65536, 131072]; + // Liste + Speicherbedarf kommen von der Box (aus der Modelldatei: + // trainiertes Maximum, KV-Cache pro Token). Alte Box: feste Liste. const curCtx = sl.ctx || 0; - const ctxOpts = (curCtx && !CTX.includes(curCtx) ? [curCtx, ...CTX] : CTX) - .map(n => ``).join(''); + let ctxList = Array.isArray(sl.options) && sl.options.length + ? sl.options.map(o => ({ ctx: o.ctx, est: o.estMB })) + : [4096, 8192, 16384, 32768, 65536, 131072].map(n => ({ ctx: n, est: 0 })); + if (curCtx && !ctxList.some(o => o.ctx === curCtx)) ctxList = [{ ctx: curCtx, 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 ctxSel = sl.model ? `` : ''; - rows.push(`
+ rows.push(`
${escapeHtml(b.node)} · Karte ${escapeHtml(sl.gpu)}
${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)
' : ''}
diff --git a/xtts/llm-adapter/adapter.py b/xtts/llm-adapter/adapter.py index 5ad45ff..bdf8f56 100644 --- a/xtts/llm-adapter/adapter.py +++ b/xtts/llm-adapter/adapter.py @@ -517,6 +517,144 @@ def _model_ctx(key: str) -> int: return int(m.group(1)) if m else 0 +# ── Kontextfenster: was das Modell kann + was es an Grafikspeicher kostet ── +# +# Aus dem Kopf der GGUF-Datei (nur Metadaten, die Gewichte werden nicht +# gelesen): trainiertes Maximum (context_length) und die Groesse des KV-Caches +# pro Token. Modelle mit Sliding-Window-Attention (Gemma) halten in den +# "lokalen" Schichten nur ein festes Fenster — nur die globalen Schichten +# wachsen mit dem Kontext. Annahme: KV-Cache in f16 (llama.cpp-Standard). + +_GGUF_T = {0: "B", 1: "b", 2: "H", 3: "h", 4: "I", 5: "i", 6: "f", 7: "?", 10: "Q", 11: "q", 12: "d"} +_gguf_cache: dict = {} # path → (mtime, meta) +CTX_STEPS = [4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576] +# Fester Aufschlag (CUDA-Kontext + Rechenpuffer von llama.cpp) — gemessen auf +# einer RTX 3060 mit Gemma 4 12B bei 32k: ~1,4 GB ueber Modell + KV-Cache. +VRAM_OVERHEAD_MB = 1400 +UBATCH = 512 + + +def _gguf_meta(path: str) -> dict: + """Metadaten-Block einer GGUF-Datei (gecacht). Lange String-Listen (der + Tokenizer) werden uebersprungen, nicht gespeichert.""" + try: + mt = os.path.getmtime(path) + except OSError: + return {} + hit = _gguf_cache.get(path) + if hit and hit[0] == mt: + return hit[1] + import struct + meta: dict = {} + + def rd(f, fmt): + return struct.unpack("<" + fmt, f.read(struct.calcsize("<" + fmt)))[0] + + def rstr(f): + return f.read(rd(f, "Q")).decode("utf-8", "replace") + + def rval(f, t): + if t == 8: + return rstr(f) + if t == 9: + et, n = rd(f, "I"), rd(f, "Q") + if et == 8: + for _ in range(n): + f.seek(rd(f, "Q"), 1) + return None + if et == 9: + raise ValueError("verschachtelte Arrays") + fmt = _GGUF_T[et] + if n > 4096: + f.seek(struct.calcsize("<" + fmt) * n, 1) + return None + return [rd(f, fmt) for _ in range(n)] + return rd(f, _GGUF_T[t]) + + try: + with open(path, "rb") as f: + if f.read(4) != b"GGUF": + return {} + rd(f, "I"); rd(f, "Q") + for _ in range(rd(f, "Q")): + k = rstr(f) + v = rval(f, rd(f, "I")) + if v is not None: + meta[k] = v + except Exception as e: + logger.debug("GGUF %s nicht lesbar: %s", path, e) + meta = {} + _gguf_cache[path] = (mt, meta) + return meta + + +def _model_files(key: str) -> tuple: + """(Gewichte.gguf, mmproj.gguf|None) eines Modells im HF-Cache unter /models + — aus "-hf org/repo:QUANT" bzw. "-m pfad" im llama-server-Aufruf.""" + import glob + cmd = _model_cmd(key) or "" + m = re.search(r"(?:^|\s)-m\s+(\S+)", cmd) + if m and os.path.isfile(m.group(1)): + return m.group(1), None + m = re.search(r"(?:^|\s)-hf\s+([^\s:]+)(?::(\S+))?", cmd) + if not m: + return None, None + repo, quant = m.group(1), (m.group(2) or "").lower() + base = os.path.join(MODELS_DIR, "models--" + repo.replace("/", "--"), "snapshots") + files = [p for p in glob.glob(os.path.join(base, "*", "*.gguf")) if os.path.isfile(p)] + weights = [p for p in files if "mmproj" not in os.path.basename(p).lower()] + if quant: + weights = [p for p in weights if quant in os.path.basename(p).lower()] or weights + mmproj = [p for p in files if "mmproj" in os.path.basename(p).lower()] + return (weights[0] if weights else None), (mmproj[0] if mmproj else None) + + +def _ctx_profile(key: str) -> dict: + """{ctxMax, options: [{ctx, estMB}]} fuer die Kontext-Auswahl der Diagnostic. + Leer, wenn die Modelldatei (noch) nicht da oder nicht lesbar ist.""" + weights, mmproj = _model_files(key) + if not weights: + return {} + meta = _gguf_meta(weights) + arch = meta.get("general.architecture") + if not arch: + return {} + g = lambda k, d=None: meta.get(f"{arch}.{k}", d) + n_layer = int(g("block_count", 0) or 0) + if not n_layer: + return {} + + def per_layer(v, d=0): + return list(v) if isinstance(v, list) else [v if v is not None else d] * n_layer + + kv_heads = per_layer(g("attention.head_count_kv", g("attention.head_count", 0))) + k_len = int(g("attention.key_length", 0) or 0) + v_len = int(g("attention.value_length", 0) or 0) + if not (k_len and v_len): + emb, heads = int(g("embedding_length", 0) or 0), int(g("attention.head_count", 0) or 0) + k_len = v_len = emb // heads if heads else 0 + k_swa = int(g("attention.key_length_swa", k_len) or k_len) + v_swa = int(g("attention.value_length_swa", v_len) or v_len) + window = int(g("attention.sliding_window", 0) or 0) + pattern = g("attention.sliding_window_pattern") + swa = per_layer(pattern, False) if isinstance(pattern, list) else [False] * n_layer + if not window: + swa = [False] * n_layer + + per_tok = sum(int(kv_heads[i]) * (k_len + v_len) * 2 for i in range(n_layer) if not swa[i]) + swa_tok = sum(int(kv_heads[i]) * (k_swa + v_swa) * 2 for i in range(n_layer) if swa[i]) + files_mb = (os.path.getsize(weights) + (os.path.getsize(mmproj) if mmproj else 0)) / 1048576 + ctx_max = int(g("context_length", 0) or 0) + steps = [c for c in CTX_STEPS if not ctx_max or c <= ctx_max] + if ctx_max and ctx_max not in steps: + steps.append(ctx_max) # krummes Maximum (z.B. 40960) mit anbieten + opts = [] + for c in steps: + kv_mb = (per_tok * c + swa_tok * min(c, window + UBATCH)) / 1048576 + opts.append({"ctx": c, "estMB": int(files_mb + kv_mb + VRAM_OVERHEAD_MB)}) + return {"ctxMax": ctx_max, "options": opts} + + async def _vram_used() -> dict: """{index: belegte MB} — frisch bei jedem Aufruf (aendert sich laufend).""" try: @@ -555,7 +693,8 @@ async def _slots_info() -> list: 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}) + "model": m, "state": state, "ctx": _model_ctx(m) if m else 0, + **(_ctx_profile(m) if m else {})}) return out