feat(sc): periodic rollout checkpointing - #3924
Conversation
c610fbb to
d60719b
Compare
terrykong
left a comment
There was a problem hiding this comment.
Thanks @macandro96 — and thanks for the two pushes while this was in flight. Re-reviewed against 962a670a; several things we had written up you have already fixed, so they are not below.
The crash-consistency protocol is the part we tried hardest to break and could not. The fsync ordering (payload → COMMITTED → fsync → rename → fsync parent), _SNAPSHOT_RE anchored with .fullmatch() at every call site, max-by-integer sequence selection, and a fingerprint that is stable across runs all hold up. We checked each of those specifically and found nothing wrong with any of them.
Fixed since we started, noted so it is clear they were looked at rather than missed. The barrier nesting is gone and claim_for_training now binds as cut. _cleanup_consumed_metas_unlocked now gets its cut at both call sites — that one was worth catching, because git reported no conflict on single_controller.py during the rebase and the missed call site failed 23 tests in test_checkpointing.py on its own. rollout_recovery.py is out of the diff, so base's _reject_unknown_fields hardening survives. The duplicated bootstrap expected dict is now one helper. The PPO exemplar has the rollout_checkpointing block. And test_setup.py now exercises the restore path.
The partition test is the right call. assert declared | ignored == fields over all six schemas means a new config field cannot silently drop out of the fingerprint — someone has to decide. The one inline comment below is about a field that landed on the wrong side of it, not about the mechanism.
Other things worth saying out loud:
- Putting training ownership in the component that owns the rows (
TQReplayBuffer._training_claims) rather than standing up a second lifecycle model is the right shape. RolloutCheckpointConfig(extra="forbid")with the reasoning in the docstring is the right call, and reads as a direct answer to theextra="allow"thread on #3923.- The
would_save()/check_save()split intimer.pyis a clean seam. - Five setup-time guards, each with a unit test — every one turns a would-be silent no-op into a startup error.
Lint. Two failures, both from this PR, one command to fix. Against ruff 0.9.9 (pinned at .pre-commit-config.yaml:11): I001 in tests/unit/single_controller/test_rollout_checkpoint.py, and a ruff-format diff in tests/unit/single_controller/test_tq_replay_buffer.py. uvx ruff@0.9.9 check --select I --fix . && uvx ruff@0.9.9 format . clears both. This came from running the pinned hooks directly, because pre-commit cannot build its venv in this checkout.
No CI:* label. Every PR needs exactly one. This looks like CI:L1 — it does add an L1 functional test.
PR description. Still the stock template. Your lead commit already has a real body covering the what, and #3923's PR body is a good model, so this reads as an oversight. The three things most worth adding: whether this is on or off by default, the on-disk format, and the failure it is meant to survive.
One caveat on all of the above. No test CI has run on this PR, we had no GPU, and nobody has executed the new functional test grpo_async_gym_single_controller_streaming_recovery.sh. Its runner registration is correct (tests/functional/L1_Functional_Tests_SingleController.sh:189) and the file mode is right — we checked both — but the test itself is unrun. Getting /ok to test issued still matters more than any single comment here.
Generated by Claude Code
962a670 to
cc0e769
Compare
12d825d to
b1307f2
Compare
terrykong
left a comment
There was a problem hiding this comment.
Re-reviewed at b1307f2a4. Everything from the last round landed, and two of the behaviour changes hold up under a real run: the fingerprint now moves on bad_words while still ignoring port_range_low, and the interrupted-delete sequence that used to strand a snapshot directory forever is swept clean. All nine earlier threads are marked fixed and resolved.
Two comments below. The first is the one that matters: 36 unit tests fail because the test doubles have not kept up with the new call sites — no assertion fails and no behaviour looks wrong. The second asks for a better message on an error that should not be reachable.
The rebase worked too — the token-capture code from #3837 that was showing up here is now upstream and out of this PR's diff.
Generated by Claude Code
9534f00 to
73205a6
Compare
|
/ok to test 73205a6 |
73205a6 to
1557d03
Compare
|
/ok to test 1557d03 |
|
/ok to test ba4413f |
Add periodic TQ, replay, lineage, and dataloader snapshots anchored to durable trainer state. Roll back groups claimed by an unfinished optimizer step into replayable state during persistence, restore them on restart, and cover partial-sibling plus 2/8 and 6/8 streamed recovery paths. (cherry picked from commit 98c196f) Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
(cherry picked from commit 4cd773d) Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
ba4413f to
17b86fb
Compare
|
/ok to test 17b86fb |
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
|
/ok to test 2835680 |
Picks up periodic rollout checkpointing (#3924) and the partial-rerun routing fix (#4048), which landed while the previous merge was being verified. One conflict, in the critic training block: main now raises _optimizer_commit_in_progress before the value_training timer so a periodic snapshot cannot land mid-update. Kept, with our rl.sc.value_training span re-applied around the timer and the flag left where main set it -- outside the span, since it guards the whole irreversible update rather than the measured region. Signed-off-by: Raj Singh <rajsin@nvidia.com>
Two conflicts, both pure adjacency -- independent additions landing at the same file position, so both sides are kept: - nemo_rl/algorithms/single_controller.py: this branch's _SUPERVISOR_DRAIN_TIMEOUT_S constant sits where #3924 (periodic rollout checkpointing) added _MAX_CONSECUTIVE_ROLLOUT_CHECKPOINT_FAILURES and the _RolloutCheckpointCut dataclass. Neither refers to the other. - tests/unit/environments/test_nemo_gym_utils.py: #4014 (L2 Gym-to-RL rollout acceptance) appended a spinup-cleanup test to the same end-of-file this branch appended TestUnresolvedAgentRefsAreDiagnosable to. main's function first, this branch's class after it. The other three commits (#3730 colocated MInf, #4002 actor venv list from a Python leaf module, #4048 partial rerun routing) merged clean. Verified after the merge: the restart keys are still in both SC exemplars, on_dead_shard is still absent from the config and both yamls, and 1287 tests pass across single_controller, fleet_health, weight_sync and the gym utils. Signed-off-by: asolergibert <asolergibert@nvidia.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What does this PR do?
Adds periodic rollout-state checkpointing to Single Controller.
Unlike trainer checkpoints, which are written at completed training-step boundaries, these snapshots can capture newer rollout progress while generation and streamed training are active. On restart, Single Controller restores the latest compatible rollout snapshot anchored to durable trainer state.
This PR builds on #3923, which provides sibling-level rollout recovery.
Why?
Long-running asynchronous rollouts may make significant progress between trainer checkpoints. If the job is interrupted, previously completed generations and buffered rollout data would otherwise need to be regenerated.
Periodic rollout snapshots reduce the amount of rollout work lost without changing the model, optimizer, or trainer checkpoint frequency.
Design
A periodic snapshot contains matching:
Snapshots are anchored to immutable trainer state:
Snapshots are built under a temporary directory, flushed, and published using an atomic directory rename. Retention first renames stale snapshots out of the live namespace before deleting them.
Streaming training recovery
The trainer may already own rollout groups when a periodic checkpoint occurs during a streamed training step. Those groups remain in TQ until the optimizer step succeeds and are represented in checkpoint replay metadata. If the job crashes before the optimizer step completes, they become ordinary replayable groups after restoration.
Checkpointing is fenced against data-plane mutations and the final optimizer commit/cleanup transition so the TQ snapshot, replay metadata, and controller state describe one logical point.
Restore behavior
rollout_checkpointing.restore_modecontrols snapshot selection:latestrestores the newest compatible published periodic snapshot.trainer_checkpointignores newer periodic snapshots and restores rollout state bundled with the trainer checkpoint.Restore selection is read-only. If bootstrap rollout state exists but no trainer checkpoint does,
trainer_checkpointfails without deleting or ignoring that state because there is no matching model/optimizer checkpoint. Uselatestto recover it or choose a new checkpoint directory to start fresh.When restoring the latest periodic snapshot:
This PR does not implement mid-token prefix recovery. A generation without a sealed recovery record must still be redispatched.
Configuration
snapshot_attempt_interval_sis an attempt cadence. A periodic write still requires a durable trainer anchor for the current completed step, socheckpointing.save_period: 1provides continuous post-step coverage.Before the first trainer checkpoint, bootstrap compatibility is fail-closed: the full configuration participates unless a path is explicitly classified as operational. External integrations can add operational-only paths to
extra_fingerprint_excluded_paths.