Files
2026-07-13 13:18:33 +08:00

434 lines
16 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
"""Shared AutoEP preset dataclasses and adapter interface."""
from __future__ import annotations
from dataclasses import dataclass, field, replace
from typing import Any, Callable, Literal, NoReturn
import torch.nn as nn
from packaging.version import InvalidVersion, Version
# Sentinel for "not specified in config, use preset default".
# Unlike None (which means "fused gate+up, no separate w3"), _UNSET means
# the user did not set the field at all. Compare with `is _UNSET`.
_UNSET = object()
def _raise_unsupported_load_balance_coeff(value: object) -> NoReturn:
raise ValueError(f"load_balance_coeff={value!r} is not supported in this AutoEP build "
"(would register expert_bias and route through unsupported "
"auxiliary-loss-free load balancing). Set load_balance_coeff to null "
"or omit the key.")
@dataclass
class MoEModelPreset:
"""Preset configuration for a known MoE model family."""
moe_layer_pattern: str
router_pattern: str
experts_pattern: str
expert_storage: Literal["fused_3d", "module_list"]
expert_w1: str
expert_w2: str
expert_w3: str | None
num_experts_attr: str
top_k_attr: str
score_func: Literal["softmax", "sigmoid"]
score_apply: Literal["pre", "post"]
route_norm: bool
gate_bias: bool
has_shared_experts: bool = False
shared_experts_pattern: str = ""
shared_experts_gate_pattern: str = ""
autoep_config_defaults: dict[str, Any] = field(default_factory=dict)
supports_expert_bias: bool = True
unsupported_router_bias_names: tuple[str, ...] = ()
preset_adapter: str = "default"
hf_model_types: tuple[str, ...] = ()
unsupported_hf_model_type_notes: dict[str, str] = field(default_factory=dict)
min_transformers_version: str | None = None
validated_transformers_versions: str = ""
docs_support_notes: str = ""
@dataclass
class MoELayerSpec:
"""Detected MoE layer specification for a single module in the model."""
moe_module_name: str
model_family: str
router_name: str
experts_name: str
expert_storage: Literal["fused_3d", "module_list"]
expert_w1_name: str
expert_w2_name: str
expert_w3_name: str | None
num_experts: int
top_k: int
hidden_size: int
ffn_hidden_size: int
score_func: Literal["softmax", "sigmoid"]
score_apply: Literal["pre", "post"]
route_norm: bool
gate_bias: bool
return_router_logits: bool
router_logits_capture_target: Literal["moe_block", "router", "none"]
router_logits_capture_index: int | None
router_logits_capture_layer_name: str | None
has_shared_experts: bool
shared_experts_name: str
shared_experts_gate_name: str = ""
route_scale: float = 1.0
num_expert_groups: int | None = None
num_limited_groups: int | None = None
group_score_func: Literal["max", "top2_sum"] = "top2_sum"
supports_expert_bias: bool = True
unsupported_router_bias_names: tuple[str, ...] = ()
preset_adapter: str = "default"
router_logits_capture_mode: Literal["raw", "post_score"] = "post_score"
moe_output_shape: Literal["batched", "flat"] = "batched"
@dataclass
class AutoEPConfig:
"""User-facing configuration parsed from DS config JSON."""
enabled: bool = False
autoep_size: int = 1
expert_tensor_parallel_size: int = 1
validate_folding_routing: bool = False
preset_model: str | None = None
moe_layer_pattern: str | None = None
expert_pattern: str | None = None
router_pattern: str | None = None
use_grouped_mm: bool = True
route_norm: bool | None = None
route_scale: float = 1.0
score_apply: Literal["auto", "pre", "post"] = "auto"
combine_impl: Literal["auto", "weighted_sum", "legacy_bmm"] = "auto"
num_expert_groups: int | None = None
num_limited_groups: int | None = None
score_func: Literal["auto", "softmax", "sigmoid"] = "auto"
top_k: int | str = "auto"
load_balance_coeff: float | None | object = _UNSET
routed_scaling_factor: float | str = "auto"
expert_w1: str | None = None
expert_w2: str | None = None
expert_w3: object = _UNSET
num_experts_attr: str | None = None
top_k_attr: str | None = None
has_shared_experts: bool | None = None
shared_experts_pattern: str | None = None
shared_experts_gate_pattern: str | None = None
_load_balance_coeff_explicit: bool = field(default=False, init=False, repr=False)
def __post_init__(self) -> None:
if self.load_balance_coeff is _UNSET:
self.load_balance_coeff = None
self._load_balance_coeff_explicit = False
else:
self._load_balance_coeff_explicit = True
@dataclass(frozen=True)
class GroupRoutingConfig:
num_expert_groups: int | None
num_limited_groups: int | None
group_score_func: Literal["max", "top2_sum"] = "top2_sum"
@dataclass(frozen=True)
class ForwardContract:
return_router_logits: bool = False
capture_target: Literal["moe_block", "router", "none"] = "none"
capture_index: int | None = None
capture_layer_name: str | None = None
router_logits_capture_mode: Literal["raw", "post_score"] = "post_score"
moe_output_shape: Literal["batched", "flat"] = "batched"
class AutoEPPresetAdapter:
"""Default behavior shared by presets without model-specific parser rules."""
def validate_compatibility(
self,
preset_name: str,
preset: MoEModelPreset,
model_config,
) -> None:
"""Validate public HF compatibility metadata for a selected preset."""
model_type = getattr(model_config, "model_type", None) if model_config is not None else None
self._validate_hf_model_type(preset_name, preset, model_type)
self._validate_transformers_version(preset_name, preset, model_type)
def _validate_hf_model_type(
self,
preset_name: str,
preset: MoEModelPreset,
model_type: str | None,
) -> None:
if model_type is None:
return
unsupported_note = preset.unsupported_hf_model_type_notes.get(model_type)
if unsupported_note is None:
return
supported = ", ".join(repr(value) for value in preset.hf_model_types) or "none"
raise ValueError(f"AutoEP preset '{preset_name}' does not support model_type='{model_type}'. "
f"{unsupported_note} Supported HF model_type value(s): {supported}.")
def _validate_transformers_version(
self,
preset_name: str,
preset: MoEModelPreset,
model_type: str | None,
) -> None:
min_version = preset.min_transformers_version
if min_version is None or model_type is None:
return
if not self._requires_transformers_version_validation():
return
if model_type not in preset.hf_model_types and model_type not in preset.unsupported_hf_model_type_notes:
return
try:
installed_version = self._installed_transformers_version()
except Exception as exc:
raise ValueError(f"AutoEP preset '{preset_name}' for model_type='{model_type}' requires "
f"Transformers >= {min_version}, but transformers could not be imported: {exc}.") from exc
try:
installed = Version(installed_version)
minimum = Version(min_version)
except InvalidVersion as exc:
raise ValueError(f"AutoEP preset '{preset_name}' for model_type='{model_type}' requires "
f"Transformers >= {min_version}, but the installed Transformers version "
f"'{installed_version}' could not be parsed.") from exc
if installed < minimum:
raise ValueError(f"AutoEP preset '{preset_name}' for model_type='{model_type}' requires "
f"Transformers >= {min_version}, but installed transformers=={installed_version}. "
"Upgrade Transformers or choose a preset/model combination supported by the "
"installed Transformers version.")
def _installed_transformers_version(self) -> str:
import transformers
return getattr(transformers, "__version__", "unknown")
def _requires_transformers_version_validation(self) -> bool:
# The default adapter also covers non-HF/mock/custom-compatible configs;
# specialized HF-only adapters opt in to minimum Transformers checks.
return False
def resolve_route_norm(
self,
config: AutoEPConfig,
preset: MoEModelPreset,
model_config,
) -> bool:
if config.route_norm is not None:
return config.route_norm
cfg_norm = getattr(model_config, 'norm_topk_prob', None)
if cfg_norm is not None:
return bool(cfg_norm)
return preset.route_norm
def resolve_group_routing(
self,
config: AutoEPConfig,
model_config,
) -> GroupRoutingConfig:
return GroupRoutingConfig(
num_expert_groups=config.num_expert_groups,
num_limited_groups=config.num_limited_groups,
)
def resolve_expert_layout(
self,
experts_module: nn.Module,
preset: MoEModelPreset,
) -> MoEModelPreset:
return preset
def adjust_forward_contract(self, contract: ForwardContract) -> ForwardContract:
return contract
def retarget_transformers_output_recorders(
self,
model: nn.Module,
spec: MoELayerSpec,
replacement: nn.Module,
retargeted_keys: set[str],
remove_output_capture_hooks: Callable[[nn.Module], int],
) -> None:
return
_MISSING_REGISTRY_ENTRY = object()
def _restore_transformers_output_capture_registry(
registry: dict[str, Any],
original_entries: dict[str, object],
) -> None:
for registry_key, original_entry in original_entries.items():
if original_entry is _MISSING_REGISTRY_ENTRY:
registry.pop(registry_key, None)
else:
registry[registry_key] = original_entry
def _install_instance_transformers_output_recorders(
model: nn.Module,
registry_entries: dict[str, dict[str, Any]],
output_capturing: Any,
remove_output_capture_hooks: Callable[[nn.Module], int],
) -> bool:
maybe_install_capturing_hooks = getattr(output_capturing, "maybe_install_capturing_hooks", None)
registry = getattr(output_capturing, "_CAN_RECORD_REGISTRY", None)
if not callable(maybe_install_capturing_hooks) or not isinstance(registry, dict):
return False
remove_output_capture_hooks(model)
for module in model.modules():
if hasattr(module, "_output_capturing_hooks_installed"):
module._output_capturing_hooks_installed = False
model._output_capturing_hooks_installed = False
original_entries = {
registry_key: registry.get(registry_key, _MISSING_REGISTRY_ENTRY)
for registry_key in registry_entries
}
try:
registry.update(registry_entries)
maybe_install_capturing_hooks(model)
finally:
_restore_transformers_output_capture_registry(registry, original_entries)
return True
def _retarget_transformers_output_recorders_for_modules(
*,
model: nn.Module,
display_name: str,
recorder_key: str,
retargeted_keys: set[str],
remove_output_capture_hooks: Callable[[nn.Module], int],
module_matches: Callable[[nn.Module], bool],
make_output_recorder: Callable[[Any], Any],
) -> int:
try:
from transformers.utils import output_capturing
except Exception:
return 0
registry = getattr(output_capturing, "_CAN_RECORD_REGISTRY", None)
if not isinstance(registry, dict):
return 0
registry_entries: dict[str, dict[str, Any]] = {}
retargeted = 0
for module in model.modules():
if not module_matches(module):
continue
registry_key = str(module.__class__)
record_outputs = getattr(module, "_can_record_outputs", None)
registry_outputs = registry.get(registry_key)
base_outputs = record_outputs if isinstance(record_outputs, dict) else registry_outputs
if not isinstance(base_outputs, dict) or "router_logits" not in base_outputs:
continue
retargeted_outputs = dict(base_outputs)
retargeted_outputs["router_logits"] = make_output_recorder(output_capturing.OutputRecorder)
module._can_record_outputs = retargeted_outputs
registry_entries[registry_key] = retargeted_outputs
retargeted += 1
if retargeted == 0:
from deepspeed.utils import logger
logger.warning(f"AutoEP: {display_name} conversion did not find a HF output-capture registry "
"entry for router_logits.")
return 0
if _install_instance_transformers_output_recorders(
model,
registry_entries,
output_capturing,
remove_output_capture_hooks,
):
return retargeted
if recorder_key in retargeted_keys:
return retargeted
retargeted_keys.add(recorder_key)
registry.update(registry_entries)
if getattr(model, "_output_capturing_hooks_installed", False):
remove_output_capture_hooks(model)
model._output_capturing_hooks_installed = False
return retargeted
class TransformersTopLevelRouterLogitsAdapter(AutoEPPresetAdapter):
"""Retarget Transformers model-level router-logit recorders to AutoEP."""
def __init__(
self,
*,
display_name: str,
hf_model_types: tuple[str, ...],
class_name_fragments: tuple[str, ...],
) -> None:
self.display_name = display_name
self.hf_model_types = hf_model_types
self.class_name_fragments = class_name_fragments
def adjust_forward_contract(self, contract: ForwardContract) -> ForwardContract:
# Mixtral/Qwen3/Qwen2 capture raw router logits through Transformers'
# model-level OutputRecorder hooks. AutoEP keeps the MoE block tensor
# return contract intact and retargets the recorder to router.gate.
return replace(
contract,
return_router_logits=False,
capture_target="router",
capture_index=0,
router_logits_capture_mode="raw",
)
def retarget_transformers_output_recorders(
self,
model: nn.Module,
spec: MoELayerSpec,
replacement: nn.Module,
retargeted_keys: set[str],
remove_output_capture_hooks: Callable[[nn.Module], int],
) -> None:
recorder_key = f"{spec.model_family}:{replacement.__class__.__module__}.{replacement.__class__.__qualname__}"
router_gate = getattr(getattr(replacement, "router", None), "gate", None)
if router_gate is None:
return
def module_matches(module: nn.Module) -> bool:
module_config = getattr(module, "config", None)
model_type = getattr(module_config, "model_type", None)
class_name = module.__class__.__name__
return (model_type in self.hf_model_types
or any(fragment in class_name for fragment in self.class_name_fragments))
_retarget_transformers_output_recorders_for_modules(
model=model,
display_name=self.display_name,
recorder_key=recorder_key,
retargeted_keys=retargeted_keys,
remove_output_capture_hooks=remove_output_capture_hooks,
module_matches=module_matches,
make_output_recorder=lambda OutputRecorder: OutputRecorder(
router_gate.__class__, index=0, layer_name="router.gate"),
)