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:
2026-10-31 15:49:10 +01:00
co-authored by Claude Opus 5.5
parent e3fe93e0e9
commit 126e33c4c0
2 changed files with 69 additions and 17 deletions
+29 -2
View File
@@ -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