Ampel / ampel (push) Failing after 24s
Pruefstand 17.09. (deploy/bench/pruefstand-hirn.py, Kandidat auf :5899, Hermes-Tool-Smoke per --provider pruefstand durch den echten Agenten): - Qwen3.6-35B-A3B-Uncensored (HauhauCS Aggressive, Q4_K_M) + giocom-DFlash: 100,8 t/s @13k (Original 92,8), Prefill 13,1 s, Tools 11/12 (1 Denk-Ausreisser), Hermes-Smoke gruen -> hermes/fast. - Qwen3.8-27B-Uncensored (Heretic, Q4_K_M, eigene mmproj) + DFlash2: 30,4 t/s @13k (Original 26,2), Tools 6/6, Coding 3/3 ausgefuehrt (= Original), Hermes-Smoke gruen -> coder/heavy. - Nemotron 3.5 Lightning 30B-A3B (Q4_0 + MTP): Hirn-Klasse (10,5 s Prefill @13k, 91,8 t/s, Tools 3/3, Hermes gruen) - liegt als Reserve-Kandidat auf der Box, nicht besetzt. Keys/Aliase/Flags unveraendert, nur Pfade; Originale bleiben liegen (Rueckweg = Pfad zurueck). Nebenbefund: Bild + DFlash2 beim Coder = HTTP 500 "failed to process speculative batch" - Vorbestand seit 04.09., unabhaengig vom Modell (ohne Draft antworten beide "Rot"); Bilder gehen ueber die Gateway-Bildweiche zu vision. Live seit 17.09. ~22:10, Stack- und Gehirn-Check gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
194 lines
11 KiB
Python
194 lines
11 KiB
Python
#!/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)."""
|
|
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"]
|
|
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"
|
|
NEMO = f"{M}/Nemotron-3.5-Lightning-30B-A3B-GGUF/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-Q4_0.gguf"
|
|
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 = []
|
|
|
|
|
|
def log(*a):
|
|
print(*a, flush=True)
|
|
|
|
|
|
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}")
|
|
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()
|
|
|
|
|
|
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")
|
|
try:
|
|
probes(f"http://127.0.0.1:{PORT}", "kandidat", label, with_hermes, ["--provider", "pruefstand", "-m", "kandidat"])
|
|
except Exception as e:
|
|
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()
|
|
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")
|