Skip to content

Commit 30280ca

Browse files
going-songterryoosailor1493
committed
feat(data): scalable canonical-grid reads for MegatronMIMO data loading
By default every MegatronMIMO data-loading rank reads the full global micro-batch and forward_step slices out its module-DP shard, so per-rank read and preprocessing cost scales with the DP degree. This adds an opt-in dataset switch, `megatron_mimo_scalable_dp`, that shards reads at the sampler instead and skips the forward-step slice. Reads are sharded on a canonical grid: the LCM of the module DP sizes. Each rank covers `grid // dp` consecutive canonical groups and concatenates one flat Megatron sampler per group, all built with the same `(micro_batch // grid, grid)` geometry and the global consumed_samples. Group streams depend only on module-independent inputs, so every module materializes the identical ordered global micro-batch under both "single" and the default "cyclic" sampler, keeping the BridgeCommunicator's positional batch-dim routing aligned. (Sharding by each module's own DP - the original mechanism - only aligns under "single"; "cyclic" became the direct-HF-SFT default in #5048.) Group geometries are truncated to whole global micro-batches so ragged dataset sizes cannot desynchronize the groups' window counts. Split (2/3) of #4608 as agreed in #4609. Original work by Chanwoo Park. Validated: unit tests for cross-geometry alignment, resume across epoch boundaries, and non-multiple dataset sizes; live PP=2 runs (language tp1/pp2/dp2 + images dp1): cyclic+scalable completes where module-DP sharding crashed on embedding-count mismatch, single scalable matches non-scalable losses within bf16 noise, and the default path matches prior losses within run-to-run noise. 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 96db955 commit 30280ca

11 files changed

Lines changed: 785 additions & 44 deletions

File tree

examples/megatron_mimo/qwen35_vl/finetune_qwen35_vl.py

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
chat_template_kwargs_from_example,
5959
)
6060
from megatron.bridge.data.datasets.utils import IGNORE_INDEX
61+
from megatron.bridge.data.megatron_mimo.canonical_sampler import build_canonical_mimo_data_loader
6162
from megatron.bridge.data.megatron_mimo.dp_utils import get_megatron_mimo_sampling_info
6263
from megatron.bridge.data.samplers import build_pretraining_data_loader
6364
from megatron.bridge.data.sources.hf import hf_dataset_supports_split
@@ -416,6 +417,7 @@ def _build_dataset_config(args: argparse.Namespace) -> DirectHFSFTDatasetConfig:
416417
# MegatronMIMO packs in the step, after the module-DP slice (deferred packing).
417418
enable_in_batch_packing=args.pack_sequences_in_batch,
418419
defer_in_batch_packing_to_step=True,
420+
megatron_mimo_scalable_dp=args.scalable_dp,
419421
do_validation=do_validation,
420422
do_test=False,
421423
trust_remote_code=args.trust_remote_code,
@@ -857,9 +859,11 @@ def _build_data_iterators(cfg, _megatron_mimo_infra, *, train_state=None):
857859
if cfg.model._grids is None:
858860
raise ValueError("MegatronMIMOProvider._grids is None. Model must be built before data iterators.")
859861

862+
scalable_dp = bool(getattr(cfg.dataset, "megatron_mimo_scalable_dp", False))
860863
sampler_dp_rank, sampler_dp_size, needs_data = get_megatron_mimo_sampling_info(
861864
cfg.model.megatron_mimo_parallelism_config,
862865
cfg.model._grids,
866+
scalable_dp=scalable_dp,
863867
)
864868
if not needs_data:
865869
return None, None
@@ -909,20 +913,42 @@ def _build_data_iterators(cfg, _megatron_mimo_infra, *, train_state=None):
909913
batch_spec=batch_spec,
910914
)
911915

912-
train_loader = build_pretraining_data_loader(
913-
dataset=train_ds,
914-
consumed_samples=train_state.consumed_train_samples,
915-
dataloader_type=cfg.dataset.dataloader_type,
916-
micro_batch_size=cfg.train.micro_batch_size,
917-
num_workers=cfg.dataset.num_workers,
918-
data_sharding=cfg.dataset.data_sharding,
919-
collate_fn=collate_fn,
920-
pin_memory=cfg.dataset.pin_memory,
921-
persistent_workers=cfg.dataset.persistent_workers,
922-
data_parallel_rank=sampler_dp_rank,
923-
data_parallel_size=sampler_dp_size,
924-
drop_last=cfg.dataset.drop_last,
925-
)
916+
if scalable_dp:
917+
# Shard reads on the canonical grid (LCM of the module DP sizes) so every
918+
# module materializes the same ordered global micro-batch under any sampler.
919+
module_dps = [
920+
p.data_parallel_size for p in cfg.model.megatron_mimo_parallelism_config.module_parallelisms.values()
921+
]
922+
train_loader = build_canonical_mimo_data_loader(
923+
train_ds,
924+
consumed_samples=train_state.consumed_train_samples,
925+
dataloader_type=cfg.dataset.dataloader_type,
926+
micro_batch_size=cfg.train.micro_batch_size,
927+
module_dp_sizes=module_dps,
928+
dp_rank=sampler_dp_rank,
929+
dp_size=sampler_dp_size,
930+
data_sharding=cfg.dataset.data_sharding,
931+
drop_last=cfg.dataset.drop_last,
932+
num_workers=cfg.dataset.num_workers,
933+
pin_memory=cfg.dataset.pin_memory,
934+
collate_fn=collate_fn,
935+
persistent_workers=cfg.dataset.persistent_workers,
936+
)
937+
else:
938+
train_loader = build_pretraining_data_loader(
939+
dataset=train_ds,
940+
consumed_samples=train_state.consumed_train_samples,
941+
dataloader_type=cfg.dataset.dataloader_type,
942+
micro_batch_size=cfg.train.micro_batch_size,
943+
num_workers=cfg.dataset.num_workers,
944+
data_sharding=cfg.dataset.data_sharding,
945+
collate_fn=collate_fn,
946+
pin_memory=cfg.dataset.pin_memory,
947+
persistent_workers=cfg.dataset.persistent_workers,
948+
data_parallel_rank=sampler_dp_rank,
949+
data_parallel_size=sampler_dp_size,
950+
drop_last=cfg.dataset.drop_last,
951+
)
926952

927953
# `pretrain_megatron_mimo` calls `next(data_iterator)` per microbatch, so
928954
# return an iterator (DataLoader is iterable but not itself an iterator).
@@ -1230,6 +1256,14 @@ def _parse_args() -> argparse.Namespace:
12301256
"tokens into one [1, T] THD sequence so the language model skips padding compute "
12311257
"(block-diagonal attention comes from cu_seqlens).",
12321258
)
1259+
parser.add_argument(
1260+
"--scalable-dp",
1261+
action="store_true",
1262+
help="Scalable data parallelism: each rank reads only its disjoint 1/dp shard of the global "
1263+
"micro-batch instead of every rank reading the full batch and slicing locally (IO scales with "
1264+
"DP). Each rank processes its natural, unbalanced shard. Uses the same DP loss reduction as "
1265+
"non-scalable runs.",
1266+
)
12331267
parser.add_argument("--profile", choices=("none", "nsys", "pytorch"), default="none")
12341268
parser.add_argument("--profile-step-start", type=int, default=1)
12351269
parser.add_argument("--profile-step-end", type=int, default=2)

src/megatron/bridge/data/builders/direct_hf_sft.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class DirectHFSFTDatasetConfig(DataloaderConfig):
9393
pad_to_max_length: bool = False
9494
pad_to_multiple_of: int = 128
9595
in_batch_packing_pad_to_multiple_of: int = 1
96+
megatron_mimo_scalable_dp: bool = False
9697

9798
def validate(self) -> None:
9899
"""Validate declarative source and dataset settings."""
@@ -112,6 +113,18 @@ def validate(self) -> None:
112113
self.test_source.validate()
113114
if self.hf_processor_path is not None and not self.hf_processor_path.strip():
114115
raise ValueError("hf_processor_path must be a non-empty string when set.")
116+
if self.megatron_mimo_scalable_dp:
117+
if self.dataloader_type not in ("single", "cyclic"):
118+
raise ValueError(
119+
"megatron_mimo_scalable_dp requires dataloader_type 'single' or 'cyclic' "
120+
f"(got {self.dataloader_type!r}); other samplers have no cross-module-consistent "
121+
"shard assignment."
122+
)
123+
if not self.drop_last:
124+
raise ValueError(
125+
"megatron_mimo_scalable_dp requires drop_last=True; a partial final micro-batch "
126+
"gives modules unequal shares and misaligns the modality routing."
127+
)
115128
validate_declarative_mapping(self.hf_processor_kwargs, field_name="hf_processor_kwargs")
116129
if self.hf_processor_kwargs is not None and "trust_remote_code" in self.hf_processor_kwargs:
117130
raise ValueError(
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Canonical-grid batch sampling for MegatronMIMO scalable data-parallel reads.
16+
17+
With ``megatron_mimo_scalable_dp`` every module's loaders must materialize the same
18+
ordered global micro-batch, because the ``BridgeCommunicator`` routes modality
19+
embeddings to language ranks by contiguous position along the batch dim. A sampler's
20+
shard assignment depends on its ``(data_parallel_rank, data_parallel_size,
21+
micro_batch_size)``, so modules with different DP sizes cannot shard by their own DP:
22+
a shuffling sampler would hand them different sample sets. Instead, all loaders shard
23+
on one shared **canonical grid** — the least common multiple of the module DP sizes.
24+
A rank whose module has DP size ``d`` covers ``grid // d`` consecutive canonical
25+
groups and concatenates their windows, so concatenating any module's rank batches
26+
reproduces the identical ordered global micro-batch under any deterministic sampler
27+
(``single`` and ``cyclic`` included; the shuffle seed derives from the epoch alone).
28+
"""
29+
30+
from __future__ import annotations
31+
32+
import math
33+
from typing import Callable, Iterator
34+
35+
from torch.utils.data import DataLoader, Dataset
36+
37+
from megatron.bridge.data.samplers import MegatronPretrainingRandomSampler, MegatronPretrainingSampler
38+
39+
40+
def canonical_grid_size(module_dp_sizes: list[int]) -> int:
41+
"""Return the shared sampler grid size: the LCM of every module's DP size."""
42+
if not module_dp_sizes or any(dp is None or dp < 1 for dp in module_dp_sizes):
43+
raise ValueError(f"module DP sizes must be positive integers (got {module_dp_sizes}).")
44+
return math.lcm(*module_dp_sizes)
45+
46+
47+
def covered_canonical_groups(dp_rank: int, dp_size: int, grid_size: int) -> list[int]:
48+
"""Return the canonical groups this rank reads.
49+
50+
The groups are the ``grid_size // dp_size`` consecutive slots matching the
51+
contiguous batch-dim chunk the ``BridgeCommunicator`` routes to this DP rank.
52+
"""
53+
if grid_size % dp_size != 0:
54+
raise ValueError(
55+
f"canonical grid size ({grid_size}) is not divisible by the module DP size ({dp_size}); "
56+
"module DP sizes must divide their least common multiple."
57+
)
58+
span = grid_size // dp_size
59+
return list(range(dp_rank * span, (dp_rank + 1) * span))
60+
61+
62+
class CanonicalGroupBatchSampler:
63+
"""Concatenate per-canonical-group Megatron samplers into one batch sampler.
64+
65+
Holds one flat sampler per covered canonical group, all built with the same
66+
``(micro_batch // grid, grid)`` geometry and the same global ``consumed_samples``.
67+
Each yield emits the groups' current windows concatenated in group order. Group
68+
streams are functions of ``(group, grid, micro_batch, consumed, epoch seed)`` only,
69+
so every rank covering group ``g`` — in any module — sees the identical stream.
70+
"""
71+
72+
def __init__(self, samplers: list) -> None:
73+
if not samplers:
74+
raise ValueError("CanonicalGroupBatchSampler needs at least one group sampler.")
75+
self.samplers = samplers
76+
77+
def __len__(self) -> int:
78+
"""Match the flat Megatron samplers' convention of reporting total samples."""
79+
return min(len(sampler) for sampler in self.samplers)
80+
81+
def __iter__(self) -> Iterator[list[int]]:
82+
"""Yield one concatenated index window per micro-batch."""
83+
iterators = [iter(sampler) for sampler in self.samplers]
84+
while True:
85+
window: list[int] = []
86+
for iterator in iterators:
87+
group_batch = next(iterator, None)
88+
if group_batch is None:
89+
# Groups share one truncated geometry, so they exhaust on the same window.
90+
return
91+
window.extend(group_batch)
92+
yield window
93+
94+
95+
def build_canonical_group_batch_sampler(
96+
*,
97+
dataloader_type: str,
98+
dataset: Dataset,
99+
consumed_samples: int,
100+
micro_batch_size: int,
101+
grid_size: int,
102+
groups: list[int],
103+
data_sharding: bool,
104+
drop_last: bool = True,
105+
) -> CanonicalGroupBatchSampler:
106+
"""Build this rank's canonical-group batch sampler for scalable MIMO reads.
107+
108+
Args:
109+
dataloader_type: ``"single"`` or ``"cyclic"``; other types have no shard
110+
assignment that is consistent across modules and are rejected.
111+
dataset: The dataset the loader reads.
112+
consumed_samples: Global consumed-sample count, exactly as the flat samplers
113+
expect (used for resume / epoch derivation).
114+
micro_batch_size: The global micro-batch size (not the per-rank share).
115+
grid_size: Canonical grid size from :func:`canonical_grid_size`.
116+
groups: This rank's groups from :func:`covered_canonical_groups`.
117+
data_sharding: Passed through to the cyclic sampler.
118+
drop_last: Must stay ``True``: a partial final window gives the groups
119+
unequal shares and breaks the positional routing.
120+
121+
Returns:
122+
The merged batch sampler for ``torch.utils.data.DataLoader(batch_sampler=...)``.
123+
"""
124+
if not drop_last:
125+
raise ValueError("megatron_mimo_scalable_dp requires drop_last=True (partial windows misalign modules).")
126+
if micro_batch_size % grid_size != 0:
127+
raise ValueError(
128+
f"micro_batch_size ({micro_batch_size}) must be divisible by the canonical grid size ({grid_size})."
129+
)
130+
group_micro_batch_size = micro_batch_size // grid_size
131+
# Truncate to whole global micro-batches: the flat samplers round their active range at
132+
# per-group granularity, which diverges per group for a non-multiple dataset size (the
133+
# cyclic data_sharding=False stride would give groups unequal window counts).
134+
total_samples = (len(dataset) // micro_batch_size) * micro_batch_size
135+
if total_samples <= 0:
136+
raise ValueError(f"dataset ({len(dataset)} samples) is smaller than one micro-batch ({micro_batch_size}).")
137+
138+
samplers = []
139+
for group in groups:
140+
if dataloader_type == "single":
141+
samplers.append(
142+
MegatronPretrainingSampler(
143+
total_samples=total_samples,
144+
consumed_samples=consumed_samples,
145+
micro_batch_size=group_micro_batch_size,
146+
data_parallel_rank=group,
147+
data_parallel_size=grid_size,
148+
drop_last=True,
149+
)
150+
)
151+
elif dataloader_type == "cyclic":
152+
samplers.append(
153+
MegatronPretrainingRandomSampler(
154+
dataset,
155+
total_samples=total_samples,
156+
consumed_samples=consumed_samples,
157+
micro_batch_size=group_micro_batch_size,
158+
data_parallel_rank=group,
159+
data_parallel_size=grid_size,
160+
data_sharding=data_sharding,
161+
)
162+
)
163+
else:
164+
raise ValueError(
165+
f"megatron_mimo_scalable_dp supports dataloader_type 'single' or 'cyclic' (got {dataloader_type!r})."
166+
)
167+
return CanonicalGroupBatchSampler(samplers)
168+
169+
170+
def build_canonical_mimo_data_loader(
171+
dataset: Dataset | None,
172+
*,
173+
consumed_samples: int,
174+
dataloader_type: str,
175+
micro_batch_size: int,
176+
module_dp_sizes: list[int],
177+
dp_rank: int,
178+
dp_size: int,
179+
data_sharding: bool,
180+
drop_last: bool,
181+
num_workers: int,
182+
pin_memory: bool,
183+
collate_fn: Callable | None,
184+
persistent_workers: bool,
185+
) -> DataLoader | None:
186+
"""Build this rank's read-sharded DataLoader for scalable MegatronMIMO reads.
187+
188+
Computes the canonical grid from ``module_dp_sizes``, derives this rank's covered
189+
groups from its module-local ``(dp_rank, dp_size)``, and wraps the merged batch
190+
sampler in a ``DataLoader``. Returns ``None`` when ``dataset`` is ``None``.
191+
"""
192+
if dataset is None:
193+
return None
194+
grid = canonical_grid_size(module_dp_sizes)
195+
groups = covered_canonical_groups(dp_rank, dp_size, grid)
196+
batch_sampler = build_canonical_group_batch_sampler(
197+
dataloader_type=dataloader_type,
198+
dataset=dataset,
199+
consumed_samples=consumed_samples,
200+
micro_batch_size=micro_batch_size,
201+
grid_size=grid,
202+
groups=groups,
203+
data_sharding=data_sharding,
204+
drop_last=drop_last,
205+
)
206+
return DataLoader(
207+
dataset,
208+
batch_sampler=batch_sampler,
209+
num_workers=num_workers,
210+
pin_memory=pin_memory,
211+
collate_fn=collate_fn,
212+
persistent_workers=persistent_workers,
213+
)

0 commit comments

Comments
 (0)