forked from Karylab-cklius/vllm
315 lines
9.6 KiB
Python
315 lines
9.6 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
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),
|
|
]
|
|
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 float
|
|
# 0111 -> 6
|
|
# 0110 -> 4
|
|
# 0101 -> 3
|
|
# 0100 -> 2
|
|
# 0011 -> 1.5
|
|
# 0010 -> 1
|
|
# 0001 -> 0.5
|
|
# 0000 -> 0
|
|
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):
|
|
# The fp4 values are packed in uint8 as [v_1st | v_2nd]
|
|
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)
|
|
# Recover the swizzled scaling factor to linear layout
|
|
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]
|
|
|
|
|
|
@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")
|
|
|
|
# C++ functional variant allocates internally
|
|
cpp_out, cpp_scale = torch.ops._C.scaled_fp4_quant(
|
|
input_tensor, input_scale, is_sf_swizzled_layout
|
|
)
|
|
|
|
# Python utility
|
|
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("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)
|
|
|
|
|
|
# ============================================================================
|
|
# SM103-native quantization correctness
|
|
# ============================================================================
|
|
|
|
_SM103_QUANT_SHAPES = SHAPES + PAD_SHAPES
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not hasattr(torch.ops._C, "scaled_fp4_quant_sm103"),
|
|
reason="scaled_fp4_quant_sm103 op not available "
|
|
"(rebuild without VLLM_USE_PRECOMPILED=1)",
|
|
)
|
|
@pytest.mark.parametrize("dtype", DTYPES)
|
|
@pytest.mark.parametrize("shape", _SM103_QUANT_SHAPES)
|
|
@pytest.mark.parametrize("seed", SEEDS)
|
|
@torch.inference_mode()
|
|
def test_scaled_fp4_quant_sm103_matches_sm100(
|
|
dtype: torch.dtype,
|
|
shape: tuple[int, int],
|
|
seed: int,
|
|
) -> None:
|
|
"""
|
|
Verify scaled_fp4_quant_sm103 (SM103-native layout) against the SM100 path.
|
|
|
|
Two invariants:
|
|
1. Packed FP4 data is identical — both kernels quantize to the same
|
|
e2m1 values; only the SF memory layout differs.
|
|
2. SM103 native SFs are byte-identical to SM100 SFs run through
|
|
convert_sf_layout_sm100_to_sm103 — confirms the in-kernel swizzle
|
|
matches the standalone conversion kernel.
|
|
"""
|
|
set_random_seed(seed)
|
|
torch.set_default_device("cuda:0")
|
|
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
|
|
|
|
# SM100 reference: ops wrapper returns fp4 (uint8) and sf (float8_e4m3fn)
|
|
fp4_sm100, sf_sm100 = ops.scaled_fp4_quant(
|
|
x, global_scale, is_sf_swizzled_layout=True
|
|
)
|
|
|
|
# SM103 native: raw C++ op returns sf as int32; view as float8 to match
|
|
fp4_sm103, sf_sm103_i32 = torch.ops._C.scaled_fp4_quant_sm103(x, global_scale)
|
|
sf_sm103 = sf_sm103_i32.view(torch.float8_e4m3fn)
|
|
|
|
# 1. FP4 quantized data must be identical
|
|
assert torch.equal(fp4_sm103, fp4_sm100), (
|
|
f"FP4 data mismatch between SM100 and SM103 quant kernels "
|
|
f"(shape={shape}, dtype={dtype})"
|
|
)
|
|
|
|
# 2. SM103 native SFs must match SM100 SFs converted to SM103 layout
|
|
sf_sm100_converted = torch.empty_like(sf_sm100)
|
|
torch.ops._C.convert_sf_layout_sm100_to_sm103(sf_sm100_converted, sf_sm100)
|
|
|
|
assert torch.equal(sf_sm103.view(torch.uint8), sf_sm100_converted.view(torch.uint8)), (
|
|
f"SM103 native SF layout doesn't match "
|
|
f"convert_sf_layout_sm100_to_sm103(SM100 SFs) "
|
|
f"(shape={shape}, dtype={dtype})"
|
|
)
|