A gap found reviewing NVIDIA-NeMo/RL #3599 ยท
the problem that PR fixes is drawn here ยท
all permalinks pin the head 95844b6.
PR #3599 makes an async-GRPO checkpoint save the loader's place from before the oldest unfinished prompt, so a restart re-does them instead of skipping them. It rests on one rule: every prompt below that saved place is already trained. That rule breaks when a prompt group is allowed to fail. The run hands work to two training steps at once (trajectory_collector.py:216-222), and when one of them loses a group to a tolerated failure it is refilled from prompts much further down the stream (:666). Training that step then pushes the saved place past prompts that are still being answered for the other step โ and those are in no buffer, so nothing brings them back.
โ two steps at once trajectory_collector.py:216-222 ยท refill sizing :666 ยท release on failure :1218 ยท frontier grpo.py:4728-4731 ยท place picked :561-575
Not observed in a run โ this is static analysis. Nobody has reproduced it on hardware, and I could not run the suite on this host. What I can show is that every ingredient is present in one shipped recipe, and that the guard which would prevent it is switched off in exactly that configuration.
| Ingredient | nemotron-3-ultra/mopd.yaml | Why it matters |
|---|---|---|
max_generation_failures | 3 | A lost group is tolerated, so a step gets refilled from later prompts. |
max_trajectory_age_steps | 1 | Two steps are still filled at once in the window after any start or restart. |
in_flight_weight_updates | true | The refit does not wait for running workers (:830-845), so one is still going at save time. This is the guard that would otherwise close the gap. |
ft_save_period | 1 | A checkpoint is written every step, so the one inside that window always gets written โ and crash recovery restarts from the newest one. |
num_prompts_per_step | 1024 | Sets how much is lost: up to one full step of prompts. |
Six more recipes under nemo_gym/nemotron-3-super/ and
nemo_gym/grpo_qwen3_30ba3b_thinking_swe*.yaml carry the first three; they save less often,
so they need the save to land in the window rather than getting one there every time.
min(that, trained_up_to) as the cut-off. That keeps the rule the design needs โ
everything below the saved place is trained, failed, or kept in the buffer โ and costs nothing at
run time. The alternative, waiting for workers to finish before each save, is simpler but gives back
the speed that in_flight_weight_updates exists to buy.