Every method we tried to lift Qwen3 4B/8B on LiveCodeBench-multilingual OCaml, ranked by what it bought: 150 held-out problems, 16 samples at t=0.6, pass@1 against the hidden tests, paired bootstrap on every delta. The winning recipe is a stack (distil, GRPO, thinking, best-of-16 by the printed tests) reaching 0.5686 from 0.0733; the 4B follows to within 3.5 points. The verified self-loop lifts the best single sample to 0.3808, and feeding its pools back in at k=50 reaches 0.4059 single-sample and 0.6067 with the test filter. Sonnet 5 one-shots 0.8867 of the same problems.
Bar is pass@1; the tick is the oracle, the best any picker could do from those same 16 samples. Thinking and best-of-16 are inference-only, so the untrained model gets them too.
Both trained and untrained end within 3 to 7 points of their own oracle: the distance between 0.5686 and 0.4859 is training raising the pool, not the picker improving.
| Stage | What it is | pass@1 | gain | oracle@16 |
|---|---|---|---|---|
| Qwen3-8B | ||||
| 0 | standard prompt, one sample | 0.0733 | — | 0.2533 |
| 1 | distil on its own thinking-mode answers that compile | 0.2246 | +0.151 | 0.4067 |
| 2 | GRPO on that checkpoint, hidden-test reward | 0.2350 | +0.010 | 0.4600 |
| 3 | thinking enabled at deployment | 0.3546 | +0.120 | 0.6133 |
| 4 | best-of-16 by the printed tests | 0.5686 | +0.214 | 0.6133 |
| — | untrained + thinking 111 probs | 0.2545 | +0.181 | 0.5045 |
| — | untrained + best-of-16 | 0.2467 | +0.173 | 0.2533 |
| — | untrained + both 111 probs | 0.4859 | +0.412 | 0.5045 |
| Qwen3-4B | ||||
| 0 | standard prompt, one sample | 0.0187 | — | 0.0733 |
| 1 | distil from the 8B's compiling thinking-mode answers | 0.1733 | +0.155 | 0.3867 |
| 2 | GRPO on that checkpoint | running | — | — |
| 3 | thinking enabled at deployment | 0.3246 | +0.151 | 0.5667 |
| 4 | best-of-16 by the printed tests | 0.5332 | +0.209 | 0.5667 |
| — | untrained + thinking | 0.0842 | +0.066 | 0.3467 |
| — | untrained + best-of-16 | 0.0733 | +0.055 | 0.0733 |
| — | untrained + both | 0.3422 | +0.324 | 0.3467 |
No larger model is involved: the teacher is the same 8B with thinking enabled (0.2545 against 0.0733), and its rollouts become targets with the reasoning trace stripped. The only filter is the type-checker (1,854 of 2,392 kept); filtering harder toward correctness is monotonically worse (0.2263 > 0.2125 > 0.2017). LoRA rank 32, saturating at 2 epochs.
Best arm per method, same base model and eval.
| Method | Signal it trains on | pass@1 |
|---|---|---|
| self-distillation x4, verified filter, thinking | own answers passing the train problems' tests, accumulated | 0.3808 |
| distillation then GRPO, thinking | compiling own answers, then hidden tests | 0.3546 |
| distillation, thinking | compiling own answers | 0.3287 |
| distillation, no thinking | compiling own answers | 0.2246 |
| SMC-EM | public tests, resampled | 0.1237 |
| self-filter, no teacher | compiles | 0.1171 |
| GRPO / RLVR from base | hidden tests | 0.1062 |
| base + 5-line OCaml cheat sheet | nothing, prompt only | 0.0975 |
| IGUANA, importance weighting | public tests, weighted | 0.0963 |
| Qwen3-8B, untrained | — | 0.0733 |
Each round: k=8 thinking rollouts of the 300 training problems from the previous checkpoint, filter, strip the traces, train a fresh LoRA on the base model; eval thinking, k=16, held-out 150. Every cell is the same measurement: pass@1 on the held-out 150 in thinking mode, estimated from 16 samples per problem. In the charts, solid lines are pass@1 and dashed lines are oracle@16 from the same rollouts (the share of problems where any of the 16 samples passes); the verified line is drawn wider, so on the shared trunk through round 2, where the two chains are the same run, both colors stay visible. The columns differ only in the filter applied to the TRAINING data between rounds: compile keeps every rollout that type-checks; verified keeps only rollouts that pass their own training problem's tests (the train split's suite, never the eval set) and accumulates kept pools across rounds. The chains are identical through round 2 and diverge at round 3. Starting points: untrained 8B 0.2485 (re-measured on the full 150 in the k=50 re-eval), untrained 4B 0.0842; the 4B's round-1 adapter (239 rows) scores 0.2550, so the loop's first and smallest round does most of the lifting.
| round | 8B, compile | 8B, verified | 4B, compile | 4B, verified |
|---|---|---|---|---|
| untrained | 0.2485 | 0.0842 | ||
| 1 | 0.3287 (= sol_think) | 0.2550 (239-row adapter) | ||
| 2 | 0.3387 | 0.3025 | ||
| 3 | 0.3225 | 0.3663 | 0.2458 | 0.2967 |
| 4 | 0.3162 | 0.3808 | 0.2075 | 0.3213 |
The compile-filter loop collapses and its own pool hides it: both sizes peak at round 2 and fall, the 4B by 9x the ±0.006 noise floor, while pool pass keeps rising (0.3654 to 0.4029 on the 8B). Verification turns collapse into compounding: keep only rollouts that pass their own training problem's tests, accumulate the pools, and the chain rises to 0.3808. Accumulation alone is not the fix (0.3246), and this was bet against in writing: purity loses to compile-filtering in one-shot distillation, but it is the only filter that resists distribution narrowing.
Matched A/B, both arms a fresh LoRA on base Qwen3-8B, same hyperparameters, pools generated by the same checkpoint; arm B adds Ag-MBPP-X rows to the LCB training set.
| arm | rows | LCB 150 | Ag-MBPP-X 95 |
|---|---|---|---|
| A: LCB only | 1,497 | 0.3329 | 0.6178 |
| B: LCB + MBPP-X | 3,131 | 0.3421 | 0.6796 |
| seed noise (same data, two runs) | 1,497 | +0.0058 | – |
The MBPP-X gain is decisive (+0.0618, CI [+0.024, +0.101]); the LCB gain (+0.0092, P=0.723) is indistinguishable from the seed-noise row. Transfer runs one way only, and mixing 52% out-of-domain rows costs nothing on LCB. ±0.006 is the seed-noise floor for single-run adapter comparisons here.
A fresh LoRA on the base model per size, trained on k=50 thinking rollouts drawn from the round-3 verified self-loop checkpoints over both training sets (LCB-train 300 and MBPP-X-train 264), pass-filtered. Evaluated on the same 150 held-out problems at k=50.
| cell | pass@1 | + compiles | + public tests | oracle@50 |
|---|---|---|---|---|
| 8B, thinking | 0.4059 | 0.5000 | 0.6067 | 0.6933 |
| 4B, thinking | 0.3315 | 0.4200 | 0.5800 | 0.6467 |
| 8B, no thinking | 0.1964 | 0.2400 | 0.3733 | 0.4333 |
| 4B, no thinking | 0.1557 | – | – | – |
Both campaign records, and the pool improved as much as the picker: oracle@50 reaches 0.6933 against 0.6733 for the previous best 8B pool. The nothink rows move the other way (0.1964 and 0.1557 against 0.2246 and 0.1733 for the original distillation): training on verified thinking rollouts buys thinking accuracy and costs direct-answer accuracy.
Six levers, each its own trained arm against the same 0.0975 reference.
| Lever | pass@1 | vs reference |
|---|---|---|
| graded reward: compile 0.25 + public 0.25 + hidden 0.5 | 0.1062 | +0.009 (P=0.93) |
| 2 epochs instead of 1 | 0.0975 | +0.000 (P=0.51) |
| the paper's training cheat-sheet prefix | 0.0892 | −0.008 (P=0.10) |
| hidden-test reward via SMC-EM | 0.0838 | −0.014 |
| the paper's own 2,000-problem training set | 0.0717 | −0.026 (P=0.001) |
| per-token vs sequence loss, t=0.7 vs 1.0 | measured nulls | |
The graded reward is the informative failure: it lifted gradient-carrying rollout groups from 38% to 95% and still moved only +0.009, not significant. Starvation was real but not binding; the base model's own success rate is.
| Qwen3-8B, untrained | pass@1 |
|---|---|
| bare prompt | 0.0488 |
| one sentence of language guidance | 0.0733 |
| a 5-line OCaml cheat sheet: operators, casts, I/O idioms | 0.0975 |
Five lines of prompt buy what the RL buys (0.0975 either way), and they stack: sheet + 16 samples + test filter reaches 0.2841 with no training. It hurts the strongest distilled checkpoints (−0.020 to −0.033), so: on for base and RL-band models, off for distilled ones.
| Model | 1 sample | + compiles | + public tests | oracle |
|---|---|---|---|---|
| 8B distil + GRPO + thinking | 0.3546 | 0.4455 | 0.5686 | 0.6133 |
| 8B distil + thinking (no GRPO) | 0.3287 | 0.4159 | 0.5550 | 0.5867 |
| 4B distil + thinking | 0.3246 | 0.3984 | 0.5332 | 0.5667 |
| 8B untrained + cheat sheet | 0.0975 | 0.2012 | 0.2841 | 0.2933 |
| 4B untrained + thinking | 0.0842 | 0.3117 | 0.3422 | 0.3467 |
| 4B untrained, no thinking | 0.0187 | 0.0525 | 0.0733 | 0.0733 |
The filter uses only tests printed in the problem statement and recovers 92 to 97% of oracle on every arm. It only re-ranks what the sampler produced, so it cannot replace training.
The two best thinking arms resampled at 50 per problem, same 150 problems. Everything else on this page stays at k=16; this is the only table where two ks meet.
| arm | k | random | + compiles | + public tests | oracle |
|---|---|---|---|---|---|
| 8B distil + GRPO + thinking | 16 | 0.3577 | 0.4487 | 0.5727 | 0.6180 |
| 50 | 0.3867 | 0.4400 | 0.5933 | 0.6867 | |
| 8B distil + thinking (no GRPO) | 16 | 0.3317 | 0.4270 | 0.5637 | 0.6047 |
| 50 | 0.3467 | 0.4333 | 0.6000 | 0.6733 |
pass@1 is stable (0.3596 at k=50 against 0.3546 at k=16), so the k=16 rows are not undersampled; the pool keeps growing (oracle +7 points on both arms); and the filter converts part of it, best-of-50 reaching 0.6000. No sample passed the hidden suite while failing the public tests, so the ceiling is set entirely by plausible-but-wrong programs.
Every sample of every stage, labelled with difficulty and one mutually-exclusive failure reason; compiling-but-failing samples were re-executed against the hidden suite.
| Stage | easy (50) | medium (46) | hard (54) | all |
|---|---|---|---|---|
| Qwen3-8B | ||||
| 0. untrained | 0.2025 | 0.0177 | 0.0012 | 0.0733 |
| 1. distil | 0.5787 | 0.0978 | 0.0046 | 0.2246 |
| 2. distil + GRPO | 0.5837 | 0.1114 | 0.0174 | 0.2350 |
| 3. distil + GRPO + thinking | 0.7412 | 0.2609 | 0.0764 | 0.3546 |
| untrained + thinking 111 probs | 0.5970 | 0.1371 | 0.0312 | 0.2545 |
| Qwen3-4B | ||||
| 0. untrained | 0.0563 | 0.0000 | 0.0000 | 0.0187 |
| 1. distil | 0.4437 | 0.0693 | 0.0116 | 0.1733 |
| 3. distil + GRPO + thinking | 0.7250 | 0.1957 | 0.0637 | 0.3246 |
| untrained + thinking | 0.2250 | 0.0258 | 0.0035 | 0.0842 |
Absolute = share of all 16 samples (comparable across stages); relative = share of that stage's failures only.
| Stage | pass | wrong answer | crash | timeout | syntax | type error | unbound | truncated | other |
|---|---|---|---|---|---|---|---|---|---|
| Qwen3-8B, % of all 16 samples | |||||||||
| 0. untrained | 7.3 | 6.5 | 4.2 | 2.0 | 33.0 | 21.3 | 20.5 | 2.5 | 2.6 |
| 1. distil | 22.5 | 24.8 | 9.8 | 10.2 | 4.9 | 16.6 | 6.4 | 2.5 | 2.4 |
| 2. distil + GRPO | 23.5 | 25.6 | 7.5 | 10.3 | 5.4 | 16.2 | 6.2 | 2.2 | 3.1 |
| 3. distil + GRPO + thinking | 35.5 | 10.8 | 9.8 | 5.7 | 9.3 | 8.2 | 7.0 | 11.0 | 2.7 |
| distil + thinking, no GRPO | 32.9 | 8.7 | 12.0 | 6.0 | 10.4 | 7.5 | 8.6 | 10.7 | 3.2 |
| untrained + thinking | 25.4 | 4.8 | 6.0 | 1.3 | 22.1 | 12.0 | 14.4 | 11.0 | 3.0 |
| Qwen3-4B, % of all 16 samples | |||||||||
| 0. untrained | 1.9 | 1.0 | 0.8 | 0.8 | 48.8 | 11.2 | 24.9 | 8.2 | 2.5 |
| 1. distil | 17.3 | 23.9 | 12.8 | 8.6 | 6.5 | 18.2 | 8.1 | 2.4 | 2.1 |
| 3. distil + GRPO + thinking | 32.5 | 14.9 | 10.5 | 6.2 | 10.0 | 13.8 | 7.6 | 0.7 | 3.9 |
| untrained + thinking | 8.4 | 1.2 | 0.6 | 0.2 | 49.0 | 12.9 | 19.3 | 6.9 | 1.5 |
Training relocates errors more than it removes them: distillation collapses syntax from 33% to 5% of samples and the mass reappears as wrong answers (6.5→24.8%) and timeouts (2.0→10.2%). Thinking converts wrong answers into passes (25.6→10.8%) and pays in truncation (2.2→11.0%), a budget artifact and the clearest remaining lever. The dominant crashes are stdin parsing, the teacher's Scanf pathology at runtime.
Without a toolchain or sandbox, can a classifier read the model's own activations and pick the right sample? One teacher-forced prefill, mean-pooled hidden states from one layer, logistic regression; baselines are the compile gate and an off-the-shelf LM judge.
| Probe | Winner on validation | Pooled AUROC | Within-problem | Mixed problems |
|---|---|---|---|---|
| base 8B, linear | mean pool, layer 27/36, C=0.1 | 0.8365 | 0.660 | 37 |
| base 8B, MLP | last pool, layer 36/36 | 0.9095 | 0.733 | 37 |
| base 4B, linear | mean pool, layer 36/36, C=0.01 | 0.8845 | 0.747 | 11 |
The drop from 0.84 pooled to 0.66 within-problem is the whole story: most of what the probe knows is which problem is hard, and only within-problem ranking matters to a selector. An MLP head buys +0.073 AUROC while changing not one selected sample.
| Selection rule | What it costs | untrained 8B | distilled 8B |
|---|---|---|---|
| one sample, no selection | — | 0.0733 | 0.2246 |
| probe argmax | 1 forward pass | 0.1267 | 0.3067 |
| compile gate | ocamlc per sample | 0.1477 | 0.2579 |
| LM judge, Python-trained, zero-shot | 8B forward pass | 0.1067 | 0.2733 |
| compile + LM judge | both | 0.2000 | 0.2933 |
| compile + probe | both | 0.1733 | 0.3333 |
| public tests | execution + sandbox | 0.2467 | 0.3844 |
| public tests + probe | both | 0.2467 | 0.4000 |
| oracle | — | 0.2533 | 0.4067 |
Does the probe know anything the tests do not? On the distilled checkpoint, yes: +0.0156 over the public filter (CI [+0.0036, +0.0303]), closing 70% of the gap to oracle. On the untrained model it moves nothing.
Pipeline and reproduction: experiments/ocaml_clf/ (dataset build, sharded
activation extraction, probe training, ladders), reusing the LCB probe tooling unchanged.
One reply per problem, no retries, no feedback, no tools: a sandboxed subagent with every tool denied, verified by a live probe; compilation and grading stay on our side. The prompt is byte-identical to the content the Qwen3 arms are evaluated with.
| arm | pass@1 (standard 150) | full test split (154) |
|---|---|---|
| Sonnet 5, frozen eval prompt | 0.8867 133/150 | 0.8831 |
0.8867 in one sample, against 0.4059 for our best single-sample arm: the headroom is capability, not prompt or selection. Sonnet type-checks 96.7% of programs, so the syntax battle that consumes the small models does not exist for it; its 18 failures are almost all semantic.
Their reported OCaml numbers and ours under their protocol: their prompt, their sampling, their comparator, 20 samples at t=0.2.
| Cell | reported | ours | verdict |
|---|---|---|---|
| Qwen3-4B, untrained | 1 | 1.8 | reproduces |
| Qwen3-8B, untrained | 0 | 4.4 | does not reproduce |
| 8B, RL gain over its own baseline | +7 | +6.5 | reproduces |
| 4B, after their RL recipe | 7 | 2.3 | does not reproduce |
Their 8B baseline is the anomaly, not their pipeline: their 4B baseline lands where ours does, and we exhausted the request side. Their headline 8B 0 to 7 is measured against a baseline our replication puts at 4.4. Their 4B gain does not reproduce under LoRA on any axis; full fine-tuning on 8xH100 is the one untested lever. The effect size itself replicates, +6.5 against their +7.
Scanf.scanf "%d" in 70% of samples (8B:
11%), collapsing correct-given-compiles from 0.62 to 0.43.