| |
| |
| import pytest |
| import torch |
|
|
| from vllm import _custom_ops as ops |
| from vllm.platforms import current_platform |
| from vllm.scalar_type import scalar_types |
| from vllm.utils.torch_utils import set_random_seed |
|
|
| if not current_platform.has_device_capability(100): |
| pytest.skip( |
| reason="Nvfp4 Requires compute capability of 10 or above.", |
| allow_module_level=True, |
| ) |
|
|
| DTYPES = [torch.float16, torch.bfloat16] |
| SHAPES = [(128, 64), (128, 128), (256, 64), (256, 128)] |
| PAD_SHAPES = [ |
| (90, 64), |
| (150, 64), |
| (128, 48), |
| (128, 80), |
| (150, 80), |
| (90, 48), |
| (90, 128), |
| (150, 128), |
| (150, 48), |
| (90, 80), |
| (128, 512), |
| (128, 1024), |
| (128, 2048), |
| (64, 7168), |
| (64, 7152), |
| (32, 14336), |
| ] |
| PADDED_OUTPUT_SHAPES = [(128, 48), (128, 80), (150, 48), (150, 80), (64, 7152)] |
| SEEDS = [42] |
| CUDA_DEVICES = ["cuda:0"] |
|
|
| FLOAT4_E2M1_MAX = scalar_types.float4_e2m1f.max() |
| FLOAT8_E4M3_MAX = torch.finfo(torch.float8_e4m3fn).max |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| E2M1_TO_FLOAT32 = [ |
| 0.0, |
| 0.5, |
| 1.0, |
| 1.5, |
| 2.0, |
| 3.0, |
| 4.0, |
| 6.0, |
| 0.0, |
| -0.5, |
| -1.0, |
| -1.5, |
| -2.0, |
| -3.0, |
| -4.0, |
| -6.0, |
| ] |
| BLOCK_SIZE = 16 |
|
|
|
|
| def cast_from_fp4(x, m, n): |
| |
| v_2nd = x & 0xF |
| v_1st = (x >> 4) & 0xF |
| c = torch.stack((v_2nd, v_1st), dim=-1) |
| out = torch.tensor([E2M1_TO_FLOAT32[x] for x in c.flatten()]) |
| out = out.reshape(m, n).to(torch.float32) |
| return out |
|
|
|
|
| def cast_to_fp4(x): |
| sign = torch.sign(x) |
| x = torch.abs(x) |
| x[(x >= 0.0) & (x <= 0.25)] = 0.0 |
| x[(x > 0.25) & (x < 0.75)] = 0.5 |
| x[(x >= 0.75) & (x <= 1.25)] = 1.0 |
| x[(x > 1.25) & (x < 1.75)] = 1.5 |
| x[(x >= 1.75) & (x <= 2.5)] = 2.0 |
| x[(x > 2.5) & (x < 3.5)] = 3.0 |
| x[(x >= 3.5) & (x <= 5.0)] = 4.0 |
| x[x > 5.0] = 6.0 |
| return x * sign |
|
|
|
|
| def get_reciprocal(x): |
| if isinstance(x, torch.Tensor): |
| return torch.where(x == 0, torch.tensor(0.0, dtype=x.dtype), 1.0 / x) |
| elif isinstance(x, (float, int)): |
| return 0.0 if x == 0 else 1.0 / x |
| else: |
| raise TypeError("Input must be a float, int, or a torch.Tensor.") |
|
|
|
|
| def ref_nvfp4_quant(x, global_scale): |
| assert global_scale.dtype == torch.float32 |
| assert x.ndim == 2 |
| m, n = x.shape |
| x = torch.reshape(x, (m, n // BLOCK_SIZE, BLOCK_SIZE)) |
| vec_max = torch.max(torch.abs(x), dim=-1, keepdim=True)[0].to(torch.float32) |
| scale = global_scale * (vec_max * get_reciprocal(FLOAT4_E2M1_MAX)) |
| scale = scale.to(torch.float8_e4m3fn).to(torch.float32) |
| output_scale = get_reciprocal(scale * get_reciprocal(global_scale)) |
|
|
| scaled_x = x.to(torch.float32) * output_scale |
| clipped_x = torch.clamp(scaled_x, -6.0, 6.0).reshape(m, n) |
| return cast_to_fp4(clipped_x), scale.squeeze(-1) |
|
|
|
|
| def recover_swizzled_scales(scale, m, n): |
| round_up = lambda x, y: (x + y - 1) // y * y |
| rounded_m = round_up(m, 128) |
| scale_n = n // BLOCK_SIZE |
| rounded_n = round_up(scale_n, 4) |
| |
| tmp = torch.reshape(scale, (1, rounded_m // 128, rounded_n // 4, 32, 4, 4)) |
| tmp = torch.permute(tmp, (0, 1, 4, 3, 2, 5)) |
| result = torch.reshape(tmp, (rounded_m, rounded_n)).to(torch.float32) |
| return result[:m, :scale_n] |
|
|
|
|
| def round_up(x: int, y: int) -> int: |
| return (x + y - 1) // y * y |
|
|
|
|
| @pytest.mark.parametrize("dtype", DTYPES) |
| @pytest.mark.parametrize("shape", SHAPES) |
| @pytest.mark.parametrize("seed", SEEDS) |
| @pytest.mark.parametrize("device", CUDA_DEVICES) |
| @torch.inference_mode() |
| def test_quantize_to_fp4( |
| dtype: torch.dtype, |
| shape: tuple[int, int], |
| seed: int, |
| device: str, |
| ) -> None: |
| set_random_seed(seed) |
| torch.set_default_device(device) |
|
|
| m, n = shape |
|
|
| x = torch.randn((m, n), dtype=dtype) |
| tensor_amax = torch.abs(x).max().to(torch.float32) |
| global_scale = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / tensor_amax |
| out_ref, scale_ref = ref_nvfp4_quant(x, global_scale) |
|
|
| out, out_scale = ops.scaled_fp4_quant(x, global_scale) |
| scale_ans = recover_swizzled_scales(out_scale, m, n) |
| out_ans = cast_from_fp4(out, m, n) |
|
|
| torch.testing.assert_close(out_ans, out_ref) |
| torch.testing.assert_close(scale_ans, scale_ref) |
|
|
|
|
| @pytest.mark.parametrize( |
| "shape", |
| [(32, 4096), (128, 4096), (1, 64), (127, 1024), (256, 16384)], |
| ) |
| @pytest.mark.parametrize("is_sf_swizzled_layout", [True, False]) |
| @torch.inference_mode() |
| def test_python_util_matches_cpp_allocation( |
| shape: tuple[int, int], |
| is_sf_swizzled_layout: bool, |
| ) -> None: |
| """ |
| Verify that the Python utility (create_fp4_output_tensors) allocates |
| tensors with the same shapes and dtypes as the C++ functional variant |
| (scaled_fp4_quant_func). |
| """ |
| from vllm._custom_ops import create_fp4_output_tensors |
|
|
| torch.set_default_device("cuda:0") |
| m, n = shape |
| input_tensor = torch.randn((m, n), dtype=torch.bfloat16) |
| input_scale = torch.tensor([1.0], dtype=torch.float32, device="cuda:0") |
|
|
| |
| cpp_out, cpp_scale = torch.ops._C.scaled_fp4_quant( |
| input_tensor, input_scale, is_sf_swizzled_layout |
| ) |
|
|
| |
| py_out, py_scale = create_fp4_output_tensors( |
| m, n, torch.device("cuda:0"), is_sf_swizzled_layout |
| ) |
|
|
| assert py_out.shape == cpp_out.shape, ( |
| f"Output shape mismatch: Python {py_out.shape} vs C++ {cpp_out.shape}" |
| ) |
| assert py_out.dtype == cpp_out.dtype, ( |
| f"Output dtype mismatch: Python {py_out.dtype} vs C++ {cpp_out.dtype}" |
| ) |
| assert py_scale.shape == cpp_scale.shape, ( |
| f"Scale shape mismatch: Python {py_scale.shape} vs C++ {cpp_scale.shape}" |
| ) |
| assert py_scale.dtype == cpp_scale.dtype, ( |
| f"Scale dtype mismatch: Python {py_scale.dtype} vs C++ {cpp_scale.dtype}" |
| ) |
|
|
|
|
| @pytest.mark.parametrize("shape", PADDED_OUTPUT_SHAPES) |
| @pytest.mark.parametrize("is_sf_swizzled_layout", [True, False]) |
| @torch.inference_mode() |
| def test_quantize_to_fp4_with_padded_output( |
| shape: tuple[int, int], |
| is_sf_swizzled_layout: bool, |
| ) -> None: |
| from vllm._custom_ops import create_fp4_output_tensors |
|
|
| dtype = torch.float16 |
| set_random_seed(42) |
| torch.set_default_device("cuda:0") |
|
|
| m, n = shape |
| padded_n = round_up(n, 32) |
| assert padded_n > n |
|
|
| x = torch.randn((m, n), dtype=dtype) |
| tensor_amax = torch.abs(x).max().to(torch.float32) |
| global_scale = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / tensor_amax |
| out_ref, scale_ref = ref_nvfp4_quant(x, global_scale) |
|
|
| out, out_scale = ops.scaled_fp4_quant( |
| x, |
| global_scale, |
| is_sf_swizzled_layout=is_sf_swizzled_layout, |
| padded_n=padded_n, |
| ) |
| py_out, py_scale = create_fp4_output_tensors( |
| m, |
| n, |
| torch.device("cuda:0"), |
| is_sf_swizzled_layout, |
| padded_n=padded_n, |
| ) |
|
|
| assert out.shape == (m, padded_n // 2) |
| assert out.shape == py_out.shape |
| assert out_scale.shape == py_scale.view(torch.float8_e4m3fn).shape |
|
|
| out_ans = cast_from_fp4(out[:, : n // 2], m, n) |
| torch.testing.assert_close(out_ans, out_ref) |
| assert torch.count_nonzero(out[:, n // 2 :]) == 0 |
|
|
| if is_sf_swizzled_layout: |
| scale_ans = recover_swizzled_scales(out_scale, m, padded_n) |
| torch.testing.assert_close(scale_ans[:, : n // BLOCK_SIZE], scale_ref) |
| assert torch.count_nonzero(scale_ans[:, n // BLOCK_SIZE :]) == 0 |
| else: |
| scale_ans = out_scale.to(torch.float32) |
| torch.testing.assert_close(scale_ans[:, : n // BLOCK_SIZE], scale_ref) |
| assert torch.count_nonzero(scale_ans[:, n // BLOCK_SIZE :]) == 0 |
|
|
|
|
| @pytest.mark.parametrize("pad_shape", PAD_SHAPES) |
| @torch.inference_mode() |
| def test_quantize_to_fp4_padded(pad_shape: tuple[int, int]) -> None: |
| dtype = torch.float16 |
| set_random_seed(42) |
| torch.set_default_device("cuda:0") |
|
|
| m, n = pad_shape |
|
|
| x = torch.randn((m, n), dtype=dtype) |
|
|
| tensor_amax = torch.abs(x).max().to(torch.float32) |
| global_scale = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / tensor_amax |
| out_ref, scale_ref = ref_nvfp4_quant(x, global_scale) |
|
|
| out, out_scale = ops.scaled_fp4_quant(x, global_scale) |
| scale_ans = recover_swizzled_scales(out_scale, m, n) |
| out_ans = cast_from_fp4(out, m, n) |
| torch.testing.assert_close(out_ans, out_ref) |
| torch.testing.assert_close(scale_ans, scale_ref) |
|
|
|
|
| @pytest.mark.parametrize("pad_shape", PAD_SHAPES) |
| @torch.inference_mode() |
| def test_quantize_to_fp4_padded_no_sf_swizzled(pad_shape: tuple[int, int]) -> None: |
| dtype = torch.float16 |
| set_random_seed(42) |
| torch.set_default_device("cuda:0") |
|
|
| m, n = pad_shape |
|
|
| x = torch.randn((m, n), dtype=dtype) |
|
|
| tensor_amax = torch.abs(x).max().to(torch.float32) |
| global_scale = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / tensor_amax |
| out_ref, scale_ref = ref_nvfp4_quant(x, global_scale) |
|
|
| out, out_scale = ops.scaled_fp4_quant(x, global_scale, is_sf_swizzled_layout=False) |
| scale_ans = out_scale.to(torch.float32) |
| out_ans = cast_from_fp4(out, m, n) |
| torch.testing.assert_close(out_ans, out_ref) |
| torch.testing.assert_close(scale_ans, scale_ref) |
|
|