Skip to content

[megatron][vlm] Context parallelism for Qwen3-VL / Qwen3.5-VL - #2404

Merged
erictang000 merged 42 commits into
NovaSky-AI:mainfrom
xinyuangui2:vlm-megatron-cp
Oct 19, 2026
Merged

erictang000 merged 42 commits into
NovaSky-AI:mainfrom
xinyuangui2:vlm-megatron-cp

Conversation

@xinyuangui2

@xinyuangui2 xinyuangui2 commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

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's Qwen3VLModel splits it itself:

  • Given a full stream, it computes this rank's partition: model.py#L632-L647
  • It places image/deepstack features, then slices the embeddings: model.py#L852-L865
  • It computes mRoPE on the full stream, then slices the positions: model.py#L931-L955
  • A pre-split stream is accepted only with explicit rank-local 3D position ids, which SkyRL doesn't compute: model.py#L630-L631

Text models and other VLMs keep SkyRL's per-rank split (model_owns_vlm_packing is false for them).

2. VLM + CP needs per-token loss mode

Bridge's Qwen-VL providers turn on calculate_per_token_loss whenever 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:

CP also requires packing (trainer.remove_microbatch_padding=true): SkyRL only splits for CP on the packed path.

3. Why loss_func multiplies by the token count

SkyRL'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_func returns loss x N together with this rank's own count (a 3-tuple). This is the same approach as verl's Megatron engine.

  • KL / entropy are per-microbatch means, so they use N / (dp x num_real_microbatches): averaged over DP and real microbatches, as in the default mode.
  • MoE aux / z losses keep working: the router pre-multiplies them by its token count and relies on this same division (router.py#L617-L640).
  • The default mode (no per-token loss) is unchanged.

4. Several forward_backward calls per optimizer step

SkyRL can split one optimizer step across several forward_backward calls (e.g. Tinker). Megatron's schedule passes each call's token count to finalize_model_grads (schedules.py#L863-L870); Bridge's finalize wrapper passes it on (peft/utils.py#L1095-L1114). SkyRL already defers that finalize to optim_step (_defer_finalize_model_grads, replayed by run_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:

buffer = 100·g1 + 200·g2 + 300·g3, divided by 600 = 1/6·g1 + 1/3·g2 + 1/2·g3   (wrong)

So the replay gets num_tokens=None, and _per_token_grad_scaling divides each call's gradients by its own N, scaling the buffer by N before the call and by 1/N after it:

call enter (xN) + backward exit (/N) buffer
1 0 + 100·g1 /100 g1
2 200·g1 + 200·g2 /200 g1 + g2
3 300·(g1+g2) + 300·g3 /300 g1 + g2 + g3

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.

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.

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_cp at 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.

logprob mean / max grad norm CP1 -> CP2 two calls / one call (CP1, CP2)
Qwen3-VL-2B, TP1 0.034 / 0.30 4729.0 -> 4679.5 (-1.0%) 1.0026, 1.0030
Qwen3.5-0.8B, TP1 0.036 / 0.28 6894.3 -> 6942.2 (+0.7%) 1.0047, 1.0042
Qwen3-VL-2B, TP2+SP 0.033 / 0.29 4497.2 -> 4446.2 (-1.1%) 1.0016, 1.0048
Qwen3.5-0.8B, TP2+SP 0.030 / 0.34 7007.9 -> 6982.6 (-0.4%) 1.0035, 1.0027

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):

eval pass@1 (step 0 / 20 / 40) train reward (steps 31-40) logprob diff mean step time
CP=2 (TP2xCP2) 0.298 / 0.747 / 0.790 0.720 0.0063 141 s
CP=1 (TP2xDP2) 0.308 / 0.754 / 0.820 0.741 0.0061 108 s

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:

  • An earlier revision showed Qwen3.5 TP2 at -7.3%. That was a test artifact: fixed old logprobs (-1) put tokens near the PPO clip boundary, and bf16-level logprob differences clipped different tokens in each layout. With clipping off it was -0.34%, with every layer within +/-1.5%.
  • After one real optimizer step at CP=2, the weights and reduced gradients are bit-identical across CP ranks, and every trainable parameter is in a DDP grad buffer.
  • One earlier run of the old post-step two-call check showed a one-off CP=2 outlier (+16-47% on Qwen3-VL). It didn't recur in three more runs; post-step two-call norms otherwise vary about +/-4% between runs.

Text only validation

without-commit
with-commit

image image image image image

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_seqs gains shard_for_cp=False so 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 DDP average_in_collective, requires overlap_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 summed num_tokens and uses _per_token_grad_scaling so multiple forward_backward calls 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_cp and 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.

xinyuangui2 and others added 5 commits October 1, 2026 01:38
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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread skyrl/backends/skyrl_train/distributed/megatron/megatron_utils.py Outdated
xinyuangui2 and others added 4 commits October 4, 2026 21:06
…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>
xinyuangui2 and others added 5 commits October 5, 2026 04:36
…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>
xinyuangui2 added a commit to xinyuangui2/SkyRL that referenced this pull request Oct 5, 2026
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>
xinyuangui2 added a commit to xinyuangui2/SkyRL that referenced this pull request Oct 5, 2026
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>
xinyuangui2 added a commit to xinyuangui2/SkyRL that referenced this pull request Oct 5, 2026
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>
xinyuangui2 and others added 3 commits October 5, 2026 22:39
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>
xinyuangui2 added a commit to xinyuangui2/SkyRL that referenced this pull request Oct 5, 2026
…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>
@xinyuangui2
xinyuangui2 marked this pull request as ready for review October 5, 2026 23:06
xinyuangui2 and others added 3 commits October 5, 2026 23:07
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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

xinyuangui2 and others added 8 commits October 6, 2026 04: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>
… 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>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

… 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
xinyuangui2 added a commit to xinyuangui2/SkyRL that referenced this pull request Oct 12, 2026
…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
@erictang000 erictang000 self-assigned this Oct 13, 2026
…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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 0c82a1e. Configure here.

Comment thread skyrl/backends/skyrl_train/workers/megatron/megatron_model_wrapper.py Outdated
erictang000 added a commit that referenced this pull request Oct 14, 2026
…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>
xinyuangui2 and others added 3 commits October 14, 2026 19:54
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 erictang000 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.

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:

Comment thread skyrl/backends/skyrl_train/workers/megatron/megatron_model_wrapper.py Outdated
xinyuangui2 and others added 4 commits October 17, 2026 16:20
…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
@xinyuangui2

Copy link
Copy Markdown
Collaborator Author

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:

Added one example script, and the run

@erictang000
erictang000 merged commit f102518 into NovaSky-AI:main Oct 19, 2026
7 checks passed
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