-
Notifications
You must be signed in to change notification settings - Fork 345
Enable VLM context parallel training #2909
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 26 commits
3b6c5c7
8d533a3
bfde708
febc12a
ba7a0c7
b259fef
fc3fac6
15956ad
b8a212d
dd099de
4b9cfd7
6a608da
beb66f4
78bdaaa
40237e1
8e293c6
465c05c
9f99604
34b72e5
0b9e252
24eee16
cc0d66f
bd9c981
975b117
a973064
7218a53
2015e71
c8cf6c9
c8a1b96
600c062
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -196,6 +196,8 @@ def prepare_sample(training_example: TrainingSample, seq_len: int) -> MicroBatch | |
| assert len(mm_token_type_ids) == len(input_ids), ( | ||
| f"mm_token_type_ids: {len(mm_token_type_ids)}, input_ids: {len(input_ids)}" | ||
| ) | ||
| if mm_kwargs is not None and "image_grid_thw" in mm_kwargs and mm_token_type_ids is None: | ||
| raise ValueError("image_grid_thw multimodal samples require mm_token_type_ids") | ||
| assert len(env_names) == len(input_ids), f"env_names: {len(env_names)}, input_ids: {len(input_ids)}" | ||
|
|
||
| return MicroBatch( | ||
|
|
@@ -214,6 +216,7 @@ def prepare_sample(training_example: TrainingSample, seq_len: int) -> MicroBatch | |
| rl_weights=rl_weights, | ||
| ce_weights=ce_weights, | ||
| ref_kl_weights=ref_kl_weights, | ||
| seq_lens=[len(input_ids)], | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -237,15 +240,14 @@ def first_sample(self) -> MicroBatch: | |
|
|
||
| def can_add(self, sample: MicroBatch, max_seq_len: int) -> bool: | ||
| # Loss routing is per token (component weight streams), so samples of | ||
| # different loss types pack together freely — only modality, length and | ||
| # routed-experts presence constrain packing. | ||
| # different loss types pack together freely. The packer groups by | ||
| # run/LoRA before this point. | ||
| first_sample = self.first_sample | ||
| return ( | ||
| not _is_multimodal_sample(first_sample) | ||
| and not _is_multimodal_sample(sample) | ||
| and self.length + len(sample.input_ids) <= max_seq_len | ||
| and (first_sample.routed_experts is None) == (sample.routed_experts is None) | ||
| ) | ||
| if self.length + len(sample.input_ids) > max_seq_len: | ||
| return False | ||
| if (first_sample.routed_experts is None) != (sample.routed_experts is None): | ||
| return False | ||
| return True | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a non-Qwen/generic VLM sample has Useful? React with 👍 / 👎. |
||
|
|
||
| def add(self, lora_idx: int, sample: MicroBatch) -> None: | ||
| self.samples.append((lora_idx, sample)) | ||
|
|
@@ -290,6 +292,8 @@ def _materialize_bin(bin_content: _MicroBatchBin, num_loras: int) -> MicroBatch: | |
| env_names: list[str] = [] | ||
| ref_logprobs: list[float] | None = [] if has_ref_logprobs else None | ||
| mm_token_type_ids: list[int] | None = [] if has_mm_token_type_ids else None | ||
| mm_kwargs: dict[str, EncodedTensor] | None = None | ||
| seq_lens: list[int] = [] | ||
| streams: dict[str, list[float] | None] = {name: ([] if has_stream[name] else None) for name in STREAM_FILL} | ||
| routed_experts: RoutedExperts | None = None | ||
| lora_num_tokens = [0] * num_loras | ||
|
|
@@ -322,11 +326,19 @@ def _materialize_bin(bin_content: _MicroBatchBin, num_loras: int) -> MicroBatch: | |
| assert routed_experts.shape[1:] == sample.routed_experts.shape[1:] | ||
| routed_experts.data += sample.routed_experts.data | ||
| routed_experts.shape[0] += sample.routed_experts.shape[0] | ||
| if sample.mm_kwargs is not None: | ||
| if mm_kwargs is None: | ||
| mm_kwargs = copy.deepcopy(sample.mm_kwargs) | ||
| 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] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Packed MM kwargs key mismatchMedium Severity When merging packed multimodal samples, Reviewed by Cursor Bugbot for commit 24eee16. Configure here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not possible to happen under 1 model / 1 image processor |
||
| seq_lens.extend(sample.seq_lens) | ||
| lora_num_tokens[lora_idx] += sample_len | ||
|
|
||
| sequence_lengths = [len(sample.input_ids) for _, sample in bin_content.samples] | ||
| assert sum(sequence_lengths) == len(input_ids), (sequence_lengths, len(input_ids)) | ||
| first_sample = bin_content.first_sample | ||
| assert sum(seq_lens) == len(input_ids), (seq_lens, len(input_ids)) | ||
|
|
||
| return MicroBatch( | ||
| input_ids=input_ids, | ||
|
|
@@ -341,10 +353,11 @@ def _materialize_bin(bin_content: _MicroBatchBin, num_loras: int) -> MicroBatch: | |
| routed_experts=routed_experts, | ||
| mm_token_type_ids=mm_token_type_ids, | ||
| env_names=env_names, | ||
| mm_kwargs=first_sample.mm_kwargs if _is_multimodal_sample(first_sample) else None, | ||
| mm_kwargs=mm_kwargs, | ||
| rl_weights=streams["rl_weights"], | ||
| ce_weights=streams["ce_weights"], | ||
| ref_kl_weights=streams["ref_kl_weights"], | ||
| seq_lens=seq_lens, | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -374,19 +387,17 @@ def packed_samples_into_micro_bs( | |
| ) -> list[MicroBatch]: | ||
| """ | ||
| Pack samples into micro_batch efficiently. | ||
| We follow the First Fit Decreasing algorithm to pack the samples into bins and minimize potential padding while never truncating. | ||
| With per-token temperatures, samples can be packed together regardless of their temperature values. | ||
|
|
||
| NOTE: Multimodal samples (with mm_kwargs) are NOT packed together as they have variable-sized | ||
| vision data that doesn't pack well. Each multimodal sample becomes its own micro batch. | ||
| We follow the First Fit Decreasing algorithm to pack samples into bins and | ||
| minimize potential padding while never truncating. Packed batches preserve | ||
| sample boundaries in ``seq_lens``. | ||
| """ | ||
| # Sort by (lora_idx, -length) for packing efficiency | ||
| samples.sort(key=lambda x: (x[0], -len(x[1].input_ids))) | ||
|
|
||
| bins: list[_MicroBatchBin] = [] | ||
|
|
||
| for idx, sample in samples: | ||
| # Try to find a bin that can fit this sequence (only pack text-only samples) | ||
| # Try to find a bin that can fit this sequence. | ||
| for bin_content in bins: | ||
| if bin_content.can_add(sample, max_seq_len): | ||
| bin_content.add(idx, sample) | ||
|
|
@@ -464,6 +475,7 @@ def pad_micro_batch(micro_batch: MicroBatch, pad_to_multiple_of: int) -> MicroBa | |
| ) | ||
| if micro_batch.mm_token_type_ids is not None: | ||
| micro_batch.mm_token_type_ids.extend([0] * padding_size) | ||
| micro_batch.seq_lens.append(padding_size) | ||
| if micro_batch.routed_experts is not None: | ||
| _pad_routed_experts(micro_batch, padding_size) | ||
| micro_batch.env_names.extend([""] * padding_size) | ||
|
|
@@ -538,32 +550,24 @@ def prepare_batch( | |
| Prepare a batch of problems for each GPU. Each batch is a list of micro batches. | ||
| Each micro batch is shape [1, seq_len], the number of samples is not fixed per micro batch. | ||
|
|
||
| FSDP requires all ranks to execute the same operations at each step. If one rank | ||
| processes a multimodal batch (triggering the vision encoder) while another processes | ||
| a text-only batch, the all-gather will hang. We separate micro batches by modality | ||
| and distribute them so that at each step index, all ranks see the same modality. | ||
| VLM models handle text-only samples with a synthetic dummy vision path, so | ||
| microbatches can be distributed through one generic packing path. | ||
|
Comment on lines
+553
to
+554
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This assumption only holds for the custom Qwen3.5 VLM path; Useful? React with 👍 / 👎. |
||
| """ | ||
| all_samples = [(idx, prepare_sample(rollout, seq_len)) for idx, rollout in zip(idxs, rollouts)] | ||
|
|
||
| micro_batches = packed_samples_into_micro_bs(all_samples, seq_len, num_loras, num_train_workers, bin_cost) | ||
| micro_batches = packed_samples_into_micro_bs( | ||
| all_samples, | ||
| seq_len, | ||
| num_loras, | ||
| num_train_workers, | ||
| bin_cost, | ||
| ) | ||
| micro_batches = [pad_micro_batch(micro_batch, pad_to_multiple_of) for micro_batch in micro_batches] | ||
|
|
||
| # 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the packed list contains separate image and text-only microbatches and Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do it on purpose - VLMs with CP need custom implementation There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When VLM training uses an HF/generic VLM (for example the registered Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. VLM require custom implementations for CP |
||
|
|
||
| # Pad each group independently so its count is divisible by num_train_workers | ||
| mm_batches = _pad_group_for_distribution(mm_batches, num_train_workers) | ||
| text_batches = _pad_group_for_distribution(text_batches, num_train_workers) | ||
|
|
||
| # Alignment check after distribution padding so the dummy batches are covered too | ||
| for micro_batch in (*mm_batches, *text_batches): | ||
| # Alignment check after distribution padding so the dummy batches are covered too. | ||
| for micro_batch in micro_batches: | ||
| _assert_token_arrays_aligned(micro_batch) | ||
|
|
||
| batches_per_gpu: list[list[MicroBatch]] = [[] for _ in range(num_train_workers)] | ||
| for group in (mm_batches, text_batches): | ||
| group_batches_per_gpu = _distribute_group(group, num_train_workers, bin_cost) | ||
| for worker_idx, worker_batches in enumerate(group_batches_per_gpu): | ||
| batches_per_gpu[worker_idx].extend(worker_batches) | ||
|
|
||
| return batches_per_gpu | ||
| return _distribute_group(micro_batches, num_train_workers, bin_cost) | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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-Dposition_idswheneverimage_grid_thwis present, and only the customqwen3_5_moepath consumesseq_lens; HF VLM paths such asqwen3_vlrecompute MRoPE for the concatenation as one sample, so positions and causal attention cross sample boundaries and corrupt multimodal training.Useful? React with 👍 / 👎.