Skip to content

Commit 3bb2f80

Browse files
author
lpb-docs
committed
fix(support): benchmark scoring bugs + compact report sections
Scoring fixes (found by inspecting real answers in the new reports): - math_log_eq: accept ±√17 forms ('+√17', 'sqrt(17)') — the model derives x = 1±√17 correctly but was scored wrong - digit-boundary guard now applies only to purely-numeric keywords; expression keywords like '1+√17' were rejected because the leading digit tripped the 'no digits around' rule - gsm_babysit: bare '10' matched '$105'/'1/10' — require $-context - math_coin_prob: prompt now asks for a plain 'num/den' fraction (the model kept answering in LaTeX \frac which the scorer can't match) - answer_text stored at 800 chars (was 400, cut off before the final number on long solutions); re-scoring from JSON falls back to it Report improvements: - per-prompt results: compact pivot matrix (problem × level) instead of a 60-row table; answers shown only for discriminating problems, one small table each - wire fidelity: synthetic — overall pass count, detail rows only on failure - token-count caveat: completion_tokens includes MTP draft tokens on Qwen3.6-MTP, so cross-model token totals are not comparable (use reasoning chars / wall time) Re-run results (12 problems, 19/19 fidelity): Qwen3.8 off 10/12 -> on-levels 11-12/12; Qwen3.6 11/12 at every level (math_log_eq is a capability gap there).
1 parent da6461e commit 3bb2f80

1 file changed

Lines changed: 96 additions & 34 deletions

File tree

support/thinking-benchmark.py

Lines changed: 96 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ def api_call(model: str, payload: dict, server: str, api_key: str, timeout: int
285285
("math_balls_boxes", "hard",
286286
"In how many ways can 3 indistinguishable balls be placed into 4 distinguishable boxes?"),
287287
("math_coin_prob", "hard",
288-
"A fair coin is flipped 5 times. What is the probability of getting exactly 3 heads? Express your answer as a fraction in lowest terms."),
288+
"A fair coin is flipped 5 times. What is the probability of getting exactly 3 heads? Give the numerator and denominator of that fraction, separated by a slash, with no spaces (for example: 1/2)."),
289289
("math_log_eq", "hard",
290290
"Solve for x: log base 2 of x plus log base 2 of (x minus 2) equals 4. Give the exact value of x."),
291291
("math_sequence", "hard",
@@ -298,16 +298,17 @@ def api_call(model: str, payload: dict, server: str, api_key: str, timeout: int
298298
# and MATH (hendrycks/math) style problems, adapted for single-shot scoring.
299299
ANSWER_KEYWORDS = {
300300
"gsm_clips": ["72"],
301-
"gsm_babysit": ["10", "$10"],
301+
"gsm_babysit": ["earned $10", "earn $10", "= $10", "**$10**", "$10."], # bare "10" too ambiguous ("105")
302302
"gsm_letter": ["624"],
303303
"gsm_wallet": ["5", "$5"],
304304
"gsm_book": ["42"],
305305
"gsm_flowers": ["35"],
306306
"math_heads_legs": ["10"],
307307
"math_div_or": ["220"],
308308
"math_balls_boxes": ["20"],
309-
"math_coin_prob": ["5/16"],
310-
"math_log_eq": ["\u221a17", "sqrt(17)"],
309+
"math_coin_prob": ["5/16", "5 / 16"],
310+
"math_log_eq": ["+ \u221a17", "+\u221a17", "- \u221a17", "-\u221a17",
311+
"sqrt(17)", "\u221a 17"],
311312
"math_sequence": ["33"],
312313
}
313314

@@ -324,7 +325,12 @@ def score_answer(prompt_key: str, result: dict) -> dict:
324325
if not result["success"]:
325326
return scoring
326327

327-
c = (result.get("answer", "") or "") + "\n" + (result.get("reasoning_content", "") or "")
328+
ans_full = result.get("answer")
329+
if not isinstance(ans_full, str):
330+
# re-scoring from stored records: answer_text holds the (possibly
331+
# tail-truncated) answer — use it instead of an empty string
332+
ans_full = result.get("answer_text", "") or ""
333+
c = ans_full + "\n" + (result.get("reasoning_content", "") or "")
328334
rc = result.get("reasoning_content", "") or ""
329335

330336
scoring["valid_response"] = len(c.strip()) > 10
@@ -341,10 +347,14 @@ def score_answer(prompt_key: str, result: dict) -> dict:
341347
def kw_hit(kw: str) -> bool:
342348
k = kw.lower()
343349
for m in re.finditer(re.escape(k), content_lower):
344-
before = content_lower[m.start() - 1] if m.start() > 0 else ""
345-
after = content_lower[m.end()] if m.end() < len(content_lower) else ""
346-
if before.isdigit() or after.isdigit():
347-
continue
350+
# digit-boundary guard only applies to purely-numeric keywords:
351+
# reject "10" inside "100", but let "1+√17" match (the leading
352+
# 1 is part of the expression, not a false-positive boundary)
353+
if k.isdigit():
354+
before = content_lower[m.start() - 1] if m.start() > 0 else ""
355+
after = content_lower[m.end()] if m.end() < len(content_lower) else ""
356+
if before.isdigit() or after.isdigit():
357+
continue
348358
return True
349359
return False
350360

@@ -511,10 +521,16 @@ def run_benchmark(models, levels, runs, server, api_key, mode="full", report_pat
511521
else:
512522
scoring = score_answer(prompt_key, api_result)
513523
rc = api_result.get("reasoning_content", "") or ""
524+
# store the answer text (truncated) for report inspection;
525+
# when content is empty keep the reasoning tail instead
526+
ans = (api_result.get("answer") or "").strip()
527+
if not ans:
528+
ans = ("…" + rc[-700:]) if len(rc) > 700 else rc
514529
record = {
515530
"model": model, "level": level, "run": run_idx,
516531
"prompt_key": prompt_key, "difficulty": difficulty,
517532
"success": True, **scoring,
533+
"answer_text": ans[:800],
518534
"elapsed_ms": api_result["elapsed_ms"],
519535
"finish_reason": api_result.get("finish_reason"),
520536
"total_tokens": api_result.get("total_tokens", 0),
@@ -692,40 +708,71 @@ def write_report(path: str, models, levels, summaries, fidelity, versions, all_r
692708
a(f"| {level} | " + " | ".join(cells) + " |")
693709
a("")
694710

695-
# ── Per-prompt detail ──
711+
# ── Per-prompt matrix (prompts × levels — compact pivot) ──
696712
a("## Per-prompt results")
697713
a("")
698-
a("Each prompt × level cell: correctness, reasoning length, wall time, and the "
699-
"model's answer (truncated). Lets you inspect WHY a level scores the way it does.")
714+
a("One row per problem: ✅ pass / ⚠ wrong answer / ❌ no answer / ⏭ skipped, "
715+
"with reasoning chars in each level column. Full answers for the "
716+
"discriminating cells (not passed at every level) follow below.")
700717
for model in models:
701718
a(f"### {model}")
702719
a("")
703-
a("| Prompt | Level | Correct | Reasoning chars | Time | Answer (truncated) |")
704-
a("|---|---|---|---|---|---|")
720+
a("| Problem | Tier | " + " | ".join(levels) + " |")
721+
a("|---|---|" + "---|" * len(levels))
705722
for prompt_key, difficulty, _ in PROMPTS:
706-
row_levels = [l for l in levels
707-
if any(r["model"] == model and r["level"] == l and r["prompt_key"] == prompt_key
708-
for r in all_results)]
709-
for level in row_levels:
723+
cells = []
724+
for level in levels:
710725
rs = [r for r in all_results if r["model"] == model and r["level"] == level
711726
and r["prompt_key"] == prompt_key]
712727
if not rs:
728+
cells.append("·")
713729
continue
714730
r = rs[0] # first run
715731
if not r.get("success"):
716-
a(f"| {prompt_key} | {level} | ⏭ skipped | — | — | {_md_escape(r.get('error', '?'))[:100]} |")
732+
cells.append("⏭")
717733
continue
718734
mark = "✅" if r["correct_answer"] else ("⚠" if r["valid_response"] else "❌")
719-
# answer may live in reasoning_content when content is empty
720-
ans_text = r.get("answer") or ""
721-
if not ans_text.strip():
722-
rc_tail = (r.get("reasoning_content") or "")[-200:]
723-
ans_text = ("…" if len(r.get("reasoning_content") or "") > 200 else "") + rc_tail
724-
ans = _md_escape(ans_text)[:150]
725-
a(f"| {prompt_key} | {level} | {mark} | {r['reasoning_chars']} | "
726-
f"{r['elapsed_ms']/1000:.1f}s | {ans} |")
735+
rc = r["reasoning_chars"]
736+
cells.append(f"{mark} {rc}" if rc > 0 else mark)
737+
a(f"| `{prompt_key}` | {difficulty} | " + " | ".join(cells) + " |")
727738
a("")
728739

740+
# ── Answers for discriminating cells (compact table per problem) ──
741+
for model in models:
742+
interesting = []
743+
for prompt_key, difficulty, _ in PROMPTS:
744+
outcomes = {}
745+
for level in levels:
746+
rs = [r for r in all_results if r["model"] == model and r["level"] == level
747+
and r["prompt_key"] == prompt_key and r.get("success")]
748+
if rs:
749+
outcomes[level] = rs[0]["correct_answer"]
750+
if len(outcomes) > 1 or (outcomes and not all(outcomes.values())):
751+
interesting.append((prompt_key, difficulty))
752+
if not interesting:
753+
continue
754+
a(f"### Answers — {model} (discriminating problems)")
755+
a("")
756+
for prompt_key, difficulty in interesting:
757+
_, _, prompt_text = next(p for p in PROMPTS if p[0] == prompt_key)
758+
a(f"**`{prompt_key}`** ({difficulty}) — {_md_escape(prompt_text)[:160]}")
759+
a("")
760+
a("| Level | Result | Answer (truncated) |")
761+
a("|---|---|---|")
762+
for level in levels:
763+
rs = [r for r in all_results if r["model"] == model and r["level"] == level
764+
and r["prompt_key"] == prompt_key]
765+
if not rs:
766+
continue
767+
r = rs[0]
768+
if not r.get("success"):
769+
a(f"| {level} | ⏭ skipped | {_md_escape(r.get('error', '?'))[:120]} |")
770+
continue
771+
mark = "✅" if r["correct_answer"] else ("⚠" if r["valid_response"] else "❌")
772+
ans_text = (r.get("answer_text") or "").strip() or "(empty)"
773+
a(f"| {level} | {mark} | {_md_escape(ans_text)[:200]} |")
774+
a("")
775+
729776
# ── Interpretation notes (auto-generated observations) ──
730777
a("## Notes")
731778
a("")
@@ -791,19 +838,29 @@ def write_report(path: str, models, levels, summaries, fidelity, versions, all_r
791838
for r in failed[:20]:
792839
a(f"- {r['model']} @ {r['level']}{r['prompt_key']}: {r.get('error', '?')[:120]}")
793840

794-
# ── Wire fidelity (plugin params → backend) ──
841+
# ── Wire fidelity (synthetic — details only on failure) ──
795842
a("")
796843
a("## Wire fidelity (plugin params → backend)")
797844
a("")
798-
a("| Model | Level | Check | Result | Detail |")
799-
a("|---|---|---|---|---|")
845+
fid_items = []
800846
for f in fidelity:
801-
# f is a 5-tuple from the live run, or a dict from the JSON path
802847
if isinstance(f, dict):
803-
m, l, lb, ok, dt = f["model"], f["level"], f["label"], f["ok"], f["detail"]
848+
fid_items.append((f["model"], f["level"], f["label"], f["ok"], f["detail"]))
804849
else:
805-
m, l, lb, ok, dt = f
806-
a(f"| {m} | {l} | {lb} | {'✅' if ok else '❌'} | {dt} |")
850+
fid_items.append(tuple(f))
851+
fid_ok = sum(1 for _, _, _, ok, _ in fid_items if ok)
852+
if fid_ok == len(fid_items):
853+
a(f"✅ **{fid_ok}/{len(fid_items)} checks passed** — every plugin-tuned param "
854+
f"(P2 budget, effortMap, P3 sampling, P5 offParams) reached the backend and "
855+
f"had its expected effect.")
856+
else:
857+
a(f"❌ **{fid_ok}/{len(fid_items)} checks passed** — failures below:")
858+
a("")
859+
a("| Model | Level | Check | Detail |")
860+
a("|---|---|---|---|")
861+
for m, l, lb, ok, dt in fid_items:
862+
if not ok:
863+
a(f"| {m} | {l} | {lb} | {dt} |")
807864

808865
failed = [r for r in all_results if not r["success"] and not r.get("template_rejection")]
809866

@@ -824,6 +881,11 @@ def write_report(path: str, models, levels, summaries, fidelity, versions, all_r
824881
"machine-verified before inclusion. Prompts and keywords live in "
825882
"`support/thinking-benchmark.py` (PROMPTS / ANSWER_KEYWORDS).")
826883
a("")
884+
a("**Token-count caveat:** `completion_tokens` includes MTP draft tokens for "
885+
"multi-token-prediction models (Qwen3.6-MTP variants), so its totals are not "
886+
"comparable to dense models (Qwen3.8). Use reasoning chars and wall time "
887+
"for cross-model comparison.")
888+
a("")
827889
a("_Generated by `support/thinking-benchmark.py --report`. Re-run after every pi / "
828890
"plugin / server version bump and diff against the previous report._")
829891
p = Path(path)

0 commit comments

Comments
 (0)