Feat: MTP-Speculative-Decoding-Support + Warm-Set-/Budget-Korrektur (Strix-Halo-Optimierung)

Backend:
- MTP-Drafter-Support (Multi-Token-Prediction): erkennt MTP-Köpfe neben dem Modell
  (mtp-*.gguf / *-assistant, arch gemma4-assistant), schreibt
  --model-draft … --spec-type draft-mtp --spec-draft-n-max statt draft-simple.
  _parse_model erkennt --model-draft/-md; drafts_for/set_spec_draft/find_compatible_draft
  MTP-bewusst. Backward-kompatibel (klassische Drafts unverändert). (config.SPEC_DRAFT_N_MAX)
- register_model: cache-reuse/-cram als Default (Drift-Fix — neue Installs wie der
  hand-getunte Box-Stand), --parallel 2 nur noch für coder (kein ctx-Halbierungs-Footgun),
  Spec-Auto-Attach für alle Rollen self-guarding.
- budget.reserved_gb auf die VERIFIZIERTE llama-swap-Gruppen-Swap-Semantik angeglichen:
  on-demand-Modelle verdrängen die brains-Gruppe und laufen allein (reservieren 0, voller
  GTT); brains-Member reservieren nur die übrigen Member. Tote _persist_members entfernt.

Frontend:
- SpecDraftModal zeigt MTP-Drafter mit MTP-Badge (DraftInfo.mtp).

Docs:
- docs/OPTIMIZATION_PLAN.md: vollständiges Audit + Umsetzungs-Log (W1/W2 Warm-Set,
  gemma ctx/fa/cache-reuse/MTP 52→70,8 t/s, fast --parallel 1, scout=GLM-4.6V-Flash),
  alles live gegen die Box verifiziert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-30 18:13:07 +02:00
parent dd99401f3e
commit f655f09ce1
6 changed files with 418 additions and 46 deletions
+24 -22
View File
@@ -75,46 +75,48 @@ def params_b_for(name: str) -> float:
return extract_params_b(name)
def _persist_members(groups: dict) -> set:
def _coresident_members(groups: dict) -> set:
"""Modelle, die GLEICHZEITIG warm sind: Mitglieder aller `swap:false`-Gruppen
(Ko-Residenz, z.B. brains = Hirn+embed+vision). Ein Modell AUSSERHALB dieser Gruppen
ist on-demand und verdrängt beim Laden die GANZE Gruppe — llama-swap swappt Gruppen
(live verifiziert: heavy laden → brains-Gruppe komplett raus, heavy läuft allein)."""
out: set = set()
for g in (groups or {}).values():
if isinstance(g, dict) and g.get("persist"):
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 bleiben muss —
gemäß verifizierter Box-Residenz. Liest die aktuelle llama-swap-Config, passt sich
also der echten Gruppen-/persist-Konfiguration an (nicht hartkodiert).
"""Speicher, der NEBEN einem Zielmodell der gegebenen Rolle resident bleibt — gemäß der
VERIFIZIERTEN llama-swap-Gruppen-Swap-Semantik (NICHT der früheren Annahme „Hirn bleibt
immer"). Nur die ko-residente `swap:false`-Gruppe läuft gemeinsam; ein on-demand-Modell
verdrängt die Gruppe und läuft ALLEIN mit dem vollen GTT.
- Rolle `hermes` (das Hirn): muss mit dem GRÖSSTEN on-demand-Modell koexistieren.
- Rolle, deren aktueller Träger im warmen (persist) Set liegt (z.B. fast/vision):
koexistiert mit Hirn + den ÜBRIGEN warmen Mitgliedern.
- sonst (heavy/coder/scout/keine): on-demand → verdrängt fast/vision, nur das Hirn bleibt.
- Modell IN der Ko-Residenz-Gruppe (Hirn/embed/vision): koexistiert mit den ÜBRIGEN
Gruppen-Mitgliedern → reserviert deren Summe.
- Modell AUSSERHALB (heavy/coder/coder-lite/fast/scout): läuft allein (Gruppe wird beim
Laden rausgeswappt) → reserviert NICHTS, darf den vollen GTT für Kontext nutzen.
"""
from services import llamaswap
models = llamaswap.list_models()
groups = llamaswap.list_groups()
persist = _persist_members(groups)
cores = _coresident_members(groups)
brain = next((m for m in models if (m.get("role") == "hermes")), None)
brain_name = brain["name"] if brain else None
brain_gb = footprint_gb(brain) if brain else 0.0
role = (role or "").strip().lower()
if role == "hermes":
reserved = max((footprint_gb(m) for m in models if m["name"] not in persist),
default=0.0)
return {"reserved_gb": reserved, "mode": "brain", "brain_gb": brain_gb}
holder = next((m for m in models if (m.get("role") == role)), None) if role else None
warm = bool(holder and holder["name"] in persist)
if warm:
others = sum(footprint_gb(m) for m in models
if m["name"] in persist and m["name"] not in {brain_name, holder["name"]})
return {"reserved_gb": brain_gb + others, "mode": "warm", "brain_gb": brain_gb}
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)
return {"reserved_gb": brain_gb, "mode": "ondemand", "brain_gb": brain_gb}
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: verdrängt die Ko-Residenz-Gruppe → läuft allein, voller GTT für Kontext.
return {"reserved_gb": 0.0, "mode": "ondemand-alone", "brain_gb": brain_gb}
def setup_aware_ctx(params_b: float, quant: str, role: str | None = None) -> dict: