vllm-project--vllm-omni
338 行
13 KiB
Python
338 行
13 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""Tests for MXFP8 quantization config and linear method dispatch.
|
|
|
|
Coverage:
|
|
- DiffusionMXFP8Config.from_config roundtrips (CPU, no NPU required)
|
|
- get_quant_method dispatch (mocked platform)
|
|
- MXFPLinearMethodBase.apply() reshape skeleton (CPU)
|
|
- Weight / scale shape-transform arithmetic from process_weights_after_loading (CPU)
|
|
- build_quant_config integration
|
|
- MXFP8_QUANT_CONFIG structure as the auto-detection contract
|
|
"""
|
|
|
|
import pytest
|
|
import torch
|
|
from pytest_mock import MockerFixture
|
|
from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
|
|
|
|
from vllm_omni.platforms import current_omni_platform
|
|
from vllm_omni.quantization import build_quant_config
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
|
|
|
npu_available = pytest.mark.skipif(not current_omni_platform.is_npu(), reason="NPU platform not available")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# DiffusionMXFP8Config — from_config roundtrips
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_mxfp8_config_get_name():
|
|
from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
|
|
|
assert DiffusionMXFP8Config.get_name() == "mxfp8"
|
|
|
|
|
|
def test_mxfp8_config_from_config_defaults():
|
|
from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
|
|
|
cfg = DiffusionMXFP8Config.from_config({})
|
|
assert cfg.is_checkpoint_mxfp8_serialized is False
|
|
assert cfg.ignored_layers == []
|
|
|
|
|
|
def test_mxfp8_config_from_config_serialized():
|
|
from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
|
|
|
cfg = DiffusionMXFP8Config.from_config({"is_checkpoint_mxfp8_serialized": True})
|
|
assert cfg.is_checkpoint_mxfp8_serialized is True
|
|
|
|
|
|
def test_mxfp8_config_from_config_ignored_layers():
|
|
from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
|
|
|
cfg = DiffusionMXFP8Config.from_config({"ignored_layers": ["proj_out"]})
|
|
assert cfg.ignored_layers == ["proj_out"]
|
|
|
|
|
|
def test_mxfp8_config_from_config_modules_to_not_convert_fallback():
|
|
"""modules_to_not_convert must be accepted as an alias for ignored_layers."""
|
|
from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
|
|
|
cfg = DiffusionMXFP8Config.from_config({"modules_to_not_convert": ["proj_out"]})
|
|
assert cfg.ignored_layers == ["proj_out"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# build_quant_config integration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_build_quant_config_mxfp8_string():
|
|
from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
|
|
|
cfg = build_quant_config("mxfp8")
|
|
assert isinstance(cfg, DiffusionMXFP8Config)
|
|
assert cfg.get_name() == "mxfp8"
|
|
assert cfg.is_checkpoint_mxfp8_serialized is False
|
|
|
|
|
|
def test_build_quant_config_mxfp8_dict():
|
|
from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
|
|
|
cfg = build_quant_config({"method": "mxfp8", "is_checkpoint_mxfp8_serialized": True})
|
|
assert isinstance(cfg, DiffusionMXFP8Config)
|
|
assert cfg.is_checkpoint_mxfp8_serialized is True
|
|
|
|
|
|
def test_build_quant_config_mxfp8_config_json_format():
|
|
"""Verify that the exact quantization_config injected by merge_mxfp8_checkpoint.py
|
|
is accepted by build_quant_config and selects the offline (serialized) path.
|
|
|
|
This is the critical auto-detection contract: TransformerConfig.from_dict()
|
|
reads quant_method + is_checkpoint_mxfp8_serialized to pick NPUMxfp8LinearMethod.
|
|
"""
|
|
from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
|
from vllm_omni.quantization.tools.merge_mxfp8_checkpoint import MXFP8_QUANT_CONFIG
|
|
|
|
cfg = build_quant_config(MXFP8_QUANT_CONFIG)
|
|
assert isinstance(cfg, DiffusionMXFP8Config)
|
|
assert cfg.is_checkpoint_mxfp8_serialized is True
|
|
|
|
|
|
def test_mxfp8_quant_config_structure():
|
|
"""MXFP8_QUANT_CONFIG must contain exactly the keys that auto-detection reads."""
|
|
from vllm_omni.quantization.tools.merge_mxfp8_checkpoint import MXFP8_QUANT_CONFIG
|
|
|
|
assert MXFP8_QUANT_CONFIG.get("quant_method") == "mxfp8"
|
|
assert MXFP8_QUANT_CONFIG.get("is_checkpoint_mxfp8_serialized") is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_quant_method dispatch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.skipif(not current_omni_platform.is_npu(), reason="Native MXFP8 offline only supported on NPU")
|
|
def test_get_quant_method_offline_npu(mocker: MockerFixture):
|
|
"""Offline (serialized) path must return NPUMxfp8LinearMethod on NPU."""
|
|
from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
|
|
|
config = DiffusionMXFP8Config(is_checkpoint_mxfp8_serialized=True)
|
|
layer = mocker.Mock(spec=LinearBase)
|
|
|
|
method = config.get_quant_method(layer, "blocks.0.attn1.to_q")
|
|
assert type(method).__name__ == "NPUMxfp8LinearMethod"
|
|
|
|
|
|
@pytest.mark.skipif(not current_omni_platform.is_xpu(), reason="XPU platform not available")
|
|
def test_get_quant_method_offline_xpu_raises(mocker: MockerFixture):
|
|
"""XPU offline mode must raise NotImplementedError (use AutoRound MXFP8 instead)."""
|
|
from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
|
|
|
config = DiffusionMXFP8Config(is_checkpoint_mxfp8_serialized=True)
|
|
layer = mocker.Mock(spec=LinearBase)
|
|
|
|
with pytest.raises(NotImplementedError, match="Native MXFP8 offline mode is not supported on XPU"):
|
|
config.get_quant_method(layer, "blocks.0.attn1.to_q")
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not (current_omni_platform.is_npu() or current_omni_platform.is_xpu()), reason="MXFP8 only supported on NPU and XPU"
|
|
)
|
|
def test_get_quant_method_online(mocker: MockerFixture):
|
|
"""Online (BF16 checkpoint) path must return platform-specific method on current platform."""
|
|
from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
|
|
|
# Mock the vLLM online method to avoid config dependency for XPU
|
|
if current_omni_platform.is_xpu():
|
|
mock_inner = mocker.Mock()
|
|
mocker.patch(
|
|
"vllm_omni.quantization.mxfp8_config.VllmMxfp8OnlineLinearMethod.__init__",
|
|
lambda self: setattr(self, "_inner", mock_inner),
|
|
)
|
|
|
|
config = DiffusionMXFP8Config(is_checkpoint_mxfp8_serialized=False)
|
|
layer = mocker.Mock(spec=LinearBase)
|
|
|
|
method = config.get_quant_method(layer, "blocks.0.attn1.to_q")
|
|
|
|
if current_omni_platform.is_npu():
|
|
assert type(method).__name__ == "NPUMxfp8OnlineLinearMethod"
|
|
elif current_omni_platform.is_xpu():
|
|
assert type(method).__name__ == "VllmMxfp8OnlineLinearMethod"
|
|
|
|
|
|
def test_get_quant_method_unsupported_platform_raises(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
|
|
"""Unsupported platform (CUDA, ROCm) must raise NotImplementedError."""
|
|
from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
|
|
|
config = DiffusionMXFP8Config()
|
|
layer = mocker.Mock(spec=LinearBase)
|
|
monkeypatch.setattr(current_omni_platform, "is_npu", lambda: False)
|
|
monkeypatch.setattr(current_omni_platform, "is_xpu", lambda: False)
|
|
monkeypatch.setattr(current_omni_platform, "is_cuda", lambda: False)
|
|
|
|
with pytest.raises(NotImplementedError):
|
|
config.get_quant_method(layer, "blocks.0.attn1.to_q")
|
|
|
|
|
|
def test_get_quant_method_ignored_layer(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
|
|
"""A prefix in ignored_layers must bypass quantization → UnquantizedLinearMethod."""
|
|
from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
|
|
|
config = DiffusionMXFP8Config(ignored_layers=["proj_out"])
|
|
layer = mocker.Mock(spec=LinearBase)
|
|
monkeypatch.setattr(current_omni_platform, "is_npu", lambda: True)
|
|
monkeypatch.setattr(current_omni_platform, "is_xpu", lambda: False)
|
|
monkeypatch.setattr(current_omni_platform, "is_cuda", lambda: False)
|
|
|
|
method = config.get_quant_method(layer, "proj_out")
|
|
assert isinstance(method, UnquantizedLinearMethod)
|
|
|
|
|
|
def test_get_quant_method_non_linear_returns_none(monkeypatch: pytest.MonkeyPatch):
|
|
"""Non-LinearBase layers (norms, embeddings) must get None → no quantization."""
|
|
from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
|
|
|
config = DiffusionMXFP8Config()
|
|
monkeypatch.setattr(current_omni_platform, "is_npu", lambda: True)
|
|
|
|
norm_layer = torch.nn.LayerNorm(64)
|
|
assert config.get_quant_method(norm_layer, "blocks.0.norm1") is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MXFPLinearMethodBase.apply() — reshape skeleton (CPU, no NPU)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_apply_reshape_skeleton():
|
|
"""apply() must flatten batch dims → _apply_inner → restore original leading dims."""
|
|
from vllm_omni.quantization.mxfp8_config import MXFPLinearMethodBase
|
|
|
|
OUT_FEATURES = 4
|
|
|
|
class _StubMethod(MXFPLinearMethodBase):
|
|
def create_weights(
|
|
self,
|
|
layer,
|
|
input_size_per_partition,
|
|
output_partition_sizes,
|
|
input_size,
|
|
output_size,
|
|
params_dtype,
|
|
**extra_weight_attrs,
|
|
):
|
|
pass
|
|
|
|
def _quantize_activation(self, x):
|
|
return x, None
|
|
|
|
def _quant_matmul(self, x_q, x_scale, layer, bias, ori_dtype):
|
|
return torch.zeros(x_q.shape[0], OUT_FEATURES, dtype=ori_dtype)
|
|
|
|
method = _StubMethod()
|
|
x = torch.randn(2, 3, 8) # (batch=2, seq=3, K=8)
|
|
out = method.apply(None, x)
|
|
assert out.shape == (2, 3, OUT_FEATURES)
|
|
|
|
|
|
def test_apply_reshape_with_bias():
|
|
"""apply() must pass bias through to _apply_inner unchanged."""
|
|
from vllm_omni.quantization.mxfp8_config import MXFPLinearMethodBase
|
|
|
|
received_bias = []
|
|
|
|
class _StubMethod(MXFPLinearMethodBase):
|
|
def create_weights(
|
|
self,
|
|
layer,
|
|
input_size_per_partition,
|
|
output_partition_sizes,
|
|
input_size,
|
|
output_size,
|
|
params_dtype,
|
|
**extra_weight_attrs,
|
|
):
|
|
pass
|
|
|
|
def _quantize_activation(self, x):
|
|
return x, None
|
|
|
|
def _quant_matmul(self, x_q, x_scale, layer, bias, ori_dtype):
|
|
received_bias.append(bias)
|
|
return torch.zeros(x_q.shape[0], 4, dtype=ori_dtype)
|
|
|
|
method = _StubMethod()
|
|
bias = torch.zeros(4)
|
|
method.apply(None, torch.randn(2, 8), bias=bias)
|
|
assert received_bias[0] is bias
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# process_weights_after_loading shape arithmetic (pure torch, no NPU ops)
|
|
#
|
|
# These tests replicate the CPU-safe portions of process_weights_after_loading
|
|
# to guard the key layout contract: (N,K) weight → (K,N) and (N,S) scale →
|
|
# (S/2,N,2). They do NOT call NPU ops; they test only the math.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"n, k",
|
|
[(64, 128), (32, 64), (16, 96)],
|
|
ids=["64x128", "32x64", "16x96"],
|
|
)
|
|
def test_weight_transpose_contract(n: int, k: int):
|
|
"""Weight must be transposed from (N, K) to (K, N) and be contiguous."""
|
|
w = torch.zeros(n, k, dtype=torch.uint8)
|
|
w = w.transpose(0, 1).contiguous()
|
|
assert w.shape == (k, n)
|
|
assert w.is_contiguous()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"n, k_groups, expected_groups",
|
|
[
|
|
(64, 4, 2), # even K_groups — no padding
|
|
(64, 3, 2), # odd K_groups — padded to 4
|
|
(32, 1, 1), # odd K_groups — padded to 2
|
|
],
|
|
ids=["even", "odd-to-4", "odd-to-2"],
|
|
)
|
|
def test_weight_scale_reshape_contract(n: int, k_groups: int, expected_groups: int):
|
|
"""Scale must be reshaped from (N, K_groups) to (K_groups_even//2, N, 2).
|
|
|
|
Odd K_groups must be padded to even before the reshape.
|
|
"""
|
|
s = torch.zeros(n, k_groups, dtype=torch.uint8)
|
|
if k_groups % 2 == 1:
|
|
s = torch.cat([s, torch.zeros(n, 1, dtype=s.dtype)], dim=1)
|
|
k_groups += 1
|
|
s = s.reshape(n, k_groups // 2, 2).transpose(0, 1).contiguous()
|
|
assert s.shape == (expected_groups, n, 2)
|
|
assert s.is_contiguous()
|
|
|
|
|
|
def test_num_groups_formula():
|
|
"""K_groups formula: ceil(K / 32) — spot-check boundary values."""
|
|
assert (31 + 31) // 32 == 1 # K=31 → 1 group
|
|
assert (32 + 31) // 32 == 1 # K=32 → 1 group
|
|
assert (33 + 31) // 32 == 2 # K=33 → 2 groups
|
|
assert (128 + 31) // 32 == 4 # K=128 → 4 groups (even)
|
|
assert (96 + 31) // 32 == 3 # K=96 → 3 groups (odd → needs padding)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SUPPORTED_QUANTIZATION_METHODS
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_supported_methods_include_mxfp8():
|
|
from vllm_omni.quantization import SUPPORTED_QUANTIZATION_METHODS
|
|
|
|
assert "mxfp8" in SUPPORTED_QUANTIZATION_METHODS
|