NVIDIA-NeMo/RL PR #3480, new since the last review round โ
the rollout recovery ledger. Links pinned to bca9a721.
Part 1 of 3 ยท the atomic cut ยท the proof
The last review round ended with a known gap: a rollout still generating when a checkpoint
lands was dropped, and its prompt with it โ the dataloader had already moved past it and
never rewinds. The author's answer is a new file,
rollout_recovery.py,
built around one rule: at any instant, every prompt has exactly one durable owner.
Either the dataloader has not handed it out yet, or the new ledger owns it (handed out,
rollouts not finished), or the replay buffer owns it (finished, waiting to be trained on).
A checkpoint saves all three owners in one atomic cut, so a restart can rebuild exactly
the in-flight work it interrupted.
Worked example โ all numbers distinct. Config: 4 rollouts per prompt group, gate lets generation run 1 step ahead, trainer at step 7:
| prompt group | state when the checkpoint lands | owner saved | on restart |
|---|---|---|---|
| g20 (step 7) | all 4 rollouts finished, in TQ | replay buffer | trained on directly โ nothing regenerates |
| g21 (step 8) | 2 of 4 rollouts still generating | ledger, ADMITTED, target_step=8 | redispatched for step 8; all 4 regenerate |
| g22 (no step yet) | taken from the loader, waiting at the gate | ledger, RESERVED | goes through admission again, then dispatches |
| g23 | still in the dataset | dataloader position | served normally when the loader reaches it |
Before this change, g21 and g22 were simply gone: the loader was saved past them and no file held them. The earlier review round got the finished case (g20) recovered; this round closes the unfinished ones.
The in-memory record keeps the live prompt by reference, but
state_dict()
persists only a locator: the dataset row number (idx) and
task_name. No prompt text, no tensors โ the file stays tiny regardless of
prompt length. Restore rebuilds each prompt by reading
dataset[idx] again and re-running the same one-row
collate_fn the original dispatch used
(_rehydrate_rollout_recovery_prompts).
This is safe under shuffling: shuffle changes the order rows are visited, not which
row an idx names.
Restore refuses to guess. The chain, each step a hard error on mismatch:
1 TQ metadata advertises the file โ file missing? error
2 file exists, metadata silent? โ orphan from another run error
3 sha256(file) vs metadata โ torn/stale pair error single_controller.py:624
4 schema_version == 1 โ written by other code error
5 group count vs metadata โ truncated file error
6 per-group field validation โ corrupt record error rollout_recovery.py:347
7 drop groups the replay buffer owns โ never two owners discard_canonical_groups
8 rebuild each prompt, check idx + task_name match error on mismatch
idx. The author removed content fingerprints deliberately
(commit fd1e424e5) and pinned the behavior with a test โ a reordered or
shrunk dataset is caught, an edited row is not. Resuming across a dataset edit silently
trains the recovered groups on the new content.