Suitesmith: training a 4B model to write test suites

September 4, 2026 · 11 min read

1. Finding#

I built suitesmith, an RL environment which trains a model to write test suites for functions which operate on structured data, and post-trained Qwen3.5-4B on it with GRPO.

20 steps took the 4B from 0.41 to 0.88 on the held-out eval split, matching the untrained 8B and 35B on the trained function families and their renamed versions, and tripling its score on a family it was never trained on (0.24 to 0.76).

The gain can be largely attributed to the model learning to read the spec and guess the implementation from it, which is the sub-capability the calibration said separated 4B from 8B (reference function failures fell from 220 to 33 per 512 rollouts).

The run was stopped at step 23 due to infra issues, so the result is the step-20 checkpoint; the improvement is significant on every tier, the comparisons with the larger models are descriptive. I evaluated that checkpoint twice and the two evals disagree by 0.13 on the held-out family, so that number is the least settled here.

4B, step 04B, step 20untrained 8Buntrained 35B
0.000.250.500.751.000.4790.94835B 0.9108B 0.899seen0.5020.9338B 0.94335B 0.896vocab0.2370.7628B 0.86335B 0.803window
Eval split, 90 tasks × 4 rollouts. Bars are the 4B before and after 20 steps of GRPO; the dashed lines are the untrained 8B and 35B on the same tasks. seen is new instances of the trained families, vocab is those families renamed, window is the held-out family the model never trained on.

2. Taskset and grader#

Suitesmith is an environment I constructed in order to teach the model to write test suites for functions which operate on structured data.

The model is trained on 3 types of functions which operate on structured data

The model is then evaluated on new instances of those function families, renamed versions of those functions, and a new function family

I chose to make it pass a behaviourally identical correct function which it cannot see (same logic, different variable names) so that it cannot fingerprint the reference text in the suite: a suite keyed to the reference text passes the reference, fails the twin, and scores 0 (i.e patching a reward hack).

3. Calibration#

I tested out various models prior to baselines + train + evals for model selection (and to choose thinking-mode). Subprocess runtime on a laptop, provider-default temperature, no token cap. Train split 50 tasks × 8 rollouts, eval split 90 × 8. The thinking-off eval ran twice; the two passes differ by 0.06 on seen, the same instability as §6.

ModelTrain meanTrain dead / all-nonzeroEval meanseen / vocab / windowEval dead
Qwen3.5-0.8B0.0228 of 50 carry gradient
Qwen3.5-2B0.04715 of 50 carry gradient
Qwen3.5-4B, thinking on0.6140 / 20.4970.63 / 0.585 / 0.2772
Qwen3.5-4B, thinking off0.3662 / 00.2710.384 / 0.390 / 0.03923
qwen3-8b0.9050 / 330.8560.886 / 0.955 / 0.7270
Qwen3.5-35B-A3B0.9040 / 340.8780.928 / 0.896 / 0.8090
gpt-4o-mini0.6930.82 / 0.90 / 0.35

The 8B train row is 332 rollouts, not 400.

The results show that given the suite passes the correct function, reward is saturated (kill rate among passing suites 0.97 to 1.00 for every model from 4B up). I find that the sub-capability of spec comprehension / passing the reference function for writing test suites is what holds discriminative power between models, and is what will need to be taught in order to wield the full capability of writing test suites for functions operating structured data.

I choose 4B as the checkpoint, as 0.8B & 2B lack gradient on most tasks, 8B & 35B saturate on train and sit near ceiling on evals. I chose thinking on as performance scored within a more learnable range within the window function family (0.277 vs 0.039 thinking off, with 23 of 90 eval groups dead).

I hypothesise that the gap between 4B, and 8B/35B is due to the sub-capability of spec comprehension and making guesses in implementation, post-training the model on suitesmith will close that gap.

4. Baselines#

Prime sandboxes, temperature 1.0, thinking on, 2 September 2026. The 4B is trained; the 8B and 35B are the comparison.

ModelCapTrain meanTrain deadEval meanseen / vocab / windowEval deadTruncated
Qwen3.5-4B40960.09026 of 500.0700.099 / 0.099 / 0.01357 of 9074–76%
Qwen3.5-4B81920.5211 of 500.4060.479 / 0.502 / 0.2373 of 9012–16%
qwen3-8b81920.9020.899 / 0.943 / 0.8630 of 900%
Qwen3.5-35B-A3B81920.8700.910 / 0.896 / 0.8030 of 901%

5. Training setup#

6. Results#

Eval split, 90 tasks × 4 rollouts, temperature 1.0, cap 8192, Prime sandboxes, the same settings as the baselines.

step 0step 20untrained 8Buntrained 35B
seen0.4790.9480.8990.910
vocab0.5020.9330.9430.896
window0.2370.7620.8630.803
mean0.4060.8810.9020.870
Reward on the trained batch0.400.600.801.00Policy entropy0.200.300.400.500.601510152023step
Reward on the trained batch, and policy entropy, over the 23 steps the run completed. Same x-axis, separate panels: they are different quantities and do not belong on one scale. The step-20 checkpoint is the result of record; steps 21 to 23 ran before the run was stopped.

I compared step 20 with the untrained 4B task by task, on the same 90 tasks, then asked how often chance alone would produce a gap that size. It never did:

tieruntrainedstep 20tasks up / same / downp
seen0.4790.94828 / 1 / 1< 0.0001
vocab0.5020.93329 / 0 / 1< 0.0001
window0.2370.76230 / 0 / 0< 0.0001

Against the 8B: the step-20 mean sits inside the 8B's 95% interval (0.873 to 0.928); seen and vocab indistinguishable; window behind, 0.762 against 0.863, p 0.014. Against the 35B: no tier distinguishable.

I evaluated the step-20 checkpoint twice. Run 1b resumed from it a day later on a different pod and ran its own step-0 eval on the same 90 tasks with the same settings, which makes it an unplanned replication.

run 1, step 20run 1b, step 0difference
seen0.9480.972+0.024
vocab0.9330.937+0.004
window0.7620.633−0.129
mean0.8810.847−0.034

Seen and vocab reproduce to within 0.02. Window moves 0.128, paired sign-flip on the same 30 tasks p 0.02, 17 tasks down against 7 up. The weights are identical, and a corrupted export would have moved seen and vocab too.

Almost all of that movement is the same task scoring differently between attempts, and it shows up on window because the model sits mid-range there while seen and vocab are near ceiling with no room to move. With only 30 tasks a tier this eval cannot resolve a difference smaller than about 0.09, which is the size of the gap to the 8B I just reported, so that comparison is descriptive and the fix is more eval tasks rather than more attempts per task.

What the verdict log says moved, the first 512 rollouts against the 512 around step 20 (the run continued to step 23; those last 512 are better still, 9 and 2):

passreference function failedmalformed / no tests
0%25%50%75%100%malformed / no tests 0.3%reference function failed 1.5%pass 98.2%start of runstep 20 →13,445 scored rollouts, in order
Every one of the 13,445 scored rollouts, binned in order and shown as a share of each bin. The reference function is where the model was losing, and it is what training closed.

Cost and time: 2× H100 on Prime Intellect at $8.38/h, 5.5 h of pod, $46 total including sandboxes. Steps 1 to 14 ran at 4 to 9 minutes; vLLM generation at about 7.5k tokens a second was the ceiling.

Run 1b, 3 September: ten more steps from the step-20 weights with a fresh optimiser. It trained seven before Prime's sandboxes stopped becoming reachable through the gateway and I stopped it. Trained-batch reward went 0.859 to 0.899 over those seven steps, flat within noise, and there is no post-training eval because the config evaluated at step 10.

What broke:

7. Conclusion#

Qwen-4B can be post-trained on suitesmith to write test suites for functions operating on structured data, matching 8B & 35B on the trained families and their renamed versions; on the held-out family the comparison is descriptive rather than tested: this eval cannot resolve a gap that size, and re-running the same checkpoint moved the number by more than the gap itself. Furthermore, it generalises to a function family (window) it was never trained on.

The improvement over the untrained 4B is statistically significant on every tier (paired test on the same 90 tasks, p < 0.0001); the parity with 8B & 35B is not a tested claim, and therefore I present it primarily as descriptive. One run, one seed, 20 of a planned 200 steps, stopped for pipeline reasons rather than by a stop rule, though run 1b's admission rate says the train split had little left to give; no KL-penalty variant. The eval is 30 tasks a tier and cannot resolve a difference smaller than about 0.09 on the held-out tier, which did not replicate. The impact of sampling parameters / config choices were not swept.


github.com/dtcolligan/suitesmith · step-20 weights on Hugging Face