fix(examples/grpo): build rollout_infos before _close_and_remove in sudoku scheduler (#9745)#9748
Conversation
There was a problem hiding this comment.
Code Review
This pull request fixes a KeyError and data loss issue in sudoku_scheduler.py by snapshotting rollout_infos before calling _close_and_remove(uuid), which cleans up the reward tracking dictionaries. The reviewer suggests also including other accumulated reward metrics (such as empty cell, valid move, repetition, progress, and correct rewards) in rollout_infos when a parse failure occurs on later turns, to prevent losing these metrics and to maintain consistency for downstream logging.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| rollout_infos = { | ||
| 'total_reward': self._total_rewards[uuid], | ||
| 'step_rewards': list(self._step_rewards.get(uuid, [])), | ||
| 'gym_done': True, | ||
| } |
There was a problem hiding this comment.
When a trajectory fails to parse on a later turn (i.e., current_turn > 1), it already has accumulated metrics in self._empty_cell_scores, self._valid_move_scores, etc. By only returning total_reward, step_rewards, and gym_done in rollout_infos, these accumulated metrics are lost. Additionally, downstream logging or metric aggregation might expect a consistent set of keys in rollout_infos and could fail or log incomplete data if these keys are missing.
We should include all the reward component metrics in rollout_infos here as well, using safe defaults/averages.
| rollout_infos = { | |
| 'total_reward': self._total_rewards[uuid], | |
| 'step_rewards': list(self._step_rewards.get(uuid, [])), | |
| 'gym_done': True, | |
| } | |
| remaining = 81 - self._initial_filled.get(uuid, 0) | |
| progress_score = (self._max_filled.get(uuid, 0) - self._initial_filled.get(uuid, 0)) / remaining if remaining > 0 else 1.0 | |
| rollout_infos = { | |
| 'total_reward': self._total_rewards[uuid], | |
| 'step_rewards': list(self._step_rewards.get(uuid, [])), | |
| 'gym_done': True, | |
| 'empty_cell_reward': sum(self._empty_cell_scores.get(uuid, [])) / max(len(self._empty_cell_scores.get(uuid, [])), 1), | |
| 'valid_move_reward': sum(self._valid_move_scores.get(uuid, [])) / max(len(self._valid_move_scores.get(uuid, [])), 1), | |
| 'repetition_reward': sum(self._repetition_scores.get(uuid, [])) / max(len(self._repetition_scores.get(uuid, [])), 1), | |
| 'progress_reward': progress_score, | |
| 'correct_reward': sum(self._correct_scores.get(uuid, [])) / max(len(self._correct_scores.get(uuid, [])), 1), | |
| } |
|
Thanks for the fix! The current change correctly addresses the I also want to share a possible workaround for the second issue mentioned in #9745 (OpenEnv session leak when the trajectory ends due to A minimal scheduler-level workaround could be adding the external termination conditions into max_turns = 20
if done or current_turn >= max_turns - 1:
await self._close_and_remove(uuid)
done = True
return {
"done": done,
"rollout_infos": rollout_infos,
}The same idea could be extended to This would provide a small, example-level workaround for users running multi-turn GRPO with OpenEnv, without requiring framework-level changes. That said, I agree that the cleaner long-term solution would be introducing a scheduler lifecycle hook such as
A unified lifecycle callback would make cleanup behavior consistent across different schedulers. Thanks again for the fix! |
|
thanks |
What
Fixes the first defect reported in #9745: in the OpenEnv Sudoku GRPO example, the parse-failure branch of
SudokuScheduler.on_turn_endbuilds itsrollout_infosdict after calling_close_and_remove(uuid).OpenEnvScheduler._close_and_remove(inswift/rollout/multi_turn.py) popsself._total_rewards[uuid]andself._step_rewards[uuid]. So when the model emits an unparseable action:self._total_rewards[uuid]raisesKeyError(andstep_rewardssilently comes back empty). Since unparseable model output is common early in GRPO training, this crashes rollouts.Fix
Snapshot
rollout_infosbefore cleanup — mirroring the already-correct terminal path at the end ofon_turn_end(which buildsrollout_infosfirst, then calls_close_and_remove). Minimal, behavior-preserving for the happy path.Note on the second reported issue
#9745 also reports an OpenEnv session leak when a trajectory stops because it hit
--max_turns(orfinish_reason == 'length') rather than the env returningdone=True. That one is a framework-level gap:MultiTurnScheduler.run()returns as soon asshould_stopis set and never invokes a post-trajectory cleanup hook, soon_turn_end(the example's only entry point) has no way to release the env on a trainer-forced stop. Fixing it properly means adding a trajectory-end hook to the base scheduler and havingGYMScheduler/OpenEnvSchedulerclose any surviving env — a change with broader blast radius across all schedulers, so I've left it out of this minimal fix and defer to maintainers on the preferred shape. Happy to follow up with that if you'd like.🤖 Generated with Claude Code