Repository navigation
[megatron][vlm] Context parallelism for Qwen3-VL / Qwen3.5-VL - #2404
Conversation
Megatron-Bridge's Qwen3-VL model rebuilds 3D mRoPE positions per packed sub-sequence when it receives a THD stream with position_ids=None, so the existing packed path works for VLMs. Context parallelism stays blocked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KuNZVD7sZm2e3tySMqTsCG Signed-off-by: xgui <xgui@anyscale.com>
…dDeltaNet layers The double-packing guard from NovaSky-AI#1769 matched every Qwen3VLModel, so it also blocked dense Qwen3-VL, which takes SkyRL's [1, T] THD stream without re-packing. Found by the 4xH100 A/B run (worker init ValueError). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KuNZVD7sZm2e3tySMqTsCG Signed-off-by: xgui <xgui@anyscale.com>
Megatron-Bridge#4532 (in the pinned 8e7077c6) makes Qwen3VLModel treat a caller-packed [1, T] THD stream as authoritative: no second compaction, and the caller's packed_seq_params reach every layer unchanged. The guard from NovaSky-AI#1769 (model_packs_sequences_internally) guarded against the pre-#4532 behaviour, so drop it, its stale GPU-test case, and comments that said the VL path cannot pack. language_model_only stays: it skips the vision tower for text-only Qwen3.5 training. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KuNZVD7sZm2e3tySMqTsCG Signed-off-by: xgui <xgui@anyscale.com>
…n3-VL and Qwen3.5-VL Variable-length image samples plus one text-only row, microbatches of 4 and a trailing single-sample microbatch, TP=1 and TP=2+SP. Compares every scored token and splits the stats by slot in the packed microbatch, so a sample-boundary leak (mRoPE restart, or GatedDeltaNet state/conv carried over) shows up as later slots being worse than slot 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
…e model apply the CP split For VLMs, preprocess_packed_seqs(shard_for_cp=False) keeps the full packed stream on every CP rank (sub-sequences still padded to 2*CP alignment) and Megatron-Bridge's Qwen3VLModel computes mRoPE, places image features and deepstack, and applies the CP partition itself (Megatron-Bridge#4532). Text models keep SkyRL's pre-split, which GPTModel requires. The bridge's Qwen VL providers force calculate_per_token_loss under CP and the model asserts it at construction; SkyRL's loss function returns no token count and uses the default scaling, so a pre-DDP-wrap hook clears the flag again. VLM+CP+MTP raises (TODO). New GPU test compares CP=2 with CP=1 on per-token logprobs and grad norm for Qwen3-VL-2B and Qwen3.5-0.8B. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
There was a problem hiding this comment.
Code Review
This pull request enables and tests context parallelism (CP) and sample packing for Vision-Language Models (VLMs) such as Qwen3-VL. It introduces a shard_for_cp option in sequence preprocessing to keep the full packed stream on each CP rank, and adds a keep_skyrl_loss_scaling pre-wrap hook to preserve default loss scaling under CP. New tests are added to verify packed-vs-unpacked parity and CP-vs-no-CP parity. Feedback on the changes suggests making the keep_skyrl_loss_scaling hook more robust by handling both single nn.Module and iterable inputs to prevent potential runtime TypeErrors.
…okens only The test read logprobs through the forward(loss_fn=cross_entropy) path, which returns each sample's values compacted to its loss_mask and left-aligned, but indexed them as right-aligned, so it compared the wrong positions (and zeros). Use the inference forward path (the RL old/ref-logprob path, right-aligned [B, response_length]) and score only each row's assistant answer, as RL does: image-placeholder targets of random-noise images have huge, bf16-sensitive logprobs that are never trained on. Found by the 4xH100 run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
…swer-token mask Same alignment fix as the packed-vs-unpacked test: read right-aligned [B, response_length] logprobs from the inference forward and score the batch's loss_mask (assistant answer tokens). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
…alone Packed and unpacked microbatches agree only to bf16 rounding, and unpacked microbatches move by the same amount when only their composition changes (measured on 4xH100: 0.034 mean on answer tokens at TP2+SP for both). A fixed packed-vs-unpacked bar therefore tests batching noise. Use each sample run alone (unpacked, micro=1) as the reference and require packed microbatches to be no further from it than unpacked ones; keep the slot-based leak check and a loose absolute guard. At TP>1 the text-only row is left out of the reference: alone it hits a Megatron-Bridge split_deepstack_embs crash on image-free microbatches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
…egatron-cp Signed-off-by: xgui <xgui@anyscale.com> # Conflicts: # tests/backends/skyrl_train/gpu/gpu_ci/megatron/test_megatron_vlm_init.py
…ing it under CP Megatron-Bridge's Qwen-VL providers force calculate_per_token_loss when CP > 1. Instead of switching it back off with a pre-DDP-wrap hook, make the policy loss work in that mode, as verl/AReaL/NeMo-RL do: - megatron_loss_output (new, loss_scaling.py) returns (loss, metrics) in the default mode as before, and (loss * global_tokens, local_tokens, metrics) in per-token mode, so finalize_model_grads' division by the global token count cancels and SkyRL's pre-scaled reductions keep their meaning. Regularizers (KL, entropy, MTP) keep their average over DP ranks and real microbatches. - The worker attaches per-microbatch token counts (padded packed tokens per CP rank, the quantity the MoE router assumes) and their DP x CP all-reduced sum. - DDP's average_in_collective is turned off when the flag is on (Megatron asserts it). - Accumulating several forward_backward calls per optim_step raises in this mode (TODO). - CPU test simulates Megatron's schedule/DDP/finalize factors for both modes over DP/CP/microbatch layouts (incl. padding microbatches) against the exact gradient. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
…or logprob bar The 4xH100 run passed the grad-norm check (CP2 vs CP1 +0.15% / -1.8%) and missed the 3e-2 logprob mean bar by the bf16 batch-composition floor (0.034 / 0.036, max 0.30 / 0.28). Run CP=1 on one GPU so both arms use DP=1 and the same microbatches, and bound the logprob diff by the bf16 floor (mean < 5e-2, max < 1); a CP split/layout bug gives O(1) differences. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
…kip the token-count division Follow NovaSky-AI#1296: the loss is normalized through the pre-scaled advantages, so the intended gradient is a plain sum. In Megatron's calculate_per_token_loss mode (forced by Megatron-Bridge Qwen-VL providers under CP) the schedule leaves a 3-tuple loss unscaled and DDP sums over DP x CP, which is already that sum; only finalize_model_grads' division by the token count has to go, and SkyRL owns that call. - megatron_loss_output: per-token mode returns (loss + regularizer/(dp*n_real), num_tokens). - The deferred grad sync never passes a token count to finalize_model_grads. - Drops the x global_tokens scaling, the per-microbatch token-count all-reduce and the one-forward_backward-per-optim_step restriction (accumulation is a plain sum again). - MoE aux / z losses raise in per-token mode (the router pre-multiplies them by the token count and relies on the division); SkyRL's moe_aux_loss_coeff defaults to 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
… helper Keep the change visible in loss_func itself: grad_sum_correction_factor and kl_entropy_microbatch_scale get a calculate_per_token_loss branch (1 and 1/(dp * num_real_microbatches)), and both return sites return a 3-tuple in that mode. Removes loss_scaling.megatron_loss_output and its CPU simulation test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
… count (verl-style) Instead of running finalize_model_grads without a token count, keep Megatron's per-token contract as upstream intends and cancel its division the same way the default path cancels the schedule/DDP divisions: - The worker attaches each microbatch's token count (padded packed tokens per CP rank) and their DP x CP all-reduced sum. - loss_func multiplies by the global count (grad_sum_correction_factor = N_global, kl_entropy_microbatch_scale = N_global / (dp * num_real_microbatches)) and returns the local count; finalize_model_grads divides by the summed count, as in verl. - The deferred grad sync is back to upstream form; accumulating several forward_backward calls per optim_step raises in this mode (TODO). - MoE aux / z losses work again (finalize's division cancels the router's token-count pre-multiplication); the guard is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
Pixel values reach only the first pipeline stage while every stage rebuilds the packed mRoPE positions; cover Qwen3-VL-2B and Qwen3.5-0.8B at PP=2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
TP2 x CP2 vs TP2 x CP1 for Qwen3-VL-2B and Qwen3.5-0.8B: the deepstack/SP split runs on top of the CP partition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
…gration branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
Sample packing for VLMs relies on the model rebuilding 3D mRoPE positions from the packed stream. Allow it only when every model chunk is Megatron-Bridge's Qwen3VLModel (Qwen3-VL, Qwen3.5-VL) or sets model_owns_packing=True (NeMo-RL's opt-in attribute), and raise a clear error for other VLMs instead of running an untested path. Unpacked VLM training is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com> # Conflicts: # skyrl/backends/skyrl_train/workers/megatron/megatron_model_wrapper.py
…it it themselves shard_for_cp=False hands every CP rank the whole packed stream and relies on the model to compute mRoPE, place image features and apply the CP split (Megatron-Bridge Qwen3VLModel). Key it on model_owns_vlm_packing instead of is_vlm, so any other VLM keeps SkyRL's pre-split; CP for those VLMs is already rejected by the packing gate, since CP requires sample packing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
There was a problem hiding this comment.
Code Review
This pull request enables context parallelism (CP) and sample packing support for Vision-Language Models (VLMs) like Qwen3-VL in the Megatron backend. Key changes include adding a shard_for_cp option to sequence preprocessing to allow models to handle CP splits internally, implementing per-token loss normalization when calculate_per_token_loss is active, and tracking global/local token counts across DP and CP ranks. Additionally, the PR adds comprehensive tests to verify packed-vs-unpacked parity and CP-vs-no-CP parity for VLMs. I have no feedback to provide as there are no review comments to evaluate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
… backward runs The check lived in the deferred finalize hook, which runs after the backward, so a refused call had already added its gradients to the window. Check at the start of the worker's forward_backward instead, on every rank. The VLM CP test now makes a second call and checks that the grad norm is unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
…per_token_loss Megatron divides the window's gradients once by the summed token count, which averages calls instead of adding them. Apply the division to each call's gradients instead: scale the grad buffers by the call's global token count before its backward and by the inverse after, and skip the division in the deferred finalize. Replaces the one-call-per-optim_step error. The VLM CP test now checks two half-batch calls per step against CP=1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
A MagicMock attribute is truthy, so the new overlap_grad_reduce guard raised. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
…tween layouts With fixed old logprobs of -1, tokens near the clip boundary were clipped in one layout and not the other because of bf16-level logprob differences. That moved Qwen3.5 TP2 grad norms by 7%. Use each layout's own forward logprobs as old/rollout logprobs (importance ratio 1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
… fixed weights Run with lr=0 so both phases see the same weights, and require two half-batch calls per step to match the full-batch grad norm within 2% in each layout, instead of comparing the post-step two-call norms across layouts within 10%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 Signed-off-by: xgui <xgui@anyscale.com>
… like for like The KL term is a mean over each forward_backward call's microbatches, so splitting the batch into two calls weights it twice. Its share of the gradient here is tiny, so the 2% band held, but the check should only cover what it claims: that the policy loss adds up across calls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018mzGtrYDreLT6TYubt7Lsj
…lining, uv run - run_swebench_multimodal.sh: prepares into tasks.partial and records the image directory, so a partial prepare or one for another IMAGE_DIR is rebuilt; MEGATRON_EP defaults to 1 (dense 9B on 4 GPUs); the header says megatron packing and CP need NovaSky-AI#2357 / NovaSky-AI#2404; no bare python. - build_snapshots.py: reuses only an active snapshot, rebuilds error / build_failed, and reports one still building instead of calling it ready. - prepare_tasks.py: replaces every form of an image's URL, and a bare URL only whole; a failed save skips the image instead of ending the prepare. - daytona.py: labels must be KEY=VALUE. mini-swe-agent's yaml says yolo, as Harbor runs it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018mzGtrYDreLT6TYubt7Lsj
…Sky-AI#2357) The branch previously carried NovaSky-AI#2357 (VLM sequence packing) through repeated merges. This merge takes upstream/main and keeps only the context-parallelism changes on top of it, so the PR diff is CP-only. Until NovaSky-AI#2357 is in main, the CP code and test refer to names it adds (model_owns_vlm_packing, get_packing_parity_batch, PACKING_MICRO_BATCH, ray_init); merging main after NovaSky-AI#2357 lands resolves them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018mzGtrYDreLT6TYubt7Lsj
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Reviewed by Cursor Bugbot for commit 0c82a1e. Configure here.
…1) (#2357) Allow sequence packing (`trainer.remove_microbatch_padding=true`) for VLMs on Megatron with CP=1, including Qwen3.5-VL. 1. NVIDIA-NeMo/Megatron-Bridge#4532 (already in our pinned bridge, 8e7077c6) makes `Qwen3VLModel` accept an already-packed `[1, T]` THD batch as is: it rebuilds 3D mRoPE per packed sub-sequence and passes `packed_seq_params` to every layer, including Qwen3.5's GatedDeltaNet. So this PR removes the no-packing assert and the old double-packing guard (`model_packs_sequences_internally`, from #1769). Packing is allowed only for models that rebuild mRoPE themselves (`Qwen3VLModel`, or `model_owns_packing = True`); other VLMs raise a clear error. CP>1 stays blocked (#2404). 2. `fused_lm_head_logprob` now supports `Qwen3VLModel`, whose `output_processor` is taken by the inner language model (passed through the wrapper's `**kwargs`) rather than by the wrapper's own `forward`. Without it, long Qwen3.5-35B-A3B VL rows OOM on the full logits in the forward logprob pass. On GPU (Qwen3.5-35B-A3B, TP2/EP8, 8xH100), the fused pass ran 8 rows of 26.7k-65.5k tokens in 81.5 s without OOM (policy peak 48.6 GB allocated). Tests: new GPU test `test_megatron_vlm_packed_vs_unpacked` (Qwen3-VL-2B, Qwen3.5-0.8B; TP1, TP2+SP, PP2) and CPU unit tests. ### Packed vs unpacked numerics Packed and unpacked logprobs agree to bf16 rounding, not bit-for-bit. Checked on 4xH100 (Qwen3-VL-2B unless noted): - Every input to the language model (pixels, vision embeds, deepstack, 3D positions, merged embeddings) is bit-identical between the two modes; the first difference is the decoder output. - One sample per microbatch: packed == unpacked exactly (TP1 and TP2+SP). - Changing only the microbatch composition moves unpacked logprobs as much as packing does (TP2+SP, mean abs diff on answer tokens: unpacked 4-vs-1 0.034, packed-vs-unpacked 0.034); packed stays closer to each sample run alone (0.015). - Per-layer profile of the shifted microbatch: first difference at layer 0 below one bf16 ulp, then smooth growth (relative 1.9e-4 -> 2.2e-2 over 28 layers), no jump. - Against an HF fp32 forward of each sample, packed and unpacked are equally close (0.046 vs 0.041 mean on answer text), both inside HF's own bf16-vs-fp32 gap (0.050). - The test therefore compares packed microbatches against each sample run alone and requires them to be no further from it than unpacked microbatches. ### RL (geometry3k, 4xH100, TP=2, 10 steps, means over steps 3-10) Qwen3-VL-8B: | | unpacked | packed, same mb | packed, 12288 tok | packed, 16384 tok | |---|---|---|---|---| | step | 130.2s | 119.5s (-8%) | 105.8s (-19%) | 102.3s (-21%) | | policy_train | 63.5s | 52.8s (-17%) | 43.3s (-32%) | 40.9s (-36%) | | fwd logprobs | 19.2s | 18.5s | 14.0s (-28%) | 13.5s (-30%) | | rollout-vs-train logprob diff mean | 0.01356 | 0.01365 | 0.01382 | 0.01358 | Qwen3.5-9B (vision tower on, train micro batch 2): | | unpacked | packed, same mb | packed, 12288 tok | |---|---|---|---| | step | 169.2s | 171.9s | 134.4s (-20.5%) | | policy_train | 95.1s | 98.8s | 62.3s (-34.5%) | | fwd logprobs | 20.8s | 20.1s | 19.6s | | tokens/s/GPU | 2784 | 2718 | 4283 (+54%) | | rollout-vs-train logprob diff mean | 0.0072 | 0.0073 | 0.0073 | 16384 tokens OOMs on 4xH100 with TP=2 and colocated vLLM at 0.8 (fp32 logits over the 248k vocab in the log-prob backward; unpacked OOMs at train micro batch 4 the same way). ### E2E: Harbor SWE-bench Multimodal, long rows (A vs B) Qwen3.5-35B-A3B (vision tower trained), Megatron TP2/EP8, 8xH100, 64k context, `fused_lm_head_logprob=true`; one step of 8 mini-swe-agent rollouts on a SWE-bench Multimodal task (Harbor + skycap, #2450), rows of 27k-65k tokens. Different runs sample different rollouts, so compare distributions. | | A unpacked, CP1 | B packed, CP1 | C packed, CP2 | |---|---|---|---| | logprob diff mean / p99 / max | 0.0240 / 0.237 / 2.54 | 0.0257 / 0.239 / 2.01 | 0.0249 / 0.236 / 2.12 | | fwd logprobs | 81.5 s | 62.7 s | 61.0 s | | policy_train | 141 s | 132 s | 131 s | | policy peak mem (allocated) | 48.6 GB | 48.1 GB | 40.4 GB | | avg rollout tokens | 55.6k | 39.8k | 57.3k | At scale (C, packed + CP2, 88 rows of a pass@8 run, avg response 50.3k tokens, 1.76M loss-masked tokens): logprob diff mean 0.0233, p99 0.229, p99.9 0.447, 0.06% of tokens > 0.5, per-row means 0.013-0.031; fwd logprobs 212 s for the 88 rows. Packing doesn't move the trainer's logprobs relative to vLLM at 27k-65k-token rows. ### GPU tests (4xH100) - `test_megatron_vlm_packed_vs_unpacked`: passed for Qwen3-VL-2B and Qwen3.5-0.8B at TP1, TP2+SP and PP2. PP2 is bit-identical to PP1. Mean abs logprob diff on answer tokens at PP2: | | packed vs alone | unpacked vs alone | packed vs unpacked | |---|---|---|---| | Qwen3-VL-2B | 0.0349 | 0.0344 | 0.0099 | | Qwen3.5-0.8B | 0.0371 | 0.0337 | 0.0281 | - `test_model_owns_vlm_packing.py` (with megatron installed): 5 passed, none skipped. - RL with packing on Qwen3.5-9B geometry3k (40 steps, TP2, CP1, packed 12288, with #2344): eval pass@1 0.308 -> 0.820, rollout-vs-train logprob diff mean 0.0061, ~108 s/step ([W&B](https://wandb.ai/sky-posttraining-uc-berkeley/vlm-first3-curves/runs/uyctc1v5)). VLM SFT stays unpacked for now (TODO in `SFTTrainer.setup()`). Known upstream issue (not from this PR): at TP>1 a microbatch with no image at all crashes Megatron-Bridge's `split_deepstack_embs` (NVIDIA-NeMo/Megatron-Bridge#3869 has a fix, unmerged). Passing `pixel_values=None` for image-free microbatches avoids it; #2322 does that on the SkyRL side. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes Megatron VLM forward/training geometry (packed THD + mRoPE/GDN boundaries) and logprob fusion for VL wrappers; incorrect boundary handling would skew RL logprobs, though new parity GPU tests mitigate this. > > **Overview** > **Enables Megatron RL sample packing (`remove_microbatch_padding`) for Megatron-Bridge `Qwen3VLModel` (Qwen3-VL / Qwen3.5-VL with vision on)** now that the bridge accepts SkyRL’s pre-packed `[1, T]` THD stream and rebuilds 3D mRoPE per sub-sequence. The old “double-pack” guard is replaced by **`model_owns_vlm_packing`**: only all-`Qwen3VLModel` chunks may pack; other VLMs still error with a targeted message. Context parallelism for VLMs stays blocked. > > **`fused_lm_head_logprob`** is extended for VL wrappers that forward `output_processor` via `**kwargs` to the inner language model, avoiding full-vocab logits OOM on long multimodal logprob passes. > > Docs and comments are updated (e.g. `language_model_only` drops the vision tower rather than being framed as a packing workaround). **VLM SFT remains unpacked.** New CPU/GPU tests cover the packing gate, fused LM head VL paths, and packed-vs-unpacked logprob parity (including Qwen3.5-VL at TP2+PP2). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a1abd7f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Signed-off-by: xgui <xgui@anyscale.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Eric Tang <etang@anyscale.com>
NovaSky-AI#2357 (VLM sequence packing) is now in main, so the names the CP changes rely on (model_owns_vlm_packing, get_packing_parity_batch, PACKING_MICRO_BATCH, ray_init) resolve. - _assert_vlm_supported: main's packing check (NovaSky-AI#2357), with the CP block replacing its "VLM + context parallelism unsupported" assert, as in this PR before the split. - test_megatron_vlm_init.py: main's file, with the CP helpers and test_megatron_vlm_cp_vs_no_cp appended after the packing test (git's auto-merge interleaved the two and left a syntax error). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174dGvY6B1BdCbHEtz4LSzg
- preprocess_packed_seqs: link where Qwen3VLModel partitions a full packed stream itself, and where it rejects a pre-split one without rank-local mRoPE positions. - _defer_finalize_model_grads: link where num_tokens is passed and used (schedule, finalize, Bridge's finalize wrapper), and why it is dropped in favor of _per_token_grad_scaling. - _per_token_grad_scaling: a worked three-call example. - Explain shard_for_cp at its call sites; link DDP's average_in_collective assert. - Correct the VLM + CP + MTP comment: the blocker is the CP-unaware MTP roll, as for every model. - Shorten _set_per_token_loss_counts' docstring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174dGvY6B1BdCbHEtz4LSzg
…e shard_for_cp docstring Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174dGvY6B1BdCbHEtz4LSzg
erictang000
left a comment
There was a problem hiding this comment.
thanks for updating the description with the links, very clear what the problem is and the pr makes sense in that context! just a couple comments on where we should raise errors and some extra validation
one more comment is that it would be good to add an example vlm training example script that includes information on which of these flags need to be set, like adding the flags explicitly at the top on the config here:
…ore any engine or worker starts - overlap_grad_reduce with a VLM under CP (per-token loss mode) is refused at config time. The VLM check reads the model's HF config only when CP > 1 and overlap_grad_reduce is on. The wrapper keeps its check as a backstop for entry points that don't run validate_cfg (the Tinker backend). - trainer.mtp with CP > 1 is refused at config time, for every model (the MTP draft loss is not CP-aware); the VLM-only runtime check is gone. - The VLM-only "CP needs remove_microbatch_padding" check is gone: validate_megatron_cfg already requires it for every CP run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174dGvY6B1BdCbHEtz4LSzg
Per-token loss mode is decided when Megatron-Bridge builds the model, so a config-time check needs the model's HF config, and the wrapper check has to stay for the Tinker backend anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174dGvY6B1BdCbHEtz4LSzg
…ing and CP Sets every VLM flag explicitly at the top, with what each one does: language_model_only=false, remove_microbatch_padding, max_tokens_per_microbatch, context_parallel_size, overlap_grad_reduce=false (per-token loss mode under CP), fused_lm_head_logprob, and the Qwen3.5 GDN engine flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174dGvY6B1BdCbHEtz4LSzg
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174dGvY6B1BdCbHEtz4LSzg
Added one example script, and the run |

Context parallelism (CP > 1) for Qwen3-VL / Qwen3.5-VL on Megatron, on top of #2357 (VLM sequence packing).
1. Every CP rank gets the full packed stream
Qwen3-VL computes 3D mRoPE positions and places image features over the whole packed sequence, so SkyRL can't cut the sequence per CP rank first, as it does for text models. For these models
preprocess_packed_seqs(shard_for_cp=False)sends the full stream to every CP rank, and Megatron-Bridge'sQwen3VLModelsplits it itself:Text models and other VLMs keep SkyRL's per-rank split (
model_owns_vlm_packingis false for them).2. VLM + CP needs per-token loss mode
Bridge's Qwen-VL providers turn on
calculate_per_token_losswhenever CP > 1 (qwen3_vl_provider.py#L137-L139, qwen35_vl_provider.py#L220-L222), and the model asserts it (model.py#L413-L416). In this mode Megatron normalizes differently:average_in_collective=False(distributed_data_parallel.py#L250-L255). SkyRL defaults it to True, so the worker turns it off.finalize_model_gradsdivides every gradient once by the global token count: finalize_model_grads.py#L694-L713CP also requires packing (
trainer.remove_microbatch_padding=true): SkyRL only splits for CP on the packed path.3. Why
loss_funcmultiplies by the token countSkyRL's policy loss is already normalized, since advantages are pre-scaled per mini-batch (#1296). So the gradient we want is the plain sum of every microbatch's loss over all ranks, with no further division. To cancel Megatron's division by the token count N, the worker computes each call's N (padded packed tokens per CP rank, all-reduced over DP x CP), and
loss_funcreturnsloss x Ntogether with this rank's own count (a 3-tuple). This is the same approach as verl's Megatron engine.N / (dp x num_real_microbatches): averaged over DP and real microbatches, as in the default mode.4. Several
forward_backwardcalls per optimizer stepSkyRL can split one optimizer step across several
forward_backwardcalls (e.g. Tinker). Megatron's schedule passes each call's token count tofinalize_model_grads(schedules.py#L863-L870); Bridge's finalize wrapper passes it on (peft/utils.py#L1095-L1114). SkyRL already defers that finalize tooptim_step(_defer_finalize_model_grads, replayed byrun_pending_grad_sync). If the replay divided once by the summed count, the calls would be averaged instead of added. With three calls of N = 100, 200, 300:So the replay gets
num_tokens=None, and_per_token_grad_scalingdivides each call's gradients by its own N, scaling the buffer by N before the call and by 1/N after it:This requires
overlap_grad_reduce=false, since rescaling the buffer would race with reductions still in flight.5. MTP stays unsupported with CP
SkyRL's decoupled MTP draft loss rolls teacher logits and labels with a plain
torch.roll, which isn't CP-aware, so MTP already requires CP=1 for every model. VLM + CP + MTP raises early with a clear message.Test: GPU
test_megatron_vlm_cp_vs_no_cp(Qwen3-VL-2B, Qwen3.5-0.8B; TP1 and TP2+SP) compares CP=2 with CP=1 on logprobs and grad norm.E2E: Harbor SWE-bench Multimodal, long rows (A vs B vs C)
Qwen3.5-35B-A3B (vision tower trained), Megatron TP2/EP8, 8xH100, 64k context,
fused_lm_head_logprob=true; one step of 8 mini-swe-agent rollouts on a SWE-bench Multimodal task (Harbor + skycap, #2450), rows of 27k-65k tokens. Different runs sample different rollouts, so compare distributions.At scale (C, packed + CP2, 88 rows of a pass@8 run, avg response 50.3k tokens, 1.76M loss-masked tokens): logprob diff mean 0.0233, p99 0.229, p99.9 0.447, 0.06% of tokens > 0.5, per-row means 0.013-0.031; fwd logprobs 212 s for the 88 rows.
CP=2 with packing matches CP=1 on logprobs and cuts policy peak memory by 8 GB; per-token loss is on (Qwen3VLModel asserts it under CP>1). MTP stays off.
GPU results (4xH100)
test_megatron_vlm_cp_vs_no_cpat 9d2fb1a, CP=2 vs CP=1 at the same TP and DP=1: 4/4 passed. The batch is on-policy (old logprobs from each layout's own forward) and lr=0, so both phases see the same weights.The logprob differences come from the CP split itself (attention partitioning and reduction order). They are the same size as other bf16 kernel-path changes and below HF's own bf16-vs-fp32 gap (0.05); a wrong split would give O(1) differences. Text-model regression
test_megatron_worker.py -k cp: 11 passed (on an earlier revision).RL, Qwen3.5-9B geometry3k, 40 steps, packed 12288, with #2344 and #2357 (W&B project):
The curves track each other (eval SE ~0.016). CP=2 costs +30% step time at these 1-2k-token lengths.
Notes from debugging the test:
Text only validation
without-commit
with-commit
https://claude.ai/code/session_01MZKuFwA4VYK2tfFDVgsWt2
Note
High Risk
Changes distributed training loss normalization, gradient finalize behavior, and packed-sequence CP layout for VLMs—incorrect scaling or sharding would silently skew RL gradients on long multimodal sequences.
Overview
Adds Megatron context parallelism (CP > 1) for Qwen3-VL / Qwen3.5-VL with packed sequences, plus an example Geometry-3K GRPO launch script for Qwen3.5-9B (TP2, CP2, packing, fused LM-head logprobs).
For Qwen3VLModel, packed inputs no longer get pre-sharded per CP rank:
preprocess_packed_seqsgainsshard_for_cp=Falseso every CP rank sees the full THD stream; the bridge model applies mRoPE, vision placement, and the CP split internally. Text and other VLMs still use SkyRL’s zigzag CP shard.VLM + CP turns on Megatron-Bridge’s
calculate_per_token_loss. The worker attaches global/local token counts, disables DDPaverage_in_collective, requiresoverlap_grad_reduce=false, and the wrapper adjusts loss scaling (policy loss × token count; KL/entropy/MTP use a separate microbatch scale). Deferred grad finalize drops summednum_tokensand uses_per_token_grad_scalingso multipleforward_backwardcalls per optimizer step add gradients instead of being averaged. Config validation rejects MTP with CP > 1.GPU parity test
test_megatron_vlm_cp_vs_no_cpand unit tests cover overlap-grad-reduce and MTP+CP validation.Reviewed by Cursor Bugbot for commit 6b0dd06. Bugbot is set up for automated code reviews on this repo. Configure here.