4e5a7eed35
a) Update-Meldungen: generischer Release-Summarizer in Lucys Stimme mit
strukturiertem Aktions-Verdikt ("Musst du etwas tun? NEIN/JA") für
Hermes, Engine (llama.cpp) und llama-swap; Fangnetz-Hinweis verheiratet
Breaking-Change-Sorge mit dem Postcheck.
b) OS ehrlich: zurückgestellte Pakete (Phasen-Rollout / kept back) werden
ausgewiesen statt scheinbar zu hängen.
d) Ehrliche Speicher-Zahlen: KV-Cache aus echten GGUF-Architektur-Daten
(Layer × KV-Köpfe × head_dim) + KV-Quant aus dem cmd statt params-blinder
Schätzung — footprint_gb als eine Zahlensprache (Zentrale, Modell-Manager,
fits-Check auf warmset+largest). Auto-Rewarm-Nudge nach Config-Reload.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
106 lines
4.2 KiB
Python
106 lines
4.2 KiB
Python
"""
|
|
Hält das Agent-Hirn (Rolle `hermes`) 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.
|
|
|
|
Abschaltbar/justierbar via Env: MC_REWARM_ENABLED=0, MC_REWARM_INTERVAL, MC_REWARM_MODEL.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
|
|
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
|
|
|
|
# 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 _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
|
|
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)
|
|
except Exception:
|
|
log.debug("rewarm: Hirn-Lookup fehlgeschlagen", exc_info=True)
|
|
return "fast"
|
|
|
|
|
|
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 [])
|
|
|
|
|
|
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 rewarm_loop() -> None:
|
|
"""Endlos-Schleife (Hintergrund-Task): prüft periodisch (und sofort nach einem Config-
|
|
Reload-Nudge), wärmt bei Idle vor."""
|
|
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)
|
|
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
|
|
except asyncio.TimeoutError:
|
|
pass
|