[Online quantization] Add online MXFP4 quantization support (#49347)

Signed-off-by: Felix Marty <Felix.Marty@amd.com>
This commit is contained in:
fxmarty-amd
2026-08-07 12:39:46 -07:00
committed by GitHub
parent a671679e9f
commit 3518110f2f
18 changed files with 1423 additions and 35 deletions
+15
View File
@@ -20,6 +20,16 @@ llm = LLM("meta-llama/Llama-3.1-8B", quantization="fp8_per_block")
# MXFP8 quantization for weights and activations
llm = LLM("meta-llama/Llama-3.1-8B", quantization="mxfp8")
# MXFP4 weight; activation quantization depends on the `linear_backend` picked
llm = LLM("meta-llama/Llama-3.1-8B", quantization="mxfp4")
# MXFP4 MOE-only weight and activation quantization
llm = LLM(
"Qwen/Qwen3.5-35B-A3B",
quantization="mxfp4",
quantization_config={"linear": {"activation": None, "weight": None}}
)
```
Or with the CLI:
@@ -28,6 +38,10 @@ Or with the CLI:
vllm serve meta-llama/Llama-3.1-8B --quantization fp8_per_tensor
vllm serve meta-llama/Llama-3.1-8B --quantization fp8_per_block
vllm serve meta-llama/Llama-3.1-8B --quantization mxfp8
vllm serve meta-llama/Llama-3.1-8B --quantization mxfp4
vllm serve Qwen/Qwen3.5-35B-A3B --quantization mxfp4 \
--quantization-config '{"linear":{"activation":null,"weight":null}}'
```
## Supported Schemes
@@ -37,6 +51,7 @@ vllm serve meta-llama/Llama-3.1-8B --quantization mxfp8
| `fp8_per_tensor` | fp8_e4m3 data, fp32 per-tensor scale | fp8_e4m3 data, fp32 per-tensor scale | On some GPUs (Ada, Hopper) linear activations use per-token scaling for better performance |
| `fp8_per_block` | fp8_e4m3 data, fp32 per-128x128-block scale | fp8_e4m3 data, fp32 per-1x128-block scale | |
| `mxfp8` | fp8_e4m3 data, e8m0 per-1x32-block scale | fp8_e4m3 data, e8m0 per-1x32-block scale | Requires SM 100+ (Blackwell or newer) for w8a8, other GPUs use a w8a16 fallback |
| `mxfp4` | fp4_e2m1 data, e8m0 per-1x32-block scale ([OCP MX specs](https://www.opencompute.org/documents/ocp-microscaling-formats-mx-v1-0-spec-final-pdf)) | - linear: fp4_e2m1 data, e8m0 per-1x32-block scale in some backends, or BF16. <br> - MOE: fp4_e2m1 data, e8m0 per-1x32-block scale. | Linear MXFP4 backend is auto-selected per platform, not enforcing activation dtype. Some use BF16 activation. Use `--linear-backend` to pin one (e.g. `--linear-backend flashinfer`). |
## Advanced Configuration
@@ -0,0 +1,15 @@
model_name: "Qwen/Qwen3-30B-A3B"
accuracy_threshold: 0.89
tolerance: 0.03
num_questions: 1319
num_fewshot: 5
server_args: >-
--max-model-len 4096
--tensor-parallel-size 2
--gpu-memory-utilization 0.35
--moe-backend aiter
--quantization mxfp4
--quantization-config '{"linear":{"activation":null,"weight":null}}'
env:
VLLM_ROCM_USE_AITER: "1"
ENABLE_CK: "0" # Avoid AITER CK-based MHA JIT compilation to save time
@@ -0,0 +1,14 @@
# Note: this model was produced from Qwen3-30B-A3B (BF16)
model_name: "amd-quark/Qwen3-30B-A3B-MXFP4"
accuracy_threshold: 0.89
tolerance: 0.03
num_questions: 1319
num_fewshot: 5
server_args: >-
--max-model-len 4096
--tensor-parallel-size 2
--gpu-memory-utilization 0.35
--moe-backend aiter
env:
VLLM_ROCM_USE_AITER: "1"
ENABLE_CK: "0" # Avoid AITER CK-based MHA JIT compilation to save time
@@ -1,3 +1,4 @@
# NOTE: this model was produced from Qwen/Qwen3.5-35B-A3B-FP8
model_name: "amd/Qwen3.5-35B-A3B-MXFP4"
accuracy_threshold: 0.89
tolerance: 0.03
@@ -1,3 +1,4 @@
# NOTE: this model was produced from Qwen/Qwen3.5-35B-A3B-FP8
model_name: "amd/Qwen3.5-35B-A3B-MXFP4"
accuracy_threshold: 0.89
tolerance: 0.03
@@ -114,9 +114,15 @@ def test_weight_only_kernels_reject_non_mxfp4_activation(kernel_cls):
"activation_quant_key",
[None, kMxfp4Dynamic, kMxfp6E3M2Dynamic, kMxfp6E2M3Dynamic],
)
def test_emulation_kernel_accepts_any_config(activation_quant_key):
def test_emulation_kernel_accepts_any_config(activation_quant_key, monkeypatch):
"""EmulationMxfp4LinearKernel is the universal fallback: it must accept
every supported activation format."""
# `EmulationMxfp4LinearKernel.can_implement` gates on `has_quark()`,
# which we are not testing here.
monkeypatch.setattr(
"vllm.model_executor.kernels.linear.mxfp4.emulation.has_quark",
lambda: True,
)
config = MxFp4LinearLayerConfig(activation_quant_key=activation_quant_key)
with patch(
"vllm.model_executor.kernels.linear._get_linear_backend",
@@ -126,9 +132,15 @@ def test_emulation_kernel_accepts_any_config(activation_quant_key):
assert can_implement, reason
def test_emulation_kernel_derives_quant_dequant_func_from_config():
def test_emulation_kernel_derives_quant_dequant_func_from_config(monkeypatch):
"""quant_dequant_func must be derived purely from the config's activation
QuantKey, not set externally."""
# `EmulationMxfp4LinearKernel.can_implement` gates on `has_quark()`,
# which we are not testing here.
monkeypatch.setattr(
"vllm.model_executor.kernels.linear.mxfp4.emulation.has_quark",
lambda: True,
)
with patch(
"vllm.model_executor.kernels.linear._get_linear_backend",
return_value="emulation",
+34 -3
View File
@@ -14,6 +14,7 @@ from tests.quantization.utils import (
is_quant_method_supported,
)
from vllm import _custom_ops as ops
from vllm._aiter_ops import rocm_aiter_ops
from vllm._custom_ops import scaled_fp4_quant
from vllm.model_executor.layers.linear import UnquantizedLinearMethod
from vllm.model_executor.layers.quantization.online.fp8 import (
@@ -27,6 +28,10 @@ from vllm.model_executor.layers.quantization.online.fp8 import (
_is_tp_sharded,
)
from vllm.model_executor.layers.quantization.online.int8 import Int8OnlineMoEMethod
from vllm.model_executor.layers.quantization.online.mxfp4 import (
Mxfp4OnlineLinearMethod,
Mxfp4OnlineMoEMethod,
)
from vllm.model_executor.layers.quantization.online.nvfp4 import (
Nvfp4OnlineMoEMethod,
_quantize_moe_weight_to_nvfp4,
@@ -40,6 +45,17 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
from vllm.platforms import current_platform
from vllm.utils.flashinfer import has_flashinfer_trtllm_fused_moe
if current_platform.is_rocm():
from vllm.platforms.rocm import on_gfx942, on_gfx950
else:
def on_gfx950() -> bool:
return False
def on_gfx942() -> bool:
return False
DEVICE = current_platform.device_type
@@ -84,6 +100,12 @@ DEVICE = current_platform.device_type
Fp8PerTensorOnlineLinearMethod,
Fp8PerTensorOnlineMoEMethod,
),
(
"mxfp4",
None,
Mxfp4OnlineLinearMethod,
Mxfp4OnlineMoEMethod,
),
],
)
@pytest.mark.parametrize(
@@ -106,8 +128,14 @@ def test_online_quantization(
Does not test performance, peak memory usage, etc.
"""
if use_rocm_aiter:
monkeypatch.setenv("VLLM_ROCM_USE_AITER", "1")
# TODO: Relax this condition once there is a native MXFP4_MXFP4
# linear/moe backend supported on cuda.
if quant_scheme == "mxfp4" and not (on_gfx950() or on_gfx942()):
pytest.skip("mxfp4 online quantization is only tested on AMD gfx942, gfx950.")
if current_platform.is_rocm():
monkeypatch.setenv("VLLM_ROCM_USE_AITER", "1" if use_rocm_aiter else "0")
rocm_aiter_ops.refresh_env_variables()
if current_platform.is_xpu() and quant_scheme == "fp8_per_block":
pytest.skip("Skip test for online fp8_per_block on XPU platform.")
@@ -144,7 +172,10 @@ def test_online_quantization(
if moe is not None:
assert isinstance(moe._quant_method, expected_moe_cls)
if current_platform.is_cuda() or current_platform.is_xpu():
if quant_scheme == "mxfp4":
# Packed e2m1 values, two per byte.
assert o_proj.weight.dtype == torch.uint8
elif current_platform.is_cuda() or current_platform.is_xpu():
assert o_proj.weight.dtype == torch.float8_e4m3fn
elif current_platform.is_rocm():
assert o_proj.weight.dtype == current_platform.fp8_dtype()
+606
View File
@@ -0,0 +1,606 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Correctness tests for the online MXFP4 weight quantization backends.
Each backend (Triton, aiter, XPU) quantizes a synthetic bf16/fp16
tensor to MXFP4, the result is dequantized back to bf16/fp16, and compared
against the pure-torch reference in `reference_mxfp4.py`.
"""
import pytest
import torch
from vllm.config.model import ModelConfig
from vllm.model_executor.kernels.linear import _POSSIBLE_MXFP4_KERNELS
from vllm.model_executor.kernels.linear.mxfp4.aiter import (
AiterMxfp4LinearKernel,
)
from vllm.model_executor.layers.fused_moe import FusedMoEFactory
from vllm.model_executor.layers.fused_moe.routed_experts import RoutedExperts
from vllm.model_executor.layers.quantization.online.mxfp4 import (
Mxfp4OnlineLinearMethod,
Mxfp4OnlineMoEMethod,
)
from vllm.model_executor.layers.quantization.quark.quark_moe import (
QuarkOCP_MX_MoEMethod,
)
from vllm.model_executor.layers.quantization.quark.schemes.quark_ocp_mx import (
QuarkOCP_MX,
)
from vllm.model_executor.layers.quantization.quark.utils import (
quark_quantize_weight_to_mxfp4,
)
from vllm.model_executor.layers.quantization.utils.mxfp4_utils import (
downcast_to_mxfp,
mxfp4_quantize,
quant_dequant_mxfp4,
)
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm.model_executor.utils import replace_parameter
from vllm.platforms import PlatformEnum, current_platform
from .reference_mxfp4 import dq_mxfp4_torch, qdq_mxfp4_torch
def fix_negative_zeros(tensor: torch.Tensor) -> torch.Tensor:
"""Normalize FP4 e2m1 negative-zero codewords (0b1000) to positive zero
(0b0000) in a packed uint8 tensor (two e2m1 values per byte, low nibble
first).
-0.0 and +0.0 dequantize to the same float value, but different MXFP4
quantization backends disagree on which one they emit for values that
round to zero magnitude. This normalizes both packed representations so
tensors from different backends can be compared with `torch.equal`.
"""
assert tensor.dtype == torch.uint8, (
f"Expected a torch.uint8 tensor, got {tensor.dtype}"
)
low_nibble = tensor & 0x0F
high_nibble = tensor & 0xF0
low_nibble = torch.where(
low_nibble == 0x08, torch.zeros_like(low_nibble), low_nibble
)
high_nibble = torch.where(
high_nibble == 0x80, torch.zeros_like(high_nibble), high_nibble
)
return low_nibble | high_nibble
def assert_quantized_weights_equal(
checkpoint_layer: torch.nn.Module,
online_layer: torch.nn.Module,
keys: tuple[str, ...],
packed_weight_keys: tuple[str, ...],
) -> None:
"""Assert the checkpoint and online paths produced byte-identical weights.
`packed_weight_keys` lists the entries holding packed FP4 codewords, which
need negative-zero normalization before comparison.
"""
for key in keys:
checkpoint_tensor = getattr(checkpoint_layer, key).view(torch.uint8)
online_tensor = getattr(online_layer, key).view(torch.uint8)
# NOTE: AMD Quark checkpoints use exclusively **positive** zeros,
# while other mxfp4_quantize implementations from mxfp4_utils.py may not.
if key in packed_weight_keys:
checkpoint_tensor = fix_negative_zeros(checkpoint_tensor)
online_tensor = fix_negative_zeros(online_tensor)
assert checkpoint_tensor.shape == online_tensor.shape
assert checkpoint_tensor.dtype == online_tensor.dtype
num_mismatched = (checkpoint_tensor != online_tensor).sum().item()
total = checkpoint_tensor.numel()
assert torch.equal(checkpoint_tensor, online_tensor), (
f"{key}: {num_mismatched}/{total} "
f"({100 * num_mismatched / total:.4f}%) mismatched bytes"
)
def _skip_reason_if_unavailable(backend: str, dtype: torch.dtype) -> str | None:
"""Return a skip reason if `backend` cannot run on the current host."""
if backend == "triton":
if not (current_platform.is_cuda() or current_platform.is_rocm()):
return "Triton MXFP4 kernel requires a CUDA or ROCm GPU."
return None
if backend == "aiter":
from vllm._aiter_ops import is_aiter_found_and_supported
if not is_aiter_found_and_supported():
return "aiter is not available/supported on this platform."
if dtype != torch.bfloat16:
return "aiter's dynamic_mxfp4_quant only supports bfloat16 input."
return None
if backend == "xpu":
if not current_platform.is_xpu():
return "not on XPU platform."
return None
if backend == "quark":
try:
import quark.torch.kernel.mx # noqa: F401
except ImportError:
return "amd-quark is not installed."
return None
raise ValueError(f"Unknown backend {backend}")
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
@pytest.mark.parametrize("backend", ["triton", "aiter", "xpu", "quark"])
def test_mxfp4_quantization_correctness(backend: str, dtype: torch.dtype):
"""
Tests that the different implementations of mxfp4_quantize
in mxfp4_utils.py all match.
"""
skip_reason = _skip_reason_if_unavailable(backend, dtype)
if skip_reason is not None:
pytest.skip(skip_reason)
torch.manual_seed(3)
num_rows = 64
hidden_size = 32 * 32 # multiple 32-element MXFP4 blocks
device = current_platform.device_type
x = (torch.rand(num_rows, hidden_size, dtype=dtype, device=device) - 0.5) * 2
# Vary the magnitude block-to-block so several scale exponents are
# exercised, rather than a single one for the whole tensor.
scalings = [2.3, 0.03, 7.3, 0.1, 0.004, 17.3, 1e4, 1e-4]
for i in range(hidden_size // 32):
x[:, i * 32 : (i + 1) * 32] *= scalings[i % len(scalings)]
if backend == "triton":
x_fp4, x_scale, _ = downcast_to_mxfp(x, axis=-1)
elif backend == "aiter":
x_fp4, x_scale = quark_quantize_weight_to_mxfp4(x)
elif backend == "xpu":
# TODO: enable this test on XPU
pytest.skip("xpu mxfp4 quantization match to reference is untested")
# from vllm.model_executor.layers.quantization.utils.mxfp4_utils import (
# xpu_mxfp4_quantize,
# )
# x_fp4, x_scale = xpu_mxfp4_quantize(x)
elif backend == "quark":
result = quant_dequant_mxfp4(x, scale_calculation_mode="even")
else:
raise ValueError(f"Unknown backend {backend}")
if backend != "quark":
result = dq_mxfp4_torch(x_fp4, x_scale, x.dtype)
reference = qdq_mxfp4_torch(x, scale_calculation_mode="even")
assert torch.equal(result, reference)
@pytest.mark.skipif(
not current_platform.is_cuda_alike(), reason="Only tested on ROCm/CUDA."
)
@pytest.mark.parametrize("tp_size", [2, 4, 8])
def test_online_mxfp4_tp_weight_quant_matches_unsharded(tp_size: int):
"""TP-aligned MXFP4 shards match slices of unsharded quantization."""
torch.manual_seed(3)
device = current_platform.device_type
hidden_size = 1024
intermediate_size = 512
dense_weight = torch.randn(
hidden_size, intermediate_size, dtype=torch.bfloat16, device=device
)
w13_weight = torch.randn(
2,
2 * intermediate_size,
hidden_size,
dtype=torch.bfloat16,
device=device,
)
w2_weight = torch.randn(
2,
hidden_size,
intermediate_size,
dtype=torch.bfloat16,
device=device,
)
dense_quantized, dense_scales = mxfp4_quantize(dense_weight)
w13_quantized, w13_scales = mxfp4_quantize(w13_weight)
w2_quantized, w2_scales = mxfp4_quantize(w2_weight)
for tp_rank in range(tp_size):
output_per_partition = hidden_size // tp_size
input_per_partition = intermediate_size // tp_size
output_start = tp_rank * output_per_partition
input_start = tp_rank * input_per_partition
column_quantized, column_scales = mxfp4_quantize(
dense_weight.narrow(0, output_start, output_per_partition)
)
assert torch.equal(
column_quantized,
dense_quantized.narrow(0, output_start, output_per_partition),
)
assert torch.equal(
column_scales,
dense_scales.narrow(0, output_start, output_per_partition),
)
row_quantized, row_scales = mxfp4_quantize(
dense_weight.narrow(1, input_start, input_per_partition)
)
assert torch.equal(
row_quantized,
dense_quantized.narrow(1, input_start // 2, input_per_partition // 2),
)
assert torch.equal(
row_scales,
dense_scales.narrow(1, input_start // 32, input_per_partition // 32),
)
w13_quantized_shard, w13_scales_shard = mxfp4_quantize(
w13_weight.narrow(1, 2 * input_start, 2 * input_per_partition)
)
assert torch.equal(
w13_quantized_shard,
w13_quantized.narrow(1, 2 * input_start, 2 * input_per_partition),
)
assert torch.equal(
w13_scales_shard,
w13_scales.narrow(1, 2 * input_start, 2 * input_per_partition),
)
w2_quantized_shard, w2_scales_shard = mxfp4_quantize(
w2_weight.narrow(2, input_start, input_per_partition)
)
assert torch.equal(
w2_quantized_shard,
w2_quantized.narrow(2, input_start // 2, input_per_partition // 2),
)
assert torch.equal(
w2_scales_shard,
w2_scales.narrow(2, input_start // 32, input_per_partition // 32),
)
@pytest.mark.skipif(
not current_platform.is_cuda_alike(), reason="Only tested on ROCm/CUDA."
)
@pytest.mark.parametrize("moe_backend", ["aiter", "emulation"])
@pytest.mark.parametrize(
"unpadded_hidden_size,unpadded_intermediate_size",
[
pytest.param(256, 256, id="no_padding"),
# Not multiples of the MXFP4 block size (32), so that every backend --
# including emulation, whose round-up granularity is 32 -- has to pad.
pytest.param(240, 208, id="padding"),
],
)
def test_online_mxfp4_moe_matches_quark(
moe_backend: str,
unpadded_hidden_size: int,
unpadded_intermediate_size: int,
default_vllm_config,
dist_init,
monkeypatch,
):
"""
Ensures `Mxfp4OnlineMoEMethod` (online quantization)
and `QuarkOCP_MX_MoEMethod` (AMD Quark checkpoints) produce the same weights,
with same MOE backend used.
The `padding` case additionally covers `maybe_roundup_sizes`: the online
path allocates its bf16 weights with `torch.empty_strided` and the loader
only writes the unpadded slice, so the padding is filled with NaN here to
reproduce that uninitialized state. It must be zeroed before quantization
for the two paths to agree, since Quark allocates its padding with
`torch.zeros`.
"""
if moe_backend == "aiter":
from vllm._aiter_ops import rocm_aiter_ops
monkeypatch.setenv("VLLM_ROCM_USE_AITER", "1")
monkeypatch.setenv("VLLM_ROCM_USE_AITER_MOE", "1")
monkeypatch.setattr("vllm.platforms.rocm.on_gfx950", lambda: True)
rocm_aiter_ops.refresh_env_variables()
if current_platform.is_cuda():
pytest.skip(
"mxfp4_backend == Mxfp4MoeBackend.AITER_MXFP4_MXFP4 requires "
"rocm_aiter_ops in weight conversion, not compatible on cuda"
)
elif moe_backend == "emulation":
# `OCP_MXQuantizationEmulationTritonExperts.is_supported_config` gates
# on `has_quark()`, but its weight processing does not require it.
monkeypatch.setattr(
"vllm.model_executor.layers.fused_moe.experts."
"ocp_mx_emulation_moe.has_quark",
lambda: True,
)
default_vllm_config.model_config = ModelConfig()
num_experts = 4
device = current_platform.device_type
def make_layer(prefix: str) -> RoutedExperts:
runner = FusedMoEFactory(
num_experts=num_experts,
top_k=2,
hidden_size=unpadded_hidden_size,
intermediate_size=unpadded_intermediate_size,
prefix=prefix,
)
layer = runner.routed_experts
layer.moe_config.moe_backend = moe_backend
return layer
# `create_weights` implementations use plain `torch.zeros`/`torch.randn`
# with no explicit device, so without this context they would default
# to CPU and diverge from the (GPU) tensors produced by
# `mxfp4_quantize`/`replace_parameter`.
with torch.device(device):
checkpoint_layer = make_layer("checkpoint_layer")
online_layer = make_layer("online_layer")
checkpoint_method = QuarkOCP_MX_MoEMethod(
weight_config={"qscheme": "per_group", "dtype": "fp4"},
input_config={"dtype": "fp4", "is_dynamic": True},
moe=checkpoint_layer.moe_config,
)
online_method = Mxfp4OnlineMoEMethod(layer=online_layer)
# `RoutedExperts.__init__` applies this round-up in production; these
# layers are built without a quant config, so it is applied explicitly.
# Both methods must agree on the padded sizes.
roundup_arguments = dict(
hidden_size=unpadded_hidden_size,
intermediate_size_per_partition=unpadded_intermediate_size,
act_dtype=torch.bfloat16,
moe_parallel_config=online_layer.moe_config.moe_parallel_config,
)
hidden_size, intermediate_size = online_method.maybe_roundup_sizes(
**roundup_arguments
)
assert (
hidden_size,
intermediate_size,
) == checkpoint_method.maybe_roundup_sizes(**roundup_arguments)
if unpadded_hidden_size == 240:
# Padded case.
assert hidden_size > unpadded_hidden_size
assert intermediate_size > unpadded_intermediate_size
for layer in (checkpoint_layer, online_layer):
assert layer.moe_config.hidden_dim_unpadded == unpadded_hidden_size
assert (
layer.moe_config.intermediate_size_per_partition_unpadded
== unpadded_intermediate_size
)
layer.moe_config.hidden_dim = hidden_size
layer.moe_config.intermediate_size_per_partition = intermediate_size
gate_up_weight = torch.randn(
num_experts,
2 * unpadded_intermediate_size,
unpadded_hidden_size,
dtype=torch.bfloat16,
)
down_weight = torch.randn(
num_experts,
unpadded_hidden_size,
unpadded_intermediate_size,
dtype=torch.bfloat16,
)
scalings = [2.3, 0.03, 7.3, 0.1, 0.004, 17.3, 1e4, 1e-4]
for i, start in enumerate(range(0, unpadded_hidden_size, 32)):
gate_up_weight[..., start : start + 32] *= scalings[i % len(scalings)]
for i, start in enumerate(range(0, unpadded_intermediate_size, 32)):
down_weight[..., start : start + 32] *= scalings[i % len(scalings)]
def scatter_into_padded(
gate_up_weight: torch.Tensor,
down_weight: torch.Tensor,
padding_value: float,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Place the source weights into buffers of the padded size, the
way the weight loader writes only the unpadded slice of a larger
allocation."""
w13 = torch.full(
(num_experts, 2 * intermediate_size, hidden_size),
padding_value,
dtype=torch.bfloat16,
)
w2 = torch.full(
(num_experts, hidden_size, intermediate_size),
padding_value,
dtype=torch.bfloat16,
)
# w13 stacks gate and up, each padded to `intermediate_size`.
w13[:, :unpadded_intermediate_size, :unpadded_hidden_size] = gate_up_weight[
:, :unpadded_intermediate_size, :
]
w13[
:,
intermediate_size : intermediate_size + unpadded_intermediate_size,
:unpadded_hidden_size,
] = gate_up_weight[:, unpadded_intermediate_size:, :]
w2[:, :unpadded_hidden_size, :unpadded_intermediate_size] = down_weight
return w13, w2
# Checkpoint path: pre-quantize the source weights to MXFP4 with the
# ~same RTN recipe a real Quark checkpoint, then feed the
# already-packed tensors into `QuarkOCP_MX_MoEMethod`. Quark's
# `create_weights` allocates with `torch.zeros`, so its padding is zero.
checkpoint_w13, checkpoint_w2 = scatter_into_padded(
gate_up_weight, down_weight, padding_value=0.0
)
checkpoint_w13, checkpoint_w13_scale = mxfp4_quantize(checkpoint_w13)
checkpoint_w2, checkpoint_w2_scale = mxfp4_quantize(checkpoint_w2)
checkpoint_method.create_weights(
layer=checkpoint_layer,
num_experts=num_experts,
hidden_size=hidden_size,
intermediate_size_per_partition=intermediate_size,
params_dtype=torch.bfloat16,
)
checkpoint_layer.w13_weight.data.copy_(checkpoint_w13)
checkpoint_layer.w2_weight.data.copy_(checkpoint_w2)
checkpoint_layer.w13_weight_scale.data.copy_(checkpoint_w13_scale)
checkpoint_layer.w2_weight_scale.data.copy_(checkpoint_w2_scale)
checkpoint_method.process_weights_after_loading(checkpoint_layer)
# Online path: feed the *same* raw bf16 source weights and let
# `Mxfp4OnlineMoEMethod` quantize them during
# `process_weights_after_loading`. The padding starts out
# uninitialized, standing in for the materialized meta tensor.
online_w13, online_w2 = scatter_into_padded(
gate_up_weight, down_weight, padding_value=float("nan")
)
online_method.create_weights(
layer=online_layer,
num_experts=num_experts,
hidden_size=hidden_size,
intermediate_size_per_partition=intermediate_size,
params_dtype=torch.bfloat16,
)
replace_parameter(online_layer, "w13_weight", online_w13)
replace_parameter(online_layer, "w2_weight", online_w2)
online_method.process_weights_after_loading(online_layer)
assert checkpoint_method.mxfp4_backend == online_method.mxfp4_backend
assert_quantized_weights_equal(
checkpoint_layer,
online_layer,
keys=("w13_weight", "w13_weight_scale", "w2_weight", "w2_weight_scale"),
packed_weight_keys=("w13_weight", "w2_weight"),
)
@pytest.mark.skipif(
not current_platform.is_cuda_alike(), reason="Only tested on ROCm/CUDA."
)
@pytest.mark.parametrize("linear_backend", ["emulation", "aiter", "marlin"])
def test_online_mxfp4_dense_matches_quark(
linear_backend: str, default_vllm_config, dist_init, monkeypatch
):
"""
Ensures `Mxfp4OnlineLinearMethod` (online quantization)
and `QuarkOCP_MX` (AMD Quark checkpoints) produce the same weights,
with same linear backend used.
"""
if linear_backend == "marlin" and not current_platform.is_cuda():
# `MarlinMxFp4LinearKernel.process_weights_after_loading` is CUDA-only
pytest.skip(
"MarlinMxFp4LinearKernel.process_weights_after_loading is CUDA-only."
)
if linear_backend == "aiter":
# `AiterMxfp4LinearKernel` is registered only for ROCm in
# `_POSSIBLE_MXFP4_KERNELS`, and `is_supported` gates on
# `current_platform.supports_mx()`. Force it onto the CUDA
# kernel list and bypass the platform gate so this backend can
# be exercised on CUDA hosts too.
monkeypatch.setattr(
AiterMxfp4LinearKernel,
"is_supported",
classmethod(lambda cls, compute_capability=None: (True, None)),
)
monkeypatch.setitem(
_POSSIBLE_MXFP4_KERNELS,
PlatformEnum.CUDA,
[
AiterMxfp4LinearKernel,
*_POSSIBLE_MXFP4_KERNELS.get(PlatformEnum.CUDA, []),
],
)
elif linear_backend == "emulation":
# `EmulationMxfp4LinearKernel.can_implement` gates on `has_quark()`,
# EmulationMxfp4LinearKernel.process_weights_after_loading does not require it.
monkeypatch.setattr(
"vllm.model_executor.kernels.linear.mxfp4.emulation.has_quark",
lambda: True,
)
default_vllm_config.model_config = ModelConfig()
default_vllm_config.kernel_config.linear_backend = linear_backend
input_size = 256
output_size = 128
device = current_platform.device_type
# `create_weights` implementations use plain `torch.empty` with no explicit
# device, so without this context they would default to CPU and diverge
# from the (GPU) tensors produced by `mxfp4_quantize`/`replace_parameter`.
with torch.device(device):
checkpoint_layer = torch.nn.Module()
online_layer = torch.nn.Module()
for layer in (checkpoint_layer, online_layer):
# `LinearBase` subclasses normally set these attributes on the
# layer before `create_weights` is called; the marlin kernel's
# `process_weights_after_loading` reads them directly off the
# layer, so a bare `torch.nn.Module` needs them set explicitly.
layer.input_size = input_size
layer.output_size = output_size
layer.input_size_per_partition = input_size
layer.output_size_per_partition = output_size
layer.params_dtype = torch.bfloat16
weight = torch.randn(output_size, input_size, dtype=torch.bfloat16)
scalings = [2.3, 0.03, 7.3, 0.1, 0.004, 17.3, 1e4, 1e-4]
for i in range(input_size // 32):
weight[:, i * 32 : (i + 1) * 32] *= scalings[i % len(scalings)]
# Checkpoint path: pre-quantize the source weight to MXFP4 with the
# ~same RTN recipe as a real Quark checkpoint, then feed the
# already-packed tensors into `QuarkOCP_MX`.
checkpoint_weight, checkpoint_weight_scale = mxfp4_quantize(weight)
checkpoint_scheme = QuarkOCP_MX(
weight_quant_spec={"qscheme": "per_group", "dtype": "fp4"},
input_quant_spec={"dtype": "fp4", "is_dynamic": True},
)
checkpoint_scheme.create_weights(
layer=checkpoint_layer,
output_partition_sizes=[output_size],
input_size_per_partition=input_size,
params_dtype=torch.bfloat16,
weight_loader=default_weight_loader,
)
checkpoint_layer.weight.data.copy_(checkpoint_weight)
checkpoint_layer.weight_scale.data.copy_(checkpoint_weight_scale)
checkpoint_scheme.process_weights_after_loading(checkpoint_layer)
# Online path: feed the *same* raw bf16 source weight and let
# `Mxfp4OnlineLinearMethod` quantize it during
# `process_weights_after_loading`.
online_method = Mxfp4OnlineLinearMethod()
online_method.create_weights(
layer=online_layer,
input_size_per_partition=input_size,
output_partition_sizes=[output_size],
input_size=input_size,
output_size=output_size,
params_dtype=torch.bfloat16,
weight_loader=default_weight_loader,
)
replace_parameter(online_layer, "weight", weight.clone())
online_method.process_weights_after_loading(online_layer)
assert type(checkpoint_scheme.ocp_mx_linear) is type(online_method.kernel)
assert_quantized_weights_equal(
checkpoint_layer,
online_layer,
keys=("weight", "weight_scale"),
packed_weight_keys=("weight",),
)
+5
View File
@@ -17,6 +17,7 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
kFp8StaticTensorSym,
kInt8StaticChannelSym,
kMxfp4Dynamic,
kMxfp4Static,
kMxfp8Dynamic,
kNvfp4Static,
)
@@ -131,6 +132,10 @@ _ONLINE_SHORTHANDS: dict[str, QuantizationConfigArgs] = {
linear=QuantSpec(weight=kMxfp8Dynamic),
moe=QuantSpec(weight=kMxfp8Dynamic),
),
"mxfp4": QuantizationConfigArgs(
linear=QuantSpec(weight=kMxfp4Static),
moe=QuantSpec(weight=kMxfp4Static),
),
# INT8 weight-only on MoE; linear stays unquantized (no `linear` field).
"int8_per_channel_weight_only": QuantizationConfigArgs(
moe=QuantSpec(weight=kInt8StaticChannelSym),
@@ -23,6 +23,7 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
kMxfp6E3M2Dynamic,
)
from vllm.platforms import current_platform
from vllm.utils.import_utils import has_quark
from .base import MxFp4LinearKernel, MxFp4LinearLayerConfig
@@ -68,6 +69,9 @@ class EmulationMxfp4LinearKernel(MxFp4LinearKernel):
):
return False, "only supports MXFP4 or MXFP6 or unquantized activations"
if not has_quark():
return False, "amd-quark package not available"
if (
current_platform.is_rocm()
and current_platform.supports_mx()
@@ -26,7 +26,11 @@ from vllm.model_executor.layers.quantization.utils.mxfp6_utils import dequant_mx
from vllm.model_executor.layers.quantization.utils.ocp_mx_utils import (
OCP_MX_Scheme,
)
from vllm.model_executor.layers.quantization.utils.quant_utils import (
QuantKey,
)
from vllm.platforms import current_platform
from vllm.utils.import_utils import has_quark
logger = init_logger(__name__)
@@ -92,6 +96,25 @@ class OCP_MXQuantizationEmulationTritonExperts(TritonExperts):
]:
self._quant_dtype = current_platform.fp8_dtype()
@staticmethod
def is_supported_config(
cls: type["mk.FusedMoEExperts"],
moe_config: FusedMoEConfig,
weight_key: QuantKey | None,
activation_key: QuantKey | None,
activation_format: mk.FusedMoEActivationFormat,
) -> tuple[bool, str | None]:
if not has_quark():
return False, "kernel requires amd-quark package"
return TritonExperts.is_supported_config(
cls=cls,
moe_config=moe_config,
weight_key=weight_key,
activation_key=activation_key,
activation_format=activation_format,
)
@property
def quant_dtype(self) -> torch.dtype | str | None:
return self._quant_dtype
@@ -1650,6 +1650,21 @@ def convert_weight_to_mxfp4_moe_kernel_format(
w13_bias,
w2_bias,
)
elif mxfp4_backend in (
Mxfp4MoeBackend.AITER_MXFP4_MXFP4,
Mxfp4MoeBackend.AITER_MXFP4_FP8,
):
return convert_gpt_oss_weight_to_mxfp4_moe_kernel_format(
mxfp4_backend=mxfp4_backend,
layer=layer,
w13_weight=w13_weight,
w2_weight=w2_weight,
w13_weight_scale=w13_weight_scale,
w2_weight_scale=w2_weight_scale,
w13_bias=w13_bias,
w2_bias=w2_bias,
_cache_permute_indices=_cache_permute_indices,
)
else:
raise ValueError(
f"Unsupported mxfp4_backend for Mxfp4MoEMethod: {mxfp4_backend}. "
@@ -36,6 +36,10 @@ from vllm.model_executor.layers.quantization.online.fp8 import (
from vllm.model_executor.layers.quantization.online.int8 import (
Int8OnlineMoEMethod,
)
from vllm.model_executor.layers.quantization.online.mxfp4 import (
Mxfp4OnlineLinearMethod,
Mxfp4OnlineMoEMethod,
)
from vllm.model_executor.layers.quantization.online.mxfp8 import (
Mxfp8OnlineLinearMethod,
Mxfp8OnlineMoEMethod,
@@ -49,6 +53,7 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
kFp8StaticChannelSym,
kFp8StaticTensorSym,
kInt8StaticChannelSym,
kMxfp4Static,
kMxfp8Dynamic,
kNvfp4Static,
)
@@ -64,6 +69,7 @@ _ONLINE_LINEAR_METHODS: dict[QuantKey, type] = {
kFp8Static128BlockSym: Fp8PerBlockOnlineLinearMethod,
kFp8StaticChannelSym: Fp8PtpcOnlineLinearMethod,
kMxfp8Dynamic: Mxfp8OnlineLinearMethod,
kMxfp4Static: Mxfp4OnlineLinearMethod,
}
_ONLINE_MOE_METHODS: dict[QuantKey, type] = {
@@ -71,6 +77,7 @@ _ONLINE_MOE_METHODS: dict[QuantKey, type] = {
kFp8Static128BlockSym: Fp8PerBlockOnlineMoEMethod,
kFp8StaticChannelSym: Fp8PtpcOnlineMoEMethod,
kMxfp8Dynamic: Mxfp8OnlineMoEMethod,
kMxfp4Static: Mxfp4OnlineMoEMethod,
kInt8StaticChannelSym: Int8OnlineMoEMethod,
kNvfp4Static: Nvfp4OnlineMoEMethod,
}
@@ -639,36 +639,6 @@ class Fp8PerBlockOnlineMoEMethod(_Fp8OnlineMoEBase):
round_up(intermediate_size_per_partition, block_size),
)
def _zero_padding(self, layer: Module) -> None:
hidden_size = layer.moe_config.hidden_dim_unpadded
intermediate_size = layer.moe_config.intermediate_size_per_partition_unpadded
w13_shard = layer.w13_weight.shape[1] // self.moe.w13_num_shards
if w13_shard > intermediate_size:
for shard in range(self.moe.w13_num_shards):
start = shard * w13_shard + intermediate_size
layer.w13_weight[:, start : (shard + 1) * w13_shard, :] = 0
if layer.w13_weight.shape[2] > hidden_size:
layer.w13_weight[:, :, hidden_size:] = 0
if layer.w2_weight.shape[1] > hidden_size:
layer.w2_weight[:, hidden_size:, :] = 0
if layer.w2_weight.shape[2] > intermediate_size:
layer.w2_weight[:, :, intermediate_size:] = 0
if getattr(layer, "w13_bias", None) is not None:
w13_bias_shard = layer.w13_bias.shape[1] // self.moe.w13_num_shards
if w13_bias_shard > intermediate_size:
for shard in range(self.moe.w13_num_shards):
start = shard * w13_bias_shard + intermediate_size
layer.w13_bias[:, start : (shard + 1) * w13_bias_shard] = 0
if (
getattr(layer, "w2_bias", None) is not None
and layer.w2_bias.shape[1] > hidden_size
):
layer.w2_bias[:, hidden_size:] = 0
def process_weights_after_loading(self, layer: Module) -> None:
if getattr(layer, "_already_called_process_weights_after_loading", False):
return
@@ -95,6 +95,36 @@ class OnlineMoEMethodBase(FusedMoEMethodBase):
initialize_online_processing(layer)
def _zero_padding(self, layer: torch.nn.Module) -> None:
hidden_size = layer.moe_config.hidden_dim_unpadded
intermediate_size = layer.moe_config.intermediate_size_per_partition_unpadded
w13_shard = layer.w13_weight.shape[1] // self.moe.w13_num_shards
if w13_shard > intermediate_size:
for shard in range(self.moe.w13_num_shards):
start = shard * w13_shard + intermediate_size
layer.w13_weight[:, start : (shard + 1) * w13_shard, :] = 0
if layer.w13_weight.shape[2] > hidden_size:
layer.w13_weight[:, :, hidden_size:] = 0
if layer.w2_weight.shape[1] > hidden_size:
layer.w2_weight[:, hidden_size:, :] = 0
if layer.w2_weight.shape[2] > intermediate_size:
layer.w2_weight[:, :, intermediate_size:] = 0
if getattr(layer, "w13_bias", None) is not None:
w13_bias_shard = layer.w13_bias.shape[1] // self.moe.w13_num_shards
if w13_bias_shard > intermediate_size:
for shard in range(self.moe.w13_num_shards):
start = shard * w13_bias_shard + intermediate_size
layer.w13_bias[:, start : (shard + 1) * w13_bias_shard] = 0
if (
getattr(layer, "w2_bias", None) is not None
and layer.w2_bias.shape[1] > hidden_size
):
layer.w2_bias[:, hidden_size:] = 0
@abstractmethod
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
pass
@@ -0,0 +1,317 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Online MXFP4 (microscaling FP4, block-32) quantization methods."""
from typing import TYPE_CHECKING
import torch
from torch.nn import Module
if TYPE_CHECKING:
import vllm.model_executor.layers.fused_moe.modular_kernel as mk
from vllm.model_executor.layers.fused_moe import (
FusedMoEParallelConfig,
FusedMoEQuantConfig,
RoutedExperts,
)
from vllm.model_executor.kernels.linear import init_mxfp4_linear_kernel
from vllm.model_executor.layers.fused_moe.oracle.mxfp4 import (
TRITON_BACKENDS,
Mxfp4MoeBackend,
convert_weight_to_mxfp4_moe_kernel_format,
make_mxfp4_moe_kernel,
make_mxfp4_moe_quant_config,
mxfp4_round_up_hidden_size_and_intermediate_size,
select_mxfp4_moe_backend,
)
from vllm.model_executor.layers.quantization.online.fp8 import (
_Fp8OnlineLinearBase,
)
from vllm.model_executor.layers.quantization.online.moe_base import (
OnlineMoEMethodBase,
)
from vllm.model_executor.layers.quantization.utils.mxfp4_utils import (
mxfp4_quantize,
)
from vllm.model_executor.layers.quantization.utils.quant_utils import kMxfp4Dynamic
from vllm.model_executor.utils import replace_parameter
# MXFP4 block size (elements per shared e8m0 scale), same as the checkpoint
# and MoE oracle paths.
MXFP4_BLOCK_SIZE = 32
def _quantize_mxfp4_moe_weight(
weight: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Batch quantization: bf16/fp16 stacked expert weights -> MXFP4.
Returns packed FP4 weights (uint8, two values per byte) and per-block
(group-32) e8m0 scales (uint8), one pair per expert.
"""
num_experts = weight.size(0)
first_quant, first_scale = mxfp4_quantize(weight[0])
# Pre-allocate the output tensors rather than stacking.
# This is important for consistent memory layout.
w_quant = torch.empty(
(num_experts, *first_quant.shape),
dtype=first_quant.dtype,
device=weight.device,
)
w_scales = torch.empty(
(num_experts, *first_scale.shape),
dtype=first_scale.dtype,
device=weight.device,
)
w_quant[0] = first_quant
w_scales[0] = first_scale
for i in range(1, num_experts):
w_quant[i], w_scales[i] = mxfp4_quantize(weight[i])
return w_quant, w_scales
class Mxfp4OnlineLinearMethod(_Fp8OnlineLinearBase):
"""Online MXFP4 linear method.
Loads bf16/fp16 checkpoints and quantizes weights to MXFP4 (microscaling
FP4 with block-32 scales) during weight loading.
"""
activation_quant_key = kMxfp4Dynamic
def __init__(self):
super().__init__()
self.kernel = init_mxfp4_linear_kernel(
activation_quant_key=self.activation_quant_key
)
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: list[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
):
if input_size_per_partition % MXFP4_BLOCK_SIZE != 0:
raise ValueError(
f"MXFP4 requires input_size_per_partition "
f"({input_size_per_partition}) to be divisible by "
f"{MXFP4_BLOCK_SIZE}."
)
super().create_weights(
layer,
input_size_per_partition,
output_partition_sizes,
input_size,
output_size,
params_dtype,
**extra_weight_attrs,
)
def process_weights_after_loading(self, layer: Module) -> None:
if getattr(layer, "_already_called_process_weights_after_loading", False):
return
weight_fp4, weight_scale = mxfp4_quantize(layer.weight.contiguous())
layer.input_scale = None
replace_parameter(layer, "weight", weight_fp4.data)
replace_parameter(layer, "weight_scale", weight_scale.data)
self.kernel.process_weights_after_loading(layer)
layer._already_called_process_weights_after_loading = True
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: torch.Tensor | None = None,
) -> torch.Tensor:
return self.kernel.apply_weights(layer, x, bias)
class Mxfp4OnlineMoEMethod(OnlineMoEMethodBase):
"""MoE method for online MXFP4 (block) quantization."""
mxfp4_backend: Mxfp4MoeBackend
experts_cls: "type[mk.FusedMoEExperts] | None"
activation_quant_key = kMxfp4Dynamic
def __init__(self, *, layer: torch.nn.Module):
super().__init__(layer.moe_config)
self.weight_block_size: list[int] = [1, MXFP4_BLOCK_SIZE]
self.weight_scale_name = "weight_scale"
self.mxfp4_backend, self.experts_cls = select_mxfp4_moe_backend(
config=self.moe, activation_key=self.activation_quant_key
)
def maybe_roundup_sizes(
self,
hidden_size: int,
intermediate_size_per_partition: int,
act_dtype: torch.dtype,
moe_parallel_config: "FusedMoEParallelConfig",
) -> tuple[int, int]:
hidden_size, intermediate_size_per_partition = super().maybe_roundup_sizes(
hidden_size=hidden_size,
intermediate_size_per_partition=intermediate_size_per_partition,
act_dtype=act_dtype,
moe_parallel_config=moe_parallel_config,
)
return mxfp4_round_up_hidden_size_and_intermediate_size(
self.mxfp4_backend, hidden_size, intermediate_size_per_partition
)
def create_weights(
self,
layer: Module,
num_experts: int,
hidden_size: int,
intermediate_size_per_partition: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
):
if (
hidden_size % MXFP4_BLOCK_SIZE != 0
or intermediate_size_per_partition % MXFP4_BLOCK_SIZE != 0
):
raise ValueError(
"Online MXFP4 MoE requires hidden/intermediate sizes divisible "
f"by {MXFP4_BLOCK_SIZE}."
)
super().create_weights(
layer=layer,
num_experts=num_experts,
hidden_size=hidden_size,
intermediate_size_per_partition=intermediate_size_per_partition,
params_dtype=params_dtype,
**extra_weight_attrs,
)
layer.weight_block_size = [1, MXFP4_BLOCK_SIZE]
def _setup_kernel(
self,
layer: "RoutedExperts",
w13: torch.Tensor,
w2: torch.Tensor,
w13_scale: torch.Tensor,
w2_scale: torch.Tensor,
w13_bias: torch.Tensor | None,
w2_bias: torch.Tensor | None,
) -> None:
w13, w2, w13_scale, w2_scale, w13_bias, w2_bias = (
convert_weight_to_mxfp4_moe_kernel_format(
mxfp4_backend=self.mxfp4_backend,
layer=layer,
w13_weight=w13,
w2_weight=w2,
w13_weight_scale=w13_scale,
w2_weight_scale=w2_scale,
w13_bias=w13_bias,
w2_bias=w2_bias,
)
)
# Handle weight/scale assignment based on backend type
# Copied from `quark_moe.py`.
# TODO: This should not be here, replace_parameter should handle
# triton_kernels.tensor.Tensor.
if self.mxfp4_backend in TRITON_BACKENDS or self.mxfp4_backend in (
Mxfp4MoeBackend.AITER_MXFP4_FP8,
):
# Triton-based backends: w13/w2 are triton_kernels.tensor.Tensor
# Store on layer for apply(), scales are PrecisionConfig
layer.w13_weight = w13
layer.w2_weight = w2
self.w13_precision_config = w13_scale
self.w2_precision_config = w2_scale
else:
# Standard backends: replace parameters
replace_parameter(layer, "w13_weight", w13)
replace_parameter(layer, "w2_weight", w2)
replace_parameter(layer, "w13_weight_scale", w13_scale)
replace_parameter(layer, "w2_weight_scale", w2_scale)
if w13_bias is not None and w2_bias is not None:
replace_parameter(layer, "w13_bias", w13_bias)
replace_parameter(layer, "w2_bias", w2_bias)
self.moe_quant_config = self.get_fused_moe_quant_config(layer)
if self.moe_quant_config is not None and self.experts_cls is not None:
self.moe_kernel = make_mxfp4_moe_kernel(
moe_quant_config=self.moe_quant_config,
moe_config=self.moe,
mxfp4_backend=self.mxfp4_backend,
experts_cls=self.experts_cls,
routing_tables=layer._expert_routing_tables(),
layer=layer,
)
def get_fused_moe_quant_config(
self, layer: torch.nn.Module
) -> "FusedMoEQuantConfig | None":
# NOTE: unlike `QuarkOCP_MX_MoEMethod.get_fused_moe_quant_config`, this
# does not branch on `self.mxfp4_backend in TRITON_BACKENDS or
# AITER_MXFP4_FP8` to read `self.w13_precision_config`/
# `self.w2_precision_config` instead of `layer.w13_weight_scale`/
# `layer.w2_weight_scale`. For those backends,
# `convert_weight_to_mxfp4_moe_kernel_format` deletes the latter, so
# `getattr` below would raise `AttributeError`. This is currently
# unreachable because `activation_quant_key` is hardcoded to
# `kMxfp4Dynamic`
# TODO: When supporting online activation quant key override,
# fix this convert_weight_to_mxfp4_moe_kernel_format issue.
w1_scale = getattr(layer, f"w13_{self.weight_scale_name}")
w2_scale = getattr(layer, f"w2_{self.weight_scale_name}")
return make_mxfp4_moe_quant_config(
mxfp4_backend=self.mxfp4_backend,
w1_scale=w1_scale,
w2_scale=w2_scale,
w1_bias=getattr(layer, "w13_bias", None),
w2_bias=getattr(layer, "w2_bias", None),
a1_scale=layer.w13_input_scale,
a2_scale=layer.w2_input_scale,
swiglu_limit=getattr(layer, "swiglu_limit", None),
gemm1_alpha=getattr(layer, "swiglu_alpha", None),
gemm1_beta=getattr(layer, "swiglu_beta", None),
layer=layer,
)
def process_weights_after_loading(self, layer: Module) -> None:
if getattr(layer, "_already_called_process_weights_after_loading", False):
return
self._zero_padding(layer)
if self.mxfp4_backend == Mxfp4MoeBackend.NONE:
layer._already_called_process_weights_after_loading = True
return
layer.w13_input_scale = None
layer.w2_input_scale = None
w13, w13_scale = _quantize_mxfp4_moe_weight(layer.w13_weight)
w2, w2_scale = _quantize_mxfp4_moe_weight(layer.w2_weight)
self._setup_kernel(
layer,
w13,
w2,
w13_scale,
w2_scale,
getattr(layer, "w13_bias", None),
getattr(layer, "w2_bias", None),
)
layer._already_called_process_weights_after_loading = True
@@ -6,6 +6,7 @@ import torch
from vllm.logger import init_logger
from vllm.platforms import current_platform
from vllm.triton_utils import tl, triton
from vllm.utils.import_utils import has_triton_kernels
from vllm.utils.torch_utils import direct_register_custom_op, is_torch_equal_or_newer
@@ -184,3 +185,319 @@ except AttributeError as error:
def xpu_mxfp4_quantize(x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
return torch.ops.vllm.xpu_mxfp4_quantize(x)
def permute_shape(shape: tuple[int, ...], permute_order: list[int]) -> tuple[int, ...]:
return tuple(shape[i] for i in permute_order)
@triton.jit
def _compute_quant_and_scale(src_tensor, valid_src_mask):
BLOCK_SIZE_OUT_DIM: tl.constexpr = src_tensor.shape[0]
BLOCK_SIZE_QUANT_DIM: tl.constexpr = src_tensor.shape[1]
BLOCK_SIZE_QUANT_MX_SCALE: tl.constexpr = src_tensor.shape[1] // 32
# Explicit cast to fp32 since most ops are not supported on bfloat16. We
# avoid needless conversions to and from bf16
f32_tensor = src_tensor.to(tl.float32)
abs_tensor = tl.abs(f32_tensor)
abs_tensor = tl.where(
valid_src_mask, abs_tensor, -1.0
) # Don't consider padding tensors in scale computation
abs_tensor = tl.reshape(
abs_tensor, [BLOCK_SIZE_OUT_DIM, BLOCK_SIZE_QUANT_MX_SCALE, 32]
)
max_val = tl.max(abs_tensor, axis=2, keep_dims=True)
# Round-to-nearest-even block scale (OCP MX spec), matching hardware/aiter
# MXFP4 quantizers: compute 2 ** (floor(log2(rounding(max_abs(v)))) - max_exp).
max_val = max_val.to(tl.int32, bitcast=True)
max_val = (max_val + 0x200000).to(tl.uint32, bitcast=True) & 0x7F800000
max_val = max_val.to(tl.float32, bitcast=True)
# Add epsilon to prevent log2(0) = -inf when max_val is zero (all-zero input block)
eps = tl.where(max_val == 0.0, 2 ** (-126), 0.0)
scale_e8m0_unbiased = tl.log2(max_val + eps).floor() - 2
scale_e8m0_unbiased = tl.clamp(scale_e8m0_unbiased, min=-127, max=127)
dequant_scale_rounded = tl.exp2(scale_e8m0_unbiased)
dequant_scale_exponent = dequant_scale_rounded.to(tl.uint32, bitcast=True)
dequant_scale_rounded = dequant_scale_exponent.to(tl.float32, bitcast=True)
quant_scale = tl.where(dequant_scale_rounded == 0, 0, 1.0 / dequant_scale_rounded)
f32_tensor = tl.reshape(
f32_tensor, [BLOCK_SIZE_OUT_DIM, BLOCK_SIZE_QUANT_MX_SCALE, 32]
)
quant_tensor = f32_tensor * quant_scale
# Reshape the tensors after scaling
quant_tensor = quant_tensor.reshape([BLOCK_SIZE_OUT_DIM, BLOCK_SIZE_QUANT_DIM])
# Set the invalid portions of the tensor to 0. This will ensure that any
# padding tensors are 0 in the mx format.
quant_tensor = tl.where(valid_src_mask, quant_tensor, 0)
dequant_scale_exponent = dequant_scale_exponent.reshape(
[BLOCK_SIZE_OUT_DIM, BLOCK_SIZE_QUANT_MX_SCALE]
)
# First, we simply extract the exponent part of the scales and store the result
dequant_scale_exponent = (dequant_scale_exponent >> 23).to(tl.uint8)
# Now we must convert the tensor to packed e2m1 (mxfp4).
quant_tensor = quant_tensor.to(tl.uint32, bitcast=True)
signs = quant_tensor & 0x80000000
exponents = (quant_tensor >> 23) & 0xFF
mantissas = quant_tensor & 0x7FFFFF
# 0.25 <= x < 0.75 maps to 0.5, a denormal number
E8_BIAS = 127
E2_BIAS = 1
# Move implicit bit 1 at the beginning to mantissa for denormals. Bits
# shifted out are OR-reduced into the LSB as a sticky bit, so the
# round-to-even logic below can tell an exact tie from a value that is
# merely close to one.
adjusted_exponents = tl.core.sub(E8_BIAS, exponents + 1, sanitize_overflow=False)
folded_mantissas = 0x400000 | (mantissas >> 1)
denormal_mantissas = folded_mantissas >> adjusted_exponents
denormal_mantissas |= (
(denormal_mantissas << adjusted_exponents) != folded_mantissas
).to(tl.uint32)
mantissas = tl.where(exponents < E8_BIAS, denormal_mantissas, mantissas)
# For normal numbers, we change the bias from 127 to 1, and for subnormals,
# we keep exponent as 0.
exponents = tl.maximum(exponents, E8_BIAS - E2_BIAS) - (E8_BIAS - E2_BIAS)
# Combine sign, exponent, and mantissa, rounding to nearest with ties
# broken to even (OCP MX spec, matching hardware/aiter MXFP4
# quantizers) via the standard "add a rounding bias, then truncate"
# trick: the bias's LSB is the kept mantissa bit, so an exact tie
# rounds to whichever candidate is even, while any lower (sticky) bit
# being set always rounds up.
rounding_bias = ((mantissas >> 22) & 1) + 0x1FFFFF
rounded_mantissa = (mantissas + rounding_bias) >> 22 # in {0, 1, 2}
e2m1_tmp = tl.minimum((exponents << 1) + rounded_mantissa, 0x7)
e2m1_value = ((signs >> 28) | e2m1_tmp).to(tl.uint8)
e2m1_value = tl.reshape(
e2m1_value, [BLOCK_SIZE_OUT_DIM, BLOCK_SIZE_QUANT_DIM // 2, 2]
)
evens, odds = tl.split(e2m1_value)
out_tensor = evens | (odds << 4)
return out_tensor, dequant_scale_exponent
@triton.jit
def _downcast_to_mxfp(
mx_tensor_ptr,
stride_mxt_outer,
stride_mxt_quant: tl.constexpr,
mx_scale_ptr,
stride_mx_scale_outer,
stride_mx_scale_quant,
src_ptr,
stride_src_outer,
stride_src_quant,
outer_dim,
quant_dim,
BLOCK_SIZE_OUT_DIM: tl.constexpr,
BLOCK_SIZE_QUANT_DIM: tl.constexpr,
):
tl.static_assert(
stride_mxt_quant == 1, f"Output stride, {stride_mxt_quant=} must be 1."
)
tl.static_assert(
BLOCK_SIZE_QUANT_DIM % 32 == 0,
f"{BLOCK_SIZE_QUANT_DIM=} must be a multiple of 32",
)
# uint8 signifies two fp4 e2m1 values packed into a single byte
tl.static_assert(
mx_tensor_ptr.dtype.element_ty == tl.uint8,
f"Invalid {mx_tensor_ptr.dtype.element_ty=}. Must be uint8.",
)
src_dtype: tl.constexpr = src_ptr.dtype.element_ty
tl.static_assert(
mx_scale_ptr.dtype.element_ty == tl.uint8,
f"{mx_scale_ptr.dtype.element_ty=} must be uint8",
)
tl.static_assert(
(src_dtype == tl.bfloat16) or (src_dtype == tl.float16),
f"{src_dtype=} must be bfloat16 or float16",
)
outer_block = tl.program_id(0).to(tl.int64)
quant_block = tl.program_id(1).to(tl.int64)
BLOCK_SIZE_QUANT_MX_SCALE: tl.constexpr = BLOCK_SIZE_QUANT_DIM // 32
BLOCK_SIZE_QUANT_MX_TENSOR: tl.constexpr = BLOCK_SIZE_QUANT_DIM // 2
start_src_quant = quant_block * BLOCK_SIZE_QUANT_DIM
start_mx_scale_quant = quant_block * BLOCK_SIZE_QUANT_MX_SCALE
start_mx_quant = quant_block * BLOCK_SIZE_QUANT_MX_TENSOR
start_out = outer_block * BLOCK_SIZE_OUT_DIM
src_ptr += start_src_quant * stride_src_quant + start_out * stride_src_outer
mx_scale_ptr += (
start_mx_scale_quant * stride_mx_scale_quant + start_out * stride_mx_scale_outer
)
mx_tensor_ptr += start_mx_quant * stride_mxt_quant + start_out * stride_mxt_outer
offs_src_quant = tl.arange(0, BLOCK_SIZE_QUANT_DIM)[None, :].to(tl.int64)
offs_mxt_quant = tl.arange(0, BLOCK_SIZE_QUANT_MX_TENSOR)[None, :].to(tl.int64)
offs_scale_quant = tl.arange(0, BLOCK_SIZE_QUANT_MX_SCALE)[None, :].to(tl.int64)
offs_outer = tl.arange(0, BLOCK_SIZE_OUT_DIM)[:, None].to(tl.int64)
mask_src_quant = start_src_quant + offs_src_quant < quant_dim
mask_n = start_out + offs_outer < outer_dim
full_mask_src = mask_src_quant and mask_n
mask_mxt_quant = start_mx_quant + offs_mxt_quant < tl.cdiv(quant_dim, 2)
full_mask_mxt = mask_mxt_quant and mask_n
scale_mask_k = start_mx_scale_quant + offs_scale_quant < tl.cdiv(quant_dim, 32)
full_scale_mask = scale_mask_k and mask_n
src_tensor_offsets = (
offs_src_quant * stride_src_quant + offs_outer * stride_src_outer
)
mx_scale_offsets = (
offs_scale_quant * stride_mx_scale_quant + offs_outer * stride_mx_scale_outer
)
mx_tensor_offsets = (
offs_mxt_quant * stride_mxt_quant + offs_outer * stride_mxt_outer
)
src_tensor = tl.load(src_ptr + src_tensor_offsets, mask=full_mask_src)
out_tensor, scale_tensor = _compute_quant_and_scale(src_tensor, full_mask_src)
tl.store(mx_scale_ptr + mx_scale_offsets, scale_tensor, mask=full_scale_mask)
tl.store(mx_tensor_ptr + mx_tensor_offsets, out_tensor, mask=full_mask_mxt)
def downcast_to_mxfp(
src_tensor: torch.Tensor,
axis: int,
out_quant_tensor: torch.Tensor | None = None,
out_scale: torch.Tensor | None = None,
BLOCK_OUT_DIM: int = 128,
BLOCK_QUANT_DIM: int = 32,
):
"""
Convert the src weights to MXFP4. The src weight is quantized along the
axis dimension into packed e2m1 values (torch.uint8, two values per
byte), so the size of that dimension in the output is half of the
logical (unpacked) size.
"""
out_quant_type = torch.uint8
ndim = src_tensor.ndim
assert -ndim <= axis < ndim, f"Invalid axis {axis=}"
axis = axis if axis >= 0 else axis + ndim
L = src_tensor.shape[axis]
# We make this assertion since we can't track if the "real" shape was odd,
# and we padded it to be even.
# We want to maintain the property dequant(quant(x)).shape == x.shape
assert L % 2 == 0, f"axis dim must be divisible by 2 for e2m1. Got {L}"
device = src_tensor.device
packed_quant_dim = triton.cdiv(L, 2)
out_scale_dim = triton.cdiv(L, 32)
# Move the quantization axis to the end for the kernel, then permute back.
permute_order = list(range(ndim))
permute_order[axis], permute_order[-1] = permute_order[-1], permute_order[axis]
prmted_quant_tensor_shape = permute_shape(src_tensor.shape, permute_order)[:-1] + (
packed_quant_dim,
)
prmted_scale_shape = permute_shape(src_tensor.shape, permute_order)[:-1] + (
out_scale_dim,
)
prmted_src_tensor = src_tensor.permute(permute_order)
if out_quant_tensor is None:
out_quant_tensor = torch.empty(
prmted_quant_tensor_shape, dtype=out_quant_type, device=device
)
else:
expected_shape = (
src_tensor.shape[:axis] + (packed_quant_dim,) + src_tensor.shape[axis + 1 :]
)
assert out_quant_tensor.shape == expected_shape, (
f"{out_quant_tensor.shape=} != {expected_shape=}"
)
assert out_quant_tensor.dtype == out_quant_type, (
f"{out_quant_tensor.dtype=} != {out_quant_type=}"
)
assert out_quant_tensor.stride(axis) == 1, (
f"{out_quant_tensor.stride(axis)=} != 1"
)
# We expect the axis dimension to be last, so permute the tensor
out_quant_tensor = out_quant_tensor.permute(permute_order)
if out_scale is None:
out_scale = torch.empty(prmted_scale_shape, dtype=torch.uint8, device=device)
else:
expected_scale_shape = permute_shape(prmted_scale_shape, permute_order)
assert out_scale.shape == expected_scale_shape, (
f"{out_scale.shape=} {expected_scale_shape=}"
)
assert out_scale.dtype == torch.uint8, f"{out_scale.dtype=} != torch.uint8"
out_scale = out_scale.permute(permute_order)
# Flatten input tensor for kernel. This will typically make a copy
reshaped_src_tensor = prmted_src_tensor.reshape(-1, L)
blocks_quant_dim = triton.cdiv(reshaped_src_tensor.shape[-1], BLOCK_QUANT_DIM)
blocks_out_dim = triton.cdiv(reshaped_src_tensor.shape[0], BLOCK_OUT_DIM)
# Flatten the output tensors for the kernel, this should be a view always
kernel_quant_tensor = out_quant_tensor.reshape(-1, packed_quant_dim)
kernel_scale = out_scale.reshape(-1, out_scale_dim)
assert kernel_quant_tensor.data_ptr() == out_quant_tensor.data_ptr()
assert kernel_scale.data_ptr() == out_scale.data_ptr()
_downcast_to_mxfp[(blocks_out_dim, blocks_quant_dim)](
kernel_quant_tensor,
*kernel_quant_tensor.stride(),
kernel_scale,
*kernel_scale.stride(),
reshaped_src_tensor,
*reshaped_src_tensor.stride(),
*reshaped_src_tensor.shape,
BLOCK_OUT_DIM,
BLOCK_QUANT_DIM,
num_warps=8,
)
out_quant_tensor = out_quant_tensor.permute(permute_order)
out_scale = out_scale.permute(permute_order).contiguous()
return out_quant_tensor, out_scale, permute_shape(prmted_scale_shape, permute_order)
def mxfp4_quantize(x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""Quantize a bf16/fp16 tensor to MXFP4 along its last dimension.
Dispatches to the fastest backend available on the current platform:
- the native XPU custom op on XPU,
- aiter on ROCm when `aiter` is installed,
- and the portable Triton kernel (``downcast_to_mxfp``) otherwise.
Returns packed FP4 values (uint8, two values per byte) and per-block
(group-32) e8m0 scales (uint8).
"""
if current_platform.is_xpu():
return xpu_mxfp4_quantize(x)
from vllm._aiter_ops import is_aiter_found_and_supported
if is_aiter_found_and_supported() and x.dtype == torch.bfloat16:
from vllm.model_executor.layers.quantization.quark.utils import (
quark_quantize_weight_to_mxfp4,
)
return quark_quantize_weight_to_mxfp4(x)
quant_tensor, scale, _ = downcast_to_mxfp(x, axis=-1)
return quant_tensor, scale
+5
View File
@@ -565,6 +565,11 @@ def has_humming() -> bool:
return _has_module("humming")
def has_quark():
"""Whether the optional `quark` package is available."""
return _has_module("quark")
def check_torchcodec_available():
"""Whether the optional `torchcodec` package is available."""
try: