[AutoRound] Support GLM-Image W4A16 quantization model (#3059)

Signed-off-by: lvliang-intel <liang1.lv@intel.com>
This commit is contained in:
Liang Lv authored and GitHub committed 2026-05-28 07:19:06 +00:00
1 parent 168033cac0
commit b72ffb82bb
8 files changed
+1143 -21

No files matched your search

+1
View File
@@ -570,6 +570,7 @@ steps:
tests/e2e/online_serving/test_longcat_image_expansion.py
tests/e2e/online_serving/test_longcat_image_edit_expansion.py
tests/e2e/online_serving/test_flux_2_dev_expansion.py
tests/e2e/offline_inference/test_glm_image_autoround_w4a16_expansion.py
-m "full_model and diffusion and H100 and distributed_cuda"
--run-level "full_model"
agents:
+1 -1
View File
@@ -52,8 +52,8 @@ vLLM-Omni's runtime module names.
| Model | Scope | Status | Notes |
|-------|-------|--------|-------|
| GLM-Image | Diffusion transformer | ✅ | `Intel/GLM-Image-int4-AutoRound` |
| BAGEL | Checkpoint-defined diffusion or transformer stage | Not validated | Requires a compatible AutoRound checkpoint |
| GLM-Image | Checkpoint-defined diffusion or transformer stage | Not validated | Requires a compatible AutoRound checkpoint |
## Configuration
@@ -0,0 +1,741 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for GLM-Image quantization support (W4A16/AutoRound).
These tests verify that the GLM-Image DiT transformer correctly accepts and uses
quantization configs for W4A16/AutoRound quantization support.
"""
import os
import pytest
import torch
from pytest_mock import MockerFixture
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.diffusion.models.glm_image.glm_image_transformer import (
ColumnParallelGELU,
ColumnParallelSiLU,
GlmImageAdaLayerNormContinuous,
GlmImageAdaLayerNormZero,
GlmImageAttention,
GlmImageFeedForward,
GlmImageImageProjector,
GlmImagePrepare,
GlmImageRotaryPosEmbed,
GlmImageTransformer2DModel,
GlmImageTransformerBlock,
_positive_divisors,
validate_glm_image_tp_constraints,
)
from vllm_omni.model_executor.models.glm_image.pipeline import GLM_IMAGE_PIPELINE
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@pytest.fixture(scope="function", autouse=True)
def _init_distributed():
"""Initialize the minimal distributed environment required by
ReplicatedLinear (tensor-parallel group must exist)."""
from vllm.distributed.parallel_state import (
cleanup_dist_env_and_memory,
init_distributed_environment,
initialize_model_parallel,
)
os.environ.setdefault("MASTER_ADDR", "localhost")
os.environ.setdefault("MASTER_PORT", "29502")
init_distributed_environment(
world_size=1,
rank=0,
local_rank=0,
distributed_init_method="env://",
)
initialize_model_parallel()
yield
cleanup_dist_env_and_memory()
@pytest.fixture(scope="function", autouse=True)
def _force_default_gemm(monkeypatch):
"""Force CPU-compatible GEMM dispatch for tests using CPU tensors.
vLLM's dispatch_unquantized_gemm() selects the backend by platform
(e.g. rocm_unquantized_gemm on AMD machines), not by tensor device.
CPU test tensors crash with NotImplementedError on ROCm. Monkeypatch
the dispatcher to always return the default (torch.nn.functional.linear)
implementation which works on any device."""
from vllm.model_executor.layers.utils import default_unquantized_gemm
monkeypatch.setattr(
"vllm.model_executor.layers.linear.dispatch_unquantized_gemm",
lambda: default_unquantized_gemm,
)
@pytest.fixture(scope="function", autouse=True)
def setup_mocks(mocker: MockerFixture):
"""Set up common mocks for all tests."""
mocker.patch(
"vllm.model_executor.layers.linear.get_tensor_model_parallel_world_size",
return_value=1,
)
mock_get_tp_group = mocker.patch("vllm.distributed.parallel_state.get_tp_group")
mock_tp_group = mocker.MagicMock()
mock_tp_group.world_size = 1
mock_get_tp_group.return_value = mock_tp_group
yield
class TestPositiveDivisors:
"""Test _positive_divisors helper function."""
def test_divisors_of_1(self):
assert _positive_divisors(1) == {1}
def test_divisors_of_12(self):
assert _positive_divisors(12) == {1, 2, 3, 4, 6, 12}
def test_divisors_of_prime(self):
assert _positive_divisors(7) == {1, 7}
def test_divisors_of_0_returns_empty(self):
assert _positive_divisors(0) == set()
def test_divisors_of_negative_returns_empty(self):
assert _positive_divisors(-5) == set()
class TestValidateGlmImageTpConstraints:
"""Test TP constraint validation for GLM-Image."""
def test_valid_tp_size_1(self):
"""TP=1 should always be valid."""
result = validate_glm_image_tp_constraints(
dim=2560,
num_heads=64,
ffn_hidden_dim=10240,
tensor_parallel_size=1,
)
assert 1 in result
def test_valid_tp_size_2_for_divisible_dim(self):
"""TP=2 is valid when all dims are divisible by 2."""
result = validate_glm_image_tp_constraints(
dim=2560,
num_heads=64,
ffn_hidden_dim=10240,
tensor_parallel_size=2,
)
assert 1 in result
assert 2 in result
def test_valid_tp_size_4_for_divisible_dim(self):
"""TP=4 is valid when all dims are divisible by 4."""
result = validate_glm_image_tp_constraints(
dim=2560,
num_heads=64,
ffn_hidden_dim=10240,
tensor_parallel_size=4,
)
assert 1 in result
assert 2 in result
assert 4 in result
def test_invalid_tp_size_3_for_divisible_dim(self):
"""TP=3 is invalid when dim is not divisible by 3."""
with pytest.raises(ValueError, match="dim % tensor_parallel_size == 0"):
validate_glm_image_tp_constraints(
dim=2560, # 2560 % 3 != 0
num_heads=64,
ffn_hidden_dim=10240,
tensor_parallel_size=3,
)
def test_invalid_tp_size_zero(self):
"""TP=0 should raise error."""
with pytest.raises(ValueError, match="tensor_parallel_size must be > 0"):
validate_glm_image_tp_constraints(
dim=2560,
num_heads=64,
ffn_hidden_dim=10240,
tensor_parallel_size=0,
)
def test_invalid_tp_size_negative(self):
"""Negative TP size should raise error."""
with pytest.raises(ValueError, match="tensor_parallel_size must be > 0"):
validate_glm_image_tp_constraints(
dim=2560,
num_heads=64,
ffn_hidden_dim=10240,
tensor_parallel_size=-1,
)
class TestGlmImageAdaLayerNormZeroQuantization:
"""Test GlmImageAdaLayerNormZero with quantization config."""
def test_accepts_quant_config_parameter(self, mocker: MockerFixture):
"""Verify the class accepts quant_config parameter."""
mock_quant_config = mocker.MagicMock()
layer = GlmImageAdaLayerNormZero(
embedding_dim=512,
dim=2560,
quant_config=mock_quant_config,
prefix="test.norm1",
)
assert layer.linear.quant_config is mock_quant_config
def test_accepts_none_quant_config(self):
"""Verify quant_config=None is accepted."""
layer = GlmImageAdaLayerNormZero(
embedding_dim=512,
dim=2560,
quant_config=None,
)
assert layer.linear.quant_config is None
def test_forward_handles_tuple_return_from_linear(self):
"""Verify forward handles tuple returns from ReplicatedLinear."""
layer = GlmImageAdaLayerNormZero(
embedding_dim=512,
dim=2560,
quant_config=None,
)
batch_size = 2
seq_len = 10
hidden_states = torch.randn(batch_size, seq_len, 2560)
encoder_hidden_states = torch.randn(batch_size, seq_len, 2560)
temb = torch.randn(batch_size, 512)
# This should work regardless of whether linear returns tuple or tensor
result = layer(hidden_states, encoder_hidden_states, temb)
assert len(result) == 10 # Should return 10 chunks
class TestGlmImageAdaLayerNormContinuousQuantization:
"""Test GlmImageAdaLayerNormContinuous with quantization config."""
def test_accepts_quant_config_parameter(self, mocker: MockerFixture):
"""Verify the class accepts quant_config parameter."""
mock_quant_config = mocker.MagicMock()
layer = GlmImageAdaLayerNormContinuous(
embedding_dim=2560,
conditioning_embedding_dim=512,
quant_config=mock_quant_config,
prefix="test.norm_out",
)
assert layer.linear.quant_config is mock_quant_config
def test_accepts_none_quant_config(self):
"""Verify quant_config=None is accepted."""
layer = GlmImageAdaLayerNormContinuous(
embedding_dim=2560,
conditioning_embedding_dim=512,
quant_config=None,
)
assert layer.linear.quant_config is None
def test_forward_handles_tuple_return_from_linear(self):
"""Verify forward handles tuple returns from ReplicatedLinear."""
layer = GlmImageAdaLayerNormContinuous(
embedding_dim=2560,
conditioning_embedding_dim=512,
quant_config=None,
)
batch_size = 2
seq_len = 10
x = torch.randn(batch_size, seq_len, 2560)
conditioning_embedding = torch.randn(batch_size, 512)
result = layer(x, conditioning_embedding)
assert result.shape == (batch_size, seq_len, 2560)
class TestGlmImageAttentionQuantization:
"""Test GlmImageAttention with quantization config."""
def test_accepts_quant_config_parameter(self, mocker: MockerFixture):
"""Verify GlmImageAttention accepts quant_config parameter."""
mock_quant_config = mocker.MagicMock()
attn = GlmImageAttention(
dim=2560,
num_heads=64,
head_dim=40,
quant_config=mock_quant_config,
prefix="test.attn1",
)
assert attn.to_qkv.quant_config is mock_quant_config
def test_accepts_none_quant_config(self):
"""Verify quant_config=None is accepted."""
attn = GlmImageAttention(
dim=2560,
num_heads=64,
head_dim=40,
quant_config=None,
)
assert attn.to_qkv.quant_config is None
class TestColumnParallelModulesQuantization:
"""Test ColumnParallelGELU and ColumnParallelSiLU with quantization config."""
def test_column_parallel_gelu_accepts_quant_config(self, mocker: MockerFixture):
"""Verify ColumnParallelGELU accepts quant_config."""
mock_quant_config = mocker.MagicMock()
layer = ColumnParallelGELU(
dim_in=2560,
dim_out=10240,
quant_config=mock_quant_config,
prefix="test.gelu",
)
assert layer.proj.quant_config is mock_quant_config
def test_column_parallel_silu_accepts_quant_config(self, mocker: MockerFixture):
"""Verify ColumnParallelSiLU accepts quant_config."""
mock_quant_config = mocker.MagicMock()
layer = ColumnParallelSiLU(
dim_in=2560,
dim_out=10240,
quant_config=mock_quant_config,
prefix="test.silu",
)
assert layer.proj.quant_config is mock_quant_config
class TestGlmImageFeedForwardQuantization:
"""Test GlmImageFeedForward with quantization config."""
def test_accepts_quant_config_parameter(self, mocker: MockerFixture):
"""Verify GlmImageFeedForward accepts quant_config parameter."""
mock_quant_config = mocker.MagicMock()
ff = GlmImageFeedForward(
dim=2560,
dim_out=2560,
inner_dim=10240,
activation_fn="gelu-approximate",
quant_config=mock_quant_config,
prefix="test.ff",
)
# Check that the first layer (ColumnParallelGELU) has quant_config
gelu_layer = ff.net[0]
assert gelu_layer.proj.quant_config is mock_quant_config
def test_accepts_none_quant_config(self):
"""Verify quant_config=None is accepted."""
ff = GlmImageFeedForward(
dim=2560,
dim_out=2560,
inner_dim=10240,
activation_fn="gelu-approximate",
quant_config=None,
)
gelu_layer = ff.net[0]
assert gelu_layer.proj.quant_config is None
def test_linear_silu_activation(self):
"""Test linear-silu activation function initialization works."""
# This test verifies the module can be instantiated with linear-silu activation.
# Full forward testing requires proper TP group setup, so we just verify construction.
ff = GlmImageFeedForward(
dim=2560,
dim_out=2560,
inner_dim=10240,
activation_fn="linear-silu",
quant_config=None,
)
# Verify the FFN has the correct structure
assert len(ff.net) == 3 # ColumnParallelSiLU, Identity, RowParallelLinear
assert isinstance(ff.net[0], ColumnParallelSiLU)
class TestGlmImageTransformerBlockQuantization:
"""Test GlmImageTransformerBlock with quantization config."""
def test_accepts_quant_config_parameter(self, mocker: MockerFixture):
"""Verify GlmImageTransformerBlock accepts quant_config parameter."""
mock_quant_config = mocker.MagicMock()
parallel_config = DiffusionParallelConfig(
tensor_parallel_size=1,
sequence_parallel_size=1,
)
block = GlmImageTransformerBlock(
dim=2560,
num_attention_heads=64,
attention_head_dim=40,
time_embed_dim=512,
parallel_config=parallel_config,
quant_config=mock_quant_config,
prefix="test.block",
)
# Check that inner modules have quant_config
assert block.norm1.linear.quant_config is mock_quant_config
assert block.attn1.to_qkv.quant_config is mock_quant_config
assert block.ff.net[0].proj.quant_config is mock_quant_config
def test_accepts_none_quant_config(self):
"""Verify quant_config=None is accepted."""
block = GlmImageTransformerBlock(
dim=2560,
num_attention_heads=64,
attention_head_dim=40,
time_embed_dim=512,
quant_config=None,
)
assert block.norm1.linear.quant_config is None
class TestGlmImagePrepareModule:
"""Test GlmImagePrepare module."""
def test_prepare_module_exists(self):
"""Verify GlmImagePrepare module exists and works."""
projector = GlmImageImageProjector(in_channels=16, hidden_size=2560, patch_size=2)
rope = GlmImageRotaryPosEmbed(dim=40, patch_size=2)
prepare = GlmImagePrepare(
image_projector=projector,
rope=rope,
patch_size=2,
)
# Test forward pass
hidden_states = torch.randn(1, 16, 64, 64) # [B, C, H, W]
result = prepare(hidden_states)
assert len(result) == 5
hidden_out, rope_cos, rope_sin, height, width = result
assert hidden_out.shape[0] == 1 # batch size
assert hidden_out.shape[1] == 1024 # seq_len (32 * 32)
assert hidden_out.shape[2] == 2560 # hidden_dim
class TestGlmImagePipelineConfig:
"""Test GLM_IMAGE_PIPELINE configuration."""
def test_glm_image_pipeline_config_exists(self):
"""Verify GLM_IMAGE_PIPELINE config exists."""
assert GLM_IMAGE_PIPELINE is not None
def test_pipeline_has_correct_model_type(self):
"""Verify pipeline has correct model_type."""
assert GLM_IMAGE_PIPELINE.model_type == "glm_image"
def test_pipeline_has_correct_model_arch(self):
"""Verify pipeline has correct model_arch."""
assert GLM_IMAGE_PIPELINE.model_arch == "GlmImageForConditionalGeneration"
def test_pipeline_has_two_stages(self):
"""Verify pipeline has two stages (AR + DiT)."""
assert len(GLM_IMAGE_PIPELINE.stages) == 2
def test_stage_0_is_ar_llm(self):
"""Verify stage 0 is the AR LLM stage."""
stage_0 = GLM_IMAGE_PIPELINE.stages[0]
assert stage_0.stage_id == 0
assert stage_0.model_stage == "ar"
assert stage_0.execution_type.value == "llm_ar"
assert stage_0.owns_tokenizer is True
def test_stage_1_is_diffusion(self):
"""Verify stage 1 is the DiT diffusion stage."""
stage_1 = GLM_IMAGE_PIPELINE.stages[1]
assert stage_1.stage_id == 1
assert stage_1.model_stage == "dit"
assert stage_1.execution_type.value == "diffusion"
assert stage_1.final_output is True
assert stage_1.final_output_type == "image"
assert stage_1.input_sources == (0,) # Takes input from stage 0
def test_pipeline_has_diffusers_class_name(self):
"""Verify pipeline has diffusers_class_name."""
assert GLM_IMAGE_PIPELINE.diffusers_class_name == "GlmImagePipeline"
class TestGlmImageImageProjector:
"""Test GlmImageImageProjector module."""
def test_projector_output_shape(self):
"""Verify projector produces correct output shape."""
projector = GlmImageImageProjector(
in_channels=16,
hidden_size=2560,
patch_size=2,
)
# Input: [B, C, H, W] = [1, 16, 64, 64]
hidden_states = torch.randn(1, 16, 64, 64)
output = projector(hidden_states)
# After patchify: [B, H/2*W/2, D] = [1, 32*32, 2560] = [1, 1024, 2560]
assert output.shape == (1, 1024, 2560)
def test_projector_with_different_patch_sizes(self):
"""Verify projector works with different patch sizes."""
for patch_size in [2, 4]:
projector = GlmImageImageProjector(
in_channels=16,
hidden_size=2560,
patch_size=patch_size,
)
h, w = 64, 64
hidden_states = torch.randn(1, 16, h, w)
output = projector(hidden_states)
expected_seq_len = (h // patch_size) * (w // patch_size)
assert output.shape[1] == expected_seq_len
class TestGlmImageRotaryPosEmbed:
"""Test GlmImageRotaryPosEmbed module."""
def test_rope_output_shape(self):
"""Verify RoPE produces correct output shape."""
rope = GlmImageRotaryPosEmbed(dim=40, patch_size=2, theta=10000.0)
hidden_states = torch.randn(1, 16, 64, 64) # [B, C, H, W]
cos, sin = rope(hidden_states)
# After patchify: height=32, width=32
# Output: [32*32, dim] = [1024, 40]
assert cos.shape[0] == 1024
assert cos.shape[1] == 40
assert sin.shape == cos.shape
def test_rope_value_range(self):
"""Verify RoPE produces values in valid range."""
rope = GlmImageRotaryPosEmbed(dim=40, patch_size=2, theta=10000.0)
hidden_states = torch.randn(2, 16, 32, 32)
cos, sin = rope(hidden_states)
# cos and sin should be in [-1, 1]
assert cos.min() >= -1.0 and cos.max() <= 1.0
assert sin.min() >= -1.0 and sin.max() <= 1.0
def test_rope_consistency(self):
"""Verify RoPE is consistent for same input."""
rope = GlmImageRotaryPosEmbed(dim=40, patch_size=2, theta=10000.0)
hidden_states = torch.randn(1, 16, 32, 32)
cos1, sin1 = rope(hidden_states)
cos2, sin2 = rope(hidden_states)
# Same input should produce same output
assert torch.allclose(cos1, cos2)
assert torch.allclose(sin1, sin2)
class TestGlmImageTransformer2DModelQuantization:
"""Test GlmImageTransformer2DModel with quantization config."""
def test_accepts_quant_config_parameter(self, mocker: MockerFixture):
"""Verify the model accepts quant_config parameter."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
mock_quant_config = mocker.MagicMock()
parallel_config = DiffusionParallelConfig(
tensor_parallel_size=1,
sequence_parallel_size=1,
)
# Create a minimal mock od_config
mock_tf_config = mocker.MagicMock()
mock_tf_config.patch_size = 2
mock_tf_config.in_channels = 16
mock_tf_config.out_channels = 16
mock_tf_config.num_attention_heads = 64
mock_tf_config.attention_head_dim = 40
mock_tf_config.time_embed_dim = 512
mock_tf_config.condition_dim = 256
mock_tf_config.prior_vq_quantizer_codebook_size = 16384
mock_tf_config.text_embed_dim = 1024
mock_tf_config.num_layers = 2 # Small number for testing
mock_od_config = mocker.MagicMock(spec=OmniDiffusionConfig)
mock_od_config.tf_model_config = mock_tf_config
mock_od_config.parallel_config = parallel_config
model = GlmImageTransformer2DModel(
od_config=mock_od_config,
quant_config=mock_quant_config,
)
# Check that quantization config was passed to transformer blocks
for block in model.transformer_blocks:
assert block.norm1.linear.quant_config is mock_quant_config
assert block.attn1.to_qkv.quant_config is mock_quant_config
assert block.ff.net[0].proj.quant_config is mock_quant_config
def test_accepts_none_quant_config(self, mocker: MockerFixture):
"""Verify quant_config=None is accepted."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
parallel_config = DiffusionParallelConfig(
tensor_parallel_size=1,
sequence_parallel_size=1,
)
mock_tf_config = mocker.MagicMock()
mock_tf_config.patch_size = 2
mock_tf_config.in_channels = 16
mock_tf_config.out_channels = 16
mock_tf_config.num_attention_heads = 64
mock_tf_config.attention_head_dim = 40
mock_tf_config.time_embed_dim = 512
mock_tf_config.condition_dim = 256
mock_tf_config.prior_vq_quantizer_codebook_size = 16384
mock_tf_config.text_embed_dim = 1024
mock_tf_config.num_layers = 2
mock_od_config = mocker.MagicMock(spec=OmniDiffusionConfig)
mock_od_config.tf_model_config = mock_tf_config
mock_od_config.parallel_config = parallel_config
model = GlmImageTransformer2DModel(
od_config=mock_od_config,
quant_config=None,
)
# Check that quantization config is None
for block in model.transformer_blocks:
assert block.norm1.linear.quant_config is None
assert block.attn1.to_qkv.quant_config is None
def test_norm_out_has_no_quantization(self, mocker: MockerFixture):
"""Verify norm_out (output layer) does NOT use quantization to preserve precision."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
mock_quant_config = mocker.MagicMock()
parallel_config = DiffusionParallelConfig(
tensor_parallel_size=1,
sequence_parallel_size=1,
)
mock_tf_config = mocker.MagicMock()
mock_tf_config.patch_size = 2
mock_tf_config.in_channels = 16
mock_tf_config.out_channels = 16
mock_tf_config.num_attention_heads = 64
mock_tf_config.attention_head_dim = 40
mock_tf_config.time_embed_dim = 512
mock_tf_config.condition_dim = 256
mock_tf_config.prior_vq_quantizer_codebook_size = 16384
mock_tf_config.text_embed_dim = 1024
mock_tf_config.num_layers = 2
mock_od_config = mocker.MagicMock(spec=OmniDiffusionConfig)
mock_od_config.tf_model_config = mock_tf_config
mock_od_config.parallel_config = parallel_config
model = GlmImageTransformer2DModel(
od_config=mock_od_config,
quant_config=mock_quant_config,
)
# norm_out.linear should NOT have quant_config to preserve output precision
assert model.norm_out.linear.quant_config is None
def test_model_has_sp_plan(self, mocker: MockerFixture):
"""Verify model has _sp_plan defined for sequence parallelism."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
parallel_config = DiffusionParallelConfig(
tensor_parallel_size=1,
sequence_parallel_size=1,
)
mock_tf_config = mocker.MagicMock()
mock_tf_config.patch_size = 2
mock_tf_config.in_channels = 16
mock_tf_config.out_channels = 16
mock_tf_config.num_attention_heads = 64
mock_tf_config.attention_head_dim = 40
mock_tf_config.time_embed_dim = 512
mock_tf_config.condition_dim = 256
mock_tf_config.prior_vq_quantizer_codebook_size = 16384
mock_tf_config.text_embed_dim = 1024
mock_tf_config.num_layers = 2
mock_od_config = mocker.MagicMock(spec=OmniDiffusionConfig)
mock_od_config.tf_model_config = mock_tf_config
mock_od_config.parallel_config = parallel_config
model = GlmImageTransformer2DModel(
od_config=mock_od_config,
quant_config=None,
)
# Verify _sp_plan exists
assert hasattr(model, "_sp_plan")
assert "prepare" in model._sp_plan
assert "proj_out" in model._sp_plan
def test_model_has_hsdp_shard_conditions(self, mocker: MockerFixture):
"""Verify model has _hsdp_shard_conditions for HSDP parallelism."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
parallel_config = DiffusionParallelConfig(
tensor_parallel_size=1,
sequence_parallel_size=1,
)
mock_tf_config = mocker.MagicMock()
mock_tf_config.patch_size = 2
mock_tf_config.in_channels = 16
mock_tf_config.out_channels = 16
mock_tf_config.num_attention_heads = 64
mock_tf_config.attention_head_dim = 40
mock_tf_config.time_embed_dim = 512
mock_tf_config.condition_dim = 256
mock_tf_config.prior_vq_quantizer_codebook_size = 16384
mock_tf_config.text_embed_dim = 1024
mock_tf_config.num_layers = 2
mock_od_config = mocker.MagicMock(spec=OmniDiffusionConfig)
mock_od_config.tf_model_config = mock_tf_config
mock_od_config.parallel_config = parallel_config
model = GlmImageTransformer2DModel(
od_config=mock_od_config,
quant_config=None,
)
assert hasattr(model, "_hsdp_shard_conditions")
assert len(model._hsdp_shard_conditions) > 0
def test_model_creates_kv_cache(self, mocker: MockerFixture):
"""Verify model can create KV cache for image editing."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
parallel_config = DiffusionParallelConfig(
tensor_parallel_size=1,
sequence_parallel_size=1,
)
mock_tf_config = mocker.MagicMock()
mock_tf_config.patch_size = 2
mock_tf_config.in_channels = 16
mock_tf_config.out_channels = 16
mock_tf_config.num_attention_heads = 64
mock_tf_config.attention_head_dim = 40
mock_tf_config.time_embed_dim = 512
mock_tf_config.condition_dim = 256
mock_tf_config.prior_vq_quantizer_codebook_size = 16384
mock_tf_config.text_embed_dim = 1024
mock_tf_config.num_layers = 2
mock_od_config = mocker.MagicMock(spec=OmniDiffusionConfig)
mock_od_config.tf_model_config = mock_tf_config
mock_od_config.parallel_config = parallel_config
model = GlmImageTransformer2DModel(
od_config=mock_od_config,
quant_config=None,
)
kv_cache = model.create_kv_cache()
assert kv_cache is not None
assert len(kv_cache) == 2 # num_layers
@@ -0,0 +1,238 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""E2E tests for GLM-Image AutoRound W4A16 quantized inference.
These tests cover text-to-image and image-to-image generation with
the W4A16 quantized GLM-Image model.
Requirements:
- 2 CUDA GPUs (H100 or equivalent)
- The quantized model checkpoint (Intel/GLM-Image-int4-AutoRound)
"""
import gc
import math
import os
import numpy as np
import pytest
from PIL import Image
from vllm import SamplingParams
from tests.helpers.env import DeviceMemoryMonitor
from tests.helpers.mark import hardware_test
from tests.helpers.media import generate_synthetic_image
from tests.helpers.runtime import OmniRunnerHandler
from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.outputs import OmniRequestOutput
from vllm_omni.platforms import current_omni_platform
QUANTIZED_MODEL = os.environ.get("GLM_IMAGE_AUTOROUND_MODEL", "Intel/GLM-Image-int4-AutoRound")
# Small resolution to keep GPU memory & time manageable
HEIGHT = 256
WIDTH = 256
NUM_STEPS = 2 # minimal for smoke-test
# GLM-Image AR generation config (from generation_config.json)
GLM_IMAGE_EOS_TOKEN_ID = 16385
GLM_IMAGE_VISION_VOCAB_SIZE = 16512
_CI_DEPLOY = get_deploy_config_path("glm_image.yaml")
def _get_stage_config():
"""Build a CI-friendly stage config with eager mode for testing."""
return modify_stage_config(
_CI_DEPLOY,
updates={
"stages": {
0: {"enforce_eager": True},
1: {"enforce_eager": True},
},
},
)
stage_config = _get_stage_config()
# (model, stage_config_path) for ``omni_runner`` indirect parametrize
_OMNI_RUNNER_PARAM = (QUANTIZED_MODEL, stage_config)
def compute_max_tokens(height: int, width: int, factor: int = 32) -> int:
"""Compute max_new_tokens for GLM-Image AR text-to-image generation."""
token_h = height // factor
token_w = width // factor
large_tokens = token_h * token_w
ratio = token_h / token_w if token_w > 0 else 1.0
small_token_h = max(1, int(math.sqrt(ratio) * (factor // 2)))
small_token_w = max(1, int(math.sqrt(1 / ratio) * (factor // 2)))
small_tokens = small_token_h * small_token_w
return small_tokens + large_tokens + 1
def _ar_sampling_params(max_tokens: int, height: int, width: int, seed: int = 42) -> SamplingParams:
"""Build AR stage SamplingParams for GLM-Image."""
return SamplingParams(
temperature=0.9,
top_p=0.75,
top_k=GLM_IMAGE_VISION_VOCAB_SIZE,
max_tokens=max_tokens,
stop_token_ids=[GLM_IMAGE_EOS_TOKEN_ID],
seed=seed,
detokenize=False,
extra_args={
"target_h": height,
"target_w": width,
},
)
def _diffusion_sampling_params(
height: int = HEIGHT,
width: int = WIDTH,
num_steps: int = NUM_STEPS,
seed: int = 42,
) -> OmniDiffusionSamplingParams:
"""Build Diffusion stage OmniDiffusionSamplingParams."""
return OmniDiffusionSamplingParams(
height=height,
width=width,
num_inference_steps=num_steps,
guidance_scale=0.0,
seed=seed,
)
pytestmark = [
pytest.mark.full_model,
pytest.mark.diffusion,
]
# ------------------------------------------------------------------
# Test: text-to-image generation produces a valid image (quantized)
# ------------------------------------------------------------------
@pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True)
@hardware_test(res={"cuda": "H100"}, num_cards=2)
def test_glm_image_autoround_w4a16_generates_image(omni_runner_handler: OmniRunnerHandler):
"""Load the W4A16 quantized GLM-Image model and verify it produces a valid image."""
gc.collect()
current_omni_platform.empty_cache()
device_index = current_omni_platform.current_device()
current_omni_platform.reset_peak_memory_stats()
monitor = DeviceMemoryMonitor(device_index=device_index, interval=0.02)
monitor.start()
prompt_dict = {
"prompt": "A photo of a cat sitting on a laptop keyboard",
"height": HEIGHT,
"width": WIDTH,
"mm_processor_kwargs": {
"target_h": HEIGHT,
"target_w": WIDTH,
},
}
ar_params = _ar_sampling_params(
max_tokens=compute_max_tokens(HEIGHT, WIDTH),
height=HEIGHT,
width=WIDTH,
seed=42,
)
diffusion_params = _diffusion_sampling_params(
height=HEIGHT,
width=WIDTH,
num_steps=NUM_STEPS,
seed=42,
)
outputs = omni_runner_handler.runner.generate(
[prompt_dict],
[ar_params, diffusion_params],
)
monitor.stop()
first_output = outputs[0]
assert first_output.final_output_type == "image"
req_out = first_output.request_output
assert isinstance(req_out, OmniRequestOutput) and hasattr(req_out, "images")
images = req_out.images
assert len(images) >= 1, "Expected at least one generated image"
img = images[0]
assert isinstance(img, Image.Image)
assert img.width == WIDTH, f"Expected width {WIDTH}, got {img.width}"
assert img.height == HEIGHT, f"Expected height {HEIGHT}, got {img.height}"
# Sanity: image should not be blank (all-zero)
arr = np.array(img)
assert arr.std() > 1.0, "Generated image appears blank (std ≈ 0)"
gc.collect()
current_omni_platform.empty_cache()
# ------------------------------------------------------------------
# Test: image-to-image generation (quantized)
# ------------------------------------------------------------------
@pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True)
@hardware_test(res={"cuda": "H100"}, num_cards=2)
def test_glm_image_autoround_w4a16_image_to_image(omni_runner_handler: OmniRunnerHandler):
"""Load the W4A16 quantized GLM-Image and verify image-to-image generation works."""
ref_image_arr = generate_synthetic_image(WIDTH, HEIGHT)["np_array"]
gc.collect()
current_omni_platform.empty_cache()
current_omni_platform.reset_peak_memory_stats()
prompt_dict = {
"prompt": "Make it look like winter",
"multi_modal_data": {"image": ref_image_arr},
"height": HEIGHT,
"width": WIDTH,
"mm_processor_kwargs": {
"target_h": HEIGHT,
"target_w": WIDTH,
},
}
ar_params = _ar_sampling_params(
max_tokens=compute_max_tokens(HEIGHT, WIDTH),
height=HEIGHT,
width=WIDTH,
seed=42,
)
diffusion_params = _diffusion_sampling_params(
height=HEIGHT,
width=WIDTH,
num_steps=NUM_STEPS,
seed=42,
)
outputs = omni_runner_handler.runner.generate(
[prompt_dict],
[ar_params, diffusion_params],
)
first_output = outputs[0]
assert first_output.final_output_type == "image"
req_out = first_output.request_output
assert isinstance(req_out, OmniRequestOutput) and hasattr(req_out, "images")
images = req_out.images
assert len(images) >= 1, "Expected at least one generated image"
img = images[0]
assert isinstance(img, Image.Image)
assert img.width == WIDTH
assert img.height == HEIGHT
gc.collect()
current_omni_platform.empty_cache()
@@ -0,0 +1,76 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for GLM-Image model __init__.py lazy import pattern.
The ``__init__.py`` uses ``__getattr__`` for lazy loading to avoid importing
``transformers.models.glm_image`` at module init, which may not be available
in all environments.
"""
import pytest
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class TestLazyImport:
"""Test the __getattr__ lazy import pattern in glm_image __init__.py."""
def test_getattr_exists_and_is_callable(self):
"""Verify __getattr__ exists and is callable."""
import vllm_omni.model_executor.models.glm_image as glm_image_pkg
assert hasattr(glm_image_pkg, "__getattr__")
assert callable(glm_image_pkg.__getattr__)
def test_getattr_returns_class_for_known_attribute(self):
"""Verify __getattr__ returns GlmImageForConditionalGeneration for known attribute."""
import vllm_omni.model_executor.models.glm_image as glm_image_pkg
# Call __getattr__ directly to test the lazy import logic
result = glm_image_pkg.__getattr__("GlmImageForConditionalGeneration")
# Verify we get a class (the actual GlmImageForConditionalGeneration)
assert result is not None
assert isinstance(result, type)
assert result.__name__ == "GlmImageForConditionalGeneration"
def test_getattr_raises_for_unknown_attribute(self):
"""Verify __getattr__ raises AttributeError for unknown attributes."""
import vllm_omni.model_executor.models.glm_image as glm_image_pkg
# Test unknown attribute via __getattr__ directly
with pytest.raises(AttributeError, match="has no attribute"):
glm_image_pkg.__getattr__("UnknownClass")
def test___all___exports_correct_symbols(self):
"""Verify __all__ contains the expected exported symbols."""
import vllm_omni.model_executor.models.glm_image as glm_image_pkg
assert hasattr(glm_image_pkg, "__all__")
assert "GlmImageForConditionalGeneration" in glm_image_pkg.__all__
class TestLazyImportDoesNotImportTransformersAtInit:
"""Verify that importing the package does not eagerly load transformers."""
def test_glm_image_module_has_getattr(self):
"""Test that the module has __getattr__ for lazy loading."""
import vllm_omni.model_executor.models.glm_image as glm_image_pkg
# The module should have __getattr__
assert hasattr(glm_image_pkg, "__getattr__")
assert callable(glm_image_pkg.__getattr__)
# And __all__
assert hasattr(glm_image_pkg, "__all__")
def test_module_has_proper_structure(self):
"""Test that the module has proper Python module structure."""
import vllm_omni.model_executor.models.glm_image as glm_image_pkg
# Should be a module
assert hasattr(glm_image_pkg, "__name__")
assert glm_image_pkg.__name__ == "vllm_omni.model_executor.models.glm_image"
# Should have __file__ (even if it's a package)
assert hasattr(glm_image_pkg, "__file__")
@@ -77,7 +77,7 @@ class DefaultAdapter:
os.path.join("transformer", "config.json"),
od_config.model,
)
od_config.tf_model_config = TransformerConfig.from_dict(tf_config_dict)
od_config.set_tf_model_config(TransformerConfig.from_dict(tf_config_dict))
loader = DiffusersPipelineLoader(LoadConfig(), od_config=od_config)
# load_model will handle dtypes / device placement, put in .eval() mode
@@ -15,6 +15,7 @@ from vllm.logger import init_logger
from vllm.model_executor.layers.linear import (
ColumnParallelLinear,
QKVParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
@@ -33,6 +34,9 @@ from vllm_omni.diffusion.distributed.sp_plan import (
)
from vllm_omni.diffusion.forward_context import get_forward_context
if TYPE_CHECKING:
from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
logger = init_logger(__name__)
@@ -230,11 +234,24 @@ class GlmImagePrepare(nn.Module):
class GlmImageAdaLayerNormZero(nn.Module):
"""Adaptive LayerNorm with zero initialization for both image and text streams."""
def __init__(self, embedding_dim: int, dim: int) -> None:
def __init__(
self,
embedding_dim: int,
dim: int,
quant_config: "QuantizationConfig | None" = None,
prefix: str = "",
) -> None:
super().__init__()
self.norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-5)
self.norm_context = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-5)
self.linear = nn.Linear(embedding_dim, 12 * dim, bias=True)
self.linear = ReplicatedLinear(
embedding_dim,
12 * dim,
bias=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.linear",
)
def forward(
self, hidden_states: torch.Tensor, encoder_hidden_states: torch.Tensor, temb: torch.Tensor
@@ -244,6 +261,8 @@ class GlmImageAdaLayerNormZero(nn.Module):
norm_encoder_hidden_states = self.norm_context(encoder_hidden_states).to(dtype=dtype)
emb = self.linear(temb)
if isinstance(emb, tuple):
emb = emb[0]
(
shift_msa,
c_shift_msa,
@@ -286,14 +305,25 @@ class GlmImageAdaLayerNormContinuous(nn.Module):
elementwise_affine: bool = True,
eps: float = 1e-5,
bias: bool = True,
quant_config: "QuantizationConfig | None" = None,
prefix: str = "",
):
super().__init__()
self.linear = nn.Linear(conditioning_embedding_dim, embedding_dim * 2, bias=bias)
self.linear = ReplicatedLinear(
conditioning_embedding_dim,
embedding_dim * 2,
bias=bias,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.linear",
)
self.norm = nn.LayerNorm(embedding_dim, eps=eps, elementwise_affine=elementwise_affine)
def forward(self, x: torch.Tensor, conditioning_embedding: torch.Tensor) -> torch.Tensor:
# NO SiLU here
emb = self.linear(conditioning_embedding.to(x.dtype))
if isinstance(emb, tuple):
emb = emb[0]
scale, shift = torch.chunk(emb, 2, dim=1)
x = self.norm(x) * (1 + scale)[:, None, :] + shift[:, None, :]
return x
@@ -469,6 +499,7 @@ class GlmImageAttention(nn.Module):
out_bias: bool = True,
eps: float = 1e-5,
quant_config: "QuantizationConfig | None" = None,
prefix: str = "",
):
super().__init__()
self.dim = dim
@@ -485,6 +516,7 @@ class GlmImageAttention(nn.Module):
bias=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.to_qkv",
)
# QK normalization (LayerNorm, not RMSNorm for GLM-Image)
@@ -501,6 +533,7 @@ class GlmImageAttention(nn.Module):
input_is_parallel=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.to_out.0",
),
nn.Dropout(0.0),
]
@@ -554,7 +587,7 @@ class GlmImageAttention(nn.Module):
hidden_states_combined = torch.cat([encoder_hidden_states, hidden_states], dim=1)
# QKV projection
qkv = self.to_qkv(hidden_states_combined)
qkv = self.to_qkv(hidden_states_combined.contiguous())
q_size = self.to_qkv.num_heads * self.head_dim
kv_size = self.to_qkv.num_kv_heads * self.head_dim
query, key, value = qkv.split([q_size, kv_size, kv_size], dim=-1)
@@ -604,8 +637,12 @@ class GlmImageAttention(nn.Module):
# Project combined [text, image] outputs, then split.
# This keeps SP numerically aligned with the non-SP path.
joint_hidden_states_out = joint_hidden_states_out.flatten(2, 3).to(dtype)
# Contiguous for FP8/W4A16 quantized RowParallelLinear
for module in self.to_out:
joint_hidden_states_out = module(joint_hidden_states_out)
if isinstance(module, RowParallelLinear):
joint_hidden_states_out = module(joint_hidden_states_out.contiguous())
else:
joint_hidden_states_out = module(joint_hidden_states_out)
encoder_hidden_states_out = joint_hidden_states_out[:, :text_seq_length, :]
hidden_states_out = joint_hidden_states_out[:, text_seq_length:, :]
@@ -645,7 +682,10 @@ class GlmImageAttention(nn.Module):
# Output projection
for module in self.to_out:
hidden_states_out = module(hidden_states_out)
if isinstance(module, RowParallelLinear):
hidden_states_out = module(hidden_states_out.contiguous())
else:
hidden_states_out = module(hidden_states_out)
# Split back to text and image
encoder_hidden_states_out = hidden_states_out[:, :text_seq_length, :]
@@ -663,6 +703,7 @@ class ColumnParallelGELU(nn.Module):
approximate: str = "none",
bias: bool = True,
quant_config: "QuantizationConfig | None" = None,
prefix: str = "",
):
super().__init__()
self.proj = ColumnParallelLinear(
@@ -672,6 +713,7 @@ class ColumnParallelGELU(nn.Module):
gather_output=False,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.proj",
)
self.approximate = approximate
@@ -688,6 +730,7 @@ class ColumnParallelSiLU(nn.Module):
*,
bias: bool = True,
quant_config: "QuantizationConfig | None" = None,
prefix: str = "",
):
super().__init__()
self.proj = ColumnParallelLinear(
@@ -697,6 +740,7 @@ class ColumnParallelSiLU(nn.Module):
gather_output=False,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.proj",
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
@@ -714,6 +758,7 @@ class GlmImageFeedForward(nn.Module):
bias: bool = True,
activation_fn: str = "gelu",
quant_config: "QuantizationConfig | None" = None,
prefix: str = "",
):
super().__init__()
inner_dim = inner_dim or int(dim * mult)
@@ -721,7 +766,7 @@ class GlmImageFeedForward(nn.Module):
if activation_fn == "linear-silu":
layers: list[nn.Module] = [
ColumnParallelSiLU(dim, inner_dim, bias=bias, quant_config=quant_config),
ColumnParallelSiLU(dim, inner_dim, bias=bias, quant_config=quant_config, prefix=f"{prefix}.net.0"),
nn.Identity(),
RowParallelLinear(
inner_dim,
@@ -730,12 +775,20 @@ class GlmImageFeedForward(nn.Module):
input_is_parallel=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.net.2",
),
]
else:
approximate = "tanh" if activation_fn == "gelu-approximate" else "none"
layers = [
ColumnParallelGELU(dim, inner_dim, approximate=approximate, bias=bias, quant_config=quant_config),
ColumnParallelGELU(
dim,
inner_dim,
approximate=approximate,
bias=bias,
quant_config=quant_config,
prefix=f"{prefix}.net.0",
),
nn.Identity(),
RowParallelLinear(
inner_dim,
@@ -744,6 +797,7 @@ class GlmImageFeedForward(nn.Module):
input_is_parallel=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.net.2",
),
]
@@ -751,7 +805,12 @@ class GlmImageFeedForward(nn.Module):
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
for module in self.net:
hidden_states = module(hidden_states)
if isinstance(module, ColumnParallelLinear):
hidden_states, _ = module(hidden_states)
elif isinstance(module, RowParallelLinear):
hidden_states = module(hidden_states.contiguous())
else:
hidden_states = module(hidden_states)
return hidden_states
@@ -765,19 +824,21 @@ class GlmImageTransformerBlock(nn.Module):
attention_head_dim: int = 40,
time_embed_dim: int = 512,
ffn_hidden_dim: int | None = None,
quant_config: "QuantizationConfig | None" = None,
parallel_config: DiffusionParallelConfig | None = None,
quant_config: "QuantizationConfig | None" = None,
prefix: str = "",
) -> None:
super().__init__()
# 1. Attention with AdaLN
self.norm1 = GlmImageAdaLayerNormZero(time_embed_dim, dim)
self.norm1 = GlmImageAdaLayerNormZero(time_embed_dim, dim, quant_config=quant_config, prefix=f"{prefix}.norm1")
self.attn1 = GlmImageAttention(
dim=dim,
num_heads=num_attention_heads,
head_dim=attention_head_dim,
quant_config=quant_config,
parallel_config=parallel_config,
quant_config=quant_config,
prefix=f"{prefix}.attn1",
)
# 2. Feedforward
@@ -789,6 +850,7 @@ class GlmImageTransformerBlock(nn.Module):
inner_dim=ffn_hidden_dim,
activation_fn="gelu-approximate",
quant_config=quant_config,
prefix=f"{prefix}.ff",
)
def forward(
@@ -959,6 +1021,7 @@ class GlmImageTransformer2DModel(CachedTransformer):
inner_dim=inner_dim,
activation_fn="gelu",
quant_config=quant_config,
prefix="glyph_projector",
)
self.prior_token_embedding = nn.Embedding(prior_vq_quantizer_codebook_size, inner_dim)
self.prior_projector = GlmImageFeedForward(
@@ -967,6 +1030,7 @@ class GlmImageTransformer2DModel(CachedTransformer):
inner_dim=inner_dim,
activation_fn="linear-silu",
quant_config=quant_config,
prefix="prior_projector",
)
# Prepare module for SP (encapsulates patch embedding and RoPE for _sp_plan)
@@ -988,15 +1052,20 @@ class GlmImageTransformer2DModel(CachedTransformer):
attention_head_dim,
time_embed_dim,
ffn_hidden_dim=ffn_hidden_dim,
quant_config=quant_config,
parallel_config=self.parallel_config,
quant_config=quant_config,
prefix=f"transformer_blocks.{i}",
)
for _ in range(num_layers)
for i in range(num_layers)
]
)
# 4. Output projection
self.norm_out = GlmImageAdaLayerNormContinuous(inner_dim, time_embed_dim, elementwise_affine=False)
# Final modulation feeds proj_out; quant_config is NOT applied here
# to avoid precision degradation in the final projection layer.
self.norm_out = GlmImageAdaLayerNormContinuous(
inner_dim, time_embed_dim, elementwise_affine=False, quant_config=None, prefix="norm_out"
)
self.proj_out = nn.Linear(inner_dim, patch_size * patch_size * out_channels, bias=True)
def forward(
@@ -301,10 +301,7 @@ class GlmImagePipeline(nn.Module, DiffusionPipelineProfilerMixin):
# Load transformer (DiT)
logger.info("Loading GlmImageTransformer2DModel (DiT)...")
self.transformer = GlmImageTransformer2DModel(
od_config=od_config,
quant_config=od_config.quantization_config,
)
self.transformer = GlmImageTransformer2DModel(od_config=od_config, quant_config=od_config.quantization_config)
# Weight sources for DiT loading
self.weights_sources = [