| --- |
| title: GRPO Training Session Log |
| description: Chronological log of GRPO training runs on Qwen3-0.6B/1.7B covering nine runs, fixes applied, multi-turn SFT breakthrough, and capacity ceiling analysis |
| doc_type: exploration |
| --- |
| |
| # GRPO Training Session Log |
|
|
| ## Context |
|
|
| Training Qwen3-1.7B as a SQL agent using SFT warmup + GRPO with TRL's `environment_factory` on Spider dataset. Running on Colab L4 (24GB). |
|
|
| Started 2026-04-02. Multi-turn SFT breakthrough on 2026-04-03. |
|
|
| ## Key Findings & Fixes Applied |
|
|
| ### 1. SFT Null-Param Injection (ROOT CAUSE of first collapse) |
| **Problem**: Qwen3's `apply_chat_template` expands dict arguments to include ALL parameter names from ALL tools with null values. SFT trained model to always generate `{"sql": null, "table_name": "X", "value": null}`. |
| **Fix**: Pass arguments as JSON strings (`json.dumps({"table_name": table})`) instead of dicts. Tokenizer uses strings verbatim. |
|
|
| ### 2. SFT Answer Formatting |
| **Problem**: Gold answers were Python literals (`['a', 'b']`, `[[1, 'amc']]`). Model learned wrong format. |
| **Fix**: `_format_answer_for_model()` converts to human-readable: comma-separated lists, pipe-separated table rows. |
|
|
| ### 3. Empty Tool Responses |
| **Problem**: TRL adapter returned `observation.result` (empty on SQL errors), hiding errors from model. |
| **Fix**: `_result_or_error()` falls back to `observation.error` so model sees "Error: SQL error: ...". |
|
|
| ### 4. Post-Episode Penalty |
| **Problem**: Model continues calling tools after answering, wasting steps with no signal. |
| **Fix**: `_POST_EPISODE_PENALTY = -0.1` applied in all 4 tool methods when `self._done` is True. |
|
|
| ### 5. Answer Stripping |
| **Problem**: Model wraps answers in quotes, code fences, "Answer:" prefix. |
| **Fix**: `_strip_answer_wrapping()` in verifier preprocesses predicted answers. |
|
|
| ### 6. Per-Turn SFT β Multi-Turn SFT (ROOT CAUSE of Run 5 stall) |
| **Problem**: SFT generated one example per assistant turn (347 examples, ~50% describe calls). Model over-learned "call describe" and never practiced queryβanswer. During GRPO with KL penalty, model stayed anchored to this single-turn policy. |
| **Fix**: Generate one full multi-turn example per question (100 examples, each containing describeβqueryβanswer). Enable `assistant_only_loss` via Qwen3 template patch so loss is on assistant turns only. |
| **Key detail**: Qwen3's chat template lacks `{% generation %}` tags required by TRL for `assistant_only_loss`. Patch the template before SFT, restore original before GRPO (TRL does exact-match template checks in `add_response_schema()` and `get_training_chat_template()`). |
|
|
| ### 7. Removed Arrow-Notation Few-Shot Examples |
| **Problem**: System prompt contained few-shot examples using arrow notation (`β describe(table_name="X")`) while the model must produce `<tool_call>{"name":"describe","arguments":...}</tool_call>` JSON. Two competing formats for a 1.7B model. |
| **Fix**: Removed `_FEW_SHOT_BLOCK` from system prompt. The textual "Strategy" section is sufficient. |
|
|
| ### 8. KL Penalty + Curriculum |
| **Problem**: GRPO drifted policy away from SFT, causing `<tool_response>` instead of `<tool_call>`. |
| **Fix**: `beta=0.04` KL penalty + easy-first curriculum (phase 1: easy only, phase 2: easy+medium). With multi-turn SFT, beta=0.04 no longer blocks exploration. |
|
|
| ### 9. OOM with Reference Model |
| **Problem**: `beta>0` loads reference model copy, doubling memory on L4. |
| **Fix**: Reduced `num_generations` 6β4, `max_new_tokens` 1024β512 for phase 1. Phase 2 drops beta=0 and uses 1024 tokens. |
|
|
| ### 10. generation_batch_size Divisibility |
| **Problem**: `generation_batch_size` (default 8) not divisible by `num_generations` (6). |
| **Fix**: Set `generation_batch_size=config.num_generations` in notebook_pipeline. |
| |
| ## Discovered Issues (not yet fixed) |
| |
| ### CTE (WITH clause) rejected by environment |
| **Problem**: `sql_environment.py` SQL validation only allows queries starting with `SELECT`. The model discovers CTEs during GRPO (`WITH dogs AS (...) SELECT ...`), gets `"Error: Only SELECT queries are allowed. Got: WITH"`, wastes a step recovering. |
| **Impact**: Burns 1-2 steps on error recovery, reducing reward. Teaches model to avoid CTEs even though they're valid read-only SQL. |
| **Root cause**: Hard-coded prefix check. The DB is already opened with `mode=ro`, so SQLite itself would reject writes. |
| **Fix**: Allow `WITH` as a valid query prefix, or remove the prefix check entirely and rely on `mode=ro`. |
|
|
| ### Post-episode repetition |
| **Problem**: Model keeps calling tools after episode ends (gets `{'error': 'Episode is over'}`). The -0.1 penalty exists but model still does 3-5 extra calls. |
| **Possible fixes**: Increase penalty, or the model may learn to stop as GRPO training progresses. |
|
|
| ### HF_SUFFIX naming bug (FIXED) |
| **Problem**: `HF_SUFFIX` is concatenated directly onto `grpo` without auto-prepending a dash. Setting `HF_SUFFIX="no-no-thinking"` produces `sqlenv-qwen3-1.7b-grpono-no-thinking` instead of the intended `sqlenv-qwen3-1.7b-grpo-no-no-thinking`. The `grpono-no-thinking` checkpoint on HF Hub was manually renamed via HF UI after push. |
| **Root cause**: Format string `f"sqlenv-{_model_short}-grpo{HF_SUFFIX}"` expects the user to include a leading dash. |
| **Fix**: Auto-prepend dash + strip existing prefixes from checkpoint names. When resuming from `hjerpe/sqlenv-qwen3-0.6b-grpo`, the old code produced `sqlenv-sqlenv-qwen3-0.6b-grpo-grpo-v2` (double prefix). Now strips `sqlenv-` and `-grpo*` from `_model_short` before rebuilding the name. |
| **Files**: `notebooks/train_grpo.ipynb` save cell. |
|
|
| ### Save cell uses Phase 1 config for output_dir |
| **Problem**: `model.save_pretrained(config.output_dir)` uses Phase 1's `config`, not Phase 2's `config2`. Both phases write to `outputs/grpo_run` β Phase 2 overwrites Phase 1 checkpoints in the same directory. |
| **Impact**: Not a correctness bug (the final model weights are from Phase 2, which is correct), but fragile if you want to preserve Phase 1 checkpoint separately. |
| **Fix**: Use `config2.output_dir` in the save cell, or save Phase 1 to a separate directory before Phase 2 starts. |
|
|
| ## Training Runs |
|
|
| ### Run 1 (pre-fixes): SFT OK, GRPO plateau at ~30-40% accuracy |
| - Model learned tool-calling but rewards flat, advantage=0 most steps |
| - Identified: no penalty for post-episode, answer format issues |
|
|
| ### Run 2 (batch 1 fixes): GRPO collapse β null args |
| - SFT taught `{"sql": null, "table_name": "X", "value": null}` |
| - Every rollout got TypeError β reward=0 β no gradient signal |
| - Root cause: Qwen3 tokenizer expanding dict args |
|
|
| ### Run 3 (JSON string args fix): GRPO collapse β format drift |
| - SFT clean, first ~30 steps showed correct tool calls |
| - By step 40+: model output `<tool_response>` instead of `<tool_call>` |
| - GRPO drifted structural tokens without KL penalty |
|
|
| ### Run 4 (KL penalty beta=0.04): OOM |
| - Reference model doubled memory, exceeded L4 24GB |
|
|
| ### Run 5 (beta=0.04, reduced tokens/generations): KL too conservative |
| - No collapse, correct format, but reward=0.00 everywhere |
| - Model only generates single describe call per rollout |
| - KL penalty keeps model too close to single-turn SFT policy |
| - All 4 rollouts identical β advantage=0 β no learning |
|
|
| ### Run 6 (multi-turn SFT + assistant_only_loss): First successful training |
| - Switched SFT from per-turn (347 examples) to multi-turn (100 full trajectories) |
| - Enabled `assistant_only_loss` via Qwen3 template patch |
| - Removed arrow-notation few-shot examples from system prompt |
| - **Phase 1** (435 easy, beta=0.04, 512 tokens, ~2h50m): |
| - Clear upward reward trend: ~0.15 β 0.5-0.75 |
| - Loss trends upward 0β0.14, showing learning from reward signal |
| - Model writes JOINs, GROUP BY HAVING, NOT IN subqueries, uses `sample` tool |
| - Recovers from SQL errors (wrong column β retry, CTE rejected β plain JOIN) |
| - CTE (WITH) queries rejected by environment β wasted steps |
| - **Phase 2** (467 easy+medium, beta=0, 1024 tokens, ~3h37m): |
| - Reward holds ~0.5 average, no format collapse without KL |
| - Peak rewards reach 0.93 |
| - Correct answers on COUNT, AVG, GROUP BY, multi-table JOINs, subqueries |
| - Medium questions harder β more column-name errors, alias confusion |
| - Final reward: 0.64 |
| - **Persistent issues**: |
| - Error loop: model repeats same failing query without changing it (step 140: "no such column: bonus" 7 times) |
| - Table alias confusion: `T2.column` when column is on T1 |
| - Missing DISTINCT in COUNT queries |
| - Post-episode repetition: 1-3 extra calls after correct answer |
| - Empty `<think>` blocks β model not reasoning about errors |
|
|
| ## Changes for Run 7 |
|
|
| Applied after Run 6 analysis: |
|
|
| ### 11. Allow CTE (WITH) queries |
| **Fix**: Changed SQL validation from `first_keyword != "SELECT"` to `first_keyword not in ("SELECT", "WITH")`. |
| **Files**: `server/sql_environment.py` (both `_execute_gold_sql` and `_execute_sql`) |
|
|
| ### 12. Increase post-episode penalty |
| **Fix**: `_POST_EPISODE_PENALTY` from -0.1 to -0.3. The -0.1 penalty wasn't strong enough β model still made 3-5 extra calls after episode end. |
| **File**: `training/trl_adapter.py` |
|
|
| ### 13. HF Hub suffix for model versioning |
| **Fix**: Added `HF_SUFFIX` parameter to save cell. Set to e.g. "-v2" or "-cte" to push to `hjerpe/sqlenv-qwen3-1.7b-grpo-v2`. |
| **File**: `notebooks/train_grpo.ipynb` cell 9 |
|
|
| ### Run 7 (repeat penalty + configure fix): Stable reward, multi-table weakness exposed |
| - **Date**: 2026-04-05 |
| - **Changes**: F015 error-repetition penalty (`_REPEAT_PENALTY = -0.2`, 3-call deque window), removed public `configure()` that TRL misidentified as a tool |
| - **Branch**: `feat/error-repetition-penalty` |
| - **SFT**: 120 multi-turn trajectories, 2 epochs, loss 2.2β0.06, assistant-only loss enabled. 14% assistant tokens. Post-SFT format check: all 3 samples produce correct `<tool_call>` JSON with `describe` as first move. |
| - **Phase 1** (435 easy, beta=0.04, 512 tokens, ~2h): |
| - Reward: β0.1 β 0.7 peak, stabilizing 0.3-0.7. Loss spike at step 320 (1.8) recovered. |
| - Model learned: `describe` β `query` β `answer`, comma-separated lists, pipe-delimited rows, `[]` for empty results, `UNION` queries, `NOT IN` subqueries, `LIKE '%North%'`. |
| - Repeat penalty observable: step 100 reward β0.22 (model re-described same table), step 120 reward β0.24 with repeat penalty stacking. |
| - Error recovery improved: after SQL error, model calls `describe` on the failing table then retries with correct column names (steps 110, 140). |
| - Persistent: hallucinated column names from pretraining (T_full_name), `ORDER BY count(*) DESC` without `GROUP BY`, CTE queries still rejected. |
| - **Phase 2** (467 easy+medium, beta=0.0, 1024 tokens, ~2h22m): |
| - Reward oscillated 0.0β1.15, no clear upward trend vs Phase 1. Mean reward ~0.5. |
| - Single-table questions consistently correct (count, filter, aggregate, WHERE + GROUP BY HAVING). |
| - Multi-table JOIN weakness: can't follow FK chains (DocumentsβTemplatesβRef_Template_Types), joins on wrong keys, hallucinates join columns. |
| - Repeat penalty firing on multi-table failures: step 150 reward β0.58 (5+ repeated failed JOINs on `T2.Template_ID`). |
| - New behavior: model answers `[]` for genuinely empty results, learned `"No results"` β `"[]"` mapping. |
| - Step 80 (Phase 2): 1.15 reward, advantage +1.50 β model wrote `SELECT avg(weight), year FROM cars_data GROUP BY year` with 13-row correct answer in 2 tool calls. Peak efficiency. |
| - Final reward: 0.61. |
| - **Persistent issues**: |
| - Multi-table JOINs: model can't chain through intermediate tables (needs the question-to-FK-path reasoning that 1.7B lacks without thinking) |
| - Answer hallucination when query returns empty: submits "No data available" or "N/A" instead of trying different query |
| - `describe` repeat on already-described tables (penalty fires but model still does it) |
| - Step 430: hex-encoded query string (`0x45636365646965...`) β degenerate output near end of training |
|
|
| ### Run 8 (thinking mode): Thinking helps error recovery but introduces degenerate loop |
| - **Date**: 2026-04-06 |
| - **Changes**: F012 `enable_thinking` config flag, `ENABLE_THINKING = True` in notebook, max_new_tokens 768 (Phase 1) / 1280 (Phase 2) |
| - **Branch**: `feat/enable-thinking-mode` |
| - **SFT**: Same 120 multi-turn trajectories as Run 7, but system prompt omits `/no_think` prefix. SFT data itself has no `<think>` blocks (approach B: let GRPO discover thinking). |
| - **Phase 1** (435 easy, beta=0.04, 768 tokens, ~4.5h): |
| - Loss 0.31βoscillating 0.05-0.40 throughout. No clear trend. |
| - Correct answers on ~50% of sampled steps (reward 1.15). Similar to Run 7 on easy questions. |
| - **Thinking triggers on errors**: Step 90 β after 2 SQL errors (`no such column: airport_code`), model opens `<think>`, reasons about column name mismatch, then generates correct `AirportCode` query. Step 180 β reasons about `course_title` vs `course_name` after error, corrects to right column. |
| - **Empty think blocks for easy questions**: Steps 20-80 all show `<think></think>` with no content β model skips thinking when confident. Good token efficiency. |
| - **NEW failure mode: `<think>assistant` degenerate loop** β ~10/43 sampled steps (23%) show `<think>assistant<think>assistant...` repeating until token limit. Model fails to close `</think>` and enters repetitive pattern. Steps 110, 140, 200, 260, 300, 340, 410, 420, 430 all exhibit this. Burns entire token budget with no useful output. |
| - Multi-table JOINs with subqueries work (Step 30: `NOT IN` subquery, Step 80: UNION, Step 435: correlated subquery with HAVING). |
| - Final step 435: model writes complex correlated subquery with `HAVING count(*) = (SELECT ... ORDER BY count(*) DESC LIMIT 1)` β correct answer "Martin". |
| - **Phase 2** (467 easy+medium, beta=0.0, 1280 tokens, stopped at step 182/467 β likely OOM): |
| - Reward oscillated 0.1-0.85, averaging ~0.45. Comparable to Run 7 Phase 2 (~0.5). |
| - Step 10: Easy question solved in 3 tool calls (describeβqueryβanswer). Reward 1.15. |
| - Step 90: Multi-table JOIN with `HAVING count(*) < 200` β correct, reward 1.15. |
| - Step 110: `NOT IN` subquery for stadiums without concerts β correct on first try. |
| - Step 140: Cross-table JOIN (evaluation + employee, `MAX(bonus)`) β correct. |
| - Step 150: Multi-table chain reasoning with thinking β corrected `Document_Name` β `Template_ID` join path after 2 errors. Long `<think>` block with correct reasoning. |
| - Step 170: Double-year intersection query (`Stadium_ID IN ... 2014 AND Stadium_ID IN ... 2015`) β correct. |
| - **Crashed at step 182** β likely OOM from 1280 max_new_tokens + thinking blocks consuming more memory during generation. |
| - Model checkpoint was NOT pushed to HF Hub before crash. |
| - **Persistent issues**: |
| - `<think>assistant` degenerate loop (~23% of Phase 1 steps) β new failure mode unique to thinking mode |
| - Multi-table FK chain queries still fail on medium difficulty (same as Run 7) |
| - Phase 2 no better than Run 7's Phase 2 β thinking mode doesn't help with the fundamental JOIN reasoning gap |
|
|
| ### Run 9 (v2 continued training, no-think): Confirms Phase 2 ceiling |
| - **Date**: 2026-04-11 |
| - **Changes**: Resumed from v1 checkpoint (Run 7's final weights), 2 epochs Phase 1 + 2 epochs Phase 2. Fixed model preset lookup (`_get_preset()` matching on "1.7b" in name string instead of exact `.get()`). |
| - **Branch**: `feat/f011-3-way-comparison-notebook` |
| - **Phase 1** (435 easy, beta=0.04, 512 tokens, ~3h34m, 870 steps): |
| - Loss: oscillates 0.01-0.13, occasional negatives (-0.05) in second half. More negative values than v1 Phase 1 β expected since starting from trained checkpoint, less to learn. |
| - Rewards: sawtooth 0.01-1.15. Easy questions solved reliably (describeβqueryβanswer in 3 calls). Medium questions from mixed batches still fail. |
| - Model behavior: solid tool-call format, comma-separated lists, pipe-delimited rows. No format collapse. |
| - Step 300: Degenerate SQL β `ORDER BY HorsepowerDESC` (missing space), repeated 3 times. Token budget consumed. |
| - Step 560: Degenerate completion β output "icher Consulting Solution" (truncated gibberish). Reward 0.00. One-off. |
| - **Phase 2** (467 easy+medium, beta=0.0, 1024 tokens, ~3h50m, 934 steps): |
| - Loss: oscillates -0.13 to +0.12, trend more negative than Phase 1 β policy sharpens on known patterns without KL regularization. |
| - Rewards: same sawtooth 0.01-1.15 as Phase 1, no upward trend. Mean ~0.5. |
| - **Successes (medium)**: Step 140 β JOINed evaluationβemployee for MAX(bonus), found "Louis Deacon" (1.13 reward). Step 750 β subquery `COUNT(*) > (SELECT ... ORDER BY Horsepower DESC LIMIT 1)`, answered "39" correctly. |
| - **Failures (medium)**: Step 20 β hallucinated `make_id`, `full_name` columns, budget exhausted after 8+ tool calls. Step 50 β invented `Course_Attendance` table, cascading errors. Step 530 β tried `Bred`, `Breed` before finding `Breeds`, then queried wrong column. |
| - **Persistent pattern**: Model describes tables correctly but writes SQL with wrong column names from pretraining knowledge (e.g., `full_name` instead of `FullName`, `country.name` when table is `singer` with `Country` column). |
| - Final reward: 0.048 (last step was incorrect) |
| - **Charts**: Reward Trend (Phase 1β2) shows flat continuation β no improvement from adding medium questions. Loss in Phase 2 oscillates around 0, with spikes to -0.13 (GRPO reinforcing already-known easy patterns). |
| - **Conclusion**: v2 confirms v1 findings. The 0.6B model's accuracy ceiling is set by pretraining SQL knowledge, not RL training budget. More epochs don't help medium questions. Next interventions: (1) more SFT on multi-table JOINs with correct column names, (2) larger model (1.7B), or (3) increase step budget to let model iterate. |
|
|
| ### Eval Format Fix (F011 comparison notebook) |
| - **Date**: 2026-04-10 |
| - **Problem**: `compare_methods.ipynb` eval fed models a different message format than TRL training: |
| 1. Tool results posted as `role: "user"` β training uses `role: "tool"` (Qwen3 renders as `<tool_response>` wrapper) |
| 2. Assistant turns stored as raw text content β training uses structured `tool_calls` dicts with JSON-string arguments |
| 3. Question + table hint separated by `\n\n` β TRL appends `reset()` return directly to user message (no separator) |
| - **Discovery method**: Added debug cell to render prompts via `apply_chat_template` and compared side-by-side with TRL training log output. The `role: "tool"` format renders as `<|im_start|>user\n<tool_response>...</tool_response>` while `role: "user"` renders as `<|im_start|>user\nplain text` β structurally different despite both appearing under `user` token. |
| - **Fix**: Changed `LLMToolCallingPolicy` in compare_methods.ipynb to match TRL exactly: structured `tool_calls`, `role: "tool"`, concatenated user message. Also parse ALL `<tool_call>` blocks per generation and buffer extras (matches TRL's `_tool_call_loop`). |
| - **Result (N=50, base=Qwen3-0.6B, 2026-04-11, with parse-failure retry, 2 runs)**: |
| - **Run A:** |
| - zero-shot: 0% accuracy, 28% parse rate, avg 10.8 steps (31/50 budget exhaust) |
| - 1-shot: 0% accuracy, 16% parse rate, avg 14.8 steps (49/50 budget exhaust) |
| - 3-shot: 0% accuracy, 20% parse rate, avg 13.8 steps (44/50 budget exhaust) |
| - grpo-v1: 28% accuracy, 95% parse rate, avg 4.0 steps, avg reward 0.355 |
| - grpo-v2: 32% accuracy, 87% parse rate, avg 3.7 steps, avg reward 0.400 |
| - **Run B (same day, different Colab session):** |
| - zero-shot: 0% accuracy, 24% parse rate, avg 12.4 steps (38/50 budget exhaust) |
| - 1-shot: 2% accuracy, 17% parse rate, avg 14.0 steps (46/50 budget exhaust) |
| - 3-shot: 0% accuracy, 19% parse rate, avg 14.8 steps (49/50 budget exhaust) |
| - grpo-v1: 30% accuracy, 100% parse rate, avg 3.5 steps, avg reward 0.386 |
| - grpo-v2: 24% accuracy, 95% parse rate, avg 3.6 steps, avg reward 0.321 |
| - **Run-to-run variation**: v1 scored 28% then 30%, v2 scored 32% then 24%. The ~6-8pp swing confirms v1 and v2 are statistically indistinguishable at N=50. Report as "~30% accuracy" for both. |
| - Parse failure retry: base models no longer die on first parse failure β they get a no-op DESCRIBE and continue. This reveals they waste their entire 15-step budget repeating the same malformed output. |
| - Base model failure mode: can't produce `<tool_call>` format (76-83% parse failure rate). GRPO failure mode: produces valid tool calls but writes wrong SQL. |
| - 1-shot scored 2% in Run B (1 lucky episode) β demonstrates N=50 noise floor for rare events. |
| - **Checkpoint naming**: `grpono-no-thinking` was caused by `HF_SUFFIX="no-no-thinking"` (missing leading dash) and subsequent HF UI rename. See "Discovered Issues" section. |
| - **TRL format verified from source**: `reset()` return is appended to last user message (TRL docs + grpo_trainer.py). Tool results use `{"role": "tool", "name": name, "content": result}`. Generation runs to EOS (no stop at `</tool_call>`), all parsed tool calls executed in sequence. |
| |
| ## Current Status (after Run 9) |
| |
| ### Working: |
| - Multi-turn SFT + `assistant_only_loss` β still the critical foundation |
| - GRPO learns on easy questions: reward β0.1β0.7 in Phase 1 (both Run 7 and 8) |
| - Repeat penalty (F015) fires correctly on exact-match repeated calls |
| - Error recovery: describeβretry after SQL error is a learned behavior |
| - Answer format: single values, comma-separated lists, pipe-delimited rows, `[]` for empty |
| - **Thinking mode triggers on errors** β model reasons about column name mismatches and table structure after SQL errors (Steps 90, 150, 180, 220, 280 in Run 8) |
| - **Empty think blocks for easy questions** β model doesn't waste tokens thinking when confident |
| |
| ### Not yet working: |
| - Multi-table FK chain queries (medium difficulty) β confirmed across Runs 7, 8, 9. More RL epochs don't help. |
| - Phase 2 shows no improvement over Phase 1 β medium questions need more SFT coverage on JOIN patterns |
| - Column name hallucination from pretraining β model reads schema correctly then writes pretrained column names |
| - Model doesn't use `sample` tool (learned in Run 6 but lost?) |
| - **`<think>assistant` degenerate loop** β thinking mode (Run 8) introduces ~23% failure rate from unclosed think tags |
| |
| ### For comparison notebook (F011): |
| - **v1 checkpoint** on HF Hub: `hjerpe/sqlenv-qwen3-0.6b-grpo` |
| - **v2 checkpoint** on HF Hub: `hjerpe/sqlenv-qwen3-0.6b-grpo-v2` |
| - **Run 8 (thinking)** checkpoint was NOT pushed β Colab session crashed before save |
| - N=50 eval completed 2026-04-11 (2 runs): v1 ~28-30%, v2 ~24-32%, confirming both are ~30% and within run-to-run noise |
| - v1 and v2 are statistically indistinguishable β the difference between runs is larger than the difference between checkpoints |
| - Thinking mode comparison can be added later when a checkpoint is available |
| |
| ### Possible next interventions: |
| - **Thinking mode training (0.6B)**: Resume from v1 with `ENABLE_THINKING=True`, push as `-think` suffix. Run 8 showed thinking helps error recovery but crashed before save. |
| - **More SFT on multi-table JOINs**: Add trajectories with 3+ table chains, correct column names after describe. Highest priority β v2 proved more RL epochs don't help without this. |
| - **Increase model size**: Switch from 0.6B to 1.7B. Larger model may override pretrained column name biases from schema context. |
|
|
| ### OOM prevention for next thinking-mode run: |
| The Run 8 Phase 2 crash at step 182/467 was likely OOM. Root causes and mitigations: |
|
|
| 1. **`max_new_tokens=1280` is too high for L4 with thinking** β medium questions trigger long `<think>` blocks (Step 50 reasoning about `>1` vs `>=1`, Step 120 about breed/size format, Step 130 about `T1.distinct_city` column mismatch). Reduce to **1024** for Phase 2. |
| 2. **`num_generations=4` compounds the problem** β each generation runs inference independently, so 4 rollouts Γ 1280 tokens = 5120 tokens of peak generation memory. Reduce to **3 generations** for thinking-mode Phase 2. The `generation_batch_size` must also be updated to match. |
| 3. **The `<think>assistant` degenerate loop inflates effective token usage** β a rollout that enters the loop consumes the full `max_new_tokens` budget producing garbage. Fixing this loop via SFT (adding 5-10 examples with proper `<think>reasoning</think>` blocks) would reduce average token consumption significantly, making OOM less likely even at higher token limits. |
| 4. **Phase 2 has no KL reference model (beta=0)** β so memory is only model + generation buffers. The OOM is purely from generation length, not model copies. |
| |
| **Recommended config for next thinking-mode run (Phase 2):** |
| ```python |
| config2 = replace(config, |
| beta=0.0, |
| max_new_tokens=1024, # was 1280 |
| num_generations=3, # was 4 |
| enable_thinking=True, |
| ) |
| ``` |
| Also set `generation_batch_size=3` in `notebook_pipeline.py` (it must equal `num_generations`). |
| |
| ## Historical: Status after Run 6 |
| |
| ### Architecture decisions to preserve: |
| - Multi-turn SFT with `assistant_only_loss` β critical over per-turn |
| - Qwen3 template patch (`{% generation %}` tags) for SFT, restore original before GRPO |
| - SFT args as JSON strings (not dicts) β critical for Qwen3 |
| - Phase 1 (easy, KL) β Phase 2 (easy+medium, no KL) |
| - DB opened with `mode=ro` β safety enforced by SQLite, not regex |
| |
| ## File Map |
| |
| | File | What changed | |
| |------|-------------| |
| | `scripts/generate_sft_data.py` | Multi-turn trajectories, JSON string args, answer formatting | |
| | `scripts/inspect_sft_data.py` | SFT data stats + tokenizer-rendered inspection | |
| | `training/trl_adapter.py` | Post-episode penalty (-0.3), error surfacing, `_result_or_error` | |
| | `training/config.py` | Added beta field (KL penalty) | |
| | `training/notebook_pipeline.py` | generation_batch_size, beta passthrough | |
| | `server/verifier.py` | `_strip_answer_wrapping` preprocessing | |
| | `server/sql_environment.py` | SQL validation allows SELECT and WITH | |
| | `notebooks/train_grpo.ipynb` | Multi-turn SFT, assistant_only_loss, template patch/restore, HF_SUFFIX | |
| |
| ## Key Learnings |
| |
| 1. **Qwen3's apply_chat_template expands dict args** β always use JSON strings for SFT tool_call arguments. |
| 2. **Multi-turn SFT is critical for agentic GRPO** β per-turn examples teach one action; the model never learns the full workflow. Full trajectory SFT with `assistant_only_loss` teaches describeβqueryβanswer as a coherent strategy. |
| 3. **Qwen3 template lacks {% generation %} tags** β patch before SFT for `assistant_only_loss`, restore before GRPO. TRL's `add_response_schema()` and `get_training_chat_template()` do exact string equality on the template. |
| 4. **Don't show competing formats to small models** β arrow-notation few-shot examples confused the model when it needed to produce `<tool_call>` JSON. |
| 5. **KL penalty effectiveness depends on SFT quality** β beta=0.04 was "too high" only because the SFT policy was single-turn. With multi-turn SFT, the same beta works fine. |
| 6. **Reference model doubles memory** β plan for this when using KL penalty on L4. |
| 7. **Let the SQL engine enforce safety, not regex** β hard-coded `SELECT`-only prefix check blocks valid read-only SQL (CTEs). The DB is already `mode=ro`. |
| 8. **Render training data through the actual tokenizer** β inspect scripts that reformat JSON are fragile. The ground truth is `apply_chat_template` output from the same tokenizer instance used for training. |
| 9. **Error loops are a 1.7B capacity limit** β the model repeats failing queries verbatim because `<think>` is suppressed and it can't reason about the error. Enabling thinking mode may help. |
| 10. **Post-episode penalty of -0.1 is too weak** β model still makes 3-5 extra calls. Increased to -0.3. |
| 11. **Repeat penalty works but doesn't fix root cause** β the β0.2 penalty fires correctly on exact-match repeated tool calls, but the model's real problem is pretrained column-name hallucination, not repetition per se. The model varies its queries enough to avoid exact repeats while still failing on the same conceptual error. |
| 12. **Phase 2 (medium) doesn't improve over Phase 1 (easy)** β reward plateau at ~0.5 suggests the model needs more SFT coverage on multi-table JOINs, not just more GRPO steps. RL can't teach FK chain reasoning that isn't in the initial policy. |
| 13. **Thinking mode helps error recovery but doesn't improve overall accuracy** β the model uses `<think>` blocks to reason about SQL errors (column name mismatches, table structure), leading to correct retries. But accuracy on easy questions is similar to no-think Run 7. The benefit is qualitative (better error recovery) not quantitative (higher reward). |
| 14. **`<think>assistant` degenerate loop is a new failure mode** β ~23% of thinking-mode steps degenerate into `<think>assistant<think>assistant...` repeating until token limit. The model fails to produce `</think>` and enters a repetitive pattern. This is the thinking-mode equivalent of Run 7's post-episode repetition. Fix: add SFT examples with proper `<think>reasoning</think>` blocks. |
| 15. **Empty `<think></think>` blocks are good** β the model learns to skip thinking on easy questions, preserving tokens for tool calls. This is emergent behavior from GRPO reward signal (thinking wastes tokens β lower reward on easy questions). |
| 16. **1280 max_new_tokens is too aggressive for thinking mode on L4** β Phase 2 crashed at step 182/467, likely OOM. The longer `<think>` blocks in Phase 2 (medium questions trigger more reasoning) push memory past L4's 24GB. Use 1024 max_new_tokens for thinking-mode Phase 2. |
| 17. **Public methods on environment_factory become TRL tools** β TRL introspects all public methods for JSON schema generation. The `configure()` classmethod caused a `DocstringParsingException`. Keep configuration methods private (`_configure`). |
| 18. **Continued training from checkpoint doesn't unlock medium questions** β v2 ran 2 more epochs of Phase 1 + Phase 2 from v1's final checkpoint. Reward stayed flat at ~0.5 mean. The model reliably solves easy single-table queries but can't learn multi-table FK chain reasoning from RL alone. The policy needs SFT coverage on the patterns it can't discover through trial-and-error. |
| 19. **Column name hallucination is the dominant error mode** β the model describes tables correctly (seeing `FullName: TEXT`) then writes `SELECT full_name` or `SELECT Maker, FullName FROM car_makers ORDER BY MakerDESC LIMIT 1` (missing space). This is pretrained SQLese overriding the schema information the model just read. A 0.6B model can't override pretraining biases through RL reward signal alone. |
| 20. **Eval must exactly match TRL's message format** β `role:"tool"` for env results (not `role:"user"`), structured `tool_calls` dicts for assistant turns (not raw `<tool_call>` text in content), question+table_hint concatenated without separator (TRL appends `reset()` return to last user message). Qwen3 renders `role:"tool"` as `<|im_start|>user\n<tool_response>...</tool_response>` β looks like a user message but is structurally different. Getting this wrong caused 0% accuracy across all conditions; fixing it recovered 10-50% on base model. |
| 21. **Incorrect answer reward of 0.0 creates an avoid-answering incentive** β exploration steps accumulate ~0.05-0.15 reward. Calling `answer(wrong)` gives 0.0 and ends the episode, so total reward (~0.05) can be lower than not answering and exploring until budget (~0.10). The model may learn to write prose instead of calling `answer()` when uncertain. PRS (Progressive Reward Shaping, arxiv 2512.07478) addresses this with a small format-compliance reward for completing the tool pipeline regardless of correctness. |
| 22. **Continued training trades guessing for abstention** β v2 outputs "Task complete." instead of calling `answer()` on hard questions β a form of calibrated uncertainty. v1 guesses more but gets fewer right per attempt. The 0.0 incorrect-answer reward (learning #21) drives this: v2 internalized that guessing wrong is worse than not answering. |
| 23. **v1 and v2 are statistically indistinguishable at N=50** β across two runs, v1 scored 28% then 30%, v2 scored 32% then 24%. The ~6-8pp run-to-run variation exceeds the checkpoint difference. v2's abstention behavior (learning #22) adds variance: on borderline questions, whether v2 guesses or outputs "Task complete." varies by run. For reporting, use "~30% accuracy" for both checkpoints. N=200+ would be needed to detect a real 4pp difference with 80% power. |
|
|