Files
vllm-omni/tests/diffusion/test_diffusion_config_propagation.py
T
2026-08-19 11:23:10 +08:00

215 lines
9.0 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tests that parallel_config survives the create_default_diffusion roundtrip.
Regression tests for https://github.com/vllm-project/vllm-omni/issues/1862
"""
from collections.abc import Mapping
import pytest
import torch
from vllm_omni.config.config_factory import StageConfigFactory
from vllm_omni.diffusion.data import (
DiffusionParallelConfig,
OmniDiffusionConfig,
)
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
from vllm_omni.diffusion.model_metadata import (
HUNYUAN_IMAGE3_MAX_INPUT_IMAGES,
QWEN_IMAGE_EDIT_PLUS_MAX_INPUT_IMAGES,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _roundtrip_diffusion_config(**kwargs) -> OmniDiffusionConfig:
"""Simulate the real path: create_default_diffusion → OmniDiffusionConfig.
Does NOT manually reconstruct parallel_config — relies on
OmniDiffusionConfig.__post_init__ to handle the dict, just like
the production code path does.
"""
stages = StageConfigFactory.create_default_diffusion(kwargs)
engine_args = dict(stages[0]["engine_args"])
return OmniDiffusionConfig.from_kwargs(**engine_args)
class TestParallelConfigPropagation:
"""Core regression tests: parallel_config must survive serialization."""
def test_tp2_roundtrip(self):
pc = DiffusionParallelConfig(tensor_parallel_size=2)
od = _roundtrip_diffusion_config(model="test-model", parallel_config=pc)
assert od.parallel_config.tensor_parallel_size == 2
assert od.parallel_config.world_size == 2
def test_tp4_devices_and_config(self):
pc = DiffusionParallelConfig(tensor_parallel_size=4)
stages = StageConfigFactory.create_default_diffusion({"parallel_config": pc, "model": "x"})
assert stages[0]["runtime"]["devices"] == "0,1,2,3"
# Let __post_init__ reconstruct from dict (real code path)
ea = dict(stages[0]["engine_args"])
od = OmniDiffusionConfig.from_kwargs(**ea)
assert od.parallel_config.tensor_parallel_size == 4
assert od.parallel_config.world_size == 4
def test_sp_config_roundtrip(self):
pc = DiffusionParallelConfig(
tensor_parallel_size=2,
ulysses_degree=2,
ring_degree=1,
)
od = _roundtrip_diffusion_config(model="x", parallel_config=pc)
assert od.parallel_config.ulysses_degree == 2
assert od.parallel_config.ring_degree == 1
def test_mask_sp_padding_roundtrip(self):
pc = DiffusionParallelConfig(ulysses_degree=2, mask_sp_padding=True)
od = _roundtrip_diffusion_config(model="x", parallel_config=pc)
assert od.parallel_config.mask_sp_padding is True
def test_mask_sp_padding_defaults_false(self):
pc = DiffusionParallelConfig(ulysses_degree=2)
od = _roundtrip_diffusion_config(model="x", parallel_config=pc)
assert od.parallel_config.mask_sp_padding is False
def test_cfg_parallel_roundtrip(self):
pc = DiffusionParallelConfig(cfg_parallel_size=2)
od = _roundtrip_diffusion_config(model="x", parallel_config=pc)
assert od.parallel_config.cfg_parallel_size == 2
assert od.parallel_config.world_size == 2
def test_no_parallel_config_defaults_to_tp1(self):
od = _roundtrip_diffusion_config(model="x")
assert od.parallel_config.tensor_parallel_size == 1
assert od.parallel_config.world_size == 1
def test_num_gpus_derived_from_world_size(self):
pc = DiffusionParallelConfig(tensor_parallel_size=2)
od = _roundtrip_diffusion_config(model="x", parallel_config=pc)
assert od.num_gpus == 2
def test_dp_is_inferred_from_num_gpus(self):
pc = DiffusionParallelConfig(tensor_parallel_size=2, ulysses_degree=2)
od = OmniDiffusionConfig.from_kwargs(model="x", parallel_config=pc, num_gpus=8)
assert od.parallel_config.data_parallel_size == 2
assert od.parallel_config.world_size == 8
def test_explicit_dp_is_validated_against_num_gpus(self):
pc = DiffusionParallelConfig(tensor_parallel_size=2, data_parallel_size=2)
od = OmniDiffusionConfig.from_kwargs(model="x", parallel_config=pc, num_gpus=4)
assert od.parallel_config.data_parallel_size == 2
with pytest.raises(ValueError, match="does not match WORLD-derived value"):
OmniDiffusionConfig.from_kwargs(
model="x",
parallel_config=DiffusionParallelConfig(tensor_parallel_size=2, data_parallel_size=2),
num_gpus=8,
)
def test_hsdp_does_not_infer_ordinary_dp(self):
pc = DiffusionParallelConfig(use_hsdp=True, hsdp_shard_size=4)
od = OmniDiffusionConfig.from_kwargs(model="x", parallel_config=pc, num_gpus=4)
assert od.parallel_config.data_parallel_size == 1
assert od.parallel_config.world_size == 4
class TestCreateDefaultDiffusion:
"""Verify engine_args structure from create_default_diffusion."""
def test_parallel_config_serialized_as_dict(self):
"""The key fix: parallel_config must appear in engine_args as a dict."""
pc = DiffusionParallelConfig(tensor_parallel_size=2)
stages = StageConfigFactory.create_default_diffusion({"model": "x", "parallel_config": pc})
ea = stages[0]["engine_args"]
assert "parallel_config" in ea
assert isinstance(ea["parallel_config"], Mapping)
assert ea["parallel_config"]["tensor_parallel_size"] == 2
def test_dtype_serialized_as_string(self):
stages = StageConfigFactory.create_default_diffusion({"dtype": torch.float16, "model": "x"})
assert stages[0]["engine_args"]["dtype"] == "torch.float16"
def test_cache_backend_defaults_to_none(self):
stages = StageConfigFactory.create_default_diffusion({"model": "x"})
assert stages[0]["engine_args"]["cache_backend"] == "none"
def test_single_gpu_default_devices(self):
stages = StageConfigFactory.create_default_diffusion({"model": "x"})
assert stages[0]["runtime"]["devices"] == "0"
def test_extra_kwargs_forwarded(self):
stages = StageConfigFactory.create_default_diffusion(
{"model": "x", "enforce_eager": True, "lora_path": "/tmp/lora"}
)
ea = stages[0]["engine_args"]
assert ea["enforce_eager"] is True
assert ea["lora_path"] == "/tmp/lora"
def test_diffusion_kv_mode_roundtrip(self):
od = _roundtrip_diffusion_config(model="x", diffusion_kv_mode="paged_scheduler")
assert od.diffusion_kv_mode is DiffusionKVCacheMode.PAGED_SCHEDULER
def test_diffusion_kv_sizing_fields_roundtrip(self, monkeypatch):
monkeypatch.setattr(OmniDiffusionConfig, "_resolve_master_port", lambda self: 29500)
od = _roundtrip_diffusion_config(
model="x",
kv_cache_memory_bytes=4096,
gpu_memory_utilization=0.75,
max_num_batched_tokens=2048,
max_model_len=4096,
)
assert od.kv_cache_memory_bytes == 4096
assert od.gpu_memory_utilization == 0.75
assert od.max_num_batched_tokens == 2048
assert od.max_model_len == 4096
def test_qwen_image_edit_plus_sets_generic_multimodal_limit():
od_config = OmniDiffusionConfig(model="Qwen/Qwen-Image-Edit-2511", model_class_name="QwenImageEditPlusPipeline")
od_config.update_multimodal_support()
assert od_config.supports_multimodal_inputs is True
assert od_config.max_multimodal_image_inputs == QWEN_IMAGE_EDIT_PLUS_MAX_INPUT_IMAGES
def test_task_type_roundtrip():
od = _roundtrip_diffusion_config(model="x", task_type="model-defined-task")
assert od.task_type == "model-defined-task"
def test_architecture_name_resolves_via_pipeline_class_fallback():
# Direct hit: the architecture name itself is the metadata table key.
qwen_od_config = OmniDiffusionConfig(
model="Qwen/Qwen-Image-Edit-2511",
model_class_name="QwenImageEditPlusPipeline",
)
qwen_od_config.update_multimodal_support()
assert qwen_od_config.supports_multimodal_inputs is True
assert qwen_od_config.max_multimodal_image_inputs == QWEN_IMAGE_EDIT_PLUS_MAX_INPUT_IMAGES
# Fallback: the HF architecture name differs from the internal pipeline class
# name, so ``get_diffusion_model_metadata`` must consult ``_DIFFUSION_MODELS``
# and resolve the metadata via the pipeline class name.
hunyuan_od_config = OmniDiffusionConfig(
model="tencent/HunyuanImage-3.0-Instruct",
model_class_name="HunyuanImage3ForCausalMM",
)
hunyuan_od_config.update_multimodal_support()
assert hunyuan_od_config.supports_multimodal_inputs is True
assert hunyuan_od_config.max_multimodal_image_inputs == HUNYUAN_IMAGE3_MAX_INPUT_IMAGES
def test_additional_config_roundtrip():
additional_config = {"torchair_graph_config": {"enabled": True}}
od = _roundtrip_diffusion_config(model="x", additional_config=additional_config)
assert od.additional_config == additional_config