grouped_topk_router.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. import os
  4. from functools import partial
  5. import torch
  6. from vllm import _custom_ops as ops
  7. from vllm import envs as envs
  8. from vllm._aiter_ops import rocm_aiter_ops
  9. from vllm.distributed.eplb.eplb_state import EplbLayerState
  10. from vllm.forward_context import get_forward_context, is_forward_context_available
  11. from vllm.model_executor.custom_op import CustomOp
  12. from vllm.model_executor.layers.fused_moe.config import (
  13. RoutingMethodType,
  14. get_routing_method_type,
  15. )
  16. from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import (
  17. rocm_aiter_grouped_topk,
  18. )
  19. from vllm.model_executor.layers.fused_moe.router.base_router import BaseRouter
  20. from vllm.model_executor.layers.fused_moe.router.fused_topk_bias_router import (
  21. fused_topk_bias,
  22. )
  23. from vllm.model_executor.layers.fused_moe.router.fused_topk_router import fused_topk
  24. from vllm.model_executor.utils import maybe_disable_graph_partition
  25. from vllm.platforms import current_platform
  26. def fused_grouped_topk(
  27. hidden_states: torch.Tensor,
  28. gating_output: torch.Tensor,
  29. topk: int,
  30. renormalize: bool,
  31. e_score_correction_bias: torch.Tensor,
  32. num_expert_group: int = 0,
  33. topk_group: int = 0,
  34. scoring_func: str = "softmax",
  35. routed_scaling_factor: float = 1.0,
  36. ) -> tuple[torch.Tensor, torch.Tensor]:
  37. assert hidden_states.size(0) == gating_output.size(0), "Number of tokens mismatch"
  38. if scoring_func == "sigmoid":
  39. # Fully fused kernel path for sigmoid
  40. topk_values, topk_indices = ops.grouped_topk(
  41. gating_output, # raw logits
  42. num_expert_group,
  43. topk_group,
  44. topk,
  45. renormalize,
  46. routed_scaling_factor,
  47. e_score_correction_bias,
  48. 1, # scoring_func=1 for sigmoid
  49. )
  50. elif scoring_func == "softmax":
  51. # Apply softmax in Python, then use fused kernel
  52. # TODO: Add support for softmax in kernel
  53. scores = torch.softmax(gating_output, dim=-1)
  54. topk_values, topk_indices = ops.grouped_topk(
  55. scores, # pre-computed scores
  56. num_expert_group,
  57. topk_group,
  58. topk,
  59. renormalize,
  60. routed_scaling_factor,
  61. e_score_correction_bias,
  62. 0, # scoring_func=0 (no activation, scores already computed)
  63. )
  64. else:
  65. raise ValueError(f"Unsupported scoring function: {scoring_func}")
  66. # Fused kernel outputs float32 values and int32 indices directly
  67. return topk_values, topk_indices
  68. # This is used by the Deepseek-V2 and Deepseek-V3 model
  69. @torch.compile(
  70. dynamic=True,
  71. backend=current_platform.simple_compile_backend,
  72. options=maybe_disable_graph_partition(current_platform.simple_compile_backend),
  73. )
  74. def grouped_topk(
  75. hidden_states: torch.Tensor,
  76. gating_output: torch.Tensor,
  77. topk: int,
  78. renormalize: bool,
  79. num_expert_group: int = 0,
  80. topk_group: int = 0,
  81. scoring_func: str = "softmax",
  82. routed_scaling_factor: float = 1.0,
  83. e_score_correction_bias: torch.Tensor | None = None,
  84. ) -> tuple[torch.Tensor, torch.Tensor]:
  85. if (
  86. envs.VLLM_USE_FUSED_MOE_GROUPED_TOPK
  87. and (current_platform.is_cuda() or current_platform.is_xpu())
  88. and num_expert_group <= 32
  89. and topk <= 32
  90. and e_score_correction_bias is not None
  91. ):
  92. return fused_grouped_topk(
  93. hidden_states=hidden_states,
  94. gating_output=gating_output,
  95. topk=topk,
  96. renormalize=renormalize,
  97. e_score_correction_bias=e_score_correction_bias,
  98. num_expert_group=num_expert_group,
  99. topk_group=topk_group,
  100. scoring_func=scoring_func,
  101. routed_scaling_factor=routed_scaling_factor,
  102. )
  103. assert hidden_states.size(0) == gating_output.size(0), "Number of tokens mismatch"
  104. if scoring_func == "softmax":
  105. scores = torch.softmax(gating_output, dim=-1)
  106. elif scoring_func == "sigmoid":
  107. scores = gating_output.sigmoid()
  108. else:
  109. raise ValueError(f"Unsupported scoring function: {scoring_func}")
  110. num_token = scores.size(0)
  111. if e_score_correction_bias is not None:
  112. # Store original scores before applying correction bias. We use biased
  113. # scores for expert selection but original scores for routing weights
  114. original_scores = scores
  115. scores = scores + e_score_correction_bias.unsqueeze(0)
  116. group_scores = (
  117. scores.view(num_token, num_expert_group, -1).topk(2, dim=-1)[0].sum(dim=-1)
  118. )
  119. else:
  120. group_scores = (
  121. scores.view(num_token, num_expert_group, -1).max(dim=-1).values
  122. ) # [n, n_group]
  123. # For batch invariance, use sorted=True to ensure deterministic expert selection
  124. use_sorted = envs.VLLM_BATCH_INVARIANT
  125. group_idx = torch.topk(group_scores, k=topk_group, dim=-1, sorted=use_sorted)[
  126. 1
  127. ] # [n, top_k_group]
  128. group_mask = torch.zeros_like(group_scores) # [n, n_group]
  129. group_mask.scatter_(1, group_idx, 1) # [n, n_group]
  130. score_mask = (
  131. group_mask.unsqueeze(-1)
  132. .expand(num_token, num_expert_group, scores.size(-1) // num_expert_group)
  133. .reshape(num_token, -1)
  134. ) # [n, e]
  135. tmp_scores = scores.masked_fill(~score_mask.bool(), float("-inf")) # [n, e]
  136. if e_score_correction_bias is not None:
  137. topk_ids = torch.topk(tmp_scores, k=topk, dim=-1, sorted=use_sorted)[1]
  138. # Use original unbiased scores for the routing weights
  139. topk_weights = original_scores.gather(1, topk_ids)
  140. else:
  141. topk_weights, topk_ids = torch.topk(
  142. tmp_scores, k=topk, dim=-1, sorted=use_sorted
  143. )
  144. if renormalize:
  145. topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
  146. if routed_scaling_factor != 1.0:
  147. topk_weights = topk_weights * routed_scaling_factor
  148. return topk_weights.to(torch.float32), topk_ids.to(torch.int32)
  149. # --8<-- [start:grouped_topk]
  150. @CustomOp.register("grouped_topk")
  151. class GroupedTopk(CustomOp):
  152. """GroupedTopk used by the Deepseek-V2 and Deepseek-V3 model."""
  153. # --8<-- [end:grouped_topk]
  154. def __init__(
  155. self,
  156. topk: int,
  157. renormalize: bool,
  158. num_expert_group: int = 0,
  159. topk_group: int = 0,
  160. scoring_func: str = "softmax",
  161. routed_scaling_factor: float = 1.0,
  162. num_fused_shared_experts: int = 0,
  163. ) -> None:
  164. super().__init__()
  165. self.native_impl = grouped_topk
  166. self.topk = topk
  167. self.renormalize = renormalize
  168. self.num_expert_group = num_expert_group
  169. self.topk_group = topk_group
  170. self.scoring_func = scoring_func
  171. self.routed_scaling_factor = routed_scaling_factor
  172. self.num_fused_shared_experts = num_fused_shared_experts
  173. def forward_native(
  174. self,
  175. hidden_states: torch.Tensor,
  176. gating_output: torch.Tensor,
  177. e_score_correction_bias: torch.Tensor | None = None,
  178. ) -> tuple[torch.Tensor, torch.Tensor]:
  179. return self.native_impl(
  180. hidden_states,
  181. gating_output,
  182. self.topk,
  183. self.renormalize,
  184. self.num_expert_group,
  185. self.topk_group,
  186. self.scoring_func,
  187. self.routed_scaling_factor,
  188. e_score_correction_bias,
  189. )
  190. def forward_cuda(
  191. self,
  192. hidden_states: torch.Tensor,
  193. gating_output: torch.Tensor,
  194. e_score_correction_bias: torch.Tensor | None = None,
  195. ) -> tuple[torch.Tensor, torch.Tensor]:
  196. return self.forward_native(
  197. hidden_states, gating_output, e_score_correction_bias
  198. )
  199. def forward_hip(
  200. self,
  201. hidden_states: torch.Tensor,
  202. gating_output: torch.Tensor,
  203. e_score_correction_bias: torch.Tensor | None = None,
  204. ) -> tuple[torch.Tensor, torch.Tensor]:
  205. if rocm_aiter_ops.is_fused_moe_enabled():
  206. if not rocm_aiter_ops.is_fusion_moe_shared_experts_enabled():
  207. assert self.num_fused_shared_experts == 0
  208. return rocm_aiter_grouped_topk(
  209. hidden_states,
  210. gating_output,
  211. self.topk,
  212. self.renormalize,
  213. self.num_expert_group,
  214. self.topk_group,
  215. self.scoring_func,
  216. self.routed_scaling_factor,
  217. e_score_correction_bias,
  218. self.num_fused_shared_experts,
  219. )
  220. else:
  221. return self.forward_native(
  222. hidden_states, gating_output, e_score_correction_bias
  223. )
  224. class GroupedTopKRouter(BaseRouter):
  225. """Router using grouped top-k routing (e.g., DeepSeekV2/V3)."""
  226. def __init__(
  227. self,
  228. top_k: int,
  229. global_num_experts: int,
  230. num_expert_group: int,
  231. topk_group: int,
  232. renormalize: bool = True,
  233. scoring_func: str = "softmax",
  234. routed_scaling_factor: float = 1.0,
  235. e_score_correction_bias: torch.Tensor | None = None,
  236. num_fused_shared_experts: int = 0,
  237. eplb_state: EplbLayerState | None = None,
  238. skip_padding: bool = False,
  239. ):
  240. super().__init__(
  241. top_k=top_k,
  242. global_num_experts=global_num_experts,
  243. eplb_state=eplb_state,
  244. )
  245. self.num_expert_group = num_expert_group
  246. self.topk_group = topk_group
  247. self.renormalize = renormalize
  248. self.scoring_func = scoring_func
  249. self.routed_scaling_factor = routed_scaling_factor
  250. self.e_score_correction_bias = e_score_correction_bias
  251. self.num_fused_shared_experts = num_fused_shared_experts
  252. # Draft truncation (draft_trunc) marks dead verify tokens as padding.
  253. self.skip_padding = skip_padding or bool(os.environ.get("VLLM_DRAFT_TRUNC_CONTROL"))
  254. @property
  255. def routing_method_type(self) -> RoutingMethodType:
  256. return get_routing_method_type(
  257. scoring_func=self.scoring_func,
  258. top_k=self.top_k,
  259. renormalize=self.renormalize,
  260. num_expert_group=self.num_expert_group,
  261. has_e_score_bias=self.e_score_correction_bias is not None,
  262. routed_scaling_factor=self.routed_scaling_factor,
  263. )
  264. def _compute_routing(
  265. self,
  266. hidden_states: torch.Tensor,
  267. router_logits: torch.Tensor,
  268. indices_type: torch.dtype | None,
  269. *,
  270. input_ids: torch.Tensor | None = None,
  271. ) -> tuple[torch.Tensor, torch.Tensor]:
  272. """Compute routing using grouped top-k."""
  273. def valid_grouping() -> bool:
  274. # Check if num_experts is greater than num_expert_group
  275. # and is divisible by num_expert_group
  276. num_experts = router_logits.shape[-1]
  277. if num_experts <= self.num_expert_group:
  278. return False
  279. return num_experts % self.num_expert_group == 0
  280. if not valid_grouping():
  281. if self.e_score_correction_bias is not None:
  282. topk_weights, topk_ids = fused_topk_bias(
  283. hidden_states=hidden_states,
  284. gating_output=router_logits,
  285. scoring_func=self.scoring_func,
  286. e_score_correction_bias=self.e_score_correction_bias.data,
  287. topk=self.top_k,
  288. renormalize=self.renormalize,
  289. )
  290. if self.routed_scaling_factor != 1.0:
  291. topk_weights *= self.routed_scaling_factor
  292. else:
  293. topk_weights, topk_ids, token_expert_indices = fused_topk(
  294. hidden_states=hidden_states,
  295. gating_output=router_logits,
  296. topk=self.top_k,
  297. renormalize=self.renormalize,
  298. indices_type=indices_type,
  299. )
  300. return topk_weights, topk_ids
  301. # Select grouped_topk implementation
  302. if rocm_aiter_ops.is_fused_moe_enabled():
  303. if not rocm_aiter_ops.is_fusion_moe_shared_experts_enabled():
  304. assert self.num_fused_shared_experts == 0
  305. grouped_topk_impl = partial(
  306. rocm_aiter_grouped_topk,
  307. num_fused_shared_experts=self.num_fused_shared_experts,
  308. )
  309. else:
  310. grouped_topk_impl = grouped_topk
  311. topk_weights, topk_ids = grouped_topk_impl(
  312. hidden_states=hidden_states,
  313. gating_output=router_logits,
  314. topk=self.top_k,
  315. renormalize=self.renormalize,
  316. num_expert_group=self.num_expert_group,
  317. topk_group=self.topk_group,
  318. scoring_func=self.scoring_func,
  319. routed_scaling_factor=self.routed_scaling_factor,
  320. e_score_correction_bias=self.e_score_correction_bias,
  321. )
  322. if (
  323. self.skip_padding
  324. and envs.VLLM_MOE_SKIP_PADDING
  325. and is_forward_context_available()
  326. ):
  327. is_padding = get_forward_context().is_padding
  328. # A sequence-parallel shard routes a slice of the batch, which the
  329. # first rows of is_padding do not describe.
  330. if is_padding is not None and is_padding.shape[0] == topk_ids.shape[0]:
  331. is_padding = is_padding[: topk_ids.shape[0]].unsqueeze(1)
  332. topk_weights = topk_weights.masked_fill(is_padding, 0)
  333. topk_ids = topk_ids.masked_fill(is_padding, -1)
  334. return topk_weights, topk_ids