main
py 607 lines 19.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import random
5 from unittest.mock import patch
6
7 import pytest
8 import torch
9
10 from tests.kernels.allclose_default import get_default_atol, get_default_rtol
11 from tests.kernels.utils import opcheck
12 from vllm.model_executor.layers.activation import (
13 FastGELU,
14 FatreluAndMul,
15 GeluAndMul,
16 MulAndSilu,
17 NewGELU,
18 QuickGELU,
19 ReLUSquaredActivation,
20 SiluAndMul,
21 SiluAndMulWithClamp,
22 SwigluOAIAndMul,
23 SwigluStepAndMul,
24 swiglustep_and_mul_triton,
25 )
26 from vllm.model_executor.layers.fused_moe.activation import (
27 ApplyMoEActivationConfig,
28 MoEActivation,
29 apply_moe_activation,
30 )
31 from vllm.model_executor.layers.fused_moe.utils import swiglu_limit_func
32 from vllm.platforms import current_platform
33 from vllm.utils.torch_utils import set_random_seed
34
35 DTYPES = [torch.half, torch.bfloat16, torch.float]
36 NUM_TOKENS = [7, 83, 2048] # Arbitrary values for testing
37 D = [512, 13824] # Arbitrary values for testing
38 SEEDS = [0]
39 CUDA_DEVICES = [
40 f"cuda:{i}" for i in range(1 if torch.accelerator.device_count() == 1 else 2)
41 ]
42
43
44 def test_masked_moe_activation_rejects_unsupported_activation() -> None:
45 input = torch.empty(1, 1, 2)
46 output = torch.empty(1, 1, 1)
47 valid_token_counts = torch.ones(1, dtype=torch.int32)
48
49 with pytest.raises(NotImplementedError, match="relu2"):
50 apply_moe_activation(
51 MoEActivation.RELU2,
52 output,
53 input,
54 valid_token_counts=valid_token_counts,
55 )
56
57
58 def test_moe_silu_clamp_uses_native_xpu_fallback(
59 default_vllm_config, monkeypatch
60 ) -> None:
61 monkeypatch.setattr(current_platform, "is_xpu", lambda: True)
62 clamp_limit = 3.0
63 input = torch.tensor([[12.0, -12.0, 8.0, -8.0], [-2.0, 2.0, -4.0, 4.0]])
64 output = torch.empty(2, 2)
65
66 apply_moe_activation(
67 MoEActivation.SILU,
68 output,
69 input,
70 activation_config=ApplyMoEActivationConfig(clamp_limit=clamp_limit),
71 )
72
73 expected = SiluAndMulWithClamp(clamp_limit, compile_native=False).forward_native(
74 input
75 )
76 torch.testing.assert_close(output, expected)
77
78
79 def _assert_masked_moe_activation(
80 activation: MoEActivation,
81 activation_config: ApplyMoEActivationConfig,
82 *,
83 dtype: torch.dtype,
84 mask_layout: str,
85 d: int,
86 max_num_tokens: int,
87 ) -> None:
88 device = CUDA_DEVICES[0]
89 num_experts = 4
90 input_dim = 2 * d if activation.is_gated else d
91 if mask_layout == "flat":
92 input = torch.randn(max_num_tokens, input_dim, dtype=dtype, device=device)
93 valid_token_counts = torch.tensor(
94 [max_num_tokens // 2], dtype=torch.int32, device=device
95 )
96 output = torch.full((max_num_tokens, d), 42.0, dtype=dtype, device=device)
97 else:
98 input = torch.randn(
99 num_experts, max_num_tokens, input_dim, dtype=dtype, device=device
100 )
101 valid_token_counts = torch.tensor(
102 [0, 1, max_num_tokens // 2, max_num_tokens],
103 dtype=torch.int32,
104 device=device,
105 )
106 output = torch.full(
107 (num_experts, max_num_tokens, d), 42.0, dtype=dtype, device=device
108 )
109
110 apply_moe_activation(
111 activation,
112 output,
113 input,
114 activation_config=activation_config,
115 valid_token_counts=valid_token_counts,
116 )
117
118 batched_input = input.view(-1, max_num_tokens, input_dim)
119 batched_output = output.view(-1, max_num_tokens, d)
120 for expert, num_tokens in enumerate(valid_token_counts.cpu().tolist()):
121 if num_tokens:
122 expected = torch.empty((num_tokens, d), dtype=dtype, device=device)
123 apply_moe_activation(
124 activation,
125 expected,
126 batched_input[expert, :num_tokens].clone(),
127 activation_config=activation_config,
128 )
129 torch.testing.assert_close(
130 batched_output[expert, :num_tokens],
131 expected,
132 atol=get_default_atol(output),
133 rtol=get_default_rtol(output),
134 )
135 assert torch.all(batched_output[expert, num_tokens:] == 42.0)
136
137
138 @pytest.mark.parametrize(
139 "activation",
140 [
141 "silu_and_mul",
142 "mul_and_silu",
143 "gelu",
144 "gelu_tanh",
145 "fatrelu",
146 "swigluoai_and_mul",
147 "swiglustep_and_mul",
148 ],
149 )
150 @pytest.mark.parametrize("num_tokens", NUM_TOKENS)
151 @pytest.mark.parametrize("d", D)
152 @pytest.mark.parametrize("dtype", DTYPES)
153 @pytest.mark.parametrize("seed", SEEDS)
154 @pytest.mark.parametrize("device", CUDA_DEVICES)
155 @torch.inference_mode()
156 def test_act_and_mul(
157 default_vllm_config,
158 activation: str,
159 num_tokens: int,
160 d: int,
161 dtype: torch.dtype,
162 seed: int,
163 device: str,
164 ) -> None:
165 set_random_seed(seed)
166 torch.set_default_device(device)
167 x = torch.randn(num_tokens, 2 * d, dtype=dtype)
168 if activation == "silu_and_mul":
169 layer = SiluAndMul(compile_native=False)
170 fn = torch.ops._C.silu_and_mul
171 if activation == "mul_and_silu":
172 layer = MulAndSilu()
173 fn = torch.ops._C.mul_and_silu
174 elif activation == "gelu":
175 layer = GeluAndMul(approximate="none")
176 fn = torch.ops._C.gelu_and_mul
177 elif activation == "gelu_tanh":
178 layer = GeluAndMul(approximate="tanh")
179 fn = torch.ops._C.gelu_tanh_and_mul
180 elif activation == "fatrelu":
181 threshold = random.uniform(0, 1)
182 layer = FatreluAndMul(threshold)
183 fn = torch.ops._C.fatrelu_and_mul
184 elif activation == "swigluoai_and_mul":
185 layer = SwigluOAIAndMul()
186 fn = torch.ops._C.swigluoai_and_mul
187 elif activation == "swiglustep_and_mul":
188 layer = SwigluStepAndMul()
189 fn = swiglustep_and_mul_triton
190 out = layer(x)
191 ref_out = layer.forward_native(x)
192 if activation in ["swigluoai_and_mul", "swiglustep_and_mul"]:
193 rtol = {
194 # For fp16, change the relative tolerance from 1e-3 to 2e-3
195 torch.float16: 2e-3,
196 torch.bfloat16: 2e-2,
197 torch.float: 1.3e-6,
198 }
199
200 def _get_rtol(output) -> float:
201 return rtol[output.dtype]
202
203 torch.testing.assert_close(
204 out, ref_out, atol=get_default_atol(out), rtol=_get_rtol(out)
205 )
206 else:
207 # The SiluAndMul, MulAndSilu, GELU and FatReLU implementations are
208 # equivalent to the native PyTorch implementations, so we can do exact
209 # comparison.
210 torch.testing.assert_close(out, ref_out, atol=0.0, rtol=0.0)
211
212 d = x.shape[-1] // 2
213 output_shape = x.shape[:-1] + (d,)
214 out = torch.empty(output_shape, dtype=x.dtype, device=x.device)
215 if activation == "fatrelu":
216 opcheck(fn, (out, x, threshold))
217 elif activation == "swigluoai_and_mul":
218 opcheck(fn, (out, x, layer.alpha, layer.limit))
219 elif activation != "swiglustep_and_mul":
220 opcheck(fn, (out, x))
221
222
223 SWIGLU_LIMITS = [3.0, 7.0, 15.0]
224
225
226 @torch.inference_mode()
227 def test_swiglu_limit_func_without_routing_uses_output_buffer() -> None:
228 x = torch.randn(7, 1024, dtype=torch.bfloat16, device="cuda")
229 output = torch.empty(7, 512, dtype=x.dtype, device=x.device)
230
231 swiglu_limit_func(output, x, swiglu_limit=7.0)
232 gate, up = x.chunk(2, dim=-1)
233 expected = torch.nn.functional.silu(gate.clamp(max=7.0)) * up.clamp(
234 min=-7.0, max=7.0
235 )
236
237 torch.testing.assert_close(output, expected, atol=2e-2, rtol=2e-2)
238
239
240 @pytest.mark.parametrize(
241 ("alpha", "beta"),
242 [(1.0, 0.0), (1.702, 0.0), (1.0, 1.0)],
243 )
244 @pytest.mark.parametrize("swiglu_limit", SWIGLU_LIMITS)
245 @pytest.mark.parametrize("num_tokens", NUM_TOKENS)
246 @pytest.mark.parametrize("d", D)
247 @pytest.mark.parametrize("dtype", DTYPES)
248 @pytest.mark.parametrize("seed", SEEDS)
249 @pytest.mark.parametrize("device", CUDA_DEVICES)
250 @torch.inference_mode()
251 def test_silu_and_mul_with_clamp(
252 default_vllm_config,
253 alpha: float,
254 beta: float,
255 swiglu_limit: float,
256 num_tokens: int,
257 d: int,
258 dtype: torch.dtype,
259 seed: int,
260 device: str,
261 ) -> None:
262 """SiluAndMulWithClamp: cuda kernel must match native reference."""
263 set_random_seed(seed)
264 torch.set_default_device(device)
265 # Use large values to ensure clamping is exercised.
266 x = torch.randn(num_tokens, 2 * d, dtype=dtype) * swiglu_limit * 2
267
268 default_vllm_config.compilation_config.custom_ops = [
269 "none",
270 "+silu_and_mul_with_clamp",
271 ]
272 layer = SiluAndMulWithClamp(
273 swiglu_limit,
274 alpha=alpha,
275 beta=beta,
276 compile_native=False,
277 )
278 if current_platform.is_rocm():
279 # forward_hip is always dispatched; the alpha/beta gate is checked
280 # inside it at call time rather than picked at construction time, so
281 # verify the actual routing by spying on the two candidate methods.
282 assert layer._forward_method == layer.forward_hip
283 with (
284 patch.object(layer, "forward_cuda", wraps=layer.forward_cuda) as cuda_spy,
285 patch.object(
286 layer, "forward_native", wraps=layer.forward_native
287 ) as native_spy,
288 ):
289 out = layer(x)
290 if alpha == 1.0 and beta == 0.0:
291 cuda_spy.assert_called_once()
292 native_spy.assert_not_called()
293 else:
294 native_spy.assert_called_once()
295 cuda_spy.assert_not_called()
296 else:
297 assert layer._forward_method == layer.forward_cuda
298 out = layer(x)
299
300 ref_out = layer.forward_native(x)
301
302 rtol = {
303 torch.float16: 2e-3,
304 torch.bfloat16: 2e-2,
305 torch.float: 1.3e-6,
306 }
307 torch.testing.assert_close(
308 out, ref_out, atol=get_default_atol(out), rtol=rtol[out.dtype]
309 )
310
311 # Verify clamping is actually being applied: the clamped output should
312 # differ from the unclamped SiluAndMul output when inputs are large.
313 if alpha == 1.0 and beta == 0.0:
314 unclamped_out = SiluAndMul.forward_native(x)
315 assert not torch.equal(ref_out.float(), unclamped_out.float()), (
316 "Input was not large enough to exercise the clamp; increase scale"
317 )
318
319 # Verify gate clamping semantics with a controlled scalar case.
320 # gate=large_val is clamped to limit first, then silu(limit) * 1.0.
321 x_gate = torch.tensor(
322 [[swiglu_limit * 20.0, 1.0]], dtype=torch.float32, device=device
323 )
324 out_gate = SiluAndMulWithClamp(swiglu_limit, compile_native=False)(x_gate)
325 expected_gate = torch.nn.functional.silu(
326 torch.tensor(swiglu_limit, dtype=torch.float32)
327 ).item()
328 torch.testing.assert_close(
329 out_gate,
330 torch.tensor([[expected_gate]], dtype=torch.float32, device=device),
331 atol=1e-3,
332 rtol=1e-3,
333 )
334
335 # Verify up clamping semantics: up >> limit gets clamped to limit.
336 x_up = torch.tensor(
337 [[1.0, swiglu_limit * 20.0]], dtype=torch.float32, device=device
338 )
339 out_up = SiluAndMulWithClamp(swiglu_limit, compile_native=False)(x_up)
340 silu_1 = torch.nn.functional.silu(torch.tensor(1.0)).item()
341 torch.testing.assert_close(
342 out_up,
343 torch.tensor([[silu_1 * swiglu_limit]], dtype=torch.float32, device=device),
344 atol=1e-3,
345 rtol=1e-3,
346 )
347
348 # opcheck
349 out_buf = torch.empty(x.shape[:-1] + (d,), dtype=dtype, device=device)
350 opcheck(
351 torch.ops._C.silu_and_mul_with_clamp,
352 (out_buf, x, swiglu_limit, layer.alpha, layer.beta),
353 )
354
355
356 @pytest.mark.parametrize("linear_beta", [-1.0, 2.0])
357 @pytest.mark.parametrize("dtype", [torch.half, torch.bfloat16])
358 @torch.inference_mode()
359 def test_masked_situ_and_mul(
360 default_vllm_config,
361 linear_beta: float,
362 dtype: torch.dtype,
363 ) -> None:
364 """Masked SITU computes valid expert rows and preserves padded zeros."""
365 device = CUDA_DEVICES[0]
366 num_experts, max_num_tokens, d = 4, 7, 512
367 beta = 1.5
368 input = torch.randn(num_experts, max_num_tokens, 2 * d, dtype=dtype, device=device)
369 expert_num_tokens = torch.tensor([0, 1, 4, 7], dtype=torch.int32, device=device)
370 output = torch.zeros(num_experts, max_num_tokens, d, dtype=dtype, device=device)
371
372 torch.ops._C.masked_situ_and_mul(
373 output, input, expert_num_tokens, beta, linear_beta
374 )
375
376 gate, up = input.float().chunk(2, dim=-1)
377 expected = beta * torch.tanh(gate / beta) * torch.sigmoid(gate)
378 if linear_beta > 0:
379 up = linear_beta * torch.tanh(up / linear_beta)
380 expected = (expected * up).to(dtype)
381 for expert, num_tokens in enumerate(expert_num_tokens.cpu().tolist()):
382 torch.testing.assert_close(
383 output[expert, :num_tokens],
384 expected[expert, :num_tokens],
385 atol=get_default_atol(output),
386 rtol=get_default_rtol(output),
387 )
388 assert torch.count_nonzero(output[expert, num_tokens:]) == 0
389
390 opcheck(
391 torch.ops._C.masked_situ_and_mul,
392 (output, input, expert_num_tokens, beta, linear_beta),
393 )
394
395
396 MOE_ACTIVATION_CASES = [
397 pytest.param(MoEActivation.SILU, ApplyMoEActivationConfig(), id="silu"),
398 pytest.param(
399 MoEActivation.SILU, ApplyMoEActivationConfig(clamp_limit=3.0), id="silu_clamp"
400 ),
401 pytest.param(MoEActivation.GELU, ApplyMoEActivationConfig(), id="gelu"),
402 pytest.param(MoEActivation.GELU_TANH, ApplyMoEActivationConfig(), id="gelu_tanh"),
403 pytest.param(
404 MoEActivation.SITU,
405 ApplyMoEActivationConfig(
406 activation_situ_beta=1.5,
407 activation_situ_linear_beta=2.0,
408 ),
409 id="situ",
410 ),
411 pytest.param(MoEActivation.SWIGLUOAI, ApplyMoEActivationConfig(), id="swigluoai"),
412 pytest.param(
413 MoEActivation.SWIGLUOAI_UNINTERLEAVE,
414 ApplyMoEActivationConfig(clamp_limit=3.0, alpha=1.3, beta=0.5),
415 id="swigluoai_uninterleave",
416 ),
417 pytest.param(MoEActivation.SWIGLUSTEP, ApplyMoEActivationConfig(), id="swiglustep"),
418 pytest.param(
419 MoEActivation.SILU_NO_MUL, ApplyMoEActivationConfig(), id="silu_no_mul"
420 ),
421 pytest.param(
422 MoEActivation.GELU_NO_MUL, ApplyMoEActivationConfig(), id="gelu_no_mul"
423 ),
424 pytest.param(
425 MoEActivation.GELU_TANH_NO_MUL,
426 ApplyMoEActivationConfig(),
427 id="gelu_tanh_no_mul",
428 ),
429 pytest.param(
430 MoEActivation.RELU2_NO_MUL, ApplyMoEActivationConfig(), id="relu2_no_mul"
431 ),
432 ]
433
434
435 @pytest.mark.parametrize(("activation", "activation_config"), MOE_ACTIVATION_CASES)
436 @torch.inference_mode()
437 def test_masked_moe_activation_dispatch(
438 default_vllm_config,
439 activation: MoEActivation,
440 activation_config: ApplyMoEActivationConfig,
441 ) -> None:
442 _assert_masked_moe_activation(
443 activation,
444 activation_config,
445 dtype=torch.bfloat16,
446 mask_layout="batched_experts",
447 d=513,
448 max_num_tokens=7,
449 )
450
451
452 @pytest.mark.parametrize(
453 ("activation", "activation_config", "mask_layout"),
454 [
455 pytest.param(
456 MoEActivation.SILU,
457 ApplyMoEActivationConfig(),
458 "flat",
459 id="flat",
460 ),
461 pytest.param(
462 MoEActivation.SITU,
463 ApplyMoEActivationConfig(
464 activation_situ_beta=1.5,
465 activation_situ_linear_beta=2.0,
466 ),
467 "batched_experts",
468 id="batched-experts",
469 ),
470 ],
471 )
472 @torch.inference_mode()
473 def test_masked_moe_activation_grid_stride(
474 default_vllm_config,
475 activation: MoEActivation,
476 activation_config: ApplyMoEActivationConfig,
477 mask_layout: str,
478 ) -> None:
479 _assert_masked_moe_activation(
480 activation,
481 activation_config,
482 dtype=torch.half,
483 mask_layout=mask_layout,
484 d=513,
485 max_num_tokens=67,
486 )
487
488
489 @torch.inference_mode()
490 def test_masked_moe_activation_opcheck(default_vllm_config) -> None:
491 device = CUDA_DEVICES[0]
492 input = torch.randn(2, 3, 64, dtype=torch.half, device=device)
493 output = torch.empty(2, 3, 32, dtype=torch.half, device=device)
494 valid_token_counts = torch.tensor([1, 3], dtype=torch.int32, device=device)
495 opcheck(
496 torch.ops._C.masked_moe_activation,
497 (output, input, valid_token_counts, "silu", 0.0, 1.0, 0.0, 1.0, -1.0),
498 )
499
500
501 @pytest.mark.parametrize(
502 "activation",
503 [
504 (FastGELU, torch.ops._C.gelu_fast),
505 (NewGELU, torch.ops._C.gelu_new),
506 (QuickGELU, torch.ops._C.gelu_quick),
507 (ReLUSquaredActivation, torch.ops._C.relu_squared),
508 ],
509 )
510 @pytest.mark.parametrize("num_tokens", NUM_TOKENS)
511 @pytest.mark.parametrize("d", D)
512 @pytest.mark.parametrize("dtype", DTYPES)
513 @pytest.mark.parametrize("seed", SEEDS)
514 @pytest.mark.parametrize("device", CUDA_DEVICES)
515 @torch.inference_mode()
516 def test_activation(
517 default_vllm_config,
518 activation: type[torch.nn.Module],
519 num_tokens: int,
520 d: int,
521 dtype: torch.dtype,
522 seed: int,
523 device: str,
524 ) -> None:
525 set_random_seed(seed)
526 torch.set_default_device(device)
527 x = torch.randn(num_tokens, d, dtype=dtype)
528 layer = activation[0]()
529 fn = activation[1]
530 out = layer(x)
531 ref_out = layer.forward_native(x)
532 torch.testing.assert_close(
533 out, ref_out, atol=get_default_atol(out), rtol=get_default_rtol(out)
534 )
535
536 out = torch.empty_like(x)
537 opcheck(fn, (out, x))
538
539
540 HUMMING_ACTIVATION_CASES = MOE_ACTIVATION_CASES + [
541 pytest.param(MoEActivation.RELU2, ApplyMoEActivationConfig(), id="relu2"),
542 ]
543 HUMMING_ACTIVATION_CASES += [
544 pytest.param(
545 MoEActivation.SITU,
546 ApplyMoEActivationConfig(
547 activation_situ_beta=1.5,
548 activation_situ_linear_beta=linear_beta,
549 ),
550 id=f"situ-linear-beta-{linear_beta}",
551 )
552 for linear_beta in (None, 0.0, -1.0)
553 ]
554
555
556 @pytest.mark.skipif(not current_platform.is_cuda(), reason="Humming requires CUDA")
557 @pytest.mark.parametrize(("activation", "activation_config"), HUMMING_ACTIVATION_CASES)
558 @pytest.mark.parametrize("dtype", DTYPES)
559 @pytest.mark.parametrize(("num_tokens", "d"), [(1, 512), (7, 768), (83, 512)])
560 @torch.inference_mode()
561 def test_humming_activation_matches_framework(
562 activation: MoEActivation,
563 activation_config: ApplyMoEActivationConfig,
564 dtype: torch.dtype,
565 num_tokens: int,
566 d: int,
567 ) -> None:
568 """Compare activation math/layouts without quantization or Hadamard error."""
569 pytest.importorskip("humming")
570 from humming.ops import process_input
571
572 from vllm.model_executor.layers.quantization.utils.humming.activation import (
573 get_humming_activation,
574 )
575
576 set_random_seed(0)
577 width = d * 2 if activation.is_gated else d
578 x = 4 * torch.randn(num_tokens, width, dtype=dtype, device=CUDA_DEVICES[0])
579 # Exercise zero, saturation, and both sides of the clamp limits (3 and 7).
580 edges = x.new_tensor([0, 0.001, 1, 2.99, 3, 3.01, 6.99, 7, 7.01, 8, 16])
581 edges = torch.cat((-edges[1:].flip(0), edges))
582 x[0] = edges[torch.arange(width, device=x.device) % edges.numel()]
583
584 actual, _, _ = process_input(
585 x,
586 quant_mode="none",
587 hadamard_block_size=0,
588 **get_humming_activation(activation, activation_config),
589 )
590 expected = torch.empty(num_tokens, d, dtype=dtype, device=x.device)
591 if activation == MoEActivation.RELU2:
592 # apply_moe_activation has only the non-gated ReLU2 variant.
593 gate, up = x.float().chunk(2, dim=-1)
594 activated_gate = torch.empty_like(gate)
595 apply_moe_activation(MoEActivation.RELU2_NO_MUL, activated_gate, gate.clone())
596 expected.copy_(activated_gate * up)
597 else:
598 # The framework's non-gated ReLU2 path modifies its input in place.
599 apply_moe_activation(
600 activation, expected, x.clone(), activation_config=activation_config
601 )
602 torch.testing.assert_close(
603 actual,
604 expected,
605 atol=get_default_atol(expected),
606 rtol=get_default_rtol(expected),
607 )