""" Speicher-Budget & SETUP-BEWUSSTE ctx-Vergabe — EINE Quelle der Wahrheit. Modelliert die auf der Box VERIFIZIERTE Residenz-Realität (llama-swap, GTT ~124 GB): • Die `persistent`-Gruppe (brains = Hirn+embed+vision) bleibt IMMER resident — seit dem Key-Fix 03.07.2026 greift der Schutz wirklich (vorher stand `persist` in der Config, das llama-swap stillschweigend ignorierte; on-demand-Last verdrängte damals die ganze Gruppe). • Ein on-demand-Modell (heavy/coder/…) lädt NEBEN die brains-Gruppe und muss deren Footprint mit einplanen. Daraus folgt, wie viel Speicher NEBEN einem Zielmodell reserviert bleiben muss — und damit der größte Kontext, der wirklich passt (nicht nur für das Modell allein). Vorher rechnete nur der Hirn-Wechsel (agent.py) setup-bewusst; die allgemeine ctx-Vergabe nahm den Gesamt-RAM in Isolation. Dieses Modul vereint beides. """ import re import psutil from services.fit import ( QUANT_BYTES_PER_PARAM, estimate_memory_gb, extract_params_b, max_ctx_in_budget, ) HEADROOM_GB = 4.0 # OS/Treiber/Fragmentierung def gtt_budget_gb() -> float: """GPU-adressierbarer Speicher (GTT) in GB — die harte Obergrenze. Liest amdgpu.gttsize aus /proc/cmdline, sonst RAM minus OS-Reserve.""" try: with open("/proc/cmdline") as f: m = re.search(r"amdgpu\.gttsize=(\d+)", f.read()) if m: return round(int(m.group(1)) / 1024.0, 1) except Exception: pass return round(psutil.virtual_memory().total / (1024 ** 3) - 6.0, 1) def params_of_model(model: dict) -> float: """Robuste Params (Mrd.) eines INSTALLIERTEN Modells: MAXIMUM aus Caps-Schätzung und Dateigröße. Deckt 'Coder-Next' ohne Größe im Namen (→ aus Datei) und Split-GGUFs (size_bytes = nur erster Teil → ignoriert) ab.""" caps = model.get("capabilities") or {} quant = model.get("quant") or "Q4_K_M" bpp = QUANT_BYTES_PER_PARAM.get(quant.upper(), 0.55) size_gb = (model.get("size_bytes") or 0) / (1024 ** 3) pb_size = (size_gb / bpp) if size_gb > 1.0 else 0.0 return max(float(caps.get("params_b") or 0), pb_size, 7.0) def footprint_gb(model: dict) -> float: """Loaded-Footprint eines Modells = Gewichte + kalibrierter KV-Anteil (bei seinem aktuellen ctx).""" quant = model.get("quant") or "Q4_K_M" ctx = int(model.get("ctx") or 32768) bpp = QUANT_BYTES_PER_PARAM.get(quant.upper(), 0.55) size_gb = (model.get("size_bytes") or 0) / (1024 ** 3) pb = params_of_model(model) weights = max(pb * bpp, size_gb) kv = estimate_memory_gb(pb, quant, ctx) - pb * bpp return weights + max(kv, 0.0) def params_b_for(name: str) -> float: """Parameter (Mrd.) für einen Modell-/Repo-Namen: KATALOG (echte Metadaten) zuerst, sonst Namens-Schätzung. Gemeinsam für Fit-Vorschau und ctx-Vergabe.""" from services import catalog meta = catalog.meta_for_name(name) if name else None if meta and meta.get("total_params_b"): return float(meta["total_params_b"]) return extract_params_b(name) def _coresident_members(groups: dict) -> set: """Modelle, die GLEICHZEITIG warm sind: Mitglieder aller `swap:false`-Gruppen (Ko-Residenz, z.B. brains = Hirn+embed+vision). Seit dem `persistent`-Fix (03.07.2026) überlebt die Gruppe auch on-demand-Last: Coder/heavy laden DANEBEN, nicht an ihre Stelle (live verifiziert: Coder + Qwen3.6 gleichzeitig `ready`). Die frühere Beobachtung „heavy verdrängt die Gruppe" war der ignorierte `persist`-Key.""" out: set = set() for g in (groups or {}).values(): if isinstance(g, dict) and g.get("swap") is False: out.update(g.get("members") or []) return out def reserved_gb(role: str | None) -> dict: """Speicher, der NEBEN einem Zielmodell der gegebenen Rolle resident bleibt — gemäß der seit dem `persistent`-Fix (03.07.2026) geltenden Semantik: die ko-residente `swap:false`-Gruppe (brains) bleibt IMMER geladen, on-demand-Modelle laden daneben. - Modell IN der Ko-Residenz-Gruppe (Hirn/embed/vision): koexistiert mit den ÜBRIGEN Gruppen-Mitgliedern → reserviert deren Summe. - Modell AUSSERHALB (heavy/coder/coder-lite/scout): lädt NEBEN die Gruppe → reserviert deren GESAMTE Summe (früher 0.0, weil der kaputte `persist`-Key die Gruppe verdrängen ließ — diese Rechnung erlaubte zu große Kontexte). """ from services import llamaswap models = llamaswap.list_models() groups = llamaswap.list_groups() cores = _coresident_members(groups) brain = next((m for m in models if (m.get("role") == "hermes")), None) brain_gb = footprint_gb(brain) if brain else 0.0 role = (role or "").strip().lower() holder = next((m for m in models if (m.get("role") == role)), None) if role else None holder_name = holder["name"] if holder else None # Hirn (hermes) ist per Definition Teil der Ko-Residenz-Gruppe; sonst Gruppen-Mitgliedschaft prüfen. in_group = role == "hermes" or bool(holder_name and holder_name in cores) if in_group: others = sum(footprint_gb(m) for m in models if m["name"] in cores and m["name"] != holder_name) return {"reserved_gb": others, "mode": "co-resident", "brain_gb": brain_gb} # on-demand: lädt neben die (persistente) Ko-Residenz-Gruppe → deren Summe reservieren. warm = sum(footprint_gb(m) for m in models if m["name"] in cores) return {"reserved_gb": warm, "mode": "ondemand-beside-warmset", "brain_gb": brain_gb} def setup_aware_ctx(params_b: float, quant: str, role: str | None = None) -> dict: """Größter Kontext, der für ein Modell (params_b/quant) der gegebenen Rolle NEBEN dem bestehenden Setup passt. Gibt ctx + die Budget-Herleitung zurück (für UI/Transparenz).""" gtt = gtt_budget_gb() r = reserved_gb(role) budget = max(gtt - r["reserved_gb"] - HEADROOM_GB, 0.0) ctx = max_ctx_in_budget(params_b, quant, budget) return { "ctx": ctx, "gtt_gb": gtt, "reserved_gb": round(r["reserved_gb"], 1), "budget_gb": round(budget, 1), "mode": r["mode"], } def setup_aware_ctx_for_model(model: dict) -> dict: """Setup-bewusster Optimal-ctx für ein INSTALLIERTES Modell (aus seiner Rolle, Params & Quant). Für den 'Auto'-Button an der Modellkarte.""" return setup_aware_ctx(params_of_model(model), model.get("quant") or "Q4_K_M", role=model.get("role"))