radar: Kandidaten messen mit der schnellsten Draft-Variante, Bild-Probe und Betrieb wie heute mit Zwilling
Ampel / ampel (push) Failing after 22s
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:
co-authored by
Claude Opus 5.5
parent
42363229d4
commit
4a05bc2a05
+131
-19
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user