Skip to content

Commit 6eaaa4b

Browse files
committed
Add DCP compatibility for FSDP2-TP sharding in TransformerEngine.
Signed-off-by: Cory Ye <cye@nvidia.com>
1 parent c68ec31 commit 6eaaa4b

15 files changed

Lines changed: 1079 additions & 103 deletions

File tree

transformer_engine/pytorch/attention/dot_product_attention/dot_product_attention.py

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
import torch
1414
from torch.nn.parameter import Parameter
15+
from torch.distributed import DeviceMesh
16+
from torch.distributed.tensor import DTensor
1517

1618
import transformer_engine_torch as tex
1719
from transformer_engine.common.recipe import (
@@ -44,6 +46,7 @@
4446
set_all_rng_states,
4547
CudaRNGStatesTracker,
4648
graph_safe_rng_available,
49+
_convert_param_to_dtensor_param,
4750
)
4851
from transformer_engine.pytorch.jit import no_torch_dynamo
4952
from transformer_engine.pytorch.graph import is_graph_capturing
@@ -298,6 +301,11 @@ class DotProductAttention(TransformerEngineBaseModule):
298301
``ProcessGroup`` is for :attr:`cp_comm_type` of ``"p2p"``, ``"all_gather"``, and ``"a2a"``.
299302
``List[ProcessGroup]`` is for :attr:`cp_comm_type` of ``"a2a+p2p"``, where :attr:`cp_group[0]`
300303
and :attr:`cp_group[1]` are for ``"a2a"`` and ``"p2p"`` communications respectively.
304+
tp_mesh : Optional[DeviceMesh], default = None
305+
A 1-D DeviceMesh containing a TP mesh dimension, e.g. device_mesh["tp"].
306+
Only required when using TP with DTensor parameters, e.g. for FSDP2 or DCP.
307+
weight_mesh : Optional[DeviceMesh]
308+
Not used for DotProductAttention as there are no quantized weights.
301309
cp_global_ranks : list of global rank IDs, default = None
302310
global rank IDs of GPUs that are in ``cp_group``.
303311
cp_stream : CUDA stream, default = None
@@ -343,6 +351,8 @@ def __init__(
343351
softmax_scale: Optional[float] = None,
344352
softmax_type: str = "vanilla",
345353
return_max_logit: Optional[bool] = False,
354+
tp_mesh: Optional[DeviceMesh] = None,
355+
weight_mesh: Optional[DeviceMesh] = None,
346356
) -> None:
347357
super().__init__()
348358

@@ -477,6 +487,10 @@ def __init__(
477487
return_max_logit=self.return_max_logit,
478488
)
479489

490+
if tp_mesh is not None or weight_mesh is not None:
491+
# Apply DeviceMesh and DTensor-related modifications.
492+
self.set_device_mesh(tp_mesh=tp_mesh, weight_mesh=weight_mesh)
493+
480494
def remove_extra_states_check(self, incompatible_keys): # pylint: disable=unused-argument
481495
"""
482496
Temporarily remove core_attention._extra_state as a missing key
@@ -533,6 +547,53 @@ def custom_forward(*input_args, **input_kwargs):
533547

534548
return hidden_states
535549

550+
def set_device_mesh(
551+
self,
552+
tp_mesh: Optional[DeviceMesh] = None,
553+
weight_mesh: Optional[DeviceMesh] = None,
554+
) -> None:
555+
"""
556+
Set DeviceMesh(s) used for sharding weights and convert main weights into DTensor
557+
depending on the TransformerEngine class to support FSDP-TP sharding with FSDP2.
558+
559+
TransformerEngine manages tensor parallel mechanics, while DTensor offers seamless
560+
integration with Torch DCP checkpointing. This method should only be invoked when
561+
using DTensor parameters, e.g. when using FSDP2 or DCP.
562+
563+
When FSDP2 fully_shard() encounters any DTensor Shard(s), it will automatically
564+
convert them into FSDP-TP strided or non-strided shards depending on the current
565+
sharding dimension and factor of the DTensor. When the sharding dimension of FSDP
566+
matches that of TP, FSDP uses a _StridedShard placement type instead of Shard.
567+
This experimental FSDP-TP logic presides in this FSDP2 initialization function:
568+
``torch.distributed.fsdp._fully_shard._fsdp_param._init_sharded_param``
569+
570+
Parameters
571+
----------
572+
tp_mesh : Optional[DeviceMesh]
573+
A 1-D DeviceMesh containing a TP mesh dimension, e.g. device_mesh["tp"].
574+
Only required when using TP with DTensor parameters, e.g. for FSDP2 or DCP.
575+
weight_mesh : Optional[DeviceMesh]
576+
Not used for DotProductAttention as there are no quantized weights.
577+
"""
578+
if tp_mesh is not None:
579+
# Validate TP DeviceMesh / Group. Must be consistent with tp_size.
580+
assert (
581+
tp_mesh.ndim == 1 and self.tp_size == tp_mesh.size(),
582+
f"TransformerEngine {self.__class__.__name__} TP init size ({self.tp_size}) "
583+
f"does not match the size of the provided TP DeviceMesh ({tp_mesh.size()})."
584+
)
585+
# Set the tensor parallel group from the mesh.
586+
self.set_tensor_parallel_group(tp_mesh.get_group())
587+
588+
# Construct TP-sharded DTensors.
589+
if self.softmax_type == "learnable":
590+
from torch.distributed.tensor.placement_types import Shard
591+
self.softmax_offset = _convert_param_to_dtensor_param(
592+
self.softmax_offset,
593+
tp_mesh,
594+
placements=(Shard(dim=0),)
595+
)
596+
536597
def set_context_parallel_group(
537598
self,
538599
cp_group: Union[dist_group_type, List[dist_group_type], None],
@@ -801,6 +862,17 @@ def set_meta_tensor(self, fwd: bool, recipe: Union[Recipe, List[Recipe]]) -> Non
801862
self.quantizers[fp8_meta_tensor_key] = []
802863
for recipe_state in recipe_states:
803864
self.quantizers[fp8_meta_tensor_key].extend(recipe_state.make_quantizers())
865+
866+
def _get_softmax_offset(self) -> torch.Tensor:
867+
"""Get the softmax offset."""
868+
softmax_offset = (
869+
self.softmax_offset.reshape(1, -1, 1, 1).to(torch.float32)
870+
if self.softmax_offset is not None
871+
else None
872+
)
873+
if isinstance(softmax_offset, DTensor):
874+
softmax_offset = softmax_offset.to_local()
875+
return softmax_offset
804876

805877
@no_torch_dynamo(recursive=False)
806878
def forward(
@@ -1434,11 +1506,7 @@ def forward(
14341506
)
14351507

14361508
# run attention
1437-
softmax_offset = (
1438-
self.softmax_offset.reshape(1, -1, 1, 1).to(torch.float32)
1439-
if self.softmax_offset is not None
1440-
else None
1441-
)
1509+
softmax_offset = self._get_softmax_offset()
14421510

14431511
if use_flash_attention:
14441512
if core_attention_bias_type == "alibi":

transformer_engine/pytorch/attention/multi_head_attention.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import collections
88
from typing import Any, Callable, List, Optional, Tuple, Union
99
import torch
10+
from torch.distributed import DeviceMesh
1011

1112
from transformer_engine.pytorch.quantization import FP8GlobalStateManager
1213
from transformer_engine.pytorch.tensor.float8_tensor import Float8Tensor
@@ -191,6 +192,19 @@ class MultiheadAttention(torch.nn.Module):
191192
``set_tensor_parallel_group(tp_group)`` method on the initialized module before the
192193
forward pass to supply the tensor parallel group needed for tensor and sequence
193194
parallel collectives.
195+
tp_mesh : Optional[DeviceMesh], default = None
196+
A 1-D DeviceMesh containing a TP mesh dimension, e.g. device_mesh["tp"].
197+
Only required when using TP with DTensor parameters, e.g. for FSDP2 or DCP.
198+
weight_mesh : Optional[DeviceMesh], default = None
199+
A 1-D DeviceMesh containing a weight-sharding mesh dimension. Only required
200+
when using the FP8 Current (per-tensor) Scaling recipe on sharded DTensor
201+
parameters and if the DTensor DeviceMesh includes dimensions that do not
202+
shard weights, such as in the case of HSDP (DP-Replicate x DP-Shard).
203+
For example:
204+
- device_mesh["dp"] for FSDP.
205+
- device_mesh["dp_cp"] if using CP ranks in FSDP.
206+
- device_mesh["tp"] if using TP.
207+
- device_mesh["dp_cp_tp"] if strided-sharding with FSDP-TP.
194208
195209
Optimization parameters
196210
-----------------------
@@ -286,6 +300,8 @@ def __init__(
286300
seq_length: Optional[int] = None,
287301
micro_batch_size: Optional[int] = None,
288302
softmax_type: str = "vanilla",
303+
tp_mesh: Optional[DeviceMesh] = None,
304+
weight_mesh: Optional[DeviceMesh] = None,
289305
) -> None:
290306
super().__init__()
291307

@@ -357,6 +373,13 @@ def __init__(
357373
self.q_norm, self.k_norm = self._create_qk_norm_modules(
358374
qk_norm_type, qk_norm_eps, device, seq_length, micro_batch_size
359375
)
376+
if tp_mesh is not None or weight_mesh is not None:
377+
# Apply DeviceMesh and DTensor-related modifications.
378+
# Only necessary for trainable weighted norms.
379+
if hasattr(self.q_norm, "set_device_mesh"):
380+
self.q_norm.set_device_mesh(tp_mesh=tp_mesh, weight_mesh=weight_mesh)
381+
if hasattr(self.k_norm, "set_device_mesh"):
382+
self.k_norm.set_device_mesh(tp_mesh=tp_mesh, weight_mesh=weight_mesh)
360383

361384
qkv_parallel_mode = "column" if set_parallel_mode else None
362385

@@ -389,6 +412,8 @@ def __init__(
389412
normalization=normalization,
390413
ub_name="qkv",
391414
name=name + ".layernorm_linear_qkv" if name is not None else None,
415+
tp_mesh=tp_mesh,
416+
weight_mesh=weight_mesh,
392417
**common_gemm_kwargs,
393418
)
394419
else:
@@ -401,6 +426,8 @@ def __init__(
401426
parallel_mode=qkv_parallel_mode,
402427
parameters_split=parameters_split,
403428
name=name + ".linear_qkv" if name is not None else None,
429+
tp_mesh=tp_mesh,
430+
weight_mesh=weight_mesh,
404431
**common_gemm_kwargs,
405432
)
406433
elif self.attention_type == "cross":
@@ -423,6 +450,8 @@ def __init__(
423450
normalization=normalization,
424451
ub_name="qkv",
425452
name=name + ".layernorm_linear_q" if name is not None else None,
453+
tp_mesh=tp_mesh,
454+
weight_mesh=weight_mesh,
426455
**common_gemm_kwargs,
427456
)
428457
else:
@@ -433,6 +462,8 @@ def __init__(
433462
bias=bias,
434463
return_bias=False,
435464
parallel_mode=qkv_parallel_mode,
465+
tp_mesh=tp_mesh,
466+
weight_mesh=weight_mesh,
436467
**common_gemm_kwargs,
437468
)
438469
self.key_value = Linear(
@@ -444,6 +475,8 @@ def __init__(
444475
parallel_mode=qkv_parallel_mode,
445476
parameters_split=("key", "value") if not fuse_qkv_params else None,
446477
name=name + ".linear_kv" if name is not None else None,
478+
tp_mesh=tp_mesh,
479+
weight_mesh=weight_mesh,
447480
**common_gemm_kwargs,
448481
)
449482

@@ -461,6 +494,8 @@ def __init__(
461494
layer_number=self.layer_number,
462495
attention_type=self.attention_type,
463496
softmax_type=self.softmax_type,
497+
tp_mesh=tp_mesh,
498+
weight_mesh=weight_mesh,
464499
)
465500

466501
# Linear
@@ -475,6 +510,8 @@ def __init__(
475510
ub_overlap_ag=ub_overlap_ag,
476511
ub_name="proj",
477512
name=name + ".proj" if name is not None else None,
513+
tp_mesh=tp_mesh,
514+
weight_mesh=weight_mesh,
478515
**common_gemm_kwargs,
479516
)
480517

@@ -566,6 +603,60 @@ def set_tensor_parallel_group(self, tp_group: Union[dist_group_type, None]) -> N
566603
"""
567604
self.tp_group = tp_group
568605

606+
def set_device_mesh(
607+
self,
608+
tp_mesh: Optional[DeviceMesh] = None,
609+
weight_mesh: Optional[DeviceMesh] = None,
610+
) -> None:
611+
"""
612+
Set DeviceMesh(s) used for sharding weights and convert main weights into DTensor
613+
depending on the TransformerEngine class to support FSDP-TP sharding with FSDP2.
614+
615+
TransformerEngine manages tensor parallel mechanics, while DTensor offers seamless
616+
integration with Torch DCP checkpointing. This method should only be invoked when
617+
using DTensor parameters, e.g. when using FSDP2 or DCP.
618+
619+
When FSDP2 fully_shard() encounters any DTensor Shard(s), it will automatically
620+
convert them into FSDP-TP strided or non-strided shards depending on the current
621+
sharding dimension and factor of the DTensor. When the sharding dimension of FSDP
622+
matches that of TP, FSDP uses a _StridedShard placement type instead of Shard.
623+
This experimental FSDP-TP logic presides in this FSDP2 initialization function:
624+
``torch.distributed.fsdp._fully_shard._fsdp_param._init_sharded_param``
625+
626+
Parameters
627+
----------
628+
tp_mesh : Optional[DeviceMesh]
629+
A 1-D DeviceMesh containing a TP mesh dimension, e.g. device_mesh["tp"].
630+
Only required when using TP with DTensor parameters, e.g. for FSDP2 or DCP.
631+
weight_mesh : Optional[DeviceMesh]
632+
A 1-D DeviceMesh containing a weight-sharding mesh dimension. Only required
633+
when using the FP8 Current (per-tensor) Scaling recipe on sharded DTensor
634+
parameters and if the DTensor DeviceMesh includes dimensions that do not
635+
shard weights, such as in the case of HSDP (DP-Replicate x DP-Shard).
636+
For example:
637+
- device_mesh["dp"] for FSDP.
638+
- device_mesh["dp_cp"] if using CP ranks in FSDP.
639+
- device_mesh["tp"] if using TP.
640+
- device_mesh["dp_cp_tp"] if strided-sharding with FSDP-TP.
641+
"""
642+
if tp_mesh is not None:
643+
# Validate TP DeviceMesh / Group. Must be consistent with tp_size.
644+
assert (
645+
tp_mesh.ndim == 1 and self.tp_size == tp_mesh.size(),
646+
f"TransformerEngine {self.__class__.__name__} TP init size ({self.tp_size}) "
647+
f"does not match the size of the provided TP DeviceMesh ({tp_mesh.size()})."
648+
)
649+
# Set the tensor parallel group from the mesh.
650+
self.set_tensor_parallel_group(tp_mesh.get_group())
651+
652+
if tp_mesh is not None or weight_mesh is not None:
653+
# Iterate through child sub-modules without deep recursion.
654+
# Automatically detects TransformerEngine TP modules and
655+
# the capability to call this method at any level.
656+
for name, child in self.named_children():
657+
if hasattr(child, "set_device_mesh"):
658+
child.set_device_mesh(tp_mesh, weight_mesh)
659+
569660
def set_context_parallel_group(
570661
self,
571662
cp_group: Union[dist_group_type, List[dist_group_type], None],

transformer_engine/pytorch/distributed.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1934,6 +1934,39 @@ def _get_module_fsdp_state(module):
19341934
return fsdp_state
19351935

19361936

1937+
def _convert_param_to_dtensor_param(
1938+
param: torch.nn.Parameter,
1939+
device_mesh: torch.distributed.DeviceMesh,
1940+
placements: Tuple[torch.distributed.tensor.placement_types.Placement],
1941+
shape: Optional[torch.Size] = None,
1942+
stride: Optional[Tuple[int]] = None,
1943+
):
1944+
"""Convert the parameter into a DTensor."""
1945+
from torch.distributed.tensor import DTensor
1946+
# If the parameter is already a DTensor, extract local Tensor.
1947+
# We overwrite the original DTensor's distributed configuration.
1948+
param_tensor = param
1949+
if isinstance(param, DTensor):
1950+
param_tensor = param.to_local()
1951+
# Convert the parameter to a DTensor.
1952+
new_param = torch.nn.Parameter(
1953+
DTensor.from_local(
1954+
param_tensor,
1955+
device_mesh,
1956+
placements=placements,
1957+
shape=shape,
1958+
stride=stride,
1959+
)
1960+
)
1961+
# Inherit attributes of the original Parameter.
1962+
# For example, "param_init_meta" or "tensor_model_parallel".
1963+
for key, val in param.__dict__.items():
1964+
if not hasattr(new_param, key):
1965+
# Set the original attribute.
1966+
setattr(new_param, key, val)
1967+
return new_param
1968+
1969+
19371970
def _fsdp_scatter_tensors(
19381971
fsdp_group: dist_group_type,
19391972
*tensors: torch.Tensor,

0 commit comments

Comments
 (0)