RTX 5090 · vLLM + llama.cpp · avg@1
CASE FILE VIBETHINKER-3B · SMALL-MODEL TEARDOWN
FINDING SUPPORTED · NARROW CONTEST MATH · VERIFIED
The "3B beats much larger models" claim

A 3B was supposed to rival models 9× larger. On math, it actually does.

VibeThinker-3B is just Qwen2.5-Coder-3B with a reasoning post-train bolted on. We re-ran it ourselves — one harness, thinking enabled on the big models, two benchmark bugs caught and fixed along the way. The result: it ties a 27B reasoner on AIME and HMMT, and leaps +90 points over its own base. It is also the weakest model here on broad science. Both of those are true, and that's the whole story. Here is exactly what we ran.

VibeThinker · AIME24
93.3%
ties a 27B · 9× the size
Its own base · AIME25
0.0%
Qwen2.5-Coder-3B
What training bought
+93
pts, identical weights
GPQA · the weak spot
62%
last of the reasoners
Listen instead — AI narration in the author's voice
Exhibit A · the lineup

One 3B, climbing a weight-class ladder.

The subject against four controls — its own untrained base, a larger off-the-shelf small reasoner, and two models 4× and 9× its size. Same questions, same harness, served-model logged per answer.

SUBJECT
3B
VibeThinker-3B
Qwen2.5-Coder base + SSP reasoning post-train. The claim under test.
3B
Qwen2.5-Coder-3B
The literal base — same weights, before the post-train. Isolates what training bought.
4B
Qwen3-4B-Thinking
Off-the-shelf small reasoner, bigger than the subject. Thinking on.
12B
Gemma-4-12B-it
4× the size. Google instruct model, run as deployed.
27B
Qwen-27B (thinking)
9× the size, a real reasoner — run with thinking ON for a fair fight.
Exhibit B · the battery

140 problems × 5 models. One harness.

Three contest-math sets where reasoning is the whole game, plus a graduate-science set as the knowledge control — the axis where the paper itself admits the model is weak. Identical inputs for every model; the served model is logged on every single answer.

30
AIME 2024
Integer-answer olympiad math. Pure multi-step reasoning.
30
AIME 2025
Newer AIME — harder to have leaked into training.
30
HMMT 2025
Harvard-MIT Math Tournament. Fractions & radicals, not just integers.
50
GPQA-Diamond
Graduate physics/chem/bio, 4-way multiple choice. The knowledge control.
30 AIME24 + 30 AIME25 + 30 HMMT25 + 50 GPQA = 140 items/model × 5 models = 700 evaluations · avg@1 · provenance logged
// PROTOCOLone model resident at a time · single 32GB GPU · served-model id logged per item
fair fightthe 27B run with thinking ON — not the deployed thinking-off config that would cripple its math
gradinginteger exact-match (AIME/HMMT) · symbolic equivalence for fractions/radicals · letter-match (GPQA)
budget60k-token generation cap · finish_reason=length logged so truncated traces can't pass as real misses
Exhibit C · the verdict

It ties the 27B on math. Then loses the knowledge round.

Two headlines do the work: what the training bought (vs its own base), and how it stacks against a model 9× its size. Bars fill to score.

What the training bought — VibeThinker vs its own base (AIME25)
VibeThinker-3B93.3%
Qwen2.5-Coder-3B base0.0%
28/30 vs 0/30 · identical weightsSSP post-train +93 pts
▸ the base cannot do contest math at all. The reasoning is the training, not the scale.
3B vs 27B — fair fight, thinking on both (AIME24)
VibeThinker-3B93.3%
Qwen-27B · thinking93.3%
28/30 vs 28/30 · 9× the parametersdead tie
▸ also tied on HMMT25 (70.0 = 70.0). The 27B only pulls ahead on knowledge — see GPQA below.
ModelAIME24AIME25HMMT25GPQA
VibeThinker-3B93.3%93.3%70.0%62.0%
Qwen2.5-Coder-3B · base3.3%0.0%0.0%28.0%
Qwen3-4B-Thinking86.7%80.0%40.0%64.0%
Gemma-4-12B76.7%53.3%23.3%66.0%
Qwen-27B · thinking93.3%83.3%70.0%78.0%

Green = best (or tied-best) in column. The 3B owns every math column; it finishes last among reasoners on GPQA.

You're reading the quick verdict. The full case — the lineup, the protocol, the two benchmark bugs we caught, and the honest caveats — is in the deep dive →

Exhibit D · same weights, rebuilt

From can't to contest-grade — no new parameters.

VibeThinker starts from Qwen2.5-Coder-3B and changes nothing but the training. So the gap between these two rows is the cleanest number in the whole study: it is entirely the SSP reasoning post-train, with size held fixed at 3B.

Benchmarkbase 3BVibeThinker 3Bwhat training bought
AIME 20243.3%93.3%+90.0
AIME 20250.0%93.3%+93.3
HMMT 20250.0%70.0%+70.0
GPQA-Diamond28.0%62.0%+34.0

The base sits at random chance on GPQA (28% ≈ the 25% you'd get guessing) and flat zero on the 2025 math. This is the democratization result in one table: frontier-grade contest reasoning, distilled into weights a consumer can run on a single card.

Exhibit E · chain of custody

How we know it's real — and what we caught.

A clean-looking leaderboard hides bugs. Re-running it ourselves surfaced two that would have shipped the wrong verdict — one inflating a number, one deflating it.

The grader was false-zeroing half of HMMT

16 of 30 HMMT answers aren't integers — they're fractions and radicals like 9√23⁄23. The naive grader fell back to exact-string match and marked correct answers wrong. We swapped in symbolic equivalence checking; VibeThinker's HMMT rose from a deflated 63% to its true 70%.

Every GPQA answer was the letter "A"

All 198 gold answers sat in position A — the options were never shuffled. That doesn't measure knowledge, it measures A-bias. We reshuffled every item to a fixed-but-uniform position and re-ran. VibeThinker's spread came out even across A/B/C/D — so its score is real reasoning, not positional luck — and the honest number settled at 62% (the inflated original read 72%).

We gave the 27B its real teeth

The 27B's deployed config runs with thinking disabled — which would have handed VibeThinker an unearned win on math. We overrode it to think for every math item. That's why the headline is a fair tie, not a rigged one.

We proved which model answered, and caught the truncations

The served-model id is logged on every item — no self-comparison. The base model's first run errored on all 140 requests (its window rejected our token budget); we caught it from an impossible 0% and re-ran correctly. VibeThinker's verbosity blew a too-small GPQA budget on 16 items; we raised it and re-ran. Silent truncation never became a fake score.

Exhibit F · the tell

A spike, not a peak.

The same 3B that ties a 27B on olympiad math finishes dead last among the reasoners on graduate science — below the 4B, below the 12B, sixteen points under the 27B. It didn't get smaller-but-general. It got narrow-but-deep.

◢ where it's elite — reasoning
AIME24 — vs 27B93.3 = 93.3 · tie
AIME25 — vs 27B93.3 vs 83.3 · +10
HMMT25 — vs 27B70.0 = 70.0 · tie
HMMT25 — vs 4B reasoner70.0 vs 40.0 · +30
◣ where it's ordinary — knowledge
GPQA — VibeThinker 3B62.0
GPQA — Qwen3 4B64.0
GPQA — Gemma 12B66.0
GPQA — Qwen 27B78.0

This is the real lesson, and it cuts both ways. Reasoning can be trained into a tiny model — scale isn't the moat people think. Knowledge breadth still scales with parameters: the 27B's edge shows up exactly, and only, on the axis that rewards remembering more.

Exhibit G · the frontier test

We kept saying "frontier." So we ran two.

The viral claim is "beats frontier models" — but every model above is open weights. To close that gap we ran the same 140-item battery, the same grader, through two actual frontier models: Claude Haiku 4.5 (the small tier) and Claude Sonnet 4.6 (the strong tier). Closed-book, no tools, reasoning only — exactly the inputs the open models got.

ModelAIME24AIME25HMMT25GPQA
VibeThinker-3B93.3%93.3%70.0%62.0%
Claude Haiku 4.5 · small frontier33.3%20.0%10.0%64.0%
Claude Sonnet 4.6 · strong frontier96.7%83.3%83.3%80.0%

Green = best in column. The 3B beats the small frontier model on every math axis (+60 / +73 / +60) and ties it on knowledge. On AIME it goes toe-to-toe with the strong one — a one-item gap on 2024, and it actually leads on 2025. Sonnet pulls clear only on the harder HMMT set and on GPQA.

vs the small frontier model — Haiku 4.5 (AIME25)
VibeThinker-3B93.3%
Claude Haiku 4.520.0%
28/30 vs 6/30 · runs on a single card3B wins +73 pts
▸ "a 3B you run locally out-maths a frontier API model" — true, at this tier.
vs the strong frontier model — Sonnet 4.6 (AIME25)
VibeThinker-3B93.3%
Claude Sonnet 4.683.3%
28/30 vs 25/30 · inside the ±9–13% noise3B +10 pts
▸ flips the other way on HMMT25 (70.0 vs 83.3) and GPQA (62 vs 80) — Sonnet takes the harder math and the knowledge.

So "beats frontier models" is half true — and the honest half is the better story: a 3B that matches or beats a strong frontier model on AIME-class math and routs a small one, then loses, fair and square, on harder olympiad math and on what it simply knows.

Two caveats, in plain sight. The Claude models are capability reference points, not contamination-controlled — we have zero visibility into their training and AIME/GPQA are public, so read their scores as a generous bound, not a clean one. And they ran without extended thinking (in-context reasoning only) — yet Sonnet still cleared 83–97 on math, which is the tell that the setup is fair and the small model's low scores are real capability, not a handicap.

Exhibit H · cost of thought

It thinks hard — sometimes past the budget.

VibeThinker buys its reasoning with tokens — chains that run tens of thousands long, occasionally past a 60k cap. Truncation counts are the tell: the models that run away vs the one that stays tight. (Truncated items across all four benchmarks, out of 140.)

Gemma-12B · runs away13 truncated
VibeThinker-3B · verbose11 truncated
Qwen-27B · thinking10 truncated
Qwen3-4B · tight0 truncated

VibeThinker's reasoning isn't free — on the hardest items it generates 10k–60k-token chains, and a handful overrun even a generous budget. Notably, raising the cap on those runaways barely moved its score: the traces that won't terminate are usually the ones it was going to miss anyway. Length is the cost of its method, not the source of its edge.

Exhibit I · the honest fine print

What this proves — and what it doesn't.

Sample size

This is avg@1 on 30–50 item sets — confidence intervals around ±9–13%. So the right claim is "matches/rivals" the 27B, not "beats": the AIME tie and the +10 on AIME25 are within noise of each other. The +90 over its own base is far beyond any noise.

Quant & engine

The two big models ran as their real local GGUF quants on llama.cpp; the small models ran BF16 on vLLM. That's the honest "as a consumer runs them" setup — but it's a mixed-precision comparison, and the 27B was GGUF-only so we couldn't cross-check it on a second engine.

The hype vs this

The viral "beats Gemini 3 / Opus 4.5" posts are test-time-scaled consensus numbers on a narrow axis — not pass@1. Our clean single-sample number is lower and more honest: it rivals far larger models on contest math, full stop. Not a frontier generalist.

Scope

"Good" here means good at verifiable math reasoning. GPQA shows the breadth limit; this is a specialist. One harness, one quant per model, one GPU — a controlled read, not a universal law. Gemma's math is mildly understated by truncation (it never flips a result).

Exhibit J · reproduce it

The whole harness, open.

One runner, the exact seeded items every model saw, and every per-item prediction tagged with the served_model that produced it — including the _gpqa_origA.jsonl.bak that proves the all-"A" bug. Read the code below, or take the bundle and re-score it yourself. The only redaction is the llama-swap host.

prep_data.py Builds the benchmark sets → data/*.jsonl (AIME24 · AIME25 · HMMT25 · GPQA-Diamond), seed-fixed 52 lines
#!/usr/bin/env python3
"""Convert VibeThinker repo math parquets -> flat jsonl the harness eats.

Source rows (verl format):
  prompt:        [{"role":"user","content": "<problem ... \\boxed{}>"}]
  reward_model:  {"ground_truth": "<answer>", "style": "rule"}

Output rows:
  {"id","category","question","answer"}

The prompt already carries the "output the final answer within \\boxed{}"
instruction, so we send it verbatim with NO extra system prompt -- identical
input for every model under test (the controlled-input invariant).
"""
import os, json, glob, argparse
import pandas as pd

DEFAULT_SRC = os.path.expanduser(
    "~/Documents/AI/VibeThinker/eval/math/data")  # adjust on the host

def convert(src, out):
    os.makedirs(out, exist_ok=True)
    total = 0
    for f in sorted(glob.glob(os.path.join(src, "*.parquet"))):
        cat = os.path.splitext(os.path.basename(f))[0]   # aime / aime25 / hmmt25 / gpqa
        df = pd.read_parquet(f)
        rows = []
        for i, r in df.iterrows():
            prompt = r["prompt"]
            # prompt is a numpy array / list of {role,content}
            content = prompt[0]["content"] if not isinstance(prompt, str) else prompt
            gt = r["reward_model"]["ground_truth"]
            rows.append({
                "id": f"{cat}-{i:03d}",
                "category": cat,
                "question": content,
                "answer": str(gt).strip(),
            })
        with open(os.path.join(out, f"{cat}.jsonl"), "w") as fh:
            for row in rows:
                fh.write(json.dumps(row, ensure_ascii=False) + "\n")
        print(f"{cat:<8} {len(rows):>4} items -> {cat}.jsonl")
        total += len(rows)
    print(f"TOTAL {total} items")

if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("--src", default=DEFAULT_SRC)
    ap.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "data"))
    a = ap.parse_args()
    convert(a.src, a.out)
vibe_eval.py The runner + grader — one harness · served-model logged per item · symbolic-equiv grading · pass@1 / cons@k · GPQA shuffle 223 lines
#!/usr/bin/env python3
"""avg@k eval against an OpenAI-compatible vLLM endpoint, for VibeThinker-3B
and its controls (Qwen2.5-Coder-3B-Instruct, Qwen3-4B-Thinking-2507).

Faithful to the paper's protocol: NO system prompt (the \\boxed instruction is
in the question), temp=1.0, top_p=0.95, top_k=-1, k independent samples/item.

Reports the same metrics the report uses:
  pass@1  = mean correctness over all k samples (the headline avg@k number)
  pass@k  = any of the k correct
  cons@k  = majority-vote (self-consistency) correctness

Crucially logs finish_reason per sample so truncated traces (finish_reason=
"length") are visible -- a truncated long CoT scores 0 and would otherwise read
as a real miss (the false-zero trap).

Usage (one resident model at a time):
  python3 vibe_eval.py --base http://localhost:8000 --model vibethinker-3b \
      --tag vibe3b --n 16
  python3 vibe_eval.py --compare results/vibe3b.jsonl results/base3b.jsonl
"""
import json, os, re, time, argparse, urllib.request, glob
from collections import Counter
from concurrent.futures import ThreadPoolExecutor

HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "data")
RES = os.path.join(HERE, "results"); os.makedirs(RES, exist_ok=True)

# ---------- io ----------
def load_items(cats=None):
    items = []
    for f in sorted(glob.glob(os.path.join(DATA, "*.jsonl"))):
        cat = os.path.splitext(os.path.basename(f))[0]
        if cats and cat not in cats:
            continue
        with open(f) as fh:
            items += [json.loads(l) for l in fh if l.strip()]
    return items

def call_one(base, model, key, question, max_tokens, temp, top_p, timeout, think=False):
    # ONE sample per request. llama.cpp's OpenAI endpoint ignores n>1, so we fire
    # k independent requests instead (portable across llama.cpp and vLLM).
    # No system prompt: identical input for every model.
    payload = {"model": model, "temperature": temp, "top_p": top_p,
               "max_tokens": max_tokens,
               "messages": [{"role": "user", "content": question}]}
    if think:                                   # override server's enable_thinking:false
        payload["chat_template_kwargs"] = {"enable_thinking": True}
    body = json.dumps(payload).encode()
    req = urllib.request.Request(base.rstrip("/") + "/v1/chat/completions", data=body,
                                 headers={"Content-Type": "application/json",
                                          **({"Authorization": f"Bearer {key}"} if key else {})})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        d = json.load(r)
    ch = d["choices"][0]
    msg = ch.get("message", {})
    text = msg.get("content") or msg.get("reasoning_content") or ""
    return {"text": text, "finish": ch.get("finish_reason")}, d.get("model")

# ---------- grading ----------
def extract(text):
    """Return (pred, from_box). from_box=True means it came from \\boxed{},
    which lets math grading apply a STRICT integer parse (reject fractions)."""
    b = list(re.finditer(r"\\boxed\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}", text))
    if b: return b[-1].group(1).strip(), True
    m = list(re.finditer(r"ANSWER:\s*(.+)", text, re.I))
    if m: return m[-1].group(1).strip(), False
    return (text.strip().splitlines()[-1].strip() if text.strip() else ""), False

def strict_int(s):                              # boxed content must BE an integer
    s = s.replace(",", "").replace("$", "").replace(" ", "").strip()
    m = re.fullmatch(r"-?\d+", s) or re.fullmatch(r"(-?\d+)\.0*", s)
    return int(m.group(1) if m and m.lastindex else s) if m else None

def loose_int(s):                               # only for the no-box fallback path
    s = s.replace(",", "").replace("$", "").replace("\\", "").strip()
    m = re.search(r"-?\d+", s)
    return int(m.group(0)) if m else None

try:                                            # symbolic LaTeX equivalence (HMMT non-int answers)
    from math_verify import parse as _mv_parse, verify as _mv_verify
    _HAS_MV = True
except Exception:
    _HAS_MV = False

def math_equiv(pred, gold):
    """Symbolic equivalence for non-integer golds (fractions, radicals, sets).
    e.g. \\dfrac{1}{576}==\\frac{1}{576}, 8\\sqrt{10}==\\sqrt{640}. Falls back to
    exact-string match if math_verify is unavailable or a parse throws."""
    if not _HAS_MV:
        return pred.strip() == gold.strip()
    try:
        return bool(_mv_verify(_mv_parse(f"${gold}$"), _mv_parse(f"${pred}$")))
    except Exception:
        return pred.strip() == gold.strip()

def grade(category, raw, gold):
    pred, from_box = extract(raw)
    if category == "gpqa":                      # multiple-choice A-D
        m = re.search(r"\b([A-D])\b", pred.upper()) or re.search(r"([A-D])", pred.upper())
        return bool(m) and m.group(1) == gold.strip().upper(), pred
    g = strict_int(gold)                         # aime / hmmt -> integer answers
    p = strict_int(pred) if from_box else loose_int(pred)
    if g is not None and p is not None:
        return p == g, pred
    if g is None:                                # non-integer gold (hmmt fractions/radicals)
        return math_equiv(pred, gold), pred
    return pred.strip() == gold.strip(), pred

# ---------- run ----------
def run(args):
    items = load_items(set(args.cats.split(",")) if args.cats else None)
    if args.limit:
        items = items[:args.limit]
    out = os.path.join(RES, f"{args.tag}.jsonl")
    print(f"[{args.tag}] {len(items)} items x avg@{args.n} -> {args.model} @ {args.base}")

    # Flatten to (item, sample) tasks -> k independent requests/item (portable).
    tasks = [(it, s) for it in items for s in range(args.n)]
    print(f"[{args.tag}] {len(items)} items x avg@{args.n} = {len(tasks)} gens "
          f"-> {args.model} @ {args.base}")

    def do_task(task):
        it, _ = task
        try:
            s, served = call_one(args.base, args.model, args.key, it["question"],
                                 args.max_tokens, args.temp, args.top_p, args.timeout,
                                 think=args.think)
            ok, pred = grade(it["category"], s["text"], it["answer"])
            return {"id": it["id"], "ok": int(ok), "pred": pred,
                    "trunc": int(s["finish"] == "length"), "served": served, "err": None}
        except Exception as e:
            return {"id": it["id"], "ok": 0, "pred": None,
                    "trunc": 0, "served": None, "err": str(e)}

    by_id = {it["id"]: it for it in items}
    order = [it["id"] for it in items]
    agg = {it["id"]: {"ok": [], "pred": [], "trunc": 0, "err": 0, "served": set()}
           for it in items}

    def finalize(iid):
        it = by_id[iid]; a = agg[iid]; scores = a["ok"]; n = len(scores)
        preds = [p for p in a["pred"] if p]
        cons_pred = Counter(preds).most_common(1)[0][0] if preds else ""
        cons_ok = grade(it["category"], "\\boxed{%s}" % cons_pred, it["answer"])[0] if cons_pred else False
        return {"id": iid, "category": it["category"], "answer": it["answer"],
                "pass1": (sum(scores) / n if n else 0.0), "passk": int(any(scores)),
                "cons": int(cons_ok), "n": n, "trunc": a["trunc"], "err": a["err"],
                "pred": cons_pred, "preds": a["pred"],   # persist model answer(s) for offline re-grading
                "served_model": sorted(a["served"])}

    rows, done, total_trunc = [], 0, 0
    fout = open(out, "w")
    with ThreadPoolExecutor(max_workers=args.concurrency) as ex:
        for r in ex.map(do_task, tasks):       # yields in task order -> grouped by item
            a = agg[r["id"]]
            a["ok"].append(r["ok"]); a["pred"].append(r["pred"]); a["trunc"] += r["trunc"]
            if r["err"]: a["err"] += 1
            if r["served"]: a["served"].add(r["served"])
            done += 1
            if done % args.n == 0:              # one item's k samples are all in
                row = finalize(order[done // args.n - 1])
                rows.append(row); fout.write(json.dumps(row, ensure_ascii=False) + "\n"); fout.flush()
                total_trunc += row["trunc"]
                tr = f"  *** TRUNC={row['trunc']} ***" if row["trunc"] else ""
                er = f" err={row['err']}" if row["err"] else ""
                print(f"  [{done//args.n:>3}/{len(items)}] {row['pass1']*100:3.0f}%  "
                      f"{row['id']:<12} gold={row['answer'][:8]:<8}{er}{tr}", flush=True)
    fout.close()
    if total_trunc:
        print(f"\n!!! {total_trunc} truncated samples (finish_reason=length) — traces exceeded "
              f"the per-slot ctx. Drop llama-server to --parallel 1 and re-run. !!!")
    served = sorted({s for r in rows for s in r["served_model"]})
    print(f"\nSERVED MODEL(S): {served}")
    summarize(rows, args.tag)
    print(f"saved -> {out}")

def summarize(rows, tag):
    cats = {}
    for r in rows:
        c = cats.setdefault(r["category"], {"n": 0, "p1": 0.0, "pk": 0, "cons": 0, "tr": 0})
        c["n"] += 1; c["p1"] += r["pass1"]; c["pk"] += r["passk"]
        c["cons"] += r["cons"]; c["tr"] += r.get("trunc", 0)
    print(f"\n=== {tag} ===")
    print(f"{'category':<10}{'pass@1':>9}{'pass@k':>9}{'cons@k':>9}{'trunc':>8}")
    for c, v in sorted(cats.items()):
        n = v["n"]
        print(f"{c:<10}{100*v['p1']/n:>8.1f}%{100*v['pk']/n:>8.0f}%{100*v['cons']/n:>8.0f}%{v['tr']:>8}")

def compare(a, b):
    def load(p): return {r["id"]: r for r in (json.loads(l) for l in open(p))}
    A, B = load(a), load(b)
    ta, tb = os.path.basename(a).split(".")[0], os.path.basename(b).split(".")[0]
    cats = {}
    for i in A:
        if i not in B: continue
        c = cats.setdefault(A[i]["category"], [0, 0.0, 0.0])
        c[0] += 1; c[1] += A[i]["pass1"]; c[2] += B[i]["pass1"]
    print(f"{'category':<10}{ta:>10}{tb:>10}{'delta':>9}")
    for c, (n, x, y) in sorted(cats.items()):
        print(f"{c:<10}{100*x/n:>9.1f}%{100*y/n:>9.1f}%{100*(x-y)/n:>+8.1f}")

if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("--base"); ap.add_argument("--model"); ap.add_argument("--key")
    ap.add_argument("--tag", default="run")
    ap.add_argument("--n", type=int, default=16, help="samples per item (avg@k)")
    ap.add_argument("--limit", type=int, help="cap items per run (e.g. subsample gpqa)")
    ap.add_argument("--cats", help="comma list e.g. aime,aime25,hmmt25 (default: all)")
    ap.add_argument("--max-tokens", type=int, default=40960, dest="max_tokens")
    ap.add_argument("--temp", type=float, default=1.0)
    ap.add_argument("--top-p", type=float, default=0.95, dest="top_p")
    ap.add_argument("--timeout", type=int, default=1800)
    ap.add_argument("--think", action="store_true",
                    help="send chat_template_kwargs enable_thinking=true (override server default)")
    ap.add_argument("--concurrency", type=int, default=4,
                    help="match llama-server --parallel (4); bump to 16+ for vLLM")
    ap.add_argument("--compare", nargs=2)
    a = ap.parse_args()
    if a.compare: compare(*a.compare)
    else: run(a)
grade_haiku.py Frontier grader — scores the Claude Haiku/Sonnet runs with the identical grade() reused from vibe_eval 65 lines
#!/usr/bin/env python3
"""Grade the Haiku sub-agent battery against the withheld gold key, reusing the
EXACT grader from vibe_eval (math-verify symbolic equiv for non-int golds, strict
int for AIME, letter-match for GPQA). Each agent returned a JSON object mapping
problem id -> final answer in its last assistant message; we extract that JSON
from the agent transcript files without dumping them into anyone's context.
"""
import json, re, sys, glob, os
from collections import defaultdict
from vibe_eval import grade  # reuse identical grading

GOLD = json.load(open("/tmp/haiku_gold.json"))   # id -> [category, answer]
TASKDIR = sys.argv[1]                              # dir holding <agentId>.output files
AGENTS = sys.argv[2:]                              # agentIds to scan

ID_RE = re.compile(r'^(aime|aime25|hmmt25|gpqa)-\d+$')

def extract_preds(path):
    """Find every JSON object in the transcript whose keys look like problem ids;
    merge them (last write wins). Robust to prose around the JSON."""
    raw = open(path, encoding="utf-8", errors="ignore").read()
    preds = {}
    # find balanced-ish {...} blobs; greedy per-line and whole-text candidates
    for m in re.finditer(r'\{[^{}]*\}', raw):
        blob = m.group(0)
        if '"aime' not in blob and '"hmmt' not in blob and '"gpqa' not in blob:
            continue
        try:
            d = json.loads(blob)
        except Exception:
            continue
        for k, v in d.items():
            if ID_RE.match(k) and isinstance(v, str):
                preds[k] = v
    return preds

allpreds = {}
for aid in AGENTS:
    p = os.path.join(TASKDIR, f"{aid}.output")
    if not os.path.exists(p):
        print(f"!! missing {aid}"); continue
    allpreds.update(extract_preds(p))

# grade
cats = defaultdict(lambda: {"n": 0, "ok": 0, "miss": 0})
missing = []
for iid, (cat, gold) in GOLD.items():
    cats[cat]["n"] += 1
    if iid not in allpreds:
        cats[cat]["miss"] += 1; missing.append(iid); continue
    raw = "\\boxed{%s}" % allpreds[iid]      # same wrapping the harness uses for cons grading
    ok, _ = grade(cat, raw, gold)
    cats[cat]["ok"] += int(ok)

order = ["aime", "aime25", "hmmt25", "gpqa"]
print("\n=== HAIKU 4.5 (closed-book, no-tools, sub-agent path) ===")
print(f"{'category':<10}{'pass@1':>9}{'correct':>9}{'n':>5}{'missing':>9}")
for c in order:
    if c not in cats: continue
    v = cats[c]; n = v["n"]
    print(f"{c:<10}{100*v['ok']/n:>8.1f}%{v['ok']:>9}{n:>5}{v['miss']:>9}")
if missing:
    print("\nmissing ids (no answer returned):", missing)
print(f"\ntotal answers parsed: {len(allpreds)}/140")
run_all.sh Orchestration — runs the full 140-item battery per model through llama-swap 32 lines
#!/usr/bin/env bash
# Drive the eval against the llama-swap endpoint on the Ubuntu box.
# llama-swap auto-loads/swaps the backend by the request's "model" field, so you
# do NOT serve models manually -- just run the three tags below in sequence.
#
# Runtime reality (llama.cpp, 5090, --parallel 4, ~500 tok/s aggregate):
#   llama.cpp splits -c across slots, so concurrency is capped -- this is MUCH
#   slower than vLLM for long reasoning traces. Ballpark:
#     math  avg@16 (90 items)  : ~8h vibe + ~10h qwen3-think + ~1h base  ~= 19h
#     gpqa  avg@16 (198 items) : ~9h each reasoner                       ~= 20h
#   => avg@16 on everything is ~1.5-2 DAYS. So: math stays avg@16 (headline,
#   needs tight CI); GPQA drops to avg@4 (it's the control, expected weak).
#   Bump N_GPQA if you want, or move to the vLLM path for ~5-10x throughput.
set -euo pipefail
BASE="${BASE:-http://LLAMA_SWAP_HOST:8000}"
N_MATH=16
N_GPQA=4
MATH="aime,aime25,hmmt25"
COMMON="--base $BASE --concurrency 4 --max-tokens 40960"

case "${1:?usage: BASE=... ./run_all.sh vibe3b|base3b|qwen3think}" in
  vibe3b)
    python3 vibe_eval.py $COMMON --model vibethinker-3b --tag vibe3b      --n $N_MATH --temp 1.0 --cats $MATH
    python3 vibe_eval.py $COMMON --model vibethinker-3b --tag vibe3b_gpqa --n $N_GPQA --temp 1.0 --cats gpqa ;;
  base3b)
    python3 vibe_eval.py $COMMON --model base-3b --tag base3b      --n $N_MATH --temp 0.7 --cats $MATH
    python3 vibe_eval.py $COMMON --model base-3b --tag base3b_gpqa --n $N_GPQA --temp 0.7 --cats gpqa ;;
  qwen3think)
    python3 vibe_eval.py $COMMON --model qwen3-4b-think --tag qwen3think      --n $N_MATH --temp 0.6 --cats $MATH
    python3 vibe_eval.py $COMMON --model qwen3-4b-think --tag qwen3think_gpqa --n $N_GPQA --temp 0.6 --cats gpqa ;;
esac
Download the full bundlevibethinker-eval-harness.tar.gz · 127 KB · scripts + seeded items + per-model outputs + frontier writeup