main
py 1,563 lines 55.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from dataclasses import dataclass, field
4 from enum import IntEnum
5 from typing import Union
6
7 import torch
8
9 import vllm.envs as envs
10 from vllm.config import ParallelConfig, SchedulerConfig
11 from vllm.config.kernel import MoEBackend
12 from vllm.distributed import get_dp_group, get_pcp_group, get_tensor_model_parallel_rank
13 from vllm.logger import init_logger
14 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
15 from vllm.model_executor.layers.quantization.utils.ocp_mx_utils import (
16 OCP_MX_DTYPES,
17 OCP_MX_Scheme,
18 )
19 from vllm.model_executor.layers.quantization.utils.quant_utils import GroupShape
20 from vllm.platforms import current_platform
21 from vllm.utils.import_utils import get_triton_kernels_version
22 from vllm.utils.math_utils import cdiv
23
24 logger = init_logger(__name__)
25
26 _triton_kernels_version = get_triton_kernels_version()
27
28 if _triton_kernels_version is not None:
29 try:
30 if _triton_kernels_version == "3.8":
31 from triton_kernels.matmul import PrecisionConfig
32 else:
33 from triton_kernels.matmul_ogs import PrecisionConfig
34 except (ImportError, AttributeError) as e:
35 logger.error(
36 "Failed to import Triton kernels. Please make sure your triton "
37 "version is compatible. Error: %s",
38 e,
39 )
40
41
42 def _get_config_dtype_str(
43 dtype: torch.dtype,
44 use_fp8_w8a8: bool = False,
45 use_fp8_w8a16: bool = False,
46 use_int8_w8a16: bool = False,
47 use_int4_w4a16: bool = False,
48 ocp_mx_scheme: str | None = None,
49 ) -> str | None:
50 """Return a string used to construct the filename that contains the
51 tuning info for a particular quantization scheme. See
52 try_get_optimal_moe_config in fused_moe.py.
53 """
54 if use_fp8_w8a8:
55 return "fp8_w8a8"
56 elif use_fp8_w8a16:
57 return "fp8_w8a16"
58 elif use_int8_w8a16:
59 return "int8_w8a16"
60 elif use_int4_w4a16:
61 return "int4_w4a16"
62 elif ocp_mx_scheme is not None:
63 # The output of this function is passed to `try_get_optimal_moe_config`,
64 # and as we only simulate OCP MX execution in fused_moe for now,
65 # we will NOT look for `*,dtype=w_mxfp4_a_mxfp4.json` for now.
66 return None
67 elif dtype == torch.float:
68 # avoiding cases where kernel fails when float32 MoE
69 # use fp16/bfloat16 configs
70 return "float32"
71 return None
72
73
74 def _quant_flags_to_group_shape(
75 quant_dtype: torch.dtype | str | None,
76 per_act_token_quant: bool,
77 per_out_ch_quant: bool,
78 block_shape: list[int] | None,
79 ) -> tuple[GroupShape | None, GroupShape | None]:
80 """Convert MoE quantization flags into more generic GroupShapes."""
81 a_shape: GroupShape | None
82 w_shape: GroupShape | None
83 if block_shape is not None:
84 assert not per_act_token_quant
85 assert not per_out_ch_quant
86 # TODO(bnell): this is not quite right for activations since first
87 # dim should be 1.
88 a_shape = GroupShape(row=block_shape[0], col=block_shape[1])
89 w_shape = GroupShape(row=block_shape[0], col=block_shape[1])
90 else:
91 w_shape = None
92 a_shape = None if quant_dtype is None else GroupShape.PER_TENSOR
93
94 if per_act_token_quant:
95 a_shape = GroupShape.PER_TOKEN
96
97 if per_out_ch_quant:
98 w_shape = GroupShape.PER_TOKEN
99
100 return a_shape, w_shape
101
102
103 # The type of method in top-K routing
104 # Please keep this in sync with the counterpart defined in https://github.com/flashinfer-ai/flashinfer/blob/main/include/flashinfer/trtllm/fused_moe/runner.h
105 class RoutingMethodType(IntEnum):
106 # Default: Softmax -> TopK
107 Default = (0,)
108 # Renormalize: TopK -> Softmax
109 Renormalize = (1,)
110 # DeepSeekV3: Sigmoid -> RoutingBiasAdd -> Top2 in group -> Top4 groups
111 # -> Top8 experts from the Top4 groups
112 DeepSeekV3 = (2,)
113 # Llama4: Top1 -> Sigmoid
114 Llama4 = (3,)
115 # RenormalizeNaive: Softmax -> TopK -> Renormalize
116 RenormalizeNaive = (4,)
117 # TopK: TopK (no softmax)
118 TopK = (5,)
119 # SigmoidRenorm: Sigmoid -> TopK -> Renormalize (divide by sum of top-K)
120 SigmoidRenorm = (6,)
121 # MiniMax2: Sigmoid + Bias -> TopK -> ScaledSumNormalize
122 # (routeScale=routed_scaling_factor, epsilon=1e-20)
123 MiniMax2 = (7,)
124 # Sigmoid: Sigmoid -> TopK (no renormalization)
125 Sigmoid = (8,)
126 # Unspecified
127 Unspecified = (9,)
128 # other routing types (not passed to FlashInfer kernels)
129 # Deepseek V4 -> sqrtsoftplus + Bias + Normalize
130 DeepseekV4 = (100,)
131 Custom = (101,)
132 Simulated = (102,)
133
134
135 def get_routing_method_type(
136 scoring_func: str,
137 top_k: int,
138 renormalize: bool,
139 num_expert_group: int | None,
140 has_e_score_bias: bool,
141 ) -> RoutingMethodType:
142 if scoring_func == "sqrtsoftplus":
143 # DeepSeek V4 uses sqrtsoftplus routing with optional routing bias
144 # and top-k renormalization.
145 if renormalize:
146 return RoutingMethodType.DeepseekV4
147 else:
148 return RoutingMethodType.Unspecified
149
150 if has_e_score_bias:
151 if scoring_func == "sigmoid":
152 if not renormalize:
153 return RoutingMethodType.Unspecified
154 if (num_expert_group or 0) > 0:
155 return RoutingMethodType.DeepSeekV3
156 return RoutingMethodType.MiniMax2
157 else:
158 return RoutingMethodType.Unspecified
159
160 if scoring_func == "sigmoid":
161 if renormalize:
162 return RoutingMethodType.SigmoidRenorm
163 return RoutingMethodType.Sigmoid
164
165 if scoring_func == "softmax":
166 if renormalize:
167 return RoutingMethodType.RenormalizeNaive
168 else:
169 return RoutingMethodType.Default
170
171 return RoutingMethodType.Unspecified
172
173
174 @dataclass
175 class FusedMoEQuantDesc:
176 """A quantization descriptor for fused MoE ops. This class can describe
177 either activations or weights.
178 """
179
180 # The quantized type of this parameters. None means unquantized or
181 # already quantized.
182 # TODO (bnell): use scalar_type instead of Union.
183 dtype: torch.dtype | str | None = None
184
185 # A field that describes the quantization group shape, from quant_utils.py.
186 # * (-1, -1) for per-tensor quantization
187 # * (1, -1) for per-row quantization
188 # * (-1, 1) for per-column quantization
189 # * (128, 128) for 128x128 deepseek style block quantization
190 # * (1, 128) for deepseek style activation quantization
191 # (i.e. per-token-per-group)
192 shape: GroupShape | None = None
193
194 # Quantization scales.
195 # TODO(bnell): maybe put PrecisionConfigs in subclass of QuantDesc?
196 scale: Union[torch.Tensor, "PrecisionConfig", None] = None
197
198 # Quantization alphas or gscales, used for nvfp4 types.
199 # W4A8 FP8: used for per-channel scales
200 # TODO(bnell): put some of these in subclasses
201 alpha_or_gscale: torch.Tensor | None = None
202
203 # Zero points for int4/int8 types
204 zp: torch.Tensor | None = None
205
206 # Biases for GPT triton MoE
207 bias: torch.Tensor | None = None
208
209
210 # TODO(bnell): have subclasses for specific moe methods?
211 # e.g. for specific arguments bias, precision, etc.
212 @dataclass
213 class FusedMoEQuantConfig:
214 """The FusedMoEQuantConfig contains all the quantization parameters for
215 a single FusedMoEMethodBase operation. It consists of four
216 FusedMoEQuantDescs, one for each activation and set of weights.
217
218 Each FusedMoEMethodBase must implement a get_fused_moe_quant_config
219 method to construct a FusedMoEQuantConfig for use with that class.
220
221 FusedMoEQuant configs are only used for modular kernels, fused_experts
222 (from fused_moe.py), cutlass_moe_fp[48], rocm_aiter_fused_experts and
223 triton_kernel_moe_forward. Other MoE methods can ignore the
224 FusedMoEQuantConfig (for now) and hardcode it to None.
225
226 There are currently some restrictions on what can be expressed:
227 - Most MoE ops only support similar quantization strategies for
228 each parameter, e.g. both weights must have the same GroupShape
229 and both activations must share the same GroupShape. One exception to
230 this is the cutlass moe which allows per channel quantization on the
231 outputs. Note: this restrictions are not always rigorously checked.
232 - Not all fused MoE functions support all the parameters, e.g. zero points,
233 global scales, alphas and biases are not universally supported.
234 - Fully general GroupShapes are not allowed. Activations only support
235 per token, per tensor or K-blocked.
236 - Weights are not required to have a GroupShape since they have already
237 been quantized.
238
239 Other notes:
240 - PrecisionConfigs are specific to GPT OSS Triton.
241 - As a follow up it would probably make sense to subclass FusedMoEQuantDesc
242 or FusedMoEQuantConfig for particular FusedMoEMethodBase subclasses
243 so that only the required quantization parameters are used/stored.
244 """
245
246 # TODO(bnell) make sure a1_scales/a2_scales don't interfere with chunking
247 _a1: FusedMoEQuantDesc
248 _a2: FusedMoEQuantDesc
249 _w1: FusedMoEQuantDesc
250 _w2: FusedMoEQuantDesc
251 is_scale_swizzled: bool = True
252
253 # MXFP4-specific TRTLLM parameters for SwiGLU activation clamping.
254 # These correspond to gemm1_alpha, gemm1_beta, gemm1_clamp_limit
255 # in TrtLlmMxfp4ExpertsBase.
256 gemm1_alpha: float | None = None
257 gemm1_beta: float | None = None
258 gemm1_clamp_limit: float | None = None
259
260 mx_alignment: int = 0
261
262 def __post_init__(self):
263 assert not self.per_act_token_quant or self.block_shape is None, (
264 "illegal quantization"
265 )
266
267 #
268 # Convenience accessors for various properties.
269 #
270
271 @property
272 def quant_dtype(self) -> torch.dtype | str | None:
273 return self._a1.dtype
274
275 @property
276 def weight_quant_dtype(self) -> torch.dtype | str | None:
277 return self._w1.dtype
278
279 @property
280 def is_quantized(self) -> bool:
281 return self.quant_dtype is not None
282
283 @property
284 def is_per_act_token(self) -> bool:
285 return self._a1.shape == GroupShape.PER_TOKEN
286
287 @property
288 def per_act_token_quant(self) -> bool:
289 return self._a1.shape == GroupShape.PER_TOKEN
290
291 @property
292 def per_out_ch_quant(self) -> bool:
293 return self._w1.shape == GroupShape.PER_TOKEN
294
295 @property
296 def is_per_tensor(self) -> bool:
297 return self._a1.shape == GroupShape.PER_TENSOR
298
299 @property
300 def block_shape(self) -> list[int] | None:
301 if (
302 self._a1.shape is not None
303 and self._a1.shape != GroupShape.PER_TENSOR
304 and self._a1.shape != GroupShape.PER_TOKEN
305 ):
306 return [self._a1.shape.row, self._a1.shape.col]
307 else:
308 return None
309
310 @property
311 def is_block_quantized(self) -> bool:
312 return self.block_shape is not None
313
314 @property
315 def a1_scale(self) -> torch.Tensor | None:
316 assert self._a1.scale is None or isinstance(self._a1.scale, torch.Tensor)
317 return self._a1.scale
318
319 @property
320 def a1_gscale(self) -> torch.Tensor | None:
321 return self._a1.alpha_or_gscale
322
323 @property
324 def a2_scale(self) -> torch.Tensor | None:
325 assert self._a2.scale is None or isinstance(self._a2.scale, torch.Tensor)
326 return self._a2.scale
327
328 @property
329 def a2_gscale(self) -> torch.Tensor | None:
330 return self._a2.alpha_or_gscale
331
332 @property
333 def w1_scale(self) -> torch.Tensor | None:
334 assert self._w1.scale is None or isinstance(self._w1.scale, torch.Tensor)
335 return self._w1.scale
336
337 @property
338 def w1_zp(self) -> torch.Tensor | None:
339 return self._w1.zp
340
341 @property
342 def w1_bias(self) -> torch.Tensor | None:
343 return self._w1.bias
344
345 @property
346 def w1_precision(self) -> "PrecisionConfig | None":
347 assert self._w1.scale is None or isinstance(self._w1.scale, PrecisionConfig)
348 return self._w1.scale
349
350 @property
351 def g1_alphas(self) -> torch.Tensor | None:
352 return self._w1.alpha_or_gscale
353
354 @property
355 def w2_scale(self) -> torch.Tensor | None:
356 assert self._w2.scale is None or isinstance(self._w2.scale, torch.Tensor)
357 return self._w2.scale
358
359 @property
360 def w2_zp(self) -> torch.Tensor | None:
361 return self._w2.zp
362
363 @property
364 def w2_bias(self) -> torch.Tensor | None:
365 return self._w2.bias
366
367 @property
368 def w2_precision(self) -> "PrecisionConfig | None":
369 assert self._w2.scale is None or isinstance(self._w2.scale, PrecisionConfig)
370 return self._w2.scale
371
372 @property
373 def g2_alphas(self) -> torch.Tensor | None:
374 return self._w2.alpha_or_gscale
375
376 @property
377 def use_fp8_w8a8(self) -> bool:
378 return self.quant_dtype == current_platform.fp8_dtype()
379
380 @property
381 def use_int8_w8a8(self) -> bool:
382 return self.quant_dtype == torch.int8
383
384 @property
385 def use_int8_w8a16(self) -> bool:
386 return self._a1.dtype is None and self._w1.dtype == torch.int8
387
388 @property
389 def use_fp8_w8a16(self) -> bool:
390 return self._a1.dtype is None and self._w1.dtype == current_platform.fp8_dtype()
391
392 @property
393 def use_int4_w4a16(self) -> bool:
394 return self._a1.dtype is None and self._w1.dtype == "int4"
395
396 @property
397 def use_nvfp4_w4a16(self) -> bool:
398 return self._a1.dtype is None and self._w1.dtype == "nvfp4"
399
400 @property
401 def ocp_mx_scheme(self) -> str | None:
402 if not hasattr(self, "_ocp_mx_scheme"):
403 if (self._a1.dtype is not None and not isinstance(self._a1.dtype, str)) or (
404 self._w1.dtype is not None and not isinstance(self._w1.dtype, str)
405 ):
406 self._ocp_mx_scheme = None
407 else:
408 ocp_mx_scheme = OCP_MX_Scheme.from_quant_dtype(
409 self._a1.dtype, self._w1.dtype
410 )
411
412 if ocp_mx_scheme is not None:
413 ocp_mx_scheme = ocp_mx_scheme.value
414
415 self._ocp_mx_scheme = ocp_mx_scheme
416
417 return self._ocp_mx_scheme
418
419 @property
420 def use_mxfp4_w4a16(self) -> bool:
421 return self._a1.dtype is None and self._w1.dtype == "mxfp4"
422
423 @property
424 def use_mxfp4_w4a4(self) -> bool:
425 return self._a1.dtype == "mxfp4" and self._w1.dtype == "mxfp4"
426
427 @property
428 def use_nvfp4_w4a4(self) -> bool:
429 return self.quant_dtype == "nvfp4"
430
431 @property
432 def use_mxfp4_w4a8(self) -> bool:
433 return self._a1.dtype == "fp8" and self._w1.dtype == "mxfp4"
434
435 def config_name(self, dtype: torch.dtype) -> str | None:
436 """Return a string used to construct the filename that contains the
437 tuning info for a particular quantization scheme. See
438 try_get_optimal_moe_config in fused_moe.py.
439 """
440 return _get_config_dtype_str(
441 use_fp8_w8a8=self.use_fp8_w8a8,
442 use_fp8_w8a16=self.use_fp8_w8a16,
443 use_int8_w8a16=self.use_int8_w8a16,
444 use_int4_w4a16=self.use_int4_w4a16,
445 ocp_mx_scheme=self.ocp_mx_scheme,
446 dtype=dtype,
447 )
448
449 def scale_shape(
450 self,
451 max_tokens: int,
452 hidden_dim: int,
453 ) -> tuple[int, int] | None:
454 """Construct the proper activation scale shape for this
455 config.
456 """
457 if self.is_quantized:
458 if self.is_block_quantized:
459 assert self.block_shape is not None
460 _, block_k = self.block_shape
461 k_tiles = cdiv(hidden_dim, block_k)
462 return (max_tokens, k_tiles)
463 elif self.is_per_act_token:
464 return (max_tokens, 1)
465 else:
466 return (1, 1)
467 else:
468 return None
469
470 def batched_scale_shape(
471 self,
472 num_experts: int,
473 max_tokens: int,
474 hidden_dim: int,
475 ) -> tuple[int, int, int] | None:
476 """Construct the proper activation batched scale shape for this
477 config, e.g. (num experts, *scale_shape).
478 """
479 if self.is_quantized:
480 scale_shape = self.scale_shape(max_tokens, hidden_dim)
481 assert scale_shape is not None
482 return (num_experts, *scale_shape)
483 else:
484 return None
485
486 @staticmethod
487 def make(
488 quant_dtype: torch.dtype | str | None = None,
489 per_act_token_quant: bool = False,
490 per_out_ch_quant: bool = False,
491 block_shape: list[int] | None = None,
492 w1_scale: Union[torch.Tensor, "PrecisionConfig", None] = None,
493 w2_scale: Union[torch.Tensor, "PrecisionConfig", None] = None,
494 a1_scale: torch.Tensor | None = None,
495 a2_scale: torch.Tensor | None = None,
496 g1_alphas: torch.Tensor | None = None,
497 g2_alphas: torch.Tensor | None = None,
498 a1_gscale: torch.Tensor | None = None,
499 a2_gscale: torch.Tensor | None = None,
500 w1_bias: torch.Tensor | None = None,
501 w2_bias: torch.Tensor | None = None,
502 w1_zp: torch.Tensor | None = None,
503 w2_zp: torch.Tensor | None = None,
504 weight_dtype: torch.dtype | str | None = None,
505 is_scale_swizzled: bool = True,
506 gemm1_alpha: float | None = None,
507 gemm1_beta: float | None = None,
508 gemm1_clamp_limit: float | None = None,
509 ) -> "FusedMoEQuantConfig":
510 """General builder function for a FusedMoEQuantConfig.
511 - quant_dtype: Optional quantization type. None if activations are
512 unquantized or quantized prior to calling. Note: "nvfp4", "mxfp4",
513 "mxfp6_e3m2", "mxfp6_e2m3" are the only valid string values
514 for quant_dtype.
515 - per_act_token_quant: Activations have per token quantization.
516 - per_out_ch_quant: Outputs have per channel quantization. (only
517 for cutlass).
518 - block_shape: Optional block size for block-wise quantization.
519 Incompatible with per_act_token and per_out_ch quant.
520 - w1_scale: Optional scale to be used for w1.
521 - w2_scale: Optional scale to be used for w2.
522 - a1_scale: Optional scale to be used for a1.
523 - a2_scale: Optional scale to be used for a2.
524 - g1_alphas: Optional global quantization scales for w1 (for nvfp4).
525 Optional per-channel scales for w1 (for W4A8 FP8).
526 Optional dq scale i.e. w_scale * a_scale (for W8A8 fp8).
527 - g2_alphas: Optional global quantization scales for w2 (for nvfp4).
528 Optional per-channel scales for w2 (for W4A8 FP8).
529 Optional dq scale i.e. w_scale * a_scale (for W8A8 fp8).
530 - a1_gscale: Optional global quantization scales for a1 (1.0 /a2_scale).
531 - a2_gscale: Optional global quantization scales for a2 (1.0 /a2_scale).
532
533 - w1_bias: Optional biases for w1 (GPT OSS Triton).
534 - w2_bias: Optional biases for w1 (GPT OSS Triton).
535 - w1_zp: Optional w1 zero points for int4/int8 quantization.
536 - w2_zp: Optional w2 zero points for int4/int8 quantization.
537 - is_scale_swizzled: Whether the activation scale-factor layout is
538 swizzled. Pass through to the underlying quantization kernel for
539 dtypes that distinguish layouts (nvfp4, mxfp8). Defaults to True.
540 - gemm1_alpha: Optional MXFP4 TRTLLM SwiGLU alpha parameter.
541 - gemm1_beta: Optional MXFP4 TRTLLM SwiGLU beta parameter.
542 - gemm1_clamp_limit: Optional MXFP4 TRTLLM SwiGLU clamp limit.
543 """
544 assert not isinstance(quant_dtype, str) or quant_dtype in {
545 "nvfp4",
546 "mxfp4",
547 "mxfp6_e3m2",
548 "mxfp6_e2m3",
549 "mxfp8",
550 }
551 assert not isinstance(weight_dtype, str) or weight_dtype in {
552 "nvfp4",
553 "mxfp4",
554 "mxfp6_e3m2",
555 "mxfp6_e2m3",
556 "int4",
557 "mxfp8",
558 }
559
560 if weight_dtype is None:
561 weight_dtype = quant_dtype
562
563 a_shape, w_shape = _quant_flags_to_group_shape(
564 quant_dtype, per_act_token_quant, per_out_ch_quant, block_shape
565 )
566 quant_config = FusedMoEQuantConfig(
567 _a1=FusedMoEQuantDesc(quant_dtype, a_shape, a1_scale, a1_gscale),
568 _a2=FusedMoEQuantDesc(quant_dtype, a_shape, a2_scale, a2_gscale),
569 _w1=FusedMoEQuantDesc(
570 weight_dtype, w_shape, w1_scale, g1_alphas, w1_zp, w1_bias
571 ),
572 _w2=FusedMoEQuantDesc(
573 weight_dtype, w_shape, w2_scale, g2_alphas, w2_zp, w2_bias
574 ),
575 is_scale_swizzled=is_scale_swizzled,
576 gemm1_alpha=gemm1_alpha,
577 gemm1_beta=gemm1_beta,
578 gemm1_clamp_limit=gemm1_clamp_limit,
579 )
580 assert quant_config.per_act_token_quant == per_act_token_quant
581 assert quant_config.per_out_ch_quant == per_out_ch_quant
582 assert quant_config.block_shape == block_shape
583 return quant_config
584
585
586 def fp8_w8a8_moe_quant_config(
587 w1_scale: torch.Tensor,
588 w2_scale: torch.Tensor,
589 a1_scale: torch.Tensor | None = None,
590 a2_scale: torch.Tensor | None = None,
591 w1_bias: torch.Tensor | None = None,
592 w2_bias: torch.Tensor | None = None,
593 per_act_token_quant: bool = False,
594 per_out_ch_quant: bool = False,
595 block_shape: list[int] | None = None,
596 a1_gscale: torch.Tensor | None = None,
597 a2_gscale: torch.Tensor | None = None,
598 g1_alphas: torch.Tensor | None = None,
599 g2_alphas: torch.Tensor | None = None,
600 gemm1_alpha: float | None = None,
601 gemm1_beta: float | None = None,
602 gemm1_clamp_limit: float | None = None,
603 ) -> FusedMoEQuantConfig:
604 """Construct a quant config for fp8 activations and fp8 weights."""
605 return FusedMoEQuantConfig.make(
606 current_platform.fp8_dtype(),
607 w1_scale=w1_scale,
608 g1_alphas=g1_alphas,
609 w2_scale=w2_scale,
610 g2_alphas=g2_alphas,
611 w1_bias=w1_bias,
612 w2_bias=w2_bias,
613 a1_scale=a1_scale,
614 a1_gscale=a1_gscale,
615 a2_scale=a2_scale,
616 a2_gscale=a2_gscale,
617 per_act_token_quant=per_act_token_quant,
618 per_out_ch_quant=per_out_ch_quant,
619 block_shape=block_shape,
620 gemm1_alpha=gemm1_alpha,
621 gemm1_beta=gemm1_beta,
622 gemm1_clamp_limit=gemm1_clamp_limit,
623 )
624
625
626 def int8_w8a8_moe_quant_config(
627 w1_scale: torch.Tensor,
628 w2_scale: torch.Tensor,
629 a1_scale: torch.Tensor | None,
630 a2_scale: torch.Tensor | None,
631 w1_bias: torch.Tensor | None = None,
632 w2_bias: torch.Tensor | None = None,
633 per_act_token_quant: bool = False,
634 ) -> FusedMoEQuantConfig:
635 """Construct a quant config for int8 activations and int8 weights."""
636 return FusedMoEQuantConfig.make(
637 torch.int8,
638 w1_scale=w1_scale,
639 w2_scale=w2_scale,
640 a1_scale=a1_scale,
641 a2_scale=a2_scale,
642 w1_bias=w1_bias,
643 w2_bias=w2_bias,
644 per_act_token_quant=per_act_token_quant,
645 per_out_ch_quant=False,
646 block_shape=None,
647 )
648
649
650 def gptq_marlin_moe_quant_config(
651 w1_scale: torch.Tensor,
652 w2_scale: torch.Tensor,
653 weight_bits: int,
654 group_size: int,
655 w1_zp: torch.Tensor | None = None,
656 w2_zp: torch.Tensor | None = None,
657 w1_bias: torch.Tensor | None = None,
658 w2_bias: torch.Tensor | None = None,
659 ):
660 """Construct a quant config for gptq marlin quantization."""
661 from vllm.model_executor.layers.quantization.utils.quant_utils import GroupShape
662
663 w_shape = None if group_size == -1 else GroupShape(row=1, col=group_size)
664
665 # Activations are NOT quantized for GPTQ (fp16/bf16)
666 a_shape = w_shape # Same as weight shape for alignment
667
668 # Determine weight dtype
669 if weight_bits == 4:
670 weight_dtype = "int4"
671 elif weight_bits == 8:
672 weight_dtype = torch.int8
673 else:
674 raise ValueError(f"Unsupported weight_bits: {weight_bits}")
675
676 return FusedMoEQuantConfig(
677 _a1=FusedMoEQuantDesc(dtype=None, shape=a_shape),
678 _a2=FusedMoEQuantDesc(dtype=None, shape=a_shape),
679 _w1=FusedMoEQuantDesc(weight_dtype, w_shape, w1_scale, None, w1_zp, w1_bias),
680 _w2=FusedMoEQuantDesc(weight_dtype, w_shape, w2_scale, None, w2_zp, w2_bias),
681 )
682
683
684 def mxfp4_w4a16_moe_quant_config(
685 w1_scale: Union[torch.Tensor, "PrecisionConfig"],
686 w2_scale: Union[torch.Tensor, "PrecisionConfig"],
687 w1_bias: torch.Tensor | None = None,
688 w2_bias: torch.Tensor | None = None,
689 gemm1_alpha: float | None = None,
690 gemm1_beta: float | None = None,
691 gemm1_clamp_limit: float | None = None,
692 ) -> FusedMoEQuantConfig:
693 """Construct a quant config for unquantized activations and mxfp4 weights."""
694 return FusedMoEQuantConfig(
695 _a1=FusedMoEQuantDesc(),
696 _a2=FusedMoEQuantDesc(),
697 _w1=FusedMoEQuantDesc("mxfp4", None, w1_scale, None, None, w1_bias),
698 _w2=FusedMoEQuantDesc("mxfp4", None, w2_scale, None, None, w2_bias),
699 gemm1_alpha=gemm1_alpha,
700 gemm1_beta=gemm1_beta,
701 gemm1_clamp_limit=gemm1_clamp_limit,
702 )
703
704
705 def mxfp4_mxfp8_moe_quant_config(
706 w1_scale: Union[torch.Tensor, "PrecisionConfig"],
707 w2_scale: Union[torch.Tensor, "PrecisionConfig"],
708 a1_scale: torch.Tensor | None = None,
709 a2_scale: torch.Tensor | None = None,
710 w1_bias: torch.Tensor | None = None,
711 w2_bias: torch.Tensor | None = None,
712 block_shape: list[int] | None = None,
713 gemm1_alpha: float | None = None,
714 gemm1_beta: float | None = None,
715 gemm1_clamp_limit: float | None = None,
716 mx_alignment: int = 0,
717 is_scale_swizzled: bool = True,
718 ) -> FusedMoEQuantConfig:
719 """Construct a quant config for mxfp4 activations and mxfp4 weights."""
720 return FusedMoEQuantConfig(
721 _a1=FusedMoEQuantDesc("mxfp8"),
722 _a2=FusedMoEQuantDesc("mxfp8"),
723 _w1=FusedMoEQuantDesc("mxfp4", None, w1_scale, None, None, w1_bias),
724 _w2=FusedMoEQuantDesc("mxfp4", None, w2_scale, None, None, w2_bias),
725 gemm1_alpha=gemm1_alpha,
726 gemm1_beta=gemm1_beta,
727 gemm1_clamp_limit=gemm1_clamp_limit,
728 mx_alignment=mx_alignment,
729 is_scale_swizzled=is_scale_swizzled,
730 )
731
732
733 def mxfp4_w4a8_moe_quant_config(
734 w1_scale: Union[torch.Tensor, "PrecisionConfig"],
735 w2_scale: Union[torch.Tensor, "PrecisionConfig"],
736 a1_scale: torch.Tensor | None = None,
737 a2_scale: torch.Tensor | None = None,
738 w1_bias: torch.Tensor | None = None,
739 w2_bias: torch.Tensor | None = None,
740 block_shape: list[int] | None = None,
741 gemm1_clamp_limit: float | None = None,
742 ) -> FusedMoEQuantConfig:
743 """Construct a quant config for fp8 activations and mxfp4 weights."""
744 return FusedMoEQuantConfig(
745 _a1=FusedMoEQuantDesc("fp8", None, a1_scale, None, None, None),
746 _a2=FusedMoEQuantDesc("fp8", None, a2_scale, None, None, None),
747 _w1=FusedMoEQuantDesc("mxfp4", None, w1_scale, None, None, w1_bias),
748 _w2=FusedMoEQuantDesc("mxfp4", None, w2_scale, None, None, w2_bias),
749 gemm1_clamp_limit=gemm1_clamp_limit,
750 )
751
752
753 def ocp_mx_moe_quant_config(
754 quant_dtype: str,
755 w1_scale: Union[torch.Tensor, "PrecisionConfig"],
756 w2_scale: Union[torch.Tensor, "PrecisionConfig"],
757 weight_dtype: str | None = None,
758 a1_scale: torch.Tensor | None = None,
759 a2_scale: torch.Tensor | None = None,
760 w1_bias: torch.Tensor | None = None,
761 w2_bias: torch.Tensor | None = None,
762 block_shape: list[int] | None = None,
763 gemm1_alpha: float | None = None,
764 gemm1_beta: float | None = None,
765 gemm1_clamp_limit: float | None = None,
766 ) -> FusedMoEQuantConfig:
767 """Construct a quant config for mxfp4 activations and mxfp4 weights."""
768 assert quant_dtype in OCP_MX_DTYPES
769 return FusedMoEQuantConfig.make(
770 quant_dtype=quant_dtype,
771 weight_dtype=weight_dtype,
772 w1_scale=w1_scale,
773 w2_scale=w2_scale,
774 a1_scale=a1_scale,
775 a2_scale=a2_scale,
776 w1_bias=w1_bias,
777 w2_bias=w2_bias,
778 per_act_token_quant=False,
779 per_out_ch_quant=False,
780 block_shape=block_shape,
781 gemm1_alpha=gemm1_alpha,
782 gemm1_beta=gemm1_beta,
783 gemm1_clamp_limit=gemm1_clamp_limit,
784 )
785
786
787 def nvfp4_moe_quant_config(
788 g1_alphas: torch.Tensor,
789 g2_alphas: torch.Tensor,
790 a1_gscale: torch.Tensor,
791 a2_gscale: torch.Tensor,
792 w1_scale: torch.Tensor,
793 w2_scale: torch.Tensor,
794 w1_bias: torch.Tensor | None = None,
795 w2_bias: torch.Tensor | None = None,
796 is_scale_swizzled: bool = True,
797 gemm1_alpha: float | None = None,
798 gemm1_beta: float | None = None,
799 gemm1_clamp_limit: float | None = None,
800 ) -> FusedMoEQuantConfig:
801 """Construct a quant config for mxfp4 activations and nvp4 weights."""
802 return FusedMoEQuantConfig.make(
803 "nvfp4",
804 w1_scale=w1_scale,
805 w2_scale=w2_scale,
806 w1_bias=w1_bias,
807 w2_bias=w2_bias,
808 a1_gscale=a1_gscale,
809 a2_gscale=a2_gscale,
810 g1_alphas=g1_alphas,
811 g2_alphas=g2_alphas,
812 per_act_token_quant=False,
813 per_out_ch_quant=False,
814 block_shape=None,
815 is_scale_swizzled=is_scale_swizzled,
816 gemm1_alpha=gemm1_alpha,
817 gemm1_beta=gemm1_beta,
818 gemm1_clamp_limit=gemm1_clamp_limit,
819 )
820
821
822 def mxfp4_moe_quant_config(
823 w1_scale: torch.Tensor,
824 w2_scale: torch.Tensor,
825 ) -> FusedMoEQuantConfig:
826 """Construct a quant config for MXFP4 x MXFP4 MoE.
827 MXFP4 uses block scaling only (E8M0 scales, 32-element groups), with no
828 separate alphas / global activation scales in this config.
829 """
830 return FusedMoEQuantConfig.make(
831 "mxfp4",
832 w1_scale=w1_scale,
833 w2_scale=w2_scale,
834 per_act_token_quant=False,
835 per_out_ch_quant=False,
836 block_shape=None,
837 )
838
839
840 def nvfp4_w4a16_moe_quant_config(
841 g1_alphas: torch.Tensor,
842 g2_alphas: torch.Tensor,
843 w1_scale: torch.Tensor,
844 w2_scale: torch.Tensor,
845 gemm1_alpha: float | None = None,
846 gemm1_beta: float | None = None,
847 gemm1_clamp_limit: float | None = None,
848 ) -> FusedMoEQuantConfig:
849 """Construct a quant config for 16-but activations and nvp4 weights."""
850 return FusedMoEQuantConfig.make(
851 quant_dtype=None,
852 w1_scale=w1_scale,
853 w2_scale=w2_scale,
854 g1_alphas=g1_alphas,
855 g2_alphas=g2_alphas,
856 weight_dtype="nvfp4",
857 gemm1_alpha=gemm1_alpha,
858 gemm1_beta=gemm1_beta,
859 gemm1_clamp_limit=gemm1_clamp_limit,
860 )
861
862
863 def int4_w4a16_moe_quant_config(
864 w1_scale: torch.Tensor,
865 w2_scale: torch.Tensor,
866 w1_zp: torch.Tensor | None = None,
867 w2_zp: torch.Tensor | None = None,
868 w1_bias: torch.Tensor | None = None,
869 w2_bias: torch.Tensor | None = None,
870 block_shape: list[int] | None = None,
871 a1_gscale: torch.Tensor | None = None,
872 a2_gscale: torch.Tensor | None = None,
873 gemm1_clamp_limit: float | None = None,
874 gemm1_alpha: float | None = None,
875 gemm1_beta: float | None = None,
876 ) -> FusedMoEQuantConfig:
877 """Construct a quant config for 16-bit float activations and int4 weights."""
878 group_shape = GroupShape(*block_shape) if block_shape is not None else None
879 return FusedMoEQuantConfig(
880 _a1=FusedMoEQuantDesc(shape=group_shape, alpha_or_gscale=a1_gscale),
881 _a2=FusedMoEQuantDesc(shape=group_shape, alpha_or_gscale=a2_gscale),
882 _w1=FusedMoEQuantDesc("int4", group_shape, w1_scale, None, w1_zp, w1_bias),
883 _w2=FusedMoEQuantDesc("int4", group_shape, w2_scale, None, w2_zp, w2_bias),
884 gemm1_clamp_limit=gemm1_clamp_limit,
885 gemm1_alpha=gemm1_alpha,
886 gemm1_beta=gemm1_beta,
887 )
888
889
890 def fp8_w8a16_moe_quant_config(
891 w1_scale: torch.Tensor,
892 w2_scale: torch.Tensor,
893 w1_bias: torch.Tensor | None = None,
894 w2_bias: torch.Tensor | None = None,
895 block_shape: list[int] | None = None,
896 gemm1_alpha: float | None = None,
897 gemm1_beta: float | None = None,
898 gemm1_clamp_limit: float | None = None,
899 ) -> FusedMoEQuantConfig:
900 """Construct a quant config for 16-bit float activations and fp8 weights."""
901 group_shape = GroupShape(*block_shape) if block_shape is not None else None
902 fp8_dtype = current_platform.fp8_dtype()
903 return FusedMoEQuantConfig(
904 _a1=FusedMoEQuantDesc(),
905 _a2=FusedMoEQuantDesc(),
906 _w1=FusedMoEQuantDesc(
907 fp8_dtype,
908 group_shape,
909 w1_scale,
910 None,
911 None,
912 w1_bias,
913 ),
914 _w2=FusedMoEQuantDesc(
915 fp8_dtype,
916 group_shape,
917 w2_scale,
918 None,
919 None,
920 w2_bias,
921 ),
922 gemm1_alpha=gemm1_alpha,
923 gemm1_beta=gemm1_beta,
924 gemm1_clamp_limit=gemm1_clamp_limit,
925 )
926
927
928 def int8_w8a16_moe_quant_config(
929 w1_scale: torch.Tensor,
930 w2_scale: torch.Tensor,
931 w1_zp: torch.Tensor | None = None,
932 w2_zp: torch.Tensor | None = None,
933 w1_bias: torch.Tensor | None = None,
934 w2_bias: torch.Tensor | None = None,
935 block_shape: list[int] | None = None,
936 a1_gscale: torch.Tensor | None = None,
937 a2_gscale: torch.Tensor | None = None,
938 gemm1_clamp_limit: float | None = None,
939 gemm1_alpha: float | None = None,
940 gemm1_beta: float | None = None,
941 ) -> FusedMoEQuantConfig:
942 """Construct a quant config for 16-bit float activations and int8 weights."""
943 group_shape = GroupShape(*block_shape) if block_shape is not None else None
944 return FusedMoEQuantConfig(
945 _a1=FusedMoEQuantDesc(shape=group_shape, alpha_or_gscale=a1_gscale),
946 _a2=FusedMoEQuantDesc(shape=group_shape, alpha_or_gscale=a2_gscale),
947 _w1=FusedMoEQuantDesc(torch.int8, group_shape, w1_scale, None, w1_zp, w1_bias),
948 _w2=FusedMoEQuantDesc(torch.int8, group_shape, w2_scale, None, w2_zp, w2_bias),
949 gemm1_clamp_limit=gemm1_clamp_limit,
950 gemm1_alpha=gemm1_alpha,
951 gemm1_beta=gemm1_beta,
952 )
953
954
955 def int4_w4afp8_moe_quant_config(
956 w1_scale: torch.Tensor,
957 w2_scale: torch.Tensor,
958 g1_alphas: torch.Tensor,
959 g2_alphas: torch.Tensor,
960 per_act_token_quant: bool = False,
961 per_out_ch_quant: bool = False,
962 block_shape: list[int] | None = None,
963 ) -> FusedMoEQuantConfig:
964 """Construct a quant config for fp8 activations and int4 weights."""
965 return FusedMoEQuantConfig.make(
966 torch.float8_e4m3fn, # quant dtype for activations
967 w1_scale=w1_scale,
968 w2_scale=w2_scale,
969 g1_alphas=g1_alphas,
970 g2_alphas=g2_alphas,
971 per_act_token_quant=per_act_token_quant,
972 per_out_ch_quant=per_out_ch_quant,
973 block_shape=block_shape,
974 weight_dtype="int4", # weight dtype for weights
975 )
976
977
978 def biased_moe_quant_config(
979 w1_bias: torch.Tensor | None,
980 w2_bias: torch.Tensor | None,
981 gemm1_alpha: float | None = None,
982 gemm1_beta: float | None = None,
983 gemm1_clamp_limit: float | None = None,
984 ) -> FusedMoEQuantConfig:
985 """Construct a quant config for unquantized activations with biases.
986
987 gemm1_alpha/gemm1_beta/gemm1_clamp_limit carry the SwiGLU gate params
988 through to the fused activation kernel (e.g. swigluoai_uninterleave).
989 """
990 return FusedMoEQuantConfig(
991 _a1=FusedMoEQuantDesc(),
992 _a2=FusedMoEQuantDesc(),
993 _w1=FusedMoEQuantDesc(bias=w1_bias),
994 _w2=FusedMoEQuantDesc(bias=w2_bias),
995 gemm1_alpha=gemm1_alpha,
996 gemm1_beta=gemm1_beta,
997 gemm1_clamp_limit=gemm1_clamp_limit,
998 )
999
1000
1001 # A FusedMoEQuantConfig constant for an unquantized MoE op.
1002 FUSED_MOE_UNQUANTIZED_CONFIG: FusedMoEQuantConfig = FusedMoEQuantConfig.make()
1003
1004
1005 @dataclass
1006 class FusedMoEParallelConfig:
1007 tp_size: int
1008 pcp_size: int
1009 dp_size: int
1010 ep_size: int
1011 tp_rank: int
1012 pcp_rank: int
1013 dp_rank: int
1014 ep_rank: int
1015 sp_size: int
1016
1017 use_ep: bool # whether to use EP or not
1018 all2all_backend: str # all2all backend for MoE communication
1019 enable_eplb: bool # whether to enable expert load balancing
1020
1021 @property
1022 def is_sequence_parallel(self) -> bool:
1023 return self.sp_size > 1
1024
1025 @property
1026 def use_all2all_kernels(self):
1027 return self.use_ep and (
1028 self.dp_size > 1 or self.pcp_size > 1 or self.is_sequence_parallel
1029 )
1030
1031 @property
1032 def use_deepep_ht_kernels(self):
1033 return (
1034 self.use_all2all_kernels
1035 and self.all2all_backend == "deepep_high_throughput"
1036 )
1037
1038 @property
1039 def use_deepep_ll_kernels(self):
1040 return self.use_all2all_kernels and self.all2all_backend == "deepep_low_latency"
1041
1042 @property
1043 def use_fi_nvl_two_sided_kernels(self):
1044 return self.use_all2all_kernels and (
1045 self.all2all_backend == "flashinfer_all2allv"
1046 or self.all2all_backend == "flashinfer_nvlink_two_sided"
1047 )
1048
1049 @property
1050 def use_fi_nvl_one_sided_kernels(self):
1051 return (
1052 self.use_all2all_kernels
1053 and self.all2all_backend == "flashinfer_nvlink_one_sided"
1054 )
1055
1056 @property
1057 def use_batched_activation_format(self):
1058 return self.use_deepep_ll_kernels or self.use_nixl_ep_kernels
1059
1060 @property
1061 def needs_round_robin_routing_tables(self):
1062 return self.use_deepep_ll_kernels or self.use_nixl_ep_kernels
1063
1064 @property
1065 def use_ag_rs_all2all_kernels(self):
1066 return (
1067 self.use_all2all_kernels
1068 and self.all2all_backend == "allgather_reducescatter"
1069 )
1070
1071 @property
1072 def use_mori_kernels(self):
1073 return self.use_all2all_kernels and self.all2all_backend in (
1074 "mori_high_throughput",
1075 "mori_low_latency",
1076 )
1077
1078 @property
1079 def use_nixl_ep_kernels(self):
1080 return self.use_all2all_kernels and self.all2all_backend == "nixl_ep"
1081
1082 @property
1083 def use_deepep_v2_kernels(self):
1084 return self.use_all2all_kernels and self.all2all_backend == "deepep_v2"
1085
1086 @property
1087 def use_moonep_kernels(self):
1088 return self.use_all2all_kernels and self.all2all_backend == "moonep"
1089
1090 @staticmethod
1091 def flatten_tp_across_dp_and_pcp(
1092 tp_size: int, dp_size: int, dp_rank: int, pcp_size: int, pcp_rank: int
1093 ) -> tuple[int, int]:
1094 tp_rank = 0 if tp_size == 1 else get_tensor_model_parallel_rank()
1095 # There are actually dp_size * pcp_size * tp_size devices.
1096 # Update tp_size and tp_rank so we shard across all devices.
1097 flatten_tp_size = dp_size * pcp_size * tp_size
1098 flatten_tp_rank = dp_rank * pcp_size * tp_size + pcp_rank * tp_size + tp_rank
1099 return flatten_tp_size, flatten_tp_rank
1100
1101 @staticmethod
1102 def make(
1103 tp_size_: int,
1104 pcp_size_: int,
1105 dp_size_: int,
1106 sp_size_: int,
1107 vllm_parallel_config: ParallelConfig,
1108 ) -> "FusedMoEParallelConfig":
1109 """Determine MoE parallel configuration. Based on the input `tp_size_`,
1110 `dp_size_` and vllm's parallel config, determine what
1111 level's of parallelism to use in the fused moe layer.
1112
1113 Args:
1114 tp_size_ (int): `tp_size` passed into the FusedMoEFactory constructor.
1115 pcp_size_ (int): `pcp_size` passed into the FusedMoEFactory constructor.
1116 dp_size_ (int): `dp_size` passed into the FusedMoEFactory constructor.
1117 sp_size_ (int): `sp_size` passed into the FusedMoEFactory constructor.
1118 vllm_parallel_config (ParallelConfig): vLLM's parallel config
1119 object which contains the `enable_expert_parallel` flag.
1120
1121 Examples:
1122 When there is no parallelism requested,
1123 i.e. `tp_size_` = `pcp_size_` = `dp_size_` = 1, we simply return the sizes
1124 unaltered and the ranks set to 0.
1125
1126 Expert Parallelism is considered only when either `dp_size_`, `pcp_size_` or
1127 `tp_size_` is non trivial.
1128
1129 Note that PCP serves the same function as DP here.
1130
1131 When TP = 2, DP(PCP) = 1 and EP = False, the configuration on different
1132 devices:
1133
1134 - device 0 : TP = {2, 0} DP = {1, 0} EP = {1, 0} //
1135 legend : {size, rank}
1136 - device 1 : TP = {2, 1} DP = {1, 0} EP = {1, 0}
1137 - Comment : Tensors are sharded across 2 devices.
1138
1139 When TP = 1, DP(PCP) = 2 and EP = False, the configuration on different
1140 devices:
1141
1142 - device 0 : TP = {2, 0} DP = {2, 0} EP = {1, 0}
1143 - device 1 : TP = {2, 1} DP = {2, 1} EP = {1, 0}
1144 - Comment: There are 2 engine instances and the tensors are sharded
1145 across 2 decvices.
1146
1147 When TP = 2, DP(PCP) = 2 and EP = False, the configuration on different
1148 devices:
1149
1150 - device 0: TP = {4, 0} DP = {2, 0} EP = {1, 0}
1151 - device 1: TP = {4, 1} DP = {2, 0} EP = {1, 0}
1152 - device 2: TP = {4, 2} DP = {2, 1} EP = {1, 0}
1153 - device 3: TP = {4, 3} DP = {2, 1} EP = {1, 0}
1154 - Comment: There are 2 engine instances and the tensors are sharded
1155 across 4 devices.
1156
1157 When, TP = 2, DP(PCP) = 1 and EP = True, the configuration on different
1158 devices:
1159
1160 - device 0: TP = {1, 0} DP = {1, 0} EP = {2, 0}
1161 - device 1: TP = {1, 0} DP = {1, 0} EP = {2, 1}
1162 - Comment: The experts are split between the 2 devices.
1163
1164 When, TP = 1, DP(PCP) = 2 and EP = True, the configuration on different
1165 devices:
1166
1167 - device 0: TP = {1, 0} DP = {2, 0} EP = {2, 0}
1168 - device 1: TP = {1, 0} DP = {2, 1} EP = {2, 1}
1169 - Comment: There are 2 engine instances and the experts are split
1170 between the 2 devices.
1171
1172 When TP = 2, DP(PCP) = 2 and EP = True, the configuration on different
1173 devices:
1174
1175 - device 0: TP = {1, 0} DP = {2, 0} EP = {4, 0}
1176 - device 1: TP = {1, 0} DP = {2, 0} EP = {4, 1}
1177 - device 2: TP = {1, 0} DP = {2, 1} EP = {4, 2}
1178 - device 3: TP = {1, 0} DP = {2, 1} EP = {4, 3}
1179 - Comment: There are 2 engine instances and the experts are split
1180 between the 4 devices.
1181
1182 """
1183 use_ep = (
1184 dp_size_ * pcp_size_ * tp_size_ > 1
1185 and vllm_parallel_config.enable_expert_parallel
1186 )
1187
1188 dp_size = dp_size_
1189 dp_rank = get_dp_group().rank_in_group if dp_size > 1 else 0
1190 pcp_size = pcp_size_
1191 pcp_rank = get_pcp_group().rank_in_group if pcp_size > 1 else 0
1192 tp_size, tp_rank = FusedMoEParallelConfig.flatten_tp_across_dp_and_pcp(
1193 tp_size_, dp_size_, dp_rank, pcp_size_, pcp_rank
1194 )
1195
1196 if not use_ep:
1197 return FusedMoEParallelConfig(
1198 tp_size=tp_size,
1199 tp_rank=tp_rank,
1200 pcp_size=pcp_size,
1201 pcp_rank=pcp_rank,
1202 dp_size=dp_size,
1203 dp_rank=dp_rank,
1204 ep_size=1,
1205 ep_rank=0,
1206 sp_size=sp_size_,
1207 use_ep=False,
1208 all2all_backend=vllm_parallel_config.all2all_backend,
1209 enable_eplb=vllm_parallel_config.enable_eplb,
1210 )
1211 # DP + EP / TP + EP / DP + TP + EP
1212 assert use_ep
1213 # In EP, each device owns a set of experts fully. There is no tensor
1214 # parallel update tp_size, tp_rank, ep_size and ep_rank to reflect that.
1215 ep_size = tp_size
1216 ep_rank = tp_rank
1217 return FusedMoEParallelConfig(
1218 tp_size=1,
1219 tp_rank=0,
1220 pcp_size=pcp_size,
1221 pcp_rank=pcp_rank,
1222 dp_size=dp_size,
1223 dp_rank=dp_rank,
1224 ep_size=ep_size,
1225 ep_rank=ep_rank,
1226 sp_size=sp_size_,
1227 use_ep=True,
1228 all2all_backend=vllm_parallel_config.all2all_backend,
1229 enable_eplb=vllm_parallel_config.enable_eplb,
1230 )
1231
1232 @classmethod
1233 def make_no_parallel(cls) -> "FusedMoEParallelConfig":
1234 """For usage in CI/CD and testing."""
1235 return FusedMoEParallelConfig(
1236 tp_size=1,
1237 tp_rank=0,
1238 pcp_size=1,
1239 pcp_rank=0,
1240 dp_size=1,
1241 dp_rank=0,
1242 ep_size=1,
1243 ep_rank=0,
1244 sp_size=1,
1245 use_ep=False,
1246 all2all_backend="allgather_reducescatter",
1247 enable_eplb=False,
1248 )
1249
1250
1251 # Model types validated for VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4=1. This flag
1252 # also changes the MXFP4 weight shuffle layout, so using it on other models
1253 # can produce garbled output.
1254 _AITER_MOE_A4W4_DSV4_VALIDATED_MODEL_TYPES = ("deepseek_v41", "deepseek_v41_text")
1255
1256
1257 # Adapted from pplx-kernels tests/all_to_all_utils.py
1258 @dataclass
1259 class FusedMoEConfig:
1260 num_experts: int
1261 experts_per_token: int
1262 hidden_dim: int
1263 intermediate_size: int
1264 num_local_experts: int
1265 num_logical_experts: int
1266 activation: MoEActivation
1267 device: torch.device | str
1268 routing_method: RoutingMethodType
1269 moe_parallel_config: FusedMoEParallelConfig
1270
1271 # The activation type.
1272 in_dtype: torch.dtype
1273
1274 # Defaults to in_dtype if not specified.
1275 router_logits_dtype: torch.dtype | None = None
1276
1277 # Defaults to hidden_dim if not specified.
1278 hidden_dim_unpadded: int | None = None
1279 # Defaults to intermediate_size_per_partition if not specified.
1280 intermediate_size_per_partition_unpadded: int | None = None
1281 # Model specific override
1282 intermediate_pad: int | None = None
1283
1284 moe_backend: MoEBackend = "auto"
1285 max_num_tokens: int = SchedulerConfig.DEFAULT_MAX_NUM_BATCHED_TOKENS_FOR_BATCHED_DP
1286 elastic_ep_max_dp_size: int | None = None
1287 has_bias: bool = False
1288 is_lora_enabled: bool = False
1289 has_hash_routing: bool = False
1290
1291 # When True, the MoE skips its final cross-rank all-reduce (and the separate
1292 # shared-expert reduce), returning the partial per-rank sum. The caller is
1293 # then responsible for the reduction (e.g. fusing it into the next RMSNorm).
1294 # Only honored on the non-reduced (late-AR) TP path. Default False.
1295 skip_final_all_reduce: bool = False
1296
1297 # Requested through `defer_moe_finalize()`, read through
1298 # `should_defer_moe_finalize()`.
1299 _defer_moe_finalize: bool = field(default=False, init=False)
1300 # Most tokens a deferred call covers: the consumer's capacity, lowered by
1301 # experts that would split a larger call across kernel launches. Negative
1302 # means unbounded.
1303 defer_moe_finalize_max_num_tokens: int = -1
1304
1305 # SwiGLU clamp limit. When set, backends that do not implement the clamp
1306 # are filtered out by `FusedMoEExperts.is_supported_config` so the oracle
1307 # cannot silently select one and drop the clamp.
1308 swiglu_limit: float | None = None
1309 swiglu_alpha: float | None = None
1310 swiglu_beta: float | None = None
1311
1312 # SituGLU parameters used by Kimi sit(u/v2) activations.
1313 activation_situ_beta: float | None = None
1314 activation_situ_linear_beta: float | None = None
1315
1316 max_capture_size: int = 0
1317
1318 # Set by __post_init__
1319 intermediate_size_per_partition: int = -1
1320 # Use the allocated width as the checkpoint TP stride, including for scales.
1321 tp_shard_with_padding: bool = False
1322 rocm_aiter_fmoe_enabled: bool = False
1323 aiter_fmoe_shared_expert_enabled: bool = False
1324 # Whether to force MXFP4 (a4w4) MoE activations for DeepSeek V4.1 on
1325 # ROCm/AITER. Opt-in via VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4=1; rejected
1326 # for any other model type. Resolved here, not in the forward path,
1327 # because get_current_vllm_config() isn't set there.
1328 use_mxfp4_w4a4_dsv4: bool = False
1329
1330 def __post_init__(self):
1331 from vllm._aiter_ops import rocm_aiter_ops
1332
1333 tp_size = self.moe_parallel_config.tp_size
1334 assert self.intermediate_size % tp_size == 0
1335 self.intermediate_size_per_partition = self.intermediate_size // tp_size
1336
1337 if self.dp_size > 1:
1338 logger.debug_once(
1339 "Using FusedMoEConfig::max_num_tokens=%d", self.max_num_tokens
1340 )
1341
1342 assert self.max_num_tokens > 0
1343
1344 if self.router_logits_dtype is None:
1345 self.router_logits_dtype = self.in_dtype
1346
1347 if self.hidden_dim_unpadded is None:
1348 self.hidden_dim_unpadded = self.hidden_dim
1349 if self.intermediate_size_per_partition_unpadded is None:
1350 self.intermediate_size_per_partition_unpadded = (
1351 self.intermediate_size_per_partition
1352 )
1353
1354 if self.is_act_and_mul:
1355 self.rocm_aiter_fmoe_enabled = rocm_aiter_ops.is_fused_moe_enabled()
1356 self.aiter_fmoe_shared_expert_enabled = (
1357 rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
1358 )
1359
1360 if self.rocm_aiter_fmoe_enabled and envs.VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4:
1361 from vllm.config import get_current_vllm_config_or_none
1362
1363 vllm_config = get_current_vllm_config_or_none()
1364 model_type = (
1365 getattr(vllm_config.model_config.hf_config, "model_type", None)
1366 if vllm_config is not None
1367 else None
1368 )
1369 if model_type not in _AITER_MOE_A4W4_DSV4_VALIDATED_MODEL_TYPES:
1370 raise ValueError(
1371 f"VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4=1 only supports "
1372 f"model_type in {_AITER_MOE_A4W4_DSV4_VALIDATED_MODEL_TYPES}, "
1373 f"got {model_type!r}. Unset this env var for this model."
1374 )
1375 if not rocm_aiter_ops.fused_moe_supports_quant_dtype_a():
1376 raise ValueError(
1377 "VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4=1 needs an AITER build "
1378 "with fused_moe(quant_dtype_a=...) support "
1379 "(ROCm/aiter#5439+). Upgrade AITER or unset this env var."
1380 )
1381 self.use_mxfp4_w4a4_dsv4 = True
1382
1383 if self.use_mori_kernels:
1384 assert self.rocm_aiter_fmoe_enabled, (
1385 "Mori needs to be used with aiter fused_moe for now."
1386 )
1387 assert not self.aiter_fmoe_shared_expert_enabled, (
1388 "Mori does not support fusion shared expert now. "
1389 "Turn it off by setting VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=0"
1390 )
1391
1392 if not self.is_act_and_mul and not (
1393 current_platform.is_cuda_alike() or current_platform.is_xpu()
1394 ):
1395 raise NotImplementedError(
1396 "is_act_and_mul=False is supported only for CUDA, XPU and ROCm for now"
1397 )
1398
1399 @property
1400 def is_act_and_mul(self) -> bool:
1401 return self.activation.is_gated
1402
1403 @property
1404 def w13_num_shards(self) -> int:
1405 """Number of shards fused into w13: gate and up for gated, up only."""
1406 return 2 if self.is_act_and_mul else 1
1407
1408 @property
1409 def tp_size(self):
1410 return self.moe_parallel_config.tp_size
1411
1412 @property
1413 def dp_size(self):
1414 return self.moe_parallel_config.dp_size
1415
1416 @property
1417 def pcp_size(self):
1418 return self.moe_parallel_config.pcp_size
1419
1420 @property
1421 def ep_size(self):
1422 return self.moe_parallel_config.ep_size
1423
1424 @property
1425 def sp_size(self):
1426 return self.moe_parallel_config.sp_size
1427
1428 @property
1429 def is_sequence_parallel(self):
1430 return self.moe_parallel_config.is_sequence_parallel
1431
1432 @property
1433 def tp_rank(self):
1434 return self.moe_parallel_config.tp_rank
1435
1436 @property
1437 def dp_rank(self):
1438 return self.moe_parallel_config.dp_rank
1439
1440 @property
1441 def pcp_rank(self):
1442 return self.moe_parallel_config.pcp_rank
1443
1444 @property
1445 def ep_rank(self):
1446 return self.moe_parallel_config.ep_rank
1447
1448 @property
1449 def use_ep(self):
1450 return self.moe_parallel_config.use_ep
1451
1452 @property
1453 def use_deferred_moe_finalize(self) -> bool:
1454 """Whether experts may return an unfinalized output on this deployment.
1455
1456 Evaluated on read rather than in ``__post_init__`` because deferral is
1457 requested after construction, like ``skip_final_all_reduce``.
1458 """
1459 # The consumer fuses a TP all-reduce. Other parallel modes require a
1460 # combine or reduce-scatter after the experts and cannot defer it, and
1461 # the consumer has no way to strip hidden-dim padding from GEMM2 rows.
1462 return (
1463 self._defer_moe_finalize
1464 and self.tp_size > 1
1465 and self.dp_size == 1
1466 and self.ep_size == 1
1467 and self.pcp_size == 1
1468 and not self.is_sequence_parallel
1469 and self.hidden_dim == self.hidden_dim_unpadded
1470 )
1471
1472 def defer_moe_finalize(self, max_num_tokens: int = -1) -> None:
1473 """Ask the experts to leave the top-k reduction to the layer's consumer.
1474
1475 A layer whose consumer can fuse the top-k reduction (e.g. into its TP
1476 all-reduce) calls this once while the model is built, and only when its
1477 experts are TRTLLM-Gen ones that can stop after GEMM2 (the
1478 ``do_finalize=False`` path). Other experts ignore the request and
1479 always finalize, which ``should_defer_moe_finalize`` can't see, so the
1480 layer checks the quant method's ``experts_cls`` first, as Kimi-K3 does.
1481 From then on:
1482
1483 - The experts return an ``UnfinalizedMoEOutput`` instead of finalized
1484 states for every call ``should_defer_moe_finalize`` accepts.
1485 - ``should_defer_moe_finalize(num_tokens)`` is the answer for a call:
1486 it requires a TP-only deployment without hidden-dim padding
1487 (``use_deferred_moe_finalize``), a non-empty call and at most
1488 ``defer_moe_finalize_max_num_tokens`` tokens. The cap starts at
1489 ``max_num_tokens`` and only ever goes down: experts that would split a
1490 larger call across kernel launches lower it to their single-launch
1491 size when they are built, since each launch permutes into its own
1492 buffer.
1493 - The model asks ``should_defer_moe_finalize`` before each call and takes
1494 the matching path. A deferred call runs the runner's ``_forward_impl``
1495 directly, since the MoE custom op returns tensors only, and the
1496 consumer then owns the top-k reduction, the shared-expert add and the
1497 all-reduce.
1498
1499 Args:
1500 max_num_tokens: Most tokens per call the consumer can take in
1501 deferred form. Negative means no limit of its own.
1502
1503 """
1504 self._defer_moe_finalize = True
1505 self.limit_deferred_moe_finalize(max_num_tokens)
1506
1507 def should_defer_moe_finalize(self, num_tokens: int) -> bool:
1508 """Return whether this invocation may defer the top-k reduction."""
1509 max_num_tokens = self.defer_moe_finalize_max_num_tokens
1510 return (
1511 self.use_deferred_moe_finalize
1512 and num_tokens > 0
1513 and (max_num_tokens < 0 or num_tokens <= max_num_tokens)
1514 )
1515
1516 def limit_deferred_moe_finalize(self, max_num_tokens: int) -> None:
1517 """Finalize calls above ``max_num_tokens`` even when deferring.
1518
1519 Negative means no limit, and leaves the current one in place.
1520 """
1521 current = self.defer_moe_finalize_max_num_tokens
1522 if max_num_tokens >= 0 and (current < 0 or max_num_tokens < current):
1523 self.defer_moe_finalize_max_num_tokens = max_num_tokens
1524
1525 @property
1526 def use_deepep_ht_kernels(self):
1527 return self.moe_parallel_config.use_deepep_ht_kernels
1528
1529 @property
1530 def use_deepep_ll_kernels(self):
1531 return self.moe_parallel_config.use_deepep_ll_kernels
1532
1533 @property
1534 def use_mori_kernels(self):
1535 return self.moe_parallel_config.use_mori_kernels
1536
1537 @property
1538 def use_fi_nvl_two_sided_kernels(self):
1539 return self.moe_parallel_config.use_fi_nvl_two_sided_kernels
1540
1541 @property
1542 def use_fi_nvl_one_sided_kernels(self):
1543 return self.moe_parallel_config.use_fi_nvl_one_sided_kernels
1544
1545 @property
1546 def use_ag_rs_all2all_kernels(self):
1547 return self.moe_parallel_config.use_ag_rs_all2all_kernels
1548
1549 @property
1550 def use_nixl_ep_kernels(self):
1551 return self.moe_parallel_config.use_nixl_ep_kernels
1552
1553 @property
1554 def use_deepep_v2_kernels(self):
1555 return self.moe_parallel_config.use_deepep_v2_kernels
1556
1557 @property
1558 def use_moonep_kernels(self):
1559 return self.moe_parallel_config.use_moonep_kernels
1560
1561 @property
1562 def needs_round_robin_routing_tables(self):
1563 return self.moe_parallel_config.needs_round_robin_routing_tables