Skip to content

Commit 2c4ead4

Browse files
yh0903pengdurice
andauthored
Add opt-in fused AutoEP DeepEP row weighting (#8644)
## Summary - Add experimental `expert_parallel.row_weighting_impl="fused"` for the AutoEP DeepEP route at both `score_apply="pre"` and `"post"` boundaries. The default `"auto"` still resolves to the existing eager expression; unsupported configurations fail explicitly rather than falling back. - The Triton operator multiplies each received row by its FP32 routing weight, rounds at the same BF16/FP16 boundary, and leaves routing, expert GEMM, DeepEP combine, and the top-k reduction unchanged. - In a prior within-allocation H200 A/B screen, fused row weighting reduced median step time by 2.10% (four repeats, 95% CI 0.075%-3.45%). Peak allocated memory differed by 192 KiB and peak reserved memory was 510 MiB higher; the latter is an observation, not an explained or accepted memory improvement. ## Numerical semantics - The forward and row gradients use eager's FP32 products and rounding. The routing-weight gradient sums the same FP32 products in another order, so bitwise equality with eager is not expected. Against an FP64 reference at 4096 x 2048, fused/eager maximum absolute errors were 1.77e-5/1.49e-5 and maximum relative errors were 8.5e-5/2.9e-4; neither order is consistently more accurate. - The model regression snapshots retained routing-score, router-gate, full-parameter, and input gradients **after `engine.backward()` and before `engine.step()` clips gradients**, using `||fused - eager||_2 / ||eager||_2 <= 2e-2` for each checked tensor/group. Same-seed eager/eager controls were bit-exact; the largest previously measured native fused/eager model-gradient relative L2 was 5.6e-3 (reentrant checkpointing), leaving about 3.6x headroom. Near-zero gradient sign flips can cause learning-rate-scale differences in Adam's first parameter update, so coordinate-wise update equality is not used as the native-path gate. ## Testing Done - [x] Local code review completed. - [x] Unit and GPU-gated operator tests cover default selection, fail-fast inputs, score boundaries, forward/row-gradient parity, and the unmodified fused weight reduction against FP64 with an FP32 tree-sum error bound. The previously run GPU operator checks are reused; they are not a substitute for the model-level test. - [x] Native-backward distributed BF16 model tests: **2 passed, 0 skipped** (`pre` and `post`) on 4 ranks of an 8-GPU NVIDIA H100 80GB HBM3 node. Neither case replaces `d_weights`; both check exact routes and normalized errors through routing scores, router gate, all model parameters, and model input. The separate summation-order-matched attribution controls remain in place. - [x] Reviewer follow-up: only explicit eager/fused comparisons use `lr=1e-2`; the original default/cleanup/legacy cases retain `lr=1e-4`. A targeted run on 4 ranks of an 8-GPU NVIDIA H100 80GB HBM3 node passed **7/7, 0 skipped**: three config-scope cases, both cleanup/legacy cases, and eager/fused comparisons at both score boundaries. - [x] Changed-files pre-commit checks passed for all ten files touched by this PR. - A prior broad run accidentally used a stale validation bundle and did not exercise the two native-backward cases; it reported 72 passed and one legacy non-reentrant checkpoint/optimizer-delta mismatch. That legacy case passed in the targeted rerun after restoring its original learning rate. Full-suite pass is **not** claimed from the broad run. ## Dependencies and scope - The cached-layout repair (#8636) and independent MoE gradient-norm fix (#8635) have merged into upstream `master`. The GPU tests used an earlier pinned source with both corrections applied separately; neither is included in this PR. The norm fix applies to `FP16_Optimizer`, which is also used by some BF16 configurations. The new model-level gradient gate reads gradients **before** this wrapper's norm/clipping in `engine.step()`. - The integrated DeepEP route currently accepts BF16 rows. The operator also supports FP16 rows, but is CUDA/Triton-only, requires contiguous `[N, H]` rows and FP32 `[N, 1]` weights, and does not support ROCm or non-DeepEP/EP=1 routes. DeepEP v2 requires a compatible NCCL build with GIN support. --------- Signed-off-by: yh0903 <helloyu0903@gmail.com> Co-authored-by: pengdurice <pengduhit@gmail.com>
1 parent b879589 commit 2c4ead4

10 files changed

Lines changed: 749 additions & 21 deletions

File tree

‎deepspeed/module_inject/auto_ep_config.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def parse_autoep_config(param_dict: dict) -> AutoEPConfig:
6060
config.route_scale = param_dict.get("route_scale", 1.0)
6161
config.score_apply = param_dict.get("score_apply", "auto")
6262
config.combine_impl = param_dict.get("combine_impl", "auto")
63+
config.row_weighting_impl = param_dict.get("row_weighting_impl", "auto")
6364
config.comm_backend = param_dict.get("comm_backend", "comm")
6465
config.comm_num_sm = param_dict.get("comm_num_sm", 12)
6566
config.comm_qp_margin = param_dict.get("comm_qp_margin", 4)
@@ -187,12 +188,28 @@ def validate_autoep_config(
187188
raise ValueError(f"combine_impl must be one of {valid_combine_impl}, "
188189
f"got '{config.combine_impl}'")
189190

191+
# Validate row_weighting_impl
192+
valid_row_weighting_impl = ("auto", "eager", "fused")
193+
if config.row_weighting_impl not in valid_row_weighting_impl:
194+
raise ValueError(f"row_weighting_impl must be one of {valid_row_weighting_impl}, "
195+
f"got '{config.row_weighting_impl}'")
196+
190197
# Validate comm_backend
191198
valid_comm_backend = ("comm", "deepep")
192199
if config.comm_backend not in valid_comm_backend:
193200
raise ValueError(f"comm_backend must be one of {valid_comm_backend}, "
194201
f"got '{config.comm_backend}'")
195202

203+
if config.row_weighting_impl == "fused":
204+
if config.comm_backend != "deepep":
205+
raise ValueError('row_weighting_impl="fused" only runs inside the DeepEP route, but '
206+
f'comm_backend="{config.comm_backend}" was selected. Set comm_backend to '
207+
'"deepep", or leave row_weighting_impl unset.')
208+
if config.autoep_size == 1:
209+
raise ValueError('row_weighting_impl="fused" only runs when DeepEP dispatch is active '
210+
'(autoep_size > 1), but autoep_size=1. Increase autoep_size, or leave '
211+
"row_weighting_impl unset.")
212+
196213
# A zero budget would hand the whole GPU to the collective, and a negative
197214
# one is meaningless; both are worth rejecting where the value is written
198215
# rather than inside a buffer constructor.

‎deepspeed/module_inject/auto_ep_layer.py‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ def resolve_combine_impl(
107107
return "weighted_sum"
108108

109109

110+
def resolve_row_weighting_impl(config_override: Literal["auto", "eager", "fused"]) -> Literal["eager", "fused"]:
111+
"""Resolve DeepEP row weighting implementation from config override."""
112+
if config_override != "auto":
113+
return config_override
114+
return "eager"
115+
116+
110117
def _copy_parameter_data(target: nn.Parameter, source: torch.Tensor) -> None:
111118
full_shape = torch.Size(getattr(source, "ds_shape", source.shape))
112119
with torch.no_grad():
@@ -158,6 +165,17 @@ def apply_scores_before_experts_if_enabled(
158165
return routed_input
159166

160167

168+
def apply_deepep_row_weights(
169+
rows: torch.Tensor,
170+
weights: torch.Tensor,
171+
row_weighting_impl: Literal["eager", "fused"],
172+
) -> torch.Tensor:
173+
"""Apply DeepEP's per-arrival routing weights at the configured boundary."""
174+
if row_weighting_impl == "fused":
175+
return fused_token_ops.fused_row_weighting(rows, weights)
176+
return (rows.float() * weights).to(rows.dtype)
177+
178+
161179
def _split_plan_from_expert_counts(
162180
num_tokens_per_expert: torch.Tensor, # [E_global], int32
163181
ep_size: int,
@@ -502,7 +520,9 @@ def __init__(
502520
self.top_k = spec.top_k
503521
self.score_apply = resolve_score_apply_mode(spec, config.score_apply)
504522
self.combine_impl = resolve_combine_impl(config.combine_impl)
523+
self.row_weighting_impl = resolve_row_weighting_impl(config.row_weighting_impl)
505524
self._fused_combine_checked = False
525+
self._fused_row_weighting_checked = False
506526
route_norm = spec.route_norm if config.route_norm is None else config.route_norm
507527
self.ep_size = ep_size
508528
self.ep_rank = ep_rank
@@ -819,7 +839,7 @@ def _deepep_route(self, tokens: torch.Tensor, ro: "RouterOutput") -> torch.Tenso
819839
# doesn't commute with the weight.
820840
weights = None if recv_weights is None else recv_weights[:arrived].reshape(-1, 1)
821841
if weights is not None and self.score_apply == "pre":
822-
received = (received.float() * weights).to(received.dtype)
842+
received = apply_deepep_row_weights(received, weights, self.row_weighting_impl)
823843
weights = None
824844

825845
# The grouped GEMM needs per-expert row counts, which arrive as a
@@ -834,7 +854,7 @@ def _deepep_route(self, tokens: torch.Tensor, ro: "RouterOutput") -> torch.Tenso
834854
expert_output = self.experts(received, counts)
835855

836856
if weights is not None:
837-
expert_output = (expert_output.float() * weights).to(expert_output.dtype)
857+
expert_output = apply_deepep_row_weights(expert_output, weights, self.row_weighting_impl)
838858

839859
return deepep_combine(exchange, expert_output, handle)
840860

@@ -909,6 +929,10 @@ def _forward(self, hidden_states: torch.Tensor) -> torch.Tensor | tuple[torch.Te
909929
if self.combine_impl == "fused_weighted_sum" and not self._fused_combine_checked:
910930
fused_token_ops.assert_supported(x, score_apply=self.score_apply)
911931
self._fused_combine_checked = True
932+
if self.row_weighting_impl == "fused" and not self._fused_row_weighting_checked:
933+
weights = x.new_empty((x.shape[0], 1), dtype=torch.float32)
934+
fused_token_ops.assert_row_weighting_supported(x, weights)
935+
self._fused_row_weighting_checked = True
912936

913937
# Router
914938
ro: RouterOutput = RouterOutput(*self.router(x, self.expert_bias))

‎deepspeed/module_inject/auto_ep_presets/base.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ class AutoEPConfig:
122122
route_scale: float = 1.0
123123
score_apply: Literal["auto", "pre", "post"] = "auto"
124124
combine_impl: Literal["auto", "weighted_sum", "fused_weighted_sum", "legacy_bmm"] = "auto"
125+
row_weighting_impl: Literal["auto", "eager", "fused"] = "auto"
125126
comm_backend: Literal["comm", "deepep"] = "comm"
126127
comm_num_sm: int = 12
127128
comm_qp_margin: int = 4

‎deepspeed/ops/triton_ops/autoep_fused_token_ops.py‎

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
_IS_ROCM_PYTORCH = getattr(torch.version, "hip", None) is not None
1616

1717
SUPPORTED_ROW_DTYPES = (torch.bfloat16, torch.float16, torch.float32)
18+
SUPPORTED_ROW_WEIGHTING_DTYPES = (torch.bfloat16, torch.float16)
1819

1920
_MAX_BLOCK_HIDDEN = 512
2021
# The forward adds one row at a time, so only a single [BLOCK_H] FP32 accumulator is live.
@@ -142,6 +143,84 @@ def _weighted_restore_backward_kernel(
142143
mask=slot_mask,
143144
)
144145

146+
@triton.jit
147+
def _row_weighting_forward_kernel(
148+
rows_ptr,
149+
weights_ptr,
150+
out_ptr,
151+
hidden,
152+
rows_stride,
153+
weights_stride,
154+
out_stride,
155+
BLOCK_H: tl.constexpr,
156+
):
157+
row = tl.program_id(0).to(tl.int64)
158+
hidden_offsets = tl.program_id(1) * BLOCK_H + tl.arange(0, BLOCK_H)
159+
hidden_mask = hidden_offsets < hidden
160+
161+
values = tl.load(rows_ptr + row * rows_stride + hidden_offsets, mask=hidden_mask, other=0.0).to(tl.float32)
162+
weight = tl.load(weights_ptr + row * weights_stride).to(tl.float32)
163+
tl.store(
164+
out_ptr + row * out_stride + hidden_offsets,
165+
(values * weight).to(out_ptr.dtype.element_ty),
166+
mask=hidden_mask,
167+
)
168+
169+
@triton.jit
170+
def _row_weighting_backward_partial_kernel(
171+
grad_out_ptr,
172+
rows_ptr,
173+
weights_ptr,
174+
grad_rows_ptr,
175+
partials_ptr,
176+
hidden,
177+
grad_out_stride,
178+
rows_stride,
179+
weights_stride,
180+
grad_rows_stride,
181+
num_hidden_blocks: tl.constexpr,
182+
BLOCK_H: tl.constexpr,
183+
):
184+
row = tl.program_id(0).to(tl.int64)
185+
hidden_block = tl.program_id(1)
186+
hidden_offsets = hidden_block * BLOCK_H + tl.arange(0, BLOCK_H)
187+
hidden_mask = hidden_offsets < hidden
188+
189+
upstream = tl.load(
190+
grad_out_ptr + row * grad_out_stride + hidden_offsets,
191+
mask=hidden_mask,
192+
other=0.0,
193+
).to(tl.float32)
194+
values = tl.load(
195+
rows_ptr + row * rows_stride + hidden_offsets,
196+
mask=hidden_mask,
197+
other=0.0,
198+
).to(tl.float32)
199+
weight = tl.load(weights_ptr + row * weights_stride).to(tl.float32)
200+
201+
tl.store(
202+
grad_rows_ptr + row * grad_rows_stride + hidden_offsets,
203+
(upstream * weight).to(grad_rows_ptr.dtype.element_ty),
204+
mask=hidden_mask,
205+
)
206+
partial = tl.sum(upstream * values, axis=0)
207+
tl.store(partials_ptr + row * num_hidden_blocks + hidden_block, partial)
208+
209+
@triton.jit
210+
def _row_weighting_backward_reduce_kernel(
211+
partials_ptr,
212+
grad_weights_ptr,
213+
grad_weights_stride,
214+
NUM_HIDDEN_BLOCKS: tl.constexpr,
215+
BLOCK_B: tl.constexpr,
216+
):
217+
row = tl.program_id(0).to(tl.int64)
218+
offsets = tl.arange(0, BLOCK_B)
219+
mask = offsets < NUM_HIDDEN_BLOCKS
220+
partials = tl.load(partials_ptr + row * NUM_HIDDEN_BLOCKS + offsets, mask=mask, other=0.0)
221+
grad_weight = tl.sum(partials, axis=0)
222+
tl.store(grad_weights_ptr + row * grad_weights_stride, grad_weight)
223+
145224

146225
def is_available() -> bool:
147226
"""Whether this build can run the fused weighted restore at all."""
@@ -174,6 +253,16 @@ def _block_hidden(hidden: int, slots: int) -> int:
174253
return min(_MAX_BLOCK_HIDDEN, budget, max(16, triton.next_power_of_2(hidden)))
175254

176255

256+
def _row_weighting_block_hidden(hidden: int) -> int:
257+
"""Choose a power-of-two hidden tile for row-local products."""
258+
return min(_MAX_BLOCK_HIDDEN, max(16, triton.next_power_of_2(hidden)))
259+
260+
261+
def _row_weighting_reduce_block(num_hidden_blocks: int) -> int:
262+
"""Round the per-row partial count up to a power of two for ``tl.arange``."""
263+
return max(1, triton.next_power_of_2(num_hidden_blocks))
264+
265+
177266
def _padded_top_k(top_k: int) -> int:
178267
"""Round top-k up to a power of two, which ``tl.arange`` requires."""
179268
return max(2, triton.next_power_of_2(top_k))
@@ -256,6 +345,114 @@ def backward(ctx, grad_output):
256345
return grad_rows, grad_scores, None, None
257346

258347

348+
def assert_row_weighting_supported(rows: torch.Tensor, weights: torch.Tensor) -> None:
349+
"""Reject unsupported per-row weighting configurations before dispatch."""
350+
if not _TRITON_AVAILABLE:
351+
raise RuntimeError('row_weighting_impl="fused" needs Triton, which is not installed in this environment. '
352+
'Install Triton, or set row_weighting_impl="eager".')
353+
if _IS_ROCM_PYTORCH:
354+
raise RuntimeError('row_weighting_impl="fused" is not yet supported on ROCm. Set '
355+
'row_weighting_impl="eager" to run here.')
356+
if rows.device.type != "cuda":
357+
raise RuntimeError('row_weighting_impl="fused" runs CUDA kernels but rows are on device '
358+
f'"{rows.device.type}". Set row_weighting_impl="eager" to run here.')
359+
if weights.device != rows.device:
360+
raise RuntimeError('row_weighting_impl="fused" requires rows and weights on the same CUDA device, got '
361+
f"rows on {rows.device} and weights on {weights.device}.")
362+
if rows.dim() != 2:
363+
raise RuntimeError('row_weighting_impl="fused" requires rows with shape [N, H], got '
364+
f"{tuple(rows.shape)}. Set row_weighting_impl=\"eager\" for unsupported shapes.")
365+
expected_weight_shape = (rows.shape[0], 1)
366+
if tuple(weights.shape) != expected_weight_shape:
367+
raise RuntimeError('row_weighting_impl="fused" requires weights with shape [N, 1] matching rows, got '
368+
f"{tuple(weights.shape)} for rows {tuple(rows.shape)}.")
369+
if rows.dtype not in SUPPORTED_ROW_WEIGHTING_DTYPES:
370+
raise RuntimeError('row_weighting_impl="fused" supports bfloat16 and float16 rows, got '
371+
f"{rows.dtype}. Set row_weighting_impl=\"eager\", or use a supported dtype.")
372+
if weights.dtype != torch.float32:
373+
raise RuntimeError('row_weighting_impl="fused" requires float32 weights, got '
374+
f"{weights.dtype}. Set row_weighting_impl=\"eager\" for unsupported weights.")
375+
if not rows.is_contiguous():
376+
raise RuntimeError('row_weighting_impl="fused" requires contiguous rows. Set row_weighting_impl="eager" '
377+
"for non-contiguous row layouts.")
378+
if not weights.is_contiguous():
379+
raise RuntimeError('row_weighting_impl="fused" requires contiguous weights. Set row_weighting_impl="eager" '
380+
"for non-contiguous weight layouts.")
381+
382+
383+
class _FusedRowWeighting(torch.autograd.Function):
384+
"""Apply one FP32 routing weight per row, with the eager rounding point."""
385+
386+
@staticmethod
387+
def forward(ctx, rows, weights):
388+
output = torch.empty_like(rows)
389+
ctx.save_for_backward(rows, weights)
390+
391+
n_rows, hidden = rows.shape
392+
if n_rows == 0 or hidden == 0:
393+
return output
394+
395+
block_hidden = _row_weighting_block_hidden(hidden)
396+
grid = (n_rows, triton.cdiv(hidden, block_hidden))
397+
_row_weighting_forward_kernel[grid](
398+
rows,
399+
weights,
400+
output,
401+
hidden,
402+
rows.stride(0),
403+
weights.stride(0),
404+
output.stride(0),
405+
BLOCK_H=block_hidden,
406+
)
407+
return output
408+
409+
@staticmethod
410+
def backward(ctx, grad_output):
411+
rows, weights = ctx.saved_tensors
412+
grad_output = grad_output.contiguous()
413+
grad_rows = torch.empty_like(rows)
414+
grad_weights = torch.empty_like(weights)
415+
416+
n_rows, hidden = rows.shape
417+
if n_rows == 0:
418+
return grad_rows, grad_weights
419+
if hidden == 0:
420+
grad_weights.zero_()
421+
return grad_rows, grad_weights
422+
423+
block_hidden = _row_weighting_block_hidden(hidden)
424+
num_hidden_blocks = triton.cdiv(hidden, block_hidden)
425+
partials = torch.empty((n_rows, num_hidden_blocks), dtype=torch.float32, device=rows.device)
426+
_row_weighting_backward_partial_kernel[(n_rows, num_hidden_blocks)](
427+
grad_output,
428+
rows,
429+
weights,
430+
grad_rows,
431+
partials,
432+
hidden,
433+
grad_output.stride(0),
434+
rows.stride(0),
435+
weights.stride(0),
436+
grad_rows.stride(0),
437+
num_hidden_blocks=num_hidden_blocks,
438+
BLOCK_H=block_hidden,
439+
)
440+
_row_weighting_backward_reduce_kernel[(n_rows, )](
441+
partials,
442+
grad_weights,
443+
grad_weights.stride(0),
444+
NUM_HIDDEN_BLOCKS=num_hidden_blocks,
445+
BLOCK_B=_row_weighting_reduce_block(num_hidden_blocks),
446+
)
447+
return grad_rows, grad_weights
448+
449+
450+
def fused_row_weighting(rows: torch.Tensor, weights: torch.Tensor) -> torch.Tensor:
451+
"""Apply ``weights`` to ``rows`` as ``(rows.float() * weights).to(rows.dtype)``."""
452+
assert_row_weighting_supported(rows, weights)
453+
return _FusedRowWeighting.apply(rows, weights)
454+
455+
259456
def fused_weighted_restore(
260457
combined_rows: torch.Tensor,
261458
top_scores: torch.Tensor,

‎docs/_pages/config-json.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,12 @@ smoke coverage used for this AutoEP surface produced the following version gates
980980
| -------------------------------------------------------------------------------------------------------------- | -------- |
981981
| How expert outputs are weighted by their router scores and reduced over top-k. `"auto"` resolves to `"weighted_sum"`. `"fused_weighted_sum"` is experimental and computes the same reduction in one Triton pass, without materializing the scattered assignment buffer or the `[tokens, top_k, hidden]` FP32 intermediate; it requires CUDA, Triton, bfloat16/float16 activations, `tensor_parallel.autotp_size=1`, `expert_tensor_parallel_size=1`, and a resolved `score_apply="post"`, and is rejected rather than silently ignored when any of those does not hold. `"legacy_bmm"` is a debug reduction retained for model-family verification. | `"auto"` |
982982

983+
***row_weighting_impl***: [string]
984+
985+
| Description | Default |
986+
| -------------------------------------------------------------------------------------------------------------- | -------- |
987+
| How the DeepEP route applies one FP32 routing weight to each received row at the existing `score_apply` boundary. `"auto"` resolves to `"eager"`, preserving `(rows.float() * weights).to(rows.dtype)`. `"fused"` is experimental and uses a separate Triton pointwise operator for that per-row product only; it does not perform the top-k reduction or move the BF16/FP16 rounding point. It requires `comm_backend="deepep"`, `autoep_size > 1`, CUDA, Triton, contiguous bfloat16/float16 rows shaped `[N, H]`, and contiguous FP32 weights shaped `[N, 1]` on the same device; DeepEP dispatch currently supports BF16 rows only. Fused weight gradients can differ from eager due to FP32 summation order. Unsupported configurations fail rather than falling back. | `"auto"` |
988+
983989
***route_norm***: [boolean]
984990

985991
| Description | Default |

0 commit comments

Comments
 (0)