main
py 450 lines 16.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """MoE activation function enum and utilities."""
4
5 from dataclasses import dataclass
6 from enum import Enum
7 from typing import TYPE_CHECKING
8
9 import torch
10 import torch.nn.functional as F
11
12 from vllm.platforms import current_platform
13
14 if TYPE_CHECKING:
15 from vllm.model_executor.layers.fused_moe.config import (
16 FusedMoEConfig,
17 FusedMoEQuantConfig,
18 )
19
20
21 class MoEActivation(Enum):
22 """Activation functions for MoE layers."""
23
24 # Gated activations (gate * activation(up)) expect input of shape [..., 2*d]
25 # and produce output of shape [..., d]
26 SILU = "silu"
27 GELU = "gelu"
28 GELU_TANH = "gelu_tanh"
29 RELU2 = "relu2"
30 # SWIGLUOAI expects gate/up *interleaved* in w13 ([gate0, up0, gate1, ...]),
31 # as in gpt-oss checkpoints. SWIGLUOAI_UNINTERLEAVE has identical math but
32 # expects the *packed* layout ([all gates; all ups]), as produced by a
33 # MergedColumnParallelLinear gate_up_proj (e.g. MiniMax-M3).
34 SWIGLUOAI = "swigluoai"
35 SITU = "situ"
36 SWIGLUOAI_UNINTERLEAVE = "swigluoai_uninterleave"
37 SWIGLUSTEP = "swiglustep"
38
39 # Non-gated activations (no mul with gate) expect input of shape [..., d]
40 # and produce output of shape [..., d].
41 # NOTE: Non-gated activations require the "_no_mul" suffix to be present.
42 SILU_NO_MUL = "silu_no_mul"
43 GELU_NO_MUL = "gelu_no_mul"
44 GELU_TANH_NO_MUL = "gelu_tanh_no_mul"
45 RELU2_NO_MUL = "relu2_no_mul"
46
47 @property
48 def is_gated(self) -> bool:
49 """Returns True if activation expects gate*activation(up) pattern.
50
51 Gated activations expect input tensor with 2x the output size,
52 where the first half is the gate and second half is the up projection.
53 """
54 return not self.value.endswith("_no_mul")
55
56 @property
57 def custom_op_name(self) -> str:
58 """Maps to the CustomOp name of activations
59 in vllm/model_executor/layers/activation.py."""
60 return _CUSTOM_OP_NAMES[self]
61
62 def without_mul(self) -> "MoEActivation":
63 """Get the non-gated variant of this activation.
64
65 For activations that have a _no_mul variant, returns that variant.
66 For activations without a _no_mul variant (or already _no_mul),
67 returns self.
68 """
69 return _WITHOUT_MUL.get(self, self)
70
71 @classmethod
72 def from_str(cls, s: str) -> "MoEActivation":
73 """Parse from string for backward compatibility."""
74 s = _STR_ALIASES.get(s, s)
75 for member in cls:
76 if member.value == s:
77 return member
78 valid = [m.value for m in cls]
79 raise ValueError(f"Unknown MoE activation: {s!r}. Valid activations: {valid}")
80
81
82 # Module-level lookup tables used by MoEActivation functions.
83 _STR_ALIASES: dict[str, str] = {
84 "gelu_pytorch_tanh": "gelu_tanh",
85 }
86
87 _CUSTOM_OP_NAMES: dict[MoEActivation, str] = {
88 MoEActivation.SILU: "silu_and_mul",
89 MoEActivation.GELU: "gelu_and_mul",
90 MoEActivation.GELU_TANH: "gelu_tanh_and_mul",
91 MoEActivation.SITU: "situ_and_mul",
92 MoEActivation.SWIGLUOAI: "swigluoai_and_mul",
93 MoEActivation.SWIGLUOAI_UNINTERLEAVE: "silu_and_mul_with_clamp",
94 MoEActivation.SWIGLUSTEP: "swiglustep_and_mul",
95 MoEActivation.RELU2: "relu2",
96 MoEActivation.SILU_NO_MUL: "silu_and_mul",
97 MoEActivation.GELU_NO_MUL: "gelu_and_mul",
98 MoEActivation.GELU_TANH_NO_MUL: "gelu_tanh_and_mul",
99 MoEActivation.RELU2_NO_MUL: "relu2",
100 }
101
102 _WITHOUT_MUL: dict[MoEActivation, MoEActivation] = {
103 MoEActivation.SILU: MoEActivation.SILU_NO_MUL,
104 MoEActivation.GELU: MoEActivation.GELU_NO_MUL,
105 MoEActivation.GELU_TANH: MoEActivation.GELU_TANH_NO_MUL,
106 MoEActivation.RELU2: MoEActivation.RELU2_NO_MUL,
107 }
108
109
110 def activation_without_mul(activation: str) -> str:
111 """Get the non-gated variant of an activation function.
112
113 Args:
114 activation: The activation function name (e.g., "silu", "gelu")
115
116 Returns:
117 The non-gated activation name (e.g., "silu_no_mul", "gelu_no_mul")
118
119 """
120 return MoEActivation.from_str(activation).without_mul().value
121
122
123 _APPLY_MOE_ACTIVATIONS = frozenset(
124 {
125 MoEActivation.SILU,
126 MoEActivation.GELU,
127 MoEActivation.GELU_TANH,
128 MoEActivation.SITU,
129 MoEActivation.SWIGLUOAI,
130 MoEActivation.SWIGLUOAI_UNINTERLEAVE,
131 MoEActivation.SWIGLUSTEP,
132 MoEActivation.SILU_NO_MUL,
133 MoEActivation.GELU_NO_MUL,
134 MoEActivation.GELU_TANH_NO_MUL,
135 MoEActivation.RELU2_NO_MUL,
136 }
137 )
138
139
140 _MASKED_MOE_ACTIVATION_NAMES: dict[MoEActivation, str] = {
141 MoEActivation.SILU: "silu",
142 MoEActivation.GELU: "gelu",
143 MoEActivation.GELU_TANH: "gelu_tanh",
144 MoEActivation.SITU: "situ",
145 MoEActivation.SWIGLUOAI: "swigluoai",
146 MoEActivation.SWIGLUOAI_UNINTERLEAVE: "swigluoai_uninterleave",
147 MoEActivation.SWIGLUSTEP: "swiglustep",
148 MoEActivation.SILU_NO_MUL: "silu_no_mul",
149 MoEActivation.GELU_NO_MUL: "gelu_no_mul",
150 MoEActivation.GELU_TANH_NO_MUL: "gelu_tanh_no_mul",
151 MoEActivation.RELU2_NO_MUL: "relu2_no_mul",
152 }
153
154
155 def apply_moe_activation_supported(activation: MoEActivation) -> bool:
156 """Whether ``apply_moe_activation`` supports an activation."""
157 return activation in _APPLY_MOE_ACTIVATIONS
158
159
160 def apply_moe_activation_masked_supported(activation: MoEActivation) -> bool:
161 """Whether the masked ``apply_moe_activation`` path supports an activation."""
162 return activation in _MASKED_MOE_ACTIVATION_NAMES
163
164
165 @dataclass(frozen=True)
166 class ApplyMoEActivationConfig:
167 """Configuration forwarded to ``apply_moe_activation``."""
168
169 clamp_limit: float | None = None
170 alpha: float = 1.0
171 beta: float = 0.0
172 activation_situ_beta: float | None = None
173 activation_situ_linear_beta: float | None = None
174
175 @classmethod
176 def from_configs(
177 cls,
178 moe_config: "FusedMoEConfig",
179 quant_config: "FusedMoEQuantConfig",
180 ) -> "ApplyMoEActivationConfig":
181 """Build from the model and quantization configurations."""
182 clamp_limit = quant_config.gemm1_clamp_limit
183 if clamp_limit is None:
184 clamp_limit = moe_config.swiglu_limit
185 alpha = quant_config.gemm1_alpha
186 if alpha is None:
187 alpha = moe_config.swiglu_alpha
188 beta = quant_config.gemm1_beta
189 if beta is None:
190 beta = moe_config.swiglu_beta
191 return cls(
192 clamp_limit=clamp_limit,
193 alpha=1.0 if alpha is None else alpha,
194 beta=0.0 if beta is None else beta,
195 activation_situ_beta=moe_config.activation_situ_beta,
196 activation_situ_linear_beta=moe_config.activation_situ_linear_beta,
197 )
198
199
200 _DEFAULT_APPLY_MOE_ACTIVATION_CONFIG = ApplyMoEActivationConfig()
201
202
203 def _validate_moe_activation_shapes(
204 activation: MoEActivation,
205 output: torch.Tensor,
206 input: torch.Tensor,
207 expected_dim: int,
208 ) -> None:
209 assert input.dim() == expected_dim, f"Input must be {expected_dim}D"
210 assert output.dim() == expected_dim, f"Output must be {expected_dim}D"
211 assert input.shape[:-1] == output.shape[:-1], (
212 f"Input/output leading shapes must match: {input.shape} vs {output.shape}"
213 )
214 if activation.is_gated:
215 assert output.size(-1) * 2 == input.size(-1), (
216 f"{activation.value} expects 2x ratio: "
217 f"{output.size(-1) * 2} vs {input.size(-1)}"
218 )
219 else:
220 assert output.size(-1) == input.size(-1), (
221 f"{activation.value} expects equal sizes: "
222 f"{output.size(-1)} vs {input.size(-1)}"
223 )
224
225
226 def _apply_moe_activation_masked(
227 activation: MoEActivation,
228 output: torch.Tensor,
229 input: torch.Tensor,
230 valid_token_counts: torch.Tensor,
231 config: ApplyMoEActivationConfig,
232 ) -> torch.Tensor:
233 masked_activation = _MASKED_MOE_ACTIVATION_NAMES.get(activation)
234 if masked_activation is None:
235 raise NotImplementedError(
236 f"Masked MoE activation is not implemented for {activation.value}"
237 )
238
239 assert input.dim() in (2, 3), "Masked input must be 2D or 3D"
240 _validate_moe_activation_shapes(activation, output, input, expected_dim=input.dim())
241 assert input.dtype == output.dtype, "Input and output dtypes must match"
242 assert input.device == output.device, "Input and output devices must match"
243 assert input.is_contiguous(), "Input must be contiguous"
244 assert output.is_contiguous(), "Output must be contiguous"
245 assert valid_token_counts.dtype == torch.int32, (
246 "valid_token_counts must use torch.int32"
247 )
248 assert valid_token_counts.dim() == 1, "valid_token_counts must be 1D"
249 assert valid_token_counts.device == input.device, (
250 "valid_token_counts must be on the input device"
251 )
252 assert valid_token_counts.is_contiguous(), "valid_token_counts must be contiguous"
253 expected_counts = input.size(0) if input.dim() == 3 else 1
254 assert valid_token_counts.size(0) == expected_counts, (
255 f"valid_token_counts must have {expected_counts} element(s) for "
256 f"{input.dim()}D input"
257 )
258
259 masked_clamp_limit = 0.0 if config.clamp_limit is None else config.clamp_limit
260 masked_alpha = config.alpha
261 if activation == MoEActivation.SILU and config.clamp_limit is not None:
262 masked_activation = "silu_with_clamp"
263 elif activation == MoEActivation.SITU:
264 assert config.activation_situ_beta is not None, (
265 "SITU requires activation_situ_beta from FusedMoEConfig"
266 )
267 elif activation == MoEActivation.SWIGLUOAI:
268 masked_clamp_limit = 7.0
269 masked_alpha = 1.702
270 elif activation == MoEActivation.SWIGLUOAI_UNINTERLEAVE:
271 assert config.clamp_limit is not None, (
272 "SWIGLUOAI_UNINTERLEAVE requires clamp_limit"
273 )
274 elif activation == MoEActivation.SWIGLUSTEP:
275 masked_clamp_limit = 7.0
276
277 torch.ops._C.masked_moe_activation(
278 output,
279 input,
280 valid_token_counts,
281 masked_activation,
282 masked_clamp_limit,
283 masked_alpha,
284 config.beta,
285 1.0 if config.activation_situ_beta is None else config.activation_situ_beta,
286 -1.0
287 if config.activation_situ_linear_beta is None
288 else config.activation_situ_linear_beta,
289 )
290 return output
291
292
293 def silu_and_mul_with_clamp(
294 output: torch.Tensor,
295 input: torch.Tensor,
296 clamp_limit: float,
297 topk_ids: torch.Tensor | None = None,
298 expert_map: torch.Tensor | None = None,
299 ) -> None:
300 if current_platform.is_xpu():
301 d = input.shape[-1] // 2
302 gate = torch.clamp(input[..., :d], max=clamp_limit)
303 up = torch.clamp(input[..., d:], min=-clamp_limit, max=clamp_limit)
304 output.copy_(gate * torch.sigmoid(gate) * up)
305 elif topk_ids is not None and expert_map is not None:
306 from vllm.model_executor.layers.fused_moe.utils import swiglu_limit_func
307
308 swiglu_limit_func(output, input, clamp_limit, topk_ids, expert_map)
309 else:
310 # Fused silu(clamp(gate)) * clamp(up); equivalent to swiglu_limit_func.
311 torch.ops._C.silu_and_mul_with_clamp(output, input, clamp_limit, 1.0, 0.0)
312
313
314 def apply_moe_activation(
315 activation: MoEActivation,
316 output: torch.Tensor,
317 input: torch.Tensor,
318 *,
319 activation_config: ApplyMoEActivationConfig | None = None,
320 topk_ids: torch.Tensor | None = None,
321 expert_map: torch.Tensor | None = None,
322 valid_token_counts: torch.Tensor | None = None,
323 ) -> torch.Tensor:
324 """Apply MoE activation function.
325
326 The configuration drives specialized activation behavior. Routing tensors
327 and valid token counts remain per-call inputs because they depend on the
328 current token assignment. A single token count masks a flat ``[T, D]``
329 buffer; one count per expert masks each prefix in a padded ``[E, T, D]``
330 buffer.
331 """
332 config = (
333 _DEFAULT_APPLY_MOE_ACTIVATION_CONFIG
334 if activation_config is None
335 else activation_config
336 )
337
338 if valid_token_counts is not None:
339 return _apply_moe_activation_masked(
340 activation,
341 output,
342 input,
343 valid_token_counts,
344 config,
345 )
346
347 _validate_moe_activation_shapes(activation, output, input, expected_dim=2)
348
349 # Activations with gated multiplication (gate × activation(up))
350 if activation == MoEActivation.SILU:
351 if config.clamp_limit is not None:
352 silu_and_mul_with_clamp(
353 output, input, config.clamp_limit, topk_ids, expert_map
354 )
355 else:
356 torch.ops._C.silu_and_mul(output, input)
357 elif activation == MoEActivation.GELU:
358 torch.ops._C.gelu_and_mul(output, input)
359 elif activation == MoEActivation.GELU_TANH:
360 torch.ops._C.gelu_tanh_and_mul(output, input)
361 elif activation == MoEActivation.SITU:
362 # Fused CUDA kernel: writes straight to `output`, no fp32 temporaries.
363 # (The pure-torch fallback below upcast both halves to fp32 and
364 # allocated ~8 temporaries per call, blowing up MoE memory.)
365 # Both betas come from FusedMoEConfig; a missing beta means the caller
366 # bypassed the config plumbing, so fail rather than silently use 1.0.
367 # linear_beta is genuinely optional: <= 0 signals "unset" to the kernel
368 # (up passed through), matching SituAndMul(linear_beta=None).
369 assert config.activation_situ_beta is not None, (
370 "SITU requires activation_situ_beta from FusedMoEConfig"
371 )
372 torch.ops._C.situ_and_mul(
373 output,
374 input,
375 config.activation_situ_beta,
376 -1.0
377 if config.activation_situ_linear_beta is None
378 else config.activation_situ_linear_beta,
379 )
380 elif activation == MoEActivation.SWIGLUOAI:
381 torch.ops._C.swigluoai_and_mul(output, input)
382 elif activation == MoEActivation.SWIGLUOAI_UNINTERLEAVE:
383 # SwiGLU-OAI on packed w13 (gate = first half, up = second half).
384 assert config.clamp_limit is not None, (
385 "SWIGLUOAI_UNINTERLEAVE requires clamp_limit"
386 )
387 torch.ops._C.silu_and_mul_with_clamp(
388 output, input, config.clamp_limit, config.alpha, config.beta
389 )
390 elif activation == MoEActivation.SWIGLUSTEP:
391 from vllm.model_executor.layers.activation import swiglustep_and_mul_triton
392
393 swiglustep_and_mul_triton(output, input)
394
395 # Activations without gated multiplication
396 elif activation == MoEActivation.SILU_NO_MUL:
397 output.copy_(F.silu(input))
398 elif activation == MoEActivation.GELU_NO_MUL:
399 output.copy_(F.gelu(input))
400 elif activation == MoEActivation.GELU_TANH_NO_MUL:
401 output.copy_(F.gelu(input, approximate="tanh"))
402 elif activation == MoEActivation.RELU2_NO_MUL:
403 F.relu(input, inplace=True)
404 torch.square(input, out=output)
405 else:
406 raise ValueError(f"Unsupported FusedMoe activation: {activation}")
407
408 return output
409
410
411 def situ_and_mul_quant(
412 output: torch.Tensor,
413 scale: torch.Tensor,
414 input: torch.Tensor,
415 *,
416 beta: float,
417 linear_beta: float | None,
418 group_size: int = 0,
419 num_valid_tokens: torch.Tensor | None = None,
420 topk: int = 1,
421 ) -> None:
422 """Fused Kimi SITU activation + dynamic FP8 quantization.
423
424 Writes the quantized fp8 down-projection input into ``output`` [M, d] and its
425 float32 scale into ``scale`` (dequant = q * scale), fusing the SITU
426 activation with the FP8 quant that Humming's w2 GEMM would otherwise perform
427 in a separate pass -- this avoids materializing (and rounding through) the
428 bf16 activation_output buffer.
429
430 ``group_size`` selects the quant granularity: ``0`` gives per-token scales
431 ``scale`` [M, 1]; ``128`` gives k-major block-FP8 group scales ``scale``
432 [M, d // 128], matching humming ``quant_input(group_size=128, float32)``.
433
434 ``linear_beta`` <= 0 (or None) means "unset" (up passed through), matching
435 ``SituAndMul(linear_beta=None)``. ``num_valid_tokens`` (int32 scalar tensor)
436 is the DeepEP v2 contiguous-layout token count (e.g. ``psum[-1:]``); the
437 kernel multiplies it by ``topk`` to bound rows. Padding rows are skipped and
438 receive a benign scale of 1.0. Kept in this module so that every
439 ``torch.ops._C.situ*`` call lives in one file.
440 """
441 torch.ops._C.situ_and_mul_quant(
442 output,
443 scale,
444 input,
445 beta,
446 -1.0 if linear_beta is None else linear_beta,
447 group_size,
448 num_valid_tokens,
449 topk,
450 )