Skip to content

Commit 32110a9

Browse files
authored
Preserve AutoEP score correction bias buffers (#8369)
## Summary Fix AutoEP replacement silently dropping `e_score_correction_bias` when Hugging Face models register it as a buffer or place it somewhere other than the router module expected by the preset. The parser now records the bias owner's actual path inside each MoE layer. Replacement then copies the bias while preserving whether it is an `nn.Parameter` or registered buffer, including the buffer's persistence setting. The implementation supports locations such as: - `gate` - the MoE block itself - `router` - nested modules such as `gate.moe_statics` Ambiguous layers containing more than one `e_score_correction_bias` are rejected rather than selecting one silently. Unsupported non-parameter/non-buffer values produce a warning. ## Validation - Full AutoEP unit test file: `103 passed` - All pre-commit hooks passed - Tested with a real Transformers 5.15.1 `DeepseekV3MoE` - GPU validation environment: - NVIDIA GeForce RTX 5090 - PyTorch 2.8.0+cu128 - Transformers 5.15.1 - DeepSpeed 0.19.6 - The replacement preserved the CUDA registered buffer and its value - Native and EP2 replacement routing selected the same experts - Single-GPU full MoE forward comparison had a maximum absolute difference of `0.0` The available validation machine has one GPU, so a multi-GPU collective/ZeRO-3 end-to-end run was not performed. Fixes #8358 Signed-off-by: poorpaper <43514747+poorpaper@users.noreply.github.com>
1 parent 05daf05 commit 32110a9

4 files changed

Lines changed: 109 additions & 6 deletions

File tree

deepspeed/module_inject/auto_ep.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ def _raise_if_duplicate_moe_specs(specs: list[MoELayerSpec]) -> None:
8282
"AutoEP patterns so each MoE module matches exactly one preset.")
8383

8484

85+
def _resolve_e_score_correction_bias_path(moe_module: nn.Module, module_name: str) -> str | None:
86+
matches = [
87+
path for path, candidate in moe_module.named_modules()
88+
if getattr(candidate, "e_score_correction_bias", None) is not None
89+
]
90+
if len(matches) > 1:
91+
locations = ", ".join(repr(path or "<root>") for path in matches)
92+
raise ValueError(
93+
f"AutoEP found e_score_correction_bias in multiple locations in layer '{module_name}': {locations}. "
94+
"Keep exactly one score correction bias in each MoE layer so AutoEP can preserve it unambiguously.")
95+
return matches[0] if matches else None
96+
97+
8598
def _source_param_shape(param: torch.Tensor | nn.Parameter) -> torch.Size:
8699
if is_zero_param(param):
87100
return torch.Size(param.ds_shape)
@@ -413,6 +426,7 @@ def ep_parser(self) -> list[MoELayerSpec]:
413426
gate_bias = getattr(router_child, 'bias', None) is not None
414427

415428
forward_contract = adapter.adjust_forward_contract(_detect_forward_contract(module, router_child))
429+
e_score_correction_bias_path = _resolve_e_score_correction_bias_path(module, module_name)
416430

417431
# Check shared experts
418432
has_shared = False
@@ -472,6 +486,7 @@ def ep_parser(self) -> list[MoELayerSpec]:
472486
preset_adapter=preset.preset_adapter,
473487
router_logits_capture_mode=forward_contract.router_logits_capture_mode,
474488
moe_output_shape=forward_contract.moe_output_shape,
489+
e_score_correction_bias_path=e_score_correction_bias_path,
475490
)
476491
specs.append(spec)
477492
logger.debug(f"Detected MoE layer: {module_name} (family={preset_name}, "

deepspeed/module_inject/auto_ep_layer.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,33 @@ def _copy_parameter_data(target: nn.Parameter, source: torch.Tensor) -> None:
8585
target.data.copy_(source_data)
8686

8787

88+
def _copy_e_score_correction_bias(
89+
target_router: nn.Module,
90+
source_owner: nn.Module,
91+
source_bias,
92+
source_path: str,
93+
) -> None:
94+
if isinstance(source_bias, nn.Parameter):
95+
target_router.e_score_correction_bias = nn.Parameter(source_bias.data.clone(),
96+
requires_grad=source_bias.requires_grad)
97+
elif (torch.is_tensor(source_bias) and source_owner._buffers.get("e_score_correction_bias") is source_bias):
98+
copied_bias = source_bias.detach().clone()
99+
copied_bias.requires_grad_(source_bias.requires_grad)
100+
if hasattr(target_router, "e_score_correction_bias"):
101+
delattr(target_router, "e_score_correction_bias")
102+
persistent = "e_score_correction_bias" not in source_owner._non_persistent_buffers_set
103+
target_router.register_buffer("e_score_correction_bias", copied_bias, persistent=persistent)
104+
else:
105+
logger.warning(
106+
"AutoEP: cannot copy e_score_correction_bias from source module path '%s': expected "
107+
"an nn.Parameter or registered buffer, got %s.", source_path or "<root>",
108+
type(source_bias).__name__)
109+
return
110+
111+
logger.info("AutoEP: copied e_score_correction_bias from source module path '%s' (shape=%s)", source_path
112+
or "<root>", source_bias.shape)
113+
114+
88115
def apply_scores_before_experts_if_enabled(
89116
routed_input: torch.Tensor,
90117
top_scores: torch.Tensor,
@@ -398,7 +425,14 @@ def __init__(
398425
# Router: copy gate weights from source
399426
source_gate = getattr(source_module, spec.router_name)
400427
source_gate_bias = getattr(source_gate, 'bias', None)
401-
source_ecb = getattr(source_gate, 'e_score_correction_bias', None)
428+
source_ecb_path = spec.e_score_correction_bias_path
429+
if source_ecb_path is None:
430+
source_ecb_owner = source_gate
431+
source_ecb_path = spec.router_name
432+
else:
433+
source_ecb_owner = (source_module
434+
if source_ecb_path == "" else source_module.get_submodule(source_ecb_path))
435+
source_ecb = getattr(source_ecb_owner, "e_score_correction_bias", None)
402436
unsupported_router_biases = [
403437
getattr(source_gate, bias_name, None) for bias_name in spec.unsupported_router_bias_names
404438
]
@@ -434,11 +468,8 @@ def __init__(
434468
self.router.gate.bias.requires_grad_(source_gate_bias.requires_grad)
435469

436470
# Copy pre-trained score correction bias (DeepSeek-V3/Moonlight noaux_tc routing)
437-
if source_ecb is not None and isinstance(source_ecb, nn.Parameter):
438-
self.router.e_score_correction_bias = nn.Parameter(source_ecb.data.clone(),
439-
requires_grad=source_ecb.requires_grad)
440-
logger.info('AutoEP: copied e_score_correction_bias from source gate '
441-
'(shape=%s)', source_ecb.shape)
471+
if source_ecb is not None:
472+
_copy_e_score_correction_bias(self.router, source_ecb_owner, source_ecb, source_ecb_path)
442473

443474
# Alias router under the name OutputRecorder expects (layer_name if provided),
444475
# but only when OutputRecorder captures from the router child and the alias is safe.

deepspeed/module_inject/auto_ep_presets/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class MoELayerSpec:
9090
preset_adapter: str = "default"
9191
router_logits_capture_mode: Literal["raw", "post_score"] = "post_score"
9292
moe_output_shape: Literal["batched", "flat"] = "batched"
93+
e_score_correction_bias_path: str | None = None
9394

9495

9596
@dataclass

tests/unit/v1/moe/test_autoep_unit.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,7 +1308,9 @@ def test_deepseek_v3_detection_and_score_correction_bias_copy(self, monkeypatch)
13081308
FakeGatheredParameters.calls = []
13091309
monkeypatch.setattr(ep_repack, "GatheredParameters", FakeGatheredParameters)
13101310
monkeypatch.setattr(get_preset_adapter("deepseek_v3"), "_installed_transformers_version", lambda: "5.0.0")
1311+
13111312
model = MockDeepSeekV3Transformer(num_layers=1, num_experts=8)
1313+
13121314
auto_ep = AutoEP(model, _runtime_config(enabled=True, autoep_size=2))
13131315
specs = auto_ep.ep_parser()
13141316

@@ -1317,6 +1319,7 @@ def test_deepseek_v3_detection_and_score_correction_bias_copy(self, monkeypatch)
13171319
assert specs[0].expert_storage == "module_list"
13181320
assert specs[0].expert_w1_name == "gate_proj"
13191321
assert specs[0].has_shared_experts is True
1322+
assert specs[0].e_score_correction_bias_path is None
13201323

13211324
source_bias = torch.arange(8, dtype=torch.float32)
13221325
model.model.layers[0].mlp.gate.e_score_correction_bias = nn.Parameter(source_bias.clone())
@@ -1333,6 +1336,59 @@ def test_deepseek_v3_detection_and_score_correction_bias_copy(self, monkeypatch)
13331336
torch.testing.assert_close(replaced.router.e_score_correction_bias, source_bias)
13341337
assert ["router.e_score_correction_bias"] in [call["names"] for call in FakeGatheredParameters.calls]
13351338

1339+
@pytest.mark.parametrize(
1340+
"owner_path,bias_kind,persistent",
1341+
[
1342+
("gate", "buffer", True),
1343+
("", "buffer", False),
1344+
("router", "buffer", True),
1345+
("gate.moe_statics", "parameter", True),
1346+
],
1347+
)
1348+
def test_score_correction_bias_location_and_registration(self, monkeypatch, owner_path, bias_kind, persistent):
1349+
monkeypatch.setattr(get_preset_adapter("deepseek_v3"), "_installed_transformers_version", lambda: "5.0.0")
1350+
model = MockDeepSeekV3Transformer(num_layers=1, num_experts=8)
1351+
source = model.model.layers[0].mlp
1352+
owner = source
1353+
for part in owner_path.split(".") if owner_path else ():
1354+
if not hasattr(owner, part):
1355+
owner.add_module(part, nn.Module())
1356+
owner = getattr(owner, part)
1357+
1358+
source_bias = torch.arange(8, dtype=torch.float32)
1359+
if bias_kind == "parameter":
1360+
owner.e_score_correction_bias = nn.Parameter(source_bias.clone(), requires_grad=False)
1361+
else:
1362+
owner.register_buffer("e_score_correction_bias", source_bias.clone(), persistent=persistent)
1363+
1364+
auto_ep = AutoEP(model, _runtime_config(enabled=True, autoep_size=2))
1365+
spec = auto_ep.ep_parser()[0]
1366+
assert spec.e_score_correction_bias_path == owner_path
1367+
1368+
auto_ep.replace_moe_layer(spec, ep_size=2, ep_rank=0)
1369+
1370+
replaced_bias = model.model.layers[0].mlp.router.e_score_correction_bias
1371+
torch.testing.assert_close(replaced_bias, source_bias)
1372+
assert replaced_bias.requires_grad is False
1373+
if bias_kind == "parameter":
1374+
assert dict(
1375+
model.model.layers[0].mlp.router.named_parameters())["e_score_correction_bias"] is replaced_bias
1376+
assert "e_score_correction_bias" not in dict(model.model.layers[0].mlp.router.named_buffers())
1377+
else:
1378+
assert dict(model.model.layers[0].mlp.router.named_buffers())["e_score_correction_bias"] is replaced_bias
1379+
assert "e_score_correction_bias" not in dict(model.model.layers[0].mlp.router.named_parameters())
1380+
assert ("e_score_correction_bias" in model.model.layers[0].mlp.router.state_dict()) is persistent
1381+
1382+
def test_score_correction_bias_multiple_locations_are_rejected(self, monkeypatch):
1383+
monkeypatch.setattr(get_preset_adapter("deepseek_v3"), "_installed_transformers_version", lambda: "5.0.0")
1384+
model = MockDeepSeekV3Transformer(num_layers=1, num_experts=8)
1385+
source = model.model.layers[0].mlp
1386+
source.register_buffer("e_score_correction_bias", torch.zeros(8))
1387+
source.gate.register_buffer("e_score_correction_bias", torch.ones(8))
1388+
1389+
with pytest.raises(ValueError, match="e_score_correction_bias in multiple locations"):
1390+
AutoEP(model, _runtime_config(enabled=True, autoep_size=2)).ep_parser()
1391+
13361392

13371393
def _eager_pep604_lines(module):
13381394
"""Line numbers where a module evaluates PEP 604 unions at import time."""

0 commit comments

Comments
 (0)