Skip to content

feat(checkpoint): recover multimodal generation prefixes - #4583

Open
macandro96 wants to merge 10 commits into
amahishi/ckpt-v2-prefix-recoveryfrom
amahishi/prefix-recovery-multimodal
Open

macandro96 wants to merge 10 commits into
amahishi/ckpt-v2-prefix-recoveryfrom
amahishi/prefix-recovery-multimodal

Conversation

@macandro96

@macandro96 macandro96 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do ?

Adds multimodal support to vLLM prefix-level rollout recovery. A checkpoint can now preserve an in-flight image or video generation prefix, record a SHA-256 digest of each of its media tensors, verify the reprocessed request against those digests, and continue decoding. Prefix rows never store pixels; the canonical terminal row stages them once, as before.

This is a stacked PR on top of #4507; the reviewable change is the seven commits on amahishi/prefix-recovery-multimodal. The stack is ordered correctness first, optimization last: #4507 → this PR → #4612 (router replay across prefixes) → #4508 (batched prefix TQ I/O).

Design

  • Fingerprints each media tensor once, when the call is admitted (off the event loop): SHA-256 over its name, dtype, shape, and bytes.
  • Stores media_spans and those tensor digests in the digest-covered extras of the first durable prefix row for a model call, with no pixel attachments, so a checkpoint cut writes no media under its deadline.
  • Keeps later prefix chunks token-only and references the first row through the existing ordered staging-key chain.
  • On recovery, reads the prefix rows (spans and digests) without any tensor read, reprocesses the reissued multimodal request, and verifies its media items and tensor digests (verify_restored_media, which names the mismatch: items, tensor set, dtype/shape, or bytes).
  • Stages the reprocessed tensors, now proven byte-identical, in the canonical terminal row.
  • Preserves the existing buffer-swap, deadline, and terminal-completion race behavior from the parent stack.
  • Fails closed when media metadata/tensors are missing or changed. A cut before media preprocessing has completed is reported as durable_failure, so that model call is redispatched instead of restoring an incomplete prefix.

Checkpoint and recovery flow

flowchart TD
    A["Gym / agent sends request plus original media reference"] --> B["vLLM resolves and preprocesses media"]
    B --> C["Decode while token capture buffers token deltas"]
    C --> D{"Checkpoint cuts this active model call?"}
    D -- "No" --> E["Stage canonical terminal row with tokens and media"]
    D -- "Yes" --> F["Atomically swap the active token buffer"]
    F --> G{"First durable prefix row for this call?"}
    G -- "Yes" --> H["Stage tokens, media_spans, and media tensor digests in TQ"]
    G -- "No" --> I["Stage a token-only prefix chunk in TQ"]
    H --> J["Publish the coordinated TQ plus Gym / SC checkpoint"]
    I --> J
    J --> K["Process restarts and loads the checkpoint"]
    K --> L["Gym reissues the call with original media and the staging-key chain"]
    L --> M["Generation worker fetches the prefix rows (tokens, spans, digests) from TQ"]
    M --> N["vLLM reprocesses the original media"]
    N --> O{"Prompt tokens, media metadata, and tensor digests match?"}
    O -- "No" --> P["Fail closed; redispatch the model call from a safe boundary"]
    O -- "Yes" --> Q["Prefill the durable token prefix with verified media"]
    Q --> R["Decode the remaining suffix"]
    R --> S["Stage the canonical terminal row with the reprocessed media tensors"]
    S --> T["Clear obsolete prefix keys only after terminal staging succeeds"]
Loading

Constraints and assumptions

Constraints

  • Prefix-level recovery remains vLLM-only. Megatron inference is not enabled by this PR.
  • Router replay with prefix-level recovery remains unsupported and retains its existing setup guard.
  • The original image or video must still be resolvable when Gym reissues the request. Prefix rows hold only digests of the packed tensors, so this PR does not provide source-free media prefill.
  • This does not checkpoint vLLM's KV cache or media-processor cache. Recovery reprocesses the media and prefills the durable token prefix.
  • A cut that arrives before media preprocessing and capture setup complete cannot save a media prefix. It returns durable_failure, and the call is redispatched from the preceding safe boundary.
  • Only the first durable prefix row for a model call may carry media metadata and tensor digests. Later rows are token-only; missing, malformed, or conflicting media metadata fails closed.
  • Missing or corrupt prefix-row metadata, changed media, or any metadata or digest mismatch rejects prefix recovery (retained_media_changed) rather than continuing with uncertain state.
  • The configured data-plane checkpoint backend must restore tensor payload rows, not controller metadata alone.

Assumptions

  • Restore uses a compatible model, tokenizer, renderer, media processor, and capture schema; the parent checkpoint stack enforces the broader configuration identity.
  • Reprocessing the same source media is deterministic enough to reproduce the exact captured metadata and tensor bytes. The implementation intentionally requires byte equality, checked by SHA-256 digest.
  • The ordered staging-key chain is preserved, and its first media-bearing row remains live until a canonical terminal row has been staged successfully.
  • The parent stack publishes Gym state, SC recovery metadata, and the corresponding TQ snapshot as one coordinated recovery point.
  • Gym/agent/environment state recovery is provided by the parent Gym v2 checkpoint protocol. This PR only adds multimodal generation-prefix payload handling.

Issues

None.

Usage

The existing prefix-recovery configuration now also accepts supported vLLM multimodal workloads:

token_capture:
  enabled: true

rollout_recovery:
  target_level: prefix

The generation backend must be vLLM and media capture must be supported by the configured VLM processor.

Testing

Added or extended unit coverage for:

  • digest-only first prefix rows and later token-only chunks, for one and two cuts;
  • the image and video (num_frames) cut/restore round trip through a real TQ prefix row, with no tensor read on restore;
  • the prefix digest written at the cut matching the one restore rebuilds (text, image with one cut, image with two cuts);
  • each restore rejection: missing, malformed, or mismatched spans and digests; item, tensor-set, dtype/shape, and byte mismatches; extras after the first chunk; a reprocessed prompt that differs from the durable prefix;
  • terminal completion racing with an active prefix cut, terminal staging failure, and obsolete-prefix cleanup failure;
  • checkpointing before media preprocessing completes;
  • a recovery request whose source media disappeared failing before any TQ read or begin_call;
  • setup accepting multimodal prefix recovery.

The shared prefix-recovery parity checks now find a restored call by counting back from its row's end, so positions the trainer masked inside the call (top-k/top-p -inf, #4154) no longer break them; the Workplace parity test uses the same fix.

Unit tests pass locally on Linux at the head commit:

  • --nemo-gym-only (test_captured_media.py, test_vllm_token_capture_hosting.py): 149 passed;
  • tests/unit/environments/test_gym_prefix_recovery_parity.py: 26 passed;
  • tests/unit/single_controller/test_setup.py: 173 passed.

Functional: tests/functional/nemotron_omni_clevr_vllm_prefix_recovery_single_controller_1n2g.sh cuts active image generations at a checkpoint, hard-kills, restores, and requires each restored call to verify its image, finish once, and train on its exact restored prefix. It is registered in L1_Functional_Tests_GB200_Vllm_Omni_Single_Controller.sh (full mode). It needs Gym's string_match to declare checkpoint_mode = "stateless" (NVIDIA-NeMo/Gym#3887), which the stack's Gym pin includes. It passed locally on H100 with 3 GPUs, TP2 training, and gpu_memory_utilization=0.9 passed as overrides (4 cuts across both image groups restored and trained, media_row_rate 1). The committed defaults (2 GPUs, TP1, 0.6) match the sibling Omni token-capture L1 test and have not yet run on GB200.

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

@macandro96 macandro96 added the CI:L1 Run doctests, unit tests, and functional tests label Oct 8, 2026
@copy-pr-bot

copy-pr-bot Bot commented Oct 8, 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.

@github-actions github-actions Bot added the Documentation Improvements or additions to documentation label Oct 8, 2026
@macandro96
macandro96 requested a review from terrykong October 8, 2026 23:42
@macandro96
macandro96 force-pushed the amahishi/prefix-recovery-multimodal branch from 6cd7225 to 01ef7df Compare October 12, 2026 18:30
@macandro96
macandro96 changed the base branch from amahishi/ckpt-v2-prefix-batched-io to amahishi/ckpt-v2-prefix-recovery October 12, 2026 18:30
@github-actions

Copy link
Copy Markdown

✅ Submodule Fast-Forward Check Results

Check based on commit: 01ef7df (PR #4583 from amahishi/prefix-recovery-multimodal)

✅ Submodules that are properly updated:

Megatron-Bridge: ✅ PR branch is ahead of amahishi/ckpt-v2-prefix-batched-io branch (fast-forward)

All submodule changes look good! ✨

1 similar comment
@github-actions

Copy link
Copy Markdown

✅ Submodule Fast-Forward Check Results

Check based on commit: 01ef7df (PR #4583 from amahishi/prefix-recovery-multimodal)

✅ Submodules that are properly updated:

Megatron-Bridge: ✅ PR branch is ahead of amahishi/ckpt-v2-prefix-batched-io branch (fast-forward)

All submodule changes look good! ✨

@macandro96
macandro96 force-pushed the amahishi/prefix-recovery-multimodal branch from 01ef7df to 0235e32 Compare October 12, 2026 18:38
@macandro96
macandro96 added this pull request to stack #4685 October 12, 2026 23:16
@macandro96
macandro96 removed this pull request from stack #4685 October 12, 2026 23:30
@macandro96
macandro96 added this pull request to stack #4686 October 12, 2026 23:32
@macandro96
macandro96 force-pushed the amahishi/prefix-recovery-multimodal branch from 0235e32 to 0bf5789 Compare October 15, 2026 02:17
@github-actions

Copy link
Copy Markdown

✅ Submodule Fast-Forward Check Results

Check based on commit: 0bf5789 (PR #4583 from amahishi/prefix-recovery-multimodal)

✅ Submodules that are properly updated:

Gym: ✅ PR branch is ahead of main branch (fast-forward)

All submodule changes look good! ✨

@terrykong terrykong left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR extends vLLM prefix-level rollout recovery to multimodal (Omni image/video) calls. At a checkpoint cut, the first prefix row of a model call stores its media_spans and packed media tensors in TQ. On restore, the worker reprocesses the original media, checks it against what was saved, and continues decoding. The PR also removes the setup guards that blocked multimodal prefix recovery.

Thanks for this. Good parts:

  • MEDIA_SPANS_FIELD is imported from Gym where it is read, with no copied string.
  • fetch_generation_cut is built on the existing fetch_for_finalization and fetch_media helpers instead of new TQ reads.
  • The fail-closed design is the right call, and the "Constraints and assumptions" section in the description makes the limits easy to check.

Size. This PR's own source change is small (about 190 lines). The comment on return restored would cut about 55 of them, and the comment on the functional script would move about 500 test lines to the PR that can actually run them. The stack is already over 20k lines, so keeping each step tight matters. Later PRs in the stack (#4612, #4508) rewrite parts of this PR's new fetch and staging code, so landing the hash change and the getattr cleanup here first avoids carrying them up the stack.

CI. Only the lint and metadata checks have run on this head (0bf5789). No unit or functional CI has run yet.

Question for the author (no action required): does the Omni chat template render the same text on every call? A media restore now re-renders the first-turn prompt and fails closed if it differs, so a template that is not stable would turn every media restore into a redispatch.

Visual explainer: https://terrykong.github.io/gh-pages-poc/terryk/pr-4583-text-vs-media.html
Visual explainer: https://terrykong.github.io/gh-pages-poc/terryk/pr-4583-size-and-pixel-copy.html

Generated by Claude Code

Comment thread nemo_rl/data_plane/tq_token_sink.py Resolved
Comment thread tests/unit/data/test_captured_media.py Resolved
@macandro96
macandro96 force-pushed the amahishi/prefix-recovery-multimodal branch from 0bf5789 to 746b304 Compare October 18, 2026 22:01
@macandro96

Copy link
Copy Markdown
Contributor Author

/ok to test 746b304

@macandro96

Copy link
Copy Markdown
Contributor Author

/ok to test 8574280

@macandro96

Copy link
Copy Markdown
Contributor Author

/ok to test 13d42ba

@macandro96

Copy link
Copy Markdown
Contributor Author

/ok to test a4a4644

@macandro96
macandro96 force-pushed the amahishi/prefix-recovery-multimodal branch from b006026 to e0f827b Compare October 22, 2026 17:24
@github-actions

Copy link
Copy Markdown

✅ Submodule Fast-Forward Check Results

Check based on commit: e0f827b (PR #4583 from amahishi/prefix-recovery-multimodal)

✅ Submodules that are properly updated:

Gym: ✅ PR branch is ahead of main branch (fast-forward)

All submodule changes look good! ✨

@macandro96

Copy link
Copy Markdown
Contributor Author

/ok to test e0f827b

@macandro96
macandro96 removed this pull request from stack #4819 November 2, 2026 22:19
@macandro96
macandro96 added this pull request to stack #5031 November 2, 2026 22:23
@macandro96
macandro96 removed this pull request from stack #5031 November 2, 2026 22:49
@macandro96
macandro96 added this pull request to stack #5032 November 2, 2026 22:50
@macandro96
macandro96 force-pushed the amahishi/prefix-recovery-multimodal branch from e0f827b to 19051ca Compare November 3, 2026 01:31
@macandro96

Copy link
Copy Markdown
Contributor Author

/ok to test 19051ca

@macandro96
macandro96 removed this pull request from stack #5032 November 3, 2026 23:39
macandro96 and others added 9 commits November 3, 2026 16:22
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Make the parity test exercise many restored prefixes and separate recovery
faults from run-to-run sampling noise:

- Workload: four Workplace prompts per step whose closing turn lists 1..N
  (N = 150/250/350/450) at natural length. Group lengths are far apart, so
  prompt-group readiness order is deterministic. One prompt expects a
  different duration so rewards are not constant. Runs Qwen3-1.7B for 3
  steps.
- Selection/verification: require a recoverable prefix in every Workplace
  prompt group at the crash (Gym refuses dispatches that land during a
  checkpoint, so siblings may start late), and verify each cut returns to
  its Gym owner under attempt+1, restores its exact prefix digest, and
  completes once with its trained generated-token count equal to the
  restored prefix plus the regenerated tail.
- Parity: classify token divergences relative to the restored prefix. A
  divergence inside the prefix is excused as phase-one noise only when the
  trained prefix tokens hash-match what the worker restored, and excused
  rows still require equal rewards and sample masks; a prompt divergence
  always fails. Gate vLLM generation_logprobs in aggregate over token-
  matched rows (bit-identical fraction, Spearman, max abs diff) since they
  are not batch-invariant, and require exact prompt-group readiness order.
  Report Kendall tau for arrival and sealing order, per-rollout timelines,
  and logprob statistics in parity-report.json before the strict checks
  run.
- Hook: timestamp every event and record completion arrival separately
  from the ledger seal.
- Relies on the Gym checkpoint test agent's natural-length closing turn
  (NEMO_GYM_TEST_PREFIX_MIN_TOKENS=0), pinned by the base branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
…very parity

The prefix-recovery parity harness reads each restored cut from Gym's
checkpoint records and reports it. NeMo Gym now names the staged prefix
prefix_token_count and prefix_digest, so the harness, its report, and
its unit tests use those keys.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
… test

The prefix-recovery snapshot helper now imports
_gym_recovery_snapshot_helpers as a sibling module. Put tests/functional on
sys.path before loading it by file path, as running it as a script does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
Parity data and gates:
- Compare runs on the trainer's own async_rl.log_full_train_data dump
  (read from the run's exp_NNN directory) instead of a recorder in the
  hook; the hook now only probes live staging keys per train step.
- Require exact parity for every row. The one exception is a restored
  row whose phase-1 prefix already differed from the reference before the
  cut (vLLM sampling is not batch-invariant): it must start with exactly
  the restored tokens (prefix_ids_sha256), match up to its closing call,
  and keep its reward and admission; at least one restored row must match
  outright.
- Gate generation logprobs on bit-identical fraction and max |diff|.
- Default to one train step and say the test checks data, not learning
  (top_p=1e-6 zeroes the policy gradient).

Removed: the ordering/timeline/Kendall report, the hand-rolled
Spearman/Pearson gate and parity-report.json, the metric comparison, the
prefix+tail==total tautology, the duplicate trained-once check, the
unreachable successor Gym-manifest check, the single-cut selection
fallback, and the swallowed Workplace layout errors. The JSON readers and
the restored-prefix log parser are shared in _gym_recovery_snapshot_helpers.
The L1 runner comment now says the test performs three runs.
The Workplace calendar sentinel count lives with the shared readers, and the
Workplace test rows are built by one jq module (_workplace_calendar_rows.jq).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
…agent

Gym main has no checkpoint test agent, so the prefix-recovery parity test
now runs the stock workplace_assistant_simple_agent behind the test hook's
policy proxy, as the turn-recovery test does. With no hold and no
follow-up tool, the proxy sends every call after the tool result with
tool_choice none and leaves its length to the model, which is what the
test agent did for the closing turn.

Gym only cuts a call whose decoding is unconstrained, judged from the
request its agent sends, so the rows leave tool_choice auto instead of
forcing calendar_create_event: a forced tool would keep every turn out of
the cut. The prompt asks for the call, and the snapshot selector already
requires the calendar write to have happened exactly once.

The rows name workplace_assistant_simple_agent, the script drops the test
agent's config_paths override and environment-server removal, and the
hook writes the proxy's events to the prefix test's events file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
…y active call

The snapshot records only the cuts that succeeded, so a snapshot where Gym
failed or skipped an active call's cut still qualified, and that call
quietly regenerated after the restore. Read the trainer's save line for the
snapshot and require zero failed and skipped cuts; a bug that keeps failing
cuts now times out the selection with the counts instead of passing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
…identical

On Gym main's Workplace agent the workload generates ~9.3k tokens, and four
runs scored 98.89-99.13% bit-identical with every exact token, mask and
reward gate passing; three fell below the old 99% floor across three
different progress-read implementations. The bf16 noise floor is a property
of the smaller workload, not of the restore, so the gate now sits at 98%.
The max-|diff| bound stays at 0.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
…s end

The prefix-recovery parity checks located a restored call at the start of
its row's last unmasked segment. When top-k/top-p filtering is on, the
trainer drops positions whose recomputed logprob is -inf from token_mask
(#4154), and the rows these checks read are dumped after that pass, so a
masked token inside the call made the check start the call too late,
miscount its tokens, and fail a correct restore.

A restored call closes its row, so the total_generation_tokens it logs on
completion end the row. _assert_prefix_boundary now counts back from the
row's end, requires the token before the call to be untrained, checks the
completion's prefix + tail = total accounting, and requires the restored
prefix at that position; a dropped or duplicated token moves the position.
It returns where each call starts, which _rows_diverged_before_cut reuses
to compare the reference row at the same position.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
@macandro96
macandro96 added this pull request to stack #5057 November 4, 2026 19:47
Lift the setup refusal of generation-prefix recovery with media capture.

A media call's first durable prefix row carries its media spans and a
SHA-256 digest of each packed media tensor in its extras, not the
pixels; later chunks carry no media. The restored prefix digest covers
those extras. On restore the worker reads the items and digests from
that row, reprocesses the reissued request's media, checks it against
them (retained_media_changed on a mismatch), and stages the reprocessed
tensors in the canonical terminal row. A row whose media metadata
cannot be read makes the call regenerate from its turn boundary.

Adds an Omni CLEVR multimodal prefix-recovery functional test, unit
coverage of media cuts, races and restores, and shares the prefix
recovery functional phases (_gym_prefix_recovery_phases.sh).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anish Mahishi <amahishi@nvidia.com>
@macandro96
macandro96 force-pushed the amahishi/prefix-recovery-multimodal branch from 19051ca to 3821aa8 Compare November 4, 2026 20:02
@terrykong terrykong added CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version) and removed CI:L1 Run doctests, unit tests, and functional tests labels Nov 5, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version) Documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants