flashinfer_cutlass_moe.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. import torch
  4. from vllm.model_executor.utils import is_weights_pre_processed
  5. import vllm.model_executor.layers.fused_moe.modular_kernel as mk
  6. from vllm.logger import init_logger
  7. from vllm.model_executor.layers.fused_moe.activation import MoEActivation
  8. from vllm.model_executor.layers.fused_moe.config import (
  9. FusedMoEParallelConfig,
  10. FusedMoEQuantConfig,
  11. )
  12. from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
  13. TopKWeightAndReduceNoOP,
  14. )
  15. from vllm.model_executor.layers.quantization.utils.flashinfer_utils import (
  16. activation_to_flashinfer_type,
  17. )
  18. from vllm.model_executor.layers.quantization.utils.quant_utils import (
  19. QuantKey,
  20. kFp8Dynamic128Sym,
  21. kFp8Static128BlockSym,
  22. kFp8StaticTensorSym,
  23. kMxfp4Static,
  24. kMxfp8Dynamic,
  25. kNvfp4Dynamic,
  26. kNvfp4Static,
  27. )
  28. from vllm.platforms import current_platform
  29. from vllm.utils.flashinfer import (
  30. flashinfer_cutlass_fused_moe,
  31. has_flashinfer_cutlass_fused_moe,
  32. )
  33. logger = init_logger(__name__)
  34. def is_valid_flashinfer_cutlass_fused_moe(
  35. hidden_states: torch.Tensor, w1: torch.Tensor, w2: torch.Tensor
  36. ) -> bool:
  37. """Check if the given problem size is supported by the FlashInfer CUTLASS MoE
  38. kernel.
  39. """
  40. if not has_flashinfer_cutlass_fused_moe():
  41. logger.debug_once(
  42. "FlashInferExperts disabled: flashinfer_cutlass_fused_moe not available."
  43. )
  44. return False
  45. # Data type checks
  46. if (
  47. w1.dtype != torch.uint8
  48. or w2.dtype != torch.uint8
  49. or hidden_states.dtype not in [torch.float32, torch.float16, torch.bfloat16]
  50. ):
  51. logger.debug_once(
  52. "FlashInferExperts disabled: w1/w2 must be torch.uint8 "
  53. f"(got w1={w1.dtype}, w2={w2.dtype}), hidden_states must be "
  54. f"float32, float16, or bfloat16 (got {hidden_states.dtype})."
  55. )
  56. return False
  57. return True
  58. class FlashInferExperts(mk.FusedMoEExpertsModular):
  59. def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
  60. # The fold is in place, so weights that already went through it (the
  61. # weight cache and snapshot loaders) must not be folded again.
  62. if self.quant_config.use_nvfp4_w4a4 and not is_weights_pre_processed():
  63. layer.w13_weight_scale_2.data.mul_(layer.w13_input_scale)
  64. layer.w2_weight_scale_2.data.mul_(layer.w2_input_scale)
  65. def __init__(
  66. self,
  67. moe_config: mk.FusedMoEConfig,
  68. quant_config: FusedMoEQuantConfig,
  69. ):
  70. super().__init__(moe_config, quant_config)
  71. assert quant_config.weight_quant_dtype in (
  72. "mxfp4",
  73. "nvfp4",
  74. torch.float8_e4m3fn,
  75. None,
  76. ), (
  77. "Only mxfp4, nvfp4, fp8, bfloat16 and"
  78. " float16 quantization are currently supported."
  79. )
  80. self.device = moe_config.device
  81. self.num_experts = moe_config.num_local_experts
  82. self.ep_rank = moe_config.moe_parallel_config.ep_rank
  83. self.ep_size = moe_config.moe_parallel_config.ep_size
  84. self.tp_rank = moe_config.moe_parallel_config.tp_rank
  85. self.tp_size = moe_config.moe_parallel_config.tp_size
  86. self.out_dtype = moe_config.in_dtype
  87. self.use_dp = moe_config.moe_parallel_config.dp_size > 1
  88. # Enables DeepSeek-style FP8 block-scale path:
  89. # - pass per-block weight scales to the kernel
  90. # - skip input activation quantization (kernel applies scaling)
  91. self.use_deepseek_fp8_block_scale = quant_config.is_block_quantized
  92. def _per_expert(value: float | None) -> torch.Tensor | None:
  93. if value is None:
  94. return None
  95. return torch.full(
  96. (self.num_experts,),
  97. float(value),
  98. dtype=torch.float32,
  99. device=self.device,
  100. )
  101. self.gemm1_clamp_limit = _per_expert(quant_config.gemm1_clamp_limit)
  102. self.gemm1_alpha = _per_expert(quant_config.gemm1_alpha)
  103. self.gemm1_beta = _per_expert(quant_config.gemm1_beta)
  104. if (
  105. quant_config.weight_quant_dtype == "mxfp4"
  106. and quant_config.quant_dtype == "mxfp8"
  107. ):
  108. self.fake_input_scale = torch.ones(
  109. self.num_experts,
  110. device=self.device,
  111. dtype=torch.float32,
  112. )
  113. @property
  114. def expects_unquantized_inputs(self) -> bool:
  115. return self.quant_config.use_fp8_w8a8 and self.quant_config.is_block_quantized
  116. @staticmethod
  117. def _supports_current_device() -> bool:
  118. p = current_platform
  119. return (
  120. p.is_cuda()
  121. and (
  122. p.is_device_capability(90)
  123. or p.is_device_capability_family(100)
  124. # SM110 excluded: flashinfer-ai/flashinfer#3134
  125. or p.is_device_capability_family(120)
  126. )
  127. and has_flashinfer_cutlass_fused_moe()
  128. )
  129. @staticmethod
  130. def _supports_no_act_and_mul() -> bool:
  131. return True
  132. @staticmethod
  133. def _supports_quant_scheme(
  134. weight_key: QuantKey | None,
  135. activation_key: QuantKey | None,
  136. ) -> bool:
  137. p = current_platform
  138. scheme = (weight_key, activation_key)
  139. # The following are supported by FlashInferExperts:
  140. return (
  141. # unquantized and fp8 static per-tensor on 9.0+
  142. (
  143. scheme
  144. in [
  145. (None, None),
  146. (kFp8StaticTensorSym, kFp8StaticTensorSym),
  147. ]
  148. and p.has_device_capability(90)
  149. )
  150. # fp8 block-scale, wmxfp4a16 on 9.0
  151. or (
  152. scheme
  153. in [
  154. (kMxfp4Static, None),
  155. (kFp8Static128BlockSym, kFp8Dynamic128Sym),
  156. ]
  157. and p.is_device_capability(90)
  158. )
  159. # nvfp4, wmxfp4amxfp8 on 10.0+
  160. or (
  161. scheme
  162. in [
  163. (kMxfp4Static, kMxfp8Dynamic),
  164. (kNvfp4Static, kNvfp4Dynamic),
  165. ]
  166. and p.has_device_capability(100)
  167. )
  168. )
  169. @staticmethod
  170. def _supports_activation(activation: MoEActivation) -> bool:
  171. return activation in [
  172. MoEActivation.SILU,
  173. MoEActivation.GELU_TANH,
  174. MoEActivation.RELU2_NO_MUL,
  175. MoEActivation.SWIGLUOAI,
  176. MoEActivation.SWIGLUOAI_UNINTERLEAVE,
  177. ]
  178. @staticmethod
  179. def _supports_parallel_config(moe_parallel_config: FusedMoEParallelConfig) -> bool:
  180. # FLASHINFER_CUTLASS currently uses its down P/F, which does not
  181. # work with SP. This will be removed in follow up after we get
  182. # rid of the FlashInfer specific P/F function.
  183. # TODO: the per-tensor fp8 kernels don't work with MNNVL FI A2As.
  184. return True
  185. @staticmethod
  186. def activation_format() -> mk.FusedMoEActivationFormat:
  187. return mk.FusedMoEActivationFormat.Standard
  188. def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
  189. return TopKWeightAndReduceNoOP()
  190. def workspace_shapes(
  191. self,
  192. M: int,
  193. N: int,
  194. K: int,
  195. topk: int,
  196. global_num_experts: int,
  197. local_num_experts: int,
  198. expert_tokens_meta: mk.ExpertTokensMetadata | None,
  199. activation: MoEActivation,
  200. ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
  201. # We use global_num_experts due to how moe_align_block_size handles
  202. # expert_maps.
  203. """Compute the shapes for the temporary and final outputs of the two gemms
  204. and activation in the fused expert function. Since the gemms are
  205. independent, the workspace for the first gemm can be shared with the
  206. workspace for the last gemm.
  207. Returns a tuple of:
  208. - workspace13 shape tuple: must be large enough to hold the
  209. result of either expert gemm.
  210. - workspace2 shape tuple: must be large enough to hold the
  211. result of the activation function.
  212. - output shape tuple: must be exact size of the final gemm output.
  213. - Workspace type: The dtype to use for the workspace tensors.
  214. - Note: in order for activation chunking to work, the first dimension
  215. of each tuple must be the number of tokens.
  216. """
  217. workspace1 = (M, K)
  218. workspace2 = (0,)
  219. # For NVFP4, the output is stored in a packed int8 format,
  220. # so the actual hidden dim is 2x the size of K here.
  221. output_shape = (M, K * 2 if self.quant_dtype == "nvfp4" else K)
  222. # The workspace is determined by `aq`, since it comes after any
  223. # potential communication op and is involved in the expert computation.
  224. return (workspace1, workspace2, output_shape)
  225. def apply(
  226. self,
  227. output: torch.Tensor,
  228. hidden_states: torch.Tensor,
  229. w1: torch.Tensor,
  230. w2: torch.Tensor,
  231. topk_weights: torch.Tensor,
  232. topk_ids: torch.Tensor,
  233. activation: MoEActivation,
  234. global_num_experts: int,
  235. expert_map: torch.Tensor | None,
  236. a1q_scale: torch.Tensor | None,
  237. a2_scale: torch.Tensor | None,
  238. workspace13: torch.Tensor | None,
  239. workspace2: torch.Tensor | None,
  240. expert_tokens_meta: mk.ExpertTokensMetadata | None,
  241. apply_router_weight_on_input: bool | None,
  242. ):
  243. quant_scales = None
  244. fc1_expert_weights = None
  245. fc2_expert_weights = None
  246. fc1_expert_biases = None
  247. fc2_expert_biases = None
  248. swiglu_alpha = None
  249. swiglu_beta = None
  250. swiglu_limit = None
  251. if activation == MoEActivation.SILU:
  252. swiglu_limit = self.gemm1_clamp_limit
  253. elif activation in (
  254. MoEActivation.SWIGLUOAI,
  255. MoEActivation.SWIGLUOAI_UNINTERLEAVE,
  256. ):
  257. swiglu_alpha = self.gemm1_alpha
  258. swiglu_beta = self.gemm1_beta
  259. swiglu_limit = self.gemm1_clamp_limit
  260. use_mxfp8_act_scaling = False
  261. use_w4_group_scaling = False
  262. # Select quantization metadata based on FP8 format/path
  263. if (
  264. self.quant_dtype == torch.float8_e4m3fn
  265. and not self.use_deepseek_fp8_block_scale
  266. ):
  267. # FP8 per-tensor path: use global alphas/scales; do not pass input_sf
  268. quant_scales = [
  269. self.g1_alphas, # w13_weight_scale * w13_input_scale
  270. self.a2_gscale, # 1.0 / w2_input_scale
  271. self.g2_alphas, # w2_weight_scale * w2_input_scale
  272. self.a1_scale,
  273. ]
  274. a1q_scale = None # not passing input_sf in fp8
  275. fc1_expert_weights = w1
  276. fc2_expert_weights = w2
  277. elif self.quant_dtype == "nvfp4":
  278. # Ensure w1_scale and w2_scale are not None before calling view
  279. assert self.w1_scale is not None and self.w2_scale is not None, (
  280. "w1_scale and w2_scale must not be None for FlashInferExperts"
  281. )
  282. # Flashinfer CUTLASS kernel takes scalar global scales,
  283. # min because inv_scale.
  284. quant_scales = [
  285. self.a1_gscale,
  286. self.w1_scale.view(torch.int32),
  287. self.g1_alphas,
  288. self.a2_gscale,
  289. self.w2_scale.view(torch.int32),
  290. self.g2_alphas,
  291. ]
  292. # FlashInfer API requires weight to be long for nvfp4
  293. fc1_expert_weights = w1.view(torch.long)
  294. fc2_expert_weights = w2.view(torch.long)
  295. elif self.weight_quant_dtype == "mxfp4":
  296. assert self.w1_scale is not None and self.w2_scale is not None
  297. assert w1.is_contiguous() and w2.is_contiguous()
  298. assert topk_ids.is_contiguous()
  299. fc1_expert_biases = self.w1_bias
  300. fc2_expert_biases = self.w2_bias
  301. swiglu_alpha = self.gemm1_alpha
  302. swiglu_beta = self.gemm1_beta
  303. swiglu_limit = self.gemm1_clamp_limit
  304. if self.quant_dtype == "mxfp8":
  305. assert self.fake_input_scale is not None
  306. fc1_expert_weights = w1.view(torch.long)
  307. fc2_expert_weights = w2.view(torch.long)
  308. quant_scales = [
  309. self.w1_scale.view(torch.int32),
  310. self.fake_input_scale,
  311. self.w2_scale.view(torch.int32),
  312. self.fake_input_scale,
  313. ]
  314. use_mxfp8_act_scaling = True
  315. else:
  316. assert hidden_states.dtype == torch.bfloat16
  317. fc1_expert_weights = w1
  318. fc2_expert_weights = w2
  319. quant_scales = [
  320. self.w1_scale,
  321. self.w2_scale,
  322. ]
  323. a1q_scale = None
  324. use_w4_group_scaling = True
  325. elif self.use_deepseek_fp8_block_scale:
  326. # FP8 block-scale path: provide block-scale weights, omit a1q_scale
  327. quant_scales = [
  328. self.w1_scale,
  329. self.w2_scale,
  330. ]
  331. a1q_scale = None
  332. fc1_expert_weights = w1
  333. fc2_expert_weights = w2
  334. else:
  335. quant_scales = []
  336. a1q_scale = None
  337. fc1_expert_weights = w1
  338. fc2_expert_weights = w2
  339. _ = flashinfer_cutlass_fused_moe(
  340. input=hidden_states,
  341. token_selected_experts=topk_ids.to(torch.int),
  342. token_final_scales=topk_weights,
  343. fc1_expert_weights=fc1_expert_weights,
  344. fc2_expert_weights=fc2_expert_weights,
  345. fc1_expert_biases=fc1_expert_biases,
  346. fc2_expert_biases=fc2_expert_biases,
  347. swiglu_alpha=swiglu_alpha,
  348. swiglu_beta=swiglu_beta,
  349. swiglu_limit=swiglu_limit,
  350. output=output,
  351. output_dtype=self.out_dtype,
  352. quant_scales=quant_scales,
  353. input_sf=a1q_scale,
  354. tp_size=self.tp_size,
  355. tp_rank=self.tp_rank,
  356. ep_size=self.ep_size,
  357. ep_rank=self.ep_rank,
  358. activation_type=activation_to_flashinfer_type(activation),
  359. # Informs FlashInfer to use the block-scale decoding path when True
  360. use_deepseek_fp8_block_scale=self.use_deepseek_fp8_block_scale,
  361. use_mxfp8_act_scaling=use_mxfp8_act_scaling,
  362. use_w4_group_scaling=use_w4_group_scaling,
  363. )
  364. def moe_sum(self, input: torch.Tensor, output: torch.Tensor) -> None:
  365. # No support for LoRA in flashinfer_cutlass_fused_moe.
  366. # See TODOs in flashinfer functions runMoe and runMoeMinLatency.
  367. raise NotImplementedError("LoRA is not supported for flashinfer_cutlass_moe")