Davidmg0815 commited on
Commit
e7328a2
·
verified ·
1 Parent(s): 9258a4d

Upload bench/bench_skills.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. bench/bench_skills.py +341 -0
bench/bench_skills.py ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Faehigkeits-Benchmark: Terminal und Programmierung, gegen einen llama-server.
3
+
4
+ Bewertet wird ausschliesslich maschinell:
5
+ * Terminal-Aufgaben — der gelieferte Shell-Einzeiler laeuft in einer
6
+ Wegwerf-Sandbox, geprueft wird die tatsaechliche Ausgabe bzw. der
7
+ Dateizustand danach.
8
+ * Code-Aufgaben — die gelieferte Funktion laeuft gegen feste Testfaelle.
9
+
10
+ Kein Modell benotet ein anderes. Wo etwas nicht laeuft, gilt es als
11
+ durchgefallen, und der Grund wird protokolliert.
12
+
13
+ ./bench_skills.py --port 8290 --modell "Qwen3.8-27B Q8_0" --out ergebnis.json
14
+ """
15
+ import argparse
16
+ import json
17
+ import os
18
+ import re
19
+ import shutil
20
+ import subprocess
21
+ import sys
22
+ import tempfile
23
+ import time
24
+ import urllib.request
25
+
26
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
27
+ import tasks_terminal
28
+ import tasks_code
29
+
30
+ # Wird aus main() gesetzt. Steuert, ob der Denkblock zugelassen wird.
31
+ REASONING = False
32
+
33
+ # Muster, die eine Shell-Antwort sofort disqualifizieren. Der Befehl laeuft zwar
34
+ # in einer Sandbox, aber ein 'rm -rf /' oder ein sudo hat dort trotzdem nichts
35
+ # verloren — das waere ein Treffer gegen das echte System.
36
+ GEFAEHRLICH = re.compile(
37
+ r"(^|[;&|`(\s])(sudo|doas|su)\s"
38
+ r"|rm\s+(-[a-zA-Z]*\s+)*(/|/\*|~|\$HOME)(\s|$)"
39
+ r"|mkfs|fdisk|dd\s+[^|]*of=/dev/"
40
+ r"|>\s*/dev/(sd|nvme)"
41
+ r"|shutdown|reboot|halt|init\s+0"
42
+ r"|:\(\)\s*\{.*\}\s*;\s*:" # Fork-Bombe
43
+ r"|curl[^|]*\|\s*(ba)?sh|wget[^|]*\|\s*(ba)?sh"
44
+ r"|chmod\s+-R\s+777\s+/"
45
+ r"|systemctl|pkill|killall"
46
+ )
47
+
48
+
49
+ def frage(port, prompt, max_tokens, timeout):
50
+ """Eine Anfrage an den Server. Liefert (text, messwerte, fehler).
51
+
52
+ messwerte enthaelt beide Token-Raten: die Generierrate (was der Nutzer als
53
+ Tempo empfindet) und die Prompt-Rate (wie schnell die Eingabe verarbeitet
54
+ wird). Nur die Generierrate zu melden verschweigt die halbe Wahrheit —
55
+ bei langen Eingaben dominiert die Prompt-Phase die Wartezeit."""
56
+ b = {
57
+ "messages": [{"role": "user", "content": prompt}],
58
+ "max_tokens": max_tokens,
59
+ "cache_prompt": False,
60
+ "temperature": 0.2,
61
+ "top_p": 0.9,
62
+ }
63
+ if not REASONING:
64
+ # NUR ueber chat_template_kwargs. Das Feld "enable_thinking" auf
65
+ # oberster Ebene wird von llama.cpp nicht an die Chat-Vorlage
66
+ # durchgereicht und bleibt wirkungslos — gemessen: 825 Zeichen
67
+ # Denkblock trotz enable_thinking=false, gegen 0 Zeichen hiermit.
68
+ b["chat_template_kwargs"] = {"enable_thinking": False}
69
+ body = json.dumps(b).encode()
70
+ req = urllib.request.Request(
71
+ f"http://127.0.0.1:{port}/v1/chat/completions",
72
+ data=body, headers={"Content-Type": "application/json"})
73
+ t0 = time.time()
74
+ leer = dict(tps=0.0, prompt_tps=0.0, tokens=0, prompt_tokens=0,
75
+ sekunden=0.0, ttft=0.0)
76
+ try:
77
+ with urllib.request.urlopen(req, timeout=timeout) as r:
78
+ d = json.load(r)
79
+ except Exception as e:
80
+ leer["sekunden"] = time.time() - t0
81
+ return "", leer, f"{type(e).__name__}: {e}"
82
+ sek = time.time() - t0
83
+ m = d.get("choices", [{}])[0].get("message", {})
84
+ txt = m.get("content") or ""
85
+ denken = m.get("reasoning_content") or ""
86
+ t = d.get("timings", {})
87
+ return txt, dict(
88
+ tps=t.get("predicted_per_second") or 0.0,
89
+ prompt_tps=t.get("prompt_per_second") or 0.0,
90
+ tokens=t.get("predicted_n") or 0,
91
+ prompt_tokens=t.get("prompt_n") or 0,
92
+ # Zeichen statt Token, weil der Server den Denkblock nicht getrennt
93
+ # zaehlt. Verhaeltnis Denken/Antwort ist damit trotzdem ablesbar.
94
+ denk_zeichen=len(denken),
95
+ antwort_zeichen=len(txt),
96
+ abbruch=d.get("choices", [{}])[0].get("finish_reason") or "",
97
+ # Zeit bis zum ersten Token: die Prompt-Phase in Millisekunden.
98
+ ttft=round((t.get("prompt_ms") or 0.0) / 1000.0, 3),
99
+ sekunden=round(sek, 2),
100
+ ), ""
101
+
102
+
103
+ def schaele_code(text):
104
+ """Holt den Code aus der Antwort. Modelle liefern mal mit Zaun, mal ohne,
105
+ mal mit Vorrede — alle drei Faelle muessen durchgehen, sonst misst man
106
+ Formatierungsdisziplin statt Koennen."""
107
+ zaun = re.findall(r"```(?:python|py|bash|sh|shell)?\s*\n(.*?)```",
108
+ text, re.S)
109
+ if zaun:
110
+ return max(zaun, key=len).strip()
111
+ # Kein Zaun: alles nach einer etwaigen Vorrede nehmen.
112
+ return text.strip()
113
+
114
+
115
+ def schaele_befehl(text):
116
+ """Holt den Shell-Einzeiler. Nimmt die erste Zeile, die nach Befehl aussieht."""
117
+ roh = schaele_code(text)
118
+ zeilen = [z.strip() for z in roh.splitlines() if z.strip()]
119
+ zeilen = [z for z in zeilen if not z.startswith("#")]
120
+ if not zeilen:
121
+ return ""
122
+ # Ein '$' oder '> ' am Anfang ist Prompt-Deko, kein Befehl.
123
+ z = zeilen[0]
124
+ z = re.sub(r"^\s*(\$|>|%)\s+", "", z)
125
+ # Mehrzeilige Befehle mit Backslash-Fortsetzung zusammensetzen.
126
+ if z.endswith("\\") and len(zeilen) > 1:
127
+ z = " ".join(x.rstrip("\\").strip() for x in zeilen)
128
+ return z.strip()
129
+
130
+
131
+ def baue_sandbox():
132
+ d = tempfile.mkdtemp(prefix="bench-sandbox-")
133
+ for name, inhalt in tasks_terminal.SANDBOX.items():
134
+ with open(os.path.join(d, name), "w") as f:
135
+ f.write(inhalt)
136
+ for name, groesse in tasks_terminal.SANDBOX_SIZED.items():
137
+ with open(os.path.join(d, name), "wb") as f:
138
+ f.write(b"\0" * groesse)
139
+ return d
140
+
141
+
142
+ def lauf_terminal(aufgabe, befehl):
143
+ """Fuehrt den Befehl in einer frischen Sandbox aus. Liefert (bestanden, grund)."""
144
+ if not befehl:
145
+ return False, "leere Antwort"
146
+ if GEFAEHRLICH.search(befehl):
147
+ return False, "abgelehnt: gefaehrliches Muster"
148
+ d = baue_sandbox()
149
+ try:
150
+ p = subprocess.run(["bash", "-c", befehl], cwd=d, timeout=15,
151
+ capture_output=True, text=True, errors="replace")
152
+ out = p.stdout
153
+ if aufgabe.get("datei_check"):
154
+ dateien = {}
155
+ for name in tasks_terminal.SANDBOX:
156
+ pfad = os.path.join(d, name)
157
+ if os.path.isfile(pfad):
158
+ dateien[name] = open(pfad, errors="replace").read()
159
+ ok = bool(aufgabe["datei_check"](dateien))
160
+ return ok, "" if ok else f"Dateizustand falsch (rc={p.returncode})"
161
+ if p.returncode != 0 and not out.strip():
162
+ return False, f"rc={p.returncode}: {p.stderr.strip()[:80]}"
163
+ try:
164
+ ok = bool(aufgabe["check"](out))
165
+ except Exception as e:
166
+ return False, f"Ausgabe unbrauchbar: {type(e).__name__}"
167
+ return ok, "" if ok else f"Ausgabe falsch: {out.strip()[:60]!r}"
168
+ except subprocess.TimeoutExpired:
169
+ return False, "Zeitueberschreitung (15s)"
170
+ except Exception as e:
171
+ return False, f"{type(e).__name__}: {e}"
172
+ finally:
173
+ shutil.rmtree(d, ignore_errors=True)
174
+
175
+
176
+ # Wird als eigener Prozess gestartet, damit eine Endlosschleife oder ein
177
+ # Absturz im Modellcode den Benchmark nicht mitreisst.
178
+ RUNNER = r'''
179
+ import json, sys
180
+ quelle = sys.stdin.read()
181
+ auftrag = json.loads(quelle)
182
+ ns = {}
183
+ ergebnisse = []
184
+ try:
185
+ exec(auftrag["code"], ns)
186
+ except Exception as e:
187
+ print(json.dumps({"fehler": "Code laedt nicht: %s: %s" % (type(e).__name__, e)}))
188
+ sys.exit(0)
189
+ for ausdruck, erwartet in auftrag["tests"]:
190
+ try:
191
+ lokal = dict(ns)
192
+ # ERG als Sentinel, nicht "_r": Funktionsnamen wie suche_rotiert
193
+ # enthalten "_r" und landeten sonst faelschlich im exec-Zweig.
194
+ if "ERG" in ausdruck:
195
+ exec(ausdruck, lokal)
196
+ wert = lokal.get("ERG")
197
+ else:
198
+ wert = eval(ausdruck, lokal)
199
+ # Tupel und Listen gelten als gleich, wenn der Inhalt stimmt —
200
+ # die Aufgabe schreibt die Behaeltersorte nicht vor.
201
+ def norm(x):
202
+ if isinstance(x, (list, tuple)):
203
+ return [norm(i) for i in x]
204
+ return x
205
+ ergebnisse.append(norm(wert) == norm(erwartet))
206
+ except Exception as e:
207
+ ergebnisse.append(False)
208
+ print(json.dumps({"ergebnisse": ergebnisse}))
209
+ '''
210
+
211
+
212
+ def lauf_code(aufgabe, code):
213
+ """Laesst den gelieferten Code gegen die Testfaelle laufen."""
214
+ if not code.strip():
215
+ return False, 0, len(aufgabe["tests"]), "leere Antwort"
216
+ auftrag = json.dumps({"code": code, "tests": aufgabe["tests"]})
217
+ try:
218
+ p = subprocess.run([sys.executable, "-c", RUNNER], input=auftrag,
219
+ capture_output=True, text=True, timeout=30)
220
+ except subprocess.TimeoutExpired:
221
+ return False, 0, len(aufgabe["tests"]), "Zeitueberschreitung (30s)"
222
+ try:
223
+ d = json.loads(p.stdout.strip().splitlines()[-1])
224
+ except Exception:
225
+ return False, 0, len(aufgabe["tests"]), f"Runner stumm: {p.stderr.strip()[:70]}"
226
+ if "fehler" in d:
227
+ return False, 0, len(aufgabe["tests"]), d["fehler"]
228
+ e = d["ergebnisse"]
229
+ return all(e), sum(e), len(e), "" if all(e) else f"{sum(e)}/{len(e)} Testfaelle"
230
+
231
+
232
+ def main():
233
+ ap = argparse.ArgumentParser()
234
+ ap.add_argument("--port", type=int, default=8290)
235
+ ap.add_argument("--modell", required=True, help="Anzeigename fuer die Tabelle")
236
+ ap.add_argument("--out", required=True)
237
+ ap.add_argument("--max-tokens", type=int, default=900)
238
+ ap.add_argument("--timeout", type=int, default=300)
239
+ ap.add_argument("--nur", default="", help="term|code, sonst beides")
240
+ ap.add_argument("--reasoning", action="store_true",
241
+ help="Denkblock zulassen (sonst per chat_template_kwargs aus)")
242
+ args = ap.parse_args()
243
+
244
+ global REASONING
245
+ REASONING = args.reasoning
246
+ print(f"Reasoning: {'AN' if REASONING else 'AUS'}, "
247
+ f"max_tokens={args.max_tokens}", file=sys.stderr)
248
+
249
+ bericht = {"modell": args.modell, "reasoning": REASONING,
250
+ "max_tokens": args.max_tokens,
251
+ "terminal": [], "code": [], "messungen": []}
252
+
253
+ if args.nur in ("", "term"):
254
+ print(f"--- Terminal ({len(tasks_terminal.TASKS)} Aufgaben) ---",
255
+ file=sys.stderr)
256
+ for a in tasks_terminal.TASKS:
257
+ txt, m, fehler = frage(args.port, a["prompt"],
258
+ args.max_tokens, args.timeout)
259
+ if fehler:
260
+ bericht["terminal"].append(
261
+ dict(id=a["id"], titel=a["titel"], ok=False, grund=fehler,
262
+ antwort=""))
263
+ print(f" {a['id']} {a['titel']:<26} SERVERFEHLER {fehler[:50]}",
264
+ file=sys.stderr)
265
+ continue
266
+ befehl = schaele_befehl(txt)
267
+ ok, grund = lauf_terminal(a, befehl)
268
+ bericht["terminal"].append(
269
+ dict(id=a["id"], titel=a["titel"], ok=ok, grund=grund,
270
+ antwort=befehl[:200], **m))
271
+ bericht["messungen"].append(m)
272
+ print(f" {a['id']} {a['titel']:<26} {'OK ' if ok else 'FEHL'} "
273
+ f"{m['tps']:5.1f} t/s {m['tokens']:4d} Tok {grund[:45]}",
274
+ file=sys.stderr)
275
+
276
+ if args.nur in ("", "code"):
277
+ print(f"--- Programmierung ({len(tasks_code.TASKS)} Aufgaben) ---",
278
+ file=sys.stderr)
279
+ for a in tasks_code.TASKS:
280
+ txt, m, fehler = frage(args.port, a["prompt"],
281
+ args.max_tokens, args.timeout)
282
+ if fehler:
283
+ bericht["code"].append(
284
+ dict(id=a["id"], titel=a["titel"], ok=False, grund=fehler,
285
+ bestanden=0, gesamt=len(a["tests"])))
286
+ print(f" {a['id']} {a['titel']:<26} SERVERFEHLER {fehler[:50]}",
287
+ file=sys.stderr)
288
+ continue
289
+ code = schaele_code(txt)
290
+ ok, best, ges, grund = lauf_code(a, code)
291
+ bericht["code"].append(
292
+ dict(id=a["id"], titel=a["titel"], ok=ok, grund=grund,
293
+ bestanden=best, gesamt=ges, code=code[:600], **m))
294
+ bericht["messungen"].append(m)
295
+ print(f" {a['id']} {a['titel']:<26} {'OK ' if ok else 'FEHL'} "
296
+ f"{m['tps']:5.1f} t/s {m['tokens']:4d} Tok "
297
+ f"{best}/{ges} {grund[:35]}", file=sys.stderr)
298
+
299
+ t, c, mm = bericht["terminal"], bericht["code"], bericht["messungen"]
300
+ tps = sorted(x["tps"] for x in mm if x["tps"] > 0)
301
+ ptps = sorted(x["prompt_tps"] for x in mm if x["prompt_tps"] > 0)
302
+ med = lambda v: v[len(v) // 2] if v else 0.0
303
+ bericht["summe"] = {
304
+ "terminal_ok": sum(1 for x in t if x["ok"]), "terminal_n": len(t),
305
+ "code_ok": sum(1 for x in c if x["ok"]), "code_n": len(c),
306
+ "code_testfaelle_ok": sum(x.get("bestanden", 0) for x in c),
307
+ "code_testfaelle_n": sum(x.get("gesamt", 0) for x in c),
308
+ "tps_median": round(med(tps), 1),
309
+ "tps_min": round(tps[0], 1) if tps else 0.0,
310
+ "tps_max": round(tps[-1], 1) if tps else 0.0,
311
+ "prompt_tps_median": round(med(ptps), 1),
312
+ "tokens_gesamt": sum(x["tokens"] for x in mm),
313
+ "sekunden_gesamt": round(sum(x["sekunden"] for x in mm), 1),
314
+ "anfragen": len(mm),
315
+ # Der Preis des Denkens: wie viele Token im Schnitt, und wie viel davon
316
+ # Grubelei statt Antwort war. Ein Lauf, der ans Token-Limit stoesst,
317
+ # hat die Antwort nie ausgegeben — deshalb wird das mitgezaehlt.
318
+ "tokens_je_aufgabe": round(sum(x["tokens"] for x in mm) / len(mm), 1)
319
+ if mm else 0,
320
+ "denk_zeichen_gesamt": sum(x.get("denk_zeichen", 0) for x in mm),
321
+ "antwort_zeichen_gesamt": sum(x.get("antwort_zeichen", 0) for x in mm),
322
+ "am_limit": sum(1 for x in mm if x.get("abbruch") == "length"),
323
+ }
324
+ with open(args.out, "w") as f:
325
+ json.dump(bericht, f, ensure_ascii=False, indent=1)
326
+ s = bericht["summe"]
327
+ print(f"\n{args.modell} [Reasoning {'AN' if REASONING else 'AUS'}]: "
328
+ f"Terminal {s['terminal_ok']}/{s['terminal_n']}, "
329
+ f"Code {s['code_ok']}/{s['code_n']} "
330
+ f"({s['code_testfaelle_ok']}/{s['code_testfaelle_n']} Testfaelle), "
331
+ f"{s['tps_median']:.1f} t/s Median "
332
+ f"({s['tps_min']:.1f}-{s['tps_max']:.1f}), "
333
+ f"Prompt {s['prompt_tps_median']:.0f} t/s, "
334
+ f"{s['tokens_gesamt']} Token in {s['sekunden_gesamt']}s, "
335
+ f"{s['tokens_je_aufgabe']:.0f} Token/Aufgabe, "
336
+ f"{s['am_limit']} am Limit abgeschnitten", file=sys.stderr)
337
+ print(f"geschrieben: {args.out}", file=sys.stderr)
338
+
339
+
340
+ if __name__ == "__main__":
341
+ main()