Research / September 17, 2026
We trained an agent to write SQL that runs
Nemotron-Nano-8B went from 26% to 35% on 140 held-out tasks after one 44-minute GRPO round, replicated three times with the interval clear of zero. Qwen3-4B moved 3 points in four rounds, unproven, then 53% to 74% on 459 held-out tasks in a fifth round that fed the trainer differently. The reward is a program that runs the query and checks the rows.
The short version. We taught two small models to write SQL against a shop database, with reinforcement learning and no judge model in the loop. The reward is a program: it runs the query and compares the rows to a reference. One model, Nemotron-Nano-8B, went from 26% to 35% on a held-out test in a single 44-minute round, and the gain held up across three repeat scores. The other, Qwen3-4B, moved 3 points in four rounds, unproven. A fifth round with the same reward fed differently, bigger updates, only the difficulty band, cut-off replies ignored, took it from 53% to 74% on 459 held-out tasks. The recipe is public.
Paired differences: Nemotron +9 points (+5 to +13), proven. Qwen +3 points (-2 to +7), not proven.
| Item | Value | 95% interval |
|---|---|---|
| Nemotron-Nano-8B, base | 26% | 20% to 33% |
| Nemotron-Nano-8B, one round | 35% | 28% to 42% |
| Qwen3-4B, base | 58% | 52% to 64% |
| Qwen3-4B, best of four rounds | 61% | 56% to 67% |
The recipe in six steps
Seed. Start with a Postgres database that has data in it. Ours has eight tables and 300 orders. Swap in yours.
Author. A teacher model writes each question together with its SQL. We run the SQL twice and keep it only if it returns one to fifty rows both times, then drop duplicates. That gave us 741 tasks. Before doing anything else, hold out the test set: 140 tasks chosen by a hash of the id, never trained on. Every number in this post comes from those 140.
Verify. The verifier runs the model's query read-only with a
timeout and compares rows to the reference as a multiset, floats to two
decimals, column order free, row order only when the reference has
ORDER BY. A match scores 1.0, a query that runs but is wrong 0.1, and
one that does not run 0. There is no judge, so a longer answer cannot
score higher.
Benchmark. Sample each test task four times at temperature 0.7 and report pass@1 with a 95% interval, with pass@4 beside it. The gap between the two is what RL has to work with. Then read the failures, because they tell you what the reward will teach. Nemotron-Nano-8B could not run 55% of its own queries, and a verifier is very good at teaching that.
Train. GRPO with a LoRA of rank 16 and eight samples per prompt. vLLM generates inside the trainer and Postgres runs inside the container. 600 steps took 44 minutes on one H100. Leave gradient checkpointing off with TRL 0.19: on Qwen3-4B it produced garbage completions at reward zero.
Score. Serve the adapter, sample the test set four times, and take the paired difference against the base with its interval. We call a gain proven only when the interval excludes zero. A single evaluation run is marked unreplicated, so score both sides three times. Then start the next round from the best checkpoint, or stop.
What happened
Nemotron-Nano-8B moved in one round: 26% to 35%, a gain of 9 points with an interval of +5 to +13. The three repeat scores came in at +9, +10 and +11, all clear of zero. The share of its queries that failed to run fell from 55% to 35%, which is most of the gain.
| Item | Value |
|---|---|
| Base | 55% |
| After one round | 35% |
Qwen3-4B was a different story. It started at 58% and we ran four GRPO rounds plus one SFT pass on its own verified outputs. The best checkpoint reached 61%, a gain of 3 points with an interval of -2 to +7, so we cannot say it improved. By round four the training reward sat at 0.65 and the KL to the base at 0.03. The model had stopped moving, and so did we.
Same recipe, two outcomes. The base that could not run its SQL moved in under an hour. The base that already could, and got the meaning wrong instead, did not. That is why we say read the failures before you buy training steps. In the first two Qwen rounds the training reward rose while the test score did not, and without the held-out set that run would have shipped.
Getting the first merged recipe to round four took fourteen hours, six checkpoints, twelve test-set scores and under $100 of GPU.
Round five: the same reward, fed differently
We compared the Qwen run with a larger one on another schema. The reward, the tasks and the model were the same. Three things about how the trainer was fed were not.
- Answers per update. Rounds one to four updated after every question, eight answers at a time, a random walk. Round five looks at 32 questions times 16 answers, 512 per update.
- Which questions. About half the questions had become always right or always wrong, and those teach nothing. Round five trains only on the 326 questions in the difficulty band, the ones the model got right some of the time on its own attempts.
- Cut-off replies. A reply that ran out of room used to score zero, so the cheapest lesson was to think less. Round five ignores those replies instead of punishing them.
We also grew the test. 140 questions gave a band of about six points, so a real three-point gain could never be proven. 1,482 more questions, written the same way, took the held-out set to 459 and the band to about three and a half points. On that test the base scores 53% (the new questions are harder), round four 55%, and the round-four gain of +2.6 sits just outside the band. It had been real all along.
Paired differences against base: round four +2.6 (+0.1 to +5.0), step 50 +20.5 (+17.9 to +23.1), step 75 +21.8 (+19.2 to +24.5).
| Item | Value | 95% interval |
|---|---|---|
| Base | 53% | 49% to 56% |
| Round four | 55% | 52% to 59% |
| Round five, step 25 | 57% | 54% to 61% |
| Round five, step 50 | 73% | 69% to 77% |
| Round five, step 75 | 74% | 71% to 78% |
| Round five, step 100 | 74% | 70% to 78% |
Step 50 was measured twice from scratch, 0.73 both times, and the base was scored through the same server as the checkpoints (+0.4 points, inside the band). Fifty steps took about seven hours on one H100 and 25,600 scored answers. Rounds one to four had used 32,000 answers over 8,000 steps for three points.
Read the other columns before calling it new capability. pass@4 barely moved, 76% to 79%. pass^4, right on every try, went from 25% to 65%. The model learned to produce the query it could already find sometimes, every time, and to stop before the reply budget: no-query replies went from 13% to none and SQL errors from 12% to 5%. That is what a verifier reward does on a difficulty band, and it is what reliable in production means.
Next. The curve is flat from step 50 and the pass@4 ceiling is flat at 79%. Re-measure the difficulty band on the round-five model so the questions it now always gets right drop out, then a bigger base or harder questions it can solve sometimes.
For researchers
Each held-out task gets samples at temperature 0.7 and of them pass. pass@ is the unbiased estimator of Chen et al. [3], pass the share of tasks that pass on every sample, and the 95% intervals are percentile bootstraps over tasks [4] with resamples:
A before-and-after comparison is paired on the same tasks. With on each side, the reported difference and its interval come from bootstrapping the per-task differences:
Here . The verifier is a program, so the reward is verifiable in the sense of [1, §7]. For a candidate query and the reference ,
Floats compare to two decimals, column order is free, and row order
counts only when has ORDER BY. Training is GRPO [2] on a
LoRA adapter [5] with TRL [7] and vLLM generation [6]. For a prompt with
samples and rewards , the advantage of sample is
the group-normalized reward , and the
loss carries a KL penalty against the base model with coefficient
, listed per round below.
Thinking on, LoRA r16, TRL 0.19.1 GRPO. r3 vs base +0.029 (-0.016 to +0.073). Date and time archetype 0.43 to 0.58, +0.15 (+0.03 to +0.27). r4 vs r3 -0.007 (-0.048 to +0.034).
| Item | Value | 95% interval |
|---|---|---|
| base | 0.58 | 0.52 to 0.64 |
| r1 | 0.58 | 0.52 to 0.64 |
| r2 | 0.60 | 0.54 to 0.67 |
| sft-think | 0.60 | 0.54 to 0.67 |
| r3 | 0.61 | 0.56 to 0.67 |
| r4 | 0.61 | 0.54 to 0.67 |
- r1, from base: GRPO 100 steps, lr 2e-5, beta 0.04, HF generate.
- r2, from r1: GRPO 200 steps, lr 5e-5, beta 0.01.
- sft-think, from base: self-distillation on 199 verified traces, hosted SFT, 2 epochs.
- r3, from r2: GRPO 1,000 steps, lr 2e-5, beta 0.01, vLLM generation, 8 prompts per generate.
- r4, from r3: GRPO 1,000 more steps, same settings.
detailed thinking off; the reasoning mode does not engage on these prompts, and the thinking-on arm also scores 0.26. r1: GRPO 600 steps, lr 2e-5, beta 0.01, vLLM generation, steps per generation 8, max completion 512, 44 min on one H100.
| Item | Value | 95% interval |
|---|---|---|
| base | 0.26 | 0.20 to 0.33 |
| r1 | 0.35 | 0.28 to 0.42 |
560 rows per side per run. Base scored 0.26 / 0.26 / 0.26, r1 0.35 / 0.36 / 0.37. By difficulty: medium +0.10 (+0.01 to +0.18), hard +0.12 (+0.05 to +0.20), easy flat.
| Item | Value | 95% interval |
|---|---|---|
| Run 1 | +0.087 | +0.048 to +0.130 |
| Run 2 | +0.098 | +0.055 to +0.148 |
| Run 3 | +0.111 | +0.068 to +0.155 |
Correction on record: before SDK 0.51, simulate(tasks=...) drafted a
tool surface and sent it to the policy, so 42 of r3's 560 first-pass
replies were tool calls scored as failures. The r3 row is the clean
re-measure, and the polluted files are under raw/with-drafted-tools/.
The method follows the RLHF book [1]: verifiable rewards (§7), the over-optimization symptoms we watched for in the Qwen rounds (§14), and the evaluation protocol of a fixed held-out split with an interval on the paired difference (§16).
Code: recipes/04-train/text-to-sql.
Data: zero-proof-ai/text-to-sql-shop,
configs eval-base to eval-r4, eval-nemotron-8b-base,
eval-nemotron-8b-r1. Adapters zero-proof-ai/text-to-sql-shop-r1 to
-r4 and -nemotron-8b-r1.
References
- Lambert, N. (2025). Reinforcement Learning from Human Feedback. arXiv:2504.12501. Online at rlhfbook.com.
- Shao, Z., Wang, P., Zhu, Q., Xu, R., Song, J., Bi, X., Zhang, H., Zhang, M., Li, Y. K., Wu, Y. and Guo, D. (2024). DeepSeekMath: Pushing the limits of mathematical reasoning in open language models. arXiv:2402.03300.
- Chen, M., Tworek, J., Jun, H., Yuan, Q., Pinto, H. P. de O., Kaplan, J., et al. (2021). Evaluating large language models trained on code. arXiv:2107.03374.
- Efron, B. (1979). Bootstrap methods: Another look at the jackknife. The Annals of Statistics, 7(1), 1-26.
- Hu, E. J., Shen, Y., Wallis, P., Allen-Zhu, Z., Li, Y., Wang, S., Wang, L. and Chen, W. (2022). LoRA: Low-rank adaptation of large language models. International Conference on Learning Representations. arXiv:2106.09685.
- Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H. and Stoica, I. (2023). Efficient memory management for large language model serving with PagedAttention. Proceedings of the 29th ACM Symposium on Operating Systems Principles. arXiv:2309.06180.
- von Werra, L., Belkada, Y., Tunstall, L., Beeching, E., Thrush, T., Lambert, N., Huang, S., Rasul, K. and Gallouédec, Q. (2020). TRL: Transformer Reinforcement Learning [software]. github.com/huggingface/trl.
- whilehq (2026). whileai SDK [software]. Apache 2.0. github.com/whilehq/whileai-sdk.
Run it
pip install "whileai>=0.51" "psycopg[binary]" openai anthropic
git clone https://github.com/whilehq/whileai-sdk
cd whileai-sdk/recipes/04-train/text-to-sql
python rollout.py --model qwen3-4b --split holdout --k 4
python build.pyThe README has the database setup, the Modal command for training, and
delta.py for the paired score.