Skip to content

Commit 704f911

Browse files
EP implementations are callables; inline the backward all-reduce check
Every `EPImplementation` defines `__call__` (AutoEP already did), and the base class keeps `run_experts`, now owning the local DTensor params context. `_all_reduce_backward` is gone: its call sites check `torch.is_grad_enabled() and x.requires_grad` before `_AllReduceBackward.apply`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a336565 commit 704f911

5 files changed

Lines changed: 45 additions & 47 deletions

File tree

‎docs/source/en/expert_parallelism.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,13 @@ The expert forward rule in `ep_plan` selects how tokens reach the experts:
6464

6565
### Adding an implementation
6666

67-
An implementation subclasses `EPImplementation` and registers under the name `ep_implementation` selects. It is built once per experts module, so it can keep state such as communication buffers. Its `forward` takes the batch this rank holds, with global expert ids, and returns the weighted expert output for each of its tokens. It runs this rank's experts with `run_experts`, on the rows, local expert ids and weights it brought them.
67+
An implementation subclasses `EPImplementation` and registers under the name `ep_implementation` selects. It is built once per experts module, so it can keep state such as communication buffers. Calling it with the batch this rank holds, with global expert ids, returns the weighted expert output for each of its tokens. It runs this rank's experts with `run_experts`, on the rows, local expert ids and weights it brought them.
6868

6969
```py
7070
from transformers.distributed.expert_parallel import ALL_EP_IMPLEMENTATIONS, EPImplementation
7171

7272
class MyEP(EPImplementation):
73-
def forward(self, hidden_states, top_k_index, top_k_weights):
73+
def __call__(self, hidden_states, top_k_index, top_k_weights):
7474
tokens, expert_ids, weights = ... # bring this rank's experts the rows routed to them
7575
expert_output = self.run_experts(tokens, expert_ids, weights)
7676
return ... # the weighted expert output for each token of the batch this rank holds

‎src/transformers/distributed/expert_parallel.py‎

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from torch.distributed._functional_collectives import all_to_all_single_autograd
4040
from torch.distributed.tensor import DTensor, Replicate, Shard, distribute_tensor
4141

42-
from .parallel_utils import _all_reduce_backward, _AllReduceForward, _WaitTensor
42+
from .parallel_utils import _AllReduceBackward, _AllReduceForward, _WaitTensor
4343

4444

4545
class ExpertParallelLayer:
@@ -173,7 +173,8 @@ def transform_output_post_forward(self, module, output, mesh):
173173
# Each rank's score gradient covers only its local experts' slots; sum the per-rank partials
174174
# before the mask (each slot has exactly one owning rank, so the sum is exact).
175175
process_group = mesh.get_group() if mesh.ndim == 1 else mesh.get_group("tp")
176-
router_scores = _all_reduce_backward(router_scores, process_group)
176+
if torch.is_grad_enabled() and router_scores.requires_grad:
177+
router_scores = _AllReduceBackward.apply(router_scores, process_group)
177178
non_local_mask = (router_indices // num_local_experts) != ep_rank
178179
router_scores = router_scores.masked_fill(non_local_mask, 0.0)
179180
router_indices = router_indices.masked_fill(non_local_mask, -1)
@@ -203,9 +204,9 @@ class EPImplementation:
203204
the plan is applied, so an implementation can keep state across forwards (communication buffers, a layout it
204205
reuses).
205206
206-
`forward(hidden_states, top_k_index, top_k_weights)` takes the batch this rank holds (global expert ids) and returns
207-
the weighted expert output for each of its tokens. It runs this rank's experts with `run_experts`, on whatever rows,
208-
local expert ids and weights it brought them.
207+
Calling it with `(hidden_states, top_k_index, top_k_weights)`, the batch this rank holds (global expert ids),
208+
returns the weighted expert output for each of its tokens. It runs this rank's experts with `run_experts`, on
209+
whatever rows, local expert ids and weights it brought them.
209210
210211
Gradients must reach every rank that holds a token, routing weights included, so a collective that autograd does not
211212
differentiate has to be wrapped in a `torch.autograd.Function`.
@@ -226,22 +227,18 @@ def __init__(self, experts, ep_mesh, tp_mesh=None):
226227
f"{type(self).__name__} needs every rank of the expert group to hold the same batch (`ep_size == tp_size`)."
227228
)
228229

229-
def forward(self, hidden_states, top_k_index, top_k_weights):
230+
def __call__(self, hidden_states, top_k_index, top_k_weights):
230231
raise NotImplementedError
231232

232233
def run_experts(self, tokens, expert_ids, weights):
233234
"""This rank's experts on `tokens`, routed to `expert_ids` (local ids) with `weights`."""
234-
if tokens.shape[0] == 0:
235-
# Keep the empty rows connected to backward, so the reverse exchange and FSDP's reduction still run.
236-
return self.experts_forward(
237-
tokens.sum(dim=0, keepdim=True), expert_ids.new_zeros((1, 1)), weights.new_ones((1, 1))
238-
)[:0]
239-
return self.experts_forward(tokens, expert_ids, weights)
240-
241-
def __call__(self, hidden_states, top_k_index, top_k_weights):
242235
with _use_local_dtensor_params(self.experts):
243-
output = self.forward(hidden_states, top_k_index, top_k_weights)
244-
return output.to(hidden_states.dtype)
236+
if tokens.shape[0] == 0:
237+
# Keep the empty rows connected to backward, so the reverse exchange and FSDP's reduction still run.
238+
return self.experts_forward(
239+
tokens.sum(dim=0, keepdim=True), expert_ids.new_zeros((1, 1)), weights.new_ones((1, 1))
240+
)[:0]
241+
return self.experts_forward(tokens, expert_ids, weights)
245242

246243

247244
class _AllToAllState(NamedTuple):
@@ -281,7 +278,7 @@ class AllToAllEP(EPImplementation):
281278
and rank 1 does not.
282279
"""
283280

284-
def forward(self, hidden_states, top_k_index, top_k_weights):
281+
def __call__(self, hidden_states, top_k_index, top_k_weights):
285282
tokens, expert_ids, weights, state = self._send(hidden_states, top_k_index, top_k_weights)
286283
return self._receive(self.run_experts(tokens, expert_ids, weights), state)
287284

@@ -370,12 +367,14 @@ class AllReduceEP(EPImplementation):
370367

371368
needs_shared_batch = True
372369

373-
def forward(self, hidden_states, top_k_index, top_k_weights):
370+
def __call__(self, hidden_states, top_k_index, top_k_weights):
374371
ep_group = self.ep_mesh.get_group()
375372
# Each rank backpropagates through its own experts only, so the gradients of the shared inputs it computes are
376373
# partial: sum them over the group (the forward all-reduce has an identity backward).
377-
hidden_states = _all_reduce_backward(hidden_states, ep_group)
378-
top_k_weights = _all_reduce_backward(top_k_weights, ep_group)
374+
if torch.is_grad_enabled() and hidden_states.requires_grad:
375+
hidden_states = _AllReduceBackward.apply(hidden_states, ep_group)
376+
if torch.is_grad_enabled() and top_k_weights.requires_grad:
377+
top_k_weights = _AllReduceBackward.apply(top_k_weights, ep_group)
379378
off_rank = (top_k_index // self.num_local_experts) != self.ep_mesh.get_local_rank()
380379
local_index = torch.where(off_rank, self.num_local_experts, top_k_index % self.num_local_experts)
381380
expert_output = self.run_experts(hidden_states, local_index, top_k_weights.masked_fill(off_rank, 0.0))
@@ -395,10 +394,10 @@ def __init__(self, experts, ep_mesh, tp_mesh=None):
395394
# Only built where it can run: an EP group whose ranks hold different batches has no all-reduce path.
396395
self.all_reduce = AllReduceEP(experts, ep_mesh, tp_mesh) if self.shares_batch else None
397396

398-
def forward(self, hidden_states, top_k_index, top_k_weights):
397+
def __call__(self, hidden_states, top_k_index, top_k_weights):
399398
if self.all_reduce is not None and not torch.is_grad_enabled():
400-
return self.all_reduce.forward(hidden_states, top_k_index, top_k_weights)
401-
return self.all_to_all.forward(hidden_states, top_k_index, top_k_weights)
399+
return self.all_reduce(hidden_states, top_k_index, top_k_weights)
400+
return self.all_to_all(hidden_states, top_k_index, top_k_weights)
402401

403402

404403
class EPImplementationInterface(GeneralInterface):
@@ -433,7 +432,7 @@ def ep_forward(hidden_states, top_k_index, top_k_weights):
433432
hidden_states = hidden_states.to_local()
434433
if isinstance(top_k_weights, DTensor):
435434
top_k_weights = top_k_weights.to_local()
436-
return implementation(hidden_states, top_k_index, top_k_weights)
435+
return implementation(hidden_states, top_k_index, top_k_weights).to(hidden_states.dtype)
437436

438437
module.forward = ep_forward
439438
return module

‎src/transformers/distributed/parallel_utils.py‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ class _AllReduceBackward(torch.autograd.Function):
122122
ranks), but the backward gradient is partial (each rank has 1/tp_size from
123123
its expert shard). We need to sum the partial gradients without dividing by
124124
world_size, which is what DTensor's Replicate backward does incorrectly.
125+
126+
Apply it only when a gradient can flow (`torch.is_grad_enabled() and x.requires_grad`): at inference the
127+
per-call overhead of an `autograd.Function` compounds across layers, e.g. in eager decode.
125128
"""
126129

127130
@staticmethod
@@ -135,14 +138,6 @@ def backward(ctx, grad):
135138
dist.all_reduce(grad, group=ctx.process_group)
136139
return grad, None
137140

138-
def _all_reduce_backward(x, process_group):
139-
"""Sum the gradient of a replicated `x` over the group in backward. Only when a gradient can flow: at
140-
inference it returns `x` untouched, so the per-call overhead of an `autograd.Function` does not compound
141-
across layers, e.g. in eager decode."""
142-
if torch.is_grad_enabled() and x.requires_grad:
143-
return _AllReduceBackward.apply(x, process_group)
144-
return x
145-
146141
class _WaitTensor(torch.autograd.Function):
147142
"""Wait on a functional collective's result, identity backward (`wait_tensor` has no autograd kernel before
148143
torch 2.11)."""

‎src/transformers/distributed/tensor_parallel.py‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
from torch.distributed.tensor import DTensor, Partial, Replicate, Shard, distribute_tensor
4848
from torch.distributed.tensor.placement_types import _StridedShard
4949

50-
from .parallel_utils import _all_reduce_backward, _AllReduceForward
50+
from .parallel_utils import _AllReduceBackward, _AllReduceForward
5151

5252

5353
def verify_tp_plan(expected_keys: list[str], tp_plan: dict[str, str] | None):
@@ -184,7 +184,8 @@ def transform_inputs_pre_forward(self, module, args, kwargs, mesh):
184184
# input-gradient contributions explicitly.
185185
if is_local_input and getattr(module, "_hf_quantized_needs_local_tp", False):
186186
process_group = mesh.get_group() if mesh.ndim == 1 else mesh.get_group("tp")
187-
x = _all_reduce_backward(x, process_group)
187+
if torch.is_grad_enabled() and x.requires_grad:
188+
x = _AllReduceBackward.apply(x, process_group)
188189
return (x,) + args[1:], kwargs
189190

190191
# DTensor path: handle regular training and layout redistribution.
@@ -438,7 +439,8 @@ def transform_output_post_forward(self, module, output, mesh):
438439
)
439440
rope_dim = module.config.qk_rope_head_dim
440441
pass_output, rope_output = output.split([output.shape[-1] - rope_dim, rope_dim], dim=-1)
441-
rope_output = _all_reduce_backward(rope_output, mesh.get_group())
442+
if torch.is_grad_enabled() and rope_output.requires_grad:
443+
rope_output = _AllReduceBackward.apply(rope_output, mesh.get_group())
442444
return torch.cat([pass_output, rope_output], dim=-1)
443445

444446

@@ -558,14 +560,16 @@ def transform_inputs_pre_forward(self, module, args, kwargs, mesh, *, is_expert_
558560
tp_group = mesh.get_group() if mesh.ndim == 1 else mesh.get_group("tp")
559561
if isinstance(hidden_states, DTensor):
560562
hidden_states = hidden_states.to_local()
561-
hidden_states = _all_reduce_backward(hidden_states, tp_group)
563+
if torch.is_grad_enabled() and hidden_states.requires_grad:
564+
hidden_states = _AllReduceBackward.apply(hidden_states, tp_group)
562565

563566
if len(routing_args) >= 2:
564567
top_k_index, top_k_weights, *extra_args = routing_args
565568
if isinstance(top_k_weights, DTensor):
566569
top_k_weights = top_k_weights.to_local()
567570
if not is_expert_parallel:
568-
top_k_weights = _all_reduce_backward(top_k_weights, tp_group)
571+
if torch.is_grad_enabled() and top_k_weights.requires_grad:
572+
top_k_weights = _AllReduceBackward.apply(top_k_weights, tp_group)
569573
routing_args = [top_k_index, top_k_weights, *extra_args]
570574

571575
return (hidden_states, *routing_args), kwargs

‎tests/expert_parallel/test_expert_parallel.py‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class Custom(EPImplementation):
105105
def __init__(self, experts, ep_mesh, tp_mesh=None):
106106
self.experts = experts
107107

108-
def forward(self, hidden_states, top_k_index, top_k_weights):
108+
def __call__(self, hidden_states, top_k_index, top_k_weights):
109109
return hidden_states + 1
110110

111111
with patch.dict(EPImplementationInterface._global_mapping, {"custom": Custom}):
@@ -175,15 +175,15 @@ def _ep_implementations_match_one_process(rank):
175175
raise AssertionError("AllReduceEP accepted an EP group without a shared batch")
176176

177177
exchanged = []
178-
all_to_all_forward = AllToAllEP.forward
178+
all_to_all_call = AllToAllEP.__call__
179179
for ep_implementation, training, exchanges in (
180180
("auto", True, True),
181181
("auto", False, False),
182182
("all_reduce", True, False),
183183
("all_to_all", False, True),
184184
):
185185
exchanged.clear()
186-
with patch.object(AllToAllEP, "forward", lambda *args: exchanged.append(True) or all_to_all_forward(*args)):
186+
with patch.object(AllToAllEP, "__call__", lambda *args: exchanged.append(True) or all_to_all_call(*args)):
187187
module = _ep_experts(rank, ep_implementation)
188188
h, w = _HIDDEN_STATES.clone().requires_grad_(), _TOP_K_WEIGHTS.clone().requires_grad_()
189189
with torch.set_grad_enabled(training):
@@ -204,19 +204,19 @@ def _ep_gradient_checkpointing_matches_reference(rank, use_reentrant):
204204
reference_output.backward(_GRAD_OUTPUT)
205205

206206
all_to_all_calls, all_reduce_calls = [], []
207-
all_to_all_forward, all_reduce_forward = AllToAllEP.forward, AllReduceEP.forward
207+
all_to_all_call, all_reduce_call = AllToAllEP.__call__, AllReduceEP.__call__
208208

209209
def record_all_to_all(self, *args):
210210
all_to_all_calls.append(True)
211-
return all_to_all_forward(self, *args)
211+
return all_to_all_call(self, *args)
212212

213213
def record_all_reduce(self, *args):
214214
all_reduce_calls.append(True)
215-
return all_reduce_forward(self, *args)
215+
return all_reduce_call(self, *args)
216216

217217
with (
218-
patch.object(AllToAllEP, "forward", record_all_to_all),
219-
patch.object(AllReduceEP, "forward", record_all_reduce),
218+
patch.object(AllToAllEP, "__call__", record_all_to_all),
219+
patch.object(AllReduceEP, "__call__", record_all_reduce),
220220
):
221221
module = _ep_experts(rank, "auto")
222222
h, w = _HIDDEN_STATES.clone().requires_grad_(), _TOP_K_WEIGHTS.clone().requires_grad_()

0 commit comments

Comments
 (0)