Add OpenEnv agent-training recipe: multi-turn Wordle GRPO (Hugging Face Jobs) - #362
Conversation
…ce Jobs) A new Agents recipe that trains a small LM to play Wordle as a multi-turn agent inside a live OpenEnv/TextArena environment with GRPO, designed to run non-interactively on Hugging Face Jobs via papermill. Each rollout spans up to six guesses with stateful per-turn feedback and an environment-driven done signal; the trained agent is evaluated the faithful way, by playing complete games and measuring win rate. Open-source stack (TRL, OpenEnv, TextArena, Qwen3); tested end-to-end on Hugging Face Jobs. Registered in _toctree.yml (Agents Recipes) and index.md.
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
|
Ready for review 🙌 cc @merveenoyan @stevhliu — and @sergiopaniego, since this builds on the OpenEnv |
|
Hi @behroozazarkhalili, thanks a lot for the careful work on this pair of PRs! The notebooks are clearly written, the SMOKE gating and portability plumbing are nicely done, and I really like the core idea. Since GRPO holds the policy and generates the rollouts, submitting the notebook to HF Jobs with papermill instead of running it interactively is a genuinely useful pattern that isn't documented anywhere else. I also think that Jobs workflow is the real contribution here. The Wordle payload itself (system prompt, Consolidate #361 and #362 into this single recipe, reframed so the HF Jobs workflow is the star. Concretely:
A few concrete fixes on the current content, most of which apply regardless of the consolidation:
Happy to go back and forth on any of this. And again, the Jobs angle is a great addition, I'd just like it to land as one strong, self-contained recipe 🙌 |
… parity, calibrated sizing Reframes the recipe around the non-interactive HF Jobs + papermill workflow, which is the part not documented elsewhere, and credits TRL's official Wordle example for the environment and training setup it builds on. Review fixes (huggingface#362): - vLLM job command was missing `pip install ipykernel` and the kernel registration, so papermill would have died with NoSuchKernel before running a cell. All three command variants now register the kernel. - The flagship copy-paste command hardcoded SMOKE=0, handing a first-time reader the expensive run. SMOKE=1 is now primary; the full run is the documented follow-up. - SMOKE=0 sizing (150 steps x 64 grad-accum = 9600 rollouts in 3h on a10g-small) did not close. Rather than substitute another unverified constant, the notebook now measures seconds-per-rollout during the smoke run and projects the full run from it, reporting the largest GRPO_MAX_STEPS that fits each --timeout on the reader's own hardware. Every knob is env-overridable so the projection feeds straight back in. - The install cell was not idempotent (`git clone` with check=True and no guard) and duplicated the install already done by the job's bash command. It now short-circuits when the env module imports and guards the clone target. - Training injected the tool schema via environment_factory while eval called apply_chat_template without tools=, so the policy was judged on a prompt format it was never trained on, with a regex fallback hiding it. Measured on Qwen3-0.6B, same messages: 81 chars without tools= vs 845 with. Eval now derives tools via env_tools(), mirroring TRL's own discovery rule (public bound methods minus reset/get_reward), and CHAT_TEMPLATE_KWARGS is shared by config and eval so they cannot drift. The parser reports how many turns came from real tool calls so any residual mismatch stays visible. - torch_dtype="auto" -> dtype="auto" (deprecated in transformers v5); `import os` consolidated from four cells to one; H1 aligned with the _toctree.yml title. Self-containment: dropped the relative grpo_agent_reasoning_gym_hf_jobs.ipynb link and the chain_sum references, linking the OpenEnv SFT-warmup and end-to-end tutorials for the single-turn and warm-start story instead. Environment Space: the Wordle env inherits SUPPORTS_CONCURRENT_SESSIONS=False and the server refuses to boot with max_concurrent_envs>1, so the 1-session cap is by design and not raisable. Duplicating the Space is therefore documented as required for training runs -- it buys exclusivity and the restart rights needed to clear sessions leaked by a crashed run. The env Space is still cloned in full rather than installed with `pip install git+...`: pip's partial clone fails for this Space specifically (`fatal: expected 'packfile'`, empty worktree) while succeeding for others, so the docstring now records the exact command and error instead of asserting a general claim.
…tive reward
The first executed smoke run reported "win rate: 3/3 = 100%" for a 0.6B policy
after five GRPO steps. It had not won anything.
Verified against the live openenv/wordle Space by replaying the model's own
guesses ([apple], [banana], [cherry]) over a real session:
[apple] done=False reward=0.0 valid
[banana] done=False reward=0.0 INVALID - "must be exactly 5 letters"
[cherry] done=True reward=0.2 episode terminated
TextArena ends an episode that keeps submitting invalid moves and still pays
partial credit, so `env.done and env.reward > 0` scores rule-breaking as
success. Two games solved by a scripted solver against the same Space both
returned exactly reward=1.0, and a plain loss returned 0.0 - the terminal
reward is what distinguishes a solve. Eval now tests `reward >= ENV_SOLVED_REWARD`
and reports a solve rate rather than a win rate.
Also surfaces the failure mode the old metric hid: `WordleEnv` records whether
the env rejected each move, and the eval prints an invalid-move rate next to the
solve rate, so a policy breaking the rules reads differently from one losing
fairly.
Second bug, exposed by fixing the train/eval tool-schema mismatch: the tool
contract asks for '[abcde]', so the tool-call parse path returned an already
bracketed string and `env.guess(f"[{word}]")` re-wrapped it to '[[apple]]'. This
was previously unreachable - without tools= the model never emitted tool calls,
so the regex path (which strips brackets via group(1)) always ran. parse_guess
now normalizes brackets and deliberately does NOT repair a wrong-length word,
since that is a real invalid move the eval should report.
Notebook prose now names both traps, since "check what the environment pays on
its failure paths before treating positive as good" generalizes past Wordle.
Addresses the review request for committed outputs: readers now see a training
log, the reward delta, the calibration projection, and the eval without running
anything.
Executed non-interactively via papermill on HF Jobs (a10g-small, Qwen3-0.6B,
PAPERMILL_EXIT=0, 16/16 cells), against an owned duplicate of the environment
Space as the notebook now instructs. ANSI colour codes are stripped from the
captured output so the rich-formatted GRPO tables render as plain text.
What the committed numbers show:
measured: 92.4s over 40 rollouts -> 2.31s per rollout
projected full run (9,600 rollouts): 6.2h
--timeout 10800 (3.0h) -> GRPO_MAX_STEPS <= 54 [full run does NOT fit]
--timeout 43200 (12.0h) -> GRPO_MAX_STEPS <= 219 [full run fits]
solve rate: 0/3 = 0% mean reward=0.33
invalid moves: 3/9 turns (33%)
parse modes over 9 turns: tool_call=9 (100%)
The sizing table is the point: at the timeout the recipe used to recommend, only
54 of the advertised 150 steps actually fit. A zero solve rate is the correct
result for five GRPO steps on a 0.6B policy, and the 33% invalid-move rate says
why - the policy breaks the five-letter rule, which is what GRPO has to train
away first. tool_call=100% confirms the eval renders the same tool schema
training used.
|
Thanks for the thorough review, and for the consolidation call — I agree the Jobs/papermill workflow is the part worth keeping, and the recipe is much stronger framed that way. I've pushed a revision to #362 that does that. Everything below was verified against the notebook source rather than taken as read, and one item came back with a result worth flagging. Consolidation
The seven items on #3622. vLLM command — confirmed and fixed. The 4. Smoke-first — fixed. The primary copy-paste command is now 3. So the original recipe was off by roughly 2x on wall-clock: at the documented 5. Idempotency — confirmed and fixed. 5b. The promisor claim — I checked, and it turns out we're both right about our own Space. Reproduced directly: $ git clone --filter=blob:none https://huggingface.co/spaces/openenv/wordle
fatal: expected 'packfile'
fatal: could not fetch a6344aac8c09253b3b630fb776ae94478aa0275b from promisor remote
warning: Clone succeeded, but checkout failed. # exit 128, empty worktree
$ git clone --filter=blob:none https://huggingface.co/spaces/sergiopaniego/reasoning_gym
exit 0 # 10 files, cleanSo 6. Train/eval mismatch — confirmed, and it was bigger than it looks. Measured on Qwen3-0.6B with identical messages:
The 845-char version carries a full def env_tools(env) -> list:
return [m for n, m in inspect.getmembers(env, predicate=inspect.ismethod)
if n not in ("reset", "get_reward") and not n.startswith("_")]It names no tool, so swapping the env class keeps eval correct for free. 7. Minor — all fixed. 1. Committed outputs — done. The notebook now ships executed, The committed run turned up a bug your review nearly caughtYou predicted the regex fallback was making the win rate understated. It was actually hiding a broken metric in the other direction. The first executed run reported: for a 0.6B policy after five GRPO steps. It had not solved anything. Verified by replaying the model's own guesses over a real session against the Space: To find the real signal I scripted a solver and actually won games against the same Space: a genuine solve returns exactly Zero solves is the correct result for a smoke run — and the 33% invalid-move rate says why: the policy breaks the 5-letter rule, which is the first thing GRPO has to train away. There was a second, related bug that only became reachable because of your item 6. The tool contract asks for One thing your review surfaced that I think extends furtherYou argued on #361 that defaulting
So the 1-session limit is by design and can't simply be raised. A previous smoke run of mine died at the GRPO cell with The revision documents this as a required step for training runs (with the shared Space fine for the smoke run), because it also turned out to be the prerequisite for item 1 — the committed outputs above were produced against an owned duplicate, exactly as the recipe now instructs. On #361Happy to fold it in as you suggested. I've left it open for the moment so it doesn't go stale before this lands — glad to close it the moment you're happy with the consolidated recipe. For the record, its five items resolve as: (1) and (3) disappear with the SFT/teacher phase leaving the cookbook (and the open-model-first guideline is well taken — it would have applied), (2) is fixed in every surviving command variant, (4) is the point above, and (5) goes away with the cross-link. Thanks again — the sizing and train/eval catches in particular made this a materially better recipe. |
sergiopaniego
left a comment
There was a problem hiding this comment.
Thanks for the thorough revision! I verified everything against the notebook and it all checks out. The solve-rate fix and the calibration cell in particular make this a much stronger recipe.
Approving now. Note the failing doc build is unrelated to this PR: it is a doc-builder regression that breaks the build for every cookbook PR, fixed in huggingface/doc-builder#819. Once that merges we will re-run the check here and merge.
Tiny optional nit if you touch the notebook again: the %pip cell has the pip root-user warning committed as output, worth stripping.
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
What does this PR do?
Adds a new Agents recipe:
grpo_agent_wordle_hf_jobs— an end-to-end walkthrough that trains a small language model to play Wordle as a multi-turn agent inside a live OpenEnv/TextArena environment with GRPO (via TRL'senvironment_factorypath), designed to run non-interactively on Hugging Face Jobs withpapermill.Each rollout spans up to six guesses with stateful per-turn feedback and an environment-driven
donesignal; the trained agent is evaluated the faithful way — by playing complete games and measuring win rate (the agentic analogue of held-out accuracy).Why the "▶ Submit this notebook as an HF Job" panel? Unlike most recipes, it opens with a
hf jobs run …panel — deliberate, since GRPO holds the policy and generates rollouts, so the notebook is meant to be submitted to HF's cloud rather than run locally cell-by-cell. The recipe clones the cookbook itself andpapermills the notebook, so it works as-is once merged. An interactive (vLLM-off) path is included for reading/experimentation.Checklist against the contribution guide:
_Authored by:line right after the first header; "What you'll learn" + tiered References with links to every open-source resource.pip install/non-informative outputs; no empty code cells.SMOKE=1,papermillexit 0, multi-turn GRPO → play-the-game eval)._toctree.yml(Agents Recipes) andindex.md.A companion single-turn reasoning-gym recipe is submitted in a separate PR.
Who can review?
@merveenoyan @stevhliu — and @sergiopaniego, since this builds on the OpenEnv
reasoning_gymSpace and the SFT-warmup tutorial.