Skip to content

Commit 911e72c

Browse files
going-songterryoosailor1493
authored
feat(data): intra-microbatch reordering for MegatronMIMO scalable reads (#6306)
Signed-off-by: kayeon.song <kayeon.song@navercorp.com> Signed-off-by: Yoonsik Kim <yoonsik.kim90@navercorp.com> Signed-off-by: Chanwoo Park <chanwoo.park98@navercorp.com> Co-authored-by: Yoonsik Kim <yoonsik.kim90@navercorp.com> Co-authored-by: Chanwoo Park <45866990+sailor1493@users.noreply.github.com>
1 parent db0ab2a commit 911e72c

17 files changed

Lines changed: 3173 additions & 10 deletions

File tree

‎docs/index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ training/cpu-offloading.md
8585
training/moe-optimization.md
8686
training/peft.md
8787
training/packed-sequences.md
88+
training/mimo-intra-microbatch-reorder.md
8889
training/multi-token-prediction.md
8990
training/dspark-speculative-decoding.md
9091
training/callbacks.md

‎docs/training/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ This directory contains comprehensive documentation for training and customizing
6363
|----------|---------|--------------|
6464
| **[PEFT](peft.md)** | Parameter-Efficient Fine-Tuning (LoRA, etc.) | Fine-tuning with limited resources |
6565
| **[Packed Sequences](packed-sequences.md)** | Sequence packing for efficiency | Optimizing data loading |
66+
| **[MegatronMIMO Intra-Microbatch Reordering](mimo-intra-microbatch-reorder.md)** | Balance per-sample vision load across module DP ranks | Non-colocated MIMO VLM finetuning with uneven image load |
6667
| **[Megatron FSDP](megatron-fsdp.md)** | Stable overview of Megatron FSDP | Choosing an FSDP path |
6768
| **[Distillation](../modelopt/distillation.md)** | Knowledge distillation techniques | Transferring knowledge between models |
6869
| **[Checkpointing](checkpointing.md)** | Checkpoint saving, loading, and resuming | Managing training state |
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# MegatronMIMO Intra-Microbatch Reordering
2+
3+
Intra-microbatch reordering rebalances the per-sample vision load of each
4+
micro-batch across a module's data-parallel ranks in non-colocated
5+
MegatronMIMO training. It removes the straggler that appears when one
6+
data-parallel rank happens to draw the heaviest images every step.
7+
8+
This page is the stable overview of what the feature does, how to enable it,
9+
and which constraints are durable. The implementation lives in
10+
`megatron.bridge.data.megatron_mimo.reorder_buffer`; the Qwen3.5-VL example
11+
under `examples/megatron_mimo/qwen35_vl/` wires it end to end.
12+
13+
## What It Is
14+
15+
In a non-colocated MegatronMIMO run the vision encoder and the language model
16+
run on disjoint rank groups, each with its own data-parallel size. With
17+
scalable data parallelism (`megatron_mimo_scalable_dp`), every rank reads only
18+
its own shard of each global micro-batch, so the images a rank must encode are
19+
whatever the sampler happened to assign to it. Image sizes vary widely, so the
20+
per-rank vision work varies too, and the slowest rank gates every step.
21+
22+
The reorder buffer wraps the training data iterator and, for each micro-batch
23+
(or window of micro-batches), does three things:
24+
25+
1. Computes a per-sample cost on every rank from fields that are identical on
26+
the vision and language modules: the image-placeholder token count in
27+
`input_ids` (proportional to the number of vision patches) and, optionally,
28+
the real token count from `attention_mask`.
29+
2. All-gathers the costs over the module's data-parallel group and computes one
30+
balanced assignment of samples to ranks. The assignment is a deterministic
31+
function of the costs, so the vision and language modules compute the same
32+
one without talking to each other.
33+
3. Exchanges samples with a single ragged `all_to_all` so each rank ends up
34+
holding its assigned samples in a canonical order.
35+
36+
Because both modules apply the same assignment, sample *i* on vision replica
37+
*r* is still sample *i* on language replica *r*, and the `BridgeCommunicator`
38+
fan-out stays aligned. Balancing is done over the canonical grid (the least
39+
common multiple of the module data-parallel sizes), so modules with different
40+
data-parallel sizes, including a module with data-parallel size 1, agree on
41+
the same order. A data-parallel-1 module has nothing to exchange and simply
42+
permutes its whole micro-batch locally; that permutation runs synchronously on
43+
the training thread (about 14 ms per micro-batch of 4 samples on A100) because
44+
there is no exchange to overlap.
45+
46+
By default the exchange for the next window runs on a background thread with a
47+
dedicated NCCL process group, overlapping with the current window's compute.
48+
49+
## When to Use It
50+
51+
Reordering is a good fit when all of the following are true:
52+
53+
- the run is non-colocated MegatronMIMO with `megatron_mimo_scalable_dp=True`
54+
- at least one module has data-parallel size 2 or more
55+
- image load varies across samples (document, receipt, or mixed text-and-image
56+
SFT data), so per-rank vision time is the per-step straggler
57+
58+
It does not help text-only training, colocated single-module runs, or data
59+
where every sample carries the same number of similarly sized images.
60+
61+
## Bridge Configuration
62+
63+
All fields live on `DirectHFSFTDatasetConfig` and default to off.
64+
65+
| Field | Default | Meaning |
66+
|---|---|---|
67+
| `megatron_mimo_scalable_dp` | `False` | Read only this rank's canonical-grid shard. Required by reordering. |
68+
| `megatron_mimo_intra_microbatch_reorder` | `False` | Enable the reorder buffer. |
69+
| `megatron_mimo_reorder_encoder_cost_weight` | `1.0` | Cost weight per vision patch. |
70+
| `megatron_mimo_reorder_language_cost_weight` | `0.0` | Cost weight per real language token; `0.0` balances on vision load only. |
71+
| `megatron_mimo_reorder_overlap` | `True` | Exchange the next window on a background thread. |
72+
| `megatron_mimo_reorder_window_size` | `1` | Micro-batches balanced together in one exchange. |
73+
74+
Only the ratio of the two cost weights matters; at least one must be positive.
75+
76+
The Qwen3.5-VL example exposes the same fields as `--scalable-dp`,
77+
`--intra-microbatch-reorder`, `--reorder-encoder-cost-weight`,
78+
`--reorder-language-cost-weight`, `--no-overlap-intra-microbatch-reorder`, and
79+
`--reorder-window-size`:
80+
81+
```bash
82+
python examples/megatron_mimo/qwen35_vl/finetune_qwen35_vl.py \
83+
--component "language=tp=1,pp=1,dp=2,rank_offset=0" \
84+
--component "images=tp=1,dp=2,rank_offset=2" \
85+
--scalable-dp --intra-microbatch-reorder \
86+
...
87+
```
88+
89+
Overlap is refused under `CUDA_DEVICE_MAX_CONNECTIONS=1`: with a single
90+
hardware queue the prefetch thread's all-to-all and the DDP all-reduce execute
91+
in issue order, that order differs across ranks, and training deadlocks
92+
(reproduced on a 4-rank run and on a 6-rank TP=2 run). Use
93+
`--no-overlap-intra-microbatch-reorder` in that case; it issues both
94+
collectives from the training thread and completed 1500 iterations under the
95+
same setting. Megatron Bridge does not require the variable for tensor
96+
parallelism; it is a Megatron-LM sequence-parallel performance setting, so TP
97+
users choose between that setting with the non-overlapped reorder, or overlap
98+
with the variable unset.
99+
100+
## Stable Constraints
101+
102+
| Configuration | Status |
103+
|---|---|
104+
| Heterogeneous module data parallelism (for example vision dp 1 or 2 with language dp 2 or 4) | Supported. Balancing uses the canonical grid; a dp-1 module reorders locally. The encoder data-parallel size must not exceed the language data-parallel size: Megatron Core's bridge fan-in with variable per-sample image-token counts is not implemented and asserts on the first forward step. |
105+
| Language pipeline parallelism > 1 | Supported. Every language stage receives `input_ids` so all stages compute the same assignment. |
106+
| Encoder pipeline parallelism > 1 | Not supported; the example raises before training starts. |
107+
| `--pad-to-seq-length false` (variable batch width) | Not supported with reordering; the example raises before training starts. Exchanged samples are concatenated back into one micro-batch, so all ranks must pad to the same length. |
108+
| `micro_batch_size` not divisible by the canonical grid size | Rejected by the canonical loader. |
109+
| `drop_last=False`, or a dataloader type other than `single` / `cyclic` | Rejected by scalable data parallelism. |
110+
| Expert tensor parallelism > 1 | Rejected by scalable data parallelism. |
111+
| Tensor parallelism > 1 | Validated (language tp 2 with and without vision tp 2). |
112+
| Context parallelism > 1 | Untested. |
113+
114+
A full MIMO run with an exchanging module needs at least three GPUs, which
115+
exceeds the two-GPU functional-test budget. CI covers the balancing and
116+
serialization logic with CPU-emulated unit tests, and the real Gloo/NCCL
117+
exchange, including the overlapped prefetch thread, with a two-rank functional
118+
test; full MIMO runs are validated manually.
119+
120+
## Expected Metric Changes
121+
122+
| Metric | Expected Change | Conditions | Evidence |
123+
|---|---|---|---|
124+
| per-rank vision load spread | down | module data-parallel size 2 or more, varied image sizes | measured |
125+
| `step_time` | flat to slightly down | every module has data-parallel size 2 or more, overlap on | measured |
126+
| `step_time` | slightly up | a module has data-parallel size 1 (its local permutation is synchronous) | measured |
127+
| `step_time` | up | overlap off (`--no-overlap-intra-microbatch-reorder`) | measured |
128+
| `peak_memory` | up by up to two extra windows of micro-batches per data rank | overlap on; grows with `megatron_mimo_reorder_window_size` | expected |
129+
130+
Balancing cannot remove the imbalance entirely: every rank keeps the same
131+
number of samples, so with few samples per rank and a bimodal image-size
132+
distribution the reachable spread is bounded by the largest single image.
133+
134+
## Representative Validation Patterns
135+
136+
On one 8×A100 node with Qwen3.5-0.8B on cord-v2 (sequence length 2048,
137+
bf16), language dp 4 with vision dp 4 at micro-batch 8 cut the mean
138+
max-minus-min image-token spread across vision ranks from `643` to `421` and
139+
lowered iteration time by about 2% with overlap on. A 3-rank run with vision
140+
dp 1 was about 5% slower with overlap and 10% slower without, because the dp-1
141+
module's permutation is synchronous. In the dp 4 run the per-micro-batch sum
142+
of image tokens over all vision ranks matched the unreordered run for all 40
143+
micro-batches. On language dp 2 + vision dp 2 the reorder off/on pair had
144+
identical iteration-1 and iteration-2 losses, checkpoint resume reproduced the
145+
continuous run, and an overlapped run completed 3000 iterations. Under
146+
`CUDA_DEVICE_MAX_CONNECTIONS=1` without overlap, language tp 2 dp 2 + vision
147+
dp 2 completed 10000 iterations; language tp 2, pp 2, and tp 2 with vision tp 2
148+
each completed 1000 iterations. Treat these as patterns to reproduce on the
149+
target model and data, not as fixed gains.
150+
151+
## Feature Interactions
152+
153+
- **In-batch sequence packing** (`enable_in_batch_packing`): reordering runs in
154+
the data iterator, before the forward step packs each language shard, so the
155+
two compose. Packing drops padding, so the exchange transfers padded rows but
156+
model compute is unaffected.
157+
- **Checkpoint resume**: reordering changes which rank holds a sample, not how
158+
many samples are consumed, so the resume arithmetic of the canonical sampler
159+
is unchanged.
160+
- **Cost weights and the language term**: setting
161+
`megatron_mimo_reorder_language_cost_weight > 0` counts real tokens from
162+
`attention_mask`; the example ships `attention_mask` on every stage for this
163+
purpose and the forward step drops it before the model.
164+
165+
## Related Docs
166+
167+
- [Packed Sequences](packed-sequences.md)
168+
- Example: `examples/megatron_mimo/qwen35_vl/finetune_qwen35_vl.py`

‎examples/megatron_mimo/qwen35_vl/finetune_qwen35_vl.py‎

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
Conversation examples are built with the standard HF VLM provider, then the
1212
resulting Qwen batch is adapted into the MIMO forward shape:
1313
14-
- language inputs: ``input_ids``, MRoPE ``position_ids``, labels, loss mask, and
15-
(when packing) the tokenizer's ``attention_mask``
14+
- language inputs: ``input_ids``, MRoPE ``position_ids``, labels, loss mask, and (when packing
15+
or with a reorder language cost term) the tokenizer's ``attention_mask``
1616
- image inputs: ``modality_inputs["images"]["qwen_visual"]``
1717
1818
Example 2-GPU smoke:
@@ -29,6 +29,7 @@
2929
from __future__ import annotations
3030

3131
import argparse
32+
import functools
3233
import logging
3334
import math
3435
import os
@@ -58,8 +59,8 @@
5859
chat_template_kwargs_from_example,
5960
)
6061
from megatron.bridge.data.datasets.utils import IGNORE_INDEX
61-
from megatron.bridge.data.megatron_mimo.canonical_sampler import build_canonical_mimo_data_loader
62-
from megatron.bridge.data.megatron_mimo.dp_utils import get_megatron_mimo_sampling_info
62+
from megatron.bridge.data.megatron_mimo.canonical_sampler import build_canonical_mimo_data_loader, canonical_grid_size
63+
from megatron.bridge.data.megatron_mimo.dp_utils import _find_rank_module, get_megatron_mimo_sampling_info
6364
from megatron.bridge.data.samplers import build_pretraining_data_loader
6465
from megatron.bridge.data.sources.hf import hf_dataset_supports_split
6566
from megatron.bridge.data.token_utils import extract_skipped_token_ids
@@ -284,26 +285,34 @@ def _batch_spec_for_rank(cfg: Any) -> MIMOBatchSpec:
284285

285286
dataset_cfg = getattr(cfg, "dataset", None)
286287
packing_active = bool(getattr(dataset_cfg, "enable_in_batch_packing", False))
288+
reorder_active = bool(
289+
getattr(dataset_cfg, "megatron_mimo_scalable_dp", False)
290+
and getattr(dataset_cfg, "megatron_mimo_intra_microbatch_reorder", False)
291+
)
292+
language_cost_active = reorder_active and (
293+
getattr(dataset_cfg, "megatron_mimo_reorder_language_cost_weight", 0.0) > 0
294+
)
287295

288296
if module_name == MIMO_LANGUAGE_MODULE_KEY:
289297
return MIMOBatchSpec(
290-
input_ids=is_first_pp,
298+
input_ids=is_first_pp or reorder_active,
291299
# Qwen3.5-VL mRoPE needs position_ids on every language PP stage.
292300
position_ids=True,
293301
labels=is_last_pp,
294302
loss_mask=is_last_pp,
295303
modality_inputs=False,
296304
# Packing length source; the packer nulls it again before the model.
297-
attention_mask=packing_active,
305+
attention_mask=packing_active or language_cost_active,
298306
)
299307

300308
return MIMOBatchSpec(
301309
# Encoder first stages need input_ids to attach per-sample split metadata.
302-
input_ids=is_first_pp,
310+
input_ids=is_first_pp or reorder_active,
303311
position_ids=False,
304312
labels=False,
305313
loss_mask=False,
306314
modality_inputs=is_first_pp,
315+
attention_mask=language_cost_active,
307316
)
308317

309318

@@ -340,6 +349,22 @@ def _validate_mimo_batch_sizes(
340349
f"--micro-batch-size ({args.micro_batch_size})."
341350
)
342351

352+
if args.intra_microbatch_reorder and not args.pad_to_seq_length:
353+
raise ValueError(
354+
"--intra-microbatch-reorder requires --pad-to-seq-length true (exchanged samples must share one "
355+
"sequence length)."
356+
)
357+
if (
358+
args.intra_microbatch_reorder
359+
and not args.no_overlap_intra_microbatch_reorder
360+
and os.environ.get("CUDA_DEVICE_MAX_CONNECTIONS") == "1"
361+
):
362+
raise ValueError(
363+
"--intra-microbatch-reorder with overlap deadlocks under CUDA_DEVICE_MAX_CONNECTIONS=1 (single "
364+
"hardware queue serializes the prefetch all-to-all and the DDP all-reduce in rank-dependent "
365+
"order). Add --no-overlap-intra-microbatch-reorder or unset the variable."
366+
)
367+
343368
summaries = []
344369
for name, parallelism in parallelism_config.module_parallelisms.items():
345370
dp = parallelism.data_parallel_size
@@ -418,6 +443,11 @@ def _build_dataset_config(args: argparse.Namespace) -> DirectHFSFTDatasetConfig:
418443
enable_in_batch_packing=args.pack_sequences_in_batch,
419444
defer_in_batch_packing_to_step=True,
420445
megatron_mimo_scalable_dp=args.scalable_dp,
446+
megatron_mimo_intra_microbatch_reorder=args.intra_microbatch_reorder,
447+
megatron_mimo_reorder_encoder_cost_weight=args.reorder_encoder_cost_weight,
448+
megatron_mimo_reorder_language_cost_weight=args.reorder_language_cost_weight,
449+
megatron_mimo_reorder_overlap=not args.no_overlap_intra_microbatch_reorder,
450+
megatron_mimo_reorder_window_size=args.reorder_window_size,
421451
do_validation=do_validation,
422452
do_test=False,
423453
trust_remote_code=args.trust_remote_code,
@@ -953,6 +983,55 @@ def _build_data_iterators(cfg, _megatron_mimo_infra, *, train_state=None):
953983
# `pretrain_megatron_mimo` calls `next(data_iterator)` per microbatch, so
954984
# return an iterator (DataLoader is iterable but not itself an iterator).
955985
loader_iter: Iterator[dict[str, Any]] = iter(train_loader)
986+
987+
# Gate on the canonical grid so every rank (dp=1 included) takes the same branch.
988+
reorder_on = (
989+
scalable_dp
990+
and bool(getattr(cfg.dataset, "megatron_mimo_intra_microbatch_reorder", False))
991+
and canonical_grid_size(module_dps) > 1
992+
)
993+
if reorder_on:
994+
from megatron.bridge.data.megatron_mimo.reorder_buffer import (
995+
ReorderingBuffer,
996+
build_module_dp_process_groups,
997+
sample_cost,
998+
)
999+
1000+
for _name, _par in cfg.model.megatron_mimo_parallelism_config.module_parallelisms.items():
1001+
if _name != MIMO_LANGUAGE_MODULE_KEY and _par.pipeline_model_parallel_size > 1:
1002+
raise NotImplementedError(
1003+
"MegatronMIMO intra-microbatch reorder does not support encoder pipeline parallelism "
1004+
f"(module {_name!r} has PP={_par.pipeline_model_parallel_size}): non-first encoder stages "
1005+
"skip data loading, so the collective process-group creation below would hang."
1006+
)
1007+
balance_n_groups = canonical_grid_size(module_dps)
1008+
_grid, _ = _find_rank_module(cfg.model._grids)
1009+
dp_rank, dp_size, dp_group_gloo, dp_group_nccl = build_module_dp_process_groups(
1010+
_grid.get_pg(["dp"]), overlap=cfg.dataset.megatron_mimo_reorder_overlap
1011+
)
1012+
cost_of = functools.partial(
1013+
sample_cost,
1014+
encoder_cost_weight=cfg.dataset.megatron_mimo_reorder_encoder_cost_weight,
1015+
language_cost_weight=cfg.dataset.megatron_mimo_reorder_language_cost_weight,
1016+
image_token_id=spec.image_token_id,
1017+
square_merge_size=spec.square_merge_size,
1018+
)
1019+
1020+
def image_count_of(b: dict[str, Any]) -> torch.Tensor:
1021+
return (b["input_ids"] == spec.vision_start_token_id).sum(dim=1).to(torch.long)
1022+
1023+
loader_iter = ReorderingBuffer(
1024+
loader_iter,
1025+
dp_rank=dp_rank,
1026+
dp_size=dp_size,
1027+
n_groups=balance_n_groups,
1028+
cost_of=cost_of,
1029+
dp_group_gloo=dp_group_gloo,
1030+
dp_group_nccl=dp_group_nccl,
1031+
overlap=cfg.dataset.megatron_mimo_reorder_overlap,
1032+
image_count_of=image_count_of,
1033+
window_size=cfg.dataset.megatron_mimo_reorder_window_size,
1034+
)
9561035
if args.log_batches:
9571036
loader_iter = _wrap_iter_logging(loader_iter, spec)
9581037
return loader_iter, None
@@ -1264,6 +1343,32 @@ def _parse_args() -> argparse.Namespace:
12641343
"DP). Each rank processes its natural, unbalanced shard. Uses the same DP loss reduction as "
12651344
"non-scalable runs.",
12661345
)
1346+
parser.add_argument(
1347+
"--intra-microbatch-reorder",
1348+
action="store_true",
1349+
help="Intra-microbatch reordering (requires --scalable-dp): rebalance each micro-batch's per-sample "
1350+
"vision load across the module DP group by a per-sample all-to-all so no rank is the straggler.",
1351+
)
1352+
parser.add_argument(
1353+
"--reorder-encoder-cost-weight",
1354+
type=float,
1355+
default=1.0,
1356+
help="Reorder cost weight per image patch.",
1357+
)
1358+
parser.add_argument(
1359+
"--reorder-language-cost-weight",
1360+
type=float,
1361+
default=0.0,
1362+
help="Reorder cost weight per real token (image tokens included); 0 = encoder-only.",
1363+
)
1364+
parser.add_argument(
1365+
"--no-overlap-intra-microbatch-reorder",
1366+
action="store_true",
1367+
help="Run the reorder exchange synchronously instead of prefetching the next window on a side thread.",
1368+
)
1369+
parser.add_argument(
1370+
"--reorder-window-size", type=int, default=1, help="Micro-batches exchanged together as one window."
1371+
)
12671372
parser.add_argument("--profile", choices=("none", "nsys", "pytorch"), default="none")
12681373
parser.add_argument("--profile-step-start", type=int, default=1)
12691374
parser.add_argument("--profile-step-end", type=int, default=2)

0 commit comments

Comments
 (0)