main
py 843 lines 26.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import copy
4 import dataclasses
5 from collections.abc import Callable
6 from math import prod
7
8 import pytest
9 import torch
10
11 import vllm.model_executor.layers.fused_moe.modular_kernel as mk
12 from tests.kernels.moe.utils import make_dummy_moe_config
13 from vllm import _custom_ops as ops
14 from vllm.config import ParallelConfig, VllmConfig, set_current_vllm_config
15 from vllm.model_executor.layers.fused_moe import (
16 ApplyMoEActivationConfig,
17 fused_experts,
18 fused_topk,
19 )
20 from vllm.model_executor.layers.fused_moe.activation import (
21 MoEActivation,
22 apply_moe_activation_supported,
23 )
24 from vllm.model_executor.layers.fused_moe.all2all_utils import (
25 maybe_make_prepare_finalize,
26 )
27 from vllm.model_executor.layers.fused_moe.config import (
28 FUSED_MOE_UNQUANTIZED_CONFIG,
29 FusedMoEQuantConfig,
30 fp8_w8a8_moe_quant_config,
31 )
32 from vllm.model_executor.layers.fused_moe.experts.cutlass_moe import (
33 CutlassExpertsFp4,
34 CutlassExpertsFp8,
35 CutlassExpertsMxfp4,
36 CutlassExpertsW4A8Fp8,
37 run_cutlass_moe_fp4,
38 run_cutlass_moe_fp8,
39 run_cutlass_moe_mxfp4,
40 )
41 from vllm.model_executor.layers.fused_moe.oracle import nvfp4 as nvfp4_oracle
42 from vllm.model_executor.layers.fused_moe.utils import moe_kernel_quantize_input
43 from vllm.platforms import current_platform
44 from vllm.utils.torch_utils import set_random_seed
45
46 NUM_EXPERTS = [40, 64]
47 TOP_KS = [6, 8]
48
49 MNK_FACTORS = [
50 (2, 1024, 1024),
51 (2, 3072, 1024),
52 (2, 3072, 1536),
53 (7, 3072, 1536),
54 (64, 1024, 1024),
55 (64, 1024, 1536),
56 (64, 3072, 1024),
57 (224, 1024, 1024),
58 (224, 3072, 1024),
59 (224, 3072, 1536),
60 (32768, 1024, 1024),
61 # These sizes trigger wrong answers.
62 # (7232, 2048, 5120),
63 # (40000, 2048, 5120),
64 ]
65
66 vllm_config = VllmConfig(parallel_config=ParallelConfig(pipeline_parallel_size=1))
67
68
69 @pytest.mark.parametrize(
70 "experts_cls",
71 [
72 CutlassExpertsFp8,
73 CutlassExpertsFp4,
74 CutlassExpertsMxfp4,
75 CutlassExpertsW4A8Fp8,
76 ],
77 )
78 @pytest.mark.parametrize("activation", list(MoEActivation))
79 def test_cutlass_moe_activation_metadata_tracks_shared_apply(
80 experts_cls: type[mk.FusedMoEExperts], activation: MoEActivation
81 ):
82 supports_shape = experts_cls._supports_no_act_and_mul() or activation.is_gated
83 expected = supports_shape and apply_moe_activation_supported(activation)
84
85 assert experts_cls._supports_activation(activation) == expected
86
87
88 def test_cutlass_moe_forwards_shared_activation_parameters():
89 moe_config = make_dummy_moe_config()
90 moe_config.swiglu_limit = 7.0
91 moe_config.swiglu_alpha = 1.5
92 moe_config.swiglu_beta = 0.25
93 moe_config.activation_situ_beta = 2.0
94 moe_config.activation_situ_linear_beta = 3.0
95 quant_config = FusedMoEQuantConfig.make(gemm1_alpha=1.75)
96
97 assert ApplyMoEActivationConfig.from_configs(
98 moe_config, quant_config
99 ) == ApplyMoEActivationConfig(
100 clamp_limit=7.0,
101 alpha=1.75,
102 beta=0.25,
103 activation_situ_beta=2.0,
104 activation_situ_linear_beta=3.0,
105 )
106
107
108 @pytest.mark.parametrize(
109 ("backend", "expected"),
110 [
111 ("cutlass", nvfp4_oracle.NvFp4MoeBackend.VLLM_CUTLASS),
112 ("humming", nvfp4_oracle.NvFp4MoeBackend.HUMMING),
113 ],
114 )
115 def test_nvfp4_clamp_allows_shared_activation_backends(
116 monkeypatch, backend: str, expected: nvfp4_oracle.NvFp4MoeBackend
117 ):
118 class SupportedExperts:
119 @staticmethod
120 def is_supported_config(*args, **kwargs):
121 return True, None
122
123 monkeypatch.setattr(
124 nvfp4_oracle, "backend_to_kernel_cls", lambda backend: [SupportedExperts]
125 )
126 moe_config = make_dummy_moe_config()
127 moe_config.moe_backend = backend
128 moe_config.swiglu_limit = 7.0
129
130 selected, _ = nvfp4_oracle.select_nvfp4_moe_backend(
131 moe_config, weight_key=None, activation_key=None
132 )
133
134 assert selected == expected
135
136
137 @pytest.mark.skipif(
138 (lambda x: x is None or not ops.cutlass_group_gemm_supported(x.to_int()))(
139 current_platform.get_device_capability()
140 ),
141 reason="Grouped gemm is not supported on this GPU type.",
142 )
143 def test_cutlass_moe_permutation_maps_padding_to_zero():
144 """Invalid top-k routes must not leave unsafe permutation-map entries."""
145 num_experts = 2
146 topk_ids = torch.tensor(
147 [[0, -1], [1, num_experts]], device="cuda", dtype=torch.int32
148 )
149 num_routes = topk_ids.numel()
150
151 expert_offsets = torch.empty(num_experts + 1, device="cuda", dtype=torch.int32)
152 blockscale_offsets = torch.empty_like(expert_offsets)
153 problem_sizes1 = torch.empty(num_experts, 3, device="cuda", dtype=torch.int32)
154 problem_sizes2 = torch.empty_like(problem_sizes1)
155 input_permutation = torch.empty(num_routes, device="cuda", dtype=torch.int32)
156 output_permutation = torch.empty_like(input_permutation)
157
158 ops.get_cutlass_moe_mm_data(
159 topk_ids,
160 expert_offsets,
161 problem_sizes1,
162 problem_sizes2,
163 input_permutation,
164 output_permutation,
165 num_experts,
166 128,
167 128,
168 blockscale_offsets,
169 )
170
171 input_tensor = torch.randn(2, 128, device="cuda", dtype=torch.bfloat16)
172 permuted = ops.shuffle_rows(input_tensor, input_permutation)
173 restored = ops.shuffle_rows(permuted, output_permutation)
174
175 torch.testing.assert_close(permuted[:2], input_tensor)
176 torch.testing.assert_close(permuted[2:], torch.zeros_like(permuted[2:]))
177 torch.testing.assert_close(restored[0], input_tensor[0])
178 torch.testing.assert_close(restored[1], torch.zeros_like(restored[1]))
179 torch.testing.assert_close(restored[2], input_tensor[1])
180 torch.testing.assert_close(restored[3], torch.zeros_like(restored[3]))
181
182
183 @pytest.mark.parametrize("quantization", ["nvfp4", "mxfp4"])
184 @torch.inference_mode()
185 def test_cutlass_fp4_moe_padded_routes_do_not_change_valid_output(quantization: str):
186 """Padded routes must not participate in either activation quantization."""
187 experts_cls = CutlassExpertsFp4 if quantization == "nvfp4" else CutlassExpertsMxfp4
188 if not experts_cls._supports_current_device():
189 pytest.skip(f"CUTLASS {quantization} MoE is not supported on this GPU")
190
191 set_random_seed(7)
192 num_experts, n, k = 4, 128, 128
193 dtype = torch.bfloat16
194 device = torch.device("cuda")
195 w1 = torch.full(
196 (num_experts, 2 * n, k // 2), 0x11, device=device, dtype=torch.uint8
197 )
198 w2 = torch.full((num_experts, k, n // 2), 0x11, device=device, dtype=torch.uint8)
199
200 runner: Callable[..., None]
201 quant_args: tuple[torch.Tensor, ...]
202 if quantization == "nvfp4":
203 scale_kwargs = {
204 "device": device,
205 "dtype": torch.float8_e4m3fn,
206 }
207 w1_scale = torch.full((num_experts, 2 * n, k // 16), 1 / 16, **scale_kwargs)
208 w2_scale = torch.full((num_experts, k, n // 16), 1 / 16, **scale_kwargs)
209 gscale = torch.ones(num_experts, device=device, dtype=torch.float32)
210 runner = run_cutlass_moe_fp4
211 quant_args = (
212 gscale,
213 w1,
214 w1_scale,
215 gscale,
216 gscale,
217 w2,
218 w2_scale,
219 gscale,
220 )
221 else:
222 scale_exponent = 127 - 4
223 w1_scale = torch.full(
224 (num_experts, 2 * n, k // 32),
225 scale_exponent,
226 device=device,
227 dtype=torch.uint8,
228 )
229 w2_scale = torch.full(
230 (num_experts, k, n // 32),
231 scale_exponent,
232 device=device,
233 dtype=torch.uint8,
234 )
235 runner = run_cutlass_moe_mxfp4
236 quant_args = (w1, w1_scale, w2, w2_scale)
237
238 def run_moe(
239 hidden_states: torch.Tensor,
240 topk_ids: torch.Tensor,
241 topk_weights: torch.Tensor,
242 workspace_value: float,
243 ) -> torch.Tensor:
244 m, topk = topk_ids.shape
245 output = torch.empty((m, k), device=device, dtype=dtype)
246 workspace13 = torch.full(
247 (m * topk, max(2 * n, k)),
248 workspace_value,
249 device=device,
250 dtype=dtype,
251 )
252 workspace2 = torch.full(
253 (m * topk, n), workspace_value, device=device, dtype=dtype
254 )
255
256 runner(
257 output,
258 hidden_states,
259 *quant_args,
260 topk_weights,
261 topk_ids,
262 MoEActivation.SILU,
263 workspace13,
264 workspace2,
265 m,
266 n,
267 k,
268 num_experts,
269 device,
270 )
271 return output
272
273 valid_input = torch.randn((1, k), device=device, dtype=dtype)
274 expected = run_moe(
275 valid_input,
276 torch.zeros((1, 1), device=device, dtype=torch.int32),
277 torch.ones((1, 1), device=device, dtype=torch.float32),
278 workspace_value=0,
279 )
280 assert expected.abs().max() > 0
281
282 # Force the quantizers' grid-stride loops to revisit padded tail rows.
283 num_tokens = 4096
284 padded_input = torch.zeros((num_tokens, k), device=device, dtype=dtype)
285 padded_input[0] = valid_input[0]
286 padded_ids = torch.full((num_tokens, 2), -1, device=device, dtype=torch.int32)
287 padded_ids[0, 0] = 0
288 padded_weights = torch.zeros((num_tokens, 2), device=device, dtype=torch.float32)
289 padded_weights[0, 0] = 1
290
291 for workspace_value in (0, 64):
292 actual = run_moe(padded_input, padded_ids, padded_weights, workspace_value)
293 torch.testing.assert_close(actual[0], expected[0], atol=1e-2, rtol=1e-2)
294 torch.testing.assert_close(actual[1:], torch.zeros_like(actual[1:]))
295
296
297 @dataclasses.dataclass
298 class MOETensors:
299 a: torch.Tensor
300 w1: torch.Tensor
301 w2: torch.Tensor
302 ab_strides1: torch.Tensor
303 c_strides1: torch.Tensor
304 ab_strides2: torch.Tensor
305 c_strides2: torch.Tensor
306
307 @staticmethod
308 def make_moe_tensors(
309 m: int, k: int, n: int, e: int, dtype: torch.dtype
310 ) -> "MOETensors":
311 a = torch.randn((m, k), device="cuda", dtype=dtype) / 10
312 w1 = torch.randn((e, 2 * n, k), device="cuda", dtype=dtype) / 10
313 w2 = torch.randn((e, k, n), device="cuda", dtype=dtype) / 10
314 ab_strides1 = torch.full((e,), k, device="cuda", dtype=torch.int64)
315 c_strides1 = torch.full((e,), 2 * n, device="cuda", dtype=torch.int64)
316 ab_strides2 = torch.full((e,), n, device="cuda", dtype=torch.int64)
317 c_strides2 = torch.full((e,), k, device="cuda", dtype=torch.int64)
318 return MOETensors(
319 a=a,
320 w1=w1,
321 w2=w2,
322 ab_strides1=ab_strides1,
323 c_strides1=c_strides1,
324 ab_strides2=ab_strides2,
325 c_strides2=c_strides2,
326 )
327
328
329 @dataclasses.dataclass
330 class MOETensors8Bit(MOETensors):
331 # quantized
332 a_q: torch.Tensor | None = None # a -> a_q
333 w1_q: torch.Tensor | None = None # w1 -> w1_q
334 w2_q: torch.Tensor | None = None # w2 -> w2_q
335 a_scale: torch.Tensor | None = None
336 w1_scale: torch.Tensor | None = None
337 w2_scale: torch.Tensor | None = None
338 # dequantized
339 a_d: torch.Tensor | None = None # a -> a_q -> a_d
340 w1_d: torch.Tensor | None = None # w1 -> w1_q -> w1_d
341 w2_d: torch.Tensor | None = None # w2 -> w2_q -> w2_d
342
343 @staticmethod
344 def make_moe_tensors_8bit(
345 m: int, k: int, n: int, e: int, per_act_token: bool, per_out_channel: bool
346 ) -> "MOETensors8Bit":
347 dtype = torch.half
348 q_dtype = torch.float8_e4m3fn
349
350 moe_tensors_fp16 = MOETensors.make_moe_tensors(m, k, n, e, dtype)
351
352 # a -> a_q, w1 -> w1_q, w2 -> w2_q
353 n_b_scales = 2 * n if per_out_channel else 1
354 k_b_scales = k if per_out_channel else 1
355 # Get the right scale for tests.
356 a_q, a_scale = ops.scaled_fp8_quant(
357 moe_tensors_fp16.a, None, use_per_token_if_dynamic=per_act_token
358 )
359
360 w1_q = torch.empty((e, 2 * n, k), device="cuda", dtype=q_dtype)
361 w2_q = torch.empty((e, k, n), device="cuda", dtype=q_dtype)
362
363 w1_scale = torch.empty((e, n_b_scales, 1), device="cuda", dtype=torch.float32)
364 w2_scale = torch.empty((e, k_b_scales, 1), device="cuda", dtype=torch.float32)
365 for expert in range(e):
366 w1_q[expert], w1_scale[expert] = ops.scaled_fp8_quant(
367 moe_tensors_fp16.w1[expert], use_per_token_if_dynamic=per_out_channel
368 )
369 w2_q[expert], w2_scale[expert] = ops.scaled_fp8_quant(
370 moe_tensors_fp16.w2[expert], use_per_token_if_dynamic=per_out_channel
371 )
372
373 # a_q -> a_d, w1_q -> w1_d, w2_q -> w2_d
374 a_d = a_q.float().mul(a_scale).to(dtype)
375 w1_d = torch.empty_like(moe_tensors_fp16.w1)
376 w2_d = torch.empty_like(moe_tensors_fp16.w2)
377 for expert in range(e):
378 w1_d[expert] = (w1_q[expert].float() * w1_scale[expert]).half()
379 w2_d[expert] = (w2_q[expert].float() * w2_scale[expert]).half()
380
381 return MOETensors8Bit(
382 a=moe_tensors_fp16.a,
383 w1=moe_tensors_fp16.w1,
384 w2=moe_tensors_fp16.w2,
385 ab_strides1=moe_tensors_fp16.ab_strides1,
386 c_strides1=moe_tensors_fp16.c_strides1,
387 ab_strides2=moe_tensors_fp16.ab_strides2,
388 c_strides2=moe_tensors_fp16.c_strides2,
389 a_q=a_q,
390 w1_q=w1_q,
391 w2_q=w2_q,
392 a_scale=a_scale,
393 w1_scale=w1_scale,
394 w2_scale=w2_scale,
395 a_d=a_d,
396 w1_d=w1_d,
397 w2_d=w2_d,
398 )
399
400
401 def run_with_expert_maps(
402 num_experts: int,
403 num_local_experts: int,
404 quant_config: FusedMoEQuantConfig,
405 **cutlass_moe_kwargs,
406 ):
407 def slice_experts():
408 slice_params = [
409 "w1",
410 "w2",
411 ]
412 full_tensors = {
413 k: v
414 for k, v in cutlass_moe_kwargs.items()
415 if k in slice_params and k in cutlass_moe_kwargs
416 }
417
418 for i in range(0, num_experts, num_local_experts):
419 s, e = i, i + num_local_experts
420
421 # make expert map
422 expert_map = [-1] * num_experts
423 expert_map[s:e] = list(range(num_local_experts))
424 expert_map = torch.tensor(expert_map, dtype=torch.int32, device="cuda")
425
426 # update cutlass moe arg with expert_map
427 cutlass_moe_kwargs["expert_map"] = expert_map
428 # update cutlass moe arg tensors
429 for k, t in full_tensors.items():
430 cutlass_moe_kwargs[k] = t[s:e]
431
432 new_quant_config = copy.deepcopy(quant_config)
433 new_quant_config._w1.scale = quant_config.w1_scale[s:e]
434 new_quant_config._w2.scale = quant_config.w2_scale[s:e]
435
436 yield cutlass_moe_kwargs, new_quant_config
437
438 out_tensor = torch.zeros_like(cutlass_moe_kwargs["hidden_states"])
439 for kwargs, new_quant_config in slice_experts():
440 w2 = kwargs["w2"]
441 a = kwargs["hidden_states"]
442 moe_config = make_dummy_moe_config(
443 max_num_tokens=kwargs.get("hidden_states").shape[0],
444 experts_per_token=kwargs.get("topk_ids").shape[1],
445 num_experts=num_experts,
446 num_local_experts=num_local_experts,
447 hidden_dim=w2.shape[1],
448 intermediate_size=w2.shape[2],
449 in_dtype=a.dtype,
450 )
451 kernel = mk.FusedMoEKernel(
452 maybe_make_prepare_finalize(
453 moe=moe_config,
454 quant_config=new_quant_config,
455 allow_new_interface=True,
456 use_monolithic=False,
457 ),
458 CutlassExpertsFp8(
459 moe_config=moe_config,
460 quant_config=new_quant_config,
461 ),
462 )
463 out_tensor = out_tensor + kernel.apply(**kwargs)
464
465 return out_tensor
466
467
468 def run_8_bit(
469 moe_tensors: MOETensors8Bit,
470 topk_weights: torch.Tensor,
471 topk_ids: torch.Tensor,
472 per_act_token: bool,
473 per_out_ch: bool,
474 num_local_experts: int | None = None,
475 ) -> torch.Tensor:
476 assert not any(
477 [
478 t is None
479 for t in [
480 moe_tensors.w1_q,
481 moe_tensors.w2_q,
482 moe_tensors.w1_scale,
483 moe_tensors.w2_scale,
484 moe_tensors.a_scale,
485 ]
486 ]
487 )
488
489 quant_config = fp8_w8a8_moe_quant_config(
490 w1_scale=moe_tensors.w1_scale,
491 w2_scale=moe_tensors.w2_scale,
492 per_act_token_quant=per_act_token,
493 per_out_ch_quant=per_out_ch,
494 # Set to moe_tensors.a_scale iff static scales + per tensor.
495 # This is not currently being tested.
496 a1_scale=None,
497 )
498
499 num_experts = moe_tensors.w1.size(0) # type: ignore[attr-defined]
500 with_ep = num_local_experts is not None or num_local_experts == num_experts
501
502 kwargs = {
503 "hidden_states": moe_tensors.a,
504 "w1": moe_tensors.w1_q, # type: ignore[union-attr]
505 "w2": moe_tensors.w2_q, # type: ignore[union-attr]
506 "topk_weights": topk_weights,
507 "topk_ids": topk_ids,
508 "global_num_experts": num_experts,
509 "activation": MoEActivation.SILU,
510 "expert_map": None,
511 "apply_router_weight_on_input": False,
512 }
513
514 if not with_ep:
515 moe_config = make_dummy_moe_config(
516 max_num_tokens=moe_tensors.a.shape[0],
517 experts_per_token=topk_ids.shape[1],
518 num_experts=num_experts,
519 num_local_experts=num_local_experts,
520 hidden_dim=moe_tensors.w2_q.shape[1], # type: ignore[union-attr]
521 intermediate_size=moe_tensors.w2_q.shape[2], # type: ignore[union-attr]
522 in_dtype=moe_tensors.a.dtype,
523 )
524 kernel = mk.FusedMoEKernel(
525 maybe_make_prepare_finalize(
526 moe=moe_config,
527 quant_config=quant_config,
528 allow_new_interface=True,
529 use_monolithic=False,
530 ),
531 CutlassExpertsFp8(
532 moe_config=moe_config,
533 quant_config=quant_config,
534 ),
535 )
536 return kernel.apply(**kwargs)
537
538 assert num_local_experts is not None
539 return run_with_expert_maps(
540 num_experts,
541 num_local_experts,
542 quant_config,
543 **kwargs,
544 )
545
546
547 @pytest.mark.parametrize("m,n,k", MNK_FACTORS)
548 @pytest.mark.parametrize("e", NUM_EXPERTS)
549 @pytest.mark.parametrize("topk", TOP_KS)
550 @pytest.mark.parametrize("per_act_token", [True, False])
551 @pytest.mark.parametrize("per_out_ch", [True, False])
552 @pytest.mark.skipif(
553 (lambda x: x is None or not ops.cutlass_group_gemm_supported(x.to_int()))(
554 current_platform.get_device_capability()
555 ),
556 reason="Grouped gemm is not supported on this GPU type.",
557 )
558 def test_cutlass_moe_8_bit_no_graph(
559 m: int,
560 n: int,
561 k: int,
562 e: int,
563 topk: int,
564 per_act_token: bool,
565 per_out_ch: bool,
566 monkeypatch,
567 workspace_init,
568 ep_size: int | None = None,
569 ):
570 set_random_seed(7)
571 with set_current_vllm_config(vllm_config):
572 mt = MOETensors8Bit.make_moe_tensors_8bit(m, k, n, e, per_act_token, per_out_ch)
573
574 score = torch.randn((m, e), device="cuda", dtype=torch.half)
575 topk_weights, topk_ids, _ = fused_topk(mt.a, score, topk, renormalize=False)
576
577 # Note that we are using the dequantized versions of the tensors.
578 # Using a, w1 and w2 directly results in minor output differences.
579
580 quant_config = FUSED_MOE_UNQUANTIZED_CONFIG
581 triton_output = fused_experts(
582 mt.a_d, mt.w1_d, mt.w2_d, topk_weights, topk_ids, quant_config=quant_config
583 )
584
585 if ep_size is not None:
586 assert e % ep_size == 0, "Cannot distribute experts evenly"
587 number_local_experts = e // ep_size
588 else:
589 number_local_experts = None
590
591 cutlass_output = run_8_bit(
592 mt, topk_weights, topk_ids, per_act_token, per_out_ch, number_local_experts
593 )
594
595 # Note 5.5 only needed for larger problem sizes, 5 works ok for
596 # the rest.
597 torch.testing.assert_close(
598 triton_output, cutlass_output, atol=5.5e-2, rtol=1e-2
599 )
600
601
602 @pytest.mark.parametrize("m,n,k", MNK_FACTORS)
603 @pytest.mark.parametrize("e", NUM_EXPERTS)
604 @pytest.mark.parametrize("topk", TOP_KS)
605 @pytest.mark.parametrize("per_act_token", [True, False])
606 @pytest.mark.parametrize("per_out_ch", [True, False])
607 @pytest.mark.skipif(
608 (lambda x: x is None or not ops.cutlass_group_gemm_supported(x.to_int()))(
609 current_platform.get_device_capability()
610 ),
611 reason="Grouped gemm is not supported on this GPU type.",
612 )
613 def test_cutlass_moe_8_bit_cuda_graph(
614 m: int,
615 n: int,
616 k: int,
617 e: int,
618 topk: int,
619 per_act_token: bool,
620 per_out_ch: bool,
621 monkeypatch,
622 workspace_init,
623 ):
624 set_random_seed(7)
625 with set_current_vllm_config(vllm_config):
626 dtype = torch.half
627
628 mt = MOETensors8Bit.make_moe_tensors_8bit(m, k, n, e, per_act_token, per_out_ch)
629
630 score = torch.randn((m, e), device="cuda", dtype=dtype)
631 topk_weights, topk_ids, _ = fused_topk(mt.a, score, topk, renormalize=False)
632
633 # Note that we are using the dequantized versions of the tensors.
634 # Using a, w1 and w2 directly results in minor output differences.
635 quant_config = FUSED_MOE_UNQUANTIZED_CONFIG
636 triton_output = fused_experts(
637 mt.a_d, mt.w1_d, mt.w2_d, topk_weights, topk_ids, quant_config=quant_config
638 )
639
640 stream = torch.cuda.Stream()
641 graph = torch.cuda.CUDAGraph()
642 with torch.cuda.graph(graph, stream=stream):
643 cutlass_output = run_8_bit(
644 mt, topk_weights, topk_ids, per_act_token, per_out_ch
645 )
646
647 torch.accelerator.synchronize()
648 graph.replay()
649 torch.accelerator.synchronize()
650
651 torch.testing.assert_close(triton_output, cutlass_output, atol=9e-2, rtol=1e-2)
652
653
654 @pytest.mark.parametrize("m", [64])
655 @pytest.mark.parametrize("n", [1024])
656 @pytest.mark.parametrize("k", [4096])
657 @pytest.mark.parametrize("e", [16])
658 @pytest.mark.parametrize("topk", [1, 8])
659 @pytest.mark.parametrize("per_act_token", [True])
660 @pytest.mark.parametrize("per_out_channel", [True])
661 @pytest.mark.parametrize("ep_size", [1, 2, 4, 8, 16])
662 @pytest.mark.skipif(
663 (lambda x: x is None or not ops.cutlass_group_gemm_supported(x.to_int()))(
664 current_platform.get_device_capability()
665 ),
666 reason="Grouped gemm is not supported on this GPU type.",
667 )
668 def test_cutlass_moe_8_bit_EP(
669 m: int,
670 n: int,
671 k: int,
672 e: int,
673 topk: int,
674 per_act_token: bool,
675 per_out_channel: bool,
676 ep_size: int,
677 monkeypatch,
678 workspace_init,
679 ):
680 test_cutlass_moe_8_bit_no_graph(
681 m,
682 n,
683 k,
684 e,
685 topk,
686 per_act_token,
687 per_out_channel,
688 monkeypatch,
689 workspace_init,
690 ep_size,
691 )
692
693
694 LARGE_MNK_FACTORS = [
695 (1, 8192, 5120, 31),
696 (32768, 1024, 1024, 16),
697 (65536, 512, 1024, 16),
698 ]
699
700
701 @pytest.mark.parametrize("m,n,k,topk", LARGE_MNK_FACTORS)
702 @pytest.mark.parametrize("e", [128])
703 @pytest.mark.parametrize("per_act_token", [False])
704 @pytest.mark.parametrize("per_out_channel", [True])
705 @pytest.mark.parametrize("ep_size", [8])
706 @pytest.mark.skipif(
707 (lambda x: x is None or not ops.cutlass_group_gemm_supported(x.to_int()))(
708 current_platform.get_device_capability()
709 ),
710 reason="Grouped gemm is not supported on this GPU type.",
711 )
712 def test_cutlass_moe_8_bit_EP_large(
713 m: int,
714 n: int,
715 k: int,
716 e: int,
717 topk: int,
718 per_act_token: bool,
719 per_out_channel: bool,
720 ep_size: int,
721 monkeypatch,
722 workspace_init,
723 ):
724 test_cutlass_moe_8_bit_no_graph(
725 m,
726 n,
727 k,
728 e,
729 topk,
730 per_act_token,
731 per_out_channel,
732 monkeypatch,
733 workspace_init,
734 ep_size,
735 )
736
737
738 @pytest.mark.parametrize("m,n,k,topk", [(1, 8192, 5120, 31)])
739 @pytest.mark.parametrize("e", [128])
740 @pytest.mark.parametrize("per_act_token", [False])
741 @pytest.mark.parametrize("per_out_channel", [True])
742 @pytest.mark.parametrize("ep_size", [8])
743 @pytest.mark.skipif(
744 (lambda x: x is None or not ops.cutlass_group_gemm_supported(x.to_int()))(
745 current_platform.get_device_capability()
746 ),
747 reason="Grouped gemm is not supported on this GPU type.",
748 )
749 def test_run_cutlass_moe_fp8(
750 m: int,
751 n: int,
752 k: int,
753 e: int,
754 topk: int,
755 per_act_token: bool,
756 per_out_channel: bool,
757 ep_size: int,
758 workspace_init,
759 ):
760 set_random_seed(7)
761 with set_current_vllm_config(vllm_config):
762 mt = MOETensors8Bit.make_moe_tensors_8bit(
763 m, k, n, e, per_act_token, per_out_channel
764 )
765
766 score = torch.randn((m, e), device="cuda", dtype=torch.half)
767 topk_weights, topk_ids, _ = fused_topk(mt.a, score, topk, renormalize=False)
768 # we want to make sure there is at least one token that's generated in
769 # this expert shard and at least one token that's NOT generated in this
770 # expert shard
771 topk_ids[0][0] = -1
772 topk_ids[0][1] = 1
773
774 workspace13_shape = (m * topk, max(2 * n, k))
775 workspace2_shape = (m * topk, max(n, k))
776 output_shape = (m, k)
777
778 workspace13 = torch.empty(
779 prod(workspace13_shape), device="cuda", dtype=mt.a.dtype
780 )
781 workspace2 = torch.empty(
782 prod(workspace2_shape), device="cuda", dtype=mt.a.dtype
783 )
784
785 num_local_experts = e // ep_size
786 start, end = 0, num_local_experts
787 expert_map = [-1] * e
788 expert_map[start:end] = list(range(num_local_experts))
789 expert_map = torch.tensor(expert_map, dtype=torch.int32, device="cuda")
790
791 ab_strides1 = torch.full((e,), k, device="cuda", dtype=torch.int64)
792 ab_strides2 = torch.full((e,), n, device="cuda", dtype=torch.int64)
793 c_strides1 = torch.full((e,), 2 * n, device="cuda", dtype=torch.int64)
794 c_strides2 = torch.full((e,), k, device="cuda", dtype=torch.int64)
795
796 activation = MoEActivation.SILU
797 a1q, a1q_scale = moe_kernel_quantize_input(
798 mt.a, mt.a_scale, torch.float8_e4m3fn, per_act_token
799 )
800 global_num_experts = -1 if mt.w1_q is None else mt.w1_q.size(0)
801 func = lambda output: run_cutlass_moe_fp8(
802 output,
803 a1q,
804 mt.w1_q,
805 mt.w2_q,
806 topk_ids,
807 activation,
808 global_num_experts,
809 expert_map,
810 mt.w1_scale,
811 mt.w2_scale,
812 a1q_scale,
813 None,
814 ab_strides1,
815 ab_strides2,
816 c_strides1,
817 c_strides2,
818 workspace13,
819 workspace2,
820 None,
821 mt.a.dtype,
822 per_act_token,
823 per_out_channel,
824 False,
825 topk_weights,
826 None,
827 )
828
829 workspace13.random_()
830 output_random_workspace = torch.empty(
831 output_shape, device="cuda", dtype=mt.a.dtype
832 )
833 func(output_random_workspace)
834
835 workspace13.fill_(0)
836 output_zero_workspace = torch.zeros(
837 output_shape, device="cuda", dtype=mt.a.dtype
838 )
839 func(output_zero_workspace)
840
841 torch.testing.assert_close(
842 output_random_workspace, output_zero_workspace, atol=5e-3, rtol=1e-3
843 )