boxwart: Modell-Radar sucht, testet nachts selbst und empfiehlt (Hirn und Coder)
Ampel / ampel (push) Failing after 21s

Suche aus Merkliste (deploy/radar-watchlist.json) und Hugging-Face-Entdeckung; nur
Kandidaten mit Bild-Projektor, die neben das Warm-Set passen (<= 115 GB inkl. KV-Cache).
Nachtlauf mc2-radar.timer 00:30, Tests nur bis 02:30 (um 03:00 kommt NerdQuiz),
hoechstens ein neuer Kandidat pro Woche, Notbremse 02:35, RuntimeMaxSec als letzte
Sicherung. Pruefstand als Modul (deploy/bench/pruefstand.py): Tempo, Werkzeuge,
Deutsch/JSON bzw. Programmieraufgaben und Bild-Probe gegen das heutige Modell der Rolle.
Durchgefallene werden geloescht, Bestandene gemeldet und erst nach "Uebernehmen" getauscht.

Beim Uebernehmen wandern die Zweitrollen mit (fast beim Hirn, heavy beim Coder), und das
Warm-Set des Stewards wird selbst umgestellt statt als Handgriff zu bleiben. Modell-Code
im Pruefstand darf keine Prozesse starten (RLIMIT_NPROC=0). Radar steht im Flugplan und
unter Waechter-Aufsicht; deploy.sh spielt seine Units ein. Oberflaeche: Vergleichswerte
je Kandidat, Rueckfrage vor Uebernehmen und Verwerfen, Status auf Deutsch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-23 22:29:38 +02:00
co-authored by Claude Opus 5.5
parent 4b226d5d22
commit 177c9a311c
30 changed files with 3511 additions and 194 deletions
+63 -167
View File
@@ -1,22 +1,23 @@
#!/usr/bin/env python3
"""Prüfstand für Hirn-Kandidaten (17.09.2026). Kandidat läuft standalone auf :5899 — der Live-Stack bleibt unberührt —,
Baselines laufen über llama-swap :8080. Je Betriebsart: Kurz- und 13k-Tiefenmessung (Prefill/Decode/Draft-Akzeptanz),
Tool-Calling direkt (3 Aufgaben, Denken an), Deutsch- und JSON-Probe (Denken aus) und der Tool-Smoke durch den ECHTEN
Hermes-Agenten (`hermes chat --provider pruefstand -m kandidat`; Provider `pruefstand` -> http://127.0.0.1:5899/v1 muss in
`providers:` der Hermes-Config stehen). Aufruf auf der Box: python3 pruefstand-hirn.py [baseline] [nemo] [u38] [u36]
(Kandidaten-Pfade unten anpassen). Ergebnis: ~/pruefstand-<datum>.json + Log auf stdout. Lehren: Denk-Modelle brauchen
für Tool-Aufrufe >=1500 Tokens Budget, /no_think wirkt bei Qwen 3.6 nicht (chat_template_kwargs.enable_thinking=false);
der erste Lauf nach dem Laden ist ~40 % langsamer (Warmlauf verwerfen)."""
"""Prüfstand für Hirn-Kandidaten (17.09.2026), seit 09/2026 eine dünne Hülle um pruefstand.py.
Die Messungen selbst (Tempo, 13k-Tiefe, Werkzeuge, Deutsch/JSON, Bild-Probe, Server auf :5899) liegen
im Modul — dasselbe nutzt das Modell-Radar nachts. Dieses Skript bleibt der Handbetrieb: Kandidaten
mit festen Pfaden gegen die Live-Modelle (llama-swap :8080) messen, optional mit dem Tool-Smoke durch
den ECHTEN Hermes-Agenten (`hermes chat --provider pruefstand -m kandidat`; der Provider `pruefstand`
-> http://127.0.0.1:5899/v1 muss in `providers:` der Hermes-Config stehen).
Aufruf auf der Box: python3 pruefstand-hirn.py [baseline] [nemo] [u38] [u36] (Pfade unten anpassen).
Ergebnis: ~/pruefstand-<datum>.json + Log auf stdout.
"""
import json
import os
import re
import subprocess
import sys
import time
import urllib.request
BIN = "/opt/llamacpp-vulkan/llama-server"; PORT = 5899
BASE = ["-c", "65536", "-ngl", "999", "-fa", "on", "--load-mode", "none", "--jinja", "--parallel", "1", "-ctk", "q8_0", "-ctv", "q8_0"]
import pruefstand as ps # liegt daneben; beim Skriptaufruf steht dieser Ordner auf sys.path
M = "/srv/models"
DFLASH_36 = f"{M}/Qwen3.6-35B-A3B-DFlash-GGUF/giocom-Qwen3.6-35B-A3B-DFlash-Q8_0.gguf"
DFLASH2_38 = f"{M}/Qwen3.8-27B-DFlash2-GGUF/Qwen3.8-27B-DFlash2-Q4_K_M.gguf"
@@ -24,170 +25,65 @@ NEMO = f"{M}/Nemotron-3.5-Lightning-30B-A3B-GGUF/NVIDIA-Nemotron-3.5-Lightning-3
NEMO_MTP = f"{M}/Nemotron-3.5-Lightning-30B-A3B-GGUF/mtp-NVIDIA-Nemotron-3.5-Lightning-30B-A3B-Q8_0.gguf"
U38 = f"{M}/Qwen3.8-27B-Uncensored-GGUF/Qwen3.8-27B-Uncensored-Q4_K_M.gguf"
U36 = f"{M}/Qwen3.6-35B-A3B-Uncensored-GGUF/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf"
HERMES = os.path.expanduser("~/.local/bin/hermes")
OUT = os.path.expanduser("~/pruefstand-2026-09-17.json")
RESULTS = []
OUT = os.path.expanduser(f"~/pruefstand-{time.strftime('%Y-%m-%d')}.json")
RESULTS: list[dict] = []
def log(*a):
print(*a, flush=True)
def save() -> None:
with open(OUT, "w", encoding="utf-8") as f:
json.dump(RESULTS, f, indent=1, ensure_ascii=False)
def post(url, payload, timeout=900):
r = urllib.request.Request(url, data=json.dumps(payload).encode(), headers={"Content-Type": "application/json"})
return json.loads(urllib.request.urlopen(r, timeout=timeout).read())
def chat(base, model, messages, max_tokens=200, tools=None, think=True):
p = {"model": model, "temperature": 0, "max_tokens": max_tokens, "messages": messages}
if not think:
p["chat_template_kwargs"] = {"enable_thinking": False}
if tools:
p["tools"] = tools
p["tool_choice"] = "auto"
t0 = time.time()
j = post(f"{base}/v1/chat/completions", p)
j["_wall"] = time.time() - t0
return j
PARA = ("Notiz {i}: Die Box läuft mit llama-swap auf Port 8080, das Hirn ist ein Mixture-of-Experts-Modell mit drei Milliarden "
"aktiven Parametern, der Coder ein dichtes 27B-Modell mit Draft-Beschleunigung. Lucy hört über Parakeet, spricht über "
"pocket-tts und meldet sich per Telegram. Der Sonntags-Job aktualisiert Router, Engine und Agent und rollt bei Rot zurück. ")
DEPTH_PROMPT = "".join(PARA.format(i=i) for i in range(130)) + \
"\n\nFrage: Fasse in drei Sätzen zusammen, was in den Notizen steht, und nenne die Nummer der letzten Notiz."
TOOLS = [
{"type": "function", "function": {"name": "terminal", "description": "Führt einen Shell-Befehl auf der Box aus und gibt stdout zurück.",
"parameters": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}},
{"type": "function", "function": {"name": "read_file", "description": "Liest eine Datei und gibt den Inhalt zurück.",
"parameters": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}},
{"type": "function", "function": {"name": "send_telegram", "description": "Schickt dem Nutzer eine Telegram-Nachricht.",
"parameters": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}},
]
TOOL_TASKS = [
("Wie viel freier Speicher ist auf /srv/models? Nutze das passende Werkzeug.", "terminal", r"df"),
("Lies die Datei /etc/hostname und sag mir, wie die Box heißt.", "read_file", r"/etc/hostname"),
("Schick mir per Telegram die Nachricht: Prüfstand läuft.", "send_telegram", r"Pr.fstand"),
]
def timings(j):
t = j.get("timings") or {}
acc = (t.get("draft_n_accepted", 0) / t["draft_n"]) if t.get("draft_n") else None
return {"pp_n": t.get("prompt_n"), "pp_s": round((t.get("prompt_ms") or 0) / 1000, 1), "pp_tps": round(t.get("prompt_per_second") or 0),
"tg_n": t.get("predicted_n"), "tg_tps": round(t.get("predicted_per_second") or 0, 1), "acc": None if acc is None else round(acc, 2)}
def save():
json.dump(RESULTS, open(OUT, "w"), indent=1, ensure_ascii=False)
def probes(base, model, label, with_hermes, hermes_args):
res = {"label": label}
chat(base, model, [{"role": "user", "content": "Sag kurz hallo."}], 32) # Warmlauf
j = chat(base, model, [{"role": "user", "content": "Erkläre in fünf Sätzen, was ein Mixture-of-Experts-Modell ist."}], 200)
res["kurz"] = timings(j); log(f" kurz : {res['kurz']}")
j = chat(base, model, [{"role": "user", "content": DEPTH_PROMPT}], 160)
res["tiefe"] = timings(j); log(f" 13k : {res['tiefe']}")
tc = []
for q, want_fn, want_arg in TOOL_TASKS:
try:
j = chat(base, model, [{"role": "user", "content": q}], 1500, TOOLS)
msg = j["choices"][0]["message"]
calls = msg.get("tool_calls") or []
ok = any(c["function"]["name"] == want_fn and re.search(want_arg, c["function"].get("arguments", "")) for c in calls)
raw = "" if calls else (msg.get("content") or "")[:120].replace("\n", " ")
tc.append({"task": want_fn, "ok": ok, "calls": [(c["function"]["name"], c["function"].get("arguments", "")[:80]) for c in calls], "raw": raw})
except Exception as e:
tc.append({"task": want_fn, "ok": False, "error": str(e)[:120]})
res["tools_direkt"] = tc
log(f" tools: {sum(1 for t in tc if t['ok'])}/3 " + "; ".join(f"{t['task']}={'' if t['ok'] else ''}" for t in tc))
for t in tc:
if not t["ok"]:
log(f" {t['task']}: calls={t.get('calls')} raw={t.get('raw', '')!r} {t.get('error', '')}")
j = chat(base, model, [{"role": "user", "content": "Antworte auf Deutsch in genau drei Sätzen: Warum braucht ein Heimserver ein Backup?"}], 300, think=False)
m = j["choices"][0]["message"]
txt = (m.get("content") or "").strip() or ("[nur reasoning] " + (m.get("reasoning_content") or "")[:200])
res["deutsch"] = txt[:400]; log(f" de : {txt[:160]!r}")
j = chat(base, model, [{"role": "user", "content": 'Antworte NUR mit JSON, ohne Erklärung: {"modell": "<dein Name>", "ok": true}'}], 120, think=False)
m = j["choices"][0]["message"]
txt = (m.get("content") or "").strip()
try:
json.loads(txt[txt.find("{"): txt.rfind("}") + 1]); js = True
except Exception:
js = False
res["json_ok"] = js; log(f" json : {'' if js else ''} {txt[:80]!r}")
def probes(basis: str, modell: str, label: str, with_hermes: bool, hermes_args: list[str], bild: bool = False) -> None:
werte = {"label": label, **ps.pruefe("hirn", basis, modell, bild=bild)}
if with_hermes:
hz = []
for q, want in [("Führe im Terminal exakt den Befehl echo pruefstand-ok aus und gib mir nur dessen Ausgabe zurück.", "pruefstand-ok"),
("Welche Verzeichnisse liegen direkt unter /srv/models? Nenne mir drei davon.", "GGUF")]:
t0 = time.time()
try:
p = subprocess.run([HERMES, "chat", *hermes_args, "-Q", "--oneshot", "--max-turns", "6", "-q", q],
capture_output=True, text=True, timeout=900)
out = (p.stdout + p.stderr)[-600:]
except subprocess.TimeoutExpired:
out = "TIMEOUT"
ok = want.lower() in out.lower()
hz.append({"ok": ok, "s": round(time.time() - t0), "tail": out[-160:].replace("\n", " ")})
log(f" hermes: {'' if ok else ''} {round(time.time() - t0)} s {out[-120:].replace(chr(10), ' ')!r}")
res["hermes"] = hz
RESULTS.append(res); save()
werte["hermes"] = ps.hermes_smoke(hermes_args)
RESULTS.append(werte)
save()
def start_server(model, extra):
cmd = [BIN, "-m", model, "--host", "127.0.0.1", "--port", str(PORT), *BASE, *extra]
logf = open("/tmp/pruefstand-server.log", "w")
p = subprocess.Popen(cmd, stdout=logf, stderr=subprocess.STDOUT)
t0 = time.time()
for _ in range(150):
time.sleep(2)
if p.poll() is not None:
tail = open("/tmp/pruefstand-server.log", errors="replace").read()[-500:]
return None, f"LOAD-CRASH rc={p.returncode}: {tail.strip()[-300:]}"
try:
if b"ok" in urllib.request.urlopen(f"http://127.0.0.1:{PORT}/health", timeout=2).read():
return p, round(time.time() - t0)
except Exception:
pass
p.kill()
return None, "TIMEOUT beim Laden"
def candidate(label, model, extra, with_hermes):
log(f"\n=== {label}"); log(f" {os.path.basename(model)} {' '.join(extra)}")
if not os.path.exists(model):
log(" FEHLT: Datei nicht da"); RESULTS.append({"label": label, "fehler": "Datei fehlt"}); save(); return
p, info = start_server(model, extra)
if p is None:
log(f" {info}"); RESULTS.append({"label": label, "fehler": info}); save(); return
log(f" geladen in {info} s")
def candidate(label: str, model: str, extra: list[str], with_hermes: bool, mmproj: str | None = None) -> None:
ps.log(f"\n=== {label}")
ps.log(f" {os.path.basename(model)} {' '.join(extra)}")
try:
probes(f"http://127.0.0.1:{PORT}", "kandidat", label, with_hermes, ["--provider", "pruefstand", "-m", "kandidat"])
server = ps.starte_server(model, mmproj=mmproj, flags=extra, ctx=ps.CTX_STANDARD)
except ps.ServerFehler as e:
ps.log(f" {e}")
RESULTS.append({"label": label, "fehler": str(e)[:300]})
save()
return
try:
probes(ps.KANDIDAT_URL, "kandidat", label, with_hermes, ["--provider", "pruefstand", "-m", "kandidat"],
bild=bool(mmproj))
except Exception as e:
log(f" FEHLER in Proben: {e}"); RESULTS.append({"label": label, "fehler": str(e)[:200]}); save()
ps.log(f" FEHLER in Proben: {e}")
RESULTS.append({"label": label, "fehler": str(e)[:200]})
save()
finally:
p.terminate()
try:
p.wait(timeout=60)
except Exception:
p.kill()
ps.stoppe_server(server)
time.sleep(3)
which = set(sys.argv[1:]) or {"baseline", "nemo", "u38", "u36"}
if "baseline" in which:
log("\n=== BASELINE fast (live, llama-swap)"); probes("http://127.0.0.1:8080", "fast", "fast-live", True, ["-m", "fast"])
log("\n=== BASELINE coder = Qwen3.8-27B+DFlash2 (live)"); probes("http://127.0.0.1:8080", "coder", "coder-live", True, ["-m", "coder"])
if "nemo" in which:
candidate("Nemotron 3.5 Lightning · MTP n3", NEMO, ["--spec-type", "draft-mtp", "--spec-draft-model", NEMO_MTP, "--spec-draft-n-max", "3"], True)
candidate("Nemotron 3.5 Lightning · ohne Spec", NEMO, [], False)
if "u38" in which:
candidate("Qwen3.8-27B-Uncensored · MTP fused n2", U38, ["--spec-type", "draft-mtp", "--spec-draft-n-max", "2"], True)
candidate("Qwen3.8-27B-Uncensored · DFlash2 (z-lab) n3", U38, ["--spec-type", "draft-dflash", "--spec-draft-model", DFLASH2_38, "--spec-draft-n-max", "3"], False)
candidate("Qwen3.8-27B-Uncensored · ohne Spec", U38, [], False)
if "u36" in which:
candidate("Qwen3.6-35B-A3B-Uncensored-Aggressive · DFlash (giocom)", U36, ["--spec-type", "draft-dflash", "--spec-draft-model", DFLASH_36], True)
candidate("Qwen3.6-35B-A3B-Uncensored-Aggressive · ohne Spec", U36, [], False)
log("\nPRUEFSTAND_DONE")
def main() -> None:
which = set(sys.argv[1:]) or {"baseline", "nemo", "u38", "u36"}
if "baseline" in which:
ps.log("\n=== BASELINE fast (live, llama-swap)")
probes(ps.SWAP_URL, "fast", "fast-live", True, ["-m", "fast"])
ps.log("\n=== BASELINE coder (live, llama-swap)")
probes(ps.SWAP_URL, "coder", "coder-live", True, ["-m", "coder"], bild=True)
if "nemo" in which:
candidate("Nemotron 3.5 Lightning · MTP n3", NEMO,
ps.draft_flags(NEMO_MTP, "draft-mtp", 3), True)
candidate("Nemotron 3.5 Lightning · ohne Spec", NEMO, [], False)
if "u38" in which:
candidate("Qwen3.8-27B · MTP fused n2", U38, ps.draft_flags(None, "draft-mtp", 2), True)
candidate("Qwen3.8-27B · DFlash2 (z-lab) n3", U38, ps.draft_flags(DFLASH2_38, "draft-dflash", 3), False)
candidate("Qwen3.8-27B · ohne Spec", U38, [], False)
if "u36" in which:
candidate("Qwen3.6-35B-A3B · DFlash (giocom)", U36, ps.draft_flags(DFLASH_36, "draft-dflash"), True)
candidate("Qwen3.6-35B-A3B · ohne Spec", U36, [], False)
ps.log("\nPRUEFSTAND_DONE")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff