For
NVIDIA-NeMo/RL #3925.
All permalinks pin a4c10a78. The evidence is a
round-trip test
that runs the real code through a borrow, a checkpoint and a restore.
Three pages from the #3925 review β this one: whether restoring can lose a prompt.
Every group in the recovery ledger is in one of two phases:
| phase | what happened | sealed siblings | on restore |
|---|---|---|---|
RESERVED |
claimed from the dataloader, but the sampler admission never committed β so it was never dispatched | always 0 | re-admitted as a batch β the only phase the discard below can touch |
ADMITTED |
admission committed; dispatch happens after, so generation may have started | 0 up to group size β 1 | relaunched unconditionally, never dropped |
The phase is set when the group is first recorded β
ADMITTED if admitted else RESERVED
β and moves to ADMITTED when the admission commits. Re-admitting a RESERVED batch
runs one branch that can release a prompt:
single_controller.py:1149-1152
It fires when the replay buffer already holds a group stamped for the step this batch is being admitted to. The one mechanism that moves finished groups between steps is the borrow: it re-stamps a finished group into an earlier step and sends a spare to repay the later one, and it runs with rollout recovery on (:1983) as well as off (:2020). Even so, a borrow cannot make the discard fire β the test below runs one through a checkpoint and a restore to show it, and then shows the one thing that can.
Real: the rollout pump with its admission, spare pool, borrow and repayment; TQReplayBuffer;
the recovery ledger; the in_order sampler; the checkpoint barrier;
_capture_rollout_checkpoint_cut;
and, in a fresh controller, _maybe_restore_replay_buffer, _maybe_restore_rollout_recovery
and _redispatch_restored_rollouts.
Faked: generation itself, and the tensor converter. Group size 2, 3
groups per step, lookahead 2, on_dropped_prompt: replace. Prompts are
numbered in dataloader order; batch 3β5 became the spare pool.
What the test prints, both ways (labels shortened; every value is verbatim):
A re-admitted batch is stamped
dispatch_index + 1.
Nothing in the buffer can carry that stamp: the borrow only takes from a step that was already admitted
(promote_ready_group
picks among slots that exist), and the repayment spare takes the lenderβs stamp
(:1987).
So count_for_target_step
is 0 for the new step, and the discard never runs. The cursor is written with every checkpoint β
rollout snapshots at :3710,
train-boundary saves at :4296
β and restored at :479.
The rebuild from trainer_version at
:477
only runs for a checkpoint that has no saved cursor.
What this means for the PR. On every checkpoint this code
writes, groups_considered and groups_redispatched are equal β the discard is
the only thing that could separate them, and it needs a cursor below stamps already in the buffer. One
of the two counters is redundant. And the branch itself should not drop prompts: on a correct cursor it
never runs, and on a wrong one it loses data with only a print. Better to raise there, so a bad cursor
stops the run instead of quietly shrinking the dataset.
Nothing in CI runs a borrow through a checkpoint, and nothing re-admits a
RESERVED batch against a real buffer β every scenario in the recovery matrix builds groups
with admitted=True.
The test above
covers both, and is offered for the PR as is.