main
py 293 lines 11.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Router for CPU MoE experts.
4
5 This is the routing logic that used to live inline in each CPU
6 FusedMoEExpertsMonolithic.apply() (see git history of
7 experts/cpu_moe.py); factoring it out into a proper FusedMoERouter is what
8 lets CPU MoE experts be FusedMoEExpertsModular like every other backend.
9 The math below is unchanged from that prior inline version -- this is a
10 relocation, not a rewrite.
11 """
12
13 from collections.abc import Callable
14
15 import torch
16
17 from vllm._custom_ops import biased_topk_cpu, hash_topk_cpu
18 from vllm.distributed.eplb.eplb_state import EplbLayerState
19 from vllm.model_executor.layers.fused_moe.config import (
20 RoutingMethodType,
21 get_routing_method_type,
22 )
23 from vllm.model_executor.layers.fused_moe.router.base_router import BaseRouter
24
25
26 def _grouped_topk(
27 gating_output: torch.Tensor,
28 topk: int,
29 renormalize: bool,
30 num_expert_group: int,
31 topk_group: int,
32 scoring_func: str,
33 routed_scaling_factor: float,
34 e_score_correction_bias: torch.Tensor | None,
35 ) -> tuple[torch.Tensor, torch.Tensor]:
36 gating_output = gating_output.float()
37 if scoring_func == "softmax":
38 scores = torch.softmax(gating_output, dim=-1)
39 elif scoring_func == "sigmoid":
40 scores = gating_output.sigmoid()
41 else:
42 raise ValueError(f"Unsupported scoring function: {scoring_func}")
43
44 num_token = scores.shape[0]
45 if e_score_correction_bias is not None:
46 original_scores = scores
47 scores = scores + e_score_correction_bias.unsqueeze(0)
48 group_scores = (
49 scores.view(num_token, num_expert_group, -1).topk(2, dim=-1)[0].sum(dim=-1)
50 )
51 else:
52 group_scores = (
53 scores.view(num_token, num_expert_group, -1).max(dim=-1).values
54 ) # [n, n_group]
55 group_idx = torch.topk(group_scores, k=topk_group, dim=-1, sorted=False)[
56 1
57 ] # [n, top_k_group]
58 group_mask = torch.zeros_like(group_scores) # [n, n_group]
59 group_mask.scatter_(1, group_idx, 1) # [n, n_group]
60 score_mask = (
61 group_mask.unsqueeze(-1)
62 .expand(num_token, num_expert_group, scores.shape[-1] // num_expert_group)
63 .reshape(num_token, -1)
64 ) # [n, e]
65 tmp_scores = scores.masked_fill(~score_mask.bool(), float("-inf")) # [n, e]
66
67 if e_score_correction_bias is not None:
68 topk_ids = torch.topk(tmp_scores, k=topk, dim=-1, sorted=False)[1]
69 topk_weights = original_scores.gather(1, topk_ids)
70 else:
71 topk_weights, topk_ids = torch.topk(tmp_scores, k=topk, dim=-1, sorted=False)
72
73 if renormalize:
74 topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
75
76 if routed_scaling_factor != 1.0:
77 topk_weights = topk_weights * routed_scaling_factor
78 return topk_weights, topk_ids.to(torch.int32)
79
80
81 def _sqrtsoftplus_bias_topk(
82 router_logits: torch.Tensor,
83 top_k: int,
84 renormalize: bool,
85 e_score_correction_bias: torch.Tensor | None,
86 routed_scaling_factor: float,
87 input_ids: torch.Tensor | None,
88 hash_indices_table: torch.Tensor | None,
89 ) -> tuple[torch.Tensor, torch.Tensor]:
90 """DeepSeek V4's routing: weight = sqrt(softplus(logit)); expert
91 selection uses a bias-corrected score (weight + correction_bias), but
92 the returned weight for each selected expert is the *unbiased* value
93 (the "noaux_tc" pattern -- bias steers selection only). Always
94 renormalizes selected weights to sum to ``routed_scaling_factor``,
95 matching the reference Triton kernel
96 (vllm/model_executor/layers/fused_moe/router/dsv4_topk.py).
97
98 When ``hash_indices_table`` is given (hash-routed layers), expert
99 selection instead comes from ``hash_indices_table[input_ids]`` --
100 the bias-corrected-score topk is skipped entirely, matching the CUDA
101 reference kernel (``dsv4HashTopkSoftplusSqrt``).
102
103 Backed by the ported SGLang AVX512 kernels `biased_topk_cpu` (the
104 correction-bias path) and `hash_topk_cpu` (the hash-routed-layer path);
105 see csrc/cpu/sgl-kernels/topk.cpp.
106 """
107 assert renormalize, "DeepseekV4 routing always renormalizes"
108 gating_output = router_logits.contiguous()
109 num_experts = gating_output.shape[1]
110 if hash_indices_table is not None:
111 assert input_ids is not None
112 # hash_indices_table is int32 by construction (see cpu/model.py) and
113 # advanced indexing always allocates a fresh contiguous tensor.
114 tid2eid = hash_indices_table[input_ids]
115 return hash_topk_cpu(
116 gating_output,
117 tid2eid,
118 top_k,
119 "sqrtsoftplus",
120 0, # num_fused_shared_experts: never fused into routing on CPU
121 num_experts,
122 routed_scaling_factor,
123 )
124 assert e_score_correction_bias is not None
125 return biased_topk_cpu(
126 gating_output, # only used for num_tokens/device (dtype forced fp32)
127 gating_output,
128 e_score_correction_bias.contiguous(),
129 top_k,
130 renormalize,
131 "sqrtsoftplus",
132 0, # num_fused_shared_experts: never fused into routing on CPU
133 routed_scaling_factor,
134 True, # apply_routed_scaling_factor_on_output
135 )
136
137
138 def _softmax_topk(
139 router_logits: torch.Tensor,
140 top_k: int,
141 renormalize: bool,
142 ) -> tuple[torch.Tensor, torch.Tensor]:
143 topk_logit_vals, topk_idx = torch.topk(router_logits, k=top_k, dim=-1, sorted=False)
144 if renormalize:
145 topk_vals = torch.softmax(topk_logit_vals, dim=-1)
146 else:
147 logZ = torch.logsumexp(router_logits, dim=-1, keepdim=True)
148 topk_vals = (topk_logit_vals - logZ).exp()
149 return topk_vals.to(torch.float32), topk_idx.to(torch.int32)
150
151
152 class CPURouter(BaseRouter):
153 """Router covering every routing scheme CPU MoE experts support: plain
154 softmax top-k, grouped top-k (with an optional correction bias), an
155 arbitrary custom_routing_function, and DeepSeek V4's sqrtsoftplus scheme
156 (bias-corrected or hash-routed via the ported biased_topk_cpu/
157 hash_topk_cpu kernels). All of the above except sqrtsoftplus are plain
158 torch ops so Dynamo traces straight through into them like it did when
159 this logic lived inline in FusedMoEExpertsMonolithic.apply() -- no new
160 compile boundary is introduced here.
161 """
162
163 def __init__(
164 self,
165 top_k: int,
166 global_num_experts: int,
167 use_grouped_topk: bool = False,
168 num_expert_group: int | None = None,
169 topk_group: int | None = None,
170 renormalize: bool = True,
171 scoring_func: str = "softmax",
172 routed_scaling_factor: float = 1.0,
173 e_score_correction_bias: torch.Tensor | None = None,
174 custom_routing_function: Callable | None = None,
175 hash_indices_table: torch.Tensor | None = None,
176 eplb_state: EplbLayerState | None = None,
177 ):
178 super().__init__(
179 top_k=top_k,
180 global_num_experts=global_num_experts,
181 eplb_state=eplb_state,
182 )
183 self.use_grouped_topk = use_grouped_topk
184 self.num_expert_group = num_expert_group
185 self.topk_group = topk_group
186 self.renormalize = renormalize
187 self.scoring_func = scoring_func
188 self.routed_scaling_factor = routed_scaling_factor
189 self.e_score_correction_bias = e_score_correction_bias
190 self.custom_routing_function = custom_routing_function
191 self._hash_indices_table = hash_indices_table
192
193 @property
194 def routing_method_type(self) -> RoutingMethodType:
195 return get_routing_method_type(
196 scoring_func=self.scoring_func,
197 top_k=self.top_k,
198 renormalize=self.renormalize,
199 num_expert_group=self.num_expert_group,
200 has_e_score_bias=self.e_score_correction_bias is not None,
201 )
202
203 def _compute_routing(
204 self,
205 hidden_states: torch.Tensor,
206 router_logits: torch.Tensor,
207 indices_type: torch.dtype | None,
208 *,
209 input_ids: torch.Tensor | None = None,
210 ) -> tuple[torch.Tensor, torch.Tensor]:
211 if self.use_grouped_topk:
212 assert self.topk_group is not None
213 assert self.num_expert_group is not None
214 return _grouped_topk(
215 gating_output=router_logits,
216 topk=self.top_k,
217 renormalize=self.renormalize,
218 num_expert_group=self.num_expert_group,
219 topk_group=self.topk_group,
220 scoring_func=self.scoring_func,
221 routed_scaling_factor=self.routed_scaling_factor,
222 e_score_correction_bias=self.e_score_correction_bias,
223 )
224 elif self.scoring_func == "sqrtsoftplus":
225 assert (
226 self._hash_indices_table is not None
227 or self.e_score_correction_bias is not None
228 )
229 return _sqrtsoftplus_bias_topk(
230 router_logits=router_logits,
231 top_k=self.top_k,
232 renormalize=self.renormalize,
233 e_score_correction_bias=self.e_score_correction_bias,
234 routed_scaling_factor=self.routed_scaling_factor,
235 input_ids=input_ids,
236 hash_indices_table=self._hash_indices_table,
237 )
238 elif self.custom_routing_function is None:
239 assert self.scoring_func == "softmax"
240 return _softmax_topk(router_logits, self.top_k, self.renormalize)
241 else:
242 topk_weights, topk_ids = self.custom_routing_function(
243 hidden_states=hidden_states,
244 gating_output=router_logits,
245 topk=self.top_k,
246 renormalize=self.renormalize,
247 )
248 # cpu_fused_moe reads routing tensors as contiguous float32/int32
249 # buffers and does not account for tensor strides.
250 topk_weights = topk_weights.to(torch.float32).contiguous()
251 topk_ids = topk_ids.to(torch.int32).contiguous()
252 return topk_weights, topk_ids
253
254
255 def select_experts(
256 hidden_states: torch.Tensor,
257 router_logits: torch.Tensor,
258 top_k: int,
259 use_grouped_topk: bool,
260 renormalize: bool,
261 topk_group: int | None = None,
262 num_expert_group: int | None = None,
263 custom_routing_function: Callable | None = None,
264 scoring_func: str = "softmax",
265 routed_scaling_factor: float = 1.0,
266 e_score_correction_bias: torch.Tensor | None = None,
267 input_ids: torch.Tensor | None = None,
268 hash_indices_table: torch.Tensor | None = None,
269 ) -> tuple[torch.Tensor, torch.Tensor]:
270 """Routing helper for the CPU MoE experts that still use the monolithic
271 contract and so must compute routing themselves from ``router_logits``
272 (the zentorch-accelerated path and the ARM W4A8 dynamic-quant kernel,
273 kept monolithic since zentorch/Arm hardware isn't available to validate
274 a modular migration here). Modular CPU experts get routing from
275 ``CPURouter`` instead; this is a thin wrapper around the same logic for
276 the two classes that can't use it.
277 """
278 router = CPURouter(
279 top_k=top_k,
280 global_num_experts=router_logits.shape[-1],
281 use_grouped_topk=use_grouped_topk,
282 num_expert_group=num_expert_group,
283 topk_group=topk_group,
284 renormalize=renormalize,
285 scoring_func=scoring_func,
286 routed_scaling_factor=routed_scaling_factor,
287 e_score_correction_bias=e_score_correction_bias,
288 custom_routing_function=custom_routing_function,
289 hash_indices_table=hash_indices_table,
290 )
291 return router._compute_routing(
292 hidden_states, router_logits, None, input_ids=input_ids
293 )