feat(sc): support drop_incomplete_targets_on_restore in in_order - #3813
feat(sc): support drop_incomplete_targets_on_restore in in_order#3813yuki-97 wants to merge 2 commits into
Conversation
|
/ok to test 4d45be5 |
yuki-97
left a comment
There was a problem hiding this comment.
Team review (5 agents: RL expert, bug finder, test agent, design reviewer, devil's advocate).
No correctness findings. The deadlock/liveness audit came back clean: peak buffer occupancy is N*(L+1) <= C and is identical with the flag on or off (gap-fill holds k restored permits and dispatches N-k; drop holds 0 and dispatches N), which rests on drop_last=True on the dataloader so every batch is exactly num_prompts_per_step. Dropping only ever reduces held permits. The sibling-inheritance check against the legacy async-PPO implementation found three structural deltas, all inherited or outcome-identical. Drop-before-truncate is right and the ordering is enforced by an executable test, not just the docstring.
Four suggestions below, all tests and docs.
Two caveats on this review: nothing was executed (uv could not run in this environment), so the mutation-survival claims are hand-traced against the source rather than run; and the perf evidence attached to the PR is a PPO run, where the shrink caveat you disclosed cannot fire by construction -- not asking for more, just noting the GRPO x shrink path has no run behind it.
Generated by Claude Code
|
/ok to test 2fcc765 |
220a173 to
2fcc765
Compare
2fcc765 to
2ccfec2
Compare
85ad93c to
7b09c83
Compare
…e the in_order sampler Signed-off-by: Yuki Huang <yukih@nvidia.com>
…restore gate, add its migration-table row, split the collapsed group-count thresholds in the restore tests Signed-off-by: Yuki Huang <yukih@nvidia.com>
7b09c83 to
fdc992a
Compare
|
note for those reading that we'll probably focus on getting #3480 in so we can restore 100% accurately instead of dropping |
What does this PR do ?
Ports
drop_incomplete_targets_on_restorefrom the legacy async-PPO path to the SingleController path, so a resume can discard a partially restored target step instead of gap-filling it.Legacy async PPO/GRPO has had this knob since #3410 (
ppo.async_ppo.drop_incomplete_targets_on_restore,nemo_rl/algorithms/async_utils/replay_buffer.py:531). SC had no equivalent: a resume always gap-filled. This addsasync_rl.drop_incomplete_targets_on_restore(defaultfalse, so existing runs are byte-identical).What a "partially restored target step" is. In-flight rollouts are not written to the checkpoint, so a step whose batch was still filling comes back with fewer than
num_prompts_per_stepgroups stamped for it.false(default) — gap-fill._rollout_pumpdispatches only the missing prompts for that step and drops the rest of that dataloader batch (nemo_rl/algorithms/single_controller.py:630-632).true— drop. The restored groups of that step are discarded at load time and the step is dispatched whole from subsequent prompts.Neither setting regenerates the original prompts, and both consume exactly one dataloader batch per admitted step.
A complete restored target step is already skipped — no new branch is needed for it. The same clamp does both jobs:
num_prompts = max(0, prompt_batch.size - buffered). When the step is full,num_promptsis0, the dispatch loop body never runs, and that dataloader batch is consumed without generating anything. This PR only changes whatbufferedis for incomplete steps. Two things worth knowing about that skip, both pre-existing: the skipped batch's prompts are discarded (the dataloader is not rewound, matching legacy'slast_target_weight_already_generatedskip), andadmitis still called and still advances the dispatch index, which it must — otherwise the stamp sequence drifts one step off the trainer version.in_orderonly.validate_single_controller_configraisesNotImplementedErrorunder any other sampler,customincluded.in_orderis the only sampler that stamps atarget_step; underwindowed/weight_fifo/ready_firstevery group is fungible across steps, so no restored group belongs to a step that could be incomplete and the flag would silently change nothing.customis rejected because setup cannot tell whether it stamps without importing it.Ordering: the drop runs before the over-capacity truncation in
load_state_dict. Truncating a target-stamped envelope raises, and the drop can bring an over-capacity checkpoint back undermax_buffered_rollouts— so it must come first.Issues
None closed. Part of the SingleController parity work tracked in #2625.
Usage
Before your PR is "Ready for review"
Pre checks:
Additional Information
Stacked on
yukih/sc-ppo— base branch is that PR, notmain.No new deadlock surface. Dropped groups never acquire a
_buffer_capacitypermit (load_state_dictreturns the post-drop count), and per-step capacity pressure is identical either way: gap-fill holdskrestored permits and dispatchesbatch - k; drop holds0and dispatchesbatch. Dataloader consumption is also identical, so_clamp_max_num_steps's step budget is unaffected. Dropping strictly reduces the restored group count, so it can only make the over-capacity raise fire less often.One caveat.
_batch_shortfallis not checkpointed, so a GRPO step that was legitimately shortened byon_dropped_prompt="shrink"reads as incomplete after a resume; with the flag on it is re-dispatched in full. That is extra rollout work, not a stall. PPO is unaffected — setup rejects drop budgets there.Tests:
tests/unit/single_controller/test_tq_replay_buffer.py(drop / unstamped groups untouched / drop-before-truncate),test_resiliency_config.py(accepted underin_order, raises under the other four),test_checkpointing.py(the flag reaches the buffer).uv run --group test pytest tests/unit/single_controller/test_tq_replay_buffer.py tests/unit/single_controller/test_resiliency_config.py tests/unit/single_controller/test_checkpointing.pyTest results. Yellow is the legacy path with the same settings, as a baseline. Red is v2. Blue is v2 resumed from checkpoint 20. Green is v2 with this PR, resumed from checkpoint 20 with

drop_incomplete_targets_on_restore: true.