Tinker RL de-risk — round 2 · 2026-07-10

M1: the first curriculum rung

Multi-turn GRPO on a deliberately harder Lean tier, asking two questions: (i) can we train on M1 from scratch, and (ii) does training on M0 first provide uplift? Companion to the M0 results page.

ROUNDS 1-2 COMPLETE ROUND 3 LIVE Round 3 (goal: reach rung 5; $300): M3/M4/M5 datasets shipped (240/240 certified each); calibration puts M4 at a brutal 1.7% cold baseline and M5 (insertion-sort suite) at 6.7%. Eight arms racing: M5 configs (cold-20b, cold-120b, ladder m3→m4→m5, mixed pool) + warm-start hypothesis tests. First evals @9: no arm starves when a bridge exists — even M4's 1.7% cliff trains (18.3% @9 cold). M5 cold already 15.0%, ladder 21.7%. Round-2 verdict below still stands.
Round-2 recap: cold-per-rung wins; warm starts cost ~20pts of next-tier transfer; scratch models zero-shot the next tier at 78–82%. Round-3 budget: ~$12 of $300 spent.

Why this round

M0 established that GRPO on Tinker trains open models on locally-graded Lean proof-completion (18%→88.5% single-turn; multi-turn credit assignment verified). The plan (PR #138, docs/curriculum-plan.md) is a ladder to the real Rocq→Lean task where each rung moves one axis: rollout length, difficulty, or infra.

M0 ──────► M1 ──────► M2 ──────► M3 ──────► M4 ──────► M5 ──────► M6
(done)    (THIS)
one-shot  multi-turn  tool loop   containers  harder      Rocq       real binary-
tactic    + harder    (run lean,  + sandboxed  Lean        syntax,    equivalence
block     tier, w/    read goal,  grading      (mathlib-   toy        tasks
          grader      edit)                    style)      theorems   (seeded)
          feedback

M1 moves the difficulty axis while keeping the multi-turn loop M0 already validated (rl11: 40%→92.3%). The interesting scientific question is the transfer one: M0's warm-start experiments cut both ways — SFT warm-starts capped the RL ceiling (entropy collapse: 79.8% vs cold 88.5%), while the output-protocol skills RL taught were worth ~60 points. Does "train on M0 first" behave like a head start or a trap?

Hypotheses (with priors, set before launch)

idclaimpriorevidence that moves it
H6Multi-turn GRPO trains from scratch at a 3–5% turn-1 frontier0.70train-reward slope over first 20 iters of rl12
H7aM0-first gives a head start on M1 (higher curve early)0.85rl13 phase-1 entry vs rl12 at same iter count
H7bM0-first raises (not caps) the final M1 plateau0.60final heldout of rl13 vs rl12; watch for entropy-trap signature (pass@1≈pass@k)
H9Lean error feedback is informative at this tier (turn-2 conditional pass > turn-1)0.80solved_by_n_turns in evals

Numbering continues the M0 scratchpad's H1–H5; full set incl. H8, H10–H12 in docs/curriculum-plan.md.

How it's all built, in plain terms

The task

Every task is one Lean 4 file: a few custom definitions (fresh random names like revr17, so nothing can be pattern-matched from a library) plus one theorem whose proof is sorry. The model sees that file and answers with ONLY a tactic block in a ```lean fence. The harness — never the model — splices that block into the fixed statement, so weakening the theorem or smuggling axioms is impossible by construction.

How a dataset (rung) gets built

Each rung is a set of parametric generator functions (~30 lines each in src/factory.py): a generator picks random names and constants, renders the definitions, the statement, and — crucially — a gold proof written as a template. The build script (scripts/build_dataset.py --set m3) generates 240 candidates, runs every gold proof through the real grader, and ships only tasks whose golds certify (M1–M5 all certified 240/240). A stratified split (5 heldout per family) is frozen at build time and never changes. Before writing a generator, each gold-proof pattern is verified by hand as a standalone Lean file — that iteration loop is minutes, not hours. Every rung also includes a 1-in-6 "bridge" family of trivial concrete evaluations — round 2 proved these are load-bearing: without them, cold-start GRPO gets zero reward everywhere and no gradient.

The grader (and why there are no containers)

Grading is a plain local Python function, grade_sync — no Docker, no service: (1) reject forbidden tokens (sorry, axiom, native_decide, set_option, …); (2) splice the tactic block into the fixed file; (3) run lean file.lean as a subprocess (~0.07–2s); (4) parse the #print axioms output and require it to be a subset of {propext, Quot.sound, Classical.choice} — this is what catches a smuggled sorry (it surfaces as sorryAx). Concurrency is a semaphore of ≤16 lean processes. Containers only enter the plan at the tool-loop rung (see docs/curriculum-plan.md): once the model can run commands or edit files, grading needs isolation; until then a local function is faster and has no orchestration bugs.

Rollouts: multi-turn, but no tool calling

There is deliberately no tool-calling protocol. A training episode is: prompt → model emits a tactic block → harness grades it → if it failed, the grader's error output is appended as a plain user message ("Your proof failed: … Provide a corrected tactic block") and the model tries once more (2 turns in training, up to 3 in evals). The model's move is always one fenced tactic block. GRPO treats the whole episode as one unit: reward 1 if any turn passed, group-centered advantages applied to every sampled token of every turn. Tool calling is the next infra rung on the ladder, not part of these rounds.

Rung by rung: what each dataset actually contains

Every rung: 240 tasks (200 hard + 40 tier-1 bridge), 6 parametric families, 210 train / 30 heldout (5 per family), 100% gold-certified, fresh random names per task. "cold base" = gpt-oss-20b multi-turn baseline on that rung's heldout.

M0 — the de-risk pools (v1–v4, 46→317 tasks)

Hand-written templates first (concrete arithmetic, single inductions, helper-lemma list/tree lemmas), then the v4 parametric factory (affine recursions f (n+1) = f n + c ⊢ closed forms, repeat/sum/len). Cold base 18–23%.

M1 — first hard tier (cold base 5.0%)

familystatement shapewhat makes it hard
poly_square (t4)f(n+1)=f n+2n+1 ⊢ f n = n²+bnonlinear closed form: must normalize with succ_mul/mul_succ before omega can see it
compose_affine (t4)g(g n) = c²n + (ca+a)invent a closed-form helper, compose it with itself
map_rev (t4)map f (rev l) = rev (map f l)needs a map-over-append helper lemma
fold_rev (t4)sum/count invariant under reversefold-over-append helper
revacc (t5)accumulator reverse = naive reversethe classic puzzle: helper must be ∀-quantified over the accumulator, plus two append lemmas
concrete (t1 bridge)f 7 = 52rfl — keeps early groups off zero variance

M2 — multi-helper chains (cold base 10.0%)

familystatement shapehardness
rev_invol (t6)rev (rev l) = lthree chained helpers: app_nil, app_assoc, rev-over-app
sum_map_mul (t5)sum (map (c·) l) = c · sum linduction + distributivity
count_filter (t5)count k (filter p l) ≤ count k ldouble case-split (p x × x==k) per step
compose_two (t5)f (g n) closed form, f≠gtwo independent closed-form helpers
height_mirror (t5)height (mirror t) = height ttree induction + Nat.max_comm
concrete (bridge)eval of sum∘maprfl

M3 — accumulators & double induction (cold base 23.3%)

familystatement shapehardness
sumacc (t6)sacc l 0 = sum lgeneralize to ∀ acc: sacc l acc = sum l + acc or the induction fails
tsumacc (t7)tree sum with threaded accumulatorsame generalization, two recursive calls threading acc through both branches
takedrop_len (t6)len (take n l) + len (drop n l) = len linduction on n with inner case-split on l, ih applied at xs
pow_mul (t7)b^(n·m) = (b^n)^mneeds pow-of-sum helper, then nested induction with mul_comm juggling
filter_idem (t6)filter p (filter p l) = filter p lif-split where the hypothesis must be reused inside the branch
concrete (bridge)accumulator evalrfl

M4 — the hardest cliff (cold base 1.7%!)

familystatement shapehardness
insert_count (t7)count j (ins k l) = [k==j] + count j lpositional insert has an if in the recursion; proof needs induction × two case-splits
flatten_tsum (t7)sumlist (flatten t) = tsum tsum-over-append helper + tree induction
pow_lt_mono (t7)n < m → 2^n < 2^man IMPLICATION: induction must carry the hypothesis, plus a positivity helper
rev_map_rev (t7)rev (map f (rev l)) = map f lcomposed helper chain through two structures
count_take_le (t7)count k (take n l) ≤ count k ldouble induction + case-split
concrete (bridge)ins/flatten evalrfl

M5 — the target rung: insertion sort (cold base 6.7%)

familystatement shapehardness
isort_count (t8)count j (isort l) = count j lpermutation-by-counts: needs the insert-count lemma as a helper, then sort induction
isort_len (t8)len (isort l) = len linsert-length helper + induction
isort_sorted (t8)sortedb (isort l) = truethe invariant proof: insert preserves sortedness — case analysis inside an inductive invariant
mirror_flatten (t8)flatten (mirror t) = rev (flatten t)3-helper chain (rev/app laws) woven into tree induction
gauss_div (t8)f n = n(n+1)/2 + a (real Nat division)prove the 2× version by induction, then let omega handle the /2
concrete (bridge)isort evalrfl

The M1 dataset: 240 certified tasks, built to sit on the frontier

Same recipe as M0 (parametric generators → gold proof per task → every gold certified by the real grader before shipping — 240/240 passed first build), but the families are a tier harder. Six families, ~1-in-6 tasks is an easy "bridge" so early groups aren't all zero-variance:

familytierwhat makes it hard
m1_poly_square4f (n+1) = f n + 2n+1 ⊢ f n = n²+b — nonlinear closed form; needs the right simp-normalization before omega can see it
m1_compose_affine4g (g n) = c²·n + (c·a+a) — must invent a closed-form helper lemma, then compose it with itself
m1_map_rev4map f (rev l) = rev (map f l) — needs a map-over-append helper
m1_fold_rev4sum/count invariant under reverse — needs a fold-over-append helper
m1_revacc5tail-recursive reverse = naive reverse — the classic generalize-the-accumulator puzzle (helper must be ∀-quantified over acc) plus two append lemmas
m1_concrete1bridge: concrete evaluation of the quadratic recursion (rfl)

Every task gets fresh random function names (revr17, zetaq42, …) so memorized lemma names from M0 training don't apply — the induction structure has to be found. Split: 210 train / 30 heldout, stratified 5-per-family. Lives at data/m1/tasks.jsonl (the data dir is now organized as data/m0/ + data/m1/).

Frontier calibration: the band is exactly where a rung wants it

Before spending on RL, both reference policies ran the M1 heldout (30 tasks × 2 repeats, 2 turns with error feedback, $0.30 total):

5.0%
base gpt-oss-20b, multi-turn (3.3% turn-1) — all five hard families 0/10
65.0%
rl5 checkpoint (M0-trained), zero-shot (55% turn-1) — but compose_affine 0/10, revacc 1/10
family (10 attempts each)base 20brl5 ckpt (M0-trained)
m1_concrete (bridge)3/1010/10
m1_poly_square0/109/10
m1_map_rev0/1010/10
m1_fold_rev0/109/10
m1_compose_affine0/100/10
m1_revacc0/101/10

Two headlines. First, zero-shot M0 transfer is huge (5%→65% without ever seeing an M1 task): the induction/helper-lemma skills and output protocol generalize to new families and names. Second, the dataset still has a genuine frontier — two families the M0-trained policy can't touch. That 0→65% spread across policies is the shape a curriculum rung wants: trainable-from-warm, cliff-from-cold, headroom for both.

The two arms

rl12-m1-scratchrl13-m1-warm
question(i) trainable from scratch?(ii) does M0-first help?
schedule60 iters on m125 iters on m0/v4pool (phase 0), then 60 iters on m1
evalsame fixed m1 heldout (30 tasks, k=4 episodes), every 10 iters, throughout both phases
shared knobsgpt-oss-20b · LoRA rank 32 · lr 4e-5 · importance-sampling GRPO · G=8 episodes × 12 problems/iter · max_turns 2 · temp 1.0 · 8k tokens/turn

Round 2b: two more arms + the next rung's dataset (launched 04:30 UTC)

runquestiondesign
rl14-m1-nobridgewas the tier-1 bridge load-bearing for the cold start (H8)?scratch, 60 iters, m1 train WITHOUT the 40 bridge tasks; identical heldout
rl15-m1-120bdoes the rung transfer across model scale?gpt-oss-120b scratch, 40 iters, same m1 tasks (+ base-120b calibration eval)
data/m2/next difficulty tier, pre-built & certified (240/240)multi-helper chains (rev∘rev=id needs 3 helpers), double case-splits, two-function composition; zero-shot calibration vs the M1 finals when they land

Round 2c: the M2 rung (launched 06:45 UTC)

runschedulequestion
rl16-m2-scratch40it m2cold-start on the harder tier
rl17-m2-warmM025it m0 → 40it m2one-rung skip (does M0 transfer stretch two tiers?)
rl18-m2-warmM0M125it m0 → 25it m1 → 40it m2the full ladder climb — does sequential curriculum beat skipping?

All on the fixed m2 heldout (30 tasks). The trainer now supports arbitrary multi-phase curricula (comma-separated pre-phase files).

M2 learning curves: cold vs rung-skip vs full ladder

Round 3: M5-race heldout trajectory (m5 heldout; HW-test arms on their own rungs)

iterrl21 cold-20brl22 cold-120brl23 ladderrl24 mixedrl25 m4-coldrl19 m3hard-cold
— (base)6.7%13.3%6.7%6.7%1.7% (m4)~8% (m3-hard)
915.0%21.7% (m3 phase)15.0%18.3%43.3%

M2 heldout trajectory (multi-turn pass, k=4)

iterrl16-coldrl17-M0→M2rl18-M0→M1→M2
— (refs)base-20b 10.0% · M1-trained zero-shots: rl15 (120b) 81.7% · rl12 (20b) 78.3%
947.5%51.7% (m0 phase)45.0% (m0 phase)
1965.8%60.8% (m0)55.0% (m0)
2975.8%66.7%60.8% (m1 phase)
39–4977.5% final (p@4 97%)70.0% → 65.0% (stopped @49, budget)56.7% → 55.0% (m1 phase — m2 score STALLS during m1 training)
59–6965.8% → 71.7% (m2 phase, still rising; stopped @69, budget)

M2 calibration: base-20b 10.0% mt (bridge only — same cliff shape as M1). And the round's most striking transfer result: rl15's M1-trained 120b zero-shots the M2 heldout at 81.7% (turn-1 70%; rev_invol 8/10, compose_two 8/10, only count_filter lags at 4/10). And it's not a scale effect: rl12's M1-trained 20b zero-shots M2 at 78.3% (turn-1 63%) — within noise of the 120b. Climbing one rung nearly solves the next for free at both scales — but only for the scratch-trained model: rl13, the M0-warm-started arm that matched rl12 on the M1 heldout (75.8% vs 77.5%), zero-shots M2 at just 58.3%, 20 points behind. The warm start's hidden cost isn't the in-distribution plateau — it's off-distribution generalization. The binding constraint is per-rung skill acquisition, not task novelty.

Why phase-0-in-one-process rather than loading the rl5 checkpoint: Tinker sampler-weights checkpoints can't seed a training client (create_training_client_from_state rejects them — only save_state training checkpoints load). Both new runs save proper training state at the end, so future rungs can warm-start directly.

Results updating live

So far: rl12-m1-scratch train reward flat at 5–10% through iter 7 (2–5 of 12 groups producing gradient — the cold-start cliff is binding, H6 under real stress). rl13-m1-warm climbed its M0 phase 8%→73% train reward by iter 9, and its iter-9 M1 heldout eval — before a single M1 training task — reads 48.3% (pass@4 70%).

Learning curves: train reward and m1 heldout pass rate for both arms

Heldout trajectory (m1 heldout, multi-turn pass rate, k=4 episodes/task)

iterrl12-m1-scratchrl13-m1-warm
— (baselines)5.0% (base 20b)65.0% (rl5 ckpt ref)
914.2% (p@4 37%)48.3% (phase 0 — M0 tasks only; p@4 70%)
1953.3% (p@4 73%)52.5% (end of phase 0; p@4 70%)
2963.3% (p@4 77%)60.8% (4 M1-iters into phase 1; p@4 80%)
3968.3% (p@4 83%)67.5% (p@4 83%)
4978.3%65.8%
29 (rl15-120b)72.5%
19 (rl15-120b)68.3% — the 120b matches the 20b's iter-39 level in half the iterations
5977.5% final (p@4 93%)71.7%
69–8473.3% → 74.2% → 75.8% final (p@4 93%)
39 (rl15-120b)72.5% final (p@4 93%) — and zero-shots M2 at 81.7%
10 (rl14-nobridge)STOPPED: reward 0.00, heldout 6.7% ≈ baseline. Without bridge tasks, cold-start GRPO starves — the tier-1 bridge is load-bearing (H8 confirmed)
9 (rl15-120b)51.7% — the 120b goes 5%→51.7% in 9 iters (vs ~19 for the 20b); rl14-nobridge still ~2% train reward at iter 3

Infra changes this round (all flags, no forks)