feat(vlm): integrate packed CP and vision sharding for Qwen3.5-MoE - #3186
Merged
Conversation
Contributor
yuhezhang-ai
force-pushed
the
yuhez/feat/cp-vlm-integration-2937
branch
2 times, most recently
from
July 23, 2026 14:33
c05725a to
fd5faa7
Compare
yuhezhang-ai
changed the base branch from
main
to
yuhez/upstream-cp-vision-shard
July 23, 2026 14:48
…ding Under context parallelism the VLM pre-embed step runs the entire vision tower on the full, unsharded set of images on every CP rank. A CP size of N therefore means N-fold redundant vision computation and all-media vision activations on each rank. Qwen3-VL-style vision towers attend per image or frame through cu_seqlens built from grid_thw, with spatial position embeddings repeated per frame. maybe_distribute_visual expands each (t, h, w) entry into t frame units, partitions them contiguously across the sharding group, runs the vision tower on each rank's slice, and reassembles the complete embedding set with a differentiable variable-length all-gather. Details: - Frame granularity allows a single large video, or a pack with fewer videos than ranks, to be divided across ranks. Images are one unit. - When there are fewer frames than ranks, minimal dummy frames keep the collective sequence uniform. Their embeddings are removed before the loss and contribute no gradient. - Batches below distributed.cp_vision_sharding.min_tokens and batches without media keep the replicated path. Disabling distributed.cp_vision_sharding provides the A/B baseline. - maybe_distribute_independent_units exposes the same partition, run, gather, and slice scheme for towers with other forward signatures. A frozen vision tower can use a wider group with replicated weights. A trainable tower must use the CP group only because the vision tower is replicated across TP ranks and a wider reduce-scatter SUM would over-count its gradient. Vision sharding is inactive when cp_size is 1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
40 CPU-only tests (CP ranks simulated in-process, collectives mocked; no GPU / no distributed init) covering: - partition: contiguous, complete, >=1 entry per rank, attention-cost balanced (including high-resolution hotspots), None when fewer entries than ranks - the core property: visual(full) == concat_r visual(slice_r) for forward AND vision-parameter gradients at world sizes 2-4, including video (t > 1) entries - variable-token differentiable all-gather forward + backward - maybe_distribute_independent_units: order preservation, single differentiable gather, dummy padding with exact zero gradient - maybe_distribute_visual end-to-end: replicate parity, deepstack gathering, a single video split across ranks at frame granularity, every fallback (no group, env-disabled, below min tokens), and the pad path backward through the production gather/slice code asserting sum_r grad_r == replicate grad with zero gradient on padded ranks Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Add an Advanced Training guide for frame-level CP vision-tower sharding: design (frame independence, balanced contiguous partition, differentiable all-gather, dummy-frame pad path), the sharding-scope design constraint (frozen tower may shard across CP x TP; trainable tower shards within the CP group only, so the reduce-scatter backward never double-counts TP-replicated gradients; pure TP is not enabled), usage and environment toggles, and a validation summary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
- maybe_distribute_visual: return the plain visual(...) call when pixel_values or grid_thw is None; the no-media fallback previously routed through _grid_for_visual and crashed on grid_thw.device. - Machine-check the frozen-vs-trainable sharding-scope constraint: set_cp_vision_group(group, *, spans_only_cp=True) records the caller's declaration and maybe_distribute_visual raises ValueError when a trainable vision tower meets a group not declared CP-only (gathering across CP x TP would accumulate the vision gradient tp-fold in the reduce-scatter(SUM) backward). - Drop dead surface with no production callers: the generic maybe_distribute_independent_units helper, the CPU-grid metadata side channel (set/reset_cp_vision_cpu_grids, cp_vision_grid_metadata), and the never-set _nemo_cpu_grid_flash_ok escape hatch; add __all__ with the four names the wiring PR calls. - Validate each rank's local visual token count (pooler + deepstack) before the gather instead of silently mis-slicing gathered blocks. - Annotate the public API signatures (Tensor | None, ProcessGroup | None, typed reset token) and downgrade the informational SHARDED / small-workload logs from WARNING to INFO. - Tests: cover the None-input fallback, the trainable-scope guard (raise) and the frozen-tower spans_only_cp=False path; drop tests of the removed helper/side channel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Drive maybe_distribute_visual forward AND backward through real all_gather / reduce_scatter(SUM) collectives on a 2-rank gloo CPU group (mp.spawn, matching the existing 2-proc gloo test conventions): each rank backprops only its sequence shard of the gathered embeds, and the group-summed vision-parameter gradients must equal the single-process replicated reference -- for the balanced-partition path (with deepstack) and for the dummy-pad path, where the padded rank must receive exactly zero gradient. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
The frozen-vs-trainable sharding-group constraint is now machine-checked via set_cp_vision_group(..., spans_only_cp=...); describe the raise, fix the pure-TP row to match the actual group-size <= 1 gate, drop the removed generic helper, and list the new real-collective gloo tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Balance the frame-level CP vision-tower partition by p*(p+alpha) instead of pure p**2. The quadratic term captures per-frame attention, while alpha*p represents linear per-patch work such as QKV and MLP projections. This improves balance for packs that mix large image frames with many smaller video frames. By default, alpha is inferred as 3 * vision_hidden_size from the model without class-name checks. Unknown towers fall back to alpha=0. distributed.cp_vision_sharding.cost_alpha accepts an exact nonnegative integer override; 0 restores p**2 and auto selects model discovery. This setting affects partitioning only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
_contiguous_balanced_bounds used bisect.bisect_left to place each rank's cut point, which undershoots by one when a frame lands exactly on a cumulative-cost boundary: 4 equal-cost frames at world=2 split [1, 3] instead of [2, 2]. This directly skews packed-sequence vision load balancing (the cost model exists precisely to balance packs mixing big image frames with many small video frames). Switch to bisect.bisect_right so the cut is placed past the frame that closes an exact-target cumulative sum, giving an even split. The >=1 entry-per-rank invariant is preserved by the existing lo/hi clamps. Add unit tests: equal-cost even splits (4/world2, 6/world3, 8/world4), an uneven-cost exact-boundary case (patches [3,4,5] -> loads [25,25]), and cuts landing exactly on a cumulative sum. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
…ivalent The vision-shard parity tests assert torch.allclose (with tolerances), not bitwise equality, so "exactly"/"byte-for-byte"/"bit-identical" overclaimed the guarantee. Reword the module docstring/comments, the cp-vision-sharding guide, and the test module docstring to say "numerically equivalent (allclose)". No tolerances or behavior change; the exact-call replicate FALLBACK wording (a literally identical call) is left as-is. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
…atch The per-rank token-count validation (and the deepstack-features check) raised a ValueError BEFORE the differentiable all-gather. If one rank's visual output diverged from its planned frame slice, that rank raised while every peer blocked forever in all_gather -> deadlock. Route both checks through _raise_if_any_rank_failed, which all-reduces a boolean flag (MAX) over the sharding group so every rank takes the same path: either all raise or all proceed. The diverging rank raises its own actionable message; peers raise a group-level message pointing at it. The flag tensor is placed on the compute device so the reduce matches the active backend (CUDA under NCCL). Add a 2-rank gloo test (_DivergentVisual drops one token on the last rank) asserting BOTH ranks raise ValueError with no hang, and mock all_reduce as a no-op in the single-process CPU simulation helper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Qwen3_5ForConditionalGeneration.prepare_model_inputs_for_cp ran the full vision tower redundantly on every CP rank, including the visual forward and all-media activations. Route its image and video calls through cp_vision_shard.maybe_distribute_visual so each CP rank processes a frame slice and gathers the resulting embeddings in their original order. The typed distributed.cp_vision_sharding policy controls the feature. When it is disabled or no CP vision group is published, the helper preserves the replicated get_image_features and get_video_features path. The published group is CP-only, which is gradient-correct for frozen and trainable towers. The integration preserves the upstream prepare_model_inputs_for_cp structure and its per-modality calls. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
…ward The VLM fine-tuning recipe drives context-parallel vision pre-embedding through model(_pre_embed_only=True, ...). Wrap the training and evaluation call sites in _run_cp_pre_embed, which publishes the CP submesh process group for the duration of the forward and restores it in a finally block. The group is CP-only and is gradient-correct for frozen and trainable towers. The wrapper is a no-op when cp_size is 1 or the typed distributed.cp_vision_sharding policy is disabled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Add a single-node dense Qwen3.5-4B VLM configuration that exercises context-parallel vision-tower sharding on eight GPUs with CP2 and DP4. Vision sharding is enabled under distributed.cp_vision_sharding, and the header documents a same-topology baseline using the typed CLI override. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
…n-shard Add coverage that exercises the ACTUAL transformers Qwen3VLVisionModel (the tower the dense Qwen3.5 VLM CP pre-embed shards), closing the "no real-tower coverage" gap: - test_cp_vision_shard_qwen3_5.py (CPU): build a tiny real Qwen3-VL tower and assert visual(all) == concat_r visual(slice_r) for pooler_output AND every deepstack feature, partitioned by the module's own _contiguous_balanced_bounds -- image entries at world 2-4, a single 8-frame video split at frame granularity, and the flat-vs-split pooler_output equivalence the Qwen3.5 wiring relies on. - test_cp_vision_shard_gloo.py: add a 2-rank gloo test driving maybe_distribute_visual on the real tiny tower through real all_gather / reduce_scatter collectives, asserting pooler + deepstack forward parity with the replicated reference (skipped when transformers Qwen3-VL is unavailable). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Contributor
Author
|
/ok to test dcca00d |
Contributor
|
/ok to test ae0ff22 |
athitten
approved these changes
Jul 29, 2026
athitten
left a comment
Contributor
There was a problem hiding this comment.
Thank you @yuhezhang-ai ! Just re-approving based on @akoumpa 's approval
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Integrate packed context parallelism and frame-level CP vision sharding into Qwen3.5-MoE VLM training, with Qwen3.5-122B-A10B at 128K context as the large-scale target.
The implementation follows the merged unified CP contract from #2937:
prepare_model_inputs_for_cpis metadata/sharder-only, while embedding, trainable vision encoding, multimodal splicing, and primary sequence sharding happen inside the model forward per microbatch.Changelog
ContextParallelSharderfor packed Qwen3.5-MoE SDPA batches; ordinary batches retain the merged round-robin CP path.forward, then shard the primary embeddings contiguously for packed CP while auxiliary tensors use the identical layout.T × H × Wresize volume can be bounded explicitly.Landing status
All prerequisite PRs are merged. This branch includes latest
main(d3f3b2b6d) at final head9b24aa970; the remaining 25-file diff is the Qwen3.5-MoE packed VLM integration described below.Validation
Rebase/refactor validation
After rebuilding on the merged #2937/#2989 design:
git diff --checkpassed.ef2225f2a: 456 focused CPU tests passed and 51 CUDA-only tests skipped; Ruff formatting/checks andgit diff --checkpassed. The local MDX parser validated all 452 pages; the Fern CLI is not installed locally, so CI supplies the finalfern check. GitHub reports this PR mergeable with 25 changed files.9b24aa970: 172 directly affected AutoPipeline, Qwen3.5-MoE, and VLM recipe tests passed; 8 CUDA-only cases skipped. Ruff formatting/checks andgit diff --checkpassed. The resolution preserves upstream mRoPE PP chunking alongside the CP/packing integration.The previously validated two-GPU packed full-attention + GDN parity run also passed (Slurm job
14214890).Qwen3.5-122B-A10B, 128K integration runs
100-step mixed-workload run
A 100-step end-to-end CP16 run completed successfully on the pre-rebase integration source revision
7ce8dc955; the history rebase preserves that integration logic while adapting it to the merged sharder API:Weights & Biases run
The validation set has 48 packed sequences and was intentionally reused for the 100-step mechanics/convergence smoke. The near-zero terminal loss demonstrates that the configuration can train and overfit this dataset; it is not evidence of production-scale convergence or generalization.
Post-refactor CP32 runs
Two exact post-refactor implementation runs used revision
df4c51ac9and the CP32/GBS4 settings now published byded4f01eb:1429383314293834Both runs used 16 nodes / 128 H100 GPUs, EP8, CP32, inferred DP4, a trainable vision tower, full activation checkpointing, and FP32 optimizer state. Video preprocessing was bounded to at most 8 sampled frames and a total sampled volume of 524,288 pixels (approximately 8 × 256 × 256).
The first logged step in both runs took approximately five minutes because it included compilation and autotuning; it is not representative steady-state throughput. The public MedPix workload is also intentionally vision-heavy—its 128K packs combine many independent single-image samples—so its approximately 3K aggregate steady TPS is a topology/correctness result, not a throughput claim. For representative long-text or bounded-video training, use the smallest CP degree that fits; CP16 preserves DP8 on this 128-GPU allocation and was more throughput-efficient than CP32/DP4.
Before ready for review
main(d3f3b2b6d), preserve upstream Qwen3.5 mRoPE PP chunking, and confirm the remaining diff is integration-only.9b24aa970.Additional information