a2091b4f5f
Live gefunden (Zed-Start warf Lucys Hirn raus): llama-swap ignoriert unbekannte Group-Keys stillschweigend — der Verdrängungsschutz der brains-Gruppe war seit jeher wirkungslos. set_group() schreibt jetzt beide Keys (persistent für llama-swap, persist für MC2-API/UI), budget.py rechnet die echte Ko-Residenz (on-demand reserviert das Warm-Set statt 0), Warn-Texte beschreiben Überlauf statt Verdrängung. Box-Config live gefixt + verifiziert: Coder und Qwen3.6 gleichzeitig ready. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
148 lines
6.4 KiB
Python
148 lines
6.4 KiB
Python
"""
|
|
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"))
|