336 lines
15 KiB
Python
336 lines
15 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
# Copyright 2024 The vLLM team.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""Transformers modeling backend mixin for Mixture of Experts (MoE) models."""
|
|
|
|
from dataclasses import dataclass
|
|
from functools import partial
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import torch
|
|
import torch.nn as nn
|
|
|
|
from vllm.config.utils import getattr_iter
|
|
from vllm.distributed import get_dp_group, get_ep_group
|
|
from vllm.forward_context import ForwardContext, get_forward_context
|
|
from vllm.logger import init_logger
|
|
from vllm.model_executor.custom_op import PluggableLayer
|
|
from vllm.model_executor.layers.fused_moe import FusedMoE, MoERunner, RoutedExperts
|
|
from vllm.model_executor.models.interfaces import MixtureOfExperts
|
|
from vllm.model_executor.models.transformers.fusers.moe import MoEBlockFuser
|
|
from vllm.model_executor.models.utils import maybe_prefix
|
|
from vllm.utils.torch_utils import direct_register_custom_op
|
|
|
|
from .utils import log_replacement
|
|
|
|
if TYPE_CHECKING:
|
|
from vllm.config import VllmConfig
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class TransformersMoEState:
|
|
topk_ids: torch.Tensor | None = None
|
|
is_sequence_parallel: bool = False
|
|
|
|
|
|
# --8<-- [start:transformers_fused_moe]
|
|
@PluggableLayer.register("transformers_fused_moe")
|
|
class TransformersMoERunner(MoERunner):
|
|
"""Custom FusedMoE for the Transformers modeling backend."""
|
|
|
|
# --8<-- [end:transformers_fused_moe]
|
|
def __init__(self, *args, moe_state: TransformersMoEState, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self._moe_state = moe_state
|
|
self._moe_state.is_sequence_parallel = self.moe_config.is_sequence_parallel
|
|
|
|
def forward(
|
|
self,
|
|
hidden_states: torch.Tensor,
|
|
topk_ids: torch.Tensor,
|
|
topk_weights: torch.Tensor,
|
|
**kwargs: Any,
|
|
) -> torch.Tensor:
|
|
"""In Transformers `experts.forward` will have this signature.
|
|
|
|
We discard any extra kwargs because we cannot use them here."""
|
|
# Note: we need to forward through a custom op so the topk_ids
|
|
# can be transferred without interfering with cudagraphs.
|
|
return torch.ops.vllm.transformers_moe_forward(
|
|
hidden_states,
|
|
topk_ids.to(torch.int32),
|
|
topk_weights.to(torch.float32),
|
|
self.layer_name,
|
|
)
|
|
|
|
def _forward_super(
|
|
self,
|
|
hidden_states: torch.Tensor,
|
|
topk_weights: torch.Tensor,
|
|
) -> torch.Tensor:
|
|
return super().forward(hidden_states, topk_weights)
|
|
|
|
|
|
def _transformers_moe_forward(
|
|
hidden_states: torch.Tensor,
|
|
topk_ids: torch.Tensor,
|
|
topk_weights: torch.Tensor,
|
|
layer_name: str,
|
|
) -> torch.Tensor:
|
|
"""Store the `topk_ids` in the layer and call the actual forward."""
|
|
forward_context: ForwardContext = get_forward_context()
|
|
self = forward_context.no_compile_layers[layer_name]
|
|
self._moe_state.topk_ids = topk_ids
|
|
return self._forward_super(hidden_states, topk_weights)
|
|
|
|
|
|
def _transformers_moe_forward_fake(
|
|
hidden_states: torch.Tensor,
|
|
topk_ids: torch.Tensor,
|
|
topk_weights: torch.Tensor,
|
|
layer_name: str,
|
|
) -> torch.Tensor:
|
|
return torch.empty_like(hidden_states)
|
|
|
|
|
|
direct_register_custom_op(
|
|
op_name="transformers_moe_forward",
|
|
op_func=_transformers_moe_forward,
|
|
mutates_args=["hidden_states"],
|
|
fake_impl=_transformers_moe_forward_fake,
|
|
tags=(torch.Tag.needs_fixed_stride_order,),
|
|
)
|
|
|
|
|
|
class TransformersRoutedExperts(RoutedExperts):
|
|
def get_expert_mapping(
|
|
self, include_fused: bool = False
|
|
) -> list[tuple[str, str, int, str]]:
|
|
common_names = ("gate_proj", "down_proj", "up_proj")
|
|
common_map = super().get_expert_mapping(*common_names, include_fused)
|
|
mixtral_map = super().get_expert_mapping("w1", "w2", "w3", include_fused)
|
|
if not include_fused:
|
|
return common_map + mixtral_map
|
|
common_fused, common_unfused = common_map[:3], common_map[3:]
|
|
mixtral_fused, mixtral_unfused = mixtral_map[:3], mixtral_map[3:]
|
|
return common_fused + mixtral_fused + common_unfused + mixtral_unfused
|
|
|
|
|
|
class MoEMixin(MixtureOfExperts):
|
|
def __init__(self, *, vllm_config: "VllmConfig", prefix: str = ""):
|
|
self.check_version("5.0.0", "MoE models support")
|
|
# Skip MixtureOfExperts.__init__ and call the next class in MRO
|
|
super(MixtureOfExperts, self).__init__(vllm_config=vllm_config, prefix=prefix)
|
|
|
|
def update_physical_experts_metadata(
|
|
self,
|
|
num_physical_experts: int,
|
|
num_local_physical_experts: int,
|
|
):
|
|
assert self.num_local_physical_experts == num_local_physical_experts
|
|
self.num_physical_experts = num_physical_experts
|
|
self.num_local_physical_experts = num_local_physical_experts
|
|
self.num_redundant_experts = num_physical_experts - self.num_logical_experts
|
|
for moe_block in self.mlp_layers:
|
|
moe_block.n_local_physical_experts = num_local_physical_experts
|
|
moe_block.n_physical_experts = num_physical_experts
|
|
moe_block.n_redundant_experts = self.num_redundant_experts
|
|
moe_block.experts.update_expert_map()
|
|
|
|
def recursive_replace(self):
|
|
"""Initialize the MoE layers."""
|
|
experts_name = "experts"
|
|
text_config = self.text_config
|
|
|
|
# Positional arguments
|
|
num_experts = self.model_config.get_num_experts()
|
|
top_k = getattr_iter(text_config, ["num_experts_per_tok", "top_k"], None)
|
|
assert top_k is not None
|
|
hidden_size = text_config.hidden_size
|
|
intermediate_size = getattr_iter(
|
|
text_config, ["moe_intermediate_size", "intermediate_size"], None
|
|
)
|
|
assert intermediate_size is not None
|
|
|
|
num_shared_experts = getattr_iter(
|
|
text_config,
|
|
[
|
|
"n_shared_experts", # DeepSeek, Docs, GLM
|
|
"moe_num_shared_experts", # Aria, Ernie
|
|
],
|
|
0,
|
|
)
|
|
|
|
# Unused kwargs since we use custom_routing_function:
|
|
# - `scoring_func` and `e_score_correction_bias` only used for grouped
|
|
# topk routing inside vLLM and are non-trivial to infer
|
|
# and hard code `use_grouped_topk=False`
|
|
# - `renormalize` passed anyway because it's easy to infer
|
|
# - `num_expert_group` and `topk_group` used for inferring expert
|
|
# placement strategy in FusedMoE
|
|
# - `apply_router_weight_on_input` is already applied in Transformers
|
|
renormalize = getattr(text_config, "norm_topk_prob", top_k > 1)
|
|
num_expert_group = getattr(text_config, "n_group", None)
|
|
topk_group = getattr(text_config, "topk_group", None)
|
|
|
|
# MoE activation function
|
|
activation = "silu"
|
|
wrapped_arch = self.config.architectures[0].lower()
|
|
if "gptoss" in wrapped_arch:
|
|
activation = "swigluoai"
|
|
|
|
# Expert parallel load balancing kwargs
|
|
enable_eplb = self.parallel_config.enable_eplb
|
|
num_redundant_experts = self.parallel_config.eplb_config.num_redundant_experts
|
|
|
|
# MixtureOfExperts mixin settings
|
|
ep_size = get_ep_group().world_size
|
|
|
|
self.mlp_layers = [] # Used for MixtureOfExperts methods
|
|
self.moe_layers = []
|
|
self.num_expert_groups = 1 if num_expert_group is None else num_expert_group
|
|
self.num_logical_experts = num_experts
|
|
self.num_physical_experts = num_experts + num_redundant_experts
|
|
self.num_local_physical_experts = self.num_physical_experts // ep_size
|
|
self.num_routed_experts = num_experts
|
|
self.num_shared_experts = num_shared_experts
|
|
self.num_redundant_experts = num_redundant_experts
|
|
|
|
# Recursively fuse MoE layers
|
|
def _recursive_replace(module: nn.Module, prefix: str):
|
|
for child_name, child_module in module.named_children():
|
|
qual_name = maybe_prefix(prefix, child_name)
|
|
# Naive implementations will have experts as ModuleList
|
|
is_modulelist = isinstance(child_module, nn.ModuleList)
|
|
# Packed implementations will have experts as 3D tensors of shapes like:
|
|
# gate_up_proj = (num_experts, 2 * intermediate_size, hidden_size)
|
|
# down_proj = (num_experts, intermediate_size, hidden_size)
|
|
params = list(child_module.parameters())
|
|
is_3d = len(params) > 0 and all(p.ndim == 3 for p in params)
|
|
if child_name == experts_name and (is_modulelist or is_3d):
|
|
# Alias for readability
|
|
moe_block = module
|
|
experts = child_module
|
|
# Class of the fused block (parent of gate/experts/shared)
|
|
moe_block_cls = type(moe_block).__name__
|
|
experts_cls = type(experts).__name__
|
|
# Do the experts have biases
|
|
has_bias = False
|
|
for experts_param_name, _ in experts.named_parameters():
|
|
if "bias" in experts_param_name:
|
|
has_bias = True
|
|
break
|
|
# If the config does not specify num_shared_experts, but
|
|
# the model has shared experts, we assume there is one.
|
|
if self.num_shared_experts == 0:
|
|
for moe_block_param_name, _ in moe_block.named_parameters():
|
|
if "shared_expert" in moe_block_param_name:
|
|
self.num_shared_experts = 1
|
|
break
|
|
|
|
kwargs: dict[str, Any] = dict(
|
|
num_experts=num_experts,
|
|
top_k=top_k,
|
|
hidden_size=hidden_size,
|
|
intermediate_size=intermediate_size,
|
|
renormalize=renormalize,
|
|
use_grouped_topk=False,
|
|
quant_config=self.quant_config,
|
|
prefix=qual_name,
|
|
activation=activation,
|
|
enable_eplb=enable_eplb,
|
|
num_redundant_experts=num_redundant_experts,
|
|
has_bias=has_bias,
|
|
routed_experts_cls=TransformersRoutedExperts,
|
|
)
|
|
fuser = MoEBlockFuser.match(moe_block, experts_name)
|
|
if self.num_expert_groups <= 1 and fuser is not None:
|
|
# MoE block forward is fully replaced.
|
|
# gate/router and shared expert (if any) runs in FusedMoE.
|
|
kwargs |= dict(
|
|
scoring_func=fuser.scoring_func,
|
|
is_sequence_parallel=(
|
|
self.parallel_config.use_sequence_parallel_moe
|
|
),
|
|
gate=fuser.gate(moe_block, prefix),
|
|
shared_experts=fuser.shared_experts(moe_block, prefix),
|
|
)
|
|
fuser.rewrite_forward(moe_block)
|
|
routed = "gate + experts"
|
|
if fuser.shared_name:
|
|
routed += " + shared experts"
|
|
logger.info_once(
|
|
"Fused: %s (%s) -> FusedMoE (internal routing)",
|
|
routed,
|
|
moe_block_cls,
|
|
)
|
|
else:
|
|
# MoE block forward is unmodified.
|
|
# gate/router and shared expert (if any) runs in Transformers.
|
|
# We then smuggle the topk_ids in using a custom op.
|
|
moe_state = TransformersMoEState()
|
|
|
|
def custom_routing_function(
|
|
hidden_states: torch.Tensor,
|
|
gating_output: torch.Tensor,
|
|
topk: int,
|
|
renormalize: bool,
|
|
moe_state: TransformersMoEState,
|
|
):
|
|
"""Return `topk_weights` from `gating_output` and the
|
|
`topk_ids` we stored in the layer earlier."""
|
|
topk_weights = gating_output
|
|
topk_ids = moe_state.topk_ids
|
|
assert topk_ids is not None
|
|
# Handle all gather in expert parallel
|
|
if topk_ids.size(0) != hidden_states.size(0):
|
|
dp_metadata = get_forward_context().dp_metadata
|
|
sizes = dp_metadata.get_chunk_sizes_across_dp_rank()
|
|
is_sp = moe_state.is_sequence_parallel
|
|
group = get_ep_group() if is_sp else get_dp_group()
|
|
assert sizes[group.rank_in_group] == topk_ids.shape[0]
|
|
(topk_ids,) = group.all_gatherv([topk_ids], 0, sizes)
|
|
return topk_weights, topk_ids
|
|
|
|
kwargs |= dict(
|
|
num_expert_group=num_expert_group,
|
|
topk_group=topk_group,
|
|
custom_routing_function=partial(
|
|
custom_routing_function, moe_state=moe_state
|
|
),
|
|
runner_cls=TransformersMoERunner,
|
|
runner_args={"moe_state": moe_state},
|
|
)
|
|
logger.info_once(
|
|
"Fused: experts (%s) -> FusedMoE (external routing)",
|
|
experts_cls,
|
|
)
|
|
fused_experts = FusedMoE(**kwargs)
|
|
moe_block.experts = fused_experts
|
|
log_replacement(qual_name, experts, fused_experts)
|
|
# Update MixtureOfExperts mixin state
|
|
self.mlp_layers.append(moe_block)
|
|
self.moe_layers.append(fused_experts)
|
|
else:
|
|
_recursive_replace(child_module, prefix=qual_name)
|
|
|
|
_recursive_replace(self.model, prefix="model")
|
|
self.num_moe_layers = len(self.moe_layers)
|
|
# Continue with the replacement of layers in Base
|
|
super().recursive_replace()
|