Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions tests/kernels/quantization/nvfp4_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,18 @@ def convert_swizzled_to_linear(a_sf_swizzled: torch.Tensor, m, k, block_size):
return out[0:m, 0:k]


def convert_swizzled_8x4_layout_to_linear(a_sf_swizzled: torch.Tensor, m, k, block_size):
m_tiles = (m + 8 - 1) // 8
f = block_size * 4
k_tiles = (k + f - 1) // f
tmp = torch.reshape(a_sf_swizzled, (1, m_tiles, k_tiles, 8, 4))
tmp = torch.permute(tmp, (0, 1, 3, 2, 4))
out = tmp.reshape(m_tiles * 8, k_tiles * f // block_size)
return out[0:m, 0:k]


def dequantize_nvfp4_to_dtype(
tensor_fp4, tensor_sf, global_scale, dtype, device, block_size=16
tensor_fp4, tensor_sf, global_scale, dtype, device, block_size=16, is_sf_128x4_layout=True
):
"""Dequantize the fp4 tensor back to high precision."""
# Two fp4 values are packed into one uint8.
Expand All @@ -34,7 +44,11 @@ def dequantize_nvfp4_to_dtype(
tensor_f32 = break_fp4_bytes(tensor_fp4, dtype)
tensor_f32 = tensor_f32.reshape(m, k // block_size, block_size)
tensor_sf = tensor_sf.view(torch.float8_e4m3fn)
tensor_sf = convert_swizzled_to_linear(tensor_sf, m, k, block_size)
if is_sf_128x4_layout:
tensor_sf = convert_swizzled_to_linear(tensor_sf, m, k, block_size)
else:
tensor_sf = convert_swizzled_8x4_layout_to_linear(tensor_sf, m, k, block_size)

tensor_sf_dtype = tensor_sf.to(torch.float32) / global_scale

# scale the tensor
Expand Down
36 changes: 25 additions & 11 deletions tests/kernels/quantization/test_flashinfer_nvfp4_scaled_mm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from vllm import _custom_ops as ops
from vllm.platforms import current_platform
from vllm.utils.flashinfer import flashinfer_scaled_fp4_mm
from vllm.utils.flashinfer import flashinfer_quant_nvfp4_8x4_sf_layout, flashinfer_scaled_fp4_mm

if not current_platform.has_device_capability(100):
pytest.skip(
Expand Down Expand Up @@ -41,12 +41,19 @@ def get_ref_results(
dtype,
block_size,
device,
is_sf_128x4_layout,
):
_, m_k = a_fp4.shape
_, n_k = b_fp4.shape
assert m_k == n_k
a_in_dtype = dequantize_nvfp4_to_dtype(
a_fp4, a_sf, a_global_scale, dtype=dtype, device=device, block_size=block_size
a_fp4,
a_sf,
a_global_scale,
dtype=dtype,
device=device,
block_size=block_size,
is_sf_128x4_layout=is_sf_128x4_layout,
)
b_in_dtype = dequantize_nvfp4_to_dtype(
b_fp4, b_sf, b_global_scale, dtype=dtype, device=device, block_size=block_size
Expand All @@ -58,7 +65,7 @@ def get_ref_results(
@pytest.mark.parametrize("shape", SHAPES)
@pytest.mark.parametrize("seed", SEEDS)
@pytest.mark.parametrize("device", CUDA_DEVICES)
@pytest.mark.parametrize("backend", ["cutlass", "trtllm"])
@pytest.mark.parametrize("backend", ["cutlass", "trtllm", "trtllm_8x4_sf_layout"])
@pytest.mark.parametrize("autotune", [False, True])
@torch.inference_mode()
def test_flashinfer_nvfp4_gemm(
Expand All @@ -69,7 +76,7 @@ def test_flashinfer_nvfp4_gemm(
backend: str,
autotune: bool,
) -> None:
if backend == "trtllm" and dtype == torch.float16:
if "trtllm" in backend and dtype == torch.float16:
pytest.skip("Only torch.bfloat16 is supported for TRTLLM FP4 GEMM operations")

current_platform.seed_everything(seed)
Expand All @@ -86,11 +93,18 @@ def test_flashinfer_nvfp4_gemm(
(FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX) / torch.amax(b_dtype.flatten(), dim=-1)
).to(torch.float32)
alpha = 1.0 / (a_global_scale * b_global_scale)
# ops.scaled_fp4_quant returns swizzled scales, while weights
# from checkpoints are in linear scales.
# So instead of needing to swizzle for cutlass as in modelopt.py,
# we need to unswizzle for trtllm here.
a_fp4, a_scale_interleaved = ops.scaled_fp4_quant(a_dtype, a_global_scale)

if backend == "trtllm_8x4_sf_layout":
a_fp4, a_scale_interleaved = flashinfer_quant_nvfp4_8x4_sf_layout(a_dtype, a_global_scale)
is_sf_128x4_layout = False
else:
# ops.scaled_fp4_quant returns swizzled scales, while weights
# from checkpoints are in linear scales.
# So instead of needing to swizzle for cutlass as in modelopt.py,
# we need to unswizzle for trtllm here.
a_fp4, a_scale_interleaved = ops.scaled_fp4_quant(a_dtype, a_global_scale)
is_sf_128x4_layout = True

b_fp4, b_scale_interleaved = ops.scaled_fp4_quant(b_dtype, b_global_scale)

# get_ref_results unswizzles the scales internally.
Expand All @@ -106,14 +120,14 @@ def test_flashinfer_nvfp4_gemm(
dtype,
block_size,
device,
is_sf_128x4_layout,
)

import flashinfer

if backend == "trtllm":
if "trtllm" in backend:
epilogue_tile_m = 128
b_fp4 = flashinfer.shuffle_matrix_a(b_fp4.view(torch.uint8), epilogue_tile_m)

b_scale_interleaved = convert_swizzled_to_linear(
b_scale_interleaved, n, k, block_size
)
Expand Down
16 changes: 16 additions & 0 deletions tests/models/quantization/test_nvfp4.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,19 @@ def test_models(example_prompts, model_name) -> None:
assert expected_str == generated_str, (
f"Test{i}:\nExpected: {expected_str!r}\nvLLM: {generated_str!r}"
)


EAGER = [True, False]

@pytest.mark.skipif(
not is_quant_method_supported("modelopt_fp4"),
reason="modelopt_fp4 is not supported on this GPU type.",
)
@pytest.mark.parametrize("model", MODELS)
@pytest.mark.parametrize("eager", EAGER)
@pytest.mark.parametrize("backend", ["flashinfer-cudnn", "flashinfer-trtllm", "flashinfer-cutlass", "flashinfer-trtllm_8x4_sf_layout"])
def test_nvfp4(vllm_runner, model, eager, backend, monkeypatch):
monkeypatch.setenv("VLLM_NVFP4_GEMM_BACKEND", backend)
with vllm_runner(model, enforce_eager=eager) as llm:
output = llm.generate_greedy(["1 2 3 4 5"], max_tokens=2)
assert output[0][1] == "1 2 3 4 5 6"
8 changes: 7 additions & 1 deletion vllm/envs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1408,7 +1408,13 @@ def get_vllm_port() -> int | None:
"VLLM_NVFP4_GEMM_BACKEND": env_with_choices(
"VLLM_NVFP4_GEMM_BACKEND",
None,
["flashinfer-cudnn", "flashinfer-trtllm", "flashinfer-cutlass", "cutlass"],
[
"flashinfer-cudnn",
"flashinfer-trtllm",
"flashinfer-cutlass",
"flashinfer-trtllm_8x4_sf_layout",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should enable this by default and let the autotuner pick the suitable tile size. I'm concerned it may cause unintended confusion to the users.

"cutlass",
],
),
# Controls garbage collection during CUDA graph capture.
# If set to 0 (default), enables GC freezing to speed up capture time.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
ModelWeightParameter,
PerTensorScaleParameter,
)
from vllm.utils.flashinfer import flashinfer_scaled_fp4_mm, has_flashinfer
from vllm.utils.flashinfer import (
flashinfer_quant_nvfp4_8x4_sf_layout,
flashinfer_scaled_fp4_mm,
has_flashinfer,
)

logger = init_logger(__name__)

Expand Down Expand Up @@ -131,7 +135,10 @@ def process_weights_after_loading(self, layer) -> None:
layer.weight_global_scale.max().to(torch.float32), requires_grad=False
)

if self.backend == "flashinfer-trtllm":
if (
self.backend == "flashinfer-trtllm"
or self.backend == "flashinfer-trtllm_8x4_sf_layout"
):
# FlashInfer TRTLLM FP4 GEMM requires a different weight layout.
# FlashInfer provides nvfp4_quantize to quantize + shuffle the
# layout but we use our own quantization so we have to call
Expand Down Expand Up @@ -186,8 +193,12 @@ def apply_weights(
output_dtype = x.dtype
output_shape = [*x.shape[:-1], layer.weight_packed.shape[0]]

# quantize BF16 or FP16 to (FP4 and interleaved block scale)
x_fp4, x_blockscale = scaled_fp4_quant(x, layer.input_global_scale)
if self.backend == "flashinfer-trtllm_8x4_sf_layout":
x_fp4, x_blockscale = flashinfer_quant_nvfp4_8x4_sf_layout(x, layer.input_scale_inv)
x_blockscale = x_blockscale.view(torch.float8_e4m3fn)
Comment thread
LopezCastroRoberto marked this conversation as resolved.
Outdated
Comment thread
LopezCastroRoberto marked this conversation as resolved.
Outdated
else:
# quantize BF16 or FP16 to (FP4 and interleaved block scale)
x_fp4, x_blockscale = scaled_fp4_quant(x, layer.input_scale_inv)
Comment thread
LopezCastroRoberto marked this conversation as resolved.
Outdated

mm_args = (
x_fp4,
Expand Down
13 changes: 10 additions & 3 deletions vllm/model_executor/layers/quantization/modelopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
flashinfer_scaled_fp4_mm,
has_flashinfer,
has_flashinfer_moe,
flashinfer_quant_nvfp4_8x4_sf_layout
)
from vllm.utils.math_utils import round_up

Expand Down Expand Up @@ -1083,7 +1084,7 @@ def process_weights_after_loading(self, layer: Module) -> None:
prepare_fp4_layer_for_marlin(layer)
del layer.alpha
del layer.input_scale
elif self.backend == "flashinfer-trtllm":
elif self.backend == "flashinfer-trtllm" or self.backend == "flashinfer-trtllm_8x4_sf_layout":
# FlashInfer TRTLLM FP4 GEMM requires a different weight layout.
# FlashInfer provides nvfp4_quantize to quantize + shuffle the
# layout but we use our own quantization so we have to call
Expand Down Expand Up @@ -1130,8 +1131,14 @@ def apply(
output_dtype = x.dtype
output_shape = [x.shape[0], layer.weight.shape[0]]

# quantize BF16 or FP16 to (FP4 and interleaved block scale)
x_fp4, x_blockscale = scaled_fp4_quant(x, layer.input_scale_inv)
if self.backend == "flashinfer-trtllm_8x4_sf_layout":
x_fp4, x_blockscale = flashinfer_quant_nvfp4_8x4_sf_layout(
x, layer.input_scale_inv
)
x_blockscale = x_blockscale.view(torch.float8_e4m3fn)
else:
# quantize BF16 or FP16 to (FP4 and interleaved block scale)
x_fp4, x_blockscale = scaled_fp4_quant(x, layer.input_scale_inv)

# validate dtypes of quantized input, input block scale,
# weight and weight_blockscale
Expand Down
56 changes: 55 additions & 1 deletion vllm/utils/flashinfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,12 +389,21 @@ def flashinfer_mm_fp4(
B_scale: torch.Tensor,
g_scale: torch.Tensor,
dtype: torch.dtype,
use_8x4_sf_layout: bool,
backend: str,
) -> torch.Tensor:
from flashinfer import mm_fp4 as flashinfer_mm_fp4_

return flashinfer_mm_fp4_(
A, B, A_scale, B_scale, g_scale, dtype, block_size=16, backend=backend
A,
B,
A_scale,
B_scale,
g_scale,
dtype,
block_size=16,
use_8x4_sf_layout=use_8x4_sf_layout,

@pavanimajety pavanimajety Dec 17, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps make this an automated setting based on when 8x4_sf would be a better choice like A.shape[0] < 32 ?

@LopezCastroRoberto LopezCastroRoberto Dec 19, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, based on my benchmarks this is the right choice. I would also make this backend the default automatically in those cases.

backend=backend,
)

@torch.library.register_fake(
Expand All @@ -407,6 +416,7 @@ def flashinfer_mm_fp4_fake(
B_scale: torch.Tensor,
g_scale: torch.Tensor,
dtype: torch.dtype,
use_8x4_sf_layout: bool,
backend: str,
) -> torch.Tensor:
return torch.empty(A.shape[0], B.shape[1], dtype=dtype, device=A.device)
Expand Down Expand Up @@ -443,6 +453,36 @@ def bmm_fp8_fake(
A.shape[0], A.shape[1], B.shape[2], dtype=dtype, device=A.device
)

@torch.library.custom_op(
"vllm::flashinfer_nvfp4_quantize",
mutates_args=[],
device_types="cuda",
)
def flashinfer_nvfp4_quantize(
a: torch.Tensor,
a_global_sf: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor]:
from flashinfer import SfLayout
from flashinfer import nvfp4_quantize as nvfp4_quantize_

return nvfp4_quantize_(
a, a_global_sf, sfLayout=SfLayout.layout_8x4, do_shuffle=False
)

@torch.library.register_fake(
"vllm::flashinfer_nvfp4_quantize",
)
def flashinfer_nvfp4_quantize_fake(
a: torch.Tensor,
a_global_sf: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor]:
padded_rows = ((a.shape[0] + 7) // 8) * 8
return torch.empty(
a.shape[0], a.shape[1] // 2, dtype=torch.uint8, device=a.device
), torch.empty(
padded_rows, a.shape[1] // 16, dtype=torch.uint8, device=a.device
)


def flashinfer_scaled_fp4_mm(
a: torch.Tensor,
Expand All @@ -462,13 +502,20 @@ def flashinfer_scaled_fp4_mm(
block_scale_a = block_scale_a.view(torch.uint8)
block_scale_b = block_scale_b.view(torch.uint8)

if backend == "trtllm_8x4_sf_layout":
use_8x4_sf_layout = True
backend = "trtllm"
else:
use_8x4_sf_layout = False

return flashinfer_mm_fp4(
a,
b.t(),
block_scale_a,
block_scale_b.t(),
alpha,
out_dtype,
use_8x4_sf_layout=use_8x4_sf_layout,
backend=backend,
)

Expand Down Expand Up @@ -503,6 +550,12 @@ def flashinfer_scaled_fp8_mm(
return output


def flashinfer_quant_nvfp4_8x4_sf_layout(
a: torch.Tensor, a_global_sf: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor]:
return flashinfer_nvfp4_quantize(a, a_global_sf)


__all__ = [
"has_flashinfer",
"flashinfer_trtllm_fp8_block_scale_moe",
Expand All @@ -525,4 +578,5 @@ def flashinfer_scaled_fp8_mm(
"use_trtllm_attention",
"flashinfer_scaled_fp4_mm",
"flashinfer_scaled_fp8_mm",
"nvfp4_quantize",
Comment thread
LopezCastroRoberto marked this conversation as resolved.
Outdated
]
Loading