From 5ee3f8f73d747c9ee08dc60be2a047d5a9dbf5f5 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Fri, 3 Jul 2026 19:54:26 +0200 Subject: [PATCH] =?UTF-8?q?D16d-Fix:=20volles=20Warm-Set=20nachw=C3=A4rmen?= =?UTF-8?q?=20statt=20nur=20das=20Hirn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-Rewarm (Nudge + Idle-Tick) lädt jetzt das GANZE Warm-Set über deploy/warmup.sh nach (fast+vision via chat, embed via /v1/embeddings, Agent-Prompt-Prefill) statt nur einen Brain-Ping. Erkennt TEIL-Kälte (Mitglied fehlt in /running), nicht nur den komplett leeren Zustand — genau der Fall nach einem watch-config-Reload/Deploy (Augen+Gedächtnis fielen raus, Hirn blieb warm). deploy.sh ruft am Ende warmup.sh. Co-Authored-By: Claude Opus 4.8 --- backend/services/warmer.py | 108 ++++++++++++++++++++++--------------- deploy/deploy.sh | 6 +++ 2 files changed, 72 insertions(+), 42 deletions(-) diff --git a/backend/services/warmer.py b/backend/services/warmer.py index ed112ce..05cad9d 100644 --- a/backend/services/warmer.py +++ b/backend/services/warmer.py @@ -1,19 +1,26 @@ """ -Hält das Agent-Hirn (Rolle `hermes`) dauerhaft warm. +Hält das ganze WARM-SET (Hirn + Augen/vision + Gedächtnis/embed) dauerhaft warm. Hintergrund: llama-swap ist EIN-Gruppen-resident — lädt ein on-demand-Modell außerhalb -der `brains`-Gruppe, wird die ganze Gruppe (inkl. Hirn) verdrängt. `persist: true` -verhindert nur Idle-Unload, NICHT die Gruppen-Verdrängung; neu vorgewärmt wird sonst erst -beim nächsten llama-swap-(Re)Start. Dieser Wächter schließt die Lücke: ist die Box idle -(nichts geladen), pingt er das Hirn vor. Während aktiver Last (irgendetwas geladen) hält -er sich raus, verdrängt also nie ein gerade genutztes Modell. +der `brains`-Gruppe, wird die ganze Gruppe verdrängt. `persist: true` verhindert nur +Idle-Unload, NICHT die Gruppen-Verdrängung; auch ein `-watch-config`-Reload (jede Config- +Änderung/Deploy) verwirft das Set, ohne llama-swaps ExecStartPost-Warmup neu auszulösen. +Dieser Wächter schließt die Lücke: ist die Box idle (nichts geladen), lädt er das ganze +Set über deploy/warmup.sh nach — NICHT nur das Hirn (sonst blieben Augen+embed kalt, live +vom Review-Wächter beobachtet). Während aktiver Last (irgendetwas geladen) hält er sich +raus, verdrängt also nie ein gerade genutztes Modell. -Abschaltbar/justierbar via Env: MC_REWARM_ENABLED=0, MC_REWARM_INTERVAL, MC_REWARM_MODEL. +warmup.sh deckt korrekt ab: fast+vision über /v1/chat/completions, embed über /v1/embeddings +(anderer Endpunkt!) und das Vorkauen des ~70-KB-Agent-Prompts. Es ist selbst-detachend. + +Abschaltbar/justierbar via Env: MC_REWARM_ENABLED=0, MC_REWARM_INTERVAL. Welche Modelle +warmup.sh lädt: MC_WARMUP_MODELS (Default 'fast vision') + MC_WARMUP_EMBED (Default 'embed'). """ import asyncio import logging import os +import subprocess import httpx @@ -26,6 +33,10 @@ INTERVAL = int(os.environ.get("MC_REWARM_INTERVAL", "90")) # Sekunden zwisch START_DELAY = int(os.environ.get("MC_REWARM_START_DELAY", "25")) NUDGE_GRACE = int(os.environ.get("MC_REWARM_NUDGE_GRACE", "3")) # llama-swap den Reload abschließen lassen +# Das geteilte Warm-Skript (auch llama-swaps ExecStartPost) — EINE Quelle für „was ist das +# Warm-Set und wie wärmt man es korrekt", statt hier eine zweite, ärmere Logik zu pflegen. +_WARMUP_SH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "deploy", "warmup.sh")) + # Wecksignal für einen sofortigen Vorwärm-Check (statt bis zum nächsten INTERVAL-Tick zu warten). # Wird von write_config() nach einer Config-Änderung gesetzt: llama-swap (-watch-config) lädt die # neue Config und verwirft dabei ALLE Modelle inkl. Hirn — ohne Nudge bliebe es bis zu INTERVAL @@ -44,56 +55,67 @@ def nudge() -> None: pass -def _brain_model() -> str: - """Aktives Agent-Hirn = Hermes' model.default (sonst model.model). So wärmt der Wächter - immer das WIRKLICH genutzte Hirn (passt sich Hirn-Wechseln an). Override: MC_REWARM_MODEL.""" - forced = os.environ.get("MC_REWARM_MODEL") - if forced: - return forced +def _run_warmup() -> bool: + """Volles Warm-Set via deploy/warmup.sh nachladen (fast+vision über /v1/chat/completions, + embed über /v1/embeddings, plus Agent-Prompt-Prefill). Selbst-detachend, blockiert nicht. + False, wenn das Skript fehlt.""" + if not os.path.exists(_WARMUP_SH): + log.warning("rewarm: warmup.sh nicht gefunden (%s) — kein Nachwärmen möglich", _WARMUP_SH) + return False try: - from ruamel.yaml import YAML - from config import HERMES_HOME - p = HERMES_HOME / "config.yaml" - if p.exists(): - with p.open(encoding="utf-8") as f: - cfg = YAML().load(f) or {} - m = (cfg.get("model") or {}) if isinstance(cfg, dict) else {} - v = m.get("default") or m.get("model") - if v: - return str(v) + subprocess.Popen(["bash", _WARMUP_SH], + env={**os.environ, "MC_LLAMA_SWAP_URL": LLAMA_SWAP_URL}, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return True except Exception: - log.debug("rewarm: Hirn-Lookup fehlgeschlagen", exc_info=True) - return "fast" + log.debug("rewarm: warmup.sh-Start fehlgeschlagen", exc_info=True) + return False -async def _running_empty() -> bool: - async with httpx.AsyncClient(timeout=8.0) as c: - r = await c.get(f"{LLAMA_SWAP_URL}/running") - data = r.json() or {} - return not (data.get("running") or []) +def _warmset_members() -> set[str]: + """Soll-Warm-Set = Mitglieder aller ko-residenten Gruppen (swap:false bzw. persist/persistent).""" + try: + from services import llamaswap + groups = llamaswap.list_groups() or {} + except Exception: + return set() + want: set[str] = set() + for g in groups.values(): + if isinstance(g, dict) and (g.get("swap") is False or g.get("persist") or g.get("persistent")): + want.update(g.get("members") or []) + return want -async def _warm(model: str) -> None: - async with httpx.AsyncClient(timeout=180.0) as c: - await c.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json={ - "model": model, "max_tokens": 1, - "messages": [{"role": "user", "content": "ping"}], - }) +async def _warmset_missing() -> list[str] | None: + """Warm-Set-Mitglieder, die NICHT 'ready' in /running sind. [] = alles warm, None = /running + nicht lesbar. Erkennt auch TEIL-Kälte (nur Hirn warm, Augen/embed rausgefallen) — genau der + Fall, der nach einem watch-config-Reload/Deploy auftritt.""" + try: + async with httpx.AsyncClient(timeout=8.0) as c: + r = await c.get(f"{LLAMA_SWAP_URL}/running") + data = r.json() or {} + except Exception: + return None + ready = {str(x.get("model")) for x in (data.get("running") or []) if x.get("state") == "ready"} + want = _warmset_members() + if not want: # Set unbekannt → alte Heuristik: nur bei ganz leer + return [] if ready else [""] + return [m for m in want if m not in ready] async def rewarm_loop() -> None: - """Endlos-Schleife (Hintergrund-Task): prüft periodisch (und sofort nach einem Config- - Reload-Nudge), wärmt bei Idle vor.""" + """Endlos-Schleife (Hintergrund-Task): hält das ganze Warm-Set warm. Prüft periodisch, ob ein + Mitglied fehlt (Teil-Kälte!), und sofort nach einem Config-Reload-Nudge — lädt via warmup.sh nach.""" global _loop, _wake _loop = asyncio.get_running_loop() _wake = asyncio.Event() await asyncio.sleep(START_DELAY) # Box/Engine nach MC-Start setzen lassen while True: try: - if await _running_empty(): - model = _brain_model() - log.info("rewarm: Box idle → Hirn '%s' wird vorgewärmt", model) - await _warm(model) + missing = await _warmset_missing() + if missing: + log.info("rewarm: Warm-Set unvollständig (%s) → warmup.sh", ", ".join(missing)) + _run_warmup() except Exception: log.debug("rewarm: Tick fehlgeschlagen", exc_info=True) # Bis zum nächsten Tick warten ODER sofort auf einen Config-Reload-Nudge reagieren. @@ -101,5 +123,7 @@ async def rewarm_loop() -> None: await asyncio.wait_for(_wake.wait(), timeout=INTERVAL) _wake.clear() await asyncio.sleep(NUDGE_GRACE) # llama-swap den Reload abschließen lassen + log.info("rewarm: Config-Reload → warmup.sh (volles Warm-Set nachladen)") + _run_warmup() except asyncio.TimeoutError: pass diff --git a/deploy/deploy.sh b/deploy/deploy.sh index f265c46..48616cd 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -93,4 +93,10 @@ command -v ttyd >/dev/null 2>&1 && systemctl --user restart hermes-terminal 2>/d sleep 2 echo "--- Health ---" curl -sf http://127.0.0.1:9001/api/health && echo + +# Warm-Set (Hirn + Augen/vision + Gedächtnis/embed) nach dem Deploy nachladen — ein Deploy bzw. +# watch-config-Reload verwirft es sonst und die erste Anfrage wäre kalt (D16d). warmup.sh ist +# selbst-detachend (blockiert den Deploy nicht) und lädt jedes Modell über den richtigen Endpunkt. +bash "$SRC/deploy/warmup.sh" || true + echo "OK — Mission Control 2.0 läuft auf :9001 (User-Dienst, sudo-frei)."