The mechanism
export writes one JSON object per line, in the shape trainers read,
and refuses a broken row. The training is one command on your Modal
account: the
SFT recipe
trains a small open model with a LoRA adapter on that file, on one A10G,
in about ten minutes, for under twenty cents. It also runs the base model
over the held-out tasks three times before it trains anything, because a
score bounces from run to run and you need to know by how much before
you can call a change real. That spread is the noise floor.
The proof is a paired comparison. Every held-out task is run by both
models, the difference is taken task by task, and the interval on the
average difference has to exclude zero and clear the noise floor. Paired,
because the same hard task is hard for both models, and pairing cancels
that out. The library also refuses to compare two runs that were not on
the same tasks.
Run it
Lessons 2 to 6 used the stand-in agent, because the ideas run offline. This lesson trains a real model, so it is the one lesson that needs an account:modal token new on your laptop. The judge is lesson 3’s,
unchanged, and that is the point: it reads messages, so the same
program scores the stand-in’s rows here and the trained model’s rows in
step 3.
Step 1 writes more rows than the earlier lessons, because sixty-four rows
train nothing: four wordings of every ask, split by task the way lesson 5
said to, the passes kept the way lesson 6 said to.
holdout.jsonl carries the forty locked tasks with the answer
the fake world gave each lookup, so the trained model is asked the same
question in the same world.
Step 2 is the training. One command, on your account, from the directory
the two files are in. It loads Qwen/Qwen2.5-1.5B-Instruct, runs the
base model over the held-out asks three times, trains a LoRA adapter for
forty steps, runs the adapter once on the same asks and the same seed,
and writes every sampled row back to your laptop as holdout_rows.jsonl.
This is what it printed on the run the rest of this page reads from.
compare takes it directly.
+0.05 [-0.03..+0.13], the verdict would be
no difference, and the honest sentence is “training did not move it”.
Two more things the line says. pass^4 went from 7% to 41%: the share
of asks the model gets right every time, which is what a customer sees,
went up six times over and is still under half. And forty tasks is fewer than the 89
that lesson 4’s holdout_size asked for; the interval is as wide as
that implies, and the gain cleared it because it is large, not because
the set is.
One PASS is one PASS. Three passes of the base model measured how much
the eval moves; the training ran once. Run the training again under a
different
--run-name before you tell anyone. A result you can repeat
is the only kind this library is built to produce.Then it starts again
The model you served is now the agent. Its traffic is the next set of rows. Lesson 2 to lesson 7, again, on what it still gets wrong. That is the loop, and the reason the agent gets better while it works.Where to go now
- Quickstart: the same program, with your own agent and your own judge.
- Your model and your key: name the model as a string, keep the key in the provider’s own variable.
- Evals: the measurement half on its own, for a team that is not ready to train.
- Train on your own GPU: the SFT recipe this lesson ran, and GRPO and DPO on the same base, with the paired delta at the end.
- Papers, reproduced: one recent post-training paper per recipe, each with its verdict.
Where it comes from
- Miller, E. Adding Error Bars to Evals. arXiv:2411.00640, 2024. The paired difference, and why it is the right test for before and after.
- Hu, E. J. et al. LoRA: Low-Rank Adaptation of Large Language Models. arXiv:2106.09685, 2021. The adapter the recipe trains.
- Lambert, N. Reinforcement Learning from Human Feedback. arXiv:2504.12501, 2025. Chapters Instruction Finetuning and Evaluation: SFT, and the noise floor from re-running an eval.
- Shao, Z. et al. DeepSeekMath. arXiv:2402.03300, 2024. The GRPO recipe next to this one.