Repository navigation
feat(checkpoint): recover multimodal generation prefixes - #4583
macandro96 wants to merge 10 commits into
Conversation
6cd7225 to
01ef7df
Compare
1 similar comment
01ef7df to
0235e32
Compare
0235e32 to
0bf5789
Compare
terrykong
left a comment
There was a problem hiding this comment.
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_FIELDis imported from Gym where it is read, with no copied string.fetch_generation_cutis built on the existingfetch_for_finalizationandfetch_mediahelpers 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
0bf5789 to
746b304
Compare
|
/ok to test 746b304 |
|
/ok to test 8574280 |
|
/ok to test 13d42ba |
|
/ok to test a4a4644 |
b006026 to
e0f827b
Compare
|
/ok to test e0f827b |
e0f827b to
19051ca
Compare
|
/ok to test 19051ca |
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>
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>
19051ca to
3821aa8
Compare
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
media_spansand 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.verify_restored_media, which names the mismatch: items, tensor set, dtype/shape, or bytes).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"]Constraints and assumptions
Constraints
durable_failure, and the call is redispatched from the preceding safe boundary.retained_media_changed) rather than continuing with uncertain state.Assumptions
Issues
None.
Usage
The existing prefix-recovery configuration now also accepts supported vLLM multimodal workloads:
The generation backend must be vLLM and media capture must be supported by the configured VLM processor.
Testing
Added or extended unit coverage for:
num_frames) cut/restore round trip through a real TQ prefix row, with no tensor read on restore;begin_call;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.shcuts 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 inL1_Functional_Tests_GB200_Vllm_Omni_Single_Controller.sh(full mode). It needs Gym'sstring_matchto declarecheckpoint_mode = "stateless"(NVIDIA-NeMo/Gym#3887), which the stack's Gym pin includes. It passed locally on H100 with 3 GPUs, TP2 training, andgpu_memory_utilization=0.9passed as overrides (4 cuts across both image groups restored and trained,media_row_rate1). 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:
Additional Information
13d42ba2eaf118982aa6364e45482e729f44ee51