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":
|
||||
|
||||
@@ -355,7 +355,10 @@ def _uebernahme_vorbereiten(zustand, monkeypatch, rolle: str, alt: dict) -> tupl
|
||||
stand = radar._leer()
|
||||
stand["kandidaten"]["o"] = _kandidat("o", rolle=rolle, status="bestanden", ordner=str(ordner))
|
||||
radar._speichere(stand)
|
||||
aufrufe, cfg = [], {"models": {}}
|
||||
aufrufe = []
|
||||
cfg = {"models": {"alter-zwilling": {"cmd": "llama-server -m /alt.gguf --mmproj /alt-mm.gguf",
|
||||
"aliases": ["vision" if rolle == "hirn" else "coder-bild"]}},
|
||||
"groups": {"bild": {"swap": True, "exclusive": False, "members": ["alter-zwilling"]}}}
|
||||
|
||||
def eintragen(pfad, **kw):
|
||||
aufrufe.append(("eintragen", pfad, kw["role"], kw["set_alias"]))
|
||||
@@ -386,7 +389,11 @@ def test_uebernehmen_hirn_nutzt_den_hirn_wechsel_und_nimmt_fast_mit(zustand, mon
|
||||
assert [a[0] for a in aufrufe] == ["eintragen", "schreiben", "hirn", "dazu", "steward"]
|
||||
assert aufrufe[0][2:] == ("hermes", False) and aufrufe[3] == ("dazu", "o", "fast")
|
||||
befehl = str(cfg["models"]["o"]["cmd"])
|
||||
assert "--parallel 2" in befehl and "-ctk q8_0" in befehl and "--mmproj" in befehl
|
||||
assert "--parallel 2" in befehl and "-ctk q8_0" in befehl and "--mmproj" not in befehl
|
||||
# Wie im Betrieb seit 24.09.: Bilder sieht der neue Bild-Zwilling (Projektor, ohne Draft), der alte fliegt raus.
|
||||
zwilling = cfg["models"]["o-Bild"]
|
||||
assert zwilling["aliases"] == ["vision"] and "--mmproj" in str(zwilling["cmd"]) and "--spec" not in str(zwilling["cmd"])
|
||||
assert "alter-zwilling" not in cfg["models"] and cfg["groups"]["bild"]["members"] == ["o-Bild"]
|
||||
assert (zustand / "models" / "o-GGUF" / "o-Q4_K_M.gguf").exists() and not (radar.RADAR_DIR / "o").exists()
|
||||
k = radar._lade()["kandidaten"]["o"]
|
||||
assert k["status"] == "uebernommen" and "vorher Qwen3.6-35B-A3B" in k["begruendung"]
|
||||
@@ -405,16 +412,98 @@ def test_uebernehmen_coder_nimmt_heavy_mit(zustand, monkeypatch):
|
||||
|
||||
|
||||
def test_betriebsbefehl_uebernimmt_die_getesteten_flags():
|
||||
pfade = {"gguf": "/srv/models/F-GGUF/f.gguf", "mmproj": "/srv/models/F-GGUF/mm.gguf", "draft": None}
|
||||
k = _kandidat("f", rolle="coder", ctx=65536, draft={"typ": "draft-mtp", "n_max": 2, "datei": None}, flags=["-ub", "512"])
|
||||
befehl = radar.betriebs_befehl("coder", {"gguf": "/srv/models/F-GGUF/f.gguf", "mmproj": "/srv/models/F-GGUF/mm.gguf",
|
||||
"draft": None}, k)
|
||||
befehl = radar.betriebs_befehl("coder", pfade, k)
|
||||
assert "-c 65536" in befehl and "--parallel 1" in befehl and "-ctk q8_0 -ctv q8_0" in befehl
|
||||
assert "--mmproj /srv/models/F-GGUF/mm.gguf" in befehl and "--load-mode none" in befehl
|
||||
assert "--mmproj" not in befehl and "--load-mode none" in befehl # Bilder sieht der Zwilling
|
||||
assert befehl.endswith("--spec-type draft-mtp --spec-draft-n-max 2 -ub 512")
|
||||
zwilling = radar.zwilling_befehl("coder", pfade, k)
|
||||
assert "--mmproj /srv/models/F-GGUF/mm.gguf" in zwilling and "--spec" not in zwilling and "-c 131072" in zwilling
|
||||
assert zwilling.endswith("-ub 512")
|
||||
assert "--parallel 2" in radar.betriebs_befehl("hirn", {"gguf": "x.gguf", "mmproj": None, "draft": None},
|
||||
_kandidat("h"))
|
||||
|
||||
|
||||
def test_betriebsbefehl_nimmt_den_im_test_gewaehlten_draft():
|
||||
"""Gewählt wurde der Draft des heutigen Modells: fester Pfad, nicht der Kandidaten-Ordner."""
|
||||
pfade = {"gguf": "/srv/models/O-GGUF/o.gguf", "mmproj": "/srv/models/O-GGUF/mm.gguf", "draft": None}
|
||||
k = _kandidat("o", draft_betrieb={"typ": "draft-dflash", "ort": "fremd", "pfad": "/srv/models/D/d.gguf",
|
||||
"n_max": None})
|
||||
assert radar.betriebs_befehl("hirn", pfade, k).endswith("--spec-type draft-dflash --spec-draft-model /srv/models/D/d.gguf")
|
||||
k["draft_betrieb"] = None # im Test war „ohne Draft“ am schnellsten
|
||||
assert "--spec" not in radar.betriebs_befehl("hirn", pfade, k)
|
||||
|
||||
|
||||
def _gguf_datei(pfad, arch: str) -> None:
|
||||
"""Winzige GGUF-Datei mit general.architecture (genug für _lokale_architektur)."""
|
||||
wert = arch.encode()
|
||||
schluessel = b"general.architecture"
|
||||
daten = (b"GGUF" + struct.pack("<I", 3) + struct.pack("<Q", 0) + struct.pack("<Q", 1)
|
||||
+ struct.pack("<Q", len(schluessel)) + schluessel + struct.pack("<I", 8)
|
||||
+ struct.pack("<Q", len(wert)) + wert)
|
||||
pfad.write_bytes(daten)
|
||||
|
||||
|
||||
def test_draft_varianten_probieren_den_draft_des_heutigen_modells_bei_gleicher_architektur(tmp_path, monkeypatch):
|
||||
hirn_gguf, draft = tmp_path / "hirn.gguf", tmp_path / "dflash.gguf"
|
||||
_gguf_datei(hirn_gguf, "qwen35moe")
|
||||
draft.write_bytes(b"x" * 1000)
|
||||
heute = {"name": "Hirn", "gguf_path": str(hirn_gguf),
|
||||
"cmd": f"llama-server -m {hirn_gguf} --spec-type draft-dflash --spec-draft-model {draft}"}
|
||||
monkeypatch.setattr(radar, "_heutige_modelle", lambda: {"hirn": heute})
|
||||
pfade = {"gguf": "/x/k.gguf", "mmproj": "/x/mm.gguf", "draft": None}
|
||||
|
||||
gleich = radar._draft_varianten(_kandidat("k", arch="qwen35moe", bedarf_gb=24.2, mtp=True), pfade)
|
||||
assert [v["name"] for v in gleich] == ["ohne Draft", "eingebauter MTP-Kopf", "Draft des heutigen Modells"]
|
||||
assert gleich[2]["flags"] == ["--spec-type", "draft-dflash", "--spec-draft-model", str(draft)]
|
||||
assert gleich[2]["draft"]["ort"] == "fremd"
|
||||
|
||||
fremd = radar._draft_varianten(_kandidat("k", arch="qwen3next", bedarf_gb=24.2), pfade)
|
||||
assert [v["name"] for v in fremd] == ["ohne Draft"] # andere Architektur: nicht probieren
|
||||
zu_gross = radar._draft_varianten(_kandidat("k", arch="qwen35moe", bedarf_gb=88.0), pfade)
|
||||
assert [v["name"] for v in zu_gross] == ["ohne Draft"] # Draft passt nicht mehr in den Speicher
|
||||
merkliste = radar._draft_varianten(_kandidat("k", arch="qwen35moe", draft={"typ": "draft-simple"}),
|
||||
{**pfade, "draft": "/x/d.gguf"})
|
||||
assert [v["name"] for v in merkliste] == ["Draft aus der Merkliste"]
|
||||
|
||||
|
||||
def test_waehle_draft_nimmt_die_schnellste_variante_die_startet(ps, monkeypatch):
|
||||
tempi = {"": 67.4, "draft-mtp": 81.0, "draft-dflash": 98.2}
|
||||
|
||||
def mit_server(gguf, arbeit, *, flags=(), **kw):
|
||||
typ = flags[1] if flags else ""
|
||||
if typ == "draft-mtp":
|
||||
raise ps.ServerFehler("vocab mismatch")
|
||||
return {"tiefe": {"tg_tps": tempi[typ], "acc": 0.8 if typ else None}}, None
|
||||
|
||||
monkeypatch.setattr(ps, "mit_server", mit_server)
|
||||
varianten = [{"name": "ohne Draft", "flags": []},
|
||||
{"name": "eingebauter MTP-Kopf", "flags": ["--spec-type", "draft-mtp"]},
|
||||
{"name": "Draft des heutigen Modells", "flags": ["--spec-type", "draft-dflash"]}]
|
||||
wahl = ps.waehle_draft("/x/k.gguf", varianten, protokoll=lambda *_: None)
|
||||
assert wahl["name"] == "Draft des heutigen Modells"
|
||||
assert wahl["messungen"]["ohne Draft"] == 67.4 and "geht nicht" in wahl["messungen"]["eingebauter MTP-Kopf"]
|
||||
assert "Gemessen mit Draft des heutigen Modells" in radar._draft_text({**wahl, "draft": {"typ": "draft-dflash"}})
|
||||
|
||||
|
||||
def test_mit_draft_laeuft_die_bild_probe_auf_einem_zwilling_ohne_draft(ps, monkeypatch):
|
||||
"""Draft und Bild gehen in llama.cpp nicht zusammen (HTTP 500) — der Prüfstand trennt sie wie der Betrieb."""
|
||||
starts = []
|
||||
|
||||
def mit_server(gguf, arbeit, *, mmproj=None, flags=(), **kw):
|
||||
starts.append((mmproj, list(flags)))
|
||||
return ({"bild": None, "tiefe": {}} if len(starts) == 1 else {"direkt": 1.0, "agentisch": True}), \
|
||||
type("S", (), {"befehl": ["llama-server"], "ladezeit": 4})()
|
||||
|
||||
monkeypatch.setattr(ps, "mit_server", mit_server)
|
||||
lauf = ps.pruefe_kandidat("hirn", "/x/k.gguf", mmproj="/x/mm.gguf",
|
||||
flags=["--spec-type", "draft-dflash", "--spec-draft-model", "/d.gguf", "-ub", "512"])
|
||||
assert starts[0] == (None, ["--spec-type", "draft-dflash", "--spec-draft-model", "/d.gguf", "-ub", "512"])
|
||||
assert starts[1] == ("/x/mm.gguf", ["-ub", "512"])
|
||||
assert lauf["werte"]["bild"] == {"direkt": 1.0, "agentisch": True}
|
||||
|
||||
|
||||
# --- Prüfstand: Urteil, Programmieraufgaben, Bild ----------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
|
||||
Reference in New Issue
Block a user