Qwen3.8-27B-MTP-GGUF / bench /bench_skills.py
Davidmg0815's picture
Upload bench/bench_skills.py with huggingface_hub
e7328a2 verified
Raw
History Blame
14.5 kB
#!/usr/bin/env python3
"""Faehigkeits-Benchmark: Terminal und Programmierung, gegen einen llama-server.
Bewertet wird ausschliesslich maschinell:
* Terminal-Aufgaben — der gelieferte Shell-Einzeiler laeuft in einer
Wegwerf-Sandbox, geprueft wird die tatsaechliche Ausgabe bzw. der
Dateizustand danach.
* Code-Aufgaben — die gelieferte Funktion laeuft gegen feste Testfaelle.
Kein Modell benotet ein anderes. Wo etwas nicht laeuft, gilt es als
durchgefallen, und der Grund wird protokolliert.
./bench_skills.py --port 8290 --modell "Qwen3.8-27B Q8_0" --out ergebnis.json
"""
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import tasks_terminal
import tasks_code
# Wird aus main() gesetzt. Steuert, ob der Denkblock zugelassen wird.
REASONING = False
# Muster, die eine Shell-Antwort sofort disqualifizieren. Der Befehl laeuft zwar
# in einer Sandbox, aber ein 'rm -rf /' oder ein sudo hat dort trotzdem nichts
# verloren — das waere ein Treffer gegen das echte System.
GEFAEHRLICH = re.compile(
r"(^|[;&|`(\s])(sudo|doas|su)\s"
r"|rm\s+(-[a-zA-Z]*\s+)*(/|/\*|~|\$HOME)(\s|$)"
r"|mkfs|fdisk|dd\s+[^|]*of=/dev/"
r"|>\s*/dev/(sd|nvme)"
r"|shutdown|reboot|halt|init\s+0"
r"|:\(\)\s*\{.*\}\s*;\s*:" # Fork-Bombe
r"|curl[^|]*\|\s*(ba)?sh|wget[^|]*\|\s*(ba)?sh"
r"|chmod\s+-R\s+777\s+/"
r"|systemctl|pkill|killall"
)
def frage(port, prompt, max_tokens, timeout):
"""Eine Anfrage an den Server. Liefert (text, messwerte, fehler).
messwerte enthaelt beide Token-Raten: die Generierrate (was der Nutzer als
Tempo empfindet) und die Prompt-Rate (wie schnell die Eingabe verarbeitet
wird). Nur die Generierrate zu melden verschweigt die halbe Wahrheit —
bei langen Eingaben dominiert die Prompt-Phase die Wartezeit."""
b = {
"messages": [{"role": "user", "content": prompt}],
"max_tokens": max_tokens,
"cache_prompt": False,
"temperature": 0.2,
"top_p": 0.9,
}
if not REASONING:
# NUR ueber chat_template_kwargs. Das Feld "enable_thinking" auf
# oberster Ebene wird von llama.cpp nicht an die Chat-Vorlage
# durchgereicht und bleibt wirkungslos — gemessen: 825 Zeichen
# Denkblock trotz enable_thinking=false, gegen 0 Zeichen hiermit.
b["chat_template_kwargs"] = {"enable_thinking": False}
body = json.dumps(b).encode()
req = urllib.request.Request(
f"http://127.0.0.1:{port}/v1/chat/completions",
data=body, headers={"Content-Type": "application/json"})
t0 = time.time()
leer = dict(tps=0.0, prompt_tps=0.0, tokens=0, prompt_tokens=0,
sekunden=0.0, ttft=0.0)
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
d = json.load(r)
except Exception as e:
leer["sekunden"] = time.time() - t0
return "", leer, f"{type(e).__name__}: {e}"
sek = time.time() - t0
m = d.get("choices", [{}])[0].get("message", {})
txt = m.get("content") or ""
denken = m.get("reasoning_content") or ""
t = d.get("timings", {})
return txt, dict(
tps=t.get("predicted_per_second") or 0.0,
prompt_tps=t.get("prompt_per_second") or 0.0,
tokens=t.get("predicted_n") or 0,
prompt_tokens=t.get("prompt_n") or 0,
# Zeichen statt Token, weil der Server den Denkblock nicht getrennt
# zaehlt. Verhaeltnis Denken/Antwort ist damit trotzdem ablesbar.
denk_zeichen=len(denken),
antwort_zeichen=len(txt),
abbruch=d.get("choices", [{}])[0].get("finish_reason") or "",
# Zeit bis zum ersten Token: die Prompt-Phase in Millisekunden.
ttft=round((t.get("prompt_ms") or 0.0) / 1000.0, 3),
sekunden=round(sek, 2),
), ""
def schaele_code(text):
"""Holt den Code aus der Antwort. Modelle liefern mal mit Zaun, mal ohne,
mal mit Vorrede — alle drei Faelle muessen durchgehen, sonst misst man
Formatierungsdisziplin statt Koennen."""
zaun = re.findall(r"```(?:python|py|bash|sh|shell)?\s*\n(.*?)```",
text, re.S)
if zaun:
return max(zaun, key=len).strip()
# Kein Zaun: alles nach einer etwaigen Vorrede nehmen.
return text.strip()
def schaele_befehl(text):
"""Holt den Shell-Einzeiler. Nimmt die erste Zeile, die nach Befehl aussieht."""
roh = schaele_code(text)
zeilen = [z.strip() for z in roh.splitlines() if z.strip()]
zeilen = [z for z in zeilen if not z.startswith("#")]
if not zeilen:
return ""
# Ein '$' oder '> ' am Anfang ist Prompt-Deko, kein Befehl.
z = zeilen[0]
z = re.sub(r"^\s*(\$|>|%)\s+", "", z)
# Mehrzeilige Befehle mit Backslash-Fortsetzung zusammensetzen.
if z.endswith("\\") and len(zeilen) > 1:
z = " ".join(x.rstrip("\\").strip() for x in zeilen)
return z.strip()
def baue_sandbox():
d = tempfile.mkdtemp(prefix="bench-sandbox-")
for name, inhalt in tasks_terminal.SANDBOX.items():
with open(os.path.join(d, name), "w") as f:
f.write(inhalt)
for name, groesse in tasks_terminal.SANDBOX_SIZED.items():
with open(os.path.join(d, name), "wb") as f:
f.write(b"\0" * groesse)
return d
def lauf_terminal(aufgabe, befehl):
"""Fuehrt den Befehl in einer frischen Sandbox aus. Liefert (bestanden, grund)."""
if not befehl:
return False, "leere Antwort"
if GEFAEHRLICH.search(befehl):
return False, "abgelehnt: gefaehrliches Muster"
d = baue_sandbox()
try:
p = subprocess.run(["bash", "-c", befehl], cwd=d, timeout=15,
capture_output=True, text=True, errors="replace")
out = p.stdout
if aufgabe.get("datei_check"):
dateien = {}
for name in tasks_terminal.SANDBOX:
pfad = os.path.join(d, name)
if os.path.isfile(pfad):
dateien[name] = open(pfad, errors="replace").read()
ok = bool(aufgabe["datei_check"](dateien))
return ok, "" if ok else f"Dateizustand falsch (rc={p.returncode})"
if p.returncode != 0 and not out.strip():
return False, f"rc={p.returncode}: {p.stderr.strip()[:80]}"
try:
ok = bool(aufgabe["check"](out))
except Exception as e:
return False, f"Ausgabe unbrauchbar: {type(e).__name__}"
return ok, "" if ok else f"Ausgabe falsch: {out.strip()[:60]!r}"
except subprocess.TimeoutExpired:
return False, "Zeitueberschreitung (15s)"
except Exception as e:
return False, f"{type(e).__name__}: {e}"
finally:
shutil.rmtree(d, ignore_errors=True)
# Wird als eigener Prozess gestartet, damit eine Endlosschleife oder ein
# Absturz im Modellcode den Benchmark nicht mitreisst.
RUNNER = r'''
import json, sys
quelle = sys.stdin.read()
auftrag = json.loads(quelle)
ns = {}
ergebnisse = []
try:
exec(auftrag["code"], ns)
except Exception as e:
print(json.dumps({"fehler": "Code laedt nicht: %s: %s" % (type(e).__name__, e)}))
sys.exit(0)
for ausdruck, erwartet in auftrag["tests"]:
try:
lokal = dict(ns)
# ERG als Sentinel, nicht "_r": Funktionsnamen wie suche_rotiert
# enthalten "_r" und landeten sonst faelschlich im exec-Zweig.
if "ERG" in ausdruck:
exec(ausdruck, lokal)
wert = lokal.get("ERG")
else:
wert = eval(ausdruck, lokal)
# Tupel und Listen gelten als gleich, wenn der Inhalt stimmt —
# die Aufgabe schreibt die Behaeltersorte nicht vor.
def norm(x):
if isinstance(x, (list, tuple)):
return [norm(i) for i in x]
return x
ergebnisse.append(norm(wert) == norm(erwartet))
except Exception as e:
ergebnisse.append(False)
print(json.dumps({"ergebnisse": ergebnisse}))
'''
def lauf_code(aufgabe, code):
"""Laesst den gelieferten Code gegen die Testfaelle laufen."""
if not code.strip():
return False, 0, len(aufgabe["tests"]), "leere Antwort"
auftrag = json.dumps({"code": code, "tests": aufgabe["tests"]})
try:
p = subprocess.run([sys.executable, "-c", RUNNER], input=auftrag,
capture_output=True, text=True, timeout=30)
except subprocess.TimeoutExpired:
return False, 0, len(aufgabe["tests"]), "Zeitueberschreitung (30s)"
try:
d = json.loads(p.stdout.strip().splitlines()[-1])
except Exception:
return False, 0, len(aufgabe["tests"]), f"Runner stumm: {p.stderr.strip()[:70]}"
if "fehler" in d:
return False, 0, len(aufgabe["tests"]), d["fehler"]
e = d["ergebnisse"]
return all(e), sum(e), len(e), "" if all(e) else f"{sum(e)}/{len(e)} Testfaelle"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", type=int, default=8290)
ap.add_argument("--modell", required=True, help="Anzeigename fuer die Tabelle")
ap.add_argument("--out", required=True)
ap.add_argument("--max-tokens", type=int, default=900)
ap.add_argument("--timeout", type=int, default=300)
ap.add_argument("--nur", default="", help="term|code, sonst beides")
ap.add_argument("--reasoning", action="store_true",
help="Denkblock zulassen (sonst per chat_template_kwargs aus)")
args = ap.parse_args()
global REASONING
REASONING = args.reasoning
print(f"Reasoning: {'AN' if REASONING else 'AUS'}, "
f"max_tokens={args.max_tokens}", file=sys.stderr)
bericht = {"modell": args.modell, "reasoning": REASONING,
"max_tokens": args.max_tokens,
"terminal": [], "code": [], "messungen": []}
if args.nur in ("", "term"):
print(f"--- Terminal ({len(tasks_terminal.TASKS)} Aufgaben) ---",
file=sys.stderr)
for a in tasks_terminal.TASKS:
txt, m, fehler = frage(args.port, a["prompt"],
args.max_tokens, args.timeout)
if fehler:
bericht["terminal"].append(
dict(id=a["id"], titel=a["titel"], ok=False, grund=fehler,
antwort=""))
print(f" {a['id']} {a['titel']:<26} SERVERFEHLER {fehler[:50]}",
file=sys.stderr)
continue
befehl = schaele_befehl(txt)
ok, grund = lauf_terminal(a, befehl)
bericht["terminal"].append(
dict(id=a["id"], titel=a["titel"], ok=ok, grund=grund,
antwort=befehl[:200], **m))
bericht["messungen"].append(m)
print(f" {a['id']} {a['titel']:<26} {'OK ' if ok else 'FEHL'} "
f"{m['tps']:5.1f} t/s {m['tokens']:4d} Tok {grund[:45]}",
file=sys.stderr)
if args.nur in ("", "code"):
print(f"--- Programmierung ({len(tasks_code.TASKS)} Aufgaben) ---",
file=sys.stderr)
for a in tasks_code.TASKS:
txt, m, fehler = frage(args.port, a["prompt"],
args.max_tokens, args.timeout)
if fehler:
bericht["code"].append(
dict(id=a["id"], titel=a["titel"], ok=False, grund=fehler,
bestanden=0, gesamt=len(a["tests"])))
print(f" {a['id']} {a['titel']:<26} SERVERFEHLER {fehler[:50]}",
file=sys.stderr)
continue
code = schaele_code(txt)
ok, best, ges, grund = lauf_code(a, code)
bericht["code"].append(
dict(id=a["id"], titel=a["titel"], ok=ok, grund=grund,
bestanden=best, gesamt=ges, code=code[:600], **m))
bericht["messungen"].append(m)
print(f" {a['id']} {a['titel']:<26} {'OK ' if ok else 'FEHL'} "
f"{m['tps']:5.1f} t/s {m['tokens']:4d} Tok "
f"{best}/{ges} {grund[:35]}", file=sys.stderr)
t, c, mm = bericht["terminal"], bericht["code"], bericht["messungen"]
tps = sorted(x["tps"] for x in mm if x["tps"] > 0)
ptps = sorted(x["prompt_tps"] for x in mm if x["prompt_tps"] > 0)
med = lambda v: v[len(v) // 2] if v else 0.0
bericht["summe"] = {
"terminal_ok": sum(1 for x in t if x["ok"]), "terminal_n": len(t),
"code_ok": sum(1 for x in c if x["ok"]), "code_n": len(c),
"code_testfaelle_ok": sum(x.get("bestanden", 0) for x in c),
"code_testfaelle_n": sum(x.get("gesamt", 0) for x in c),
"tps_median": round(med(tps), 1),
"tps_min": round(tps[0], 1) if tps else 0.0,
"tps_max": round(tps[-1], 1) if tps else 0.0,
"prompt_tps_median": round(med(ptps), 1),
"tokens_gesamt": sum(x["tokens"] for x in mm),
"sekunden_gesamt": round(sum(x["sekunden"] for x in mm), 1),
"anfragen": len(mm),
# Der Preis des Denkens: wie viele Token im Schnitt, und wie viel davon
# Grubelei statt Antwort war. Ein Lauf, der ans Token-Limit stoesst,
# hat die Antwort nie ausgegeben — deshalb wird das mitgezaehlt.
"tokens_je_aufgabe": round(sum(x["tokens"] for x in mm) / len(mm), 1)
if mm else 0,
"denk_zeichen_gesamt": sum(x.get("denk_zeichen", 0) for x in mm),
"antwort_zeichen_gesamt": sum(x.get("antwort_zeichen", 0) for x in mm),
"am_limit": sum(1 for x in mm if x.get("abbruch") == "length"),
}
with open(args.out, "w") as f:
json.dump(bericht, f, ensure_ascii=False, indent=1)
s = bericht["summe"]
print(f"\n{args.modell} [Reasoning {'AN' if REASONING else 'AUS'}]: "
f"Terminal {s['terminal_ok']}/{s['terminal_n']}, "
f"Code {s['code_ok']}/{s['code_n']} "
f"({s['code_testfaelle_ok']}/{s['code_testfaelle_n']} Testfaelle), "
f"{s['tps_median']:.1f} t/s Median "
f"({s['tps_min']:.1f}-{s['tps_max']:.1f}), "
f"Prompt {s['prompt_tps_median']:.0f} t/s, "
f"{s['tokens_gesamt']} Token in {s['sekunden_gesamt']}s, "
f"{s['tokens_je_aufgabe']:.0f} Token/Aufgabe, "
f"{s['am_limit']} am Limit abgeschnitten", file=sys.stderr)
print(f"geschrieben: {args.out}", file=sys.stderr)
if __name__ == "__main__":
main()