Skip to content

Commit 9c712a5

Browse files
committed
Refactor EP sharding to apply DTensor wrapping during loading
Move EP parameter DTensor wrapping from post-load model wrapping to the tensor parallel layer's `post_shard_wrap` method, which applies during parameter loading. This ensures DTensor wrapping happens at the right time in the loading pipeline and removes duplicated logic.
1 parent 37c106b commit 9c712a5

4 files changed

Lines changed: 36 additions & 36 deletions

File tree

src/transformers/core_model_loading.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,8 @@ def set_param_for_module(
10771077
if ref is not None and param_value.shape != expected_shape and hf_quantizer is None:
10781078
loading_info.mismatched_keys.add((target_name, param_value.shape, expected_shape))
10791079
else:
1080+
if distributed_operation is not None:
1081+
param_value = distributed_operation.post_shard_wrap(param_value)
10801082
# super important otherwise _init_weight will re-init the param
10811083
param_value._is_hf_initialized = True
10821084
setattr(module_obj, param_name, param_value)

src/transformers/integrations/tensor_parallel.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import torch
3030
import torch.distributed as dist
3131
from torch import nn
32+
from torch.distributed.tensor import DTensor, Shard
3233

3334
# Cache this result has it's a C FFI call which can be pretty time-consuming
3435
_torch_distributed_available = torch.distributed.is_available()
@@ -130,6 +131,17 @@ def _get_parameter_tp_plan(parameter_name: str, tp_plan: dict[str, str], is_weig
130131
return None
131132

132133

134+
def get_ep_sharded_param_names(model) -> list[str]:
135+
"""FQNs of parameters whose data is per-rank unique under EP sharding."""
136+
if not getattr(model, "has_ep", False):
137+
return []
138+
return [
139+
name
140+
for name, _ in model.named_parameters()
141+
if _get_parameter_tp_plan(parameter_name=name, tp_plan=model.tp_plan, is_weight=True) == "grouped_gemm"
142+
]
143+
144+
133145
# =============================================================================
134146
# Tensor Sharding Utilities
135147
# =============================================================================
@@ -685,6 +697,14 @@ def update_module_attributes(self, module: nn.Module):
685697
"""
686698
pass
687699

700+
def post_shard_wrap(self, param: nn.Parameter) -> nn.Parameter:
701+
"""
702+
Optional final wrap applied to a parameter after `shard_tensor` and before it is
703+
attached to the module. Default is identity. Subclasses can override to e.g. wrap
704+
the local shard as a DTensor.
705+
"""
706+
return param
707+
688708

689709
class ColwiseParallel(TensorParallelLayer):
690710
"""
@@ -1078,6 +1098,15 @@ def update_module_attributes(self, module: nn.Module):
10781098
if hasattr(module, "num_experts"):
10791099
module.num_experts = self.get_expected_sharded_shape((self.empty_param.shape[0],))[0]
10801100

1101+
def post_shard_wrap(self, param: nn.Parameter) -> nn.Parameter:
1102+
"""
1103+
Wrap the EP-sharded local tensor as a DTensor on the TP/EP mesh. Without this, the
1104+
optimizer's foreach ops error with "mixed Tensor and DTensor" against the
1105+
FSDP-wrapped DTensor params on the rest of the model.
1106+
"""
1107+
dt = DTensor.from_local(param.data, self.device_mesh, [Shard(0)], run_check=False)
1108+
return nn.Parameter(dt, requires_grad=param.requires_grad)
1109+
10811110

10821111
class RouterParallel(TensorParallelLayer):
10831112
"""
@@ -1487,6 +1516,8 @@ def shard_and_distribute_module(
14871516
# otherwise loading is crazy slow
14881517
if not isinstance(param, torch.nn.Parameter):
14891518
param = torch.nn.Parameter(param, requires_grad=empty_param.is_floating_point())
1519+
if current_shard_plan is not None:
1520+
param = tp_layer.post_shard_wrap(param)
14901521
setattr(module_to_tp, param_type, param)
14911522
tp_layer.update_module_attributes(module_to_tp)
14921523
return param

src/transformers/modeling_utils.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
from safetensors.torch import load as _safe_load_bytes
4040
from safetensors.torch import save_file as safe_save_file
4141
from torch import Tensor, nn
42-
from torch.distributed.tensor import DTensor, Shard
4342
from torch.distributions import constraints
4443
from torch.utils.checkpoint import checkpoint
4544

@@ -1381,18 +1380,6 @@ def has_ep(self) -> bool:
13811380
distributed_config = getattr(getattr(self, "config", None), "distributed_config", None)
13821381
return distributed_config is not None and getattr(distributed_config, "enable_expert_parallel", False)
13831382

1384-
@property
1385-
def ep_sharded_param_names(self) -> list[str]:
1386-
"""FQNs of parameters whose data is per-rank unique under EP sharding."""
1387-
if not self.has_ep:
1388-
return []
1389-
plan = self.tp_plan
1390-
return [
1391-
name
1392-
for name, _ in self.named_parameters()
1393-
if _get_parameter_tp_plan(parameter_name=name, tp_plan=plan, is_weight=True) == "grouped_gemm"
1394-
]
1395-
13961383
@property
13971384
def tp_plan(self) -> dict[str, str]:
13981385
"""
@@ -4236,8 +4223,6 @@ def from_pretrained(
42364223
model.eval() # Set model in evaluation mode to deactivate Dropout modules by default
42374224
model.set_use_kernels(use_kernels, kernel_config)
42384225

4239-
cls._wrap_ep_params_as_dtensor(model, device_mesh)
4240-
42414226
# If it is a model with generation capabilities, attempt to load generation files (generation config,
42424227
# custom generate function)
42434228
if model.can_generate() and hasattr(model, "adjust_generation_fn") and not gguf_file:
@@ -4376,24 +4361,6 @@ def _load_pretrained_model(
43764361

43774362
return loading_info, disk_offload_index
43784363

4379-
@staticmethod
4380-
def _wrap_ep_params_as_dtensor(model, device_mesh) -> None:
4381-
"""Wrap EP-sharded params (`grouped_gemm` style) as DTensors in-place.
4382-
4383-
Without this, the optimizer's foreach ops error with "mixed Tensor and DTensor"
4384-
against the FSDP-wrapped DTensor params on the rest of the model.
4385-
"""
4386-
4387-
if not model.has_ep:
4388-
return
4389-
plan = model.tp_plan
4390-
for name, p in list(model.named_parameters()):
4391-
if _get_parameter_tp_plan(parameter_name=name, tp_plan=plan, is_weight=True) != "grouped_gemm":
4392-
continue
4393-
parent, attr = get_module_from_name(model, name)
4394-
dt = DTensor.from_local(p.data, device_mesh, [Shard(0)], run_check=False)
4395-
setattr(parent, attr, nn.Parameter(dt, requires_grad=p.requires_grad))
4396-
43974364
@staticmethod
43984365
def _finalize_model_loading(
43994366
model, load_config: LoadStateDictConfig, loading_info: LoadStateDictInfo

src/transformers/trainer.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
from .integrations.liger import apply_liger_kernel
7171
from .integrations.neftune import activate_neftune, deactivate_neftune
7272
from .integrations.peft import MIN_PEFT_VERSION
73+
from .integrations.tensor_parallel import get_ep_sharded_param_names
7374
from .integrations.tpu import save_tpu_checkpoint, tpu_spmd_dataloader, wrap_model_xla_fsdp
7475
from .modelcard import TrainingSummary
7576
from .modeling_utils import PreTrainedModel, unwrap_model
@@ -828,9 +829,8 @@ def create_accelerator_and_postprocess(self) -> None:
828829
# post accelerator creation setup
829830
if self.is_fsdp_enabled:
830831
fsdp_plugin = self.accelerator.state.fsdp_plugin
831-
# EP-sharded experts must not be re-sharded by FSDP — their params are
832-
# already DTensors on the EP mesh.
833-
ep_param_names = getattr(self.model, "ep_sharded_param_names", []) or []
832+
# EP-sharded experts must not be re-sharded by FSDP, their params are DTensors on the EP mesh.
833+
ep_param_names = get_ep_sharded_param_names(self.model)
834834
if ep_param_names:
835835
module_names = list({n.rsplit(".", 1)[0] for n in ep_param_names})
836836
fsdp_plugin.ignored_modules = [self.model.get_submodule(n) for n in module_names]

0 commit comments

Comments
 (0)