c074d977ce
Inspiriert von Odysseus' Cookbook: backend/models_catalog.json mit echten Metadaten (total/active params, moe, generation) je Rolle. services/catalog.py: Laden, Name-Match, MoE-bewusstes Scoring (Wissen + Tempo via tps -> MoE-first auf der bandbreiten-Box), Fit. - discover.py: Empfehlung jetzt KATALOG-FIRST (kuratiert, korrekt), HF-Dynamik als Ergaenzung/Fallback. - maintenance.model_upgrades: Metadaten aus Katalog -> praezise Familie/Generation/Groesse + MoE-first (dense ersetzt MoE nur bei grossem Wissens-Sprung). Behebt Coder-Next=7B-Fehlschaetzung, Qwen2.5-VL-Generations-Downgrade, falsches dense-scout-Upgrade. - fit.py: MXFP4/FP8/AWQ in der Quant-Tabelle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
116 lines
4.4 KiB
Python
116 lines
4.4 KiB
Python
"""
|
|
Kuratierter Modell-Katalog ("Cookbook", inspiriert von Odysseus): EINE Quelle der
|
|
Wahrheit für KORREKTE Metadaten (total/active params, moe, generation) statt
|
|
Namens-Raterei. Macht Empfehlung + Upgrade-Erkennung präzise und MoE-bewusst
|
|
für die bandbreiten-limitierte Strix-Halo-Box.
|
|
|
|
Daten: backend/models_catalog.json. Fällt sanft aus (leerer Katalog), wenn die
|
|
Datei fehlt → discover nutzt dann nur die HF-Dynamik.
|
|
"""
|
|
|
|
import json
|
|
import math
|
|
import os
|
|
import re
|
|
|
|
from services.fit import estimate_memory_gb, estimate_speed
|
|
|
|
_CATALOG_PATH = os.path.join(os.path.dirname(__file__), "..", "models_catalog.json")
|
|
_cache: dict = {"mtime": 0.0, "models": []}
|
|
|
|
|
|
def _load() -> list[dict]:
|
|
try:
|
|
mt = os.path.getmtime(_CATALOG_PATH)
|
|
if mt != _cache["mtime"]:
|
|
with open(_CATALOG_PATH, encoding="utf-8") as f:
|
|
data = json.load(f) or {}
|
|
_cache.update(mtime=mt, models=[m for m in data.get("models", []) if m.get("name")])
|
|
except (OSError, ValueError):
|
|
_cache.update(mtime=0.0, models=[])
|
|
return _cache["models"]
|
|
|
|
|
|
def _norm(name: str) -> str:
|
|
"""Vergleichs-Stamm: kleingeschrieben, Org-Prefix/Quant/GGUF/Split entfernt."""
|
|
s = (name or "").lower().split("/")[-1]
|
|
s = re.sub(r"\.gguf$", "", s)
|
|
s = re.sub(r"-\d+-of-\d+$", "", s)
|
|
s = re.sub(r"[-_](ud-)?(i?q\d[\w]*|f16|bf16|fp16|f32|mxfp4)$", "", s)
|
|
return s.strip("-_ ")
|
|
|
|
|
|
def entries() -> list[dict]:
|
|
return list(_load())
|
|
|
|
|
|
def entries_for_role(role: str) -> list[dict]:
|
|
return [e for e in _load() if e.get("role") == role]
|
|
|
|
|
|
def meta_for_name(name: str) -> dict | None:
|
|
"""Katalog-Metadaten zu einem Modell(namen) — matcht lokalen Namen ODER HF-Repo."""
|
|
n = _norm(name)
|
|
if not n:
|
|
return None
|
|
for e in _load():
|
|
cand = {_norm(e.get("name", "")), _norm(e.get("repo", ""))}
|
|
if n in cand or any(c and (c in n or n in c) for c in cand):
|
|
return e
|
|
return None
|
|
|
|
|
|
def fit_of(e: dict, ram_gb: float) -> dict:
|
|
"""Hardware-Fit eines Katalog-Eintrags (MoE-bewusst über active_params_b)."""
|
|
total = float(e.get("total_params_b") or 7)
|
|
active = float(e.get("active_params_b") or total)
|
|
quant = e.get("quant") or "Q4_K_M"
|
|
ctx = int(e.get("ctx") or 32768)
|
|
req_gb = estimate_memory_gb(total, quant, ctx)
|
|
tps = estimate_speed(req_gb, ram_gb, (active / total) if total else 1.0)
|
|
usable = max(ram_gb - 4.0, 0)
|
|
if req_gb > usable:
|
|
level, text = "too_tight", "Zu groß (OOM)"
|
|
elif req_gb > usable * 0.8:
|
|
level, text = "marginal", "Könnte knapp werden"
|
|
else:
|
|
level, text = "perfect", "Passt perfekt"
|
|
return {"level": level, "text": text, "req_gb": round(req_gb, 1), "tps": round(tps, 0)}
|
|
|
|
|
|
def stack_score(e: dict, ram_gb: float) -> float:
|
|
"""Score für DIESE Hardware: muss passen, dann Wissen (total params) + Tempo
|
|
(tps — belohnt MoE durch niedrige aktive Params automatisch). Bandbreiten-Box
|
|
→ MoE gewinnt bei vergleichbarem Wissen gegen dense."""
|
|
fit = fit_of(e, ram_gb)
|
|
if fit["level"] == "too_tight":
|
|
return -100.0
|
|
total = float(e.get("total_params_b") or 7)
|
|
fit_bonus = 3.0 if fit["level"] == "perfect" else 1.0
|
|
knowledge = math.log2(total + 1) / 8.0 # ~0..1 (bis ~256B)
|
|
speed = min((fit["tps"] or 0) / 80.0, 1.0) # normalisiert; MoE = hohe tps
|
|
return fit_bonus + 1.2 * knowledge + 1.0 * speed
|
|
|
|
|
|
def to_model_dict(e: dict, ram_gb: float) -> dict:
|
|
"""Katalog-Eintrag → discover-kompatibles Modell-Dict (echte Metadaten)."""
|
|
total = float(e.get("total_params_b") or 7)
|
|
active = e.get("active_params_b")
|
|
repo = e.get("repo") or e.get("name")
|
|
role = e.get("role")
|
|
caps = {
|
|
"moe": bool(e.get("moe")), "active_b": active,
|
|
"tools": "yes" if e.get("tools") else "no",
|
|
"vision": bool(e.get("vision")), "coder": role == "coder",
|
|
"reasoning": role == "heavy", "embedding": False,
|
|
"ctx": e.get("ctx"), "params_b": total, "arch": e.get("family"),
|
|
}
|
|
return {
|
|
"name": e.get("name"), "author": repo.split("/")[0] if "/" in repo else "catalog",
|
|
"repo": repo, "role": role, "params_b": total, "active_b": active,
|
|
"moe": bool(e.get("moe")), "generation": e.get("generation"),
|
|
"family": e.get("family"), "quant": e.get("quant") or "Q4_K_M",
|
|
"tags": ["catalog"], "downloads": 0, "fit": fit_of(e, ram_gb),
|
|
"optimal_ctx": int(e.get("ctx") or 32768), "caps": caps, "curated": True,
|
|
}
|