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
|
// Kontext-Stufen fuer Modelle, deren Speicherbedarf die Box noch nicht kennt
|
||||||
const _CTX_STEPS = [4096, 8192, 16384, 32768, 65536, 131072];
|
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); }
|
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
|
// Auswahl im Katalog (Ziel-Karte, Kontext) je Modell — ueberlebt das Neuzeichnen
|
||||||
const _llmCatSel = {};
|
const _llmCatSel = {};
|
||||||
function llmCatPick(i, field, value) {
|
function llmCatPick(i, field, value) {
|
||||||
const m = _llmCatalog[i];
|
const m = _llmCatalog[i];
|
||||||
if (m) _llmCatSel[m.id] = { ...(_llmCatSel[m.id] || {}), [field]: value };
|
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() {
|
function renderLlmCatalog() {
|
||||||
const box = document.getElementById('llm-catalog-list');
|
const box = document.getElementById('llm-catalog-list');
|
||||||
@@ -9394,6 +9426,8 @@
|
|||||||
const pre = targets.some(t => t.value === pick.target) ? pick.target
|
const pre = targets.some(t => t.value === pick.target) ? pick.target
|
||||||
: (targets.find(t => t.slot && !t.slot.model) || targets[0] || {}).value;
|
: (targets.find(t => t.slot && !t.slot.model) || targets[0] || {}).value;
|
||||||
const ctxPick = pick.ctx || m.ctx || 8192;
|
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
|
const opts = targets.length
|
||||||
? targets.map(t => `<option value="${escapeHtml(t.value)}"${t.value === pre ? ' selected' : ''}>${escapeHtml(t.label)}</option>`).join('')
|
? targets.map(t => `<option value="${escapeHtml(t.value)}"${t.value === pre ? ' selected' : ''}>${escapeHtml(t.label)}</option>`).join('')
|
||||||
: '<option value="">(keine Box online)</option>';
|
: '<option value="">(keine Box online)</option>';
|
||||||
@@ -9407,8 +9441,8 @@
|
|||||||
'<td ' + td + vramStyle + 'white-space:nowrap;">' + vram + '</td>' +
|
'<td ' + td + vramStyle + 'white-space:nowrap;">' + vram + '</td>' +
|
||||||
'<td ' + td + 'white-space:nowrap;">' + haveTxt + '</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-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;">'
|
'<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;">'
|
||||||
+ _ctxSteps(ctxPick).map(n => `<option value="${n}"${n === ctxPick ? ' selected' : ''}>${_ctxLabel(n)} Tokens</option>`).join('') + '</select></td>' +
|
+ _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>'
|
'<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>' +
|
+ ' <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>';
|
'</tr>';
|
||||||
@@ -9593,20 +9627,11 @@
|
|||||||
// Speicherbedarf nicht → feste Stufen ohne GB-Angabe.
|
// Speicherbedarf nicht → feste Stufen ohne GB-Angabe.
|
||||||
const sameModel = shown && shown === sl.model;
|
const sameModel = shown && shown === sl.model;
|
||||||
const selCtx = pend ? pend.ctx : curCtx;
|
const selCtx = pend ? pend.ctx : curCtx;
|
||||||
let ctxList = sameModel && Array.isArray(sl.options) && sl.options.length
|
const ch = _ctxChoices(shown, b, sl);
|
||||||
? sl.options.map(o => ({ ctx: o.ctx, est: o.estMB }))
|
const ctxOpts = _ctxOptionsHtml(ch, selCtx, sl.vramMB || 0);
|
||||||
: _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 ctxSel = shown
|
const ctxSel = shown
|
||||||
? `<select data-key="${escapeHtml(pkey)}" data-model="${escapeHtml(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>`
|
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}
|
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)
|
weights, mmproj = _model_files(key)
|
||||||
if not weights:
|
if not weights:
|
||||||
return {}
|
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)
|
meta = _gguf_meta(weights)
|
||||||
arch = meta.get("general.architecture")
|
arch = meta.get("general.architecture")
|
||||||
if not arch:
|
if not arch:
|
||||||
@@ -784,7 +791,7 @@ def _ctx_profile(key: str) -> dict:
|
|||||||
ctx_max = int(g("context_length", 0) or 0)
|
ctx_max = int(g("context_length", 0) or 0)
|
||||||
exp_by_layer = meta.get("_expert_bytes_by_layer") or {}
|
exp_by_layer = meta.get("_expert_bytes_by_layer") or {}
|
||||||
moe = bool(exp_by_layer) or int(g("expert_count", 0) or 0) > 0
|
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:
|
if cpu_moe:
|
||||||
layers = sorted(exp_by_layer) if cpu_moe < 0 else sorted(exp_by_layer)[: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
|
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
|
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:
|
def _disk_info() -> dict:
|
||||||
"""Freier Platz + fertig heruntergeladene Modelle (je Repo) fuer die Diagnostic."""
|
"""Freier Platz + fertig heruntergeladene Modelle (je Repo) fuer die Diagnostic."""
|
||||||
out = {"freeBytes": 0, "models": []}
|
out = {"freeBytes": 0, "models": []}
|
||||||
@@ -1381,7 +1407,8 @@ def _disk_info() -> dict:
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
if size:
|
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:
|
except OSError as e:
|
||||||
logger.info("Plattenbelegung nicht lesbar: %s", e)
|
logger.info("Plattenbelegung nicht lesbar: %s", e)
|
||||||
return out
|
return out
|
||||||
|
|||||||
Reference in New Issue
Block a user