feat(llm): Kontext-Auswahl zeigt den Grafikspeicher und geht bis zum Modell-Maximum
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 <noreply@anthropic.com>
This commit is contained in:
+15
-5
@@ -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 => `<option value="${n}"${n === curCtx ? ' selected' : ''}>${n >= 1024 ? (n / 1024) + 'k' : n} Tokens</option>`).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 `<option value="${o.ctx}"${o.ctx === curCtx ? ' selected' : ''}>${label} Tokens${gb}${tooBig}</option>`;
|
||||
}).join('');
|
||||
const ctxSel = sl.model
|
||||
? `<select data-box="${escapeHtml(b.instanceId)}" data-gpu="${escapeHtml(sl.gpu)}" data-model="${escapeHtml(sl.model)}" data-current="${curCtx}"
|
||||
onchange="onLlmCardCtx(this)" ${job ? 'disabled' : ''} title="Kontextfenster (Eingabe + Ausgabe). Mehr braucht mehr Grafikspeicher."
|
||||
onchange="onLlmCardCtx(this)" ${job ? 'disabled' : ''} title="Kontextfenster (Eingabe + Ausgabe). GB = geschätzter Grafikspeicher für Modell + Kontext auf dieser Karte.${sl.ctxMax ? ' Das Modell kann höchstens ' + sl.ctxMax + ' Tokens.' : ''}"
|
||||
style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:5px 6px;color:#E0E0F0;font-size:12px;">${ctxOpts}</select>`
|
||||
: '<span></span>';
|
||||
rows.push(`<div style="display:grid;grid-template-columns:minmax(220px,1fr) minmax(260px,2fr) 130px 160px;gap:10px;align-items:center;">
|
||||
rows.push(`<div style="display:grid;grid-template-columns:minmax(220px,1fr) minmax(260px,2fr) 230px 160px;gap:10px;align-items:center;">
|
||||
<div><b style="color:#E0E0F0;">${escapeHtml(b.node)}</b> · Karte ${escapeHtml(sl.gpu)}
|
||||
<div style="color:#555570;font-size:11px;">${escapeHtml(sl.gpuName || '')}${vramGB ? ' · ' + vramGB.toFixed(0) + ' GB' : ''}${sl.vramUsedMB >= 0 ? ' · belegt ' + (sl.vramUsedMB / 1024).toFixed(1) + ' GB' : ''}${sl.ctx ? ' · Kontext ' + sl.ctx : ''}</div>
|
||||
${sl.onCpu ? '<div style="color:#FF9F0A;font-size:11px;" title="llama.cpp findet die Karte nicht und rechnet langsam auf der CPU. Ursache meist: Treiber zu alt fuer das CUDA im llama-swap-Image. Abhilfe: auf der Box sudo ./bootstrap.sh --upgrade-driver">⚠ Modell läuft auf der CPU, Karte leer — Treiber zu alt? (bootstrap.sh --upgrade-driver)</div>' : ''}</div>
|
||||
|
||||
+140
-1
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user