From f655f09ce1f537666111a0e437436876d9620976 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Tue, 30 Jun 2026 18:13:07 +0200 Subject: [PATCH] Feat: MTP-Speculative-Decoding-Support + Warm-Set-/Budget-Korrektur (Strix-Halo-Optimierung) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/config.py | 5 + backend/services/budget.py | 46 +-- backend/services/llamaswap.py | 102 ++++-- docs/OPTIMIZATION_PLAN.md | 298 ++++++++++++++++++ .../src/components/models/SpecDraftModal.tsx | 12 +- frontend/src/lib/api.ts | 1 + 6 files changed, 418 insertions(+), 46 deletions(-) create mode 100644 docs/OPTIMIZATION_PLAN.md diff --git a/backend/config.py b/backend/config.py index 1babc44..051092e 100644 --- a/backend/config.py +++ b/backend/config.py @@ -49,6 +49,11 @@ SPEC_DRAFT_MODEL_PATH = os.environ.get("MC_SPEC_DRAFT_MODEL", "") # Speculative-Decoding-Typ (llama.cpp dieser Generation braucht --spec-type zusätzlich # zu --spec-draft-model, sonst ist Spec inaktiv). SPEC_TYPE = os.environ.get("MC_SPEC_TYPE", "draft-simple") +# MTP-Speculative-Decoding (Multi-Token-Prediction): manche Modelle bringen einen eigenen +# MTP-Kopf mit (z.B. gemma-4 → arch 'gemma4-assistant', Datei 'mtp-*.gguf'). Der wird mit +# `--model-draft --spec-type draft-mtp --spec-draft-n-max N` geladen (NICHT +# --spec-draft-model/draft-simple). 1,5–2× Durchsatz bei null Qualitätsverlust. +SPEC_DRAFT_N_MAX = int(os.environ.get("MC_SPEC_DRAFT_N_MAX", "4")) # Env für HuggingFace-Downloads: XET deaktivieren (Hänger bei ~6 MB, siehe v1-Gotcha). HF_DOWNLOAD_ENV = {"HF_HUB_DISABLE_XET": "1"} diff --git a/backend/services/budget.py b/backend/services/budget.py index 874a4ff..be45e54 100644 --- a/backend/services/budget.py +++ b/backend/services/budget.py @@ -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: diff --git a/backend/services/llamaswap.py b/backend/services/llamaswap.py index 48e641a..dea4396 100644 --- a/backend/services/llamaswap.py +++ b/backend/services/llamaswap.py @@ -15,7 +15,7 @@ from ruamel.yaml.scalarstring import LiteralScalarString from config import ( CMD_TEMPLATE, CONFIG_PATH, DEFAULT_TTL, DRAFTS_DIR, LLAMA_SWAP_URL, - SPEC_DRAFT_MODEL_PATH, SPEC_TYPE, + SPEC_DRAFT_MODEL_PATH, SPEC_DRAFT_N_MAX, SPEC_TYPE, ) log = logging.getLogger(__name__) @@ -84,15 +84,15 @@ def _parse_model(name: str, spec: dict) -> dict: role = aliases[0].lower() if aliases else (name.lower() if name.lower() in ROLE_IDS else None) prompt_cache = "--prompt-cache " in cmd or cmd.endswith("--prompt-cache") or "--prompt-cache-all" in cmd + # Draft-Modell erkennen — klassisch (--spec-draft-model) ODER MTP (--model-draft / -md). spec_draft = None - if "--spec-draft-model" in cmd: - m_draft = re.search(r"--spec-draft-model\s+([^\s]+)", cmd) - if m_draft: - spec_draft = os.path.basename(m_draft.group(1).replace("'", "").replace('"', "")) + if (m_draft := re.search(r"--(?:spec-draft-model|model-draft)\s+([^\s]+)", cmd)) \ + or (m_draft := re.search(r"(? list[dict]: return out +def _is_mtp_draft(draft_path: str) -> bool: + """Ist dieser Draft ein MTP-Kopf (Multi-Token-Prediction) statt eines klassischen + Draft-Modells? MTP-Köpfe (z.B. gemma-4) laden mit `--model-draft … --spec-type + draft-mtp` statt `--spec-draft-model … --spec-type draft-simple`. Erkennung am Arch + ('…-assistant' / 'mtp') oder Dateinamen ('mtp-*', '*-MTP', '*-assistant').""" + base = os.path.basename(draft_path).lower() + if base.startswith("mtp-") or "-mtp" in base or "assistant" in base: + return True + from services import gguf_meta + arch = ((gguf_meta.fingerprint(draft_path) or {}).get("arch") or "").lower() + return arch.endswith("-assistant") or "mtp" in arch + + +def _spec_flags_for_draft(draft_path: str) -> str: + """Korrekte llama-server-Spec-Flags für einen (vocab-kompatiblen) Draft. MTP-Kopf → + `--model-draft … --spec-type draft-mtp --spec-draft-n-max N`; klassischer Draft → + `--spec-draft-model … --spec-type draft-simple`. (Beides nötig, sonst Spec inaktiv.)""" + if _is_mtp_draft(draft_path): + return (f" --model-draft {draft_path} --spec-type draft-mtp" + f" --spec-draft-n-max {SPEC_DRAFT_N_MAX}") + return f" --spec-draft-model {draft_path} --spec-type {SPEC_TYPE}" + + +def _sibling_mtp_drafters(target_path: str) -> list[str]: + """MTP-Kopf-GGUFs NEBEN dem Zielmodell (gleicher Ordner): 'mtp-*.gguf', '*-MTP.gguf', + '*-assistant*.gguf'. Per Konstruktion vocab-identisch zum Modell → idealer Draft.""" + out: list[str] = [] + d = os.path.dirname(target_path) + if os.path.isdir(d): + for f in sorted(os.listdir(d)): + fl = f.lower() + if fl.endswith(".gguf") and (fl.startswith("mtp-") or "-mtp" in fl or "assistant" in fl): + p = os.path.join(d, f) + if p != target_path: + out.append(p) + return out + + def find_compatible_draft(target_path: str) -> str | None: """Pfad eines vocab-kompatiblen Drafts für target_path, oder None. - Bevorzugt MC_SPEC_DRAFT_MODEL (falls gesetzt+kompatibel), sonst der erste - kompatible Draft in DRAFTS_DIR. None auch, wenn target (noch) fehlt (dann - nicht verifizierbar → bewusst KEIN Draft anhängen).""" + Bevorzugt einen MTP-Kopf NEBEN dem Modell (höchste Qualität, by-construction), + dann MC_SPEC_DRAFT_MODEL (falls gesetzt+kompatibel), sonst der erste kompatible + Draft in DRAFTS_DIR. None auch, wenn target (noch) fehlt (nicht verifizierbar → + bewusst KEIN Draft anhängen).""" if not target_path or not os.path.exists(target_path): return None from services import gguf_meta - candidates: list[str] = [] + candidates: list[str] = list(_sibling_mtp_drafters(target_path)) if SPEC_DRAFT_MODEL_PATH and os.path.exists(SPEC_DRAFT_MODEL_PATH): candidates.append(SPEC_DRAFT_MODEL_PATH) for d in list_drafts(): @@ -272,19 +318,27 @@ def find_compatible_draft(target_path: str) -> str | None: def spec_draft_flags(target_path: str) -> str: """llama-server-Flags für Speculative Decoding (Draft + --spec-type), oder '' - wenn kein kompatibler Draft existiert. Beides nötig, sonst ist Spec inaktiv.""" + wenn kein kompatibler Draft existiert. MTP-bewusst (s. _spec_flags_for_draft).""" d = find_compatible_draft(target_path) - return f" --spec-draft-model {d} --spec-type {SPEC_TYPE}" if d else "" + return _spec_flags_for_draft(d) if d else "" def drafts_for(target_path: str) -> dict: - """Für die UI: alle Drafts + ihre Kompatibilität zum Ziel-Modell. + """Für die UI: alle Drafts + ihre Kompatibilität zum Ziel-Modell. Schließt MTP-Köpfe + NEBEN dem Zielmodell ein (DRAFTS_DIR kennt sie nicht). `mtp:true` markiert MTP-Drafts. compatible=None heißt 'nicht prüfbar' (Ziel- oder Draft-GGUF fehlt).""" from services import gguf_meta exists = bool(target_path and os.path.exists(target_path)) drafts = list_drafts() + seen = {d["path"] for d in drafts} + for p in _sibling_mtp_drafters(target_path): + if p not in seen: + drafts.append({"path": p, "filename": os.path.basename(p), + "size_bytes": os.path.getsize(p) if os.path.exists(p) else None, + "vocab": gguf_meta.fingerprint(p)}) for d in drafts: d["compatible"] = gguf_meta.compatible(target_path, d["path"]) if exists else None + d["mtp"] = _is_mtp_draft(d["path"]) return { "target_path": target_path, "target_exists": exists, @@ -302,9 +356,11 @@ def set_spec_draft(model_id: str, draft_path: str | None) -> dict: if not isinstance(spec, dict): return {"ok": False, "reason": "Modell nicht gefunden"} cmd = str(spec.get("cmd", "")) - # vorhandene Spec-Flags entfernen (idempotent) - cmd = re.sub(r"\s+--spec-draft-model\s+\S+", "", cmd) + # vorhandene Spec-Flags entfernen (idempotent) — klassisch UND MTP. + cmd = re.sub(r"\s+--(?:spec-draft-model|model-draft)\s+\S+", "", cmd) + cmd = re.sub(r"\s+-md\s+\S+", "", cmd) cmd = re.sub(r"\s+--spec-type\s+\S+", "", cmd) + cmd = re.sub(r"\s+--spec-draft-n-(?:max|min)\s+\S+", "", cmd) if draft_path: # relative Angabe (nur Dateiname) gegen DRAFTS_DIR auflösen @@ -323,7 +379,7 @@ def set_spec_draft(model_id: str, draft_path: str | None) -> dict: if comp is False else "Kompatibilität nicht prüfbar (Modell-GGUF fehlt) — Draft nicht gesetzt.") return {"ok": False, "reason": reason} - cmd = cmd.rstrip() + f" --spec-draft-model {draft_path} --spec-type {SPEC_TYPE}" + cmd = cmd.rstrip() + _spec_flags_for_draft(draft_path) spec["cmd"] = LiteralScalarString(cmd.rstrip() + "\n") write_config(cfg) diff --git a/docs/OPTIMIZATION_PLAN.md b/docs/OPTIMIZATION_PLAN.md new file mode 100644 index 0000000..5e94869 --- /dev/null +++ b/docs/OPTIMIZATION_PLAN.md @@ -0,0 +1,298 @@ +# Optimierungsplan MC2 — Rollen, Warm-Set & Durchsatz (Strix Halo) + +> Audit-Ergebnis + Maßnahmenplan. **Erst Plan, dann Umsetzung nach Freigabe.** Alles reversibel +> (Config-Backups vor jeder Box-Änderung). Stand: 2026-06-30. +> Quellen: Code-Audit (`backend/`, `frontend/`) + **Live-Box-Verifikation per SSH** (`/running`, voller +> `config.yaml`, `free`, `du`, t/s-Probe) + `docs/AUDIT_KICKOFF.md` / `CLAUDE_CODE_BRIEF_lucy-brain-role.md`. + +--- + +## 0. TL;DR (Priorität nach echtem Impact) + +1. **✅ ERLEDIGT — Warm-Set korrigiert (W1/W2, 2026-06-30, live verifiziert):** `brains` jetzt = **gemma + + embedding + vision** (`swap:false, persist:true`, ~31 GB). **Wichtige Korrektur durch Live-Test:** das + ursprünglich vermutete OOM-Risiko existiert NICHT — llama-swap **swappt ganze Gruppen** statt sie zu + ko-laden (cross-group Load = Gruppe raus, neues Modell rein; kein Speicher-Überlauf). Der echte Punkt war + ein anderer: vorher pinnte `brains` auch `fast` (Chat-Lane-Hirn, kein Nutzen für Lucy → ~28 GB verschenkt), + und in der reinen `gemma+embed`-Variante hätte **Lucys Sehen ihr Hirn verdrängt**. Jetzt halten Hirn + + Gedächtnis + Sehen zusammen warm (verifiziert: gemma & vision ko-resident, 37 GB, kein Evict). **Reversibel.** +2. **MTP-Spec-Decoding für gemma:** 53 t/s → erwartet ~80–100 t/s (1,5–2×, 0 Qualitätsverlust). Drafter-GGUF + fehlt auf der Box **und MC2 kennt MTP-Drafter gar nicht** (nur klassisches `draft-simple`). **[mittel]** +3. **coder-Spec-Draft verifizieren:** `coder` (Qwen3-Coder-Next) hat `--spec-draft-model Qwen3-0.6B-Q8_0 + --spec-type draft-simple` **aktiv** — ist dieser Draft wirklich vocab-kompatibel zu Qwen3-Coder-Next? + Wenn nein, bremst/bricht Spec still. **[Check]** +4. **Neues `scout`-Modell** (multimodaler Allrounder, Tools): HF-verifizierter Primärpick **GLM-4.6V-Flash (9B)** + — Q4 ~6 GB + mmproj, natives Tool-Calling, schlägt unser vision-Modell. Alt: Gemma-4-12B. (Qwen3.5-VL-MoE + verworfen — kein gepflegtes GGUF.) **[Recherche/Download]** +5. **gemma-ctx 128k → 64k:** *kleiner* Hebel (real nur ~2 GB, weil Gemmas KV winzig ist — s. §1.4), + trotzdem sinnvoll (freigegeben). Kein Funktions-Risiko für agentische Turns. **[risikoarm]** + +--- + +## 1. IST-Zustand (live verifiziert per SSH, 2026-06-30) + +### 1.1 Rollen-Taxonomie — Code ist konsistent ✅ +Der Brain-Rolle-Umbau aus `CLAUDE_CODE_BRIEF_lucy-brain-role.md` ist **bereits umgesetzt** (Commit `dd99401`). +`hermes` (= Lucys Hirn) ist als kanonische Rolle in ALLEN Quellen synchron: `llamaswap.ROLE_IDS`, +`sources.ROLE_IDS`+`CATEGORIES`, `roles.py` (`_capability_suit`/`_pref`/`_reason`), Frontend `ModelBadges.ROLES`. +Die „hermes-Altlast/Inkonsistenz" aus dem Brief existiert nicht mehr. + +### 1.2 Brain-Warm-Flow — Code ist fertig ✅, Box bestätigt ✅ +`POST /api/models/{id}/role role=hermes` → `agent.set_agent_brain()`: Alias + `brains:{swap:false,persist:true}` ++ ttl 0 (altes Hirn raus/entspannt) + Hermes `model.default` + Gateway-Restart + Budget-Warnung. `brain_status()` +prüft per `/running`. +**Box bestätigt das Akzeptanzkriterium:** `gemma-4-26B-A4B-it` ist `ready`, `ttl:0`, Alias `hermes`, und in +`groups.brains` (`swap:false, persist:true`). **Lucy bleibt warm — kein Kalt-Nachladen.** ✅ + +### 1.3 Live-Config aller Rollen (`/etc/llama-swap/config.yaml`, `globalTTL: 0`) + +| Rolle (alias) | Modell | Gewichte (du) | ctx | ttl | Besonderheiten | +|---|---|---|---|---|---| +| fast | Qwen3.6-35B-A3B | 22 GB | 65536 | 0 | `--parallel 2`, mmproj, `--cache-reuse 256 -cram 16384`, **kein Spec**, **in `brains`** | +| heavy | Qwen3.5-122B-A10B | 73 GB | 32768 | 600 | split-GGUF, `--cache-reuse 256 -cram 16384`, kein Spec | +| coder | Qwen3-Coder-Next | 46 GB | 131072 | 600 | `--parallel 2`, **Spec aktiv** (Qwen3-0.6B-Q8 / draft-simple) | +| coder-lite | Qwen3-Coder-30B-A3B | 18 GB | 131072 | 300 | `--cache-reuse 256 -cram 16384` | +| vision | Qwen3-VL-8B-Instruct | 6 GB | 32768 | 300 | mmproj, **in `brains` (gepinnt!)** | +| embed | Qwen3-Embedding-0.6B | 1 GB | 8192 | 0 | `--embedding --pooling last`, **in `brains`** | +| **hermes** (Hirn) | **gemma-4-26B-A4B-it** | 17 GB (+1,2 mmproj) | **131072** | 0 | mmproj, `--jinja`, **kein Spec**, **in `brains`** | + +`groups.brains = {swap:false, persist:true, members:[embedding, vision, fast, gemma]}`. + +### 1.4 Speicher-Realität (GTT = 124 GB, `amdgpu.gttsize=126976`) +**Wichtige Korrektur ggü. der reinen `fit.py`-Schätzung:** mit NUR gemma@131072 geladen meldet die Box +**25,9 GB used** → gemma resident ≈ **~22 GB** (≈17 Gewichte + 1,2 mmproj + nur **~4 GB KV**). Gemma-4 nutzt +starke Sliding-Window-Attention → KV ist viel kleiner, als `fit.py` (kalibriert an Hermes-14B) vorhersagt. +**Folge:** gemma-ctx-Reduktion spart real nur ~2 GB; der echte Druck kommt vom **Warm-Set**, nicht vom Hirn-ctx. + +**llama-swap-Gruppen-Semantik (live verifiziert, korrigiert frühere Annahme):** Eine `swap:false`-Gruppe hält +NUR ihre EIGENEN Member ko-resident. Ein Modell in einer ANDEREN Gruppe (heavy/coder = je eigene implizite +Gruppe) zu laden **swappt die gesamte aktive Gruppe raus** und lädt das neue Modell allein. `persist:true` +verhindert nur das Idle-TTL-Entladen, NICHT das Gruppen-Swapping. **Folge:** es gibt **kein** OOM durch +Ko-Laden (heavy lädt allein, 79 GB < 124 ✓) — aber das Hirn ist bei cross-group Loads (heavy/coder) **nicht** +geschützt: es wird mitgeswappt und lädt danach kalt nach (~Sekunden). Beweis: heavy laden → `/running` zeigte +nur heavy (78,7 GB), gemma weg trotz persist. + +**Konsequenz fürs Design (umgesetzt):** Lucys zusammengehöriges Set (Hirn + Gedächtnis + Sehen) MUSS in DERSELBEN +`swap:false`-Gruppe stehen, sonst verdrängt schon Lucys eigenes Sehen ihr Hirn. → `brains = gemma + embed + +vision` (~31 GB). heavy/coder/coder-lite/fast = on-demand (swappen die brains-Gruppe beim Laden raus — ok, das +sind separate Aktivitäten IDE/Delegation; Lucy lädt danach kurz nach). + +### 1.5 Durchsatz (live gemessen) +gemma (Hirn): **~53 t/s** Generierung, ~173 t/s Prompt (warm, kurzer Prompt). Solide Basis für ein 26B-A4B +(4B aktiv) auf der bandbreitenlimitierten APU; MTP-Ziel ~80–100 t/s. + +--- + +## 2. Verifikations-Status (war §2 „offen" — jetzt aufgelöst) +- **V1 (gemma in `brains:{swap:false}`?)** → **JA** ✅ (Akzeptanzkriterium erfüllt). +- **V2 (t/s)** → gemma 53 t/s gemessen ✅. fast/heavy/coder noch nicht gemessen (Laden würde Warm-Set stören). +- **V3 (Voll-Config)** → komplett gelesen (§1.3) ✅. +- **V4 (GTT/Belegung)** → GTT 124 GB, gemma-only 25,9 GB used ✅. Cross-group-Load swappt Gruppen (kein OOM, + aber Hirn ungeschützt) — live verifiziert (§1.4). Behoben durch W1/W2. + +- **V5 (coder-Spec-Draft)** → **AUFGELÖST ✅**: Qwen3-0.6B-Q8 ↔ Qwen3-Coder-Next = gleicher `tokens_sha + facf459…`, n_vocab 151936, `compatible: True`. coder-Spec ist gültig & aktiv. (Bonus-Opportunität §9.2 F4.) + +--- + +## 3. Maßnahmen je Rolle + +### 3.1 ✅ Warm-Set / KO-Residenz — ERLEDIGT (2026-06-30, live verifiziert) +| # | Maßnahme | Status / Begründung | +|---|---|---| +| W1/W2 | **`brains:{swap:false,persist:true}` = gemma + embedding + vision** (~31 GB); fast raus (Chat-Lane-Hirn, kein Lucy-Nutzen, ~28 GB gespart) | ✅ angewendet via `PUT /api/groups` (Backup `config.yaml.bak-20260630-152911`). **Verifiziert:** gemma & vision ko-resident, kein Evict (37 GB used); gemma bleibt `ready`. | +| — | heavy/coder/coder-lite/fast = on-demand | Korrekt: kein OOM (Gruppen-Swap), Lucys Set bleibt zusammen warm. Trade-off: nach heavy-Delegation / IDE-Coding lädt Lucy kurz nach (~s). | +| Optional | fast wieder pinnen, falls Chat-Lane dauerwarm sein soll | +28 GB Pin, swappt aber bei jedem heavy/coder-Load eh raus → geringer Nutzen. Auf Wunsch nachrüstbar. | + +### 3.2 `hermes` / Lucys Hirn — gemma-4-26B-A4B-it +| # | Maßnahme | Begründung / Zahlen | Risiko | Revert | +|---|---|---|---|---| +| H1 | **ctx 131072 → 65536** | Spart real nur ~2 GB (Gemma-KV winzig), aber konsistent mit `fast` ([[hermes-fast-context-blocker]]) und Agent-Turns brauchen selten >64k. | niedrig | ctx zurück | +| H2 | **MTP-Spec-Decoding** (s. §4) | 53 → ~80–100 t/s. Drafter ~0,4 B → Budget vernachlässigbar. | mittel | Spec-Flags raus | +| H3 | gemma sollte ggf. `--cache-reuse 256` bekommen (wie fast/heavy/coder) — fehlt aktuell | KV-Reuse über Turns → weniger Prompt-Reprocessing bei Agent-Ketten. | niedrig | Flag raus | + +### 3.3 `fast` — Qwen3.6-35B-A3B (chat-Lane) +- ctx 65536, `--parallel 2`, **kein Spec** (gut — vermeidet den qwen2.5-Vocab-Bruch). **Belassen**, aber W2 (raus + aus dem harten Pin). Optional Qwen-Spec-Draft nur, wenn vocab-kompatibel zu Qwen3.6 (Check via `gguf_meta`). + +### 3.4 `heavy` — Qwen3.5-122B-A10B +- 73 GB, ctx 32768, ttl 600, on-demand. **Belassen.** Profitiert direkt von W1/W2 (kann wieder laden). Kein Spec + (kein etablierter kompatibler Qwen3.5-Draft) — erst prüfen, sonst lassen. + +### 3.5 `coder` / `coder-lite` +- **coder** (Qwen3-Coder-Next, 46 GB, ctx 131072): **Spec aktiv mit Qwen3-0.6B-Q8** → **V5: Vocab-Kompatibilität + verifizieren.** Falls inkompatibel → Draft entfernen (still kein Speed-Gewinn, evtl. Fehlerquelle). +- **coder-lite** (Qwen3-Coder-30B-A3B, 18 GB, ctx 131072): coding-Lane-Default, schnell (3B aktiv). **Belassen.** + Hinweis: Alias heißt `coder-lite` (Bindestrich) — die Lanes referenzieren `coder_lite`; sicherstellen, dass das + Routing (`router_logic.py`/`routing_policy.py`) auf den richtigen Alias zeigt (Konsistenz-Check, kein Box-Risiko). + +### 3.6 `vision` — Qwen3-VL-8B-Instruct +- 6 GB, ctx 32768. **Belassen**, aber W2 (verdrängbar statt hart gepinnt). Sehen ist bursty → on-demand/warm-soft reicht. + +### 3.7 `embed` — Qwen3-Embedding-0.6B +- 1 GB, ttl 0, **bleibt hart gepinnt** (W1). Unantastbar — Mem0/Gedächtnis hängt dran. + +### 3.8 `scout` — VAKANT → §5 + +--- + +## 4. MTP-Speculative-Decoding für gemma (Backend+UI-Erweiterung) +**Problem:** MC2 kennt nur **klassische** Drafts. `spec_draft_flags()` schreibt `--spec-draft-model +--spec-type draft-simple`; `find_compatible_draft()` scannt nur `DRAFTS_DIR` mit stumpfem +`gguf_meta.compatible()`. Ein **MTP-Kopf** (`gemma-4-26B-A4B-it-assistant`, Arch `gemma4_assistant`) wird nie +angeboten. Engine 9843 kann `--spec-type draft-mtp` ✓; alte `--draft-max/-min` sind **entfernt** → +`--spec-draft-n-max/-n-min`. + +**Schritte:** +1. **Drafter beschaffen** (Box, fehlt noch) — ⚠️ **Brief-Korrektur (HF-verifiziert 2026-06-30):** der Drafter + heißt NICHT `…-assistant`. In `unsloth/gemma-4-26B-A4B-it-GGUF` liegt er als **`mtp-gemma-4-26B-A4B-it.gguf`** + (Repo-Root, **0,46 GB**) bzw. **`MTP/gemma-4-26B-A4B-it-Q8_0-MTP.gguf`** (0,46 GB; BF16/F16-Varianten 0,86 GB). + Eine Datei nach `/srv/models/gemma-4-26B-A4B-it-GGUF/` laden (Q8-MTP = kleinste, ~0,46 GB). + **Flags (laut unsloth MTP/README):** `--model-draft --spec-type draft-mtp --spec-draft-n-max 4` + (kein `--spec-draft-n-min` nötig; neuere llama.cpp **auto-entdeckt** die Root-`mtp-*.gguf`). `-md` = Kurzform. +2. **Backend (`llamaswap.py`):** MTP-Pfad ergänzen — MTP-Drafter als gültigen, vocab-kompatiblen Draft erkennen + (Arch/Name `*-assistant*`); beim Aktivieren `-md --spec-type draft-mtp + --spec-draft-n-max N --spec-draft-n-min M` schreiben (statt `--spec-draft-model`/`draft-simple`). `_parse_model` + um `draft-mtp`/`-md` erweitern (UI-Status). Flag-Namen mit `llama-server --help` (9843) gegenprüfen. +3. **Frontend:** Draft-Modal MTP-Drafter als empfohlene Option für gemma führen („MTP-Kopf, vocab-kompatibel ✓"). +4. **Verifizieren:** t/s vor/nach (Basis 53 t/s) gegen die Box. + +> Risiko mittel (neue Flag-Logik); voll reversibel. + +--- + +## 5. Neues `scout`-Modell (Empfehlung) +Profil: multimodaler Allrounder, klein/MoE (schnell auf bandbreitenlimitierter Box), Tool-Calling, +KO-Residenz-freundlich, **distinkt** von Hirn (gemma-4) und vision (Qwen3-VL-8B). + +**HF-verifiziert 2026-06-30** (Verfügbarkeit + Dateigrößen real geprüft, nicht geraten): + +| Pick | Modell | Verifizierte Fakten | Hinweis | +|---|---|---|---| +| **Primär** | **GLM-4.6V-Flash (9B)** | GGUF da: `unsloth/GLM-4.6V-Flash-GGUF` (29 Quants, 12k+ dl), `lmstudio-community`, `MaziyarPanahi`. **Q4_K_M = 6,17 GB + mmproj 1,84 GB ≈ 8 GB.** Natives multimodales Function-Calling, 128k ctx. Benchmarks: **schlägt Qwen3-VL-8B** (= unser aktuelles vision-Modell) in fast allen Kategorien. | **9B dense** (nicht MoE), aber bei 9B/6 GB trotzdem schnell. **on-demand** (nicht pinnen). Könnte perspektivisch sogar `vision` ablösen. | +| Alt A | **Gemma-4-12B-it (dense, multimodal)** | GGUF breit verfügbar: `unsloth/gemma-4-12b-it-GGUF` (1,4 Mio dl), `google/…-qat`, `lmstudio`. | dense → auf Bandbreiten-Box etwas langsamer; Familien-Dopplung mit dem Hirn (auch Gemma-4). | +| ~~Alt B~~ | ~~Qwen3.5-VL-MoE~~ | **VERWORFEN:** kein gut gepflegtes GGUF — nur EIN Nischen-Repo (`jc-builds/Qwen3.5-9B-VLM-Q4_K_M`, kein Trusted-Author). Meine frühere „familien-treu"-Empfehlung war ungedeckt. | nicht nehmen. | + +**Vorgehen:** GLM-4.6V-Flash (Q4_K_M ~6 GB + mmproj) via „Modelle finden"/`/api/models/install` laden → Rolle +`scout`, **on-demand** → Mini-Benchmark (t/s + Vision+Tool-Prompt). Quellen (tagesaktuell, 2026-06): +[VentureBeat: GLM-4.6V native tool-calling](https://venturebeat.com/ai/z-ai-debuts-open-source-glm-4-6v-a-native-tool-calling-vision-model-for) · +[zai-org/GLM-4.6V-Flash (HF)](https://huggingface.co/zai-org/GLM-4.6V-Flash) · +[GLM-4.6V Flash 9B Review/Benchmarks](https://binaryverseai.com/glm-4-6v-review-benchmarks-pricing-local-install/) · +[SiliconFlow: schnellste OSS-Multimodal 2026](https://www.siliconflow.com/articles/en/fastest-open-source-multimodal-models). + +--- + +## 6. KO-Residenz-Set (umgesetzt, live verifiziert) +**Lucys Warm-Set (`brains:{swap:false,persist:true}`):** gemma-Hirn + embedding + vision = **~31 GB.** +Diese drei MÜSSEN zusammen in einer Gruppe sein (sonst verdrängt Lucys Sehen ihr eigenes Hirn — Gruppen-Swap). +**Rein on-demand (swappen die brains-Gruppe beim Laden raus):** fast, heavy, coder, coder-lite, scout. + +**Verifiziert:** gemma+vision ko-resident (37 GB, kein Evict); heavy-Load swappt die Gruppe (kein OOM, 79 GB). +**Hinweis `budget.reserved_gb`:** das Modul nimmt an, fast/vision seien verdrängbar persist-Member, die NEBEN +dem Hirn warm bleiben. Real swappt llama-swap aber die ganze Gruppe → die `reserved_gb`-Mathematik ist für +cross-group-Loads **zu konservativ** (rechnet Hirn+Rest gleichzeitig, was nie passiert). **Folge-Task (Code):** +`budget.reserved_gb` an die echte Gruppen-Swap-Semantik angleichen (nur das brains-Set zählt als resident, +on-demand-Modelle laufen allein). Kein Box-Risiko, nur genauere ctx-Empfehlungen. + +--- + +## 7. Reihenfolge / Risiko / Revert +| Schritt | Aktion | Risiko | Revert | +|---|---|---|---| +| 0 | ✅ **Box-Backup** `config.yaml.bak-20260630-152911` | – | – | +| 1 | ✅ **W1+W2** brains = gemma+embed+vision (via `PUT /api/groups`), verifiziert | niedrig | `set_group` zurück | +| 2 | **H1** gemma ctx → 65536; **H3** `--cache-reuse 256` ergänzen | niedrig | ctx/Flag zurück | +| 3 | **V5** coder-Draft-Vocab prüfen; inaktive Spec entfernen | niedrig | – | +| 4 | **§4** MTP: Drafter laden → Backend/UI-Erweiterung → gemma-cmd → t/s-Vergleich | mittel | Spec-Flags raus | +| 5 | **§5** scout recherchieren/installieren/zuweisen (on-demand) | niedrig | Modell löschen | + +--- + +## 8. Offene Entscheidungen (für dich) +1. **Warm-Set-Fix (W1/W2) jetzt umsetzen?** — behebt die heavy-OOM-Gefahr, mein klare Empfehlung als Erstes. +2. **fast/vision verdrängbar** via eigener Gruppe `swap:true` (sie swappen sich gegenseitig) **oder** schlicht + `persist:false`? (Detail des Umbaus.) +3. **MTP jetzt oder als eigener Block?** (Code-Erweiterung Backend+UI + Drafter-Download.) +4. **scout-Pick:** GLM-4.6V (Primär) oder familien-treu Qwen3.5-VL-MoE? +5. gemma-ctx **65536** ist gesetzt (deine Wahl) — ok so. + +--- + +## 9. Gesamtbild: Engine- (llama.cpp) & Router- (llama-swap) Flags — verifiziert gegen Build 9843 +> Der Brief deckte das nicht ab. Alle Aussagen hier gegen `llama-server --help` (9843) + Live-Config geprüft. + +### 9.1 Cross-cutting (alle Chat-Modelle): `-ngl 999 -fa 1 --no-mmap -c --jinja` +- `-ngl 999` ✅ korrekt (volle Offload-Last in GTT auf der APU). +- `-fa 1` → funktioniert, ist aber **Legacy-Form**. Aktuell: `-fa [on|off|auto]` (Default `auto`). Auf `-fa on` + normalisieren (oder weglassen → auto), sonst Risiko bei künftigem Build. **[niedrig]** +- `--no-mmap` ✅ bewusst (volle RAM-Residenz statt file-backed Pages; sinnvoll für warm gehaltene Modelle auf + Unified Memory). Behalten. + +### 9.2 Pro-Modell-Befunde & Hebel +| # | Befund | Hebel | Prio | +|---|---|---|---| +| F1 | **gemma (Hirn) fehlt `--cache-reuse 256 -cram 16384`** — der Agent macht die meiste Multi-Turn-/Tool-Arbeit, nutzt aber nur den 8 GB-Default-Cache & **kein KV-Reuse über Turns** | ergänzen → System-Prompt + History-KV werden wiederverwendet, weniger Prompt-Reprocessing, schnellere Folge-Antworten | **hoch (Quick-Win)** | +| F2 | **`--parallel 2` (fast/coder) ↔ Kontext:** unified KV ist „enabled if slots **auto**" — mit explizitem `--parallel 2` evtl. AUS → harte ctx-Teilung (fast 32k/Anfrage, coder 65k/Anfrage). `-cram` SOLL unified erzwingen, Help mehrdeutig | **V6:** `n_ctx_per_seq` aus `/props` verifizieren (beim nächsten Load). Dann: `--parallel 1`/auto (volle ctx, wenn keine Nebenläufigkeit) **oder** explizit `-kvu` setzen | mittel | +| F3 | **KV-Quant `-ctk q8_0 -ctv q8_0` nirgends** — v.a. coder/coder-lite @131072 tragen große KV | für die langen Coding-Kontexte testen → ~halbe KV bei min. Qualitätsverlust (FA-Interaktion prüfen) | mittel | +| F4 | **coder-Spec ✅ kompatibel** (verifiziert). Derselbe Draft (`tokens_sha facf459`) ist evtl. auch zu **heavy (122B-A10B)** & coder-lite kompatibel | Spec-Decoding für **heavy** prüfen → 10B aktiv, dense-artig → spürbarer Durchsatz möglich. Via `/api/models/drafts?target=` verifizieren | mittel | +| F5 | **fast trägt `--mmproj`** (ist vision-fähig) — bewusst? +~1 GB | wenn die Chat-Lane nie Bilder bekommt: mmproj sparen | niedrig | +| F6 | `-b/-ub` (batch/ubatch), `-t` (threads) ungenutzt = Defaults | Prompt-Speed evtl. via `-ub` tunbar — **messen statt raten**, niedrige Prio | niedrig | + +### 9.3 llama-swap (Router-Ebene) +- `globalTTL: 0` ✅ ok (Modelle haben überwiegend explizite ttl). `healthCheckTimeout: 300` ✅ reicht auch für + heavy (~60 s Load gemessen). Gruppen-Swap-Semantik verstanden (§1.4), brains gefixt (§3.1). +- **Systemischer Befund (Code↔Box-Drift):** MC2 `config._DEFAULT_CMD_TEMPLATE` = + `llama-server -m {model} --host … --port ${PORT} -c {ctx} -ngl 999 -fa 1 --no-mmap` — kennt **KEINE** der auf + der Box manuell ergänzten Optimierungen (`--cache-reuse`, `-cram`, `--parallel`, KV-Quant). **Folge: neu über + MC2 installierte Modelle bekommen diese Tunings NICHT automatisch** → die Box driftet von der Code-Quelle weg. + **Fix (Code):** sinnvolle Defaults ins Template / `register_model` (rollenabhängig), damit „Modelle finden" + schon optimiert registriert. Optional: llama-swap `macros` für wiederholte Flag-Blöcke (Wartbarkeit). **[mittel]** + +### 9.4 Verifikationspunkte — aufgelöst (2026-06-30) +- **V6 ✅:** fast-Upstream `/props`: `total_slots:2`, **per-slot n_ctx 32768** → `--parallel 2` teilt den Kontext + HART (kein unified-Sharing trotz `-cram`). **Maßnahme F2 angewendet:** fast → `--parallel 1` = **per-slot + n_ctx 65536** (verifiziert). coder bleibt `--parallel 2` (IDE-Concurrency, 65k/Req). +- **V7 ✅:** Qwen3-0.6B-Draft (`tokens_sha facf459`) → **heavy INKOMPATIBEL** (`a5e1ccff`, kein Spec möglich), + **coder-lite KOMPATIBEL** (gleiche sha; aber MoE 3B-aktiv → Spec-Gewinn gering, vor Aktivierung benchmarken). + +### Umsetzungs-Log (Box, 2026-06-30, Backups vorhanden) +- ✅ **W1/W2** brains = gemma+embed+vision (`config.yaml.bak-20260630-152911`). +- ✅ **gemma F1+H1+fa-on**: ctx 131072→65536, `-fa 1`→`-fa on`, `+ --cache-reuse 256 -cram 16384` + (`bak-20260630-154842`). Verifiziert: ready, ~52 t/s, 24,4 GB. +- ✅ **F2** fast `--parallel 2`→`1` (voller 65k-Kontext, verifiziert). +- ✅ **MTP für gemma** (`bak-20260630-155520`): Drafter `mtp-gemma-4-26B-A4B-it.gguf` (461 MB) geladen, + cmd `+ --model-draft … --spec-type draft-mtp --spec-draft-n-max 4`. **Verifiziert: 52 → 70,8 t/s (≈1,36×)**, + Draft-Akzeptanz ~50 %, kein Crash, ready. +- ✅ **scout** GLM-4.6V-Flash installiert (Q4 6,17 GB + mmproj 1,84 GB), role=scout, ttl 300, on-demand. + Verifiziert: lädt sauber (neue GLM-4.6V-Arch auf Engine 9843), 34,6 t/s, reasoniert korrekt + (`reasoning_content`). **Hinweis:** ist ein *Thinking*-Modell → höhere Latenz für Kurzantworten; Vision + (mmproj) geladen, aber Bild-Eingabe noch nicht getestet. Ggf. No-Think-Modus prüfen, falls als schneller + Allrounder gewünscht. + +### Code-Änderungen (Dev-Repo F:\, **noch nicht deployt**) +- ✅ **MC2 MTP-Draft-Support** (Brief-Deliverable). Verifiziert: py_compile OK, Logik-Test gegen echte GGUF, + Frontend `tsc --noEmit` exit 0. + - `config.py`: `SPEC_DRAFT_N_MAX` (Default 4). + - `services/llamaswap.py`: `_is_mtp_draft`, `_spec_flags_for_draft`, `_sibling_mtp_drafters` (findet MTP-Köpfe + NEBEN dem Modell); `find_compatible_draft`/`spec_draft_flags`/`drafts_for`/`set_spec_draft` MTP-bewusst; + `_parse_model` erkennt `--model-draft`/`-md`. Backward-kompatibel (klassische Drafts unverändert). + - Frontend `api.ts` (`DraftInfo.mtp`), `SpecDraftModal.tsx` (MTP-Badge). → gemmas MTP-Drafter erscheint im + Spec-Modal automatisch als kompatibel + aktivierbar, schreibt die korrekten `draft-mtp`-Flags. +- ✅ **CMD-Template-Drift** (`register_model`): `--cache-reuse 256 -cram 16384` als Default für alle + Template-Modelle; `--parallel 2` nur noch für `coder` (kein ctx-Halbierungs-Footgun mehr); Spec-Auto-Attach + MTP-bewusst + für alle Rollen self-guarding. Verifiziert: py_compile OK. +- ✅ **`budget.reserved_gb`** an Gruppen-Swap-Semantik angeglichen (`_coresident_members` = `swap:false`-Set; + on-demand = läuft allein → reserviert 0, voller GTT; brains-Member = reserviert die übrigen Member). + Verifiziert gegen echte Config: hermes→16,2 GB, heavy/coder/scout→0 GB (ondemand-alone). Tote + `_persist_members` entfernt. +- ⬜ **Deploy:** Backend+Frontend müssen auf die Box (git push → box pull → `restart mission-control-2` + + Frontend-Build). Box-`config.yaml`-Tunings sind davon unabhängig und bereits live. + +--- + +## 10. Leitplanken (eingehalten) +- Features (Sehen/Hören/Sprechen/Embedding/Gedächtnis) + IDE-Lanes (chat/coding) bleiben funktionsfähig — + W1/W2 macht sie sogar robuster (kein OOM mehr). +- Vor jeder Box-Config-Änderung Backup; llama-swap reloadt per `-watch-config`. +- **Keine destruktiven Aktionen ohne Freigabe dieses Plans.** + diff --git a/frontend/src/components/models/SpecDraftModal.tsx b/frontend/src/components/models/SpecDraftModal.tsx index b86d37e..8212539 100644 --- a/frontend/src/components/models/SpecDraftModal.tsx +++ b/frontend/src/components/models/SpecDraftModal.tsx @@ -112,7 +112,17 @@ export function SpecDraftModal({ )} >
-
{d.filename}
+
+ {d.filename} + {d.mtp && ( + + MTP + + )} +
{fmtSize(d.size_bytes)} · Vocab: {vocabLabel(d.vocab)}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index ae850e8..f6c710a 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -149,6 +149,7 @@ export interface DraftInfo { size_bytes: number | null vocab: VocabFingerprint | null compatible: boolean | null // null = nicht prüfbar (Ziel-GGUF fehlt) + mtp?: boolean // MTP-Kopf (Multi-Token-Prediction) statt klassischem Draft } export interface DraftsResp {