Skip to content

Commit 6897754

Browse files
Always Pre-Split Microbatches for PP
Summary: - Have PP training and validation dataloaders emit pipeline microbatches directly. - Group dataloader-produced microbatches inline into pre-split args/kwargs/targets for torch.distributed.pipelining schedule.step/eval. - 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. 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: e45a042 Pull Request resolved: #3856
1 parent 9524278 commit 6897754

7 files changed

Lines changed: 358 additions & 193 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: 97 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -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
@@ -205,53 +209,24 @@ def forward_backward_step(
205209
) -> torch.Tensor:
206210
model_parts = self.model_parts
207211
parallel_dims = self.parallel_dims
212+
assert not parallel_dims.pp_enabled
208213

209214
inputs, labels, extra_kwargs = self.post_dataloading_process(input_dict, labels)
210215

211-
if parallel_dims.pp_enabled:
212-
# Pipeline Parallel forward / backward inside step() call
213-
with self.train_context():
214-
targets, losses = (
215-
(labels, []) if self.pp_has_last_stage else (None, None)
216-
)
217-
if self.pp_has_first_stage:
218-
self.pp_schedule.step(
219-
inputs,
220-
**extra_kwargs,
221-
target=targets,
222-
losses=losses,
223-
)
224-
else:
225-
self.pp_schedule.step(
226-
**extra_kwargs,
227-
target=targets,
228-
losses=losses,
229-
)
216+
# Non-PP forward / backward
217+
with self.train_context():
218+
assert len(model_parts) == 1
219+
pred = model_parts[0](inputs, **extra_kwargs)
220+
# Compute loss sum (reduction='sum')
221+
loss_sum, _ = self.loss_fn(pred, labels)
230222

231-
# accumulate losses across pipeline microbatches
232-
# TODO: PP+FSDP unexpectedly puts the loss back to the CPU
233-
loss = (
234-
# Rescale PP loss to be "local loss sum / global valid tokens"
235-
# because each microbatch could have different number of valid tokens
236-
(torch.sum(torch.stack(losses)) / global_valid_tokens).to(self.device)
237-
if self.pp_has_last_stage
238-
else torch.tensor([-1.0], device=self.device)
239-
)
240-
else:
241-
# Non-PP forward / backward
242-
with self.train_context():
243-
assert len(model_parts) == 1
244-
pred = model_parts[0](inputs, **extra_kwargs)
245-
# Compute loss sum (reduction='sum')
246-
loss_sum, _ = self.loss_fn(pred, labels)
223+
# Scale the loss by the inverse of the total weight denominator before backward
224+
# This ensures gradients are properly normalized across all microbatches
225+
loss = loss_sum / global_valid_tokens
247226

248-
# Scale the loss by the inverse of the total weight denominator before backward
249-
# This ensures gradients are properly normalized across all microbatches
250-
loss = loss_sum / global_valid_tokens
251-
252-
# need to free pred before bwd to avoid peaking memory
253-
del pred
254-
loss.backward()
227+
# need to free pred before bwd to avoid peaking memory
228+
del pred
229+
loss.backward()
255230

256231
# The returned loss here is local SUM loss / global_valid_tokens
257232
return loss
@@ -264,15 +239,23 @@ def train_step(
264239
# Keep these variables local to shorten the code as these are
265240
# the major variables that are used in the training loop.
266241
parallel_dims = self.parallel_dims
242+
num_microbatches = (
243+
self.config.training.local_batch_size
244+
// self.config.parallelism.pipeline_parallel_microbatch_size
245+
if parallel_dims.pp_enabled
246+
else 1
247+
)
267248

268249
# Collect all microbatches on CPU and count total valid tokens
269-
# Here we assume the inputs/labels are on GPU
270-
microbatches = []
250+
batches = []
271251
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()
275-
microbatches.append((input_dict, labels))
252+
for _ in range(self.gradient_accumulation_steps):
253+
step_microbatches = []
254+
for _ in range(num_microbatches):
255+
input_dict, labels = next(data_iterator)
256+
local_valid_tokens += (labels != IGNORE_INDEX).sum()
257+
step_microbatches.append((input_dict, labels))
258+
batches.append(step_microbatches)
276259

277260
# All-reduce to get global token count across DP ranks
278261
# Move to GPU for distributed communication
@@ -284,20 +267,72 @@ def train_step(
284267
else:
285268
global_valid_tokens = float(local_valid_tokens.item())
286269

287-
# Process each microbatch: move to GPU, forward/backward, then free
270+
# Process each batch: move to GPU, forward/backward, then free
288271
accumulated_losses = []
289-
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)
295-
296-
loss = self.forward_backward_step(
297-
input_dict=input_dict,
298-
labels=labels,
299-
global_valid_tokens=global_valid_tokens,
300-
)
272+
for step_microbatches in batches:
273+
if parallel_dims.pp_enabled:
274+
arg_mbs: list[tuple[torch.Tensor, ...]] = []
275+
kwarg_mbs: list[dict[str, Any]] = []
276+
target_mbs: list[torch.Tensor] | None = (
277+
[] if self.pp_has_last_stage else None
278+
)
279+
280+
for input_dict, labels in step_microbatches:
281+
for k, v in input_dict.items():
282+
if isinstance(v, torch.Tensor):
283+
input_dict[k] = v.to(self.device)
284+
labels = labels.to(self.device)
285+
286+
inputs, labels, extra_kwargs = self.post_dataloading_process(
287+
input_dict, labels
288+
)
289+
if self.pp_has_first_stage:
290+
arg_mbs.append((inputs,))
291+
kwarg_mbs.append(extra_kwargs)
292+
if target_mbs is not None:
293+
target_mbs.append(labels)
294+
295+
with self.train_context():
296+
losses = [] if self.pp_has_last_stage else None
297+
if self.pp_has_first_stage:
298+
self.pp_schedule.step(
299+
arg_mbs,
300+
kwargs=kwarg_mbs,
301+
target=target_mbs,
302+
losses=losses,
303+
pre_split_args_kwargs=True,
304+
)
305+
else:
306+
self.pp_schedule.step(
307+
kwargs=kwarg_mbs,
308+
target=target_mbs,
309+
losses=losses,
310+
pre_split_args_kwargs=True,
311+
)
312+
313+
# accumulate losses across pipeline microbatches
314+
# TODO: PP+FSDP unexpectedly puts the loss back to the CPU
315+
if self.pp_has_last_stage:
316+
assert losses is not None
317+
# Rescale PP loss to be "local loss sum / global valid tokens"
318+
# because each microbatch could have different number of valid tokens
319+
loss = (torch.sum(torch.stack(losses)) / global_valid_tokens).to(
320+
self.device
321+
)
322+
else:
323+
loss = torch.tensor([-1.0], device=self.device)
324+
else:
325+
assert len(step_microbatches) == 1
326+
input_dict, labels = step_microbatches[0]
327+
for k, v in input_dict.items():
328+
if isinstance(v, torch.Tensor):
329+
input_dict[k] = v.to(self.device)
330+
labels = labels.to(self.device)
331+
loss = self.forward_backward_step(
332+
input_dict=input_dict,
333+
labels=labels,
334+
global_valid_tokens=global_valid_tokens,
335+
)
301336
accumulated_losses.append(loss.detach())
302337

303338
grad_norm = dist_utils.clip_grad_norm_(

0 commit comments

Comments
 (0)