Skip to content

Commit c3dd946

Browse files
Always Pre-Split Microbatches for PP
Summary: - Have PP training and validation dataloaders emit pipeline microbatches directly. - Keep PP execution in the existing forward_backward_step/eval flow while passing pre-split args/kwargs/targets to torch.distributed.pipelining. - Remove the PP+Varlen guard and switch the GPT-OSS PP integration test back to the default varlen config. - Update copied PP train paths in TorchFT and Forge to use the pre-split schedule API with the same structure-preserving pattern. Motivation: TorchTitan previously built a full local batch and relied on torch.distributed.pipelining to split it. That does not work for batch-dependent non-tensor metadata such as varlen attention metadata. Owning the split at the dataloader/trainer boundary lets TorchTitan generate block masks and varlen metadata per pipeline microbatch before calling the PP schedule. Test Plan: - python -m py_compile torchtitan/trainer.py torchtitan/components/validate.py torchtitan/models/common/decoder.py torchtitan/models/gpt_oss/config_registry.py tests/integration_tests/models.py torchtitan/experiments/torchft/trainer.py torchtitan/experiments/forge/engine.py torchtitan/experiments/forge/example_train.py - pre-commit run --files torchtitan/trainer.py torchtitan/components/validate.py torchtitan/models/common/decoder.py torchtitan/models/gpt_oss/config_registry.py tests/integration_tests/models.py torchtitan/experiments/torchft/trainer.py torchtitan/experiments/forge/engine.py torchtitan/experiments/forge/example_train.py - python -m tests.integration_tests.run_tests <output_dir> --test_suite models --test_name gpt_oss_pp+fsdp+ep+sacop --ngpu 8 - NGPU=2 LOG_RANK=0,1 ./run_train.sh --dump_folder <output_dir> --module gpt_oss --config gpt_oss_debugmodel --parallelism.pipeline_parallel_degree 2 --parallelism.pipeline_parallel_schedule 1F1B --training.steps 1 --validator.enable --validator.steps 1 ghstack-source-id: 51a5e12 Pull Request resolved: #3856
1 parent 9524278 commit c3dd946

7 files changed

Lines changed: 251 additions & 94 deletions

File tree

tests/integration_tests/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def build_model_tests_list() -> list[OverrideDefinitions]:
194194
OverrideDefinitions(
195195
[
196196
[
197-
"--module gpt_oss --config gpt_oss_debugmodel_flex",
197+
"--module gpt_oss --config gpt_oss_debugmodel",
198198
"--parallelism.data_parallel_shard_degree 4",
199199
"--parallelism.pipeline_parallel_degree 2",
200200
"--parallelism.pipeline_parallel_schedule Interleaved1F1B",

torchtitan/components/validate.py

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -227,28 +227,45 @@ def validate(
227227
accumulated_losses = []
228228
device_type = utils.device_type
229229
num_steps = 0
230+
num_microbatches = (
231+
self.local_batch_size // self.parallelism.pipeline_parallel_microbatch_size
232+
if parallel_dims.pp_enabled
233+
else 1
234+
)
230235

231236
validation_dataloader = self.dl_config.build(
232237
dp_world_size=self.dp_world_size,
233238
dp_rank=self.dp_rank,
234239
tokenizer=self.tokenizer,
235240
seq_len=self.seq_len,
236-
local_batch_size=self.local_batch_size,
241+
local_batch_size=(
242+
self.parallelism.pipeline_parallel_microbatch_size
243+
if parallel_dims.pp_enabled
244+
else self.local_batch_size
245+
),
237246
)
238247

239-
for input_dict, labels in validation_dataloader:
248+
validation_iterator = iter(validation_dataloader)
249+
while True:
240250
# pyrefly: ignore [missing-attribute, unsupported-operation]
241251
if self.config.steps != -1 and num_steps >= self.config.steps:
242252
break
243253

244-
self.metrics_processor.ntokens_since_last_log += labels.numel()
245-
for k, v in input_dict.items():
246-
input_dict[k] = v.to(device_type)
247-
labels = labels.to(device_type)
248-
249-
# Count valid tokens for this batch
250-
local_valid_tokens = torch.tensor(0, dtype=torch.int64, device=device_type)
251-
local_valid_tokens += (labels != IGNORE_INDEX).sum()
254+
try:
255+
microbatches = []
256+
local_valid_tokens = torch.tensor(
257+
0, dtype=torch.int64, device=device_type
258+
)
259+
for _ in range(num_microbatches):
260+
input_dict, labels = next(validation_iterator)
261+
self.metrics_processor.ntokens_since_last_log += labels.numel()
262+
for k, v in input_dict.items():
263+
input_dict[k] = v.to(device_type)
264+
labels = labels.to(device_type)
265+
local_valid_tokens += (labels != IGNORE_INDEX).sum()
266+
microbatches.append((input_dict, labels))
267+
except StopIteration:
268+
break
252269

253270
# All-reduce token count across DP ranks to get global token count
254271
if parallel_dims.dp_enabled:
@@ -259,32 +276,43 @@ def validate(
259276
else:
260277
global_valid_tokens = float(local_valid_tokens.item())
261278

262-
# Process data (extract inputs, handle attention masks, CP sharding)
263-
inputs, labels, extra_kwargs = self.post_dataloading_process(
264-
input_dict, labels, model_parts
265-
)
266-
267279
if parallel_dims.pp_enabled:
268280
assert self.pp_schedule is not None
269281
assert self.pp_has_first_stage is not None
270282
assert self.pp_has_last_stage is not None
271-
# Pipeline Parallel forward inside eval() call
272-
with self.validation_context():
273-
targets, losses = (
274-
(labels, []) if self.pp_has_last_stage else (None, None)
283+
284+
arg_mbs: list[tuple[torch.Tensor, ...]] = []
285+
kwarg_mbs: list[dict[str, Any]] = []
286+
target_mbs: list[torch.Tensor] | None = (
287+
[] if self.pp_has_last_stage else None
288+
)
289+
290+
for input_dict, labels in microbatches:
291+
inputs, labels, extra_kwargs = self.post_dataloading_process(
292+
input_dict, labels, model_parts
275293
)
294+
if self.pp_has_first_stage:
295+
arg_mbs.append((inputs,))
296+
kwarg_mbs.append(extra_kwargs)
297+
if target_mbs is not None:
298+
target_mbs.append(labels)
299+
300+
with self.validation_context():
301+
losses = [] if self.pp_has_last_stage else None
276302
if self.pp_has_first_stage:
277303
self.pp_schedule.eval(
278-
inputs,
279-
**extra_kwargs,
280-
target=targets,
304+
arg_mbs,
305+
kwargs=kwarg_mbs,
306+
target=target_mbs,
281307
losses=losses,
308+
pre_split_args_kwargs=True,
282309
)
283310
else:
284311
self.pp_schedule.eval(
285-
**extra_kwargs,
286-
target=targets,
312+
kwargs=kwarg_mbs,
313+
target=target_mbs,
287314
losses=losses,
315+
pre_split_args_kwargs=True,
288316
)
289317

290318
# accumulate losses across pipeline microbatches
@@ -296,6 +324,12 @@ def validate(
296324
else:
297325
loss_sum = torch.tensor([-1.0], device=device_type)
298326
else:
327+
assert len(microbatches) == 1
328+
input_dict, labels = microbatches[0]
329+
# Process data (extract inputs, handle attention masks, CP sharding)
330+
inputs, labels, extra_kwargs = self.post_dataloading_process(
331+
input_dict, labels, model_parts
332+
)
299333
with self.validation_context():
300334
assert len(model_parts) == 1
301335
predictions = model_parts[0](inputs, **extra_kwargs)

torchtitan/experiments/forge/example_train.py

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import time
99
from collections.abc import Iterable
1010
from datetime import timedelta
11-
from typing import Any
11+
from typing import Any, cast
1212

1313
import torch
1414
from torch.distributed.elastic.multiprocessing.errors import record
@@ -59,7 +59,11 @@ def __init__(self, config: TitanTrainer.Config):
5959
dp_rank=self.dp_rank,
6060
tokenizer=self.tokenizer,
6161
seq_len=config.training.seq_len,
62-
local_batch_size=config.training.local_batch_size,
62+
local_batch_size=(
63+
config.parallelism.pipeline_parallel_microbatch_size
64+
if self.parallel_dims.pp_enabled
65+
else config.training.local_batch_size
66+
),
6367
)
6468

6569
model_args = self.model_config
@@ -206,26 +210,41 @@ def forward_backward_step(
206210
model_parts = self.model_parts
207211
parallel_dims = self.parallel_dims
208212

209-
inputs, labels, extra_kwargs = self.post_dataloading_process(input_dict, labels)
210-
211213
if parallel_dims.pp_enabled:
214+
input_dict_mbs = cast(list[dict[str, torch.Tensor]], input_dict)
215+
label_mbs = cast(list[torch.Tensor], labels)
216+
arg_mbs: list[tuple[torch.Tensor, ...]] = []
217+
kwarg_mbs: list[dict[str, Any]] = []
218+
target_mbs: list[torch.Tensor] | None = (
219+
[] if self.pp_has_last_stage else None
220+
)
221+
for mb_input_dict, mb_labels in zip(input_dict_mbs, label_mbs, strict=True):
222+
inputs, mb_labels, extra_kwargs = self.post_dataloading_process(
223+
mb_input_dict, mb_labels
224+
)
225+
if self.pp_has_first_stage:
226+
arg_mbs.append((inputs,))
227+
kwarg_mbs.append(extra_kwargs)
228+
if target_mbs is not None:
229+
target_mbs.append(mb_labels)
230+
212231
# Pipeline Parallel forward / backward inside step() call
213232
with self.train_context():
214-
targets, losses = (
215-
(labels, []) if self.pp_has_last_stage else (None, None)
216-
)
233+
losses = [] if self.pp_has_last_stage else None
217234
if self.pp_has_first_stage:
218235
self.pp_schedule.step(
219-
inputs,
220-
**extra_kwargs,
221-
target=targets,
236+
arg_mbs,
237+
kwargs=kwarg_mbs,
238+
target=target_mbs,
222239
losses=losses,
240+
pre_split_args_kwargs=True,
223241
)
224242
else:
225243
self.pp_schedule.step(
226-
**extra_kwargs,
227-
target=targets,
244+
kwargs=kwarg_mbs,
245+
target=target_mbs,
228246
losses=losses,
247+
pre_split_args_kwargs=True,
229248
)
230249

231250
# accumulate losses across pipeline microbatches
@@ -238,6 +257,10 @@ def forward_backward_step(
238257
else torch.tensor([-1.0], device=self.device)
239258
)
240259
else:
260+
inputs, labels, extra_kwargs = self.post_dataloading_process(
261+
input_dict, labels
262+
)
263+
241264
# Non-PP forward / backward
242265
with self.train_context():
243266
assert len(model_parts) == 1
@@ -264,14 +287,28 @@ def train_step(
264287
# Keep these variables local to shorten the code as these are
265288
# the major variables that are used in the training loop.
266289
parallel_dims = self.parallel_dims
290+
num_microbatches = (
291+
self.config.training.local_batch_size
292+
// self.config.parallelism.pipeline_parallel_microbatch_size
293+
if parallel_dims.pp_enabled
294+
else 1
295+
)
267296

268297
# Collect all microbatches on CPU and count total valid tokens
269-
# Here we assume the inputs/labels are on GPU
270-
microbatches = []
298+
microbatches: list[tuple[Any, Any]] = []
271299
local_valid_tokens = torch.tensor(0, dtype=torch.int64)
272-
for _microbatch in range(self.gradient_accumulation_steps):
273-
input_dict, labels = next(data_iterator)
274-
local_valid_tokens += (labels != IGNORE_INDEX).sum()
300+
for _ in range(self.gradient_accumulation_steps):
301+
if parallel_dims.pp_enabled:
302+
input_dict = []
303+
labels = []
304+
for _ in range(num_microbatches):
305+
mb_input_dict, mb_labels = next(data_iterator)
306+
local_valid_tokens += (mb_labels != IGNORE_INDEX).sum()
307+
input_dict.append(mb_input_dict)
308+
labels.append(mb_labels)
309+
else:
310+
input_dict, labels = next(data_iterator)
311+
local_valid_tokens += (labels != IGNORE_INDEX).sum()
275312
microbatches.append((input_dict, labels))
276313

277314
# All-reduce to get global token count across DP ranks
@@ -287,15 +324,26 @@ def train_step(
287324
# Process each microbatch: move to GPU, forward/backward, then free
288325
accumulated_losses = []
289326
for input_dict, labels in microbatches:
290-
# Move tensors to GPU
291-
for k, v in input_dict.items():
292-
if isinstance(v, torch.Tensor):
293-
input_dict[k] = v.to(self.device)
294-
labels = labels.to(self.device)
327+
if parallel_dims.pp_enabled:
328+
assert isinstance(input_dict, list)
329+
assert isinstance(labels, list)
330+
for mb_input_dict in input_dict:
331+
for k, v in mb_input_dict.items():
332+
if isinstance(v, torch.Tensor):
333+
mb_input_dict[k] = v.to(self.device)
334+
labels = [mb_labels.to(self.device) for mb_labels in labels]
335+
else:
336+
assert isinstance(input_dict, dict)
337+
assert isinstance(labels, torch.Tensor)
338+
# Move tensors to GPU
339+
for k, v in input_dict.items():
340+
if isinstance(v, torch.Tensor):
341+
input_dict[k] = v.to(self.device)
342+
labels = labels.to(self.device)
295343

296344
loss = self.forward_backward_step(
297345
input_dict=input_dict,
298-
labels=labels,
346+
labels=labels, # pyrefly: ignore [bad-argument-type]
299347
global_valid_tokens=global_valid_tokens,
300348
)
301349
accumulated_losses.append(loss.detach())

torchtitan/experiments/torchft/trainer.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ def __init__(self, config: Config):
9696
dp_rank=batch_rank,
9797
tokenizer=self.tokenizer,
9898
seq_len=config.training.seq_len,
99-
local_batch_size=config.training.local_batch_size,
99+
local_batch_size=(
100+
config.parallelism.pipeline_parallel_microbatch_size
101+
if parallel_dims.pp_enabled
102+
else config.training.local_batch_size
103+
),
100104
)
101105

102106
# build model (using meta init)
@@ -378,13 +382,28 @@ def train_step(
378382
# Keep these variables local to shorten the code as these are
379383
# the major variables that are used in the training loop.
380384
parallel_dims = self.parallel_dims
385+
num_microbatches = (
386+
self.config.training.local_batch_size
387+
// self.config.parallelism.pipeline_parallel_microbatch_size
388+
if parallel_dims.pp_enabled
389+
else 1
390+
)
381391

382392
# Collect all microbatches on CPU and count total valid tokens
383393
microbatches = []
384394
local_valid_tokens = torch.tensor(0, dtype=torch.int64)
385-
for _microbatch in range(self.gradient_accumulation_steps):
386-
input_dict, labels = next(data_iterator)
387-
local_valid_tokens += (labels != IGNORE_INDEX).sum()
395+
for _ in range(self.gradient_accumulation_steps):
396+
if parallel_dims.pp_enabled:
397+
input_dict = []
398+
labels = []
399+
for _ in range(num_microbatches):
400+
mb_input_dict, mb_labels = next(data_iterator)
401+
local_valid_tokens += (mb_labels != IGNORE_INDEX).sum()
402+
input_dict.append(mb_input_dict)
403+
labels.append(mb_labels)
404+
else:
405+
input_dict, labels = next(data_iterator)
406+
local_valid_tokens += (labels != IGNORE_INDEX).sum()
388407
microbatches.append((input_dict, labels))
389408

390409
# All-reduce to get global token count across DP ranks
@@ -400,11 +419,22 @@ def train_step(
400419
# Process each microbatch: move to GPU, forward/backward, then free
401420
accumulated_losses = []
402421
for input_dict, labels in microbatches:
403-
# Move tensors to GPU
404-
for k, v in input_dict.items():
405-
if isinstance(v, torch.Tensor):
406-
input_dict[k] = v.to(self.device)
407-
labels = labels.to(self.device)
422+
if parallel_dims.pp_enabled:
423+
assert isinstance(input_dict, list)
424+
assert isinstance(labels, list)
425+
for mb_input_dict in input_dict:
426+
for k, v in mb_input_dict.items():
427+
if isinstance(v, torch.Tensor):
428+
mb_input_dict[k] = v.to(self.device)
429+
labels = [mb_labels.to(self.device) for mb_labels in labels]
430+
else:
431+
assert isinstance(input_dict, dict)
432+
assert isinstance(labels, torch.Tensor)
433+
# Move tensors to GPU
434+
for k, v in input_dict.items():
435+
if isinstance(v, torch.Tensor):
436+
input_dict[k] = v.to(self.device)
437+
labels = labels.to(self.device)
408438

409439
loss = self.forward_backward_step(
410440
input_dict=input_dict,

torchtitan/models/common/decoder.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -142,17 +142,6 @@ def update_from_config(
142142
"Weight tying is not supported with Pipeline Parallel."
143143
)
144144

145-
if parallelism.pipeline_parallel_degree > 1 and any(
146-
layer.attention is not None
147-
and isinstance(layer.attention.inner_attention, VarlenAttention.Config)
148-
for layer in self.layers
149-
):
150-
raise ValueError(
151-
"Pipeline Parallel is not compatible with VarlenAttention. "
152-
"Use a FlexAttention backend (attn_backend='flex' or "
153-
"'flex_flash') for pipelined models."
154-
)
155-
156145
tp = parallelism.tensor_parallel_degree
157146
attention = self.first_attention
158147
if tp > 1 and attention is not None:

torchtitan/models/gpt_oss/config_registry.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@ def gpt_oss_debugmodel() -> Trainer.Config:
6565

6666

6767
def gpt_oss_debugmodel_flex() -> Trainer.Config:
68-
# FlexAttention variant. Pipeline Parallel is incompatible with
69-
# VarlenAttention, so PP integration tests use this flex config.
7068
return _gpt_oss_debugmodel(attn_backend="flex")
7169

7270

0 commit comments

Comments
 (0)