Blog

Research / September 17, 2026

RL for text to SQL

Two small models learn to write SQL with reinforcement learning and no judge model. The reward is a program that runs the query and checks the rows. Nemotron-Nano-8B went from 26% to 35% in 44 minutes. Qwen3-4B needed a better-fed trainer, then 53% to 74%. A newer 9B base reached 82% in one round.

The short version. We wanted small models that turn a plain-English question into a SQL query for a shop's database. Instead of a judge model, the reward is a program: it runs the query and checks the rows. Nemotron-Nano-8B improved in a single 44-minute round, and the gain held up on three repeat scores. Qwen3-4B barely moved in four rounds. Feeding the trainer differently in a fifth round took it from 53% to 74%. A newer base, Qwen3.5-9B, reached 82% in one round, once we found two bugs in our own tooling that had hidden the gain. The recipe is public.

pass@1 on 140 held-out tasks, with 95% intervals

Paired differences: Nemotron +9 points (+5 to +13), proven. Qwen +3 points (-2 to +7), not proven. The interval is the range the true number very likely sits in.

pass@1 on 140 held-out tasks, with 95% intervals
ItemValue95% interval
Nemotron-Nano-8B, base26%20% to 33%
Nemotron-Nano-8B, one round35%28% to 42%
Qwen3-4B, base58%52% to 64%
Qwen3-4B, best of four rounds61%56% to 67%

The recipe in six steps

Seed. A Postgres database with data in it. Ours has eight tables and 300 orders.

Author. A teacher model writes each question with its SQL. We keep a task only if the SQL returns one to fifty rows on two runs. That gave 741 tasks. Before anything else, set aside the test set: 140 tasks picked by a hash of the id, never trained on.

Verify. The verifier runs the model's query read-only and compares the rows to the reference. A match scores 1, wrong rows 0.1, a query that does not run 0. There is no judge, so a longer answer cannot score higher.

Benchmark. Sample each test task four times. Report the first-try pass rate (pass@1) with its 95% interval, beside the rate for any of four tries (pass@4). The gap between them is what RL can work with. Then read the failures.

Train. GRPO with a LoRA adapter and eight samples per question. 600 steps took 44 minutes on one H100.

Score. Sample the test set four times and take the paired difference against the base. A gain is proven only when the interval excludes zero, and we score both sides three times.

What happened

Nemotron moved in one round. Most of the gain was queries that now run.

Nemotron-Nano-8B queries that failed to run

Three repeat scores of the gain: +9, +10 and +11 points, all clear of zero.

Nemotron-Nano-8B queries that failed to run
ItemValue
Base55%
After one round35%

Qwen3-4B started at 58% and got the meaning wrong, not the syntax. Four GRPO rounds and one SFT pass reached 61%, a gain we cannot separate from noise. In the first two rounds the training reward rose while the test score did not. Without the held-out set, that run would have shipped.

Round five: the same reward, fed differently

A larger run on another schema used the same reward, tasks and model. Three things about feeding the trainer differed.

Rounds one to fourRound five
Answers per update8, one question at a time512, 32 questions times 16 answers
Which questionsall 601the 326 the model got right some of the time (the difficulty band)
Replies cut off by the length budgetscored zeroignored
Test set140 tasks, band about 6 points459 tasks, band about 3.5 points

We also grew the test, because a 6-point band can never prove a 3-point gain. On the bigger test the round-four gain turned out to be real.

Qwen3-4B, pass@1 on 459 held-out tasks, with 95% intervals

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). Step 50 was measured twice from scratch, 0.73 both times. The base was scored through the same server as the checkpoints (+0.4 points, inside the band).

Qwen3-4B, pass@1 on 459 held-out tasks, with 95% intervals
ItemValue95% interval
Base53%49% to 56%
Round four55%52% to 59%
Round five, step 2557%54% to 61%
Round five, step 5073%69% to 77%
Round five, step 7574%71% to 78%
Round five, step 10074%70% to 78%
Line chart of first-try accuracy on held-out questions against training samples. Qwen3-4B moves from 53% at the base to 55% after round four, then 57%, 73%, 74% and 74% at round five steps 25, 50, 75 and 100, with 95% bands. Nemotron-Nano-8B moves from 26% to 35% in one round. Qwen3.5-9B moves from 60% at the base to 79%, 81% and 82% at steps 25, 50 and 75, then 84% at round three steps 25 and 50.

The same numbers against training samples. For Qwen3-4B, rounds one to four are left of the vertical line and round five is right of it; the Qwen3.5-9B line runs the round-five recipe from its first step. The shaded band is the 95% uncertainty band on each point.

What the model learned is reliability, not new ability.

Qwen3-4B, base to round five step 75, on 459 tasks

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.

Qwen3-4B, base to round five step 75, on 459 tasks
ItemValue
pass@4, right on any of four tries, base76%
pass@4, step 7579%
pass^4, right on every try, base25%
pass^4, step 7565%
Replies with no query, base13%
Replies with no query, step 750%
SQL errors, base12%
SQL errors, step 755%

Next. The curve is flat from step 50. Re-measure the difficulty band on the round-five model, then a bigger base or harder questions.

Qwen3.5 on the same test

Does a newer base start higher? Same prompt, reply budget and 459 questions.

pass@1 on 459 held-out tasks, 4,096-token reply budget, with 95% intervals

Both Qwen3.5 models reason in plain text before the query. Every bar is scored on the first reply. Earlier versions of this chart read the 9B checkpoints as flat at 55%: one bug scored the wrong turn, the other served the base model under the adapter's name.

pass@1 on 459 held-out tasks, 4,096-token reply budget, with 95% intervals
ItemValue95% interval
Qwen3-4B (the model above), base53%49% to 56%
Qwen3.5-9B, base60%57% to 64%
Qwen3.5-9B, round one, step 2579%75% to 82%
Qwen3.5-9B, round one, step 5081%77% to 84%
Qwen3.5-9B, round one, step 75, adapter served correctly82%79% to 85%
Qwen3.5-9B, round one, step 75, second scoring pass83%79% to 86%
Qwen3.5-9B, round two, step 2583%80% to 86%
Qwen3.5-9B, round three, step 25 (questions re-picked on step 75)84%81% to 87%
Qwen3.5-9B, round three, step 50 (the round's end)84%81% to 87%
Qwen3.5-9B, all four checkpoints as first served62%58% to 65%
Qwen3.5-4B, base35%32% to 38%

One round of the round-five recipe puts the 9B above every Qwen3-4B checkpoint, and a second scoring pass on the same adapter landed within a point of the first. Three quarters of the questions it now gets right all four times. Most of the gain comes early: 79% at step 25, 81% at step 50, 82% at step 75, and round two's first checkpoint, 83%, is within noise of that. Round three re-picked the training questions from the step-75 model itself, so the trainer saw questions it still gets wrong sometimes, and its first checkpoint is 84%: two points over step 75, an interval that just clears zero. Step 50, the end of the round, is 84% again, with the easy and the hard questions both up. That is where this model stops on this test: 84% on the first try, 88% in four tries, and the recipe has no lever left that the interval can see.

We nearly missed it. For two days every checkpoint of this model scored the same as its base, and we wrote the run up as flat. The training log had said so all along: the trainer's own reward on the questions it trained on was 0.95 while the served adapter scored 0.65 on them. Both bugs are fixed.

Bug that hid the gainThe check that catches it now
The simulator wrote a second user turn where the eval asked for one, and on 15% of the 9B rows and 65% of the 4B rows the grader scored that second reply (#586)count the turns per row
The trained adapter, saved by one library and loaded by another, used layer names the server did not recognise, so it loaded without a warning and changed nothing: every "checkpoint" we scored was the base model (#588)score any served adapter on a hundred questions it trained on before believing a number
Fix in the recipe for the Qwen3.5 stackWithout it
Weight-sync rename between transformers 5 and vLLM 0.29every trained weight silently dropped
Optimized linear-attention kernelsone micro-batch took 138 GB
vLLM memory share large enough for weights plus cacheout of memory
Gradient checkpointing on TRL 1.13out of memory (on TRL 0.19 with Qwen3-4B, leave it off: garbage completions at reward zero)

For researchers

Each held-out task ii gets n=4n = 4 samples at temperature 0.7 and cic_i pass. pass@kk is the estimator of Chen et al. [3], passk^k the share passing every sample, intervals are percentile bootstraps over tasks [4], B=2,000B = 2{,}000:

pass@k=1Ni=1N[1(ncik)(nk)],passk=1Ni=1N1[ci=n].\text{pass@}k = \frac{1}{N} \sum_{i=1}^{N} \left[ 1 - \frac{\binom{n - c_i}{k}}{\binom{n}{k}} \right], \qquad \text{pass}^k = \frac{1}{N} \sum_{i=1}^{N} \mathbb{1}[c_i = n].

The difference is paired on tasks, p^i=ci/n\hat p_i = c_i / n each side:

Δ=1Ni=1N(p^iafterp^ibefore).\Delta = \frac{1}{N} \sum_{i=1}^{N} \left( \hat p_i^{\text{after}} - \hat p_i^{\text{before}} \right).

The verifier is a program, so the reward is verifiable in the sense of [1, §7]. For a candidate query qq and the reference qq^\star,

r(q)={1if q runs and rows(q)=rows(q) as multisets0.1if q runs and the rows differ0if q does not run.r(q) = \begin{cases} 1 & \text{if } q \text{ runs and } \operatorname{rows}(q) = \operatorname{rows}(q^\star) \text{ as multisets} \\ 0.1 & \text{if } q \text{ runs and the rows differ} \\ 0 & \text{if } q \text{ does not run.} \end{cases}

Floats compare to two decimals, column order is free, row order counts only when qq^\star has ORDER BY. GRPO [2] on a LoRA adapter [5], TRL [7], vLLM generation [6]. Advantage Aj=(rjmean(r))/std(r)A_j = (r_j - \operatorname{mean}(r)) / \operatorname{std}(r) over the GG samples of a prompt, KL penalty against the base with coefficient β\beta.

Qwen/Qwen3-4B checkpoints, pass@1 with 95% intervals, 140 tasks

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). By round four the training reward sat at 0.65 and the KL to the base at 0.03.

Qwen/Qwen3-4B checkpoints, pass@1 with 95% intervals, 140 tasks
ItemValue95% interval
base0.580.52 to 0.64
r10.580.52 to 0.64
r20.600.54 to 0.67
sft-think0.600.54 to 0.67
r30.610.56 to 0.67
r40.610.54 to 0.67
RoundFromStepslrβ\betaGeneration
r1baseGRPO 1002e-50.04HF generate
r2r1GRPO 2005e-50.01HF generate
sft-thinkbaseself-distillation on 199 verified traces, hosted SFT, 2 epochs
r3r2GRPO 1,0002e-50.01vLLM, 8 prompts per generate
r4r3GRPO 1,0002e-50.01vLLM, same settings
Nemotron r1baseGRPO 6002e-50.01vLLM, steps per generation 8, max completion 512, 44 min on one H100
Cost to r4fourteen hours, six checkpoints, twelve test-set scores, under $100 of GPU
Correction on recordbefore 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/
Round-five testthe 1,482 extra questions were written the same way as the first 741
MethodRLHF book [1]: verifiable rewards (§7), over-optimization symptoms (§14), fixed held-out split with an interval on the paired difference (§16)
Coderecipes/04-train/text-to-sql
Datawhile-ai/text-to-sql-shop, configs eval-base to eval-r4, eval-nemotron-8b-base, eval-nemotron-8b-r1; adapters while-ai/text-to-sql-shop-r1 to -r4 and -nemotron-8b-r1
nvidia/Llama-3.1-Nemotron-Nano-8B-v1, pass@1 with 95% intervals

detailed thinking off; the reasoning mode does not engage on these prompts, and the thinking-on arm also scores 0.26.

nvidia/Llama-3.1-Nemotron-Nano-8B-v1, pass@1 with 95% intervals
ItemValue95% interval
base0.260.20 to 0.33
r10.350.28 to 0.42
Nemotron r1 vs base, paired delta across three evaluation runs

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.

Nemotron r1 vs base, paired delta across three evaluation runs
ItemValue95% 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

References

  1. Lambert, N. (2025). Reinforcement Learning from Human Feedback. arXiv:2504.12501. Online at rlhfbook.com.
  2. 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.
  3. 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.
  4. Efron, B. (1979). Bootstrap methods: Another look at the jackknife. The Annals of Statistics, 7(1), 1-26.
  5. 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.
  6. 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.
  7. 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.
  8. 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.py

The README has the database setup, the Modal command for training, and delta.py for the paired score.