fix(llm): Kontext-Auswahl zeigt wieder Maximum + GB — auch fuer Modelle auf der Platte
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 <noreply@anthropic.com>
This commit is contained in:
+40
-15
@@ -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 `<option value="${o.ctx}"${o.ctx === selCtx ? ' selected' : ''}>${_ctxLabel(o.ctx)} Tokens${max}${gb}${warn}</option>`;
|
||||
}).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 => `<option value="${escapeHtml(t.value)}"${t.value === pre ? ' selected' : ''}>${escapeHtml(t.label)}</option>`).join('')
|
||||
: '<option value="">(keine Box online)</option>';
|
||||
@@ -9407,8 +9441,8 @@
|
||||
'<td ' + td + vramStyle + 'white-space:nowrap;">' + vram + '</td>' +
|
||||
'<td ' + td + 'white-space:nowrap;">' + haveTxt + '</td>' +
|
||||
'<td ' + td + '"><select id="llm-cat-box-' + i + '" onchange="llmCatPick(' + i + ', \'target\', this.value)" style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:3px 6px;color:#E0E0F0;font-size:11px;">' + opts + '</select></td>' +
|
||||
'<td ' + td + '"><select id="llm-cat-ctx-' + i + '" onchange="llmCatPick(' + i + ', \'ctx\', Number(this.value))" title="Kontextfenster, mit dem das Modell geladen wird — mehr Kontext braucht mehr Grafikspeicher" style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:3px 6px;color:#E0E0F0;font-size:11px;">'
|
||||
+ _ctxSteps(ctxPick).map(n => `<option value="${n}"${n === ctxPick ? ' selected' : ''}>${_ctxLabel(n)} Tokens</option>`).join('') + '</select></td>' +
|
||||
'<td ' + td + '"><select id="llm-cat-ctx-' + i + '" onchange="llmCatPick(' + i + ', \'ctx\', Number(this.value))" title="' + escapeHtml(_ctxTitle(chCat)) + '" style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:3px 6px;color:#E0E0F0;font-size:11px;max-width:230px;">'
|
||||
+ _ctxOptionsHtml(chCat, ctxPick, (tgtObj.slot && tgtObj.slot.vramMB) || 0) + '</select></td>' +
|
||||
'<td ' + td + 'white-space:nowrap;"><button class="btn secondary" ' + (boxes.length ? '' : 'disabled') + ' onclick="provisionModel(' + i + ')" title="Herunterladen (falls nötig) und in die Ziel-Karte laden" style="padding:3px 10px;font-size:11px;">Download + Laden</button>'
|
||||
+ ' <button class="btn secondary" ' + (boxes.length ? '' : 'disabled') + ' onclick="downloadModelOnly(' + i + ')" title="Nur auf die Box herunterladen — in keine Karte laden (Ziel-Karte und Kontext spielen dafür keine Rolle)" style="padding:3px 10px;font-size:11px;">Nur downloaden</button></td>' +
|
||||
'</tr>';
|
||||
@@ -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 `<option value="${o.ctx}"${o.ctx === selCtx ? ' selected' : ''}>${label} Tokens${gb}${tooBig}</option>`;
|
||||
}).join('');
|
||||
const ch = _ctxChoices(shown, b, sl);
|
||||
const ctxOpts = _ctxOptionsHtml(ch, selCtx, sl.vramMB || 0);
|
||||
const ctxSel = shown
|
||||
? `<select data-key="${escapeHtml(pkey)}" data-model="${escapeHtml(shown)}"
|
||||
onchange="onLlmCardCtxPick(this)" ${job ? 'disabled' : ''} title="Kontextfenster (Eingabe + Ausgabe).${sameModel ? ' GB = geschätzter Grafikspeicher für Modell + Kontext auf dieser Karte.' : ' Speicherbedarf kennt die Box erst nach dem ersten Laden.'}${sameModel && sl.ctxMax ? ' Das Modell kann höchstens ' + sl.ctxMax + ' Tokens.' : ''}"
|
||||
onchange="onLlmCardCtxPick(this)" ${job ? 'disabled' : ''} title="${escapeHtml(_ctxTitle(ch))}"
|
||||
style="flex:1;min-width:0;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:5px 6px;color:#E0E0F0;font-size:12px;">${ctxOpts}</select>`
|
||||
: '';
|
||||
const ctxCell = `<div style="display:flex;gap:6px;align-items:center;min-width:0;">${ctxSel}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user