Skip to content

Enable VLM context parallel training#2909

Closed
hubert-marek wants to merge 30 commits into
mainfrom
feat/multimodal-cp
Closed

Enable VLM context parallel training#2909
hubert-marek wants to merge 30 commits into
mainfrom
feat/multimodal-cp

Conversation

@hubert-marek

@hubert-marek hubert-marek commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Enables Qwen3.5 VLM training with Ulysses context parallelism.

  • Moves VLM multimodal prep (embedding lookup, vision encode + merge, MRoPE position IDs, CP sharding) into the model root forward. This fixes the FSDP2 lazy-init ordering crash (FSDP state has already been lazily initialized for model.language_model.embed_tokens) that happened when embedding/vision children were called before the FSDP root.
  • Allows trainable language embeddings and trainable vision encoders under VLM CP (previously forced frozen). Fully frozen vision stays replicated out of FSDP; trainable params are FSDP-sharded normally.
  • Runs a zero-contribution dummy vision pass for text-only VLM batches so all ranks enter the same vision collectives.
  • Routes Qwen3.5 linear-attention convolutions through FLA's CP-aware causal_conv1d so conv/recurrent state crosses CP rank boundaries instead of resetting at the shard.
  • Uses global seq_lens as the single source for packed cu_seqlens under CP, so packed text-only microbatches don't leak state across sample boundaries.
  • Adds an early setup gate: model.vlm + cp > 1 requires a custom PrimeRL VLM implementation (generic HF VLMs remain supported at cp=1).
  • Simplifies multimodal packing/config plumbing now that VLM batches no longer need modality-specific distribution.
  • Treats packed boundaries as opt-in PrimeRL custom-model metadata: the trainer calls PreTrainedModelPrimeRL.prime_forward_kwargs(...), Qwen3.5 returns the seq_lens kwargs it needs, and generic HF models plus unrelated custom models keep their normal forward signatures.

Notes for new VLM models

VLM CP requires a custom implementation: the root forward must, when CP metadata (cp_group/cp_rank/cp_world_size/cp_style) is present, do embedding + (real-or-dummy) vision merge + position IDs, then shard for CP. Generic HF VLMs don't satisfy this and are gated to cp=1.

Models that need trainer-owned packed-boundary metadata should opt in via prime_forward_kwargs(...) on their PrimeRL top-level model class instead of broadening every custom model forward signature.

Verification

  • Cursor lints clean for changed files.
  • uvx ruff==0.13.0 format --check --config=pyproject.toml src/prime_rl/trainer/model.py passed after the CI formatting failure.
  • uv run --no-sync python -m py_compile src/prime_rl/trainer/model.py tests/unit/train/test_model_forward.py src/prime_rl/trainer/models/*/modeling_*.py passed for the latest custom-model packed metadata interface.
  • H100 e2e (commit 975b117): root-forward Qwen3.5 VLM CP=2 passes for frozen vision, trainable vision, text-only (dummy vision), and multimodal forward/backward. Known gap: the linear-attention FLA path failed to compile on the remote CUDA/TileLang toolchain (fatal error: cuda/atomic: No such file or directory) — environment issue, not logic.
  • H100 integration subset (commit 2015e71): the prior generic-forward seq_lens unexpected-kwarg failure is gone. Five of six modules passed; only benchmark_regression failed because measured throughput/step time were much faster than the stored baseline.
  • Local full pytest remains blocked before collection because deps/pydantic-config is missing Python project metadata.

Made with Cursor

eligotts and others added 20 commits June 14, 2026 23:50
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Fail fast for unsupported multimodal packing runtimes and move the VLM context-parallelism incompatibility into config validation.

Co-authored-by: Cursor <cursoragent@cursor.com>
Avoid an unnecessary multimodal sidecar copy in sample preparation and keep Qwen3-VL configs opted out of strict multimodal packing by default.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Simplify packed multimodal support detection to the root model flag and align unit fixtures with token-level advantages after the main merge.

Co-authored-by: Cursor <cursoragent@cursor.com>
Reject trainer.pack_multimodal with context parallelism regardless of whether an explicit VLM config is present.

Co-authored-by: Cursor <cursoragent@cursor.com>
Two fixes that let context parallelism run for the Qwen3.5 VLM path (the
DeltaNet CP correctness fix landed separately):

1. Vision attention under CP. substitute_ulysses_attn patches the global
   flash_attention_2 dispatch to do the Ulysses all-to-all, which is causal-
   and sharded-sequence-only. The vision encoder runs *before* CP sharding
   (full, replicated patches) with bidirectional attention, so it must not go
   through that path. Pin the vision encoder to sdpa under CP so it dispatches
   around the patched flash_attention_2 entry.

2. FSDP vs the pre-shard merge. The merge (prepare_vlm_inputs_for_context_
   parallel) calls the vision encoder and embed_tokens standalone, before the
   root FSDP forward. FSDP2 requires the first forward of an iteration to go
   through the root, so a separately-sharded module called here lazy-inits as
   its own root and then collides with the real root forward ("FSDP state has
   already been lazily initialized"). Keep these pre-shard-merge modules out of
   FSDP (full/replicated, materialized by model.to_empty) so the standalone
   calls just work — but only when they are FROZEN, since ignored params get no
   FSDP gradient reduction. Frozen -> ignore; trainable -> raise a clear error
   pointing at LoRA / freeze_vision_encoder / the structural fix. This makes
   CP+VLM work out of the box for frozen-embedding recipes (e.g. LoRA) and fails
   loudly (instead of silently mis-training) for full fine-tuning.

Validation (2x H200, cp=2 ulysses, random-init Qwen3.5-35B-A3B, fake data):
- full fine-tune (trainable embed) -> raises the clear error, as intended.
- LoRA (embed + vision frozen) -> guard takes the ignore branch, FSDP setup
  succeeds, reaches the training loop.
- frozen-embed run -> 6 steps end-to-end with images, finite loss, no crash.

Known follow-ups (out of scope here): full-parameter VLM-CP needs the
structural fix (run the pre-shard merge through the root forward); and full
rl-pipeline (orchestrator + inference) validation at real scale is still
required.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7d83615)
Checkpoint current VLM CP cleanup so the branch can return to this state while review continues.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use a model-level dummy vision pass for text-only VLM batches so packing can stay generic while CP sharding still operates on the merged language sequence.

Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve the removed qwen35 RLM SWE config in favor of main while updating the branch.

Co-authored-by: Cursor <cursoragent@cursor.com>
@hubert-marek hubert-marek marked this pull request as ready for review June 29, 2026 23:23
Restore empty trainer table headers for config readability and drop a redundant packing distribution test.

Co-authored-by: Cursor <cursoragent@cursor.com>
@hubert-marek hubert-marek requested review from S1ro1, hallerite and samsja and removed request for S1ro1, hallerite and samsja June 29, 2026 23:23
@hubert-marek hubert-marek requested review from S1ro1 and removed request for samsja June 29, 2026 23:23
@hubert-marek hubert-marek marked this pull request as draft June 29, 2026 23:24
@hubert-marek hubert-marek requested a review from samsja June 29, 2026 23:24
@hubert-marek hubert-marek marked this pull request as ready for review June 29, 2026 23:24
Comment thread src/prime_rl/trainer/model.py Outdated
Comment thread src/prime_rl/trainer/model.py Outdated
else:
for key in mm_kwargs:
mm_kwargs[key].data += sample.mm_kwargs[key].data
mm_kwargs[key].shape[0] += sample.mm_kwargs[key].shape[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Packed MM kwargs key mismatch

Medium Severity

When merging packed multimodal samples, _materialize_bin only concatenates keys already present in the accumulated mm_kwargs. Extra keys on a later sample are dropped, and a missing key on a later sample causes KeyError during packing.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 24eee16. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not possible to happen under 1 model / 1 image processor

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 24eee168c9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Separate by modality so each step index has uniform modality across all ranks
mm_batches = [b for b in micro_batches if _is_multimodal_sample(b)]
text_batches = [b for b in micro_batches if not _is_multimodal_sample(b)]
micro_batches = _pad_group_for_distribution(micro_batches, num_train_workers)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep VLM modality groups aligned across ranks

When the packed list contains separate image and text-only microbatches and num_train_workers > 1, distributing one combined pool can schedule an image microbatch on one rank and a text-only microbatch at the same step on another. For registered HF VLMs that do not have the new dummy-vision path (the custom VLM mapping only covers qwen3_5_moe), the text-only rank skips the FSDP-sharded vision encoder while the image rank enters it, reintroducing the unmatched all-gather/hang that the old mm/text grouping avoided.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We do it on purpose - VLMs with CP need custom implementation

return False
if (first_sample.routed_experts is None) != (sample.routed_experts is None):
return False
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid packing non-segment-aware multimodal samples

When two Qwen-style multimodal samples fit in one bin, this unconditional acceptance packs them into a single sequence. trainer.model.forward() drops the trainer's reset 2-D position_ids whenever image_grid_thw is present, and only the custom qwen3_5_moe path consumes seq_lens; HF VLM paths such as qwen3_vl recompute MRoPE for the concatenation as one sample, so positions and causal attention cross sample boundaries and corrupt multimodal training.

Useful? React with 👍 / 👎.

Comment thread src/prime_rl/trainer/model.py Outdated
Comment thread src/prime_rl/trainer/rl/train.py Outdated
Comment thread src/prime_rl/trainer/rl/train.py Outdated
Comment thread src/prime_rl/trainer/models/qwen3_5_moe/modeling_qwen3_5_moe.py
@hubert-marek hubert-marek marked this pull request as draft June 30, 2026 00:57
Shard language embeddings normally under VLM context parallelism while keeping frozen vision params ignored.

Co-authored-by: Cursor <cursoragent@cursor.com>
@hubert-marek hubert-marek marked this pull request as ready for review June 30, 2026 03:02
Co-authored-by: Cursor <cursoragent@cursor.com>
@hubert-marek

Copy link
Copy Markdown
Contributor Author

@codex

Co-authored-by: Cursor <cursoragent@cursor.com>
@hubert-marek

Copy link
Copy Markdown
Contributor Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 975b11721f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Separate by modality so each step index has uniform modality across all ranks
mm_batches = [b for b in micro_batches if _is_multimodal_sample(b)]
text_batches = [b for b in micro_batches if not _is_multimodal_sample(b)]
micro_batches = _pad_group_for_distribution(micro_batches, num_train_workers)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep VLM microsteps modality-aligned

When VLM training uses an HF/generic VLM (for example the registered gemma4 path loaded through AutoModelForImageTextToText) and a step contains both text-only and image samples, this single mixed group can now be distributed so one data rank runs the vision encoder while another rank gets a text-only forward. Unlike the custom Qwen3.5 path, those HF text-only forwards do not run a dummy vision encoder, so FSDP ranks enter different vision-encoder collectives and can hang—the case the removed modality split was preventing. Keep modality grouping unless the selected model is known to execute a dummy vision path for text-only batches.

Useful? React with 👍 / 👎.

@hubert-marek hubert-marek Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

VLM require custom implementations for CP

if cu_seqlens is not None and int(cu_seqlens[-1].item()) == global_seq_len:
global_cu_seqlens = cu_seqlens.to(device=device, dtype=torch.int32)
else:
global_cu_seqlens = torch.tensor([0, global_seq_len], dtype=torch.int32, device=device)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve packed boundaries for text-only CP

When a Qwen3.5 VLM CP microbatch is all text-only, position_ids stay 2D and Qwen3_5MoeModel.forward recomputes cu_seqlens after sharding, so cu_seqlens[-1] is only the local shard length. This fallback then replaces the packed boundaries with [0, global_seq_len], making FLA's CP convolution/DeltaNet treat multiple packed text samples as one long sequence and leak state across sample boundaries, silently corrupting logprobs. Use the global seq_lens already passed by the packer for this path instead of collapsing to one segment.

Useful? React with 👍 / 👎.

forward_cp_rank = None
forward_cp_world_size = None
forward_cp_style = None
vlm_cp_enabled = cp_enabled and config.model.vlm is not None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate root-level VLM CP to custom models

With CP enabled, this condition takes the root-level VLM CP path for every [model.vlm] configuration, but only the custom Qwen3.5 VLM forward consumes cp_group/cp_rank and shards the merged embeddings internally. Registered HF/generic VLMs such as gemma4 are loaded through AutoModelForImageTextToText, so they either reject the unexpected CP kwargs or run the full sequence on every CP rank while labels and temperatures are already sharded, producing mismatched/duplicated CP outputs. Restrict this path to models that implement the CP-aware root forward, or keep the normal setup_cp_params path for other VLMs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

CP on VLMs require custom implementation

return False
if (first_sample.routed_experts is None) != (sample.routed_experts is None):
return False
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid packing generic multimodal prompts together

When a non-Qwen/generic VLM sample has mm_kwargs without image_grid_thw (for example the registered HF gemma4 path), this now allows multiple image prompts to be concatenated into one microbatch. That forward path only sends reset position_ids and no attention mask or seq_lens, so HF causal attention treats the packed tokens and images as one long prompt; later samples can attend to earlier samples and corrupt training targets. Keep multimodal packing limited to models that actually consume packed boundaries, or leave these samples as separate microbatches.

Useful? React with 👍 / 👎.

Co-authored-by: Cursor <cursoragent@cursor.com>
@hubert-marek

Copy link
Copy Markdown
Contributor Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a973064e36

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +553 to +554
VLM models handle text-only samples with a synthetic dummy vision path, so
microbatches can be distributed through one generic packing path.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep HF VLM batches modality-aligned

This assumption only holds for the custom Qwen3.5 VLM path; get_model still selects AutoModelForImageTextToText for registered VLMs without a custom class, such as Gemma/Qwen3-VL. In those HF VLM runs, text-only microbatches skip the FSDP-wrapped vision encoder while image microbatches enter it, and the combined distribution below can put those two modalities at the same micro-step on different ranks, recreating the vision-collective hang the deleted modality grouping prevented. Keep modality grouping for non-custom VLMs or add the dummy vision path there too.

Useful? React with 👍 / 👎.

Co-authored-by: Cursor <cursoragent@cursor.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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7218a53. Configure here.

bias=self.conv1d.bias,
activation=self.activation,
cp_context=cp_context,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing FLA conv import guard

High Severity

When Ulysses context parallelism builds a FLA cp_context, the linear-attention forward always calls fla_causal_conv1d, but that symbol is imported in a separate optional block and can be None. Training then crashes with a TypeError instead of using a fallback or a clear error, blocking VLM CP on environments where FLA’s CP conv module is unavailable.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7218a53. Configure here.

Co-authored-by: Cursor <cursoragent@cursor.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2015e717aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

def _qwen35_vlm_ulysses_equivalence_worker(rank: int, port: int):
os.environ["MASTER_ADDR"] = "127.0.0.1"
os.environ["MASTER_PORT"] = str(port)
dist.init_process_group("gloo", rank=rank, world_size=2)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use a CUDA-capable backend in the Ulysses GPU test

When this GPU test runs, the worker builds the model inputs/logits on CUDA and the Ulysses path uses distributed all-to-all, followed by dist.all_gather, on those CUDA tensors through dist.group.WORLD. A Gloo process group is CPU-only for these collectives, so GPU CI will fail before the equivalence assertion is reached; use NCCL with one GPU per rank, or skip this test when that setup is unavailable.

Useful? React with 👍 / 👎.

hubert-marek and others added 2 commits June 30, 2026 04:32
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@hubert-marek hubert-marek marked this pull request as draft June 30, 2026 04:43
@hubert-marek

Copy link
Copy Markdown
Contributor Author

@codex

Co-authored-by: Cursor <cursoragent@cursor.com>
@hubert-marek hubert-marek marked this pull request as ready for review June 30, 2026 04:50
@hubert-marek

Copy link
Copy Markdown
Contributor Author

Superseded by the 7-PR stack that split #2485 — the VLM-CP work here is covered by #2946 (SFT/hybrid CP) and #2948 (RL MM CP); the seq_lens threading by #2944. Full list in the closing comment on #2485.

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.

3 participants