# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project import os from functools import partial import torch from vllm import _custom_ops as ops from vllm import envs as envs from vllm._aiter_ops import rocm_aiter_ops from vllm.distributed.eplb.eplb_state import EplbLayerState from vllm.forward_context import get_forward_context, is_forward_context_available from vllm.model_executor.custom_op import CustomOp from vllm.model_executor.layers.fused_moe.config import ( RoutingMethodType, get_routing_method_type, ) from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import ( rocm_aiter_grouped_topk, ) from vllm.model_executor.layers.fused_moe.router.base_router import BaseRouter from vllm.model_executor.layers.fused_moe.router.fused_topk_bias_router import ( fused_topk_bias, ) from vllm.model_executor.layers.fused_moe.router.fused_topk_router import fused_topk from vllm.model_executor.utils import maybe_disable_graph_partition from vllm.platforms import current_platform def fused_grouped_topk( hidden_states: torch.Tensor, gating_output: torch.Tensor, topk: int, renormalize: bool, e_score_correction_bias: torch.Tensor, num_expert_group: int = 0, topk_group: int = 0, scoring_func: str = "softmax", routed_scaling_factor: float = 1.0, ) -> tuple[torch.Tensor, torch.Tensor]: assert hidden_states.size(0) == gating_output.size(0), "Number of tokens mismatch" if scoring_func == "sigmoid": # Fully fused kernel path for sigmoid topk_values, topk_indices = ops.grouped_topk( gating_output, # raw logits num_expert_group, topk_group, topk, renormalize, routed_scaling_factor, e_score_correction_bias, 1, # scoring_func=1 for sigmoid ) elif scoring_func == "softmax": # Apply softmax in Python, then use fused kernel # TODO: Add support for softmax in kernel scores = torch.softmax(gating_output, dim=-1) topk_values, topk_indices = ops.grouped_topk( scores, # pre-computed scores num_expert_group, topk_group, topk, renormalize, routed_scaling_factor, e_score_correction_bias, 0, # scoring_func=0 (no activation, scores already computed) ) else: raise ValueError(f"Unsupported scoring function: {scoring_func}") # Fused kernel outputs float32 values and int32 indices directly return topk_values, topk_indices # This is used by the Deepseek-V2 and Deepseek-V3 model @torch.compile( dynamic=True, backend=current_platform.simple_compile_backend, options=maybe_disable_graph_partition(current_platform.simple_compile_backend), ) def grouped_topk( hidden_states: torch.Tensor, gating_output: torch.Tensor, topk: int, renormalize: bool, num_expert_group: int = 0, topk_group: int = 0, scoring_func: str = "softmax", routed_scaling_factor: float = 1.0, e_score_correction_bias: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: if ( envs.VLLM_USE_FUSED_MOE_GROUPED_TOPK and (current_platform.is_cuda() or current_platform.is_xpu()) and num_expert_group <= 32 and topk <= 32 and e_score_correction_bias is not None ): return fused_grouped_topk( hidden_states=hidden_states, gating_output=gating_output, topk=topk, renormalize=renormalize, e_score_correction_bias=e_score_correction_bias, num_expert_group=num_expert_group, topk_group=topk_group, scoring_func=scoring_func, routed_scaling_factor=routed_scaling_factor, ) assert hidden_states.size(0) == gating_output.size(0), "Number of tokens mismatch" if scoring_func == "softmax": scores = torch.softmax(gating_output, dim=-1) elif scoring_func == "sigmoid": scores = gating_output.sigmoid() else: raise ValueError(f"Unsupported scoring function: {scoring_func}") num_token = scores.size(0) if e_score_correction_bias is not None: # Store original scores before applying correction bias. We use biased # scores for expert selection but original scores for routing weights original_scores = scores scores = scores + e_score_correction_bias.unsqueeze(0) group_scores = ( scores.view(num_token, num_expert_group, -1).topk(2, dim=-1)[0].sum(dim=-1) ) else: group_scores = ( scores.view(num_token, num_expert_group, -1).max(dim=-1).values ) # [n, n_group] # For batch invariance, use sorted=True to ensure deterministic expert selection use_sorted = envs.VLLM_BATCH_INVARIANT group_idx = torch.topk(group_scores, k=topk_group, dim=-1, sorted=use_sorted)[ 1 ] # [n, top_k_group] group_mask = torch.zeros_like(group_scores) # [n, n_group] group_mask.scatter_(1, group_idx, 1) # [n, n_group] score_mask = ( group_mask.unsqueeze(-1) .expand(num_token, num_expert_group, scores.size(-1) // num_expert_group) .reshape(num_token, -1) ) # [n, e] tmp_scores = scores.masked_fill(~score_mask.bool(), float("-inf")) # [n, e] if e_score_correction_bias is not None: topk_ids = torch.topk(tmp_scores, k=topk, dim=-1, sorted=use_sorted)[1] # Use original unbiased scores for the routing weights topk_weights = original_scores.gather(1, topk_ids) else: topk_weights, topk_ids = torch.topk( tmp_scores, k=topk, dim=-1, sorted=use_sorted ) if renormalize: topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True) if routed_scaling_factor != 1.0: topk_weights = topk_weights * routed_scaling_factor return topk_weights.to(torch.float32), topk_ids.to(torch.int32) # --8<-- [start:grouped_topk] @CustomOp.register("grouped_topk") class GroupedTopk(CustomOp): """GroupedTopk used by the Deepseek-V2 and Deepseek-V3 model.""" # --8<-- [end:grouped_topk] def __init__( self, topk: int, renormalize: bool, num_expert_group: int = 0, topk_group: int = 0, scoring_func: str = "softmax", routed_scaling_factor: float = 1.0, num_fused_shared_experts: int = 0, ) -> None: super().__init__() self.native_impl = grouped_topk self.topk = topk self.renormalize = renormalize self.num_expert_group = num_expert_group self.topk_group = topk_group self.scoring_func = scoring_func self.routed_scaling_factor = routed_scaling_factor self.num_fused_shared_experts = num_fused_shared_experts def forward_native( self, hidden_states: torch.Tensor, gating_output: torch.Tensor, e_score_correction_bias: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: return self.native_impl( hidden_states, gating_output, self.topk, self.renormalize, self.num_expert_group, self.topk_group, self.scoring_func, self.routed_scaling_factor, e_score_correction_bias, ) def forward_cuda( self, hidden_states: torch.Tensor, gating_output: torch.Tensor, e_score_correction_bias: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: return self.forward_native( hidden_states, gating_output, e_score_correction_bias ) def forward_hip( self, hidden_states: torch.Tensor, gating_output: torch.Tensor, e_score_correction_bias: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: if rocm_aiter_ops.is_fused_moe_enabled(): if not rocm_aiter_ops.is_fusion_moe_shared_experts_enabled(): assert self.num_fused_shared_experts == 0 return rocm_aiter_grouped_topk( hidden_states, gating_output, self.topk, self.renormalize, self.num_expert_group, self.topk_group, self.scoring_func, self.routed_scaling_factor, e_score_correction_bias, self.num_fused_shared_experts, ) else: return self.forward_native( hidden_states, gating_output, e_score_correction_bias ) class GroupedTopKRouter(BaseRouter): """Router using grouped top-k routing (e.g., DeepSeekV2/V3).""" def __init__( self, top_k: int, global_num_experts: int, num_expert_group: int, topk_group: int, renormalize: bool = True, scoring_func: str = "softmax", routed_scaling_factor: float = 1.0, e_score_correction_bias: torch.Tensor | None = None, num_fused_shared_experts: int = 0, eplb_state: EplbLayerState | None = None, skip_padding: bool = False, ): super().__init__( top_k=top_k, global_num_experts=global_num_experts, eplb_state=eplb_state, ) self.num_expert_group = num_expert_group self.topk_group = topk_group self.renormalize = renormalize self.scoring_func = scoring_func self.routed_scaling_factor = routed_scaling_factor self.e_score_correction_bias = e_score_correction_bias self.num_fused_shared_experts = num_fused_shared_experts # Draft truncation (draft_trunc) marks dead verify tokens as padding. self.skip_padding = skip_padding or bool(os.environ.get("VLLM_DRAFT_TRUNC_CONTROL")) @property def routing_method_type(self) -> RoutingMethodType: return get_routing_method_type( scoring_func=self.scoring_func, top_k=self.top_k, renormalize=self.renormalize, num_expert_group=self.num_expert_group, has_e_score_bias=self.e_score_correction_bias is not None, routed_scaling_factor=self.routed_scaling_factor, ) def _compute_routing( self, hidden_states: torch.Tensor, router_logits: torch.Tensor, indices_type: torch.dtype | None, *, input_ids: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: """Compute routing using grouped top-k.""" def valid_grouping() -> bool: # Check if num_experts is greater than num_expert_group # and is divisible by num_expert_group num_experts = router_logits.shape[-1] if num_experts <= self.num_expert_group: return False return num_experts % self.num_expert_group == 0 if not valid_grouping(): if self.e_score_correction_bias is not None: topk_weights, topk_ids = fused_topk_bias( hidden_states=hidden_states, gating_output=router_logits, scoring_func=self.scoring_func, e_score_correction_bias=self.e_score_correction_bias.data, topk=self.top_k, renormalize=self.renormalize, ) if self.routed_scaling_factor != 1.0: topk_weights *= self.routed_scaling_factor else: topk_weights, topk_ids, token_expert_indices = fused_topk( hidden_states=hidden_states, gating_output=router_logits, topk=self.top_k, renormalize=self.renormalize, indices_type=indices_type, ) return topk_weights, topk_ids # Select grouped_topk implementation if rocm_aiter_ops.is_fused_moe_enabled(): if not rocm_aiter_ops.is_fusion_moe_shared_experts_enabled(): assert self.num_fused_shared_experts == 0 grouped_topk_impl = partial( rocm_aiter_grouped_topk, num_fused_shared_experts=self.num_fused_shared_experts, ) else: grouped_topk_impl = grouped_topk topk_weights, topk_ids = grouped_topk_impl( hidden_states=hidden_states, gating_output=router_logits, topk=self.top_k, renormalize=self.renormalize, num_expert_group=self.num_expert_group, topk_group=self.topk_group, scoring_func=self.scoring_func, routed_scaling_factor=self.routed_scaling_factor, e_score_correction_bias=self.e_score_correction_bias, ) if ( self.skip_padding and envs.VLLM_MOE_SKIP_PADDING and is_forward_context_available() ): is_padding = get_forward_context().is_padding # A sequence-parallel shard routes a slice of the batch, which the # first rows of is_padding do not describe. if is_padding is not None and is_padding.shape[0] == topk_ids.shape[0]: is_padding = is_padding[: topk_ids.shape[0]].unsqueeze(1) topk_weights = topk_weights.masked_fill(is_padding, 0) topk_ids = topk_ids.masked_fill(is_padding, -1) return topk_weights, topk_ids