Skip to content

feat(sc): recover replay buffer from native TQ checkpoints - #3480

Draft
macandro96 wants to merge 15 commits into
mainfrom
amahishi/sc-tq-native-recovery
Draft

feat(sc): recover replay buffer from native TQ checkpoints#3480
macandro96 wants to merge 15 commits into
mainfrom
amahishi/sc-tq-native-recovery

Conversation

@macandro96

Copy link
Copy Markdown
Contributor

What does this PR do ?

Summary

Adds authoritative recovery of completed, unconsumed Single Controller rollouts from native TransferQueue checkpoints.

TQ is the sole durable store for rollout tensor payloads. The SC checkpoint stores only a metadata replay index describing which TQ rows belong to ready prompt groups. On restart, TQ is loaded first and SC reconstructs its local replay-buffer indexes without copying tensors back into TQ.

This is a stacked change and currently depends on:

Motivation

Previously, SC checkpoint recovery either serialized rollout tensors separately in replay_buffer.pt or restarted with an empty rollout buffer.

Serializing tensors in both the replay-buffer checkpoint and TQ:

  • duplicates potentially large payloads;
  • creates two possible sources of truth;
  • requires re-putting tensors into TQ during recovery;
  • cannot guarantee that the replay index and TQ snapshot describe the same rows.

This change makes the native TQ snapshot authoritative for tensors and keeps only the controller metadata required to resume consuming those rows.

Checkpoint contents

step_N/
├── policy/
│   ├── weights/
│   └── optimizer/
├── train_dataloader.pt
├── training_info.json
├── data_plane/                 # Native TQ checkpoint; owns tensors
└── replay_buffer_metadata.pt   # Metadata-only SC replay index

For every completed prompt group, the replay sidecar stores:

  • group ID;
  • TQ partition and sample IDs;
  • start/end policy weight versions;
  • optional target step;
  • sequence lengths, fields, tags, and auxiliary KVBatchMeta information.

It does not contain rollout tensors or fields_data.

Save workflow

flowchart LR
    Generation["Generation workers"] -->|"Completed prompt group"| Commit["TQReplayBuffer.commit"]
    Commit -->|"Tensor rows"| TQ["Canonical TQ partition"]
    Commit -->|"Ready-group metadata"| Index["SC replay index"]

    SC["Single Controller checkpoint"] --> Barrier["Exclusive checkpoint barrier"]
    Barrier --> TQSnapshot["Save native TQ snapshot"]
    Barrier --> Sidecar["Capture metadata-only replay index"]

    TQSnapshot --> Validate["Validate exact sample-ID inventory"]
    Sidecar --> Validate
    Validate --> Bundle["Finalize step_N bundle"]
Loading

Commits and destructive clears participate in a shared/exclusive checkpoint barrier:

  • normal mutations may run concurrently;
  • a checkpoint waits for active mutations to finish;
  • new mutations wait while the snapshot is being captured;
  • generation may continue, but completed groups wait at commit;
  • the TQ snapshot and metadata sidecar therefore describe the same canonical rows.

Checkpoint finalization fails if:

  • TQ save fails;
  • the metadata sidecar names a missing TQ row;
  • TQ contains an unexpected canonical row;
  • serialization or bundle finalization fails.

The previously finalized checkpoint remains available as the fallback.

Restore workflow

flowchart LR
    Bundle["Latest finalized step_N"] --> Trainer["Restore model, optimizer, dataloader"]
    Bundle --> Bootstrap["Clean TQ bootstrap client"]
    Bootstrap -->|"Load data_plane/"| TQ["Restored native TQ state"]
    TQ --> Client["Create normal SC data-plane client"]
    Bundle -->|"Load replay metadata"| Index["Rebuild local replay index"]
    Client --> Validate["Validate digest, group count, and exact inventory"]
    Index --> Validate
    Validate --> Pumps["Start rollout and train pumps"]
Loading

Restore happens before SingleControllerActor.run() and before any ordinary data-plane operation.

Recovery validates:

  • checkpoint schema versions;
  • trainer step, trainer weight version, and epoch;
  • sampler name and partition ID;
  • replay manifest digest;
  • replay group count;
  • configured buffer capacity;
  • exact equality between sidecar sample IDs and canonical TQ sample IDs.

No rollout tensor is re-serialized or re-put into TQ.

Sampler support

Authoritative replay recovery currently supports the ungated windowed sampler.

windowed
    restored in-window groups → selectable
    restored stale groups     → evicted by the normal sampler path

weight_fifo / in_order
    native TQ snapshot        → shadow mode only
    replay recovery           → not enabled

Gated samplers need additional durable dispatch/quota state before completed subsets can be recovered safely.

Sampler configs now declare their replay-checkpoint capability, and the sampler factory verifies that the declaration matches the runtime implementation. Custom sampler capability is checked after construction.

Configuration

checkpointing:
  enabled: true

data_plane:
  enabled: true
  impl: transfer_queue
  backend: simple
  checkpointing_enabled: true

async_rl:
  sampler:
    name: windowed
    max_staleness_versions: 1

Current native restore support requires the TQ simple backend. Unsupported configurations fail during setup rather than silently producing incomplete checkpoints.

Compatibility

  • Legacy tensor-bearing replay_buffer.pt checkpoints are detected and rejected with an actionable error.
  • Checkpoints without any replay artifact continue with an empty replay buffer.
  • Gated samplers retain shadow-mode native TQ saves but do not advertise recoverable rollout state.
  • Trainer checkpoints remain immutable; TQ and replay metadata are finalized inside the same step bundle.

Testing

  • Native TQ fresh-process save/load verifier
  • Native TQ parent-directory rename round trip
  • Focused checkpoint, replay-buffer, data-plane lifecycle, and interface unit tests
  • bash tests/functional/grpo_dp_single_controller_tq_recovery.sh
    • phase 1 trains one step and saves an authoritative TQ checkpoint;
    • phase 2 starts a fresh process;
    • native TQ state and replay metadata are restored;
    • exact inventory is validated;
    • training continues to step 2.
  • bash tests/functional/grpo_dp_single_controller.sh
    • existing SC convergence regression test

Scope and limitations

This PR recovers completed, unconsumed prompt groups.

It does not yet recover:

  • token-level unfinished generations;
  • in-flight reservations that have not committed canonical TQ rows;
  • NeMo-Gym episode/environment state;
  • sandbox memory or filesystem state;
  • gated-sampler dispatch ledgers.

The planned #3456 integration remains upstream of this mechanism:

Staged token records
        ↓
Receipt/finalizer recovery
        ↓
Canonical completed rollout in TQ
        ↓
This PR: replay-index restoration and trainer consumption

Review guide

The primary areas to review are:

  1. TQReplayBuffer metadata-only serialization and restore validation.
  2. Shared/exclusive checkpoint-barrier coverage for commits and clears.
  3. Native TQ restore ordering in SC setup.
  4. Fail-loud schema, digest, inventory, and configuration checks.
  5. Windowed sampler recovery semantics and gated-sampler exclusions.
  6. Two-process functional recovery coverage.

Issues

List issues that this PR closes (syntax):

Usage

  • You can potentially add a usage example below
# Add a code snippet demonstrating how to use this

Before your PR is "Ready for review"

Pre checks:

  • Make sure you read and followed Contributor guidelines
  • Did you write any new necessary tests?
  • Did you run the unit tests and functional tests locally? Visit our Testing Guide for how to run tests
  • Did you add or update any necessary documentation? Visit our Document Development Guide for how to write, build and test the docs.

Additional Information

  • ...

haitian-nvidia and others added 15 commits July 29, 2026 16:14
…ting

Port the replay-buffer checkpoint state capture from PR #3138 onto the
split single-controller path. state_dict snapshots ready slots on the
event loop, then fetches each group's DataPlane rows; unready
reservations (in-flight rollouts) are dropped. load_state_dict validates
the envelope (partition, group size, sample_id uniqueness) before any
DataPlane write, truncates to the current capacity keeping the freshest
groups, and re-puts rows while rebuilding the parallel slot lists.
Staleness filtering is intentionally left to the sampler's first evict.

Covered by 9 new unit tests (round-trip, preflight rejection,
capacity truncation); 20/20 pass in tests/unit/single_controller/
test_tq_replay_buffer.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: haitian-nvidia <haitianj@nvidia.com>
Two additions the SC checkpointing path needs from the sampler layer:

- resume_from_step: BaseSampler (and the three built-in policies +
  create_sampler) now accept the trainer step the run starts from — 0
  for a fresh run, the restored current_step on resume. It seeds the
  dispatch cursor to preserve the fresh-start invariant
  _dispatch_index == trainer_version - 1. Without it a restored
  InOrderSampler stamps target_steps from 0 and every dispatched batch
  is instantly evicted (target < trainer_version), livelocking the
  train pump. create_sampler forwards the kwarg to custom samplers
  only on resume, so fresh starts don't constrain their constructors
  and an unsupported class fails loudly instead of silently running
  with an unseeded cursor.

- supports_buffer_checkpoint: new PromptGroupSampler property gating
  replay-buffer save/restore. Only the ungated WindowedSampler returns
  True — gated policies dispatch a fixed quota per trainer step, so
  restored groups could never complete an already-consumed window.

Covered by 8 new unit tests in test_sampler_interface.py (cursor
seeding, gate behavior after resume, factory forwarding, custom
fail-loud, checkpoint-support matrix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: haitian-nvidia <haitianj@nvidia.com>
Replace the checkpointing NotImplementedError guard with the actual
driver-side resume wiring, following the grpo.py setup pattern:

- Build a CheckpointManager unconditionally and resolve the latest
  checkpoint: load_training_info() populates save_state (default
  GRPOSaveState when starting fresh) and get_resume_paths() yields the
  weights/optimizer paths.
- _build_trainer takes kw-only weights_path/optimizer_path and forwards
  them to TQPolicy (previously hardcoded to None) on both the colocated
  and non-colocated build paths.
- Restore the dataloader position from train_dataloader.pt when present
  (load_dataloader_state, with its dataset-swap guard); warn and start
  fresh otherwise. Runs before _clamp_max_num_steps as before.
- Forward checkpointing.pretrained_checkpoint into the policy config.
- SingleControllerActorArgs carries two new fields, save_state and
  last_checkpoint_path, for the actor-side restore (next step).

Saving itself is not wired yet — that lands in the SingleControllerActor
train pump next. Existing tests updated for the new surface: the setup
tests' hand-built checkpointing block now carries the keys
CheckpointManager indexes, and the pump tests pass the two new
ActorArgs fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: haitian-nvidia <haitianj@nvidia.com>
Wire the actor side of SC checkpointing (the setup/resume half landed in
the previous commit), with Megatron async_save supported end to end:

- Restore: __init__ rebuilds counters (train_steps/trainer_version/
  current_epoch/consumed_samples/total_valid_tokens) from the save_state
  loaded by setup, seeds the sampler with resume_from_step, and run()
  reloads replay-buffer groups (ungated samplers only, one capacity
  permit per restored group) before the pumps start.
- Save: after each weight sync, _save_checkpoint mirrors
  async_grpo_train's block — finalize_pending flushes the previous
  background finalization, save_checkpoint returns after D2H staging
  under async_save, aux state (training info, dataloader position,
  replay buffer when the sampler supports it) is written synchronously,
  then begin_finalization defers the tmp->step rename until the async
  weight writes complete. run() flushes the last checkpoint via
  checkpointer.shutdown() on every exit path.
- TimeoutChecker drives checkpoint_must_save_by: a timeout save also
  stops training early, matching the legacy loops.
- latest_checkpoint_status.json is refreshed after each save for
  external watchdogs (reuses grpo's _write_latest_checkpoint_status).

The pump tests' hand-built configs gain the checkpointing block the
actor now reads (enabled=false keeps them write-free).

Validated end to end on GB200 (Qwen3-0.6B, megatron async_save=true):
4-step run saves step_2/step_4 with no tmp_step_* leftovers; a
checkpoint_must_save_by run stops early with a complete checkpoint; the
resume run restores dataloader + 4 replay groups and continues from
step 2 to step 4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: haitian-nvidia <haitianj@nvidia.com>
Port the checkpointing test suite from PR #3138 onto the split SC
architecture (actor_args, the PromptGroupSampler protocol, the split
trainer step API) and extend it for the async-save path:

- counter/sampler-cursor restore, save triggers (period boundary, last
  step, checkpoint_must_save_by timeout, disabled, save_optimizer),
  metric_name handling, dataloader state round-trip with the
  dataset-swap guard, and setup resume wiring (get_resume_paths
  forwarded to the trainer factory, training_info.json loaded).
- replay-buffer persistence is asserted against
  sampler.supports_buffer_checkpoint (windowed saves/restores with one
  capacity permit per group; gated samplers skip both sides).
- new async-save coverage: the tmp->step rename stays deferred until
  finalize_async_save completes and is flushed by shutdown; a failed
  background finalization re-raises at the next save; _save_checkpoint
  records val_metrics into val_reward and a val:* metric_name.

32 tests, in-process actor with fakes (ray.cluster_resources patched);
108 passed together with the existing single_controller suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: haitian-nvidia <haitianj@nvidia.com>
On exit, run() now propagates a failed checkpoint finalization only on
the clean path; when an exception is already propagating the flush is
best-effort (warning), so the original training failure stays the
raised exception — matching async_grpo_train's guarded cleanup
shutdown. logger.finish() moves into its own finally so it runs either
way.

Also drop the stale "SC does not support checkpointing yet." comment
from the SC exemplar config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: haitian-nvidia <haitianj@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Track Mooncake 1D promotion in durable TQ metadata so genuine singleton token columns retain their rank. Remove the obsolete pre-0.1.9 storage import fallback and add focused regression coverage.

Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Densify uniform nested reads for the simple backend without applying Mooncake's singleton squeeze. Make wire-field typing explicit, test invalid shape provenance, and warn when either TQ actor runtime patch is unavailable.

Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
TransferQueue v0.1.9 dropped its numpy<2.0.0 pin, so the override's
original justification no longer applies. The constraint it actually
bypasses now is tensorrt-llm's numpy>=2.0.0,<2.4.

Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
@macandro96
macandro96 requested review from a team as code owners August 3, 2026 21:30
@copy-pr-bot

copy-pr-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@macandro96
macandro96 marked this pull request as draft August 3, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants