main
py 1,859 lines 59.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """CPU fused MoE experts."""
4
5 import math
6 import sys
7 from typing import cast
8
9 import torch
10
11 import vllm.model_executor.layers.fused_moe.modular_kernel as mk
12 from vllm._custom_ops import (
13 CPUQuantAlgo,
14 CPUQuantMethod,
15 convert_weight_packed_scale_zp,
16 cpu_fused_moe,
17 cpu_fused_moe_int8,
18 cpu_has_amx_fp8,
19 cpu_prepack_moe_weight,
20 cpu_prepack_moe_weight_int8,
21 fused_experts_cpu,
22 )
23 from vllm.logger import init_logger
24 from vllm.model_executor.kernels.linear.zentorch_utils import (
25 _ZENTORCH_MOE_ACTIVATIONS,
26 has_zentorch_op,
27 is_zentorch_moe_config_supported,
28 is_zentorch_moe_supported,
29 )
30 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
31 from vllm.model_executor.layers.fused_moe.config import (
32 FusedMoEConfig,
33 FusedMoEParallelConfig,
34 FusedMoEQuantConfig,
35 RoutingMethodType,
36 )
37 from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
38 TopKWeightAndReduceNoOP,
39 )
40 from vllm.model_executor.layers.quantization.utils.quant_utils import (
41 QuantKey,
42 kFp8Dynamic128Sym,
43 kFp8Static128BlockSym,
44 kInt4Static,
45 kInt8DynamicTokenSym,
46 kInt8StaticChannelSym,
47 kMxfp4Static,
48 )
49 from vllm.model_executor.utils import replace_parameter
50 from vllm.platforms import CpuArchEnum, current_platform
51 from vllm.utils.math_utils import round_up
52
53 logger = init_logger(__name__)
54 # ===========================================================================
55 # Weight layout
56 # ===========================================================================
57
58
59 def swigluoai_w13_interleave_perm(
60 experts_cls: type[mk.FusedMoEExperts],
61 activation: MoEActivation,
62 two_i: int,
63 device: torch.device,
64 ) -> torch.Tensor | None:
65 """Index taking a half-split w13 ``[gate | up]`` to the interleaved order
66 ZenDNN's ``swiglu_oai_mul`` expects, or None when no reorder is needed.
67 Callers apply it along whichever axis holds their output channels."""
68 if not getattr(experts_cls, "requires_interleaved_w13", False):
69 return None
70 if activation != MoEActivation.SWIGLUOAI:
71 return None
72
73 i = two_i // 2
74 return torch.stack(
75 [torch.arange(0, i, device=device), torch.arange(i, two_i, device=device)],
76 dim=1,
77 ).flatten()
78
79
80 # ===========================================================================
81 # Unquantized (BF16/FP16/FP32) MoE
82 # ===========================================================================
83
84
85 class CPUUnquantizedExperts(mk.FusedMoEExpertsModular):
86 """Portable vector grouped-gemm unquantized MoE experts."""
87
88 isa = "vec"
89 output_alignment = 32
90 reduction_alignment = 1
91
92 def __init__(
93 self,
94 moe_config: FusedMoEConfig,
95 quant_config: FusedMoEQuantConfig,
96 ):
97 super().__init__(moe_config, quant_config)
98 self._use_zentorch = False
99
100 @classmethod
101 def _intermediate_alignment(cls) -> int:
102 return math.lcm(cls.output_alignment, cls.reduction_alignment)
103
104 @classmethod
105 def _padded_intermediate_size(cls, moe_config: FusedMoEConfig) -> int:
106 intermediate_size = moe_config.intermediate_size_per_partition
107 if moe_config.activation == MoEActivation.SWIGLUOAI:
108 return intermediate_size
109 return round_up(intermediate_size, cls._intermediate_alignment())
110
111 @classmethod
112 def _supports_grouped_gemm(
113 cls,
114 moe_config: FusedMoEConfig,
115 ) -> tuple[bool, str | None]:
116 intermediate_size = cls._padded_intermediate_size(moe_config)
117 if (
118 moe_config.hidden_dim % cls.output_alignment != 0
119 or intermediate_size % cls.output_alignment != 0
120 ):
121 return False, (
122 "kernel requires hidden and intermediate dimensions divisible by "
123 f"{cls.output_alignment}"
124 )
125 if (
126 moe_config.hidden_dim % cls.reduction_alignment != 0
127 or intermediate_size % cls.reduction_alignment != 0
128 ):
129 return False, (
130 "kernel requires reduction dimensions divisible by "
131 f"{cls.reduction_alignment}"
132 )
133 return True, None
134
135 @property
136 def expects_unquantized_inputs(self) -> bool:
137 return True
138
139 @staticmethod
140 def activation_format() -> mk.FusedMoEActivationFormat:
141 return mk.FusedMoEActivationFormat.Standard
142
143 @staticmethod
144 def _supports_current_device() -> bool:
145 return current_platform.is_cpu()
146
147 @staticmethod
148 def is_supported_config(
149 cls: type[mk.FusedMoEExperts],
150 moe_config: FusedMoEConfig,
151 weight_key: QuantKey | None,
152 activation_key: QuantKey | None,
153 activation_format: mk.FusedMoEActivationFormat,
154 ) -> tuple[bool, str | None]:
155 supported, reason = mk.FusedMoEExperts.is_supported_config(
156 cls, moe_config, weight_key, activation_key, activation_format
157 )
158 if not supported:
159 return supported, reason
160 if is_zentorch_moe_config_supported(moe_config):
161 return True, None
162 cpu_cls = cast(type[CPUUnquantizedExperts], cls)
163 return cpu_cls._supports_grouped_gemm(moe_config)
164
165 @staticmethod
166 def _supports_no_act_and_mul() -> bool:
167 return False
168
169 @staticmethod
170 def _supports_activation(activation: MoEActivation) -> bool:
171 return activation in (
172 MoEActivation.SILU,
173 MoEActivation.SWIGLUOAI,
174 MoEActivation.GELU,
175 MoEActivation.GELU_TANH,
176 )
177
178 @staticmethod
179 def _supports_quant_scheme(
180 weight_key: QuantKey | None,
181 activation_key: QuantKey | None,
182 ) -> bool:
183 return (weight_key, activation_key) == (None, None)
184
185 @staticmethod
186 def _supports_parallel_config(
187 moe_parallel_config: FusedMoEParallelConfig,
188 ) -> bool:
189 return True
190
191 @staticmethod
192 def _supports_routing_method(
193 routing_method: RoutingMethodType,
194 weight_key: QuantKey | None,
195 activation_key: QuantKey | None,
196 ) -> bool:
197 # Routing runs in the CPURouter, which covers every routing method a
198 # layer can be configured with, including custom routing functions.
199 return True
200
201 @staticmethod
202 def _supports_router_logits_dtype(
203 router_logits_dtype: torch.dtype | None,
204 routing_method: RoutingMethodType,
205 ) -> bool:
206 return True
207
208 def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
209 self._use_zentorch = is_zentorch_moe_supported(layer)
210 if self._use_zentorch:
211 return
212 self._pad_moe_intermediate(layer)
213 replace_parameter(
214 layer, "w13_weight", cpu_prepack_moe_weight(layer.w13_weight, self.isa)
215 )
216 replace_parameter(
217 layer, "w2_weight", cpu_prepack_moe_weight(layer.w2_weight, self.isa)
218 )
219
220 def _pad_moe_intermediate(self, layer: torch.nn.Module) -> None:
221 """Zero-pad the per-partition MoE intermediate dim of both weights and
222 the expert bias, see `_padded_intermediate_size`."""
223 intermediate_size = self.moe_config.intermediate_size_per_partition
224 padded_size = self._padded_intermediate_size(self.moe_config)
225 if padded_size == intermediate_size:
226 return
227
228 num_experts, _, hidden_size = layer.w13_weight.shape
229
230 new_w13 = layer.w13_weight.new_zeros(num_experts, 2 * padded_size, hidden_size)
231 new_w13[:, :intermediate_size] = layer.w13_weight[:, :intermediate_size]
232 new_w13[:, padded_size : padded_size + intermediate_size] = layer.w13_weight[
233 :, intermediate_size:
234 ]
235 replace_parameter(layer, "w13_weight", new_w13)
236
237 new_w2 = layer.w2_weight.new_zeros(num_experts, hidden_size, padded_size)
238 new_w2[:, :, :intermediate_size] = layer.w2_weight
239 replace_parameter(layer, "w2_weight", new_w2)
240
241 if hasattr(layer, "w13_bias"):
242 new_bias = layer.w13_bias.new_zeros(num_experts, 2 * padded_size)
243 new_bias[:, :intermediate_size] = layer.w13_bias[:, :intermediate_size]
244 new_bias[:, padded_size : padded_size + intermediate_size] = layer.w13_bias[
245 :, intermediate_size:
246 ]
247 # Assign through .data rather than replacing the Parameter: the
248 # quant config is built before this runs and holds a reference to
249 # this very object, which is what feeds self.w1_bias in apply().
250 layer.w13_bias.data = new_bias
251
252 def workspace_shapes(
253 self,
254 M: int,
255 N: int,
256 K: int,
257 topk: int,
258 global_num_experts: int,
259 local_num_experts: int,
260 expert_tokens_meta: mk.ExpertTokensMetadata | None,
261 activation: MoEActivation,
262 ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
263 # cpu_fused_moe manages its own scratch space.
264 return (0,), (0,), (M, K)
265
266 def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
267 return TopKWeightAndReduceNoOP()
268
269 def apply(
270 self,
271 output: torch.Tensor,
272 hidden_states: torch.Tensor,
273 w1: torch.Tensor,
274 w2: torch.Tensor,
275 topk_weights: torch.Tensor,
276 topk_ids: torch.Tensor,
277 activation: MoEActivation,
278 global_num_experts: int,
279 expert_map: torch.Tensor | None,
280 a1q_scale: torch.Tensor | None,
281 a2_scale: torch.Tensor | None,
282 workspace13: torch.Tensor,
283 workspace2: torch.Tensor,
284 expert_tokens_meta: mk.ExpertTokensMetadata | None,
285 apply_router_weight_on_input: bool,
286 ) -> None:
287 # apply_router_weight_on_input (topk=1 only) is already applied to
288 # hidden_states by MoEPrepareAndFinalizeNoDPEPModular.prepare().
289 if self._use_zentorch:
290 torch.ops.zentorch.zentorch_fused_moe(
291 output,
292 hidden_states,
293 w1,
294 w2,
295 self.w1_bias,
296 self.w2_bias,
297 topk_weights,
298 topk_ids,
299 apply_router_weight_on_input,
300 str(activation.value).lower(),
301 )
302 return
303 cpu_fused_moe(
304 output,
305 hidden_states,
306 w1,
307 w2,
308 self.w1_bias,
309 self.w2_bias,
310 topk_weights,
311 topk_ids,
312 activation.value,
313 self.isa,
314 apply_router_weight_on_input,
315 )
316
317
318 class X86CPUUnquantizedExperts(CPUUnquantizedExperts):
319 """x86 AMX grouped-gemm unquantized MoE experts."""
320
321 isa = "amx"
322 output_alignment = 32
323 reduction_alignment = 32
324
325 @staticmethod
326 def _supports_current_device() -> bool:
327 return (
328 current_platform.is_cpu()
329 and current_platform.get_cpu_architecture() == CpuArchEnum.X86
330 and torch.cpu._is_amx_tile_supported()
331 )
332
333 @staticmethod
334 def is_supported_config(
335 cls: type[mk.FusedMoEExperts],
336 moe_config: FusedMoEConfig,
337 weight_key: QuantKey | None,
338 activation_key: QuantKey | None,
339 activation_format: mk.FusedMoEActivationFormat,
340 ) -> tuple[bool, str | None]:
341 supported, reason = mk.FusedMoEExperts.is_supported_config(
342 cls, moe_config, weight_key, activation_key, activation_format
343 )
344 if not supported:
345 return supported, reason
346 if moe_config.in_dtype != torch.bfloat16:
347 return False, "kernel requires bfloat16 activations"
348 cpu_cls = cast(type[CPUUnquantizedExperts], cls)
349 return cpu_cls._supports_grouped_gemm(moe_config)
350
351
352 class ArmCPUUnquantizedExperts(CPUUnquantizedExperts):
353 """Arm NEON grouped-gemm unquantized MoE experts."""
354
355 isa = "neon"
356 output_alignment = 32
357 reduction_alignment = 4
358
359 @staticmethod
360 def _supports_current_device() -> bool:
361 return (
362 current_platform.is_cpu()
363 and current_platform.get_cpu_architecture() == CpuArchEnum.ARM
364 and sys.platform != "darwin"
365 )
366
367 @staticmethod
368 def is_supported_config(
369 cls: type[mk.FusedMoEExperts],
370 moe_config: FusedMoEConfig,
371 weight_key: QuantKey | None,
372 activation_key: QuantKey | None,
373 activation_format: mk.FusedMoEActivationFormat,
374 ) -> tuple[bool, str | None]:
375 supported, reason = mk.FusedMoEExperts.is_supported_config(
376 cls, moe_config, weight_key, activation_key, activation_format
377 )
378 if not supported:
379 return supported, reason
380 if moe_config.in_dtype != torch.bfloat16:
381 return False, "kernel requires bfloat16 activations"
382 cpu_cls = cast(type[CPUUnquantizedExperts], cls)
383 return cpu_cls._supports_grouped_gemm(moe_config)
384
385
386 class PowerCPUUnquantizedExperts(CPUUnquantizedExperts):
387 """PowerPC VSX grouped-gemm unquantized MoE experts."""
388
389 isa = "vsx"
390 output_alignment = 16
391 reduction_alignment = 2
392
393 @staticmethod
394 def _supports_current_device() -> bool:
395 return (
396 current_platform.is_cpu()
397 and current_platform.get_cpu_architecture() == CpuArchEnum.POWERPC
398 )
399
400 @staticmethod
401 def is_supported_config(
402 cls: type[mk.FusedMoEExperts],
403 moe_config: FusedMoEConfig,
404 weight_key: QuantKey | None,
405 activation_key: QuantKey | None,
406 activation_format: mk.FusedMoEActivationFormat,
407 ) -> tuple[bool, str | None]:
408 supported, reason = mk.FusedMoEExperts.is_supported_config(
409 cls, moe_config, weight_key, activation_key, activation_format
410 )
411 if not supported:
412 return supported, reason
413 if moe_config.in_dtype != torch.bfloat16:
414 return False, "kernel requires bfloat16 activations"
415 cpu_cls = cast(type[CPUUnquantizedExperts], cls)
416 return cpu_cls._supports_grouped_gemm(moe_config)
417
418
419 # ===========================================================================
420 # FP8 W8A16 MoE
421 # ===========================================================================
422
423
424 def prepare_fp8_moe_layer_for_cpu(
425 w13: torch.Tensor,
426 w2: torch.Tensor,
427 ) -> tuple[torch.Tensor, torch.Tensor]:
428 """VNNI-prepack FP8 MoE weights for CPU kernel."""
429 packed_w13 = torch.ops._C.convert_weight_packed(w13)
430 packed_w2 = torch.ops._C.convert_weight_packed(w2)
431 return packed_w13, packed_w2
432
433
434 class CPUExpertsFp8(mk.FusedMoEExpertsModular):
435 """CPU FP8 W8A16 block-quantized modular MoE experts."""
436
437 def __init__(
438 self,
439 moe_config: FusedMoEConfig,
440 quant_config: FusedMoEQuantConfig,
441 ):
442 super().__init__(
443 moe_config,
444 quant_config,
445 )
446
447 @property
448 def expects_unquantized_inputs(self) -> bool:
449 return True
450
451 @staticmethod
452 def activation_format() -> mk.FusedMoEActivationFormat:
453 return mk.FusedMoEActivationFormat.Standard
454
455 @staticmethod
456 def _supports_current_device() -> bool:
457 return (
458 current_platform.is_cpu()
459 and current_platform.get_cpu_architecture() == CpuArchEnum.X86
460 and torch.cpu._is_amx_tile_supported()
461 )
462
463 @staticmethod
464 def _supports_no_act_and_mul() -> bool:
465 return False
466
467 @staticmethod
468 def _supports_activation(activation: MoEActivation) -> bool:
469 return activation == MoEActivation.SILU
470
471 @staticmethod
472 def _supports_parallel_config(
473 moe_parallel_config: FusedMoEParallelConfig,
474 ) -> bool:
475 return True
476
477 @staticmethod
478 def _supports_quant_scheme(
479 weight_key: QuantKey | None,
480 activation_key: QuantKey | None,
481 ) -> bool:
482 SUPPORTED_W_A = [
483 (kFp8Static128BlockSym, kFp8Dynamic128Sym),
484 ]
485 return (weight_key, activation_key) in SUPPORTED_W_A
486
487 @staticmethod
488 def _supports_routing_method(
489 routing_method: RoutingMethodType,
490 weight_key: QuantKey | None,
491 activation_key: QuantKey | None,
492 ) -> bool:
493 return routing_method in [
494 RoutingMethodType.Default,
495 RoutingMethodType.Renormalize,
496 RoutingMethodType.RenormalizeNaive,
497 RoutingMethodType.DeepSeekV3,
498 RoutingMethodType.DeepseekV4,
499 RoutingMethodType.DeepSeekV3,
500 ]
501
502 @staticmethod
503 def _supports_router_logits_dtype(
504 router_logits_dtype: torch.dtype | None,
505 routing_method: RoutingMethodType,
506 ) -> bool:
507 return True
508
509 def workspace_shapes(
510 self,
511 M: int,
512 N: int,
513 K: int,
514 topk: int,
515 global_num_experts: int,
516 local_num_experts: int,
517 expert_tokens_meta: mk.ExpertTokensMetadata | None,
518 activation: MoEActivation,
519 ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
520 # fused_experts_cpu manages its own scratch space.
521 return (0,), (0,), (M, K)
522
523 def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
524 return TopKWeightAndReduceNoOP()
525
526 def apply(
527 self,
528 output: torch.Tensor,
529 hidden_states: torch.Tensor,
530 w1: torch.Tensor,
531 w2: torch.Tensor,
532 topk_weights: torch.Tensor,
533 topk_ids: torch.Tensor,
534 activation: MoEActivation,
535 global_num_experts: int,
536 expert_map: torch.Tensor | None,
537 a1q_scale: torch.Tensor | None,
538 a2_scale: torch.Tensor | None,
539 workspace13: torch.Tensor,
540 workspace2: torch.Tensor,
541 expert_tokens_meta: mk.ExpertTokensMetadata | None,
542 apply_router_weight_on_input: bool,
543 ) -> None:
544 if apply_router_weight_on_input:
545 # fused_experts_cpu always applies topk_weights internally on
546 # combine; MoEPrepareAndFinalizeNoDPEPModular.prepare() would
547 # also pre-apply it to hidden_states, double-weighting the
548 # output. Not needed by any CPU FP8 model today.
549 raise NotImplementedError(
550 "CPUExpertsFp8 does not support apply_router_weight_on_input=True."
551 )
552 block_shape = (
553 list(self.quant_config.block_shape)
554 if self.quant_config.block_shape
555 else (
556 [self.quant_config._w1.shape.row, self.quant_config._w1.shape.col]
557 if self.quant_config._w1.shape is not None
558 else None
559 )
560 )
561
562 fused_experts_cpu(
563 output,
564 hidden_states,
565 w1,
566 w2,
567 topk_weights,
568 topk_ids,
569 CPUQuantMethod.FP8_W8A16, # moe_comp_method
570 self.w1_scale, # w1_scale
571 self.w2_scale, # w2_scale
572 None, # w1_zero
573 None, # w2_zero
574 block_shape, # block_size
575 None, # w1_bias
576 None, # w2_bias
577 None, # alpha
578 None, # limit
579 True, # is_vnni
580 )
581
582
583 # ===========================================================================
584 # MXFP4 W4A16 MoE
585 # ===========================================================================
586
587
588 def prepare_mxfp4_moe_layer_for_cpu(
589 w13: torch.Tensor,
590 w2: torch.Tensor,
591 w13_scale: torch.Tensor,
592 w2_scale: torch.Tensor,
593 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
594 """VNNI-prepack MXFP4 MoE weights and repack scales for CPU AMX kernel."""
595 packed_w13 = torch.ops._C.convert_weight_packed(w13)
596 packed_w2 = torch.ops._C.convert_weight_packed(w2)
597 packed_w13_scale = torch.ops._C.convert_scale_packed(w13_scale)
598 packed_w2_scale = torch.ops._C.convert_scale_packed(w2_scale)
599 return packed_w13, packed_w2, packed_w13_scale, packed_w2_scale
600
601
602 class CPUExpertsMxfp4(mk.FusedMoEExpertsModular):
603 """CPU MXFP4 W4A16 modular MoE experts."""
604
605 def __init__(
606 self,
607 moe_config: FusedMoEConfig,
608 quant_config: FusedMoEQuantConfig,
609 ):
610 super().__init__(
611 moe_config,
612 quant_config,
613 )
614
615 @property
616 def expects_unquantized_inputs(self) -> bool:
617 return True
618
619 @staticmethod
620 def activation_format() -> mk.FusedMoEActivationFormat:
621 return mk.FusedMoEActivationFormat.Standard
622
623 @staticmethod
624 def _supports_current_device() -> bool:
625 return (
626 current_platform.is_cpu()
627 and current_platform.get_cpu_architecture() == CpuArchEnum.X86
628 and torch.cpu._is_amx_tile_supported()
629 )
630
631 @staticmethod
632 def _supports_no_act_and_mul() -> bool:
633 return False
634
635 @staticmethod
636 def _supports_activation(activation: MoEActivation) -> bool:
637 return activation in (MoEActivation.SILU, MoEActivation.SWIGLUOAI)
638
639 @staticmethod
640 def _supports_parallel_config(
641 moe_parallel_config: FusedMoEParallelConfig,
642 ) -> bool:
643 return True
644
645 @staticmethod
646 def _supports_quant_scheme(
647 weight_key: QuantKey | None,
648 activation_key: QuantKey | None,
649 ) -> bool:
650 SUPPORTED_W_A = [
651 (kMxfp4Static, None),
652 ]
653 return (weight_key, activation_key) in SUPPORTED_W_A
654
655 @staticmethod
656 def _supports_routing_method(
657 routing_method: RoutingMethodType,
658 weight_key: QuantKey | None,
659 activation_key: QuantKey | None,
660 ) -> bool:
661 return routing_method in [
662 RoutingMethodType.Default,
663 RoutingMethodType.Renormalize,
664 RoutingMethodType.RenormalizeNaive,
665 RoutingMethodType.DeepseekV4,
666 ]
667
668 @staticmethod
669 def _supports_router_logits_dtype(
670 router_logits_dtype: torch.dtype | None,
671 routing_method: RoutingMethodType,
672 ) -> bool:
673 return True
674
675 def workspace_shapes(
676 self,
677 M: int,
678 N: int,
679 K: int,
680 topk: int,
681 global_num_experts: int,
682 local_num_experts: int,
683 expert_tokens_meta: mk.ExpertTokensMetadata | None,
684 activation: MoEActivation,
685 ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
686 # fused_experts_cpu manages its own scratch space.
687 return (0,), (0,), (M, K)
688
689 def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
690 return TopKWeightAndReduceNoOP()
691
692 def apply(
693 self,
694 output: torch.Tensor,
695 hidden_states: torch.Tensor,
696 w1: torch.Tensor,
697 w2: torch.Tensor,
698 topk_weights: torch.Tensor,
699 topk_ids: torch.Tensor,
700 activation: MoEActivation,
701 global_num_experts: int,
702 expert_map: torch.Tensor | None,
703 a1q_scale: torch.Tensor | None,
704 a2_scale: torch.Tensor | None,
705 workspace13: torch.Tensor,
706 workspace2: torch.Tensor,
707 expert_tokens_meta: mk.ExpertTokensMetadata | None,
708 apply_router_weight_on_input: bool,
709 ) -> None:
710 if apply_router_weight_on_input:
711 # fused_experts_cpu always applies topk_weights internally on
712 # combine; MoEPrepareAndFinalizeNoDPEPModular.prepare() would
713 # also pre-apply it to hidden_states, double-weighting the
714 # output. Not needed by any CPU MXFP4 model today.
715 raise NotImplementedError(
716 "CPUExpertsMxfp4 does not support apply_router_weight_on_input=True."
717 )
718 # Get bias and swiglu params from quant config
719 w1_bias = self.quant_config.w1_bias
720 w2_bias = self.quant_config.w2_bias
721 alpha = getattr(self.quant_config, "gemm1_alpha", None)
722 limit = getattr(self.quant_config, "gemm1_clamp_limit", None)
723
724 fused_experts_cpu(
725 output,
726 hidden_states,
727 w1,
728 w2,
729 topk_weights,
730 topk_ids,
731 CPUQuantMethod.MXFP4, # moe_comp_method
732 self.w1_scale, # w1_scale
733 self.w2_scale, # w2_scale
734 None, # w1_zero
735 None, # w2_zero
736 None, # block_size
737 w1_bias,
738 w2_bias,
739 alpha,
740 limit,
741 True, # is_vnni
742 )
743
744
745 # ===========================================================================
746 # INT4 W4A16 MoE
747 # ===========================================================================
748
749
750 def prepare_int4_moe_layer_for_cpu(
751 w13_packed: torch.Tensor,
752 w2_packed: torch.Tensor,
753 w13_scale: torch.Tensor,
754 w2_scale: torch.Tensor,
755 quant_algo: CPUQuantAlgo = CPUQuantAlgo.GPTQ,
756 w13_zeros: torch.Tensor | None = None,
757 w2_zeros: torch.Tensor | None = None,
758 ) -> tuple[
759 torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor
760 ]:
761 """Repack INT4 MoE weights via convert_weight_packed_scale_zp for CPU.
762
763 Args:
764 w13_packed: [E, K//8, 2*I] int32 (packed int4)
765 w2_packed: [E, I//8, K] int32 (packed int4)
766 w13_scale: [E, num_groups, 2*I] float16/bf16
767 w2_scale: [E, num_groups, K] float16/bf16
768 quant_algo: CPUQuantAlgo.GPTQ or CPUQuantAlgo.AWQ
769 w13_zeros: optional [E, num_groups, N//8] int32 packed zeros.
770 If None, synthetic zeros are created for symmetric quant.
771 w2_zeros: optional [E, num_groups, N//8] int32 packed zeros.
772 If None, synthetic zeros are created for symmetric quant.
773
774 Returns:
775 (blocked_w13, blocked_w2, blocked_s13, blocked_s2, blocked_z13, blocked_z2)
776
777 """
778 E = w13_packed.size(0)
779
780 # No qzeros are available in compressed-tensors symmetric checkpoints.
781 # The GPTQ unpack kernel (unpack_4bit_to_32bit_signed) adds +1 to stored zeros,
782 # so we store 7 per nibble: 0x77777777 → +1 → 8.
783 if w13_zeros is None:
784 num_groups_w13 = w13_scale.size(1)
785 N_w13 = w13_scale.size(2) # 2*I
786 _zp = 0x77777777
787 w13_zeros = torch.full(
788 (E, num_groups_w13, N_w13 // 8),
789 _zp,
790 dtype=torch.int32,
791 )
792
793 if w2_zeros is None:
794 num_groups_w2 = w2_scale.size(1)
795 N_w2 = w2_scale.size(2) # K
796 _zp = 0x77777777
797 w2_zeros = torch.full(
798 (E, num_groups_w2, N_w2 // 8),
799 _zp,
800 dtype=torch.int32,
801 )
802
803 blocked_w13, blocked_z13, blocked_s13 = convert_weight_packed_scale_zp(
804 w13_packed, w13_zeros, w13_scale, quant_algo
805 )
806 blocked_w2, blocked_z2, blocked_s2 = convert_weight_packed_scale_zp(
807 w2_packed, w2_zeros, w2_scale, quant_algo
808 )
809 return (blocked_w13, blocked_w2, blocked_s13, blocked_s2, blocked_z13, blocked_z2)
810
811
812 class CPUExpertsInt4(mk.FusedMoEExpertsModular):
813 """CPU INT4 W4A16 group-quantized modular MoE experts.
814
815 Weights are int4 (packed), activations are bf16/fp16.
816 Internally uses int8 compute via fused_experts_cpu with INT4_W4A8.
817 """
818
819 def __init__(
820 self,
821 moe_config: FusedMoEConfig,
822 quant_config: FusedMoEQuantConfig,
823 max_num_tokens: int | None = None,
824 num_dispatchers: int | None = None,
825 ):
826 super().__init__(
827 moe_config,
828 quant_config,
829 )
830
831 @property
832 def expects_unquantized_inputs(self) -> bool:
833 return True
834
835 @staticmethod
836 def activation_format() -> mk.FusedMoEActivationFormat:
837 return mk.FusedMoEActivationFormat.Standard
838
839 @staticmethod
840 def _supports_current_device() -> bool:
841 return (
842 current_platform.is_cpu()
843 and current_platform.get_cpu_architecture() == CpuArchEnum.X86
844 and torch.cpu._is_amx_tile_supported()
845 )
846
847 @staticmethod
848 def _supports_no_act_and_mul() -> bool:
849 return False
850
851 @staticmethod
852 def _supports_activation(activation: MoEActivation) -> bool:
853 return activation == MoEActivation.SILU
854
855 @staticmethod
856 def _supports_parallel_config(
857 moe_parallel_config: FusedMoEParallelConfig,
858 ) -> bool:
859 return True
860
861 @staticmethod
862 def _supports_quant_scheme(
863 weight_key: QuantKey | None,
864 activation_key: QuantKey | None,
865 ) -> bool:
866 SUPPORTED_W_A = [
867 (kInt4Static, None),
868 ]
869 return (weight_key, activation_key) in SUPPORTED_W_A
870
871 @staticmethod
872 def _supports_routing_method(
873 routing_method: RoutingMethodType,
874 weight_key: QuantKey | None,
875 activation_key: QuantKey | None,
876 ) -> bool:
877 return routing_method in [
878 RoutingMethodType.Default,
879 RoutingMethodType.Renormalize,
880 RoutingMethodType.RenormalizeNaive,
881 ]
882
883 @staticmethod
884 def _supports_router_logits_dtype(
885 router_logits_dtype: torch.dtype | None,
886 routing_method: RoutingMethodType,
887 ) -> bool:
888 return True
889
890 def supports_expert_map(self) -> bool:
891 return False
892
893 def moe_problem_size(
894 self,
895 a1: torch.Tensor,
896 w1: torch.Tensor,
897 w2: torch.Tensor,
898 topk_ids: torch.Tensor,
899 ) -> tuple[int, int, int, int, int]:
900 # convert_weight_packed_scale_zp blocks w1/w2 into AMX tiles
901 # (E, Nc, Kc, buffer_bytes), not the (E, N, K) layout the base
902 # implementation assumes -- N/K aren't recoverable from that shape,
903 # so read them from moe_config instead.
904 E = w1.shape[0]
905 K = a1.size(-1)
906 N = (
907 self.moe_config.intermediate_size_per_partition
908 * self.moe_config.w13_num_shards
909 )
910 M = a1.size(0) if a1.dim() == 2 else a1.size(1)
911 topk = topk_ids.size(1)
912 return E, M, N, K, topk
913
914 def workspace_shapes(
915 self,
916 M: int,
917 N: int,
918 K: int,
919 topk: int,
920 global_num_experts: int,
921 local_num_experts: int,
922 expert_tokens_meta: mk.ExpertTokensMetadata | None,
923 activation: MoEActivation,
924 ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
925 # fused_experts_cpu manages its own scratch space.
926 return (0,), (0,), (M, K)
927
928 def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
929 return TopKWeightAndReduceNoOP()
930
931 def apply(
932 self,
933 output: torch.Tensor,
934 hidden_states: torch.Tensor,
935 w1: torch.Tensor,
936 w2: torch.Tensor,
937 topk_weights: torch.Tensor,
938 topk_ids: torch.Tensor,
939 activation: MoEActivation,
940 global_num_experts: int,
941 expert_map: torch.Tensor | None,
942 a1q_scale: torch.Tensor | None,
943 a2_scale: torch.Tensor | None,
944 workspace13: torch.Tensor,
945 workspace2: torch.Tensor,
946 expert_tokens_meta: mk.ExpertTokensMetadata | None,
947 apply_router_weight_on_input: bool,
948 ) -> None:
949 if apply_router_weight_on_input:
950 raise NotImplementedError(
951 "CPUExpertsInt4 (W4A16) does not support "
952 "apply_router_weight_on_input=True. "
953 )
954
955 fused_experts_cpu(
956 output,
957 hidden_states,
958 w1,
959 w2,
960 topk_weights,
961 topk_ids,
962 CPUQuantMethod.INT4_W4A8,
963 self.w1_scale,
964 self.w2_scale,
965 self.w1_zp,
966 self.w2_zp,
967 None, # block_size
968 None, # w1_bias
969 None, # w2_bias
970 None, # alpha
971 None, # limit
972 True, # is_vnni
973 )
974
975
976 # ===========================================================================
977 # INT8 W8A8 MoE
978 # ===========================================================================
979
980
981 def prepare_int8_moe_layer_for_cpu(
982 w13: torch.Tensor,
983 w2: torch.Tensor,
984 ) -> tuple[torch.Tensor, torch.Tensor]:
985 """Prepack INT8 MoE weights for the current CPU architecture."""
986 # SMMLA packing for AArch64
987 if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
988 return (
989 cpu_prepack_moe_weight_int8(w13, "neon"),
990 cpu_prepack_moe_weight_int8(w2, "neon"),
991 )
992 # VSX packing for POWER
993 if current_platform.get_cpu_architecture() == CpuArchEnum.POWERPC:
994 return (
995 cpu_prepack_moe_weight_int8(w13, "vsx"),
996 cpu_prepack_moe_weight_int8(w2, "vsx"),
997 )
998 # VNNI packing for x86
999 packed_w13 = torch.ops._C.convert_weight_packed(w13)
1000 packed_w2 = torch.ops._C.convert_weight_packed(w2)
1001 return packed_w13, packed_w2
1002
1003
1004 class CPUExpertsInt8(mk.FusedMoEExpertsModular):
1005 """CPU INT8 W8A8 per-channel weight / dynamic per-token activation
1006 modular MoE experts."""
1007
1008 def __init__(
1009 self,
1010 moe_config: FusedMoEConfig,
1011 quant_config: FusedMoEQuantConfig,
1012 ):
1013 super().__init__(
1014 moe_config,
1015 quant_config,
1016 )
1017
1018 @property
1019 def expects_unquantized_inputs(self) -> bool:
1020 return True
1021
1022 @staticmethod
1023 def activation_format() -> mk.FusedMoEActivationFormat:
1024 return mk.FusedMoEActivationFormat.Standard
1025
1026 @staticmethod
1027 def _supports_current_device() -> bool:
1028 return (
1029 current_platform.is_cpu()
1030 and current_platform.get_cpu_architecture() == CpuArchEnum.X86
1031 and torch.cpu._is_amx_tile_supported()
1032 )
1033
1034 @staticmethod
1035 def is_supported_config(
1036 cls: type[mk.FusedMoEExperts],
1037 moe_config: FusedMoEConfig,
1038 weight_key: QuantKey | None,
1039 activation_key: QuantKey | None,
1040 activation_format: mk.FusedMoEActivationFormat,
1041 ) -> tuple[bool, str | None]:
1042 supported, reason = mk.FusedMoEExperts.is_supported_config(
1043 cls,
1044 moe_config,
1045 weight_key,
1046 activation_key,
1047 activation_format,
1048 )
1049 if not supported:
1050 return supported, reason
1051 # convert_weight_packed (shared VNNI prepack) requires the w13
1052 # OC/IC and w2 OC/IC to be multiples of TILE_N=16/TILE_K=32; the
1053 # w1 gate-up kernel additionally requires the intermediate size
1054 # itself (not 2x) to be a multiple of 32 (moe_int8.cpp), which
1055 # dominates. Net effect: both dims must be multiples of 32.
1056 if moe_config.hidden_dim % 32 != 0:
1057 return False, "kernel requires hidden dim divisible by 32"
1058 if moe_config.intermediate_size_per_partition % 32 != 0:
1059 return False, "kernel requires intermediate dim divisible by 32"
1060 return True, None
1061
1062 @staticmethod
1063 def _supports_no_act_and_mul() -> bool:
1064 return False
1065
1066 @staticmethod
1067 def _supports_activation(activation: MoEActivation) -> bool:
1068 return activation == MoEActivation.SILU
1069
1070 @staticmethod
1071 def _supports_parallel_config(
1072 moe_parallel_config: FusedMoEParallelConfig,
1073 ) -> bool:
1074 return True
1075
1076 @staticmethod
1077 def _supports_quant_scheme(
1078 weight_key: QuantKey | None,
1079 activation_key: QuantKey | None,
1080 ) -> bool:
1081 SUPPORTED_W_A = [
1082 (kInt8StaticChannelSym, kInt8DynamicTokenSym),
1083 ]
1084 return (weight_key, activation_key) in SUPPORTED_W_A
1085
1086 @staticmethod
1087 def _supports_routing_method(
1088 routing_method: RoutingMethodType,
1089 weight_key: QuantKey | None,
1090 activation_key: QuantKey | None,
1091 ) -> bool:
1092 return routing_method in [
1093 RoutingMethodType.Default,
1094 RoutingMethodType.Renormalize,
1095 RoutingMethodType.RenormalizeNaive,
1096 ]
1097
1098 @staticmethod
1099 def _supports_router_logits_dtype(
1100 router_logits_dtype: torch.dtype | None,
1101 routing_method: RoutingMethodType,
1102 ) -> bool:
1103 return True
1104
1105 def supports_expert_map(self) -> bool:
1106 return False
1107
1108 def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
1109 """VNNI-prepack INT8 MoE weights for CPU kernel."""
1110 w13 = torch.ops._C.convert_weight_packed(layer.w13_weight)
1111 w2 = torch.ops._C.convert_weight_packed(layer.w2_weight)
1112 replace_parameter(layer, "w13_weight", w13)
1113 replace_parameter(layer, "w2_weight", w2)
1114
1115 def workspace_shapes(
1116 self,
1117 M: int,
1118 N: int,
1119 K: int,
1120 topk: int,
1121 global_num_experts: int,
1122 local_num_experts: int,
1123 expert_tokens_meta: mk.ExpertTokensMetadata | None,
1124 activation: MoEActivation,
1125 ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
1126 # fused_experts_cpu manages its own scratch space.
1127 return (0,), (0,), (M, K)
1128
1129 def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
1130 return TopKWeightAndReduceNoOP()
1131
1132 def apply(
1133 self,
1134 output: torch.Tensor,
1135 hidden_states: torch.Tensor,
1136 w1: torch.Tensor,
1137 w2: torch.Tensor,
1138 topk_weights: torch.Tensor,
1139 topk_ids: torch.Tensor,
1140 activation: MoEActivation,
1141 global_num_experts: int,
1142 expert_map: torch.Tensor | None,
1143 a1q_scale: torch.Tensor | None,
1144 a2_scale: torch.Tensor | None,
1145 workspace13: torch.Tensor,
1146 workspace2: torch.Tensor,
1147 expert_tokens_meta: mk.ExpertTokensMetadata | None,
1148 apply_router_weight_on_input: bool,
1149 ) -> None:
1150 if apply_router_weight_on_input:
1151 # fused_experts_cpu always applies topk_weights internally on
1152 # combine; MoEPrepareAndFinalizeNoDPEPModular.prepare() would
1153 # also pre-apply it to hidden_states, double-weighting the
1154 # output. Not needed by any CPU INT8 W8A8 model today.
1155 raise NotImplementedError(
1156 "CPUExpertsInt8 does not support apply_router_weight_on_input=True."
1157 )
1158 fused_experts_cpu(
1159 output,
1160 hidden_states,
1161 w1,
1162 w2,
1163 topk_weights,
1164 topk_ids,
1165 CPUQuantMethod.INT8_W8A8,
1166 self.w1_scale,
1167 self.w2_scale,
1168 None, # w1_zero
1169 None, # w2_zero
1170 None, # block_size (per-channel, no block)
1171 None, # w1_bias
1172 None, # w2_bias
1173 None, # alpha
1174 None, # limit
1175 True, # is_vnni
1176 )
1177
1178
1179 class ArmCPUExpertsInt8(mk.FusedMoEExpertsModular):
1180 """Arm INT8 MoE with per-token activation and channelwise weight quantization."""
1181
1182 @property
1183 def expects_unquantized_inputs(self) -> bool:
1184 return True
1185
1186 @staticmethod
1187 def activation_format() -> mk.FusedMoEActivationFormat:
1188 return mk.FusedMoEActivationFormat.Standard
1189
1190 @staticmethod
1191 def is_supported_config(
1192 cls: type[mk.FusedMoEExperts],
1193 moe_config: FusedMoEConfig,
1194 weight_key: QuantKey | None,
1195 activation_key: QuantKey | None,
1196 activation_format: mk.FusedMoEActivationFormat,
1197 ) -> tuple[bool, str | None]:
1198 supported, reason = mk.FusedMoEExperts.is_supported_config(
1199 cls,
1200 moe_config,
1201 weight_key,
1202 activation_key,
1203 activation_format,
1204 )
1205 if not supported:
1206 return supported, reason
1207 if moe_config.in_dtype not in (
1208 torch.float32,
1209 torch.float16,
1210 torch.bfloat16,
1211 ):
1212 return False, "kernel requires float32, float16, or bfloat16 activations"
1213 if moe_config.hidden_dim % 32 != 0:
1214 return False, "kernel requires hidden dim divisible by 32"
1215 if moe_config.intermediate_size_per_partition % 32 != 0:
1216 return False, "kernel requires intermediate dim divisible by 32"
1217 return True, None
1218
1219 @staticmethod
1220 def _supports_current_device() -> bool:
1221 return (
1222 current_platform.is_cpu()
1223 and current_platform.get_cpu_architecture() == CpuArchEnum.ARM
1224 and hasattr(torch.ops._C, "cpu_fused_moe_int8")
1225 )
1226
1227 @staticmethod
1228 def _supports_no_act_and_mul() -> bool:
1229 return False
1230
1231 @staticmethod
1232 def _supports_activation(activation: MoEActivation) -> bool:
1233 return activation in (
1234 MoEActivation.SILU,
1235 MoEActivation.SWIGLUOAI,
1236 MoEActivation.GELU,
1237 MoEActivation.GELU_TANH,
1238 )
1239
1240 @staticmethod
1241 def _supports_parallel_config(
1242 moe_parallel_config: FusedMoEParallelConfig,
1243 ) -> bool:
1244 return not moe_parallel_config.use_ep
1245
1246 @staticmethod
1247 def _supports_quant_scheme(
1248 weight_key: QuantKey | None,
1249 activation_key: QuantKey | None,
1250 ) -> bool:
1251 return (weight_key, activation_key) == (
1252 kInt8StaticChannelSym,
1253 kInt8DynamicTokenSym,
1254 )
1255
1256 @staticmethod
1257 def _supports_routing_method(
1258 routing_method: RoutingMethodType,
1259 weight_key: QuantKey | None,
1260 activation_key: QuantKey | None,
1261 ) -> bool:
1262 return routing_method in [
1263 RoutingMethodType.Default,
1264 RoutingMethodType.Renormalize,
1265 RoutingMethodType.RenormalizeNaive,
1266 ]
1267
1268 @staticmethod
1269 def _supports_router_logits_dtype(
1270 router_logits_dtype: torch.dtype | None,
1271 routing_method: RoutingMethodType,
1272 ) -> bool:
1273 return True
1274
1275 def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
1276 w13 = cpu_prepack_moe_weight_int8(layer.w13_weight, "neon")
1277 w2 = cpu_prepack_moe_weight_int8(layer.w2_weight, "neon")
1278 replace_parameter(layer, "w13_weight", w13)
1279 replace_parameter(layer, "w2_weight", w2)
1280
1281 def workspace_shapes(
1282 self,
1283 M: int,
1284 N: int,
1285 K: int,
1286 topk: int,
1287 global_num_experts: int,
1288 local_num_experts: int,
1289 expert_tokens_meta: mk.ExpertTokensMetadata | None,
1290 activation: MoEActivation,
1291 ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
1292 # cpu_fused_moe_int8 manages its own scratch space.
1293 return (0,), (0,), (M, K)
1294
1295 def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
1296 return TopKWeightAndReduceNoOP()
1297
1298 def apply(
1299 self,
1300 output: torch.Tensor,
1301 hidden_states: torch.Tensor,
1302 w1: torch.Tensor,
1303 w2: torch.Tensor,
1304 topk_weights: torch.Tensor,
1305 topk_ids: torch.Tensor,
1306 activation: MoEActivation,
1307 global_num_experts: int,
1308 expert_map: torch.Tensor | None,
1309 a1q_scale: torch.Tensor | None,
1310 a2_scale: torch.Tensor | None,
1311 workspace13: torch.Tensor,
1312 workspace2: torch.Tensor,
1313 expert_tokens_meta: mk.ExpertTokensMetadata | None,
1314 apply_router_weight_on_input: bool,
1315 ) -> None:
1316 # apply_router_weight_on_input (topk=1 only) is already applied to
1317 # hidden_states by MoEPrepareAndFinalizeNoDPEPModular.prepare().
1318 assert self.w1_scale is not None
1319 assert self.w2_scale is not None
1320 cpu_fused_moe_int8(
1321 output,
1322 hidden_states,
1323 w1,
1324 w2,
1325 self.w1_scale,
1326 self.w2_scale,
1327 self.w1_bias,
1328 self.w2_bias,
1329 topk_weights,
1330 topk_ids,
1331 activation.value,
1332 "neon",
1333 skip_weighted=apply_router_weight_on_input,
1334 )
1335
1336
1337 class PowerCPUExpertsInt8(mk.FusedMoEExpertsModular):
1338 """POWER VSX INT8 MoE with per-token activation and channelwise weight quant."""
1339
1340 @property
1341 def expects_unquantized_inputs(self) -> bool:
1342 return True
1343
1344 @staticmethod
1345 def activation_format() -> mk.FusedMoEActivationFormat:
1346 return mk.FusedMoEActivationFormat.Standard
1347
1348 @staticmethod
1349 def is_supported_config(
1350 cls: type[mk.FusedMoEExperts],
1351 moe_config: FusedMoEConfig,
1352 weight_key: QuantKey | None,
1353 activation_key: QuantKey | None,
1354 activation_format: mk.FusedMoEActivationFormat,
1355 ) -> tuple[bool, str | None]:
1356 supported, reason = mk.FusedMoEExpertsModular.is_supported_config(
1357 cls,
1358 moe_config,
1359 weight_key,
1360 activation_key,
1361 activation_format,
1362 )
1363 if not supported:
1364 return supported, reason
1365 if moe_config.in_dtype not in (torch.float32, torch.bfloat16):
1366 return False, "kernel requires float32 or bfloat16 activations"
1367 if moe_config.hidden_dim % 16 != 0:
1368 return False, "kernel requires hidden dim divisible by 16"
1369 if moe_config.intermediate_size_per_partition % 16 != 0:
1370 return False, "kernel requires intermediate dim divisible by 16"
1371 return True, None
1372
1373 @staticmethod
1374 def _supports_current_device() -> bool:
1375 return (
1376 current_platform.is_cpu()
1377 and current_platform.get_cpu_architecture() == CpuArchEnum.POWERPC
1378 and hasattr(torch.ops._C, "cpu_fused_moe_int8")
1379 )
1380
1381 @staticmethod
1382 def _supports_no_act_and_mul() -> bool:
1383 return False
1384
1385 @staticmethod
1386 def _supports_activation(activation: MoEActivation) -> bool:
1387 return activation in (
1388 MoEActivation.SILU,
1389 MoEActivation.SWIGLUOAI,
1390 MoEActivation.GELU,
1391 MoEActivation.GELU_TANH,
1392 )
1393
1394 @staticmethod
1395 def _supports_parallel_config(
1396 moe_parallel_config: FusedMoEParallelConfig,
1397 ) -> bool:
1398 return not moe_parallel_config.use_ep
1399
1400 @staticmethod
1401 def _supports_quant_scheme(
1402 weight_key: QuantKey | None,
1403 activation_key: QuantKey | None,
1404 ) -> bool:
1405 return (weight_key, activation_key) == (
1406 kInt8StaticChannelSym,
1407 kInt8DynamicTokenSym,
1408 )
1409
1410 @staticmethod
1411 def _supports_routing_method(
1412 routing_method: RoutingMethodType,
1413 weight_key: QuantKey | None,
1414 activation_key: QuantKey | None,
1415 ) -> bool:
1416 return routing_method in [
1417 RoutingMethodType.Default,
1418 RoutingMethodType.Renormalize,
1419 RoutingMethodType.RenormalizeNaive,
1420 ]
1421
1422 @staticmethod
1423 def _supports_router_logits_dtype(
1424 router_logits_dtype: torch.dtype | None,
1425 routing_method: RoutingMethodType,
1426 ) -> bool:
1427 return True
1428
1429 def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
1430 w13 = cpu_prepack_moe_weight_int8(layer.w13_weight, "vsx")
1431 w2 = cpu_prepack_moe_weight_int8(layer.w2_weight, "vsx")
1432 replace_parameter(layer, "w13_weight", w13)
1433 replace_parameter(layer, "w2_weight", w2)
1434
1435 def workspace_shapes(
1436 self,
1437 M: int,
1438 N: int,
1439 K: int,
1440 topk: int,
1441 global_num_experts: int,
1442 local_num_experts: int,
1443 expert_tokens_meta: mk.ExpertTokensMetadata | None,
1444 activation: MoEActivation,
1445 ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
1446 # cpu_fused_moe_int8 manages its own scratch space.
1447 return (0,), (0,), (M, K)
1448
1449 def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
1450 return TopKWeightAndReduceNoOP()
1451
1452 def apply(
1453 self,
1454 output: torch.Tensor,
1455 hidden_states: torch.Tensor,
1456 w1: torch.Tensor,
1457 w2: torch.Tensor,
1458 topk_weights: torch.Tensor,
1459 topk_ids: torch.Tensor,
1460 activation: MoEActivation,
1461 global_num_experts: int,
1462 expert_map: torch.Tensor | None,
1463 a1q_scale: torch.Tensor | None,
1464 a2_scale: torch.Tensor | None,
1465 workspace13: torch.Tensor,
1466 workspace2: torch.Tensor,
1467 expert_tokens_meta: mk.ExpertTokensMetadata | None,
1468 apply_router_weight_on_input: bool,
1469 ) -> None:
1470 # The modular prepare step applies router weights to the input when
1471 # apply_router_weight_on_input is enabled.
1472 assert self.w1_scale is not None
1473 assert self.w2_scale is not None
1474 cpu_fused_moe_int8(
1475 output,
1476 hidden_states,
1477 w1,
1478 w2,
1479 self.w1_scale,
1480 self.w2_scale,
1481 self.w1_bias,
1482 self.w2_bias,
1483 topk_weights,
1484 topk_ids,
1485 activation.value,
1486 "vsx",
1487 skip_weighted=apply_router_weight_on_input,
1488 )
1489
1490
1491 class ZenCPUExpertsInt8(mk.FusedMoEExpertsModular):
1492 """AMD Zen INT8 MoE with per-token activation and channelwise weight
1493 quantization, dispatched through zentorch."""
1494
1495 def __init__(
1496 self,
1497 moe_config: FusedMoEConfig,
1498 quant_config: FusedMoEQuantConfig,
1499 ):
1500 super().__init__(moe_config, quant_config)
1501 assert self.w1_scale is not None and self.w2_scale is not None, (
1502 "ZenCPUExpertsInt8 requires per-channel weight scales on the layer."
1503 )
1504 num_experts = self.w1_scale.shape[0]
1505 # ZenDNN's fast routed MoE kernel only accepts f32 expert scales; bf16
1506 # silently drops the layer onto the slower per-expert path.
1507 self._w1_scale_f32 = (
1508 self.w1_scale.detach()
1509 .to(torch.float32)
1510 .reshape(num_experts, -1)
1511 .contiguous()
1512 )
1513 self._w2_scale_f32 = (
1514 self.w2_scale.detach()
1515 .to(torch.float32)
1516 .reshape(num_experts, -1)
1517 .contiguous()
1518 )
1519 self._w1_bias_bf16 = (
1520 None
1521 if self.w1_bias is None
1522 else self.w1_bias.detach().to(torch.bfloat16).contiguous()
1523 )
1524 self._w2_bias_bf16 = (
1525 None
1526 if self.w2_bias is None
1527 else self.w2_bias.detach().to(torch.bfloat16).contiguous()
1528 )
1529 logger.info_once("[zen_cpu] Using zentorch_fused_moe for W8A8 INT8 MoE")
1530
1531 @property
1532 def expects_unquantized_inputs(self) -> bool:
1533 # zentorch_fused_moe quantizes activations itself.
1534 return True
1535
1536 @staticmethod
1537 def activation_format() -> mk.FusedMoEActivationFormat:
1538 return mk.FusedMoEActivationFormat.Standard
1539
1540 @staticmethod
1541 def _supports_current_device() -> bool:
1542 return has_zentorch_op(["zentorch_fused_moe"])
1543
1544 @staticmethod
1545 def _supports_no_act_and_mul() -> bool:
1546 return False
1547
1548 @staticmethod
1549 def _supports_activation(activation: MoEActivation) -> bool:
1550 return str(activation.value).lower() in _ZENTORCH_MOE_ACTIVATIONS
1551
1552 @staticmethod
1553 def _supports_parallel_config(
1554 moe_parallel_config: FusedMoEParallelConfig,
1555 ) -> bool:
1556 return not moe_parallel_config.use_ep
1557
1558 @staticmethod
1559 def _supports_quant_scheme(
1560 weight_key: QuantKey | None,
1561 activation_key: QuantKey | None,
1562 ) -> bool:
1563 return (weight_key, activation_key) == (
1564 kInt8StaticChannelSym,
1565 kInt8DynamicTokenSym,
1566 )
1567
1568 @staticmethod
1569 def _supports_routing_method(
1570 routing_method: RoutingMethodType,
1571 weight_key: QuantKey | None,
1572 activation_key: QuantKey | None,
1573 ) -> bool:
1574 return routing_method in [
1575 RoutingMethodType.Default,
1576 RoutingMethodType.Renormalize,
1577 RoutingMethodType.RenormalizeNaive,
1578 ]
1579
1580 @staticmethod
1581 def _supports_router_logits_dtype(
1582 router_logits_dtype: torch.dtype | None,
1583 routing_method: RoutingMethodType,
1584 ) -> bool:
1585 return True
1586
1587 def supports_expert_map(self) -> bool:
1588 return False
1589
1590 def workspace_shapes(
1591 self,
1592 M: int,
1593 N: int,
1594 K: int,
1595 topk: int,
1596 global_num_experts: int,
1597 local_num_experts: int,
1598 expert_tokens_meta: mk.ExpertTokensMetadata | None,
1599 activation: MoEActivation,
1600 ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
1601 # zentorch_fused_moe manages its own scratch space.
1602 return (0,), (0,), (M, K)
1603
1604 def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
1605 return TopKWeightAndReduceNoOP()
1606
1607 def apply(
1608 self,
1609 output: torch.Tensor,
1610 hidden_states: torch.Tensor,
1611 w1: torch.Tensor,
1612 w2: torch.Tensor,
1613 topk_weights: torch.Tensor,
1614 topk_ids: torch.Tensor,
1615 activation: MoEActivation,
1616 global_num_experts: int,
1617 expert_map: torch.Tensor | None,
1618 a1q_scale: torch.Tensor | None,
1619 a2_scale: torch.Tensor | None,
1620 workspace13: torch.Tensor,
1621 workspace2: torch.Tensor,
1622 expert_tokens_meta: mk.ExpertTokensMetadata | None,
1623 apply_router_weight_on_input: bool,
1624 ) -> None:
1625 if apply_router_weight_on_input:
1626 assert topk_ids.size(1) == 1, (
1627 "apply_router_weight_on_input is only implemented for topk=1"
1628 )
1629 hidden_states = hidden_states.mul(topk_weights.to(hidden_states.dtype))
1630
1631 torch.ops.zentorch.zentorch_fused_moe(
1632 output,
1633 hidden_states,
1634 w1,
1635 w2,
1636 self._w1_bias_bf16,
1637 self._w2_bias_bf16,
1638 topk_weights.to(torch.float32).contiguous(),
1639 topk_ids.to(torch.int32).contiguous(),
1640 apply_router_weight_on_input, # skip_weighted
1641 str(activation.value).lower(),
1642 self._w1_scale_f32,
1643 self._w2_scale_f32,
1644 )
1645
1646
1647 # ===========================================================================
1648 # FP8 W8A8 MoE
1649 # ===========================================================================
1650
1651
1652 def prepare_fp8_w8a8_moe_layer_for_cpu(
1653 w13: torch.Tensor,
1654 w2: torch.Tensor,
1655 w13_scale: torch.Tensor,
1656 w2_scale: torch.Tensor,
1657 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
1658 """Prepack FP8 W8A8 MoE weights for CPU kernel."""
1659 E = w13.size(0)
1660
1661 packed_w13_list = []
1662 packed_w13_scale_list = []
1663
1664 for i in range(E):
1665 w1_exp = w13[i] # [2N, K] FP8
1666 ws_exp = w13_scale[i] # [2N, K/128] or [2N_blocks, K/128]
1667
1668 # Align scale rows to weight rows if necessary (for block-128 checkpoints
1669 # where scale may have fewer rows: [2N/128, K/128] → [2N, K/128])
1670 if ws_exp.size(0) < w1_exp.size(0):
1671 repeat_factor = w1_exp.size(0) // ws_exp.size(0)
1672 ws_exp = torch.repeat_interleave(ws_exp, repeat_factor, dim=0)
1673 ws_exp = ws_exp[: w1_exp.size(0), :].contiguous()
1674
1675 pw, ps = torch.ops._C.float8_linear_prepack_cpu(w1_exp, ws_exp)
1676 packed_w13_list.append(pw)
1677 packed_w13_scale_list.append(ps)
1678
1679 packed_w13 = torch.stack(packed_w13_list)
1680 packed_w13_scale = torch.stack(packed_w13_scale_list)
1681
1682 # w2 uses the W8A16 path (BF16 activation × FP8 weight), VNNI packed
1683 packed_w2 = torch.ops._C.convert_weight_packed(w2)
1684 return packed_w13, packed_w13_scale, packed_w2, w2_scale
1685
1686
1687 class CPUExpertsFp8W8A8(mk.FusedMoEExpertsModular):
1688 """CPU FP8 W8A8 block-quantized modular MoE experts."""
1689
1690 def __init__(
1691 self,
1692 moe_config: FusedMoEConfig,
1693 quant_config: FusedMoEQuantConfig,
1694 ):
1695 super().__init__(moe_config, quant_config)
1696
1697 @property
1698 def expects_unquantized_inputs(self) -> bool:
1699 # Receives BF16 hidden states; quantization to FP8 is done inside apply()
1700 return True
1701
1702 @staticmethod
1703 def activation_format() -> mk.FusedMoEActivationFormat:
1704 return mk.FusedMoEActivationFormat.Standard
1705
1706 @staticmethod
1707 def _supports_current_device() -> bool:
1708 # Requires native AMX-FP8 hardware.
1709 return current_platform.is_cpu() and cpu_has_amx_fp8()
1710
1711 @staticmethod
1712 def _supports_no_act_and_mul() -> bool:
1713 return False
1714
1715 @staticmethod
1716 def _supports_activation(activation: MoEActivation) -> bool:
1717 return activation == MoEActivation.SILU
1718
1719 @staticmethod
1720 def _supports_parallel_config(
1721 moe_parallel_config: FusedMoEParallelConfig,
1722 ) -> bool:
1723 return True
1724
1725 @staticmethod
1726 def _supports_quant_scheme(
1727 weight_key: QuantKey | None,
1728 activation_key: QuantKey | None,
1729 ) -> bool:
1730 # Block-quantized FP8.
1731 return (weight_key, activation_key) == (
1732 kFp8Static128BlockSym,
1733 kFp8Dynamic128Sym,
1734 )
1735
1736 @staticmethod
1737 def _supports_routing_method(
1738 routing_method: RoutingMethodType,
1739 weight_key: QuantKey | None,
1740 activation_key: QuantKey | None,
1741 ) -> bool:
1742 return routing_method in [
1743 RoutingMethodType.Default,
1744 RoutingMethodType.Renormalize,
1745 RoutingMethodType.RenormalizeNaive,
1746 RoutingMethodType.DeepSeekV3,
1747 RoutingMethodType.DeepseekV4,
1748 ]
1749
1750 @staticmethod
1751 def _supports_router_logits_dtype(
1752 router_logits_dtype: torch.dtype | None,
1753 routing_method: RoutingMethodType,
1754 ) -> bool:
1755 return True
1756
1757 def moe_problem_size(
1758 self,
1759 a1: torch.Tensor,
1760 w1: torch.Tensor,
1761 w2: torch.Tensor,
1762 topk_ids: torch.Tensor,
1763 ) -> tuple[int, int, int, int, int]:
1764 # float8_linear_prepack_cpu blocks w1 into [E, Nc, Kc, block_k, block_n],
1765 # not the (E, N, K) layout the base implementation assumes -- N isn't
1766 # recoverable from that shape, so read it from moe_config instead.
1767 E = w1.shape[0]
1768 K = a1.size(-1)
1769 N = (
1770 self.moe_config.intermediate_size_per_partition
1771 * self.moe_config.w13_num_shards
1772 )
1773 M = a1.size(0) if a1.dim() == 2 else a1.size(1)
1774 topk = topk_ids.size(1)
1775 return E, M, N, K, topk
1776
1777 def workspace_shapes(
1778 self,
1779 M: int,
1780 N: int,
1781 K: int,
1782 topk: int,
1783 global_num_experts: int,
1784 local_num_experts: int,
1785 expert_tokens_meta: mk.ExpertTokensMetadata | None,
1786 activation: MoEActivation,
1787 ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
1788 # fused_experts_cpu manages its own scratch space.
1789 return (0,), (0,), (M, K)
1790
1791 def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
1792 return TopKWeightAndReduceNoOP()
1793
1794 def apply(
1795 self,
1796 output: torch.Tensor,
1797 hidden_states: torch.Tensor,
1798 w1: torch.Tensor,
1799 w2: torch.Tensor,
1800 topk_weights: torch.Tensor,
1801 topk_ids: torch.Tensor,
1802 activation: MoEActivation,
1803 global_num_experts: int,
1804 expert_map: torch.Tensor | None,
1805 a1q_scale: torch.Tensor | None,
1806 a2_scale: torch.Tensor | None,
1807 workspace13: torch.Tensor,
1808 workspace2: torch.Tensor,
1809 expert_tokens_meta: mk.ExpertTokensMetadata | None,
1810 apply_router_weight_on_input: bool,
1811 ) -> None:
1812 if apply_router_weight_on_input:
1813 # fused_experts_cpu always applies topk_weights internally on
1814 # combine; MoEPrepareAndFinalizeNoDPEPModular.prepare() would
1815 # also pre-apply it to hidden_states, double-weighting the
1816 # output. Not needed by any CPU FP8 model today.
1817 raise NotImplementedError(
1818 "CPUExpertsFp8W8A8 does not support apply_router_weight_on_input=True."
1819 )
1820
1821 block_shape = (
1822 list(self.quant_config.block_shape)
1823 if self.quant_config.block_shape
1824 else (
1825 [
1826 self.quant_config._w1.shape.row,
1827 self.quant_config._w1.shape.col,
1828 ]
1829 if self.quant_config._w1.shape is not None
1830 else None
1831 )
1832 )
1833
1834 # Quantize hidden_states (BF16) → FP8 with per-token scales
1835 hidden_states_2d = hidden_states.reshape(-1, hidden_states.shape[-1])
1836 x_fp8, x_scales = torch.ops._C.quantize_fp8e4m3_vec(
1837 hidden_states_2d, True, None
1838 )
1839
1840 fused_experts_cpu(
1841 output,
1842 x_fp8,
1843 w1,
1844 w2,
1845 topk_weights,
1846 topk_ids,
1847 CPUQuantMethod.FP8_W8A8,
1848 self.w1_scale, # w1_scale
1849 self.w2_scale, # w2_scale
1850 None, # w1_zero
1851 None, # w2_zero
1852 block_shape, # block_size
1853 None, # w1_bias
1854 None, # w2_bias
1855 None, # alpha
1856 None, # limit
1857 True, # is_vnni
1858 x_scales, # a1_scale: per-token FP8 activation scales
1859 )