Skip to content

Commit e16f918

Browse files
committed
refactor(data): limit GPT SFT packing to microbatch loaders
Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
1 parent 8a3e754 commit e16f918

13 files changed

Lines changed: 66 additions & 304 deletions

File tree

‎docs/training/packed-sequences.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ schemas: prompt/completion (`GPTSFTDataset`) and chat
4646
read, parse, and tokenize only the rows selected for the current logical
4747
microbatch. Collation then concatenates those rows into one physical THD batch
4848
row; it does not materialize an offline dataset or load the full source into
49-
RAM. With GPT-SFT's default global-batch sampler, Bridge first divides the
50-
global batch into logical microbatches and packs each one independently.
49+
RAM. Use a microbatch-yielding `single` or `cyclic` dataloader; GPT-SFT
50+
in-batch packing does not support the global-batch `batch` dataloader.
5151

5252
## When to Use It
5353

@@ -120,14 +120,16 @@ config and use a logical micro-batch larger than one:
120120

121121
```text
122122
dataset.enable_in_batch_packing=true
123+
dataset.dataloader_type=single
123124
train.micro_batch_size=4
124125
```
125126

126127
The collator preserves each sample's prompt/completion or chat loss mask and
127128
emits current MCore packed metadata (`cu_seqlens_q`, `cu_seqlens_kv`, and the
128129
corresponding padded boundaries when CP/SP alignment is required). The model
129130
sees one physical THD row. `enable_in_batch_packing` and
130-
`enable_offline_packing` are mutually exclusive.
131+
`enable_offline_packing` are mutually exclusive. The `batch` dataloader is not
132+
supported for GPT-SFT in-batch packing; use `single` or `cyclic`.
131133

132134
## Stable Constraints
133135

@@ -136,6 +138,8 @@ The durable constraints for packed sequences in Bridge are:
136138
- offline packed SFT requires configured `micro_batch_size == 1`
137139
- GPT-SFT/Direct-HF/VLM in-batch packing requires configured `micro_batch_size > 1`;
138140
collation flattens those input rows into one physical THD batch row
141+
- GPT-SFT in-batch packing requires `dataloader_type="single"` or `"cyclic"`;
142+
the global-batch `"batch"` dataloader is not supported
139143
- Energon online packing currently supports the eager Qwen-VL collator path,
140144
requires physical `micro_batch_size == 1`, the generic `vlm_step`, per-token loss, and
141145
`ddp.average_in_collective=False`

‎scripts/training/README.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,15 @@ host memory. For example:
208208
--dataset local-jsonl \
209209
dataset.dataset_root=/data/sft \
210210
dataset.enable_in_batch_packing=true \
211+
dataset.dataloader_type=single \
211212
train.micro_batch_size=4
212213
```
213214

214215
The logical micro-batch must be larger than one. Collation emits one physical
215216
THD row and derives per-sequence CP/SP alignment from the resolved topology.
216-
Do not combine `dataset.enable_in_batch_packing=true` with offline packing.
217+
Use the `single` or `cyclic` dataloader; the global-batch `batch` dataloader is
218+
not supported. Do not combine `dataset.enable_in_batch_packing=true` with
219+
offline packing.
217220

218221
### VLM data
219222

‎skills/nemo-mbridge-perf-sequence-packing/SKILL.md‎

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,16 @@ In-batch packing for GPT SFT and supported VLM finetuning:
108108

109109
```python
110110
cfg.dataset.enable_in_batch_packing = True
111+
cfg.dataset.dataloader_type = "single"
111112
cfg.train.micro_batch_size = 4
112113
```
113114

114115
For local or materialized GPT-SFT JSONL, this keeps the existing mmap-backed
115116
dataset and performs tokenization lazily. Both prompt/completion
116117
(`GPTSFTDataset`) and chat (`GPTSFTChatDataset`) preserve their loss-mask
117-
semantics. With the default `dataloader_type="batch"`, the DataLoader first
118-
divides each per-DP-rank global batch into logical microbatches and packs each
119-
logical microbatch into its own physical THD row.
118+
semantics. Use `dataloader_type="single"` or `"cyclic"` so every DataLoader
119+
yield is one logical microbatch; GPT-SFT in-batch packing does not support the
120+
global-batch `"batch"` dataloader.
120121

121122
Energon online packing for Qwen-VL uses Energon's per-worker candidate buffer
122123
instead of limiting selection to one collator micro batch:
@@ -239,23 +240,14 @@ def prepare_padded_or_packed_sequence_batch(
239240
return
240241
```
241242

242-
GPT-SFT direct-row packing and global-batch microbatch preservation:
243+
GPT-SFT direct-row packing:
243244

244245
```627:671:src/megatron/bridge/data/datasets/gpt_sft.py
245246
def _collate_in_batch(self, batch):
246247
...
247248
return build_mcore_thd_sequence_batch_from_rows(...)
248249
```
249250

250-
```18:57:src/megatron/bridge/data/batch_utils.py
251-
def collate_finetuning_microbatches(samples, *, micro_batch_size, collate_fn):
252-
...
253-
```
254-
255-
Do not flatten a GPT-SFT `batch`-sampler global batch into one THD row. The
256-
training schedule still consumes the configured number of logical
257-
microbatches; each one must retain independent packed metadata.
258-
259251
Packed THD runtime constraint:
260252

261253
```94:108:src/megatron/bridge/training/gpt_step.py
@@ -275,17 +267,18 @@ if cu_seqlens.dim() > 1 and cu_seqlens.size(0) != 1:
275267
## Pitfalls
276268

277269
1. Offline packed SFT, runtime in-batch packing, and Energon online packing are different features. Offline and Energon packing use physical MBS1; runtime in-batch packing uses MBS greater than one.
278-
2. When CP is enabled, packed sequence lengths must respect `2 * context_parallel_size` divisibility.
279-
3. For finetuning with CP, `calculate_per_token_loss=True` and `ddp.average_in_collective=False` are required.
280-
4. `pad_cu_seqlens=True` also requires `pad_to_max_length=True`.
281-
5. Packing support is model-family-specific. `Qwen3-Next`, `GLM-4.5`, and `Qwen3.5-VL` contain explicit opt-outs in different paths.
282-
6. MTP finetuning is documented as incompatible with packed sequences.
283-
7. Synthetic padding rows, including negative indices remapped through `samples_mapping`, must retain an all-zero loss mask.
284-
8. `global_batch_size` must be divisible by and no smaller than data parallel size when offline packing uses MBS1.
285-
9. Derive `pad_seq_to_mult` from CP/TP/SP for both SFT and PEFT; do not hardcode different values by workload type.
286-
10. `pad_to_max_length` controls final pack width and is conditional on fixed-shape execution requirements.
287-
11. Energon `packing_buffer_size` is per worker and also affects validation; global/eval batch counts refer to physical packs rather than source conversations.
288-
12. Exact Energon loader resume requires unchanged shards/splits, DP world size, worker counts, shuffle settings/seed, processor, sequence length, topology, and packing-buffer size.
270+
2. GPT-SFT in-batch packing requires `dataloader_type="single"` or `"cyclic"`; it does not support `"batch"`.
271+
3. When CP is enabled, packed sequence lengths must respect `2 * context_parallel_size` divisibility.
272+
4. For finetuning with CP, `calculate_per_token_loss=True` and `ddp.average_in_collective=False` are required.
273+
5. `pad_cu_seqlens=True` also requires `pad_to_max_length=True`.
274+
6. Packing support is model-family-specific. `Qwen3-Next`, `GLM-4.5`, and `Qwen3.5-VL` contain explicit opt-outs in different paths.
275+
7. MTP finetuning is documented as incompatible with packed sequences.
276+
8. Synthetic padding rows, including negative indices remapped through `samples_mapping`, must retain an all-zero loss mask.
277+
9. `global_batch_size` must be divisible by and no smaller than data parallel size when offline packing uses MBS1.
278+
10. Derive `pad_seq_to_mult` from CP/TP/SP for both SFT and PEFT; do not hardcode different values by workload type.
279+
11. `pad_to_max_length` controls final pack width and is conditional on fixed-shape execution requirements.
280+
12. Energon `packing_buffer_size` is per worker and also affects validation; global/eval batch counts refer to physical packs rather than source conversations.
281+
13. Exact Energon loader resume requires unchanged shards/splits, DP world size, worker counts, shuffle settings/seed, processor, sequence length, topology, and packing-buffer size.
289282

290283
## Verification
291284

@@ -295,9 +288,8 @@ Use the checked-in unit coverage:
295288
uv run python -m pytest tests/unit_tests/training/utils/test_packed_seq_utils.py -v && \
296289
uv run python -m pytest tests/unit_tests/training/test_config.py -k "packed_sequence or enable_in_batch_packing or offline_and_in_batch_packing_are_mutually_exclusive or context_parallel_seq_length_divisibility or context_parallel_finetuning_validations" -v && \
297290
uv run python -m pytest tests/unit_tests/data/packing/test_in_batch.py -v && \
298-
uv run python -m pytest tests/unit_tests/data/datasets/test_gpt_sft.py -k "in_batch_packing or batch_dataloader" -v && \
291+
uv run python -m pytest tests/unit_tests/data/datasets/test_gpt_sft.py -k "in_batch_packing" -v && \
299292
uv run python -m pytest tests/unit_tests/data/builders/test_gpt_sft_config.py -v && \
300-
uv run python -m pytest tests/unit_tests/data/test_batch_utils.py -v && \
301293
uv run python -m pytest tests/unit_tests/training/test_vlm_step.py -k "deferred_in_batch_packing or packed_metadata" -v && \
302294
uv run python -m pytest tests/unit_tests/models/qwen_vl/data/test_energon.py tests/unit_tests/data/builders/test_energon_builder.py -v && \
303295
uv run python -m pytest tests/unit_tests/tutorials/test_multimodal_data_tutorials.py -k "native_packing_loader" -v && \
@@ -310,6 +302,6 @@ Success criteria:
310302
- all selected tests pass
311303
- offline and in-batch configuration validation remains mutually exclusive
312304
- packed metadata reaches the training step in MCore THD form
313-
- a GPT-SFT global batch is pre-collated as one THD row per logical microbatch
305+
- GPT-SFT in-batch packing rejects the global-batch `"batch"` dataloader
314306
- native Energon packing restores pending groups exactly and flushes finite partial buffers without dropping samples
315307
- mapped padding rows do not contribute to loss

‎skills/nemo-mbridge-perf-sequence-packing/card.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ known_constraints:
7979
- pad_seq_to_mult is derived from CP/TP/SP and follows the same rule for SFT and PEFT.
8080
- pad_to_max_length is conditional on a fixed-width dispatcher, kernel, or CUDA-graph path.
8181
- VLM in-batch packing requires micro_batch_size > 1.
82+
- GPT-SFT in-batch packing requires dataloader_type single or cyclic; batch is unsupported.
8283
- Energon online packing requires physical micro_batch_size == 1 and counts candidate samples per worker.
8384
- Energon online packing is mutually exclusive with collate-time in-batch packing and the deferred Qwen step.
8485
- Energon online packing does not support MTP, CUDA graphs, or pipeline parallelism.

‎src/megatron/bridge/data/batch_utils.py‎

Lines changed: 2 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -14,61 +14,11 @@
1414

1515
"""Batch handling utilities for finetuning data."""
1616

17-
from collections.abc import Callable
1817
from typing import Any, Iterator
1918

2019
import torch
2120

2221

23-
_PRECOLLATED_MICROBATCHES_KEY = "__precollated_microbatches__"
24-
25-
26-
def collate_finetuning_microbatches(
27-
samples: list[Any],
28-
*,
29-
micro_batch_size: int,
30-
collate_fn: Callable[[list[Any]], dict[str, Any]],
31-
) -> dict[str, list[dict[str, Any]]]:
32-
"""Collate a global batch as independently packed logical microbatches.
33-
34-
The ``batch`` finetuning sampler deliberately sends a full per-DP-rank
35-
global batch to one DataLoader collate call. In-batch packing must happen
36-
after that input is divided into logical microbatches; otherwise the whole
37-
global batch becomes one physical THD row and cannot be consumed by the
38-
configured number of pipeline microbatches.
39-
40-
Args:
41-
samples: Samples assigned to one DP rank for the current global batch.
42-
micro_batch_size: Number of logical samples to pack together.
43-
collate_fn: Dataset collator that converts one logical microbatch to a
44-
physical packed batch.
45-
46-
Returns:
47-
A pin-memory-compatible mapping containing the pre-collated
48-
microbatches.
49-
50-
Raises:
51-
ValueError: If the sample count cannot form complete microbatches or a
52-
collate result is not a mapping.
53-
"""
54-
if micro_batch_size <= 0:
55-
raise ValueError("micro_batch_size must be greater than 0.")
56-
if not samples or len(samples) % micro_batch_size != 0:
57-
raise ValueError(
58-
f"Global batch sample count {len(samples)} must be a positive multiple of "
59-
f"micro_batch_size {micro_batch_size}."
60-
)
61-
62-
microbatches = []
63-
for start in range(0, len(samples), micro_batch_size):
64-
microbatch = collate_fn(samples[start : start + micro_batch_size])
65-
if not isinstance(microbatch, dict):
66-
raise ValueError("Finetuning collate_fn must return a batch dictionary.")
67-
microbatches.append(microbatch)
68-
69-
return {_PRECOLLATED_MICROBATCHES_KEY: microbatches}
70-
71-
7222
def split_batch_into_microbatches(
7323
batch: dict[str, Any], num_microbatches: int, enforce_divisible: bool = True
7424
) -> list[dict[str, Any]]:
@@ -149,9 +99,8 @@ def prepare_finetuning_batch(
14999
150100
This function handles the finetuning-specific data flow:
151101
1. Gets the full global batch from the iterator
152-
2. Reuses pre-collated logical microbatches when the DataLoader performed
153-
in-batch packing, otherwise splits the dense global batch
154-
3. Extracts the dynamic sequence length from the resulting microbatches
102+
2. Extracts the dynamic sequence length from the batch
103+
3. Splits the batch into microbatches with consistent sequence length
155104
4. Returns an iterator over microbatches and the extracted sequence length
156105
157106
Args:
@@ -182,27 +131,6 @@ def prepare_finetuning_batch(
182131
# Get full global batch from dataloader
183132
global_batch = next(data_iterator)
184133

185-
precollated_microbatches = (
186-
global_batch.get(_PRECOLLATED_MICROBATCHES_KEY) if isinstance(global_batch, dict) else None
187-
)
188-
if precollated_microbatches is not None:
189-
if not isinstance(precollated_microbatches, list) or not all(
190-
isinstance(microbatch, dict) for microbatch in precollated_microbatches
191-
):
192-
raise ValueError("Pre-collated finetuning microbatches must be a list of batch dictionaries.")
193-
if len(precollated_microbatches) != num_microbatches:
194-
raise ValueError(
195-
f"Expected {num_microbatches} pre-collated microbatches, got {len(precollated_microbatches)}."
196-
)
197-
198-
sequence_lengths = [
199-
microbatch[seq_key].size(1)
200-
for microbatch in precollated_microbatches
201-
if isinstance(microbatch.get(seq_key), torch.Tensor) and microbatch[seq_key].dim() >= 2
202-
]
203-
seq_length = max(sequence_lengths, default=default_seq_length)
204-
return iter(precollated_microbatches), seq_length
205-
206134
# Extract dynamic seq_length from the full batch
207135
seq_length = default_seq_length
208136
if seq_key in global_batch and isinstance(global_batch[seq_key], torch.Tensor):

‎src/megatron/bridge/data/builders/gpt_sft.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ def validate(self) -> None:
197197
raise ValueError("enable_offline_packing and enable_in_batch_packing are mutually exclusive.")
198198
if self.in_batch_packing_pad_to_multiple_of <= 0:
199199
raise ValueError("in_batch_packing_pad_to_multiple_of must be greater than 0.")
200+
if self.enable_in_batch_packing and self.dataloader_type == "batch":
201+
raise ValueError(
202+
"GPT-SFT in-batch packing does not support dataloader_type='batch'; use 'single' or 'cyclic'."
203+
)
200204
if self.enable_offline_packing and self.offline_packing_specs is None:
201205
raise ValueError("offline_packing_specs must be set when enable_offline_packing=True.")
202206
if self.offline_packing_specs is not None and not self.enable_offline_packing:

‎src/megatron/bridge/data/datasets/direct_sft.py‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,6 @@ def __init__(
7373
self._base_examples = base_examples
7474
self._length = int(max(0, target_length))
7575
self._processor = processor
76-
# The global-batch sampler uses this runtime capability to collate each
77-
# logical microbatch independently before THD packing. Step-owned packing
78-
# must continue through the ordinary dense global-batch path.
79-
self.enable_in_batch_packing = enable_in_batch_packing and not defer_in_batch_packing_to_step
8076
# Choose collate implementation by processor type name when not provided
8177
collate_key = type(processor).__name__ if processor is not None else "default"
8278
explicit_collate_impl = collate_impl is not None
@@ -92,7 +88,7 @@ def __init__(
9288
"pad_to_multiple_of": pad_to_multiple_of,
9389
# Active deferral user: Qwen3-VL. Other VLM/HF collates should pack
9490
# here when enable_in_batch_packing is set.
95-
"enable_in_batch_packing": self.enable_in_batch_packing,
91+
"enable_in_batch_packing": enable_in_batch_packing and not defer_in_batch_packing_to_step,
9692
"in_batch_packing_pad_to_multiple_of": in_batch_packing_pad_to_multiple_of,
9793
}
9894
if explicit_collate_impl:

‎src/megatron/bridge/data/samplers.py‎

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@
33
"""Dataloaders."""
44

55
import random
6-
from functools import partial
76
from typing import Any, Callable, Iterator, Optional
87

98
import numpy as np
109
import torch
1110
from torch.utils.data import DataLoader, Dataset
1211

13-
from megatron.bridge.data.batch_utils import collate_finetuning_microbatches
14-
1512

1613
def build_pretraining_data_loader(
1714
dataset: Dataset,
@@ -113,23 +110,13 @@ def build_pretraining_data_loader(
113110
else:
114111
raise Exception("{} dataloader type is not supported.".format(dataloader_type))
115112

116-
effective_collate_fn = collate_fn
117-
if dataloader_type == "batch" and getattr(dataset, "enable_in_batch_packing", False) is True:
118-
if collate_fn is None:
119-
raise ValueError("In-batch packing with dataloader_type='batch' requires a dataset collate_fn.")
120-
effective_collate_fn = partial(
121-
collate_finetuning_microbatches,
122-
micro_batch_size=micro_batch_size,
123-
collate_fn=collate_fn,
124-
)
125-
126113
# Torch dataloader.
127114
return DataLoader(
128115
dataset,
129116
batch_sampler=batch_sampler,
130117
num_workers=num_workers,
131118
pin_memory=pin_memory,
132-
collate_fn=effective_collate_fn,
119+
collate_fn=collate_fn,
133120
persistent_workers=persistent_workers,
134121
worker_init_fn=worker_init_fn,
135122
)

‎tests/unit_tests/data/builders/test_gpt_sft_config.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def test_in_batch_config_round_trip_is_declarative_and_serializable(tmp_path):
8383
preprocessing=PromptCompletionSFTPreprocessingConfig(),
8484
enable_in_batch_packing=True,
8585
in_batch_packing_pad_to_multiple_of=8,
86+
dataloader_type="single",
8687
)
8788

8889
serialized = ConfigContainer._convert_value_to_dict(config)
@@ -99,12 +100,25 @@ def test_config_rejects_nonpositive_in_batch_packing_multiple(tmp_path):
99100
dataset_root=tmp_path,
100101
enable_in_batch_packing=True,
101102
in_batch_packing_pad_to_multiple_of=0,
103+
dataloader_type="single",
102104
)
103105

104106
with pytest.raises(ValueError, match="in_batch_packing_pad_to_multiple_of must be greater than 0"):
105107
config.validate()
106108

107109

110+
def test_config_rejects_batch_dataloader_with_in_batch_packing(tmp_path):
111+
config = GPTSFTDatasetConfig(
112+
seq_length=128,
113+
dataset_root=tmp_path,
114+
enable_in_batch_packing=True,
115+
dataloader_type="batch",
116+
)
117+
118+
with pytest.raises(ValueError, match="does not support dataloader_type='batch'"):
119+
config.validate()
120+
121+
108122
@pytest.mark.parametrize(
109123
"dataset_kwargs",
110124
[
@@ -128,6 +142,7 @@ def test_config_rejects_dense_attention_mask_with_in_batch_packing(tmp_path):
128142
seq_length=128,
129143
dataset_root=tmp_path,
130144
enable_in_batch_packing=True,
145+
dataloader_type="single",
131146
dataset_kwargs={"get_attention_mask_from_fusion": False},
132147
)
133148

@@ -140,6 +155,7 @@ def test_config_rejects_offline_and_in_batch_packing(tmp_path):
140155
seq_length=128,
141156
dataset_root=tmp_path,
142157
enable_in_batch_packing=True,
158+
dataloader_type="single",
143159
enable_offline_packing=True,
144160
offline_packing_specs=PackedSequenceSpecs(packed_sequence_size=128),
145161
)
@@ -663,6 +679,7 @@ def test_builder_owns_runtime_materialization_and_shared_construction(monkeypatc
663679
config = _hf_config(tmp_path)
664680
config.enable_in_batch_packing = True
665681
config.in_batch_packing_pad_to_multiple_of = 8
682+
config.dataloader_type = "single"
666683
materialize_mock = []
667684
dataset_calls = []
668685

0 commit comments

Comments
 (0)