| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | import pytest |
| 4 | import torch |
| 5 | import torch.nn.functional as F |
| 6 | from nvfp4_utils import ( |
| 7 | FLOAT4_E2M1_MAX, |
| 8 | FLOAT8_E4M3_MAX, |
| 9 | convert_swizzled_to_linear, |
| 10 | dequantize_nvfp4_to_dtype, |
| 11 | ) |
| 12 | |
| 13 | from vllm import _custom_ops as ops |
| 14 | from vllm.model_executor.kernels.linear.nvfp4 import NvFp4LinearLayerConfig |
| 15 | from vllm.model_executor.kernels.linear.nvfp4.flashinfer import ( |
| 16 | FlashInferCuteDslNvFp4W4A16LinearKernel, |
| 17 | ) |
| 18 | from vllm.model_executor.layers.quantization.utils.nvfp4_emulation_utils import ( |
| 19 | dequantize_to_dtype, |
| 20 | ) |
| 21 | from vllm.platforms import current_platform |
| 22 | from vllm.utils.flashinfer import ( |
| 23 | flashinfer_scaled_fp4_mm, |
| 24 | has_flashinfer_b12x_gemm, |
| 25 | ) |
| 26 | from vllm.utils.torch_utils import set_random_seed |
| 27 | |
| 28 | if not current_platform.has_device_capability(100): |
| 29 | pytest.skip( |
| 30 | reason="Nvfp4 Requires compute capability of 10 or above.", |
| 31 | allow_module_level=True, |
| 32 | ) |
| 33 | |
| 34 | DTYPES = [torch.float16, torch.bfloat16] |
| 35 | # m, n, k |
| 36 | SHAPES = [ |
| 37 | (128, 128, 64), |
| 38 | (128, 128, 128), |
| 39 | (256, 128, 64), |
| 40 | (128, 256, 128), |
| 41 | (1, 128, 128), |
| 42 | ] |
| 43 | PAD_SHAPES = [(150, 128, 64), (128, 128, 96), (2, 128, 64), (3, 128, 96)] |
| 44 | SHAPES.extend(PAD_SHAPES) |
| 45 | |
| 46 | SEEDS = [42] |
| 47 | CUDA_DEVICES = ["cuda:0"] |
| 48 | |
| 49 | |
| 50 | def get_ref_results( |
| 51 | a_fp4, |
| 52 | b_fp4, |
| 53 | a_sf, |
| 54 | b_sf, |
| 55 | a_global_scale, |
| 56 | b_global_scale, |
| 57 | m, |
| 58 | n, |
| 59 | dtype, |
| 60 | block_size, |
| 61 | device, |
| 62 | is_sf_128x4_layout, |
| 63 | ): |
| 64 | _, m_k = a_fp4.shape |
| 65 | _, n_k = b_fp4.shape |
| 66 | assert m_k == n_k |
| 67 | a_in_dtype = dequantize_nvfp4_to_dtype( |
| 68 | a_fp4, |
| 69 | a_sf, |
| 70 | a_global_scale, |
| 71 | dtype=dtype, |
| 72 | device=device, |
| 73 | block_size=block_size, |
| 74 | is_sf_128x4_layout=is_sf_128x4_layout, |
| 75 | ) |
| 76 | b_in_dtype = dequantize_nvfp4_to_dtype( |
| 77 | b_fp4, b_sf, b_global_scale, dtype=dtype, device=device, block_size=block_size |
| 78 | ) |
| 79 | return torch.matmul(a_in_dtype, b_in_dtype.t()) |
| 80 | |
| 81 | |
| 82 | @pytest.mark.parametrize("dtype", DTYPES) |
| 83 | @pytest.mark.parametrize("shape", SHAPES) |
| 84 | @pytest.mark.parametrize("seed", SEEDS) |
| 85 | @pytest.mark.parametrize("device", CUDA_DEVICES) |
| 86 | @pytest.mark.parametrize("backend", ["cute-dsl", "cutlass", "cudnn", "trtllm", "b12x"]) |
| 87 | @pytest.mark.parametrize("autotune", [False, True]) |
| 88 | @torch.inference_mode() |
| 89 | def test_flashinfer_nvfp4_gemm( |
| 90 | dtype: torch.dtype, |
| 91 | shape: tuple[int, int, int], |
| 92 | seed: int, |
| 93 | device: str, |
| 94 | backend: str, |
| 95 | autotune: bool, |
| 96 | ) -> None: |
| 97 | if "trtllm" in backend and dtype == torch.float16: |
| 98 | pytest.skip("Only torch.bfloat16 is supported for TRTLLM FP4 GEMM operations") |
| 99 | if backend == "cute-dsl" and not current_platform.is_device_capability_family(100): |
| 100 | pytest.skip("FlashInfer cutedsl backend is only supported on SM10x") |
| 101 | if backend == "b12x" and not current_platform.has_device_capability(120): |
| 102 | pytest.skip("b12x FP4 GEMM requires SM120+ (CC 12.0+)") |
| 103 | if backend == "b12x" and not has_flashinfer_b12x_gemm(): |
| 104 | pytest.skip("b12x FP4 GEMM backend not available in installed FlashInfer") |
| 105 | |
| 106 | set_random_seed(seed) |
| 107 | m, n, packed_k = shape |
| 108 | k = packed_k * 2 |
| 109 | block_size = 16 |
| 110 | a_dtype = torch.randn((m, k), dtype=dtype, device=device) |
| 111 | b_dtype = torch.randn((n, k), dtype=dtype, device=device) |
| 112 | |
| 113 | a_global_scale = ( |
| 114 | (FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX) / torch.amax(a_dtype.flatten(), dim=-1) |
| 115 | ).to(torch.float32) |
| 116 | b_global_scale = ( |
| 117 | (FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX) / torch.amax(b_dtype.flatten(), dim=-1) |
| 118 | ).to(torch.float32) |
| 119 | alpha = 1.0 / (a_global_scale * b_global_scale) |
| 120 | |
| 121 | # ops.scaled_fp4_quant returns swizzled scales, while weights |
| 122 | # from checkpoints are in linear scales. |
| 123 | # cutlass and b12x use swizzled scales directly; trtllm needs them unswizzled. |
| 124 | a_fp4, a_scale_interleaved = ops.scaled_fp4_quant( |
| 125 | a_dtype, a_global_scale, is_sf_swizzled_layout=True, backend=backend |
| 126 | ) |
| 127 | is_sf_128x4_layout = not (backend == "trtllm" and m <= 32) |
| 128 | |
| 129 | b_fp4, b_scale_interleaved = ops.scaled_fp4_quant( |
| 130 | b_dtype, b_global_scale, is_sf_swizzled_layout=True |
| 131 | ) |
| 132 | |
| 133 | # get_ref_results unswizzles the scales internally. |
| 134 | expected_out = get_ref_results( |
| 135 | a_fp4, |
| 136 | b_fp4, |
| 137 | a_scale_interleaved, |
| 138 | b_scale_interleaved, |
| 139 | a_global_scale, |
| 140 | b_global_scale, |
| 141 | m, |
| 142 | n, |
| 143 | dtype, |
| 144 | block_size, |
| 145 | device, |
| 146 | is_sf_128x4_layout, |
| 147 | ) |
| 148 | |
| 149 | import flashinfer |
| 150 | |
| 151 | if "trtllm" in backend: |
| 152 | epilogue_tile_m = 128 |
| 153 | b_fp4 = flashinfer.shuffle_matrix_a(b_fp4.view(torch.uint8), epilogue_tile_m) |
| 154 | b_scale_interleaved = convert_swizzled_to_linear( |
| 155 | b_scale_interleaved, n, k, block_size |
| 156 | ) |
| 157 | b_scale_interleaved = ( |
| 158 | flashinfer.shuffle_matrix_sf_a( |
| 159 | b_scale_interleaved.view(torch.uint8), epilogue_tile_m |
| 160 | ) |
| 161 | .reshape(b_scale_interleaved.shape) |
| 162 | .view(torch.float8_e4m3fn) |
| 163 | ) |
| 164 | |
| 165 | with flashinfer.autotune(autotune): |
| 166 | out = flashinfer_scaled_fp4_mm( |
| 167 | a_fp4, |
| 168 | b_fp4, |
| 169 | a_scale_interleaved, |
| 170 | b_scale_interleaved, |
| 171 | alpha, |
| 172 | dtype, |
| 173 | backend=backend, |
| 174 | ) |
| 175 | |
| 176 | torch.testing.assert_close(out, expected_out.to(dtype=dtype), atol=1e-1, rtol=1e-1) |
| 177 | |
| 178 | |
| 179 | @pytest.mark.parametrize("shape", SHAPES) |
| 180 | @pytest.mark.parametrize("seed", SEEDS) |
| 181 | @pytest.mark.parametrize("device", CUDA_DEVICES) |
| 182 | @torch.inference_mode() |
| 183 | def test_flashinfer_cutedsl_nvfp4_w4a16_linear( |
| 184 | shape: tuple[int, int, int], |
| 185 | seed: int, |
| 186 | device: str, |
| 187 | ) -> None: |
| 188 | supported, reason = FlashInferCuteDslNvFp4W4A16LinearKernel.is_supported() |
| 189 | if not supported: |
| 190 | pytest.skip(reason) |
| 191 | |
| 192 | set_random_seed(seed) |
| 193 | m, n, packed_k = shape |
| 194 | k = packed_k * 2 |
| 195 | x = torch.randn((m, k), dtype=torch.bfloat16, device=device) |
| 196 | weight = torch.randn((n, k), dtype=torch.bfloat16, device=device) |
| 197 | weight_quant_scale = (FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / weight.abs().max()).to( |
| 198 | torch.float32 |
| 199 | ) |
| 200 | weight_global_scale = weight_quant_scale.reciprocal() |
| 201 | weight_fp4, weight_scale = ops.scaled_fp4_quant( |
| 202 | weight, |
| 203 | weight_quant_scale, |
| 204 | is_sf_swizzled_layout=False, |
| 205 | ) |
| 206 | weight_ref = dequantize_to_dtype( |
| 207 | weight_fp4, |
| 208 | weight_scale, |
| 209 | weight_global_scale, |
| 210 | dtype=torch.bfloat16, |
| 211 | swizzle=False, |
| 212 | ) |
| 213 | |
| 214 | layer = torch.nn.Module() |
| 215 | layer.output_size_per_partition = n |
| 216 | layer.weight = torch.nn.Parameter(weight_fp4, requires_grad=False) |
| 217 | layer.weight_scale = torch.nn.Parameter(weight_scale, requires_grad=False) |
| 218 | layer.weight_global_scale = torch.nn.Parameter( |
| 219 | weight_global_scale, requires_grad=False |
| 220 | ) |
| 221 | kernel = FlashInferCuteDslNvFp4W4A16LinearKernel(NvFp4LinearLayerConfig()) |
| 222 | |
| 223 | kernel.process_weights_after_loading(layer) |
| 224 | output = kernel.apply_weights(layer, x) |
| 225 | |
| 226 | expected = F.linear(x, weight_ref) |
| 227 | assert output.shape == (m, n) |
| 228 | torch.testing.assert_close(output, expected, atol=1e-1, rtol=1e-1) |