| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | # Adapted from https://github.com/sgl-project/sglang/blob/main/test/srt/test_triton_moe_channel_fp8_kernel.py |
| 5 | import itertools |
| 6 | |
| 7 | import pytest |
| 8 | import torch |
| 9 | |
| 10 | from tests.kernels.moe.utils import fused_moe |
| 11 | from vllm import _custom_ops as ops |
| 12 | from vllm.config import VllmConfig, set_current_vllm_config |
| 13 | from vllm.model_executor.layers.activation import SiluAndMul |
| 14 | from vllm.model_executor.layers.fused_moe.config import fp8_w8a8_moe_quant_config |
| 15 | from vllm.platforms import current_platform |
| 16 | |
| 17 | if current_platform.get_device_capability() < (9, 0): |
| 18 | pytest.skip("FP8 Triton requires CUDA 9.0 or higher", allow_module_level=True) |
| 19 | |
| 20 | vllm_config = VllmConfig() |
| 21 | |
| 22 | if current_platform.is_fp8_fnuz(): |
| 23 | pytest.skip( |
| 24 | "Tests in this file require float8_e4m3fn and platform does not support", |
| 25 | allow_module_level=True, |
| 26 | ) |
| 27 | |
| 28 | |
| 29 | def native_w8a8_per_token_matmul(A, B, As, Bs, output_dtype=torch.float16): |
| 30 | """Matrix multiplication function that supports per-token input |
| 31 | quantization and per-column weight quantization""" |
| 32 | A = A.to(torch.float32) |
| 33 | B = B.to(torch.float32) |
| 34 | |
| 35 | assert A.shape[-1] == B.shape[-1], "Dimension mismatch" |
| 36 | assert B.ndim == 2 and B.is_contiguous(), "B must be a 2D contiguous tensor" |
| 37 | |
| 38 | # Reshape input |
| 39 | M = A.numel() // A.shape[-1] |
| 40 | B = B.t() # Transpose weight matrix |
| 41 | N, K = B.shape |
| 42 | origin_C_shape = A.shape[:-1] + (K,) |
| 43 | A = A.reshape(M, N) |
| 44 | |
| 45 | # As is per-token [M, 1], Bs is per-column [1, K] |
| 46 | C = torch.matmul(A, B) # [M, K] |
| 47 | C = As * C * Bs.view(1, -1) # Broadcast per-column scale |
| 48 | |
| 49 | return C.reshape(origin_C_shape).to(output_dtype) |
| 50 | |
| 51 | |
| 52 | def fp8_mask(a, mask): |
| 53 | dtype = a.dtype |
| 54 | return a.view(torch.int8)[mask].view(dtype) |
| 55 | |
| 56 | |
| 57 | def torch_w8a8_per_column_moe(a, w1, w2, w1_s, w2_s, score, topk): |
| 58 | """This function performs fused moe with per-column int8 |
| 59 | quantization using native torch.""" |
| 60 | B, D = a.shape |
| 61 | # Perform per-token quantization |
| 62 | a_q, a_s = ops.scaled_fp8_quant(a, use_per_token_if_dynamic=True) |
| 63 | # Repeat tokens to match topk |
| 64 | a_q = a_q.view(B, -1, D).repeat(1, topk, 1).reshape(-1, D) |
| 65 | # Also repeat the scale |
| 66 | a_s = a_s.view(B, -1, 1).repeat(1, topk, 1).reshape(-1, 1) # [B*topk, 1] |
| 67 | |
| 68 | out = torch.zeros(B * topk, w2.shape[1], dtype=a.dtype, device=a.device) |
| 69 | |
| 70 | # Calculate routing |
| 71 | score = torch.softmax(score, dim=-1, dtype=torch.float32) |
| 72 | topk_weight, topk_ids = torch.topk(score, topk) |
| 73 | topk_weight = topk_weight.view(-1) |
| 74 | topk_ids = topk_ids.view(-1) |
| 75 | # Process each expert |
| 76 | for i in range(w1.shape[0]): |
| 77 | mask = topk_ids == i |
| 78 | if mask.sum(): |
| 79 | # First MLP layer: note that a_s is now per-token |
| 80 | inter_out = native_w8a8_per_token_matmul( |
| 81 | fp8_mask(a_q, mask), |
| 82 | w1[i], |
| 83 | fp8_mask(a_s, mask), |
| 84 | w1_s[i], |
| 85 | output_dtype=a.dtype, |
| 86 | ) |
| 87 | # Activation function |
| 88 | act_out = SiluAndMul().forward_native(inter_out) |
| 89 | # Quantize activation output with per-token |
| 90 | act_out_q, act_out_s = ops.scaled_fp8_quant( |
| 91 | act_out, use_per_token_if_dynamic=True |
| 92 | ) |
| 93 | |
| 94 | # Second MLP layer |
| 95 | out[mask] = native_w8a8_per_token_matmul( |
| 96 | act_out_q, w2[i], act_out_s, w2_s[i], output_dtype=a.dtype |
| 97 | ) |
| 98 | # Apply routing weights and sum |
| 99 | return ( |
| 100 | out.view(B, -1, w2.shape[1]) * topk_weight.view(B, -1, 1).to(out.dtype) |
| 101 | ).sum(dim=1) |
| 102 | |
| 103 | |
| 104 | @pytest.fixture(autouse=True) |
| 105 | def setup_cuda(): |
| 106 | """Sets the default CUDA device before each test in this module.""" |
| 107 | torch.set_default_device("cuda") |
| 108 | |
| 109 | |
| 110 | DTYPES = [torch.half, torch.bfloat16] |
| 111 | M = [1, 33] |
| 112 | N = [128, 1024] |
| 113 | K = [256, 4096] |
| 114 | E = [8] |
| 115 | TOP_KS = [2, 6] |
| 116 | SEEDS = [0] |
| 117 | |
| 118 | |
| 119 | @pytest.mark.parametrize( |
| 120 | "M, N, K, E, topk, dtype, seed", |
| 121 | itertools.product(M, N, K, E, TOP_KS, DTYPES, SEEDS), |
| 122 | ) |
| 123 | @torch.inference_mode() |
| 124 | def test_w8a8_fp8_fused_moe(M, N, K, E, topk, dtype, seed): |
| 125 | torch.manual_seed(seed) |
| 126 | # Initialize int8 quantization parameters |
| 127 | factor_for_scale = 1e-2 |
| 128 | finfo = torch.finfo(torch.float8_e4m3fn) |
| 129 | fp8_max = finfo.max |
| 130 | fp8_min = finfo.min |
| 131 | |
| 132 | # Input tensor |
| 133 | # M * K |
| 134 | a = torch.randn((M, K), dtype=dtype) / 10 |
| 135 | |
| 136 | # Generate int8 weights |
| 137 | w1_fp32 = (torch.rand((E, 2 * N, K), dtype=torch.float32) - 0.5) * 2 |
| 138 | w1 = (w1_fp32 * fp8_max).clamp(min=fp8_min, max=fp8_max).to(torch.float8_e4m3fn) |
| 139 | |
| 140 | w2_fp32 = (torch.rand((E, K, N), dtype=torch.float32) - 0.5) * 2 |
| 141 | w2 = (w2_fp32 * fp8_max).clamp(min=fp8_min, max=fp8_max).to(torch.float8_e4m3fn) |
| 142 | |
| 143 | # Generate scale for each column (per-column quantization) |
| 144 | w1_s = torch.rand(E, 2 * N, device=w1_fp32.device) * factor_for_scale |
| 145 | w2_s = torch.rand(E, K, device=w2_fp32.device) * factor_for_scale |
| 146 | score = torch.randn((M, E), dtype=dtype) |
| 147 | |
| 148 | with set_current_vllm_config(vllm_config): |
| 149 | ref_out = torch_w8a8_per_column_moe(a, w1, w2, w1_s, w2_s, score, topk) |
| 150 | out = fused_moe( |
| 151 | a, |
| 152 | w1, |
| 153 | w2, |
| 154 | score, |
| 155 | topk, |
| 156 | renormalize=False, |
| 157 | quant_config=fp8_w8a8_moe_quant_config( |
| 158 | per_act_token_quant=True, |
| 159 | w1_scale=w1_s, |
| 160 | w2_scale=w2_s, |
| 161 | block_shape=None, # Not using block quantization |
| 162 | ), |
| 163 | ) |
| 164 | |
| 165 | # Check results |
| 166 | rel_diff = torch.mean( |
| 167 | torch.abs(out.to(torch.float32) - ref_out.to(torch.float32)) |
| 168 | ) / torch.mean(torch.abs(ref_out.to(torch.float32))) |
| 169 | assert rel_diff < 0.05 |