f88ed4e5df
User-Entscheid 07.07.: 'Gehirn + Embedding reichen komplett aus' — die Augen (VL-30B, ~19 GB) werden nur gebraucht, wenn Lucy auf dem Desktop an ist oder die IDE-Lane sie ruft. Warm-Set = nur noch Qwen3.6 + embed; damit passt der 60-GB-Chef-Gutachter (gpt-oss) wieder neben das Hirn. - llama-swap.config.yaml: VL-30B aus brains raus, ttl 0 -> 900 (15 min Nachlauf); Kommentare entstaubt. AUSSERDEM Template<->Live-Drift beendet: Template hatte coder-lite wiederbelebt (live seit 05.07. entfernt) und KV q4_k (lief NIE live) — beides auf Live-Wahrheit (q8_0, kein coder-lite) zurueckgesetzt; Qualitaet vor ein paar GB, RAM-Engpass ist mit der Diaet weg. - warmup.sh: Default 'fast vision' -> 'fast' (Root-Kopie unter /usr/local/bin braucht spaeter einmal sudo cp — bis dahin waermt ein llama-swap-Restart vision einmalig, ttl 900 raeumt es wieder ab) - warmer.py: Docstring auf neues Warm-Set angepasst - Lucy-Seite (eigenes Repo): App waermt die Augen beim Start + alle 10 min, solange sie laeuft — Augen-Lebenszyklus == Lucy-Lebenszyklus Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
131 lines
6.1 KiB
Python
131 lines
6.1 KiB
Python
"""
|
|
Hält das ganze WARM-SET (Hirn + Gedächtnis/embed) dauerhaft warm. Die Augen (vision/VL-30B)
|
|
sind seit 07.07. ON-DEMAND (ttl 900, User-Entscheid) und gehören NICHT mehr zum Warm-Set.
|
|
|
|
Hintergrund: llama-swap ist EIN-Gruppen-resident — lädt ein on-demand-Modell außerhalb
|
|
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 — Hirn UND embed (sonst bliebe 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.
|
|
|
|
warmup.sh deckt korrekt ab: fast ü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') + MC_WARMUP_EMBED (Default 'embed').
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
|
|
import httpx
|
|
|
|
from config import LLAMA_SWAP_URL
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
ENABLED = os.environ.get("MC_REWARM_ENABLED", "1") != "0"
|
|
INTERVAL = int(os.environ.get("MC_REWARM_INTERVAL", "90")) # Sekunden zwischen Checks
|
|
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
|
|
# Sekunden kalt liegen, bis der nächste Tick oder eine Anfrage es wieder lädt.
|
|
_loop: asyncio.AbstractEventLoop | None = None
|
|
_wake: asyncio.Event | None = None
|
|
|
|
|
|
def nudge() -> None:
|
|
"""Threadsicher: bittet den Wächter, nach einem Config-Reload bald vorzuwärmen. No-op,
|
|
solange der Wächter (noch) nicht läuft."""
|
|
if _loop is not None and _wake is not None and not _loop.is_closed():
|
|
try:
|
|
_loop.call_soon_threadsafe(_wake.set)
|
|
except RuntimeError:
|
|
pass
|
|
|
|
|
|
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:
|
|
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: warmup.sh-Start fehlgeschlagen", exc_info=True)
|
|
return False
|
|
|
|
|
|
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 _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 ["<leer>"]
|
|
return [m for m in want if m not in ready]
|
|
|
|
|
|
async def rewarm_loop() -> None:
|
|
"""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:
|
|
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.
|
|
try:
|
|
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
|