Skip to content

Commit fa86c50

Browse files
committed
Update
[ghstack-poisoned]
1 parent b72f31a commit fa86c50

18 files changed

Lines changed: 582 additions & 102 deletions

File tree

tests/integration_tests/features.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,134 @@ def build_features_test_list() -> list[OverrideDefinitions]:
332332
"hsdp",
333333
ngpu=4,
334334
),
335+
OverrideDefinitions(
336+
[
337+
[
338+
"--training.full_dtensor",
339+
]
340+
],
341+
"Full DTensor FSDP",
342+
"full_dtensor_fsdp",
343+
ngpu=4,
344+
),
345+
OverrideDefinitions(
346+
[
347+
[
348+
"--training.full_dtensor",
349+
"--parallelism.data_parallel_shard_degree=2",
350+
"--parallelism.data_parallel_replicate_degree=2",
351+
]
352+
],
353+
"Full DTensor HSDP",
354+
"full_dtensor_hsdp",
355+
ngpu=4,
356+
),
357+
OverrideDefinitions(
358+
[
359+
[
360+
"--training.full_dtensor",
361+
"--parallelism.tensor_parallel_degree=2",
362+
"--parallelism.data_parallel_shard_degree=2",
363+
]
364+
],
365+
"Full DTensor TP + FSDP",
366+
"full_dtensor_tp_fsdp",
367+
ngpu=4,
368+
),
369+
OverrideDefinitions(
370+
[
371+
[
372+
"--training.full_dtensor",
373+
"--module llama3 --config llama3_debugmodel_flex_attn",
374+
"--parallelism.context_parallel_degree=2",
375+
"--parallelism.data_parallel_shard_degree=2",
376+
]
377+
],
378+
"Full DTensor CP + FSDP (FlexAttention)",
379+
"full_dtensor_cp_fsdp",
380+
ngpu=4,
381+
),
382+
OverrideDefinitions(
383+
[
384+
[
385+
"--training.full_dtensor",
386+
"--module llama3 --config llama3_debugmodel_flex_attn",
387+
"--parallelism.tensor_parallel_degree=2",
388+
"--parallelism.context_parallel_degree=2",
389+
"--parallelism.data_parallel_shard_degree=2",
390+
]
391+
],
392+
"Full DTensor TP + CP + FSDP (FlexAttention)",
393+
"full_dtensor_tp_cp_fsdp",
394+
ngpu=8,
395+
),
396+
OverrideDefinitions(
397+
[
398+
[
399+
"--training.full_dtensor",
400+
"--parallelism.tensor_parallel_degree=2",
401+
"--parallelism.data_parallel_shard_degree=2",
402+
"--parallelism.data_parallel_replicate_degree=2",
403+
]
404+
],
405+
"Full DTensor TP + HSDP",
406+
"full_dtensor_tp_hsdp",
407+
ngpu=8,
408+
),
409+
OverrideDefinitions(
410+
[
411+
[
412+
"--training.full_dtensor",
413+
"--parallelism.pipeline_parallel_degree=2",
414+
"--parallelism.tensor_parallel_degree=2",
415+
"--parallelism.data_parallel_shard_degree=2",
416+
]
417+
],
418+
"Full DTensor PP + TP + FSDP",
419+
"full_dtensor_pp_tp_fsdp",
420+
ngpu=8,
421+
),
422+
OverrideDefinitions(
423+
[
424+
[
425+
"--training.full_dtensor",
426+
"--module llama3 --config llama3_debugmodel_flex_attn",
427+
"--parallelism.pipeline_parallel_degree=2",
428+
"--parallelism.context_parallel_degree=2",
429+
"--parallelism.data_parallel_shard_degree=2",
430+
]
431+
],
432+
"Full DTensor PP + CP + FSDP (FlexAttention)",
433+
"full_dtensor_pp_cp_fsdp",
434+
ngpu=8,
435+
),
436+
OverrideDefinitions(
437+
[
438+
[
439+
"--training.full_dtensor",
440+
"--module llama3 --config llama3_debugmodel_flex_attn",
441+
"--parallelism.pipeline_parallel_degree=2",
442+
"--parallelism.context_parallel_degree=2",
443+
"--parallelism.tensor_parallel_degree=2",
444+
]
445+
],
446+
"Full DTensor PP + CP + TP (FlexAttention)",
447+
"full_dtensor_pp_cp_tp",
448+
ngpu=8,
449+
),
450+
OverrideDefinitions(
451+
[
452+
[
453+
"--training.full_dtensor",
454+
"--parallelism.pipeline_parallel_degree=2",
455+
"--parallelism.context_parallel_degree=2",
456+
"--parallelism.data_parallel_shard_degree=2",
457+
]
458+
],
459+
"Full DTensor PP + CP + FSDP",
460+
"full_dtensor_pp_cp_fsdp",
461+
ngpu=8,
462+
),
335463
OverrideDefinitions(
336464
[
337465
[

torchtitan/components/loss.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@
2020

2121
def cross_entropy_loss(pred: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
2222
"""Cross-entropy loss with sum reduction for token-based normalization."""
23+
from torch.distributed.tensor import DTensor
24+
25+
if isinstance(pred, DTensor) and pred.ndim == 3:
26+
# 3D DTensor: (batch, seq, vocab) → (batch, vocab, seq) so that
27+
# F.cross_entropy treats dim 1 as the class dim. Avoids
28+
# flatten(0, 1) which produces _StridedShard when batch and seq
29+
# are sharded on different mesh dims (e.g. dp_shard + cp).
30+
return torch.nn.functional.cross_entropy(
31+
pred.permute(0, 2, 1).float(),
32+
labels,
33+
reduction="sum",
34+
ignore_index=IGNORE_INDEX,
35+
)
2336
return torch.nn.functional.cross_entropy(
2437
pred.flatten(0, 1).float(),
2538
labels.flatten(0, 1),

torchtitan/config/configs.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ class TrainingConfig:
4747
Whether to apply CPU offloading of parameters, gradients, and optimizer states in FSDP
4848
"""
4949

50+
full_dtensor: bool = False
51+
"""
52+
Whether to use full DTensor for input parallelization instead of the default
53+
FSDP/TP-based approach. This is experimental.
54+
"""
55+
5056
dtype: Literal["bfloat16", "float32"] = "float32"
5157
"""
5258
torch dtype for training. In contrast to mixed precision training, setting training_dtype=bfloat16 will
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Full DTensor infrastructure for SPMD-style parallelization.
8+
9+
When ``training.full_dtensor`` is enabled, all model parameters, buffers,
10+
and inputs become DTensors on a multi-dimensional SPMD mesh. FSDP uses
11+
``DataParallelMeshDims`` to identify which mesh dimensions are
12+
data-parallel.
13+
14+
TP sharding is handled by ``Module.parallelize(spmd_mesh, full_dtensor=True)``
15+
using config-based ``ShardingSpec``.
16+
"""
17+
18+
from typing import Any
19+
20+
import torch
21+
import torch.nn as nn
22+
from torch.distributed.device_mesh import DeviceMesh
23+
from torch.distributed.fsdp import DataParallelMeshDims
24+
from torch.distributed.tensor import DTensor, Replicate, Shard
25+
from torch.distributed.tensor.placement_types import Placement
26+
27+
from torchtitan.distributed.parallel_dims import ParallelDims
28+
29+
30+
def validate_config(parallel_dims: ParallelDims, model_config: Any) -> None:
31+
"""Validate that the current configuration is compatible with full DTensor.
32+
33+
Raises NotImplementedError with a clear message if incompatible.
34+
"""
35+
if parallel_dims.ep_enabled:
36+
raise NotImplementedError(
37+
"full_dtensor is not supported with Expert Parallel. "
38+
"Disable EP or disable full_dtensor."
39+
)
40+
41+
layers = getattr(model_config, "layers", None)
42+
layer = layers[0] if layers else None
43+
attn_config = getattr(layer, "attention", None) if layer else None
44+
attn_backend = getattr(attn_config, "attn_backend", "sdpa")
45+
if attn_backend in ("flex", "varlen"):
46+
raise NotImplementedError(
47+
f"full_dtensor is not supported with {attn_backend} attention. "
48+
"Flex/varlen attention does not support DTensor dispatch. "
49+
"Use sdpa attention or disable full_dtensor."
50+
)
51+
52+
53+
def get_dense_spmd_mesh(parallel_dims: ParallelDims) -> DeviceMesh:
54+
"""Get the dense SPMD mesh for full DTensor parallelization.
55+
56+
Uses the ``full_dtensor_dense`` global mesh which has separate ``dp_shard``
57+
and ``cp`` dimensions. Disabled dimensions (degree 1) are filtered out.
58+
59+
The result is cached on the ``ParallelDims`` object so that all callers
60+
share the exact same ``DeviceMesh`` object — FSDP requires object identity.
61+
"""
62+
if hasattr(parallel_dims, "_spmd_mesh"):
63+
return parallel_dims._spmd_mesh # type: ignore[return-value]
64+
65+
mesh_names = [
66+
n
67+
for n in ["dp_replicate", "dp_shard", "cp", "tp"]
68+
if parallel_dims.get_optional_mesh(n)
69+
]
70+
assert mesh_names, "full_dtensor requires at least one mesh dimension"
71+
mesh = parallel_dims.get_mesh(mesh_names)
72+
parallel_dims._spmd_mesh = mesh # type: ignore[attr-defined]
73+
return mesh
74+
75+
76+
def get_dp_mesh_dims(parallel_dims: ParallelDims) -> DataParallelMeshDims:
77+
"""Build DataParallelMeshDims for dense (non-MoE) parameters.
78+
79+
Uses ``dp_shard`` and ``cp`` as separate shard dimensions rather than
80+
the flattened ``fsdp`` from the non-full-dtensor path.
81+
"""
82+
shard_dims: list[str] = []
83+
if parallel_dims.dp_shard_enabled:
84+
shard_dims.append("dp_shard")
85+
if parallel_dims.cp_enabled:
86+
shard_dims.append("cp")
87+
88+
if len(shard_dims) > 1:
89+
shard: str | tuple[str, ...] | None = tuple(shard_dims)
90+
elif shard_dims:
91+
shard = shard_dims[0]
92+
else:
93+
shard = None
94+
95+
replicate: str | None = None
96+
if parallel_dims.dp_replicate_enabled:
97+
replicate = "dp_replicate"
98+
99+
return DataParallelMeshDims(shard=shard, replicate=replicate)
100+
101+
102+
def resolve_fsdp_mesh(
103+
model: nn.Module,
104+
parallel_dims: ParallelDims,
105+
full_dtensor: bool,
106+
) -> tuple[DeviceMesh, DataParallelMeshDims | None]:
107+
"""Select the FSDP mesh and optional DataParallelMeshDims.
108+
109+
In full DTensor mode, returns the SPMD mesh and DataParallelMeshDims.
110+
In standard mode, returns the conventional dp_mesh and None.
111+
"""
112+
if full_dtensor:
113+
spmd_mesh = get_dense_spmd_mesh(parallel_dims)
114+
_remove_sdpa_math_backend(model)
115+
dp_mesh_dims = get_dp_mesh_dims(parallel_dims)
116+
return spmd_mesh, dp_mesh_dims
117+
else:
118+
names = (
119+
["dp_replicate", "fsdp"] if parallel_dims.dp_replicate_enabled else ["fsdp"]
120+
)
121+
return parallel_dims.get_mesh(names), None
122+
123+
124+
def _remove_sdpa_math_backend(model: nn.Module) -> None:
125+
"""Remove MATH backend from SDPA modules.
126+
127+
SDPA MATH backend decomposes into primitive ops that mix plain tensors
128+
with DTensors, causing errors. Flash and efficient backends have proper
129+
DTensor dispatch rules.
130+
"""
131+
from torch.nn.attention import SDPBackend
132+
133+
from torchtitan.models.common.attention import ScaledDotProductAttention
134+
135+
for module in model.modules():
136+
if isinstance(module, ScaledDotProductAttention):
137+
if SDPBackend.MATH in module.sdpa_backends:
138+
module.sdpa_backends = [
139+
b for b in module.sdpa_backends if b != SDPBackend.MATH
140+
]
141+
142+
143+
def parallelize_inputs(
144+
parallel_dims: ParallelDims,
145+
inputs: torch.Tensor,
146+
labels: torch.Tensor,
147+
extra_kwargs: dict[str, torch.Tensor | None] | None = None,
148+
) -> tuple[DTensor, DTensor]:
149+
"""Convert inputs, labels, and extra kwargs to DTensors on the SPMD mesh.
150+
151+
DP dims get Shard(0) (batch), CP gets Shard(1) (sequence), TP gets Replicate.
152+
Tensor values in extra_kwargs (e.g. positions) use the same placements.
153+
"""
154+
mesh = get_dense_spmd_mesh(parallel_dims)
155+
placements: list[Placement] = []
156+
if parallel_dims.dp_replicate_enabled:
157+
placements.append(Shard(0))
158+
if parallel_dims.dp_shard_enabled:
159+
placements.append(Shard(0))
160+
if parallel_dims.cp_enabled:
161+
placements.append(Shard(1))
162+
if parallel_dims.tp_enabled:
163+
placements.append(Replicate())
164+
165+
assert mesh.ndim == len(placements)
166+
167+
# Convert extra_kwargs tensors (e.g. positions) to DTensors
168+
if extra_kwargs is not None:
169+
for key, value in extra_kwargs.items():
170+
if isinstance(value, torch.Tensor) and not isinstance(value, DTensor):
171+
extra_kwargs[key] = DTensor.from_local(value, mesh, placements)
172+
173+
return DTensor.from_local(inputs, mesh, placements), DTensor.from_local(
174+
labels, mesh, placements
175+
)

torchtitan/distributed/parallel_dims.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,18 +180,30 @@ def unflatten_mesh(
180180
(self.pp, self.dp_replicate, efsdp, self.ep, self.etp),
181181
)
182182

183+
# Full DTensor dense mesh: separate dp_shard and cp dims.
184+
# The non-full-dtensor path uses dense_mesh (fsdp = dp_shard * cp).
185+
# The full DTensor path uses this mesh with explicit separation so
186+
# FSDP can shard on both dims via DataParallelMeshDims.
187+
full_dtensor_dense_mesh = unflatten_mesh(
188+
self._world_mesh,
189+
("pp", "dp_replicate", "dp_shard", "cp", "tp"),
190+
(self.pp, self.dp_replicate, self.dp_shard, self.cp, self.tp),
191+
)
192+
183193
self._global_meshes = {
184194
"dataloading": dataloading_mesh,
185195
"loss": loss_mesh,
186196
"dense": dense_mesh,
187197
"sparse": sparse_mesh,
198+
"full_dtensor_dense": full_dtensor_dense_mesh,
188199
}
189200

190201
self._meshes = {
191202
"pp": dataloading_mesh["pp"],
192203
"batch": dataloading_mesh["batch"],
193204
"loss": loss_mesh,
194205
"dp_replicate": dense_mesh["dp_replicate"],
206+
"dp_shard": full_dtensor_dense_mesh["dp_shard"],
195207
"fsdp": dense_mesh["fsdp"],
196208
"cp": dataloading_mesh["cp"],
197209
"tp": dataloading_mesh["tp"],
@@ -217,6 +229,7 @@ def _validate_meshes(self):
217229
"batch": self.dp_replicate * self.dp_shard,
218230
"loss": self.dp_replicate * self.dp_shard * self.cp,
219231
"dp_replicate": self.dp_replicate,
232+
"dp_shard": self.dp_shard,
220233
"fsdp": self.dp_shard * self.cp,
221234
"cp": self.cp,
222235
"tp": self.tp,
@@ -345,6 +358,7 @@ def dp_replicate_enabled(self):
345358

346359
@property
347360
def dp_shard_enabled(self):
361+
"""True when dp_shard > 1 (not counting cp folded into fsdp)."""
348362
return self.dp_shard > 1
349363

350364
@property

0 commit comments

Comments
 (0)