[Core] Unified quantization framework (#1764)

This commit is contained in:
SYLAR authored and GitHub committed 2026-03-24 18:56:55 +08:00
1 parent 2a630313ce
commit 6b93459826
40 files changed
+3306 -605

No files matched your search

+322 -93
View File
@@ -1,137 +1,366 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for FP8 quantization config."""
"""Tests for the unified quantization framework."""
import pytest
from torch import nn
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion]
def test_fp8_config_creation():
"""Test that FP8 config can be created."""
from vllm_omni.diffusion.quantization import get_diffusion_quant_config
def test_build_quant_config_fp8():
from vllm_omni.quantization import build_quant_config
config = get_diffusion_quant_config("fp8")
config = build_quant_config("fp8")
assert config is not None
assert config.get_name() == "fp8"
assert config.activation_scheme == "dynamic"
def test_vllm_config_extraction():
"""Test that vLLM config can be extracted from diffusion config."""
from vllm_omni.diffusion.quantization import (
get_diffusion_quant_config,
get_vllm_quant_config_for_layers,
)
def test_build_quant_config_none():
from vllm_omni.quantization import build_quant_config
diff_config = get_diffusion_quant_config("fp8")
vllm_config = get_vllm_quant_config_for_layers(diff_config)
assert vllm_config is not None
assert vllm_config.activation_scheme == "dynamic"
assert build_quant_config(None) is None
def test_none_quantization():
"""Test that None quantization returns None config."""
from vllm_omni.diffusion.quantization import (
get_diffusion_quant_config,
get_vllm_quant_config_for_layers,
)
def test_build_quant_config_none_string():
from vllm_omni.quantization import build_quant_config
config = get_diffusion_quant_config(None)
assert config is None
vllm_config = get_vllm_quant_config_for_layers(config)
assert vllm_config is None
assert build_quant_config("none") is None
def test_invalid_quantization():
"""Test that invalid quantization method raises error."""
from vllm_omni.diffusion.quantization import get_diffusion_quant_config
def test_build_quant_config_invalid():
from vllm_omni.quantization import build_quant_config
with pytest.raises(ValueError, match="Unknown quantization method"):
get_diffusion_quant_config("invalid_method")
build_quant_config("invalid_method")
def test_fp8_config_with_custom_params():
"""Test FP8 config with custom parameters."""
from vllm_omni.diffusion.quantization import get_diffusion_quant_config
def test_build_quant_config_dict():
from vllm_omni.quantization import build_quant_config
config = get_diffusion_quant_config(
"fp8",
activation_scheme="static",
ignored_layers=["proj_out"],
)
config = build_quant_config({"method": "fp8", "activation_scheme": "static"})
assert config is not None
assert config.get_name() == "fp8"
assert config.activation_scheme == "static"
assert "proj_out" in config.ignored_layers
def test_supported_methods():
"""Test that supported methods list is correct."""
from vllm_omni.diffusion.quantization import SUPPORTED_QUANTIZATION_METHODS
def test_build_quant_config_dict_not_mutated():
from vllm_omni.quantization import build_quant_config
assert "fp8" in SUPPORTED_QUANTIZATION_METHODS
original = {"method": "fp8", "activation_scheme": "static"}
copy = original.copy()
build_quant_config(original)
assert original == copy
def test_quantization_integration():
"""Test end-to-end quantization flow through OmniDiffusionConfig."""
def test_build_quant_config_per_component():
from vllm_omni.quantization import ComponentQuantizationConfig, build_quant_config
config = build_quant_config(
{
"transformer": {"method": "fp8"},
"vae": None,
}
)
assert isinstance(config, ComponentQuantizationConfig)
assert config.component_configs["transformer"].get_name() == "fp8"
assert config.component_configs["vae"] is None
def test_build_quant_config_per_component_string():
from vllm_omni.quantization import ComponentQuantizationConfig, build_quant_config
config = build_quant_config({"transformer": "fp8", "vae": None})
assert isinstance(config, ComponentQuantizationConfig)
assert config.component_configs["transformer"].get_name() == "fp8"
def test_build_quant_config_per_component_inner_dict_not_mutated():
"""Inner component dicts should not be mutated by build_quant_config."""
from vllm_omni.quantization import build_quant_config
inner = {"method": "fp8", "activation_scheme": "static"}
original = inner.copy()
build_quant_config({"transformer": inner, "vae": None})
assert inner == original
def test_flat_dict_not_misdetected_as_per_component():
"""A flat config like {"activation_scheme": "static"} must NOT be treated
as a per-component dict — it should raise ValueError for missing 'method'."""
from vllm_omni.quantization import build_quant_config
with pytest.raises(ValueError, match="must have a 'method' key"):
build_quant_config({"activation_scheme": "static"})
def test_build_quant_config_passthrough():
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm_omni.quantization import build_quant_config
fp8 = Fp8Config(is_checkpoint_fp8_serialized=False, activation_scheme="dynamic")
assert build_quant_config(fp8) is fp8
def test_component_config_routing():
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm_omni.quantization import ComponentQuantizationConfig
fp8 = Fp8Config(is_checkpoint_fp8_serialized=False, activation_scheme="dynamic")
config = ComponentQuantizationConfig(
component_configs={"transformer": fp8, "vae": None},
)
assert config.get_name() == "component"
assert config.resolve("transformer.blocks.0.attn") is fp8
assert config.resolve("vae.encoder.conv_in") is None
assert config.resolve("unknown.layer") is None
def test_component_config_with_default():
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm_omni.quantization import ComponentQuantizationConfig
fp8 = Fp8Config(is_checkpoint_fp8_serialized=False, activation_scheme="dynamic")
config = ComponentQuantizationConfig(
component_configs={"vae": None},
default_config=fp8,
)
assert config.resolve("transformer.blocks.0") is fp8
assert config.resolve("vae.encoder") is None
def test_gguf_config():
from vllm_omni.quantization import build_quant_config
from vllm_omni.quantization.gguf_config import DiffusionGGUFConfig
config = build_quant_config(
{
"method": "gguf",
"gguf_model": "path/to/model.gguf",
}
)
assert isinstance(config, DiffusionGGUFConfig)
assert config.gguf_model == "path/to/model.gguf"
assert config.get_name() == "gguf"
def test_integration_string():
from vllm_omni.diffusion.data import OmniDiffusionConfig
# Test with quantization string only
config = OmniDiffusionConfig(model="test", quantization="fp8")
config = OmniDiffusionConfig(model="test", quantization_config="fp8")
assert config.quantization_config is not None
assert config.quantization_config.get_name() == "fp8"
# Test with quantization_config dict
config2 = OmniDiffusionConfig(
def test_integration_dict():
from vllm_omni.diffusion.data import OmniDiffusionConfig
config = OmniDiffusionConfig(
model="test",
quantization_config={"method": "fp8", "activation_scheme": "static"},
)
assert config2.quantization_config is not None
assert config2.quantization_config.get_name() == "fp8"
assert config2.quantization_config.activation_scheme == "static"
# Test that vLLM config can be extracted
vllm_config = config.quantization_config.get_vllm_quant_config()
assert vllm_config is not None
def test_quantization_dict_not_mutated():
"""Test that passing a dict to quantization_config doesn't mutate it."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
original_dict = {"method": "fp8", "activation_scheme": "static"}
dict_copy = original_dict.copy()
OmniDiffusionConfig(model="test", quantization_config=original_dict)
# Original dict should be unchanged
assert original_dict == dict_copy
def test_quantization_conflicting_methods_warning(caplog):
"""Test warning when quantization and quantization_config['method'] conflict."""
import logging
from vllm_omni.diffusion.data import OmniDiffusionConfig
with caplog.at_level(logging.WARNING):
config = OmniDiffusionConfig(
model="test",
quantization="fp8", # This should be overridden
quantization_config={"method": "fp8", "activation_scheme": "static"},
)
# No warning when methods match
assert config.quantization_config is not None
assert config.quantization_config.get_name() == "fp8"
assert config.quantization_config.activation_scheme == "static"
def test_fp8_delegates_to_vllm_config():
"""Test that DiffusionFp8Config delegates to vLLM's Fp8Config."""
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
def test_integration_no_quant():
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.diffusion.quantization import DiffusionFp8Config
config = OmniDiffusionConfig(model="test")
assert config.quantization_config is None
# Test that quant_config_cls is set correctly
assert DiffusionFp8Config.quant_config_cls is Fp8Config
# Test that get_name() delegates to vLLM
assert DiffusionFp8Config.get_name() == Fp8Config.get_name()
def test_integration_per_component():
"""OmniDiffusionConfig with per-component quantization dict."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.quantization import ComponentQuantizationConfig
# Test that get_min_capability() delegates to vLLM
assert DiffusionFp8Config.get_min_capability() == Fp8Config.get_min_capability()
config = OmniDiffusionConfig(
model="test",
quantization_config={
"transformer": {"method": "fp8"},
"vae": None,
},
)
assert isinstance(config.quantization_config, ComponentQuantizationConfig)
assert config.quantization_config.component_configs["transformer"].get_name() == "fp8"
assert config.quantization_config.component_configs["vae"] is None
def test_supported_methods_includes_vllm():
from vllm_omni.quantization import SUPPORTED_QUANTIZATION_METHODS
for method in ["fp8", "gguf", "awq", "gptq", "bitsandbytes", "modelopt", "modelopt_fp4"]:
assert method in SUPPORTED_QUANTIZATION_METHODS, f"{method} missing"
def test_supported_methods_count():
from vllm_omni.quantization import SUPPORTED_QUANTIZATION_METHODS
assert len(SUPPORTED_QUANTIZATION_METHODS) >= 20
def test_per_component_routing_with_resolve():
"""Verify resolve() routes correctly by prefix."""
from vllm_omni.quantization import ComponentQuantizationConfig, build_quant_config
config = build_quant_config(
{
"transformer": {"method": "fp8"},
"vae": None,
}
)
assert isinstance(config, ComponentQuantizationConfig)
assert config.resolve("transformer.blocks.0.attn.to_q") is not None
assert config.resolve("transformer.blocks.0.attn.to_q").get_name() == "fp8"
assert config.resolve("vae.encoder.conv_in") is None
assert config.resolve("unknown.layer.0.weight") is None
def test_per_component_routing_with_default():
"""Verify default config applies to unmatched prefixes."""
from vllm_omni.quantization import ComponentQuantizationConfig, build_quant_config
config = build_quant_config(
{
"vae": None,
"default": "fp8",
}
)
assert isinstance(config, ComponentQuantizationConfig)
assert config.resolve("vae.decoder.conv") is None
resolved = config.resolve("transformer.blocks.0.attn")
assert resolved is not None
assert resolved.get_name() == "fp8"
@pytest.mark.parametrize(
"quant_algo",
["FP8", "NVFP4"],
ids=["modelopt_fp8", "modelopt_nvfp4"],
)
def test_omni_convertor_thinker_finds_text_config_quant(quant_algo):
"""Thinker stage should discover quantization_config from
thinker_config.text_config for both FP8 and NVFP4 modelopt checkpoints."""
from types import SimpleNamespace
from vllm_omni.config.model import OmniModelArchConfigConvertor
text_config = SimpleNamespace(
quantization_config={
"quant_method": "modelopt",
"quant_algo": quant_algo,
"ignore": ["lm_head", "model.layers.0.mlp.gate"],
},
model_type="qwen3_moe",
)
thinker_config = SimpleNamespace(text_config=text_config)
hf_config = SimpleNamespace(
thinker_config=thinker_config,
talker_config=SimpleNamespace(text_config=SimpleNamespace()),
model_type="qwen3_omni_moe",
)
convertor = OmniModelArchConfigConvertor(hf_config, text_config, stage_config_name="thinker_config")
quant_cfg = convertor.get_quantization_config()
assert quant_cfg is not None
assert quant_cfg["quant_method"] == "modelopt"
assert "lm_head" in quant_cfg["ignore"]
def test_omni_convertor_talker_returns_none():
"""Talker stage should get no quantization config when its text_config
has no quantization_config (talker weights are BF16)."""
from types import SimpleNamespace
from vllm_omni.config.model import OmniModelArchConfigConvertor
talker_text_config = SimpleNamespace(model_type="qwen3_omni_moe_talker")
talker_config = SimpleNamespace(text_config=talker_text_config)
hf_config = SimpleNamespace(
talker_config=talker_config,
model_type="qwen3_omni_moe",
)
convertor = OmniModelArchConfigConvertor(hf_config, talker_text_config, stage_config_name="talker_config")
quant_cfg = convertor.get_quantization_config()
assert quant_cfg is None
def test_omni_convertor_no_stage_name_falls_back():
"""Without stage_config_name, should fall back to base behavior."""
from types import SimpleNamespace
from vllm_omni.config.model import OmniModelArchConfigConvertor
hf_config = SimpleNamespace(model_type="qwen3_omni_moe")
text_config = SimpleNamespace()
convertor = OmniModelArchConfigConvertor(hf_config, text_config)
quant_cfg = convertor.get_quantization_config()
assert quant_cfg is None
def test_multi_component_model_routing():
"""Integration test: walk a multi-component model and verify per-component
quantization routes resolve() correctly for every linear layer."""
from vllm_omni.quantization import ComponentQuantizationConfig, build_quant_config
# Build a mock multi-stage model mimicking Bagel/Qwen3-Omni layout
class MockTransformerBlock(nn.Module):
def __init__(self):
super().__init__()
self.attn_q = nn.Linear(64, 64)
self.attn_k = nn.Linear(64, 64)
self.mlp = nn.Linear(64, 256)
class MockVAEBlock(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Linear(64, 64)
class MockMultiStageModel(nn.Module):
def __init__(self):
super().__init__()
self.transformer = nn.ModuleDict(
{
"block_0": MockTransformerBlock(),
"block_1": MockTransformerBlock(),
}
)
self.vae = nn.ModuleDict(
{
"encoder": MockVAEBlock(),
"decoder": MockVAEBlock(),
}
)
model = MockMultiStageModel()
config = build_quant_config(
{
"transformer": {"method": "fp8", "activation_scheme": "dynamic"},
"vae": None,
}
)
assert isinstance(config, ComponentQuantizationConfig)
for name, module in model.named_modules():
if isinstance(module, nn.Linear):
resolved = config.resolve(name)
if name.startswith("transformer"):
assert resolved is not None, f"{name} should be quantized"
assert resolved.get_name() == "fp8"
elif name.startswith("vae"):
assert resolved is None, f"{name} should NOT be quantized"
@@ -9,16 +9,12 @@ from types import SimpleNamespace
import pytest
import torch
from vllm.model_executor.layers.linear import LinearBase
from vllm.model_executor.layers.quantization.gguf import UNQUANTIZED_TYPES
from vllm.model_executor.layers.quantization.gguf import UNQUANTIZED_TYPES, UnquantizedLinearMethod
from vllm_omni.diffusion.quantization import (
DiffusionGgufConfig,
get_diffusion_quant_config,
)
from vllm_omni.diffusion.quantization.gguf import (
from vllm_omni.quantization import build_quant_config
from vllm_omni.quantization.gguf_config import (
DiffusionGGUFConfig,
DiffusionGGUFLinearMethod,
UnquantizedLinearMethod,
_GGUFConfig,
dequant_gemm_gguf,
)
@@ -26,7 +22,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
def test_gguf_config_creation_and_delegation():
config = DiffusionGgufConfig(
config = DiffusionGGUFConfig(
gguf_model="weights.gguf",
unquantized_modules=["proj_out"],
)
@@ -34,37 +30,36 @@ def test_gguf_config_creation_and_delegation():
assert config.gguf_model == "weights.gguf"
assert config.unquantized_modules == ["proj_out"]
assert config.get_name() == "gguf"
assert isinstance(config.get_vllm_quant_config(), _GGUFConfig)
def test_get_diffusion_quant_config_builds_gguf_config():
config = get_diffusion_quant_config(
def test_build_quant_config_builds_gguf_config():
config = build_quant_config(
"gguf",
gguf_model="repo/model-Q4_0.gguf",
unquantized_modules=["norm_out"],
)
assert isinstance(config, DiffusionGgufConfig)
assert isinstance(config, DiffusionGGUFConfig)
assert config.gguf_model == "repo/model-Q4_0.gguf"
assert config.unquantized_modules == ["norm_out"]
def test_gguf_vllm_config_returns_diffusion_linear_method_for_linear_layers():
linear = object.__new__(LinearBase)
method = _GGUFConfig(unquantized_modules=[]).get_quant_method(linear, "transformer.img_in")
method = DiffusionGGUFConfig(unquantized_modules=[]).get_quant_method(linear, "transformer.img_in")
assert isinstance(method, DiffusionGGUFLinearMethod)
def test_gguf_vllm_config_respects_unquantized_modules():
linear = object.__new__(LinearBase)
method = _GGUFConfig(unquantized_modules=["proj_out"]).get_quant_method(linear, "transformer.proj_out")
method = DiffusionGGUFConfig(unquantized_modules=["proj_out"]).get_quant_method(linear, "transformer.proj_out")
assert isinstance(method, UnquantizedLinearMethod)
def test_gguf_vllm_config_returns_none_for_non_linear_layers():
method = _GGUFConfig(unquantized_modules=[]).get_quant_method(torch.nn.LayerNorm(4), "norm")
method = DiffusionGGUFConfig(unquantized_modules=[]).get_quant_method(torch.nn.LayerNorm(4), "norm")
assert method is None
@@ -81,7 +76,7 @@ def test_dequant_gemm_gguf_uses_plain_matmul_for_unquantized_types():
def test_diffusion_gguf_linear_method_applies_bias_on_unquantized_weight():
qweight_type = next(iter(UNQUANTIZED_TYPES))
method = DiffusionGGUFLinearMethod(_GGUFConfig(unquantized_modules=[]))
method = DiffusionGGUFLinearMethod(DiffusionGGUFConfig(unquantized_modules=[]))
layer = SimpleNamespace(
qweight=torch.tensor([[1.0, 2.0], [3.0, 4.0]], dtype=torch.float32),
qweight_type=SimpleNamespace(weight_type=qweight_type),
@@ -97,7 +92,7 @@ def test_diffusion_gguf_linear_method_applies_bias_on_unquantized_weight():
def test_diffusion_gguf_linear_method_concatenates_sharded_outputs():
qweight_type = next(iter(UNQUANTIZED_TYPES))
method = DiffusionGGUFLinearMethod(_GGUFConfig(unquantized_modules=[]))
method = DiffusionGGUFLinearMethod(DiffusionGGUFConfig(unquantized_modules=[]))
qweight = torch.nn.Parameter(
torch.tensor(
[
@@ -10,11 +10,11 @@ from pytest_mock import MockerFixture
from torch.nn import Module, Parameter
from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
from vllm_omni.diffusion.quantization import (
get_diffusion_quant_config,
get_vllm_quant_config_for_layers,
)
from vllm_omni.platforms import current_omni_platform
from vllm_omni.quantization import build_quant_config
from vllm_omni.quantization.factory import SUPPORTED_QUANTIZATION_METHODS
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion]
npu_available = pytest.mark.skipif(not current_omni_platform.is_npu(), reason="NPU platform not available.")
@@ -23,36 +23,26 @@ cuda_available = pytest.mark.skipif(not current_omni_platform.is_cuda(), reason=
def test_int8_config_creation():
"""Test that Int8 config can be created."""
config = get_diffusion_quant_config("int8")
config = build_quant_config("int8")
assert config is not None
assert config.get_name() == "int8"
def test_vllm_config_extraction():
"""Test that vLLM config can be extracted from diffusion config."""
diff_config = get_diffusion_quant_config("int8")
vllm_config = get_vllm_quant_config_for_layers(diff_config)
assert vllm_config is not None
assert vllm_config.activation_scheme == "dynamic"
def test_none_quantization():
"""Test that None quantization returns None config."""
config = get_diffusion_quant_config(None)
config = build_quant_config(None)
assert config is None
vllm_config = get_vllm_quant_config_for_layers(config)
assert vllm_config is None
def test_invalid_quantization():
"""Test that invalid quantization method raises error."""
with pytest.raises(ValueError, match="Unknown quantization method"):
get_diffusion_quant_config("invalid_method")
build_quant_config("invalid_method")
def test_int8_config_with_custom_params():
"""Test Int8 config with custom parameters."""
config = get_diffusion_quant_config(
config = build_quant_config(
"int8",
activation_scheme="dynamic",
ignored_layers=["proj_out"],
@@ -64,8 +54,6 @@ def test_int8_config_with_custom_params():
def test_supported_methods():
"""Test that supported methods list is correct."""
from vllm_omni.diffusion.quantization import SUPPORTED_QUANTIZATION_METHODS
assert "int8" in SUPPORTED_QUANTIZATION_METHODS
@@ -73,8 +61,8 @@ def test_quantization_integration():
"""Test end-to-end quantization flow through OmniDiffusionConfig."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
# Test with quantization string only
config = OmniDiffusionConfig(model="test", quantization="int8")
# Test with quantization_config string
config = OmniDiffusionConfig(model="test", quantization_config="int8")
assert config.quantization_config is not None
assert config.quantization_config.get_name() == "int8"
@@ -87,10 +75,6 @@ def test_quantization_integration():
assert config2.quantization_config.get_name() == "int8"
assert config2.quantization_config.activation_scheme == "dynamic"
# Test that vLLM config can be extracted
vllm_config = config.quantization_config.get_vllm_quant_config()
assert vllm_config is not None
def test_quantization_dict_not_mutated():
"""Test that passing a dict to quantization_config doesn't mutate it."""
@@ -105,28 +89,24 @@ def test_quantization_dict_not_mutated():
assert original_dict == dict_copy
def test_quantization_conflicting_methods_warning(caplog):
"""Test warning when quantization and quantization_config['method'] conflict."""
import logging
def test_quantization_config_string_and_dict_equivalent():
"""Test that string and dict forms produce equivalent configs."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
with caplog.at_level(logging.WARNING):
config = OmniDiffusionConfig(
model="test",
quantization="int8", # This should be overridden
quantization_config={"method": "int8", "activation_scheme": "dynamic"},
)
# No warning when methods match
assert config.quantization_config is not None
config_str = OmniDiffusionConfig(model="test", quantization_config="int8")
config_dict = OmniDiffusionConfig(
model="test",
quantization_config={"method": "int8", "activation_scheme": "dynamic"},
)
assert config_str.quantization_config.get_name() == config_dict.quantization_config.get_name()
assert config_str.quantization_config.activation_scheme == config_dict.quantization_config.activation_scheme
def test_get_quant_method(mocker: MockerFixture):
"""Test for get_quant_method method for GPU"""
from vllm_omni.diffusion.quantization.int8 import Int8OnlineLinearMethod
from vllm_omni.quantization.int8_config import Int8OnlineLinearMethod
diff_config = get_diffusion_quant_config("int8")
vllm_config = get_vllm_quant_config_for_layers(diff_config)
config = build_quant_config("int8")
def _fake_init(self, quant_config):
pass
@@ -141,21 +121,20 @@ def test_get_quant_method(mocker: MockerFixture):
patch("vllm_omni.platforms.current_omni_platform.is_cuda", return_value=True),
patch("vllm_omni.platforms.current_omni_platform.is_npu", return_value=False),
):
method = vllm_config.get_quant_method(layer, prefix)
method = config.get_quant_method(layer, prefix)
assert isinstance(method, Int8OnlineLinearMethod)
# Test skipping quantization for a layer
vllm_config.ignored_layers = [prefix]
method = vllm_config.get_quant_method(layer, prefix)
config.ignored_layers = [prefix]
method = config.get_quant_method(layer, prefix)
assert isinstance(method, UnquantizedLinearMethod)
def test_get_npu_quant_method():
"""Test for get_quant_method method for NPU"""
from vllm_omni.diffusion.quantization.int8 import NPUInt8OnlineLinearMethod
from vllm_omni.quantization.int8_config import NPUInt8OnlineLinearMethod
diff_config = get_diffusion_quant_config("int8")
vllm_config = get_vllm_quant_config_for_layers(diff_config)
config = build_quant_config("int8")
layer = MagicMock(spec=LinearBase)
prefix = "test_layer"
@@ -165,12 +144,12 @@ def test_get_npu_quant_method():
patch("vllm_omni.platforms.current_omni_platform.is_cuda", return_value=False),
patch("vllm_omni.platforms.current_omni_platform.is_npu", return_value=True),
):
method = vllm_config.get_quant_method(layer, prefix)
method = config.get_quant_method(layer, prefix)
assert isinstance(method, NPUInt8OnlineLinearMethod)
# Test skipping quantization for a layer
vllm_config.ignored_layers = [prefix]
method = vllm_config.get_quant_method(layer, prefix)
config.ignored_layers = [prefix]
method = config.get_quant_method(layer, prefix)
assert isinstance(method, UnquantizedLinearMethod)
@@ -189,12 +168,12 @@ class TestInt8LinearMethod:
@pytest.fixture
def patch_deps(self, mocker, mock_kernel):
# mock init_int8_linear_kernel
mocker.patch("vllm_omni.diffusion.quantization.int8.init_int8_linear_kernel", return_value=mock_kernel)
mocker.patch("vllm_omni.quantization.int8_config.init_int8_linear_kernel", return_value=mock_kernel)
return mock_kernel
def test_init(self, patch_deps, mock_quant_config):
# test for Int8LinearMethod init
from vllm_omni.diffusion.quantization.int8 import Int8LinearMethod, init_int8_linear_kernel
from vllm_omni.quantization.int8_config import Int8LinearMethod, init_int8_linear_kernel
method = Int8LinearMethod(mock_quant_config)
@@ -205,7 +184,7 @@ class TestInt8LinearMethod:
assert method.int8_linear == patch_deps
def test_process_weights_after_loading(self, patch_deps, mock_quant_config):
from vllm_omni.diffusion.quantization.int8 import Int8LinearMethod
from vllm_omni.quantization.int8_config import Int8LinearMethod
method = Int8LinearMethod(mock_quant_config)
layer = Module()
@@ -214,7 +193,7 @@ class TestInt8LinearMethod:
patch_deps.process_weights_after_loading.assert_called_once_with(layer)
def test_apply(self, patch_deps, mock_quant_config):
from vllm_omni.diffusion.quantization.int8 import Int8LinearMethod
from vllm_omni.quantization.int8_config import Int8LinearMethod
method = Int8LinearMethod(mock_quant_config)
layer = Module()
@@ -237,19 +216,19 @@ class TestInt8OnlineLinearMethod:
# mock kernel
mock_kernel = mocker.Mock()
mock_kernel.layer_param_names = ("weight", "weight_scale", "input_scale", "input_zero_point", "azp_adj")
mocker.patch("vllm_omni.diffusion.quantization.int8.init_int8_linear_kernel", return_value=mock_kernel)
mocker.patch("vllm_omni.diffusion.quantization.int8.replace_parameter")
mocker.patch("vllm_omni.quantization.int8_config.init_int8_linear_kernel", return_value=mock_kernel)
mocker.patch("vllm_omni.quantization.int8_config.replace_parameter")
# mock scaled_int8_quant return value
mock_qweight = torch.ones((128, 64), dtype=torch.int8)
mock_scale = torch.randn(128)
mock_quant = mocker.patch(
"vllm_omni.diffusion.quantization.int8.ops.scaled_int8_quant", return_value=(mock_qweight, mock_scale, None)
"vllm_omni.quantization.int8_config.ops.scaled_int8_quant", return_value=(mock_qweight, mock_scale, None)
)
return {"kernel": mock_kernel, "quant": mock_quant, "mock_qweight": mock_qweight, "mock_scale": mock_scale}
def test_process_weights_after_loading(self, mock_deps, mock_quant_config):
from vllm_omni.diffusion.quantization.int8 import Int8OnlineLinearMethod
from vllm_omni.quantization.int8_config import Int8OnlineLinearMethod
method = Int8OnlineLinearMethod(mock_quant_config)
layer = Module()
@@ -268,12 +247,12 @@ class TestNPUInt8LinearMethod:
def mock_torch_npu(self, mocker):
torch_npu = MagicMock()
mocker.patch("vllm_omni.diffusion.quantization.int8.torch_npu", return_value=torch_npu)
mocker.patch("vllm_omni.quantization.int8_config.torch_npu", return_value=torch_npu)
mocker.patch(
"vllm_omni.diffusion.quantization.int8.torch_npu.npu_dynamic_quant",
"vllm_omni.quantization.int8_config.torch_npu.npu_dynamic_quant",
return_value=(self.qweight_mock, self.scale_mock),
)
mocker.patch("vllm_omni.diffusion.quantization.int8.torch_npu.npu_quant_matmul", return_value=self.out_mock)
mocker.patch("vllm_omni.quantization.int8_config.torch_npu.npu_quant_matmul", return_value=self.out_mock)
return torch_npu
@pytest.fixture
@@ -288,7 +267,7 @@ class TestNPUInt8LinearMethod:
return layer
def test_npu_int8_process_weights_after_loading(self, mock_layer, mock_quant_config, mock_torch_npu):
from vllm_omni.diffusion.quantization.int8 import NPUInt8LinearMethod
from vllm_omni.quantization.int8_config import NPUInt8LinearMethod
method = NPUInt8LinearMethod(mock_quant_config)
ori_weight_shape = mock_layer.weight.shape
@@ -299,7 +278,7 @@ class TestNPUInt8LinearMethod:
assert mock_layer.weight.is_contiguous()
def test_npu_int8_apply(self, mock_layer, mock_quant_config, mock_torch_npu):
from vllm_omni.diffusion.quantization.int8 import NPUInt8LinearMethod
from vllm_omni.quantization.int8_config import NPUInt8LinearMethod
method = NPUInt8LinearMethod(mock_quant_config)
x = torch.randn(1, 16, 64)
@@ -308,7 +287,7 @@ class TestNPUInt8LinearMethod:
assert output.shape == (1, 16, 128)
def test_npu_int8_online_process_weights(self, mock_layer, mock_quant_config, mock_torch_npu):
from vllm_omni.diffusion.quantization.int8 import NPUInt8OnlineLinearMethod
from vllm_omni.quantization.int8_config import NPUInt8OnlineLinearMethod
method = NPUInt8OnlineLinearMethod(mock_quant_config)
method.process_weights_after_loading(mock_layer)
@@ -320,9 +299,9 @@ class TestNPUInt8LinearMethod:
@pytest.fixture
def quant_config():
"""Shared quant config fixture for smoke tests."""
from vllm_omni.diffusion.quantization.int8 import Int8Config
from vllm_omni.quantization.int8_config import DiffusionInt8Config
return Int8Config(
return DiffusionInt8Config(
is_checkpoint_int8_serialized=False,
activation_scheme="dynamic",
)
@@ -360,7 +339,7 @@ class TestNPUInt8Smoke:
def test_real_npu_online_process_weights_after_loading(self, quant_config, real_layer):
"""Smoke test: full process_weights_after_loading with real torch_npu."""
from vllm_omni.diffusion.quantization.int8 import NPUInt8OnlineLinearMethod
from vllm_omni.quantization.int8_config import NPUInt8OnlineLinearMethod
method = NPUInt8OnlineLinearMethod(quant_config)
@@ -375,7 +354,7 @@ class TestNPUInt8Smoke:
"""Smoke test: forward pass with real npu_quant_matmul."""
import torch_npu
from vllm_omni.diffusion.quantization.int8 import NPUInt8LinearMethod
from vllm_omni.quantization.int8_config import NPUInt8LinearMethod
method = NPUInt8LinearMethod(quant_config)
@@ -425,7 +404,7 @@ class TestCudaInt8Smoke:
def test_real_cuda_online_process_weights_after_loading(self, quant_config, real_layer):
"""Smoke test: full process_weights_after_loading with real CUDA ops."""
from vllm_omni.diffusion.quantization.int8 import Int8OnlineLinearMethod
from vllm_omni.quantization.int8_config import Int8OnlineLinearMethod
method = Int8OnlineLinearMethod(quant_config)
@@ -439,7 +418,7 @@ class TestCudaInt8Smoke:
"""Smoke test: forward pass with real CUDA int8 kernel."""
from vllm import _custom_ops as ops
from vllm_omni.diffusion.quantization.int8 import Int8LinearMethod
from vllm_omni.quantization.int8_config import Int8LinearMethod
method = Int8LinearMethod(quant_config)
@@ -0,0 +1,240 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Quantization quality gate — validates that quantized models stay within
acceptable perceptual distance (LPIPS) of the BF16 baseline.
Developers adding a new quantization method should:
1. Add their method + model to QUALITY_CONFIGS below
2. Set a max_lpips threshold (use 0.15 for image, 0.20 for video as defaults)
3. Run: pytest tests/diffusion/quantization/test_quantization_quality.py -v -m ""
4. Paste the output table into their PR description
The test generates outputs with both BF16 and the quantized method using the
same seed, computes LPIPS, and fails if it exceeds the threshold.
Requirements:
pip install lpips
Example — run only FP8 tests:
pytest tests/diffusion/quantization/test_quantization_quality.py -v -m "" -k "fp8"
Example — run a specific model:
pytest tests/diffusion/quantization/test_quantization_quality.py -v -m "" -k "z_image"
"""
from __future__ import annotations
import gc
from dataclasses import dataclass
import numpy as np
import pytest
import torch
from tests.utils import hardware_marks
# ---------------------------------------------------------------------------
# Configuration — add new quantization methods / models here
# ---------------------------------------------------------------------------
@dataclass
class QualityTestConfig:
"""Defines a single quantization quality test case."""
id: str # pytest ID, e.g. "fp8_z_image"
model: str # HF model name
quantization: str # quantization method, e.g. "fp8"
task: str # "t2i" or "t2v"
prompt: str # generation prompt
max_lpips: float # fail threshold — higher = more lenient
height: int = 1024
width: int = 1024
num_inference_steps: int = 20 # keep low for CI speed
num_frames: int = 5 # only for t2v
seed: int = 42
gpu: str = "H100" # minimum GPU requirement
# Add new quantization methods / models here.
# Developers: copy a config, change quantization + max_lpips, run the test.
QUALITY_CONFIGS = [
QualityTestConfig(
id="fp8_z_image",
model="Tongyi-MAI/Z-Image-Turbo",
quantization="fp8",
task="t2i",
prompt="a cup of coffee on a wooden table, morning light",
max_lpips=0.10,
num_inference_steps=20,
),
QualityTestConfig(
id="fp8_flux",
model="black-forest-labs/FLUX.1-dev",
quantization="fp8",
task="t2i",
prompt="a cup of coffee on a wooden table, morning light",
max_lpips=0.20,
num_inference_steps=10,
),
QualityTestConfig(
id="fp8_qwen_image",
model="Qwen/Qwen-Image",
quantization="fp8",
task="t2i",
prompt="a cup of coffee on a wooden table, morning light",
max_lpips=0.35,
seed=142,
num_inference_steps=20,
),
]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _generate_image(omni, config: QualityTestConfig):
"""Generate a single image, return (PIL.Image, peak_mem_gib)."""
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.platforms import current_omni_platform
generator = torch.Generator(
device=current_omni_platform.device_type,
).manual_seed(config.seed)
torch.cuda.reset_peak_memory_stats()
outputs = omni.generate(
{"prompt": config.prompt},
OmniDiffusionSamplingParams(
height=config.height,
width=config.width,
generator=generator,
num_inference_steps=config.num_inference_steps,
),
)
peak_mem = torch.cuda.max_memory_allocated() / (1024**3)
first = outputs[0]
req_out = first.request_output[0] if hasattr(first, "request_output") else first
return req_out.images[0], peak_mem
def _generate_video(omni, config: QualityTestConfig):
"""Generate a video, return (np.ndarray [F,H,W,C], peak_mem_gib)."""
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.outputs import OmniRequestOutput
from vllm_omni.platforms import current_omni_platform
generator = torch.Generator(
device=current_omni_platform.device_type,
).manual_seed(config.seed)
torch.cuda.reset_peak_memory_stats()
outputs = omni.generate(
{"prompt": config.prompt, "negative_prompt": ""},
OmniDiffusionSamplingParams(
height=config.height,
width=config.width,
generator=generator,
num_inference_steps=config.num_inference_steps,
num_frames=config.num_frames,
),
)
peak_mem = torch.cuda.max_memory_allocated() / (1024**3)
first = outputs[0]
if hasattr(first, "request_output") and isinstance(first.request_output, list):
inner = first.request_output[0]
if isinstance(inner, OmniRequestOutput) and hasattr(inner, "images"):
frames = inner.images[0] if inner.images else None
else:
frames = inner
elif hasattr(first, "images") and first.images:
frames = first.images
else:
raise ValueError("Could not extract video frames from output.")
if isinstance(frames, torch.Tensor):
video = frames.detach().cpu()
if video.dim() == 5:
video = video[0]
if video.dim() == 4 and video.shape[0] in (3, 4):
video = video.permute(1, 2, 3, 0)
if video.is_floating_point():
video = video.clamp(-1, 1) * 0.5 + 0.5
return video.float().numpy(), peak_mem
return np.asarray(frames), peak_mem
def _compute_lpips(baseline, quantized, task: str) -> float:
"""Compute LPIPS between baseline and quantized outputs."""
from benchmarks.diffusion.quantization_quality import (
compute_lpips_images,
compute_lpips_video,
)
if task == "t2i":
return compute_lpips_images([baseline], [quantized])[0]
return compute_lpips_video(baseline, quantized)
def _unload(omni):
del omni
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.synchronize()
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
_marks = hardware_marks(res={"cuda": "H100"})
@pytest.mark.advanced_model
@pytest.mark.diffusion
@pytest.mark.parametrize(
"config",
[pytest.param(c, id=c.id, marks=_marks) for c in QUALITY_CONFIGS],
)
def test_quantization_quality(config: QualityTestConfig):
"""Validate that quantized output stays within LPIPS threshold of BF16."""
from vllm_omni.entrypoints.omni import Omni
generate_fn = _generate_video if config.task == "t2v" else _generate_image
# --- BF16 baseline ---
omni_bl = Omni(model=config.model)
baseline_out, bl_mem = generate_fn(omni_bl, config)
_unload(omni_bl)
# --- Quantized ---
omni_qt = Omni(model=config.model, quantization_config=config.quantization)
quant_out, qt_mem = generate_fn(omni_qt, config)
_unload(omni_qt)
# --- LPIPS ---
lpips_score = _compute_lpips(baseline_out, quant_out, config.task)
# --- Report ---
mem_reduction = (bl_mem - qt_mem) / bl_mem * 100 if bl_mem > 0 else 0
print(f"\n{'=' * 60}")
print(f"Quantization Quality: {config.id}")
print(f"{'=' * 60}")
print(f" Model: {config.model}")
print(f" Method: {config.quantization}")
print(f" LPIPS: {lpips_score:.4f} (threshold: {config.max_lpips})")
print(f" BF16 memory: {bl_mem:.2f} GiB")
print(f" Quant memory: {qt_mem:.2f} GiB ({mem_reduction:.0f}% reduction)")
print(f" Result: {'PASS' if lpips_score <= config.max_lpips else 'FAIL'}")
print(f"{'=' * 60}\n")
assert lpips_score <= config.max_lpips, (
f"LPIPS {lpips_score:.4f} exceeds threshold {config.max_lpips} for {config.quantization} on {config.model}"
)