boxwart: Modell-Radar sucht, testet nachts selbst und empfiehlt (Hirn und Coder)
Ampel / ampel (push) Failing after 21s
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:
co-authored by
Claude Opus 5.5
parent
4b226d5d22
commit
177c9a311c
+63
-167
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user