diff --git a/deploy/bench/pruefstand-hirn.py b/deploy/bench/pruefstand-hirn.py new file mode 100644 index 0000000..71f8812 --- /dev/null +++ b/deploy/bench/pruefstand-hirn.py @@ -0,0 +1,193 @@ +#!/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-.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": "", "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") diff --git a/deploy/llama-swap.config.yaml b/deploy/llama-swap.config.yaml index 00d387c..402e6a8 100644 --- a/deploy/llama-swap.config.yaml +++ b/deploy/llama-swap.config.yaml @@ -9,8 +9,11 @@ models: # Q8_0 kostet 0 t/s, MTP n-max 3 = +26% vs. ohne Spec. (Q4_K-KV stand mal im Template, # lief aber NIE live — bewusst bei Q8_0 geblieben: Qualität vor ein paar GB, seit der # Warm-Set-Diät 07.07. ist RAM nicht mehr der Engpass.) + # 17.09.2026: Uncensored-Variante (HauhauCS Aggressive, Q4_K_M) mit demselben giocom-DFlash-Draft. Prüfstand: 100,8 t/s + # @13k (Original 92,8), Prefill 13,1 s, Tools 11/12 (1 Denk-Ausreißer), Hermes-Smoke grün. Original bleibt in + # Qwen3.6-35B-A3B-MTP-GGUF liegen — Rückweg = Pfad zurück. cmd: | - llama-server -m /srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --load-mode none --jinja --parallel 2 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-dflash --spec-draft-model /srv/models/Qwen3.6-35B-A3B-DFlash-GGUF/giocom-Qwen3.6-35B-A3B-DFlash-Q8_0.gguf + llama-server -m /srv/models/Qwen3.6-35B-A3B-Uncensored-GGUF/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --load-mode none --jinja --parallel 2 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-dflash --spec-draft-model /srv/models/Qwen3.6-35B-A3B-DFlash-GGUF/giocom-Qwen3.6-35B-A3B-DFlash-Q8_0.gguf ttl: 0 aliases: - hermes @@ -27,8 +30,11 @@ models: # Unser neuer Haupt-Coder mit 262k Kontext, Thinking Mode und extrem sauberer Code-Qualität. # DFlash2-Draft (z-lab, Q4_K_M) seit 04.09.2026: gemessen 12,6 -> 31 t/s (Akzeptanz 0,78, n-max 3 default; # n-max 5 und f16-KV bringen nichts, Q4-Draft = Q8-Draft). Braucht llama.cpp >= 28.08. (PR 27342 + Vulkan-Fix 27812). + # 17.09.2026: Uncensored-Variante (JonathanColetti, Heretic-Abliteration: KL 0,12, Refusals 98->12/100, Benchmarks -0,5) + # mit demselben DFlash2-Draft (Akzeptanz 0,82 @13k). Prüfstand: 30,4 t/s @13k (Original 26,2), Tools 6/6, Coding 3/3, + # Hermes-Smoke grün. Original bleibt in Qwen3.8-27B-GGUF liegen — Rückweg = beide Pfade zurück. cmd: | - llama-server -m /srv/models/Qwen3.8-27B-GGUF/Qwen3.8-27B-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --load-mode none --mmproj /srv/models/Qwen3.8-27B-GGUF/mmproj-BF16.gguf --jinja --parallel 1 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-dflash --spec-draft-model /srv/models/Qwen3.8-27B-DFlash2-GGUF/Qwen3.8-27B-DFlash2-Q4_K_M.gguf + llama-server -m /srv/models/Qwen3.8-27B-Uncensored-GGUF/Qwen3.8-27B-Uncensored-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --load-mode none --mmproj /srv/models/Qwen3.8-27B-Uncensored-GGUF/mmproj-Qwen3.8-27B-Uncensored-F16.gguf --jinja --parallel 1 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-dflash --spec-draft-model /srv/models/Qwen3.8-27B-DFlash2-GGUF/Qwen3.8-27B-DFlash2-Q4_K_M.gguf ttl: 5400 aliases: - coder diff --git a/docs/wissen/STACK.md b/docs/wissen/STACK.md index c868837..2b3233c 100644 --- a/docs/wissen/STACK.md +++ b/docs/wissen/STACK.md @@ -49,15 +49,22 @@ Qwen3.6-35B-A3B + Qwen3-Embedding-0.6B + Qwen3-Reranker-0.6B. Sonst NICHTS dauer | Alias/Rolle | Modell | ttl | Notizen | |---|---|---|---| -| `hermes` + `fast` | Qwen3.6-35B-A3B (UD-Q4_K_M, DFlash) | 0 | Agent-Hirn. `-c 131072 --parallel 2` → **65536/Slot**, KV **q8_0**, ~70–90 t/s | -| `coder` | Qwen3.8-27B (Q4_K_M, mmproj BF16) | 5400 | **131072 ctx** — `--parallel 1`, der Coder bekommt den ganzen Slot (34a9862). Multimodal, **~32 t/s mit DFlash2-Draft** (17.09.), ohne Draft 12,7 (dicht = bandbreitengebunden) | +| `hermes` + `fast` | Qwen3.6-35B-A3B **Uncensored** (HauhauCS Aggressive, Q4_K_M, giocom-DFlash) — seit 17.09.2026 | 0 | Agent-Hirn. `-c 131072 --parallel 2` → **65536/Slot**, KV **q8_0**, ~94–101 t/s (Prüfstand 17.09.). Original (MTP-GGUF UD-Q4_K_M) liegt daneben, Rückweg = Pfad | +| `coder` | Qwen3.8-27B **Uncensored** (JonathanColetti/Heretic, Q4_K_M, eigene mmproj F16) — seit 17.09.2026 | 5400 | **131072 ctx** — `--parallel 1`, der Coder bekommt den ganzen Slot (34a9862). Multimodal, **~32 t/s mit DFlash2-Draft** (17.09.), ohne Draft 12,7 (dicht = bandbreitengebunden) | | `debugger` | Muse-Glimmer-30B (Q4_K_XL, DFlash) | 600 | 65536 ctx; Runtime-Diagnostik & Fehler-Debugger (multimodal) | | `vision` | Qwen3-VL-30B-A3B-Instruct (Q4_K_M) | 900 | 32768 ctx; Multimodal-Augen (On-Demand) | | `heavy` | **= Qwen3.8-27B** (zweiter Alias des Coders, seit 04.09.) | 5400 | Planen/Review (OpenChamber) + chat-Lane des Gateways. gpt-oss-120b (60 GB, AA-Index 24 vs 52) liegt ohne Rolle als Rollback bereit, direkt als `gpt-oss-120b` ansprechbar | | `reranker` | Qwen3-Reranker-0.6B (q8_0, Mungert) | 0 | Sortiert Suchtreffer nach echter Relevanz (`/v1/rerank`) | | `embed` | Qwen3-Embedding-0.6B (f16) | 0 | Vektorisierung für Suche/Sortierung | -> **Diese sieben sind ALLES** — abgeglichen mit `/etc/llama-swap/config.yaml` am 27.08.2026. +> **Diese sieben sind ALLES** — abgeglichen mit `/etc/llama-swap/config.yaml` am 27.08.2026 (Pfade 17.09.2026). +> +> **Prüfstand 17.09.2026** (`deploy/bench/pruefstand-hirn.py`; Kandidat auf `:5899`, Hermes spricht per `--provider pruefstand` +> aus `providers:` der Hermes-Config dagegen — Live-Stack bleibt unberührt): Uncensored-3.6 = 100,8 t/s @13k (Original 92,8), +> Tools 11/12, Hermes-Smoke grün · Uncensored-27B = 30,4 t/s @13k (Original 26,2), Tools 6/6, Coding 3/3 · **Nemotron 3.5 Lightning +> 30B-A3B** (Q4_0 + MTP-Draft, `/srv/models/Nemotron-3.5-Lightning-30B-A3B-GGUF/`) = Hirn-Klasse: 10,5 s Prefill @13k, 91,8 t/s, +> Tools 3/3, Hermes grün — liegt als Reserve-Kandidat bereit, nicht besetzt. ‼ **Bild + DFlash2 = HTTP 500** („failed to process +> speculative batch") beim Coder — Vorbestand seit 04.09., unabhängig vom Modell; Bilder laufen über die Gateway-Bildweiche zu `vision`. > Frühere Tabellen führten zusätzlich `kritiker` (Devstral-Small-2-24B), `scout` (GLM-4.6V-Flash), > `dense-planer` und `doctor`. Die gibt es nicht mehr: die Modell-Konsolidierung (`9a35be9`, 19.08.) > warf sie aus llama-swap, und `fremdblick.sh` — der einzige Aufrufer des Kritikers — fiel mit dem