Signed-off-by: hyh_hh <huyinghong1@huawei.com> Co-authored-by: hyh_hh <huyinghong1@huawei.com>
400 lines
16 KiB
Python
400 lines
16 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""Tests for merge_mxfp4_dualscale_checkpoint.py key-remapping helpers.
|
|
|
|
These are pure-Python unit tests that exercise the transformation functions
|
|
without loading any actual checkpoint files or requiring NPU hardware.
|
|
"""
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SUPPORTED_MODEL_TYPES
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_supported_model_types_includes_a14b():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import SUPPORTED_MODEL_TYPES
|
|
|
|
assert "Wan2.2-T2V-A14B" in SUPPORTED_MODEL_TYPES
|
|
assert "Wan2.2-I2V-A14B" in SUPPORTED_MODEL_TYPES
|
|
|
|
|
|
def test_supported_model_types_excludes_ti2v_5b():
|
|
"""TI2V-5B is explicitly NOT supported under W4A4 quantization."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import SUPPORTED_MODEL_TYPES
|
|
|
|
assert "Wan2.2-TI2V-5B" not in SUPPORTED_MODEL_TYPES
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _apply_rename_dict
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_apply_rename_dict_self_attn_qkvo():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _apply_rename_dict
|
|
|
|
assert _apply_rename_dict("blocks.0.self_attn.q.weight") == "blocks.0.attn1.to_q.weight"
|
|
assert _apply_rename_dict("blocks.0.self_attn.k.weight") == "blocks.0.attn1.to_k.weight"
|
|
assert _apply_rename_dict("blocks.0.self_attn.v.weight") == "blocks.0.attn1.to_v.weight"
|
|
assert _apply_rename_dict("blocks.0.self_attn.o.weight") == "blocks.0.attn1.to_out.0.weight"
|
|
|
|
|
|
def test_apply_rename_dict_ffn():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _apply_rename_dict
|
|
|
|
assert _apply_rename_dict("blocks.1.ffn.0.weight") == "blocks.1.ffn.net.0.proj.weight"
|
|
assert _apply_rename_dict("blocks.1.ffn.2.weight") == "blocks.1.ffn.net.2.weight"
|
|
|
|
|
|
def test_apply_rename_dict_norm_order_swap():
|
|
"""norm2↔norm3 swap: quant tool uses norm1/norm3/norm2 order,
|
|
Diffusers uses norm1/norm2/norm3."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _apply_rename_dict
|
|
|
|
assert _apply_rename_dict("blocks.0.norm2.weight") == "blocks.0.norm3.weight"
|
|
assert _apply_rename_dict("blocks.0.norm3.weight") == "blocks.0.norm2.weight"
|
|
|
|
|
|
def test_apply_rename_dict_cross_attn():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _apply_rename_dict
|
|
|
|
assert _apply_rename_dict("blocks.0.cross_attn.q.weight") == "blocks.0.attn2.to_q.weight"
|
|
assert _apply_rename_dict("blocks.0.cross_attn.k.weight") == "blocks.0.attn2.to_k.weight"
|
|
assert _apply_rename_dict("blocks.0.cross_attn.v.weight") == "blocks.0.attn2.to_v.weight"
|
|
assert _apply_rename_dict("blocks.0.cross_attn.o.weight") == "blocks.0.attn2.to_out.0.weight"
|
|
|
|
|
|
def test_apply_rename_dict_head_and_modulation():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _apply_rename_dict
|
|
|
|
assert _apply_rename_dict("head.head.weight") == "proj_out.weight"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _strip_mxfp4_wrapper
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_strip_mxfp4_wrapper_linear_weight():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _strip_mxfp4_wrapper
|
|
|
|
assert _strip_mxfp4_wrapper("blocks.0.attn1.to_q.linear.weight") == "blocks.0.attn1.to_q.weight"
|
|
|
|
|
|
def test_strip_mxfp4_wrapper_linear_weight_scale():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _strip_mxfp4_wrapper
|
|
|
|
assert _strip_mxfp4_wrapper("blocks.0.attn1.to_q.linear.weight_scale") == "blocks.0.attn1.to_q.weight_scale"
|
|
|
|
|
|
def test_strip_mxfp4_wrapper_linear_weight_dual_scale():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _strip_mxfp4_wrapper
|
|
|
|
assert (
|
|
_strip_mxfp4_wrapper("blocks.0.attn1.to_q.linear.weight_dual_scale") == "blocks.0.attn1.to_q.weight_dual_scale"
|
|
)
|
|
|
|
|
|
def test_strip_mxfp4_wrapper_linear_bias():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _strip_mxfp4_wrapper
|
|
|
|
assert _strip_mxfp4_wrapper("blocks.0.attn1.to_q.linear.bias") == "blocks.0.attn1.to_q.bias"
|
|
|
|
|
|
def test_strip_mxfp4_wrapper_div_mul_scale():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _strip_mxfp4_wrapper
|
|
|
|
assert _strip_mxfp4_wrapper("blocks.0.attn1.to_q.div.mul_scale") == "blocks.0.attn1.to_q.mul_scale"
|
|
|
|
|
|
def test_strip_mxfp4_wrapper_noop_for_plain_weight():
|
|
"""MXFP8 / FLOAT tensors have no wrapper — must be returned unchanged."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _strip_mxfp4_wrapper
|
|
|
|
assert _strip_mxfp4_wrapper("blocks.0.attn1.to_q.weight") == "blocks.0.attn1.to_q.weight"
|
|
assert _strip_mxfp4_wrapper("blocks.0.norm_q.weight") == "blocks.0.norm_q.weight"
|
|
assert _strip_mxfp4_wrapper("condition_embedder.time_embedder.linear_1.weight") == (
|
|
"condition_embedder.time_embedder.linear_1.weight"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _classify_blocks
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_classify_blocks_mixed_mxfp8_and_mxfp4():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _classify_blocks
|
|
|
|
quant_meta = {
|
|
"blocks.0.attn1.to_q.weight": "W8A8_MXFP8",
|
|
"blocks.1.attn1.to_q.weight": "W8A8_MXFP8",
|
|
"blocks.2.attn1.to_q.weight": "W4A4_MXFP4_DUALSCALE",
|
|
"blocks.3.attn1.to_q.weight": "W4A4_MXFP4_DUALSCALE",
|
|
"condition_embedder.time_embedder.linear_1.weight": "FLOAT",
|
|
}
|
|
block_types = _classify_blocks(quant_meta)
|
|
assert block_types[0] == "mxfp8"
|
|
assert block_types[1] == "mxfp8"
|
|
assert block_types[2] == "mxfp4_dualscale"
|
|
assert block_types[3] == "mxfp4_dualscale"
|
|
# Non-block key must not produce an entry
|
|
assert None not in block_types
|
|
|
|
|
|
def test_classify_blocks_float_entries_skipped():
|
|
"""FLOAT-typed tensors must not contribute to block classification."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _classify_blocks
|
|
|
|
quant_meta = {
|
|
"blocks.0.bias": "FLOAT",
|
|
"blocks.1.attn1.to_q.weight": "W4A4_MXFP4_DUALSCALE",
|
|
}
|
|
block_types = _classify_blocks(quant_meta)
|
|
assert 0 not in block_types
|
|
assert block_types[1] == "mxfp4_dualscale"
|
|
|
|
|
|
def test_classify_blocks_empty():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _classify_blocks
|
|
|
|
assert _classify_blocks({}) == {}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _is_mxfp4_tensor
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_is_mxfp4_tensor_quantized_weight():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _is_mxfp4_tensor
|
|
|
|
assert _is_mxfp4_tensor("blocks.0.attn1.to_q.linear.weight", "W4A4_MXFP4_DUALSCALE") is True
|
|
assert _is_mxfp4_tensor("blocks.0.attn1.to_q.linear.weight_scale", "W4A4_MXFP4_DUALSCALE") is True
|
|
assert _is_mxfp4_tensor("blocks.0.attn1.to_q.linear.weight_dual_scale", "W4A4_MXFP4_DUALSCALE") is True
|
|
|
|
|
|
def test_is_mxfp4_tensor_companion_bias_and_mul_scale():
|
|
"""Companion tensors (bias, mul_scale) are FLOAT but belong to MXFP4 layers
|
|
and must be included in the merged checkpoint."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _is_mxfp4_tensor
|
|
|
|
# bias inside .linear. wrapper → must be included
|
|
assert _is_mxfp4_tensor("blocks.0.attn1.to_q.linear.bias", "FLOAT") is True
|
|
# mul_scale inside .div. wrapper → must be included
|
|
assert _is_mxfp4_tensor("blocks.0.attn1.to_q.div.mul_scale", "FLOAT") is True
|
|
|
|
|
|
def test_is_mxfp4_tensor_bf16_fallback_weight():
|
|
"""BF16 fallback linear layers have plain .weight keys (no wrapper) → must NOT be included."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _is_mxfp4_tensor
|
|
|
|
assert _is_mxfp4_tensor("blocks.0.attn1.to_q.weight", "FLOAT") is False
|
|
assert _is_mxfp4_tensor("condition_embedder.time_embedder.linear_1.weight", "FLOAT") is False
|
|
|
|
|
|
def test_is_mxfp4_tensor_norm_weight():
|
|
"""Norm layers are always FLOAT with plain .weight → not MXFP4 related."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _is_mxfp4_tensor
|
|
|
|
assert _is_mxfp4_tensor("blocks.0.norm1.weight", "FLOAT") is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _collect_ignored_layers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_collect_ignored_layers_basic():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _collect_ignored_layers
|
|
|
|
# block 0: all three of Q/K/V are BF16 → fused as to_qkv in ignored_layers
|
|
# block 1: to_qkv is MXFP4 → not in ignored_layers
|
|
merged = {
|
|
"blocks.0.attn1.to_q.weight": torch.zeros(4, 4),
|
|
"blocks.0.attn1.to_k.weight": torch.zeros(4, 4),
|
|
"blocks.0.attn1.to_v.weight": torch.zeros(4, 4),
|
|
"blocks.1.attn1.to_q.weight": torch.zeros(4, 4),
|
|
"blocks.1.attn1.to_q.weight_scale": torch.zeros(4, 1),
|
|
}
|
|
mxfp4_prefixes = {"blocks.1.attn1.to_q"}
|
|
|
|
ignored = _collect_ignored_layers(merged, mxfp4_prefixes)
|
|
assert "blocks.0.attn1.to_qkv" in ignored # fused vllm-omni name
|
|
assert "blocks.0.attn1.to_q" not in ignored # diffusers name must not appear
|
|
assert "blocks.0.attn1.to_k" not in ignored
|
|
assert "blocks.1.attn1.to_qkv" not in ignored # MXFP4, not ignored
|
|
|
|
|
|
def test_collect_ignored_layers_empty_mxfp4():
|
|
"""When no layers are MXFP4 (all BF16), all .weight prefixes become ignored_layers."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _collect_ignored_layers
|
|
|
|
# Use non-attn layers to avoid the Q/K/V completeness requirement.
|
|
merged = {
|
|
"blocks.0.ffn.net.0.proj.weight": torch.zeros(4, 4),
|
|
"proj_out.weight": torch.zeros(4, 4),
|
|
}
|
|
ignored = _collect_ignored_layers(merged, set())
|
|
assert sorted(ignored) == ["blocks.0.ffn.net_0.proj", "proj_out"]
|
|
|
|
|
|
def test_collect_ignored_layers_returns_sorted():
|
|
"""ignored_layers must be sorted for deterministic config.json output."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _collect_ignored_layers
|
|
|
|
# Use FFN layers (no Q/K/V fusion concern) to isolate the sort guarantee.
|
|
merged = {
|
|
"blocks.2.ffn.net.0.proj.weight": torch.zeros(1),
|
|
"blocks.0.ffn.net.0.proj.weight": torch.zeros(1),
|
|
"blocks.1.ffn.net.0.proj.weight": torch.zeros(1),
|
|
}
|
|
ignored = _collect_ignored_layers(merged, set())
|
|
assert ignored == sorted(ignored)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _build_quant_config (new mxfp4_dualscale format)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_build_quant_config_new_format():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _build_quant_config
|
|
|
|
config = _build_quant_config(["blocks.0.attn1.to_q", "proj_out"])
|
|
assert config["quant_method"] == "mxfp4_dualscale"
|
|
assert config["is_checkpoint_serialized"] is True
|
|
assert config["ignored_layers"] == ["blocks.0.attn1.to_q", "proj_out"]
|
|
assert "num_mxfp8_blocks" not in config
|
|
|
|
|
|
def test_build_quant_config_empty_ignored_layers():
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _build_quant_config
|
|
|
|
config = _build_quant_config([])
|
|
assert config["ignored_layers"] == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _diffusers_to_vllm_ignored
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_remap_self_attn_qkv_fusion():
|
|
"""All three of attn1.to_q/k/v in BF16 → fused attn1.to_qkv."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _diffusers_to_vllm_ignored
|
|
|
|
result = _diffusers_to_vllm_ignored(["blocks.0.attn1.to_k", "blocks.0.attn1.to_q", "blocks.0.attn1.to_v"])
|
|
assert result == ["blocks.0.attn1.to_qkv"]
|
|
|
|
|
|
def test_remap_self_attn_qkv_partial_raises():
|
|
"""Partial Q/K/V in ignored_layers is invalid — must raise ValueError.
|
|
|
|
Self-attention Q/K/V are fused into a single to_qkv layer at runtime;
|
|
partial precision (some BF16, some MXFP4) cannot be expressed and must
|
|
be caught early in the merge script.
|
|
"""
|
|
import pytest
|
|
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _diffusers_to_vllm_ignored
|
|
|
|
with pytest.raises(ValueError, match="Partial BF16 fallback"):
|
|
_diffusers_to_vllm_ignored(["blocks.0.attn1.to_q", "blocks.0.attn1.to_k"])
|
|
|
|
|
|
def test_remap_cross_attn_kept_separate():
|
|
"""Cross-attention (attn2) to_q/k/v must NOT be fused."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _diffusers_to_vllm_ignored
|
|
|
|
result = _diffusers_to_vllm_ignored(["blocks.0.attn2.to_k", "blocks.0.attn2.to_q", "blocks.0.attn2.to_v"])
|
|
assert "blocks.0.attn2.to_qkv" not in result
|
|
assert "blocks.0.attn2.to_q" in result
|
|
assert "blocks.0.attn2.to_k" in result
|
|
assert "blocks.0.attn2.to_v" in result
|
|
|
|
|
|
def test_remap_ffn_dot_to_underscore():
|
|
"""ffn.net.0.proj → ffn.net_0.proj; ffn.net.2 → ffn.net_2."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _diffusers_to_vllm_ignored
|
|
|
|
result = _diffusers_to_vllm_ignored(["blocks.0.ffn.net.0.proj", "blocks.0.ffn.net.2"])
|
|
assert "blocks.0.ffn.net_0.proj" in result
|
|
assert "blocks.0.ffn.net_2" in result
|
|
assert "blocks.0.ffn.net.0.proj" not in result
|
|
assert "blocks.0.ffn.net.2" not in result
|
|
|
|
|
|
def test_remap_to_out_strips_index():
|
|
"""attn1.to_out.0 and attn2.to_out.0 must have the .0 suffix removed."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _diffusers_to_vllm_ignored
|
|
|
|
result = _diffusers_to_vllm_ignored(["blocks.0.attn1.to_out.0", "blocks.0.attn2.to_out.0"])
|
|
assert "blocks.0.attn1.to_out" in result
|
|
assert "blocks.0.attn2.to_out" in result
|
|
assert "blocks.0.attn1.to_out.0" not in result
|
|
assert "blocks.0.attn2.to_out.0" not in result
|
|
|
|
|
|
def test_remap_noop_for_non_block_layers():
|
|
"""Layers outside 'blocks.N.*' (condition_embedder, proj_out) pass through unchanged."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _diffusers_to_vllm_ignored
|
|
|
|
inputs = [
|
|
"condition_embedder.time_embedder.linear_1",
|
|
"condition_embedder.text_embedder.linear_1",
|
|
"proj_out",
|
|
]
|
|
result = _diffusers_to_vllm_ignored(inputs)
|
|
assert result == sorted(inputs)
|
|
|
|
|
|
def test_remap_mixed_block_layers():
|
|
"""Typical mixed block: QKV fused, to_out stripped, FFN renamed, cross-attn separate."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _diffusers_to_vllm_ignored
|
|
|
|
inputs = [
|
|
"blocks.0.attn1.to_k",
|
|
"blocks.0.attn1.to_out.0",
|
|
"blocks.0.attn1.to_q",
|
|
"blocks.0.attn1.to_v",
|
|
"blocks.0.attn2.to_k",
|
|
"blocks.0.attn2.to_out.0",
|
|
"blocks.0.attn2.to_q",
|
|
"blocks.0.attn2.to_v",
|
|
"blocks.0.ffn.net.0.proj",
|
|
"blocks.0.ffn.net.2",
|
|
]
|
|
result = _diffusers_to_vllm_ignored(inputs)
|
|
assert "blocks.0.attn1.to_qkv" in result
|
|
assert "blocks.0.attn1.to_out" in result
|
|
assert "blocks.0.attn2.to_q" in result
|
|
assert "blocks.0.attn2.to_k" in result
|
|
assert "blocks.0.attn2.to_v" in result
|
|
assert "blocks.0.attn2.to_out" in result
|
|
assert "blocks.0.ffn.net_0.proj" in result
|
|
assert "blocks.0.ffn.net_2" in result
|
|
# Old names must be gone
|
|
assert "blocks.0.attn1.to_q" not in result
|
|
assert "blocks.0.ffn.net.0.proj" not in result
|
|
|
|
|
|
def test_remap_returns_sorted():
|
|
"""Output of _diffusers_to_vllm_ignored must always be sorted."""
|
|
from vllm_omni.quantization.tools.merge_mxfp4_dualscale_checkpoint import _diffusers_to_vllm_ignored
|
|
|
|
inputs = [
|
|
"blocks.2.ffn.net.0.proj",
|
|
"blocks.0.attn1.to_q",
|
|
"blocks.0.attn1.to_k",
|
|
"blocks.0.attn1.to_v",
|
|
"blocks.1.attn2.to_out.0",
|
|
]
|
|
result = _diffusers_to_vllm_ignored(inputs)
|
|
assert result == sorted(result)
|