fix(vlm): match training-side image tiling to the rollout's placeholder runs - #3940
Conversation
…er runs Async GRPO VLM training crashed mid-run with "Expanded-sequence media alignment failed: found 32160 valid placeholders for 40800 projected features". The generation engine sizes image tiles per request (shrinking them as prompts approach max_model_len); the training-side attach path re-processed the original images under the processor's static config budget, so budget-bound rows (e.g. 16 frames at 2400x1080 in a near-32k prompt) produced more projected vision features than the rollout's placeholder tokens, and the model forward raised on every rank. Derive the truth from the rollout itself instead of mirroring the engine's budget arithmetic: - multimodal_utils: parse per-image <img><image>*N</img> placeholder runs out of the rollout token ids; verify the processor's native output against them and, on mismatch, re-process each image pinned to the exact run length (budget pin via the image processor's max_model_len clamp, with a deterministic exact-grid resize fallback). Raise rather than attach misaligned media. - nemo_gym actor: pass each turn's placeholder runs into the attach; with deduplicate_multimodal_data on, keep the omission only when the statically-budgeted pre-attached tensors provably match the rollout's first-turn runs (predicted via the processor's own grid math), else attach rollout-matched tensors actor-side. - rollouts: the driver-side static reattach never overwrites media the actor already attached; video rows are unaffected (their tensors attach at datum time and their turns carry no extracted images). Verified offline against the checkpoint processor (exact-count repair across a 7-size x 7-budget sweep; the crash row now yields exactly matching feature counts; non-binding rows byte-match the previous fast path) and in production: the failing run resumed through the previously fatal batch and trained to completion with no alignment errors. Signed-off-by: Pulkit Kumar <pulkitk@nvidia.com>
|
Curious, why not just increase the context length temporarily? The mismatch should be properly fixed though. |
|
It seems the repair infers the learner-side resize from the number of image placeholder tokens. The generic multimodal layer should only transport and validate this metadata. Nemotron placeholder parsing, grid calculations, and replay should live in the existing Nemotron helper or behind a |
| target[key] = value | ||
| if not (isinstance(value, PackedTensor) or key in NATIVE_MULTIMODAL_KEYS): | ||
| continue | ||
| if key in target: |
There was a problem hiding this comment.
[P1] Do not treat key presence as proof that Gym media is authoritative
A pre-existing target value can be a placeholder or stale payload, not rollout-matched media. test_reattach_original_multimodal_payloads_is_media_only_and_turn_aligned supplies "remote placeholder" under pixel_values and expects the original PackedTensor to replace it; this branch preserves the string, so the unchanged base test fails.
Please use explicit provenance or the omission marker to identify media produced from the rollout decision, and retain the existing reattachment behavior otherwise.
…s relocated - Replace the key-presence reattach guard with an explicit provenance marker (ROLLOUT_MATCHED_MEDIA_KEY): the attach sets it only on turns it actually REPAIRED to the rollout's placeholder runs, and the driver-side static reattach skips (and consumes) exactly those turns. Unmarked values — including placeholder or stale payloads — are replaced as before, restoring the documented behavior of test_reattach_original_multimodal_payloads_is_media_only_and_turn_aligned, and unrepaired turns keep the shared-tensor restore across a prompt group's repeated rows. - Move the Nemotron-specific parity logic (placeholder-run parsing, static budget prediction, exact-count re-processing and grid math) out of multimodal_utils into the existing Nemotron helper module (nemo_rl/environments/nemotron_utils.py). The generic attach keeps only the verification contract and delegates the repair via a local import; processors without the placeholder grammar degrade gracefully as before. - Add a regression test: a marked turn keeps its rollout-matched tensors and the marker is consumed, while unmarked representations are still restored from the static source. Signed-off-by: Pulkit Kumar <pulkitk@nvidia.com>
77258a5 to
59d0db8
Compare
|
Thanks for the detailed review. All points are addressed in 59d0db8. Summary below. 1. Reattach guard: key presence is not provenance (inline comment)You are right, the previous guard treated any pre-existing value under a media key as authoritative, which broke the documented behavior of This is now done with an explicit provenance marker instead:
2. Code placement: Nemotron specifics out of multimodal_utilsAgreed. The placeholder-run parsing, the static budget prediction, the exact-count re-processing and the grid math now live in the existing Nemotron helper module, 3. Why not just increase the context lengthRaising 4. Having vLLM return its exact preprocessing decisionsAgreed on the direction, and this change is compatible with it. The placeholder runs in the returned prompt token ids already are the engine's exact decision, one token per projected feature, so we Validation after the changes
|
What does this PR do?
Fixes a train/generation image-tiling mismatch that crashes async GRPO VLM training mid-run:
The bug
A VLM sample needs the number of vision features in the training tensors to equal the number of image-placeholder tokens in the sequence — the model forward merges features into placeholder positions one-to-one.
Those two counts are computed by different components:
max_model_len. The rollout's token ids reflect that choice.On most rows both sides agree, so the bug is invisible. On budget-bound rows (large frames + long text — e.g. 16 frames at 2400×1080 in a near-32k prompt) the engine shrinks tiles (2010 tokens/image) while the training side produces its static count (2550/image) → 40800 features vs 32160 placeholders → hard crash on every rank, at whatever step first samples such a row.
The fix
Use the rollout itself as ground truth instead of trying to mirror the engine's budget arithmetic (which lives in engine internals and can drift):
multimodal_utils— parse per-image<img><image>*N</img>placeholder runs out of the rollout token ids; the run lengths are exactly the feature counts the model will demand. Verify the processor's native output against them; on mismatch, re-process each image pinned to the exact count (the image processor'smax_model_lenclamp is the budget lever, with a deterministic exact-grid resize fallback). If parity cannot be established, raise a clear error instead of attaching misaligned media.nemo_gymactor — pass each turn's placeholder runs into the attach. Withdeduplicate_multimodal_data: true, keep the media omission only when the statically-budgeted pre-attached tensors provably match the rollout's first-turn runs (predicted via the processor's own grid math); otherwise attach rollout-matched tensors actor-side.rollouts— the driver-side static reattach never overwrites media the actor already attached. Video rows are unaffected: their tensors attach at datum time and their turns carry no extracted images, so the parity logic never engages for them.Non-binding rows (the common case) take a fast path — the batched processor call is verified against the runs and used as-is, so behavior and cost are unchanged.
Validation
Notes for reviewers
<img><image>*N</img>) and thenum_tokensoutput key follow the NemotronH Omni processor family; processors that don't expose these degrade gracefully (parity checks are skipped, current behavior preserved).