radar: Kandidaten messen mit der schnellsten Draft-Variante, Bild-Probe und Betrieb wie heute mit Zwilling
Ampel / ampel (push) Failing after 22s

Das heutige Modell misst mit seinem Draft (Hirn 106 t/s, ohne 68), Kandidaten liefen ohne -
Ornith fiel am 24.09. deshalb durch, obwohl es dieselbe Architektur hat. Jetzt probiert das Radar
kurz: ohne Draft, eingebauten MTP-Kopf und den Draft des heutigen Modells (gleiche Architektur,
Speicher reicht) und testet mit der schnellsten. Spekulatives Dekodieren aendert die Antworten
nicht, nur das Tempo.

Mit Draft laeuft die Bild-Probe auf einem Zwilling ohne Draft (llama.cpp: Draft und Bild = HTTP
500). Uebernehmen baut wie der Betrieb seit 24.09.: Hauptmodell mit dem gewaehlten Draft ohne
Projektor, dazu ein Bild-Zwilling; der alte Zwilling fliegt raus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 13:26:08 +02:00
co-authored by Claude Opus 5.5
parent 42363229d4
commit 4a05bc2a05
3 changed files with 308 additions and 43 deletions
+131 -19
View File
@@ -102,6 +102,7 @@ HF_BASIS = "https://huggingface.co"
ROLLEN = ("hirn", "coder")
ALIAS = {"hirn": "hermes", "coder": "coder"} # llama-swap-Alias der Rolle heute
ZWILLING = {"hirn": "vision", "coder": "coder-bild"} # Bild-Zwilling der Rolle (seit 24.09.)
ZWILLING_CTX = {"hirn": 65536, "coder": 131072} # Kontext der Zwillinge wie im Betrieb
DISCOVER_ROLLEN = {"coder": "coder", "fast": "hirn", "hermes": "hirn"}
PARALLEL = {"hirn": 2, "coder": 1} # Slots im Betrieb, wie heute
KANDIDAT_FELDER = ("id", "name", "rolle", "repo", "datei", "groesse_gb", "passt", "eng", "quelle", "status",
@@ -109,7 +110,8 @@ KANDIDAT_FELDER = ("id", "name", "rolle", "repo", "datei", "groesse_gb", "passt"
TEST_FELDER = ("id", "kandidat", "rolle", "datum", "ergebnis", "werte", "vergleich", "zusammenfassung")
_REIHENFOLGE = {"wartet": 0, "neu": 1, "getestet": 2, "bestanden": 3, "uebernommen": 4, "durchgefallen": 5,
"verworfen": 6}
_SCHRITTE = {"download": "lädt herunter", "baseline": "misst das heutige Modell", "test": "testet den Kandidaten"}
_SCHRITTE = {"download": "lädt herunter", "baseline": "misst das heutige Modell",
"draft": "sucht die schnellste Draft-Variante", "test": "testet den Kandidaten"}
class Abbruch(Exception):
@@ -712,7 +714,8 @@ def _bewerte(fund: dict) -> dict:
kv = (kv_je_token_gb(kopf) if kopf else None) or _kv_schaetzung(repo, groesse / 1e9)
passung = speicher_passung(rolle, groesse / 1e9, kv, eng_markiert=bool(fund.get("eng")))
k.update(groesse_gb=round(groesse / 1e9, 1), passt=passung["passt"], eng=passung["eng"], ctx=passung["ctx"],
bedarf_gb=passung["bedarf_gb"], tauglich=passung["passt"], arch=arch)
bedarf_gb=passung["bedarf_gb"], tauglich=passung["passt"], arch=arch,
mtp=bool((kopf or {}).get(f"{arch}.nextn_predict_layers")))
k["begruendung"] = f"{herkunft} {passung['text']}{notiz}"
if not kopf and not passung["passt"]:
# Ohne Kopf rechnet die Schätzung den KV-Cache grob zu groß — darauf hin nichts verwerfen,
@@ -724,7 +727,7 @@ def _bewerte(fund: dict) -> dict:
_UEBERNEHMEN = ("name", "rolle", "repo", "quant", "quelle", "rang", "datei", "dateien", "mmproj", "draft", "flags",
"groesse_gb", "passt", "eng", "ctx", "bedarf_gb", "arch", "begruendung")
"groesse_gb", "passt", "eng", "ctx", "bedarf_gb", "arch", "mtp", "begruendung")
def _suche_anwenden(stand: dict, bewertet: list[dict], quellen_ok: dict[str, bool], zeit: float) -> dict:
@@ -1101,6 +1104,74 @@ def _baseline(rolle: str, frist: float) -> dict | None:
protokoll=_protokoll)
_ARCH_CACHE: dict[tuple[str, float], str | None] = {}
def _lokale_architektur(pfad: str) -> str | None:
"""general.architecture einer lokalen GGUF-Datei (die ersten 8 MB reichen, der Kopf steht vorn)."""
try:
schluessel = (pfad, os.path.getmtime(pfad))
except OSError:
return None
if schluessel not in _ARCH_CACHE:
try:
with open(pfad, "rb") as f:
kopf = gguf_kopf_aus_bytes(f.read(8 << 20))
except OSError:
kopf = None
_ARCH_CACHE[schluessel] = (kopf or {}).get("general.architecture")
return _ARCH_CACHE[schluessel]
def _live_draft(heute: dict) -> dict | None:
"""Spekulatives Dekodieren des heutigen Modells der Rolle: Typ, Draft-Datei, n_max und die Architektur."""
befehl = str(heute.get("cmd") or "")
typ = re.search(r"--spec-type\s+(\S+)", befehl)
if not typ:
return None
pfad = re.search(r"--spec-draft-model\s+(\S+)", befehl)
n_max = re.search(r"--spec-draft-n-max\s+(\d+)", befehl)
return {"typ": typ.group(1), "pfad": pfad.group(1) if pfad else None,
"n_max": int(n_max.group(1)) if n_max else None,
"arch": _lokale_architektur(str(heute.get("gguf_path") or ""))}
def _draft_varianten(k: dict, pfade: dict) -> list[dict]:
"""Womit der Kandidat spekulativ dekodieren könnte. Fairness: das heutige Modell misst mit seinem Draft
(Hirn 106 t/s, ohne Draft 68) — Ornith fiel am 24.09. ohne Draft durch, obwohl es dieselbe Architektur hat
und den Draft des Hirns hätte nutzen können. Ausprobiert werden: ohne Draft, ein eingebauter MTP-Kopf und
der Draft des heutigen Modells (nur bei gleicher Architektur und wenn der Speicher reicht). Ein Draft aus
der Merkliste ist eine bewusste Wahl und wird allein genommen. „ort“ sagt, wo die Draft-Datei liegt:
im Kandidaten-Ordner (wandert beim Übernehmen mit) oder fremd (fester Pfad)."""
ps = _pruefstand()
if (d := k.get("draft") or {}).get("typ"):
return [{"name": "Draft aus der Merkliste", "flags": ps.draft_flags(pfade.get("draft"), d["typ"], d.get("n_max")),
"draft": {"typ": d["typ"], "ort": "kandidat" if pfade.get("draft") else None, "n_max": d.get("n_max")}}]
varianten = [{"name": "ohne Draft", "flags": [], "draft": None}]
if k.get("mtp"):
varianten.append({"name": "eingebauter MTP-Kopf", "flags": ps.draft_flags(None, "draft-mtp"),
"draft": {"typ": "draft-mtp", "ort": None, "n_max": None}})
live = _live_draft(_heutige_modelle().get(k["rolle"]) or {})
if live and live["pfad"] and k.get("arch") and live["arch"] == k["arch"]:
try:
draft_gb = os.path.getsize(live["pfad"]) / 1e9
except OSError:
draft_gb = None
if draft_gb is not None and float(k.get("bedarf_gb") or 0) + draft_gb <= SPEICHER_GRENZE_GB - WARMSET_GB:
varianten.append({"name": "Draft des heutigen Modells",
"flags": ps.draft_flags(live["pfad"], live["typ"], live["n_max"]),
"draft": {"typ": live["typ"], "ort": "fremd", "pfad": live["pfad"], "n_max": live["n_max"]}})
return varianten
def _draft_text(wahl: dict) -> str:
"""„Gemessen mit …“ für die Begründung: gewählte Variante und die Tempi aller Varianten."""
tempi = [f"{name} {str(wert).replace('.', ',')} t/s" if isinstance(wert, (int, float)) else f"{name}: {wert}"
for name, wert in (wahl.get("messungen") or {}).items()]
return f" Gemessen {'mit' if wahl.get('draft') else ''} {wahl['name']}".replace(" ", " ") + (
f" (13k: {'; '.join(tempi)})." if len(tempi) > 1 else ".")
def teste(kid: str, frist: float) -> dict | None:
"""Einen Kandidaten prüfen: Download → Baseline → nur Warm-Set in llama-swap → Kandidat auf :5899 →
Urteil. Gibt den Test-Eintrag zurück (None, wenn nur gemessen wurde oder es den Kandidaten nicht gibt)."""
@@ -1110,24 +1181,31 @@ def teste(kid: str, frist: float) -> dict | None:
return None
_markiere_lauf(kid, "download", frist)
ergebnis, satz, werte, vergleich, zaehlt = "abgebrochen", "", None, None, True
waechter, von_aussen = None, None
waechter, von_aussen, draft_betrieb = None, None, None
try:
ordner = _download(k, frist - TESTZEIT_MIN * 60)
pfade = _lokale_pfade(k, ordner)
_markiere_lauf(kid, "baseline", frist)
basis = _baseline(k["rolle"], frist)
_markiere_lauf(kid, "test", frist)
waechter = _Nachtwaechter(_nur_warmset())
waechter.start()
draft = k.get("draft") or {}
flags = ps.draft_flags(pfade["draft"], draft.get("typ"), draft.get("n_max")) + list(k.get("flags") or [])
varianten = _draft_varianten(k, pfade)
if len(varianten) > 1:
_markiere_lauf(kid, "draft", frist)
wahl = ps.waehle_draft(pfade["gguf"], varianten, frist=frist, protokoll=_protokoll)
else:
wahl = {**varianten[0], "messungen": {}}
_markiere_lauf(kid, "test", frist)
flags = list(wahl["flags"]) + list(k.get("flags") or [])
lauf = ps.pruefe_kandidat(k["rolle"], pfade["gguf"], mmproj=pfade["mmproj"], flags=flags,
ctx=int(k.get("ctx") or ps.CTX_STANDARD), basis=(basis or {}).get("werte"),
frist=frist, protokoll=_protokoll)
werte, vergleich = lauf["werte"], lauf["vergleich"]
werte["draft"] = {"gewaehlt": wahl["name"], "messungen": wahl.get("messungen") or {}}
draft_betrieb = wahl.get("draft")
if basis:
werte["baseline"] = {"modell": (basis.get("werte") or {}).get("modell"), "gemessen": _iso(basis.get("zeit"))}
ergebnis, satz = lauf["ergebnis"], lauf["zusammenfassung"]
ergebnis, satz = lauf["ergebnis"], lauf["zusammenfassung"] + _draft_text(wahl)
except ps.Zeitende:
satz = f"Das Nachtfenster endete um {FENSTER_ENDE}, bevor der Test fertig war."
except ps.ServerFehler as e:
@@ -1153,6 +1231,8 @@ def teste(kid: str, frist: float) -> dict | None:
def abschluss(stand: dict):
stand["lauf"] = None
if kid in stand["kandidaten"]: # der Draft, mit dem gemessen wurde, läuft auch im Betrieb
stand["kandidaten"][kid]["draft_betrieb"] = draft_betrieb
return _abschluss(stand, kid, ergebnis, satz, werte=werte, vergleich=vergleich, zaehlt=zaehlt)
test = _aendere(abschluss)
log.info("radar: %s%s (%s)", kid, ergebnis, satz)
@@ -1197,23 +1277,53 @@ def notstopp() -> None:
# --- Übernehmen und Verwerfen ----------------------------------------------------------------
def betriebs_befehl(rolle: str, pfade: dict, k: dict) -> str:
"""llama-swap-Befehl für den übernommenen Kandidaten: die getesteten Flags plus die Betriebs-Flags
der Rolle wie heute (Hirn 2 Slots, Coder 1 Slot, KV q8_0, Prompt-Cache 16 GB)."""
"""llama-swap-Befehl für den übernommenen Kandidaten wie heute im Betrieb: ohne Projektor (Bilder gehen an
den Zwilling), mit dem Draft, mit dem er getestet wurde, dazu die Betriebs-Flags der Rolle (Hirn 2 Slots,
Coder 1 Slot, KV q8_0, Prompt-Cache 16 GB)."""
teile = ["llama-server", "-m", pfade["gguf"], "--host", "127.0.0.1", "--port", "${PORT}",
"-c", str(int(k.get("ctx") or CTX_ROLLE[rolle])), "-ngl", "999", "-fa", "on", "--load-mode", "none"]
if pfade.get("mmproj"):
teile += ["--mmproj", pfade["mmproj"]]
teile += ["--jinja", "--parallel", str(PARALLEL[rolle]), "-cram", "16384", "-ctk", "q8_0", "-ctv", "q8_0"]
draft = k.get("draft") or {}
if draft.get("typ"):
"-c", str(int(k.get("ctx") or CTX_ROLLE[rolle])), "-ngl", "999", "-fa", "on", "--load-mode", "none",
"--jinja", "--parallel", str(PARALLEL[rolle]), "-cram", "16384", "-ctk", "q8_0", "-ctv", "q8_0"]
draft = k.get("draft_betrieb")
if draft is None and (k.get("draft") or {}).get("typ"): # vor dem 25.09. getestet: Draft der Merkliste
draft = {"typ": k["draft"]["typ"], "ort": "kandidat", "n_max": k["draft"].get("n_max")}
if draft and draft.get("typ"):
teile += ["--spec-type", draft["typ"]]
if pfade.get("draft"):
teile += ["--spec-draft-model", pfade["draft"]]
pfad = pfade.get("draft") if draft.get("ort") == "kandidat" else draft.get("pfad")
if pfad:
teile += ["--spec-draft-model", pfad]
if draft.get("n_max"):
teile += ["--spec-draft-n-max", str(draft["n_max"])]
return " ".join(teile + [str(f) for f in k.get("flags") or []])
def zwilling_befehl(rolle: str, pfade: dict, k: dict) -> str:
"""Bild-Zwilling im Betrieb (seit 24.09.): gleiche Gewichte plus Projektor, OHNE Draft — llama.cpp kann
Draft und Bild nicht zusammen. Das Gateway schickt nur Anfragen mit einem neuen Bild hierher."""
teile = ["llama-server", "-m", pfade["gguf"], "--host", "127.0.0.1", "--port", "${PORT}",
"-c", str(ZWILLING_CTX[rolle]), "-ngl", "999", "-fa", "on", "--load-mode", "none",
"--mmproj", pfade["mmproj"], "--jinja", "--parallel", "1", "-cram", "8192", "-ctk", "q8_0", "-ctv", "q8_0"]
return " ".join(teile + _pruefstand().ohne_draft([str(f) for f in k.get("flags") or []]))
def _zwilling_eintragen(cfg: dict, rolle: str, modell_id: str, pfade: dict, k: dict) -> str:
"""Bild-Zwilling des neuen Modells anlegen (Gruppe „bild“, verdrängt nichts). Der alte Zwilling der Rolle
fliegt aus der Config — ohne sein Hauptmodell ist er nutzlos; seine Dateien bleiben liegen und
tauchen im Aufräumen beim alten Modell auf."""
alias = ZWILLING[rolle]
models = cfg.setdefault("models", {})
groups = cfg.setdefault("groups", {})
for alt in [n for n, sp in models.items() if alias in {str(a).lower() for a in (sp or {}).get("aliases") or []}]:
del models[alt]
for gruppe in groups.values():
if isinstance(gruppe, dict) and alt in (gruppe.get("members") or []):
gruppe["members"] = [m for m in gruppe["members"] if m != alt]
name = f"{modell_id}-Bild"
models[name] = {"cmd": LiteralScalarString(zwilling_befehl(rolle, pfade, k) + "\n"), "ttl": 900, "aliases": [alias]}
bild = groups.setdefault("bild", {"swap": True, "exclusive": False, "persistent": False, "members": []})
bild["members"] = [m for m in bild.get("members") or [] if m != name] + [name]
return name
def _steward_neu_starten() -> str | None:
"""Drop-in neu einlesen und den Steward neu starten (User-Dienst, ohne sudo). Fehlertext oder None."""
if os.name != "posix":
@@ -1275,12 +1385,14 @@ def _tausche_ein(k: dict) -> dict:
ziel = quelle # schon verschoben (früherer, abgebrochener Versuch)
pfade = _lokale_pfade(k, ziel)
modell_id = llamaswap.register_model(pfade["gguf"], role=ALIAS[rolle], ctx=int(k.get("ctx") or CTX_ROLLE[rolle]),
mmproj_path=pfade["mmproj"], jinja=True, set_alias=False)
mmproj_path=None, jinja=True, set_alias=False)
cfg = llamaswap.read_config()
try: # die getesteten Flags statt der Vorlage aus register_model (sonst liefe ein ungetesteter Draft mit)
cfg["models"][modell_id]["cmd"] = LiteralScalarString(betriebs_befehl(rolle, pfade, k) + "\n")
except (KeyError, TypeError) as e:
raise RuntimeError(f"{modell_id} fehlt nach dem Eintragen in der llama-swap-Config.") from e
if pfade.get("mmproj"):
_zwilling_eintragen(cfg, rolle, modell_id, pfade, k)
llamaswap.write_config(cfg)
hinweis = None
if rolle == "hirn":