One question inside
NVIDIA-NeMo/RL #3410 —
feat(ppo): support async ppo. All permalinks pin the PR head 6e0362d.
Async PPO generates rollouts in the background while training runs. A background thread pulls prompt batches from the dataloader (_collection_loop) and puts finished rollouts into a replay buffer, each stamped with the training step it is meant for. A checkpoint saves two things side by side: the replay buffer, and the dataloader’s place — read straight off that same running loader (trajectory_collector.py:707, saved at ppo.py:2903). That place is already past every prompt handed to generation, including the ones whose rollouts have not come back yet, so a resume can never fetch those prompts again. This PR adds a flag that decides how many more prompts get thrown away on top of them.
Rows 2 and 3 are the same crash and the same checkpoint. The only difference is one boolean.
On resume the buffer counts how many prompt groups survived for each training step, and calls a step “incomplete” if it has fewer than a full batch.
replay_buffer.py:513 · replay_buffer.py:519
Then the flag picks a branch. One deletes those 5 rollouts; the other keeps them and prints a note.
Either way the collector then asks the buffer how many more it needs, and the loader supplies them starting from the place the checkpoint saved — #19.
replay_buffer.py:640 · ppo.py:466
Prompts #12–#18 are behind #19, so nothing can bring them back. Deleting the 5 finished rollouts therefore does not cause them to be regenerated — it causes them to be replaced by the next 7 prompts in the stream, and the compute spent on them is spent again.
Both branches end up training on 7 prompt groups, because get_trajectories_needed just asks for whatever is missing. So deleting the rows does not buy a better batch — the batch is the same size either way. It only changes which prompts fill it, and how much generation gets paid for twice.
| caller | value | matches main? |
|---|---|---|
| async GRPO — grpo.py:4143 | false | yes |
| async PPO — ppo.py:131 (the default; no recipe sets it) | true | no — new here |
On main, this resume path never deleted incomplete steps — it only printed them.
Deletion existed, but only on the branch taken when no training step is supplied, which resume does not take
(git show origin/main:nemo_rl/algorithms/async_utils/replay_buffer.py, lines 451–462).
So true is behaviour this PR introduces, for PPO only.
No. There is no run posted anywhere for this path — not in the PR description, not in any review
thread, and no nightly test resumes an async PPO job from a checkpoint. The unit test that builds a
replay buffer for PPO passes false (test_ppo.py:2361), so the shipped default is
not the branch being exercised there.
The thing to ask for before merging is one async PPO run stopped mid-generation and resumed, with
the resume log line reported: it prints either
Dropping incomplete restored targets: 11=5/7 or Incomplete target 11: 5/7, which
says directly which branch ran and how much was thrown away.
main both do, and
it shrinks the loss to only the rollouts genuinely in flight — the smallest it can be until the saved
loader place is rewound to cover them.