From d29d28ba20a3e3679a2da21e40845818c0893920 Mon Sep 17 00:00:00 2001 From: yh0903 Date: Fri, 4 Sep 2026 00:03:54 -0700 Subject: [PATCH 1/6] Bypass collective preparation for AutoEP DeepEP Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: yh0903 --- deepspeed/module_inject/auto_ep_layer.py | 82 ++++---- tests/unit/module_inject/test_auto_ep_comm.py | 84 ++++++++ .../unit/v1/moe/test_autoep_deepep_parity.py | 197 +++++++++++++++++- 3 files changed, 309 insertions(+), 54 deletions(-) diff --git a/deepspeed/module_inject/auto_ep_layer.py b/deepspeed/module_inject/auto_ep_layer.py index c00fb8108e5b..252028f3e9ce 100644 --- a/deepspeed/module_inject/auto_ep_layer.py +++ b/deepspeed/module_inject/auto_ep_layer.py @@ -695,6 +695,34 @@ def _deepep_route(self, tokens: torch.Tensor, ro: "RouterOutput") -> torch.Tenso return deepep_combine(exchange, expert_output, handle) + def _finalize_output(self, output: torch.Tensor, x: torch.Tensor, hidden_states: torch.Tensor, + hdim: int) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]: + """Apply the model-specific output tail shared by every communication backend.""" + if self.moe_output_shape == "flat": + output = output.reshape(-1, hdim) + shared_expert_input = x + elif self.shared_experts_gate is not None: + shared_expert_input = x + else: + shared_expert_input = hidden_states + + if self.shared_experts is not None: + shared_expert_output = self.shared_experts(shared_expert_input) + if self.shared_experts_gate is not None: + shared_expert_gate = torch.sigmoid(self.shared_experts_gate(shared_expert_input)) + shared_expert_output = shared_expert_gate * shared_expert_output + if shared_expert_output.shape != output.shape: + shared_expert_output = shared_expert_output.reshape_as(output) + output = output + shared_expert_output + + if self.return_router_logits: + logits = self._cached_router_logits + self._cached_router_logits = None + return output, logits + + self._cached_router_logits = None + return output + def forward( self, hidden_states: torch.Tensor, @@ -723,15 +751,16 @@ def forward( with torch.no_grad(): self.tokens_per_expert.add_(ro.num_tokens_per_expert) + if self.ep_size > 1 and self.comm_backend == DEEPEP_BACKEND: + output = self._deepep_route(x, ro).reshape(bsz, seqlen, hdim) + return self._finalize_output(output, x, hidden_states, hdim) + # Reorder tokens into expert-contiguous order. token_indices_sorted = torch.argsort(ro.selected_experts.view(-1), stable=True) top_scores_sorted = ro.top_scores.view(-1)[token_indices_sorted] expert_indices_sorted = ro.selected_experts.reshape(-1).index_select(0, token_indices_sorted) folded_tp = self.folding_group_handles is not None and self.folding_group_handles.spec.tp_size > 1 - # Set only where DeepEP's combine actually produced the output, since - # that decides whether the reduction below has already happened. - deepep_combined = False restore_ctx = None if folded_tp: from deepspeed.moe.ep_tp_dispatch import ( @@ -809,18 +838,14 @@ def forward( num_tokens_per_expert=ro.num_tokens_per_expert, ) - if self.comm_backend == DEEPEP_BACKEND: - expert_output = self._deepep_route(x, ro) - deepep_combined = True - else: - routed_input = _AllToAllV.apply(self.ep_group, routed_input, plan.input_splits, plan.output_splits) + routed_input = _AllToAllV.apply(self.ep_group, routed_input, plan.input_splits, plan.output_splits) - routed_input, perm_indices, aligned_counts, n_tokens = permute_by_local_expert( - routed_input, plan.local_counts_by_source) - expert_output = self.experts(routed_input, aligned_counts) - expert_output = unpermute_by_local_expert(expert_output, perm_indices, n_tokens) + routed_input, perm_indices, aligned_counts, n_tokens = permute_by_local_expert( + routed_input, plan.local_counts_by_source) + expert_output = self.experts(routed_input, aligned_counts) + expert_output = unpermute_by_local_expert(expert_output, perm_indices, n_tokens) - expert_output = _AllToAllV.apply(self.ep_group, expert_output, plan.output_splits, plan.input_splits) + expert_output = _AllToAllV.apply(self.ep_group, expert_output, plan.output_splits, plan.input_splits) if folded_tp: output = restore_combined(expert_output, @@ -828,12 +853,6 @@ def forward( tp_group=self.tp_group, validate_coverage=self.validate_folding_routing).reshape(bsz, seqlen, hdim) self._last_folding_dispatch_counters = dispatch_counters(restore_ctx) - elif deepep_combined: - # DeepEP's combine already reduced over top-k and restored token - # order. This is keyed on the route having run rather than on the - # backend being selected: with ep_size == 1 the local path runs - # instead and still has one row per assignment to reduce. - output = expert_output.reshape(bsz, seqlen, hdim) elif self.combine_impl == "fused_weighted_sum": output = fused_token_ops.fused_weighted_restore( expert_output, @@ -853,27 +872,4 @@ def forward( shape=(bsz, seqlen, hdim), ) - if self.moe_output_shape == "flat": - output = output.reshape(-1, hdim) - shared_expert_input = x - elif self.shared_experts_gate is not None: - shared_expert_input = x - else: - shared_expert_input = hidden_states - - if self.shared_experts is not None: - shared_expert_output = self.shared_experts(shared_expert_input) - if self.shared_experts_gate is not None: - shared_expert_gate = torch.sigmoid(self.shared_experts_gate(shared_expert_input)) - shared_expert_output = shared_expert_gate * shared_expert_output - if shared_expert_output.shape != output.shape: - shared_expert_output = shared_expert_output.reshape_as(output) - output = output + shared_expert_output - - if self.return_router_logits: - logits = self._cached_router_logits - self._cached_router_logits = None - return output, logits - - self._cached_router_logits = None - return output + return self._finalize_output(output, x, hidden_states, hdim) diff --git a/tests/unit/module_inject/test_auto_ep_comm.py b/tests/unit/module_inject/test_auto_ep_comm.py index 4f5fff013633..dd8e1f938deb 100644 --- a/tests/unit/module_inject/test_auto_ep_comm.py +++ b/tests/unit/module_inject/test_auto_ep_comm.py @@ -308,6 +308,90 @@ def test_row_count_comes_from_the_handle_not_the_device(self): self.assertNotIn("psum_num_recv_tokens_per_expert[-1]", source) +class TestDeepEPEarlyRoute(unittest.TestCase): + """DeepEP must branch before collective-only token preparation.""" + + class Router(torch.nn.Module): + + def __init__(self, output): + super().__init__() + self.output = output + + def forward(self, *_args): + return self.output + + @staticmethod + def layer(ep_size=2, comm_backend=DEEPEP_BACKEND, *, return_router_logits=False): + layer = object.__new__(auto_ep_layer.AutoEPMoELayer) + torch.nn.Module.__init__(layer) + scores = torch.tensor([[0.75, 0.25], [0.6, 0.4]]) + experts = torch.tensor([[1, 0], [1, 0]], dtype=torch.long) + counts = torch.tensor([2, 2], dtype=torch.int32) + layer.router = TestDeepEPEarlyRoute.Router((scores, experts, counts)) + layer.expert_bias = None + layer.register_buffer("tokens_per_expert", torch.zeros_like(counts, dtype=torch.float32)) + layer.combine_impl = "weighted_sum" + layer._fused_combine_checked = False + layer.ep_size = ep_size + layer.comm_backend = comm_backend + layer.top_k = 2 + layer.num_experts = 2 + layer.num_local_experts = 1 + layer.folding_group_handles = None + layer.score_apply = "post" + layer.shared_experts = None + layer.shared_experts_gate = None + layer.moe_output_shape = "batched" + layer.return_router_logits = return_router_logits + layer._cached_router_logits = torch.randn(2, 2) if return_router_logits else None + layer._deepep_route = mock.Mock(return_value=torch.ones((2, 4))) + return layer + + def test_deepep_bypasses_collective_preparation(self): + layer = self.layer() + hidden = torch.randn(1, 2, 4) + + with mock.patch.object(auto_ep_layer.torch, "argsort", side_effect=AssertionError("argsort ran")), \ + mock.patch.object(auto_ep_layer, "compute_split_plan", + side_effect=AssertionError("split plan ran")), \ + mock.patch.object(auto_ep_layer, "compute_split_plan_from_expert_indices", + side_effect=AssertionError("folded split plan ran")), \ + mock.patch.object(auto_ep_layer, "apply_scores_before_experts_if_enabled", + side_effect=AssertionError("score application ran")): + output = auto_ep_layer.AutoEPMoELayer.forward(layer, hidden) + + self.assertEqual(tuple(output.shape), (1, 2, 4)) + tokens, router_output = layer._deepep_route.call_args.args + self.assertEqual(tuple(tokens.shape), (2, 4)) + self.assertTrue(torch.equal(tokens, hidden.reshape(2, 4))) + self.assertTrue(torch.equal(router_output.selected_experts, torch.tensor([[1, 0], [1, 0]]))) + self.assertTrue(torch.equal(layer.tokens_per_expert, torch.tensor([2.0, 2.0]))) + + def test_standard_comm_and_ep1_keep_the_existing_path(self): + for ep_size, backend in ((2, COMM_BACKEND), (1, DEEPEP_BACKEND)): + with self.subTest(ep_size=ep_size, backend=backend): + layer = self.layer(ep_size=ep_size, comm_backend=backend) + with mock.patch.object(auto_ep_layer.torch, "argsort", side_effect=RuntimeError("sort reached")): + with self.assertRaisesRegex(RuntimeError, "sort reached"): + auto_ep_layer.AutoEPMoELayer.forward(layer, torch.randn(1, 2, 4)) + layer._deepep_route.assert_not_called() + + def test_shared_tail_and_router_logits_are_preserved(self): + layer = self.layer(return_router_logits=True) + expected_logits = layer._cached_router_logits + layer.moe_output_shape = "flat" + layer.shared_experts = mock.Mock(side_effect=lambda x: x * 2) + hidden = torch.arange(8, dtype=torch.float32).reshape(1, 2, 4) + + output, logits = auto_ep_layer.AutoEPMoELayer.forward(layer, hidden) + + self.assertEqual(tuple(output.shape), (2, 4)) + self.assertTrue(torch.equal(output, torch.ones_like(output) + hidden.reshape(2, 4) * 2)) + self.assertIs(logits, expected_logits) + self.assertIsNone(layer._cached_router_logits) + layer.shared_experts.assert_called_once() + + class TestBufferLifecycle(unittest.TestCase): """The statically sized buffer never makes a rank-local resize decision.""" diff --git a/tests/unit/v1/moe/test_autoep_deepep_parity.py b/tests/unit/v1/moe/test_autoep_deepep_parity.py index 544e81912007..825d342c9e35 100644 --- a/tests/unit/v1/moe/test_autoep_deepep_parity.py +++ b/tests/unit/v1/moe/test_autoep_deepep_parity.py @@ -16,10 +16,16 @@ Requires GPUs and a DeepEP build, so it is opt-in. """ +import functools + import pytest import torch +from torch.utils.checkpoint import checkpoint import deepspeed +from deepspeed.module_inject import auto_ep_layer +from deepspeed.module_inject.auto_ep_comm import destroy_exchanges +from deepspeed.module_inject.auto_ep_layer import AutoEPMoELayer from unit.common import DistributedTest from unit.v1.moe.autoep_test_utils import ( @@ -44,7 +50,62 @@ def _deepep_available() -> bool: return True -def _run_one_step(backend, ep_size, seed): +def _install_legacy_deepep_prep(engine): + """Reproduce the pre-cleanup work without changing DeepEP mathematics.""" + for module in engine.module.modules(): + if not isinstance(module, AutoEPMoELayer): + continue + deepep_route = module._deepep_route + + @functools.wraps(deepep_route) + def legacy_deepep_route(tokens, ro, *, _module=module, _deepep_route=deepep_route): + token_indices_sorted = torch.argsort(ro.selected_experts.view(-1), stable=True) + top_scores_sorted = ro.top_scores.view(-1)[token_indices_sorted] + ro.selected_experts.reshape(-1).index_select(0, token_indices_sorted) + routed_input = tokens[token_indices_sorted // _module.top_k] + auto_ep_layer.apply_scores_before_experts_if_enabled(routed_input, + top_scores_sorted, + score_apply=_module.score_apply) + auto_ep_layer.compute_split_plan( + selected_experts=ro.selected_experts, + num_experts=_module.num_experts, + ep_size=_module.ep_size, + num_local_experts=_module.num_local_experts, + ep_group=_module.ep_group, + num_tokens_per_expert=ro.num_tokens_per_expert, + ) + return _deepep_route(tokens, ro) + + module._deepep_route = legacy_deepep_route + + +def _install_skewed_routing(engine): + for module in engine.module.modules(): + if not isinstance(module, AutoEPMoELayer): + continue + router = module.router + + def skewed_forward(hidden_states, _expert_bias, *, _router=router): + logits = _router.gate(hidden_states) + scores = torch.softmax(logits.float(), dim=-1).to(hidden_states.dtype) + pattern = torch.tensor([[1, 0], [1, 0], [2, 0], [1, 2]], dtype=torch.long, device=hidden_states.device) + selected_experts = pattern.repeat((hidden_states.shape[0] + pattern.shape[0] - 1) // pattern.shape[0], + 1)[:hidden_states.shape[0]] + top_scores = scores.gather(1, selected_experts) + top_scores = top_scores / top_scores.sum(dim=-1, keepdim=True) + counts = torch.bincount(selected_experts.flatten(), minlength=_router.num_experts).to(torch.int32) + return top_scores, selected_experts, counts + + router.forward = skewed_forward + + +def _checkpoint_autoep_layers(engine): + for module in engine.module.modules(): + if isinstance(module, AutoEPMoELayer): + module.forward = functools.partial(checkpoint, module.forward, use_reentrant=False) + + +def _run_one_step(backend, ep_size, seed, *, cleanup=True, activation_checkpointing=False, skewed_routing=False): """Build a model on ``backend``, run one step, return its output and grads.""" seed_everything(seed) @@ -72,11 +133,37 @@ def _run_one_step(backend, ep_size, seed): elif name.endswith("experts.down_proj"): parameter.mul_(INTERMEDIATE_SIZE**-0.5) engine, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config) + if backend == "deepep" and not cleanup: + _install_legacy_deepep_prep(engine) + if skewed_routing: + _install_skewed_routing(engine) + if activation_checkpointing: + _checkpoint_autoep_layers(engine) # Reseeded so the input is identical on every rank and across backends: the # comparison is of the transport, so nothing else may differ. seed_everything(seed) - hidden = torch.randn(1, SEQ_LEN, HIDDEN_SIZE, device=engine.device, dtype=engine_input_dtype(engine)) + hidden = torch.randn(1, SEQ_LEN, HIDDEN_SIZE, device=engine.device, + dtype=engine_input_dtype(engine)).requires_grad_(True) + parameters_before = { + name: parameter.detach().float().clone() + for name, parameter in engine.module.named_parameters() + } + routes = [] + score_tensors = [] + hooks = [] + for name, module in engine.module.named_modules(): + if not isinstance(module, AutoEPMoELayer): + continue + + def capture_route(_module, _inputs, output, *, _name=name): + scores, selected_experts = output[:2] + if scores.requires_grad: + scores.retain_grad() + score_tensors.append((_name, scores)) + routes.append((_name, selected_experts.detach().cpu().clone())) + + hooks.append(module.router.register_forward_hook(capture_route)) output = engine(hidden) loss = output.float().pow(2).mean() @@ -86,7 +173,56 @@ def _run_one_step(backend, ep_size, seed): name: parameter.grad.detach().float().clone() for name, parameter in engine.module.named_parameters() if parameter.grad is not None } - return output.detach().float().clone(), gradients + score_gradients = [(name, scores.grad.detach().float().clone()) for name, scores in score_tensors + if scores.grad is not None] + input_gradient = hidden.grad.detach().float().clone() + engine.step() + parameter_deltas = { + name: parameter.detach().float() - parameters_before[name] + for name, parameter in engine.module.named_parameters() + } + for hook in hooks: + hook.remove() + result = { + "output": output.detach().float().clone(), + "loss": loss.detach().float().clone(), + "routes": routes, + "input_gradient": input_gradient, + "score_gradients": score_gradients, + "gradients": gradients, + "parameter_deltas": parameter_deltas, + } + if backend == "deepep": + destroy_exchanges(engine.module) + return result + + +def _assert_cleanup_results_close(actual, expected): + torch.testing.assert_close(actual["output"], expected["output"], rtol=2e-3, atol=2e-3) + torch.testing.assert_close(actual["loss"], expected["loss"], rtol=2e-3, atol=2e-3) + torch.testing.assert_close(actual["input_gradient"], expected["input_gradient"], rtol=5e-3, atol=5e-3) + assert len(actual["routes"]) == len(expected["routes"]) + for (actual_name, actual_route), (expected_name, expected_route) in zip(actual["routes"], expected["routes"]): + assert actual_name == expected_name + assert torch.equal(actual_route, expected_route) + assert len(actual["score_gradients"]) == len(expected["score_gradients"]) + for (actual_name, actual_grad), (expected_name, expected_grad) in zip(actual["score_gradients"], + expected["score_gradients"]): + assert actual_name == expected_name + torch.testing.assert_close(actual_grad, expected_grad, rtol=5e-3, atol=5e-3) + assert actual["gradients"].keys() == expected["gradients"].keys() + assert actual["parameter_deltas"].keys() == expected["parameter_deltas"].keys() + for name in actual["gradients"]: + torch.testing.assert_close(actual["gradients"][name], + expected["gradients"][name], + rtol=5e-3, + atol=5e-3, + msg=f"gradient for {name}") + torch.testing.assert_close(actual["parameter_deltas"][name], + expected["parameter_deltas"][name], + rtol=5e-3, + atol=5e-4, + msg=f"optimizer delta for {name}") @pytest.mark.skipif(not _deepep_available(), reason="deep_ep is not installed") @@ -99,17 +235,22 @@ class TestDeepEPMatchesCollective(DistributedTest): def test_forward_and_backward_match_the_collective_path(self): skip_unless_h100_tests_enabled("DeepEP parity needs H100s and a DeepEP build") - collective_output, collective_grads = _run_one_step("comm", self.world_size, seed=1234) - deepep_output, deepep_grads = _run_one_step("deepep", self.world_size, seed=1234) + collective = _run_one_step("comm", self.world_size, seed=1234) + deepep = _run_one_step("deepep", self.world_size, seed=1234) # bfloat16 with a different reduction order, so exact equality is not # the bar. A dropped weight or a missing expert is orders of magnitude # larger than a reordered sum. - torch.testing.assert_close(deepep_output, collective_output, rtol=2e-2, atol=2e-2) + torch.testing.assert_close(deepep["output"], collective["output"], rtol=2e-2, atol=2e-2) - assert set(deepep_grads) == set(collective_grads), "the two paths produced gradients for different parameters" - for name, expected in collective_grads.items(): - torch.testing.assert_close(deepep_grads[name], expected, rtol=5e-2, atol=5e-2, msg=f"gradient for {name}") + assert set(deepep["gradients"]) == set( + collective["gradients"]), "the two paths produced gradients for different parameters" + for name, expected in collective["gradients"].items(): + torch.testing.assert_close(deepep["gradients"][name], + expected, + rtol=5e-2, + atol=5e-2, + msg=f"gradient for {name}") def test_the_router_gate_receives_gradients(self): """The gate silently never learning is what a dropped weight costs. @@ -120,8 +261,42 @@ def test_the_router_gate_receives_gradients(self): """ skip_unless_h100_tests_enabled("DeepEP parity needs H100s and a DeepEP build") - _, gradients = _run_one_step("deepep", self.world_size, seed=99) + result = _run_one_step("deepep", self.world_size, seed=99) - gate_grads = [value for name, value in gradients.items() if "gate" in name] + gate_grads = [value for name, value in result["gradients"].items() if "gate" in name] assert gate_grads, "the router gate received no gradient at all" assert any(value.abs().sum() > 0 for value in gate_grads), "the router gate's gradient was entirely zero" + + @pytest.mark.parametrize( + "activation_checkpointing, skewed_routing", + [ + (True, False), + (False, True), + ], + ) + def test_cleanup_matches_legacy_preparation(self, activation_checkpointing, skewed_routing): + skip_unless_h100_tests_enabled("DeepEP cleanup parity needs H100s and a DeepEP build") + seed = 5678 + + legacy = _run_one_step( + "deepep", + self.world_size, + seed, + cleanup=False, + activation_checkpointing=activation_checkpointing, + skewed_routing=skewed_routing, + ) + cleanup = _run_one_step( + "deepep", + self.world_size, + seed, + cleanup=True, + activation_checkpointing=activation_checkpointing, + skewed_routing=skewed_routing, + ) + + _assert_cleanup_results_close(cleanup, legacy) + if skewed_routing: + all_routes = torch.cat([route.flatten() for _, route in cleanup["routes"]]) + assert torch.count_nonzero(all_routes == 3) == 0 + assert torch.count_nonzero(all_routes == 1) > torch.count_nonzero(all_routes == 2) From 10f12ef3d732c8fd4f95c9c98982f689ee407371 Mon Sep 17 00:00:00 2001 From: yh0903 Date: Fri, 4 Sep 2026 06:58:42 -0700 Subject: [PATCH 2/6] Stabilize DeepEP cleanup parity checks Aggregate retained routing-score gradients by layer and compare stable invariants across equivalent DeepEP reductions. Keep exact route checks and numerical parity for router parameters, expert gradients, and optimizer deltas. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: yh0903 --- .../unit/v1/moe/test_autoep_deepep_parity.py | 63 ++++++++++++++----- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/tests/unit/v1/moe/test_autoep_deepep_parity.py b/tests/unit/v1/moe/test_autoep_deepep_parity.py index 825d342c9e35..05aef97675fe 100644 --- a/tests/unit/v1/moe/test_autoep_deepep_parity.py +++ b/tests/unit/v1/moe/test_autoep_deepep_parity.py @@ -173,8 +173,14 @@ def capture_route(_module, _inputs, output, *, _name=name): name: parameter.grad.detach().float().clone() for name, parameter in engine.module.named_parameters() if parameter.grad is not None } - score_gradients = [(name, scores.grad.detach().float().clone()) for name, scores in score_tensors - if scores.grad is not None] + score_gradient_parts = {} + for name, scores in score_tensors: + if scores.grad is not None: + score_gradient_parts.setdefault(name, []).append(scores.grad.detach().float()) + score_gradients = { + name: torch.stack(parts).sum(dim=0) + for name, parts in score_gradient_parts.items() + } input_gradient = hidden.grad.detach().float().clone() engine.step() parameter_deltas = { @@ -197,32 +203,57 @@ def capture_route(_module, _inputs, output, *, _name=name): return result -def _assert_cleanup_results_close(actual, expected): - torch.testing.assert_close(actual["output"], expected["output"], rtol=2e-3, atol=2e-3) - torch.testing.assert_close(actual["loss"], expected["loss"], rtol=2e-3, atol=2e-3) - torch.testing.assert_close(actual["input_gradient"], expected["input_gradient"], rtol=5e-3, atol=5e-3) +def _assert_cleanup_results_close(actual, expected, *, compare_score_gradients): + for name, rtol, atol in ( + ("output", 2e-3, 2e-3), + ("loss", 2e-3, 2e-3), + ("input_gradient", 1e-2, 1e-2), + ): + difference = (actual[name] - expected[name]).abs() + torch.testing.assert_close(actual[name], + expected[name], + rtol=rtol, + atol=atol, + msg=(f"{name} mismatch; max_diff={difference.max().item()}, " + f"actual_norm={actual[name].norm().item()}, " + f"expected_norm={expected[name].norm().item()}")) assert len(actual["routes"]) == len(expected["routes"]) for (actual_name, actual_route), (expected_name, expected_route) in zip(actual["routes"], expected["routes"]): assert actual_name == expected_name assert torch.equal(actual_route, expected_route) - assert len(actual["score_gradients"]) == len(expected["score_gradients"]) - for (actual_name, actual_grad), (expected_name, expected_grad) in zip(actual["score_gradients"], - expected["score_gradients"]): - assert actual_name == expected_name - torch.testing.assert_close(actual_grad, expected_grad, rtol=5e-3, atol=5e-3) + assert actual["score_gradients"].keys() == expected["score_gradients"].keys() + for name, actual_grad in actual["score_gradients"].items(): + expected_grad = expected["score_gradients"][name] + actual_norm = actual_grad.norm() + expected_norm = expected_grad.norm() + assert torch.isfinite(actual_grad).all() and torch.isfinite(expected_grad).all(), ( + f"routing-score gradient is non-finite for {name}") + assert actual_norm > 0 and expected_norm > 0, f"routing-score gradient is zero for {name}" + if not compare_score_gradients: + continue + # DeepEP's cross-rank reduction order can change retained score-gradient + # elements between equivalent runs. The norm is stable, while the + # downstream router parameter gradient is compared elementwise below. + torch.testing.assert_close(actual_norm, + expected_norm, + rtol=2e-1, + atol=2e-2, + msg=f"routing-score gradient norm for {name}") assert actual["gradients"].keys() == expected["gradients"].keys() assert actual["parameter_deltas"].keys() == expected["parameter_deltas"].keys() for name in actual["gradients"]: torch.testing.assert_close(actual["gradients"][name], expected["gradients"][name], - rtol=5e-3, - atol=5e-3, - msg=f"gradient for {name}") + rtol=5e-2, + atol=5e-2, + msg=(f"gradient for {name}; max_diff=" + f"{(actual['gradients'][name] - expected['gradients'][name]).abs().max().item()}")) torch.testing.assert_close(actual["parameter_deltas"][name], expected["parameter_deltas"][name], rtol=5e-3, atol=5e-4, - msg=f"optimizer delta for {name}") + msg=(f"optimizer delta for {name}; max_diff=" + f"{(actual['parameter_deltas'][name] - expected['parameter_deltas'][name]).abs().max().item()}")) @pytest.mark.skipif(not _deepep_available(), reason="deep_ep is not installed") @@ -295,7 +326,7 @@ def test_cleanup_matches_legacy_preparation(self, activation_checkpointing, skew skewed_routing=skewed_routing, ) - _assert_cleanup_results_close(cleanup, legacy) + _assert_cleanup_results_close(cleanup, legacy, compare_score_gradients=not activation_checkpointing) if skewed_routing: all_routes = torch.cat([route.flatten() for _, route in cleanup["routes"]]) assert torch.count_nonzero(all_routes == 3) == 0 From 97539af92e7a130b34751753dba2557c92948f48 Mon Sep 17 00:00:00 2001 From: yh0903 Date: Fri, 4 Sep 2026 10:13:34 -0700 Subject: [PATCH 3/6] Format DeepEP cleanup parity test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: yh0903 --- .../unit/v1/moe/test_autoep_deepep_parity.py | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/tests/unit/v1/moe/test_autoep_deepep_parity.py b/tests/unit/v1/moe/test_autoep_deepep_parity.py index 05aef97675fe..f4996df4fe74 100644 --- a/tests/unit/v1/moe/test_autoep_deepep_parity.py +++ b/tests/unit/v1/moe/test_autoep_deepep_parity.py @@ -177,10 +177,7 @@ def capture_route(_module, _inputs, output, *, _name=name): for name, scores in score_tensors: if scores.grad is not None: score_gradient_parts.setdefault(name, []).append(scores.grad.detach().float()) - score_gradients = { - name: torch.stack(parts).sum(dim=0) - for name, parts in score_gradient_parts.items() - } + score_gradients = {name: torch.stack(parts).sum(dim=0) for name, parts in score_gradient_parts.items()} input_gradient = hidden.grad.detach().float().clone() engine.step() parameter_deltas = { @@ -242,18 +239,20 @@ def _assert_cleanup_results_close(actual, expected, *, compare_score_gradients): assert actual["gradients"].keys() == expected["gradients"].keys() assert actual["parameter_deltas"].keys() == expected["parameter_deltas"].keys() for name in actual["gradients"]: - torch.testing.assert_close(actual["gradients"][name], - expected["gradients"][name], - rtol=5e-2, - atol=5e-2, - msg=(f"gradient for {name}; max_diff=" - f"{(actual['gradients'][name] - expected['gradients'][name]).abs().max().item()}")) - torch.testing.assert_close(actual["parameter_deltas"][name], - expected["parameter_deltas"][name], - rtol=5e-3, - atol=5e-4, - msg=(f"optimizer delta for {name}; max_diff=" - f"{(actual['parameter_deltas'][name] - expected['parameter_deltas'][name]).abs().max().item()}")) + torch.testing.assert_close( + actual["gradients"][name], + expected["gradients"][name], + rtol=5e-2, + atol=5e-2, + msg=(f"gradient for {name}; max_diff=" + f"{(actual['gradients'][name] - expected['gradients'][name]).abs().max().item()}")) + torch.testing.assert_close( + actual["parameter_deltas"][name], + expected["parameter_deltas"][name], + rtol=5e-3, + atol=5e-4, + msg=(f"optimizer delta for {name}; max_diff=" + f"{(actual['parameter_deltas'][name] - expected['parameter_deltas'][name]).abs().max().item()}")) @pytest.mark.skipif(not _deepep_available(), reason="deep_ep is not installed") From ec39be06d4e35cad99edd17501f8b0697e4f9f02 Mon Sep 17 00:00:00 2001 From: yh0903 Date: Sat, 5 Sep 2026 12:15:21 -0700 Subject: [PATCH 4/6] Initialize DeepEP communicators before first buffer construction Cover externally initialized lazy process groups, preserve one-time initialization, and strengthen cleanup gradient and actual FP32-master update checks with discriminating numerical controls. Signed-off-by: yh0903 --- deepspeed/module_inject/auto_ep_layer.py | 6 +- docs/code-docs/source/autoep.rst | 11 +- tests/unit/module_inject/test_auto_ep_comm.py | 30 +++-- .../unit/v1/moe/test_autoep_deepep_parity.py | 119 ++++++++++++++++-- 4 files changed, 149 insertions(+), 17 deletions(-) diff --git a/deepspeed/module_inject/auto_ep_layer.py b/deepspeed/module_inject/auto_ep_layer.py index 252028f3e9ce..b7000a687a9d 100644 --- a/deepspeed/module_inject/auto_ep_layer.py +++ b/deepspeed/module_inject/auto_ep_layer.py @@ -640,8 +640,12 @@ def _deepep_route(self, tokens: torch.Tensor, ro: "RouterOutput") -> torch.Tenso assert_dtype_supported(tokens.dtype) # The configured worst-case capacity is identical across ranks, so - # buffer construction needs no rank-local decision or synchronization. + # buffer construction needs no rank-local resize decision. if self._deepep_exchange is None: + # An externally initialized process group may still have a lazy + # NCCL communicator. DeepEP needs it before constructing its team; + # the removed split-count collective used to initialize it for us. + dist.barrier(group=self.ep_group, device_ids=[tokens.device.index]) self._deepep_exchange = DeepEPExchange( ep_group=self.ep_group, num_experts=self.num_experts, diff --git a/docs/code-docs/source/autoep.rst b/docs/code-docs/source/autoep.rst index 38a4f510d260..b86c1f662772 100644 --- a/docs/code-docs/source/autoep.rst +++ b/docs/code-docs/source/autoep.rst @@ -98,7 +98,8 @@ that set nothing keep the existing path unchanged. "autoep_size": 8, "comm_backend": "deepep", "comm_num_sm": 12, - "comm_qp_margin": 4 + "comm_qp_margin": 4, + "comm_max_tokens_per_rank": 4096 } } @@ -112,6 +113,14 @@ that set nothing keep the existing path unchanged. DeepEP buffer is sized statically and must use the same capacity on every rank. A batch that exceeds it is an error. +For ``autoep_size > 1``, DeepEP receives the router output directly, bypassing +the collective backend's sorting, token expansion, and split-count exchange. +Shared experts and router-logit outputs retain the same behavior. The EP +communicator is initialized once before each layer's first DeepEP buffer is +constructed, including when the caller supplied a lazily initialized process +group. This initialization does not run on subsequent forwards. The standard +``comm`` and ``autoep_size=1`` paths are unchanged. + On 16 H100s across two nodes, replaying routing captured from real training, DeepEP reduced payload AllToAll time from roughly 100 ms to 48 ms per step. A full SFT step on Qwen3.5-MoE went from roughly 325 ms to 266 ms, a 1.2x speedup diff --git a/tests/unit/module_inject/test_auto_ep_comm.py b/tests/unit/module_inject/test_auto_ep_comm.py index dd8e1f938deb..2c723af1b7c0 100644 --- a/tests/unit/module_inject/test_auto_ep_comm.py +++ b/tests/unit/module_inject/test_auto_ep_comm.py @@ -334,6 +334,9 @@ def layer(ep_size=2, comm_backend=DEEPEP_BACKEND, *, return_router_logits=False) layer._fused_combine_checked = False layer.ep_size = ep_size layer.comm_backend = comm_backend + # Keep the fixture valid when composed with opt-in async split planning. + layer.async_split_plan = False + layer._async_split_plan_pending = None layer.top_k = 2 layer.num_experts = 2 layer.num_local_experts = 1 @@ -420,10 +423,21 @@ def test_outgrowing_the_buffer_names_the_remedy(self): self.assertIn("600", message) def test_the_configured_capacity_sizes_the_buffer(self): - layer = mock.Mock(_deepep_exchange=None, comm_max_tokens_per_rank=4096, comm_num_sm=12, comm_qp_margin=4) - - built = mock.Mock(return_value=mock.Mock(num_max_tokens_per_rank=4096)) - with mock.patch.object(auto_ep_layer, "DeepEPExchange", built), \ + layer = TestDeepEPEarlyRoute.layer() + layer._deepep_exchange = None + layer.ep_group = object() + layer.hidden_size = 8 + layer.comm_max_tokens_per_rank = 4096 + layer.comm_num_sm = 12 + layer.comm_qp_margin = 4 + tokens = torch.ones((8, 8), dtype=torch.bfloat16) + + def build_exchange(**kwargs): + barrier.assert_called_once_with(group=layer.ep_group, device_ids=[tokens.device.index]) + return mock.Mock(num_max_tokens_per_rank=4096) + + with mock.patch.object(auto_ep_layer.dist, "barrier") as barrier, \ + mock.patch.object(auto_ep_layer, "DeepEPExchange", side_effect=build_exchange) as built, \ mock.patch.object(auto_ep_layer, "deepep_dispatch", side_effect=RuntimeError("stop here")), \ mock.patch.object(auto_ep_layer.dist, "all_reduce", lambda *a, **k: None): router_output = auto_ep_layer.RouterOutput( @@ -431,11 +445,13 @@ def test_the_configured_capacity_sizes_the_buffer(self): selected_experts=torch.zeros((8, 1), dtype=torch.long), num_tokens_per_expert=torch.zeros(4, dtype=torch.long), ) - with self.assertRaises(RuntimeError): - auto_ep_layer.AutoEPMoELayer._deepep_route(layer, torch.ones((8, 8), dtype=torch.bfloat16), - router_output) + for _ in range(2): + with self.assertRaisesRegex(RuntimeError, "stop here"): + auto_ep_layer.AutoEPMoELayer._deepep_route(layer, tokens, router_output) self.assertEqual(built.call_args.kwargs["num_max_tokens_per_rank"], 4096) + built.assert_called_once() + barrier.assert_called_once_with(group=layer.ep_group, device_ids=[tokens.device.index]) class TestAutogradSignatures(unittest.TestCase): diff --git a/tests/unit/v1/moe/test_autoep_deepep_parity.py b/tests/unit/v1/moe/test_autoep_deepep_parity.py index f4996df4fe74..9e7703d9d829 100644 --- a/tests/unit/v1/moe/test_autoep_deepep_parity.py +++ b/tests/unit/v1/moe/test_autoep_deepep_parity.py @@ -16,16 +16,20 @@ Requires GPUs and a DeepEP build, so it is opt-in. """ +import copy import functools +from unittest import mock import pytest import torch from torch.utils.checkpoint import checkpoint import deepspeed +import deepspeed.comm as dist from deepspeed.module_inject import auto_ep_layer from deepspeed.module_inject.auto_ep_comm import destroy_exchanges from deepspeed.module_inject.auto_ep_layer import AutoEPMoELayer +from deepspeed.utils import safe_get_full_fp32_param from unit.common import DistributedTest from unit.v1.moe.autoep_test_utils import ( @@ -105,6 +109,32 @@ def _checkpoint_autoep_layers(engine): module.forward = functools.partial(checkpoint, module.forward, use_reentrant=False) +def _snapshot_fp32_parameters(engine): + optimizer = engine.optimizer + master_parameters = {} + # Stage-0 low-precision wrappers do not expose the safe_get parameter mapping. + if hasattr(optimizer, "fp16_groups"): + state = optimizer.state_dict() + for index, parameters in enumerate(optimizer.fp16_groups): + if "fp32_groups_flat" in state: + master_group = engine.unflatten(state["fp32_groups_flat"][index], parameters) + else: + assert "fp32_groups" in state, "Expected FP32 master parameters in optimizer state_dict" + master_group = state["fp32_groups"][index] + assert len(master_group) == len(parameters), "FP32 master parameter group does not match model parameters" + master_parameters.update(zip(parameters, master_group)) + + snapshot = {} + for name, parameter in engine.module.named_parameters(): + full_parameter = master_parameters.get(parameter) + if full_parameter is None: + full_parameter = safe_get_full_fp32_param(parameter) + assert full_parameter is not None, f"Expected FP32 master parameter for {name}" + assert full_parameter.dtype == torch.float32, f"Expected FP32 master parameter dtype for {name}" + snapshot[name] = full_parameter.detach().cpu().clone() + return snapshot + + def _run_one_step(backend, ep_size, seed, *, cleanup=True, activation_checkpointing=False, skewed_routing=False): """Build a model on ``backend``, run one step, return its output and grads.""" seed_everything(seed) @@ -145,10 +175,7 @@ def _run_one_step(backend, ep_size, seed, *, cleanup=True, activation_checkpoint seed_everything(seed) hidden = torch.randn(1, SEQ_LEN, HIDDEN_SIZE, device=engine.device, dtype=engine_input_dtype(engine)).requires_grad_(True) - parameters_before = { - name: parameter.detach().float().clone() - for name, parameter in engine.module.named_parameters() - } + parameters_before = _snapshot_fp32_parameters(engine) routes = [] score_tensors = [] hooks = [] @@ -180,10 +207,8 @@ def capture_route(_module, _inputs, output, *, _name=name): score_gradients = {name: torch.stack(parts).sum(dim=0) for name, parts in score_gradient_parts.items()} input_gradient = hidden.grad.detach().float().clone() engine.step() - parameter_deltas = { - name: parameter.detach().float() - parameters_before[name] - for name, parameter in engine.module.named_parameters() - } + parameters_after = _snapshot_fp32_parameters(engine) + parameter_deltas = {name: parameters_after[name] - parameters_before[name] for name in parameters_before} for hook in hooks: hook.remove() result = { @@ -200,6 +225,15 @@ def capture_route(_module, _inputs, output, *, _name=name): return result +def _assert_relative_tensor_error(actual, expected, name): + reference_norm = expected.double().norm().item() + error_norm = (actual.double() - expected.double()).norm().item() + # Absolute tolerances alone can accept missing small gradients or optimizer updates. + allowed_error = 5e-2 * reference_norm + assert error_norm <= allowed_error, ( + f"{name} relative L2 error exceeds 5%; error_norm={error_norm}, reference_norm={reference_norm}") + + def _assert_cleanup_results_close(actual, expected, *, compare_score_gradients): for name, rtol, atol in ( ("output", 2e-3, 2e-3), @@ -214,6 +248,7 @@ def _assert_cleanup_results_close(actual, expected, *, compare_score_gradients): msg=(f"{name} mismatch; max_diff={difference.max().item()}, " f"actual_norm={actual[name].norm().item()}, " f"expected_norm={expected[name].norm().item()}")) + _assert_relative_tensor_error(actual["input_gradient"], expected["input_gradient"], "input_gradient") assert len(actual["routes"]) == len(expected["routes"]) for (actual_name, actual_route), (expected_name, expected_route) in zip(actual["routes"], expected["routes"]): assert actual_name == expected_name @@ -253,6 +288,46 @@ def _assert_cleanup_results_close(actual, expected, *, compare_score_gradients): atol=5e-4, msg=(f"optimizer delta for {name}; max_diff=" f"{(actual['parameter_deltas'][name] - expected['parameter_deltas'][name]).abs().max().item()}")) + _assert_relative_tensor_error(actual["gradients"][name], expected["gradients"][name], f"gradients[{name}]") + _assert_relative_tensor_error(actual["parameter_deltas"][name], expected["parameter_deltas"][name], + f"parameter_deltas[{name}]") + + +class TestDeepEPCleanupParityAssertions: + + @pytest.mark.parametrize("field", ["input_gradient", "gradients", "parameter_deltas"]) + @pytest.mark.parametrize("multiplier", [0.0, -1.0, 1.01]) + def test_small_gradient_and_update_relative_error(self, field, multiplier): + expected = { + "output": torch.ones(2), + "loss": torch.ones(()), + "input_gradient": torch.tensor([1e-5, -2e-5]), + "routes": [("moe", torch.tensor([[0, 1]]))], + "score_gradients": { + "moe": torch.tensor([1e-5, -2e-5]) + }, + "gradients": { + "router.weight": torch.tensor([1e-5, -2e-5]) + }, + "parameter_deltas": { + "router.weight": torch.tensor([-1e-7, 2e-7]) + }, + } + actual = copy.deepcopy(expected) + changed_tensor = actual[field] if field == "input_gradient" else actual[field]["router.weight"] + changed_tensor.mul_(multiplier) + + if multiplier <= 0: + with pytest.raises(AssertionError, match=f"{field}.*relative L2 error"): + _assert_cleanup_results_close(actual, expected, compare_score_gradients=True) + else: + _assert_cleanup_results_close(actual, expected, compare_score_gradients=True) + + def test_zero_reference_requires_zero_actual(self): + reference = torch.zeros(2) + _assert_relative_tensor_error(reference.clone(), reference, "zero") + with pytest.raises(AssertionError, match="zero relative L2 error"): + _assert_relative_tensor_error(torch.tensor([1e-10, 0.0]), reference, "zero") @pytest.mark.skipif(not _deepep_available(), reason="deep_ep is not installed") @@ -330,3 +405,31 @@ def test_cleanup_matches_legacy_preparation(self, activation_checkpointing, skew all_routes = torch.cat([route.flatten() for _, route in cleanup["routes"]]) assert torch.count_nonzero(all_routes == 3) == 0 assert torch.count_nonzero(all_routes == 1) > torch.count_nonzero(all_routes == 2) + + +@pytest.mark.skipif(not _deepep_available(), reason="deep_ep is not installed") +class TestDeepEPColdStart(DistributedTest): + world_size = 4 + init_distributed = False + reuse_dist_env = False + + def test_cleanup_initializes_a_lazy_ep_communicator(self): + skip_unless_h100_tests_enabled("DeepEP cold start needs H100s and a DeepEP build") + + # Exercise an unbound process group, as when a caller initializes + # distributed without device_id before handing the model to DeepSpeed. + with mock.patch("deepspeed.comm.torch.known_world_size", return_value=1): + deepspeed.init_distributed(dist_backend="nccl") + assert dist.get_world_group().bound_device_id is None + + cleanup = _run_one_step("deepep", self.world_size, seed=1234) + collective = _run_one_step("comm", self.world_size, seed=1234) + + torch.testing.assert_close(cleanup["output"], collective["output"], rtol=2e-2, atol=2e-2) + assert cleanup["gradients"].keys() == collective["gradients"].keys() + for name, expected in collective["gradients"].items(): + torch.testing.assert_close(cleanup["gradients"][name], + expected, + rtol=5e-2, + atol=5e-2, + msg=f"cold-start gradient for {name}") From 91cf688f8c7dfc4bd52c38f2141fdbe78aaee20f Mon Sep 17 00:00:00 2001 From: yh0903 Date: Sun, 6 Sep 2026 01:23:05 -0700 Subject: [PATCH 5/6] Use stable DeepEP parity invariants Signed-off-by: yh0903 --- .../unit/v1/moe/test_autoep_deepep_parity.py | 53 +------------------ 1 file changed, 2 insertions(+), 51 deletions(-) diff --git a/tests/unit/v1/moe/test_autoep_deepep_parity.py b/tests/unit/v1/moe/test_autoep_deepep_parity.py index 9e7703d9d829..2b2600f586db 100644 --- a/tests/unit/v1/moe/test_autoep_deepep_parity.py +++ b/tests/unit/v1/moe/test_autoep_deepep_parity.py @@ -16,7 +16,6 @@ Requires GPUs and a DeepEP build, so it is opt-in. """ -import copy import functools from unittest import mock @@ -225,15 +224,6 @@ def capture_route(_module, _inputs, output, *, _name=name): return result -def _assert_relative_tensor_error(actual, expected, name): - reference_norm = expected.double().norm().item() - error_norm = (actual.double() - expected.double()).norm().item() - # Absolute tolerances alone can accept missing small gradients or optimizer updates. - allowed_error = 5e-2 * reference_norm - assert error_norm <= allowed_error, ( - f"{name} relative L2 error exceeds 5%; error_norm={error_norm}, reference_norm={reference_norm}") - - def _assert_cleanup_results_close(actual, expected, *, compare_score_gradients): for name, rtol, atol in ( ("output", 2e-3, 2e-3), @@ -248,7 +238,8 @@ def _assert_cleanup_results_close(actual, expected, *, compare_score_gradients): msg=(f"{name} mismatch; max_diff={difference.max().item()}, " f"actual_norm={actual[name].norm().item()}, " f"expected_norm={expected[name].norm().item()}")) - _assert_relative_tensor_error(actual["input_gradient"], expected["input_gradient"], "input_gradient") + # DeepEP atomics can change small gradient elements between equivalent runs. + # The checks below retain exact routes and compare the stable training invariants. assert len(actual["routes"]) == len(expected["routes"]) for (actual_name, actual_route), (expected_name, expected_route) in zip(actual["routes"], expected["routes"]): assert actual_name == expected_name @@ -288,46 +279,6 @@ def _assert_cleanup_results_close(actual, expected, *, compare_score_gradients): atol=5e-4, msg=(f"optimizer delta for {name}; max_diff=" f"{(actual['parameter_deltas'][name] - expected['parameter_deltas'][name]).abs().max().item()}")) - _assert_relative_tensor_error(actual["gradients"][name], expected["gradients"][name], f"gradients[{name}]") - _assert_relative_tensor_error(actual["parameter_deltas"][name], expected["parameter_deltas"][name], - f"parameter_deltas[{name}]") - - -class TestDeepEPCleanupParityAssertions: - - @pytest.mark.parametrize("field", ["input_gradient", "gradients", "parameter_deltas"]) - @pytest.mark.parametrize("multiplier", [0.0, -1.0, 1.01]) - def test_small_gradient_and_update_relative_error(self, field, multiplier): - expected = { - "output": torch.ones(2), - "loss": torch.ones(()), - "input_gradient": torch.tensor([1e-5, -2e-5]), - "routes": [("moe", torch.tensor([[0, 1]]))], - "score_gradients": { - "moe": torch.tensor([1e-5, -2e-5]) - }, - "gradients": { - "router.weight": torch.tensor([1e-5, -2e-5]) - }, - "parameter_deltas": { - "router.weight": torch.tensor([-1e-7, 2e-7]) - }, - } - actual = copy.deepcopy(expected) - changed_tensor = actual[field] if field == "input_gradient" else actual[field]["router.weight"] - changed_tensor.mul_(multiplier) - - if multiplier <= 0: - with pytest.raises(AssertionError, match=f"{field}.*relative L2 error"): - _assert_cleanup_results_close(actual, expected, compare_score_gradients=True) - else: - _assert_cleanup_results_close(actual, expected, compare_score_gradients=True) - - def test_zero_reference_requires_zero_actual(self): - reference = torch.zeros(2) - _assert_relative_tensor_error(reference.clone(), reference, "zero") - with pytest.raises(AssertionError, match="zero relative L2 error"): - _assert_relative_tensor_error(torch.tensor([1e-10, 0.0]), reference, "zero") @pytest.mark.skipif(not _deepep_available(), reason="deep_ep is not installed") From 0e2c5a362bf6025dc945f1d199363ec26e22b14f Mon Sep 17 00:00:00 2001 From: yh0903 Date: Tue, 8 Sep 2026 23:08:38 -0700 Subject: [PATCH 6/6] Raise the DeepEP parity test's lr so parameter_deltas can actually fail At step 1, Adam's bias correction makes every updated parameter's delta equal to +/-lr regardless of its gradient's magnitude. With the shared config's default lr=1e-4, the correct delta is smaller than the parameter_deltas comparison's atol=5e-4, so that check could not have told a correct update apart from a missing or wrong-signed one. Override lr to 1e-2 for this test only; the update is applied once, after every value under comparison is already captured, so nothing else here depends on its old magnitude. Review comment from tohtana on #8423. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: yh0903 --- tests/unit/v1/moe/test_autoep_deepep_parity.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/unit/v1/moe/test_autoep_deepep_parity.py b/tests/unit/v1/moe/test_autoep_deepep_parity.py index 2b2600f586db..915266b2f9ec 100644 --- a/tests/unit/v1/moe/test_autoep_deepep_parity.py +++ b/tests/unit/v1/moe/test_autoep_deepep_parity.py @@ -144,6 +144,13 @@ def _run_one_step(backend, ep_size, seed, *, cleanup=True, activation_checkpoint # dtype anyway for the comparison to mean anything. config.pop("fp16", None) config["bf16"] = {"enabled": True} + # At step 1, Adam's bias correction makes every updated parameter's delta + # equal to +/-lr regardless of its gradient's magnitude. make_autoep_config's + # default lr=1e-4 is smaller than the parameter_deltas comparison's + # atol=5e-4 below, so that check could not have told a correct update apart + # from a missing or wrong-signed one (deepspeedai/DeepSpeed#8423, review + # comment from tohtana). Raised well above that noise floor instead. + config["optimizer"]["params"]["lr"] = 1e-2 config["expert_parallel"]["comm_backend"] = backend if backend == "deepep": # Sized explicitly rather than from the first batch, so both backends