How the dataloader cursor advances past in-flight prompts at checkpoint save
Companion illustration for
NVIDIA-NeMo/RL #3429
(SingleController checkpoint save/restore) and
#3594.
All permalinks pin the PR head c039949.
The SingleController path runs a rollout pump (generates completions) and a
train pump (consumes them) concurrently, connected by a replay buffer. The cursor
moves at batch granularity — the instant
for prompt_batch in self._dataloader: yields a batch, the cursor is past it
(single_controller.py:375) —
while dispatch inside the batch proceeds one prompt at a time, each gated on a
buffer/in-flight permit
(:398-400).
The checkpoint saves that already-advanced cursor
(:698)
but only ready buffer groups
(replay_buffer.py:861).
batch 1 ─▶ generated ─▶ trained (step 1) ✓ gone from the buffer
batch 2 ─▶ generated ─▶ trained (step 2) ✓ ◀━━ SAVE fires here (save_period=2)
batch 3 ─▶ generated, not yet trained → buffer slots 0,1 (ready)
batch 4 ─▶ dispatched, generation in flight → buffer slots 2,3 (unready)
batch 5 ─▶ yielded, 0 of 2 prompts dispatched — parked on a full buffer
batch 6 next read — this is where the saved cursor points
Buffer at the instant of the save (capacity 4, full):
slot: 0 1 2 3
from batch: 3 3 4 4
ready: True True False False
status: SAVEDSAVEDDROPPEDDROPPED
The consequence. The saved cursor says "resume from batch 6" — but batch 4
(slots 2,3, dropped as unready) and all of batch 5 (never dispatched) are behind it. On resume
those 4 prompts are neither in the buffer nor re-drawn from the loader: silently never trained
on. The bias is non-uniform — long-generation prompts are disproportionately likely to be in
flight at a save boundary, so they are disproportionately the ones dropped. The interim fix
(persist the pending prompts' DatumSpecs and re-dispatch on resume) is requested in
the #3429 review; full partial-rollout
persistence is tracked in
#3594.