Files
vllm-project--vllm-omni/tests/diffusion/diffusion_backend/test_diffusers_backend.py
T
wehub-resource-sync eec33d25b2
pre-commit / pre-commit (push) Failing after 1s
Build Wheel / build (3.11) (push) Failing after 1s
Build Wheel / build (3.12) (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:29:08 +08:00

675 lines
25 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from collections import namedtuple
from types import SimpleNamespace
import pytest
import torch
from diffusers import DiffusionPipeline # pyright: ignore[reportPrivateImportUsage]
from PIL import Image
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data
from vllm_omni.diffusion.data import (
DiffusionOutput,
DiffusionParallelConfig,
OmniDiffusionConfig,
)
from vllm_omni.diffusion.models.diffusers_adapter import DiffusersAdapterPipeline
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
pytestmark = [pytest.mark.diffusion]
def _make_od_config(**overrides) -> OmniDiffusionConfig:
od_config = OmniDiffusionConfig(
model="test/model",
model_class_name="DiffusersAdapterPipeline",
dtype=torch.float16,
diffusion_load_format="diffusers",
diffusers_load_kwargs={},
diffusers_call_kwargs={},
output_type="pil",
parallel_config=DiffusionParallelConfig(cfg_parallel_size=1, sequence_parallel_size=1),
cache_backend="none",
)
for key, value in overrides.items():
setattr(od_config, key, value)
return od_config
def _make_request(**overrides) -> OmniDiffusionRequest:
prompt = overrides.pop("prompt", "a test prompt")
negative_prompt = overrides.pop("negative_prompt", None)
prompt_obj: dict[str, str] = {"prompt": prompt}
if negative_prompt is not None:
prompt_obj["negative_prompt"] = negative_prompt
defaults = {
"prompt": prompt_obj,
"sampling_params": OmniDiffusionSamplingParams(
num_inference_steps=20,
guidance_scale=7.5,
height=16,
width=16,
num_frames=1,
num_outputs_per_prompt=1,
seed=42,
output_type="pil",
generator_device="cpu",
),
"request_id": "diffusers-backend",
}
defaults.update(overrides)
return OmniDiffusionRequest(**defaults)
def _patch_fake_diffusers_quantization_backends(mocker):
class FakePipelineQuantizationConfig:
def __init__(self, *, quant_mapping=None, quant_backend=None, quant_kwargs=None, components_to_quantize=None):
self.quant_mapping = quant_mapping
self.quant_backend = quant_backend
self.quant_kwargs = quant_kwargs
self.components_to_quantize = components_to_quantize
class FakeTorchAoConfig:
def __init__(self, quant_type, modules_to_not_convert=None):
self.quant_type = quant_type
self.modules_to_not_convert = modules_to_not_convert
@classmethod
def from_dict(cls, config):
return cls(quant_type=config["quant_type"])
class FakeInt8DynamicActivationInt8WeightConfig:
pass
mocker.patch(
"vllm_omni.diffusion.models.diffusers_adapter.quantization_utils.PipelineQuantizationConfig",
FakePipelineQuantizationConfig,
)
mocker.patch(
"vllm_omni.diffusion.models.diffusers_adapter.quantization_utils.TorchAoConfig",
FakeTorchAoConfig,
)
mocker.patch(
"vllm_omni.diffusion.models.diffusers_adapter.quantization_utils._get_torchao_quant_type_cls",
return_value=FakeInt8DynamicActivationInt8WeightConfig,
)
mocker.patch(
"vllm_omni.diffusion.models.diffusers_adapter.quantization_utils._get_diffusers_quantization_config_cls",
return_value=FakeTorchAoConfig,
)
return FakePipelineQuantizationConfig, FakeTorchAoConfig, FakeInt8DynamicActivationInt8WeightConfig
class _TransformerBackedPipeline(DiffusionPipeline):
def __init__(self, transformer):
pass
def _make_batch(**overrides) -> DiffusionRequestBatch:
"""Wrap a single request in a DiffusionRequestBatch, matching the forward contract."""
return DiffusionRequestBatch(requests=[_make_request(**overrides)])
@pytest.mark.core_model
@pytest.mark.cpu
class TestPipelineArgumentsHandling:
def test_adapter_forward_returns_output(self, mocker):
od_config = _make_od_config()
request = _make_request()
stub_image = Image.new("RGB", (request.sampling_params.width, request.sampling_params.height)) # pyright: ignore[reportArgumentType]
adapter = DiffusersAdapterPipeline(od_config=od_config)
MockPipelineOutput = namedtuple("MockPipelineOutput", ["image"])
MockPipeline = type("MockPipeline", (DiffusionPipeline,), {})
adapter._pipeline = MockPipeline()
mocker.patch.object(
MockPipeline,
"__call__",
return_value=MockPipelineOutput(image=stub_image),
)
output = adapter.forward(DiffusionRequestBatch(requests=[request]))
assert isinstance(output, DiffusionOutput)
assert isinstance(output.output, MockPipelineOutput)
assert output.output.image is stub_image
@pytest.mark.parametrize(
"feature_id",
["cfg_parallel", "ulysses", "ring", "teacache", "cache_dit", "enforce_eager", "unsupported_quantization"],
)
def test_adapter_guard_unsupported_feature(self, feature_id):
if feature_id == "cfg_parallel":
od_config = _make_od_config(
parallel_config=DiffusionParallelConfig(cfg_parallel_size=2, sequence_parallel_size=1),
cache_backend="none",
)
elif feature_id == "ulysses":
od_config = _make_od_config(
parallel_config=DiffusionParallelConfig(cfg_parallel_size=1, ulysses_degree=2),
cache_backend="none",
)
elif feature_id == "ring":
od_config = _make_od_config(
parallel_config=DiffusionParallelConfig(cfg_parallel_size=1, ring_degree=2),
cache_backend="none",
)
elif feature_id == "teacache":
od_config = _make_od_config(
parallel_config=DiffusionParallelConfig(cfg_parallel_size=1, sequence_parallel_size=1),
cache_backend="tea_cache",
)
elif feature_id == "cache_dit":
od_config = _make_od_config(
parallel_config=DiffusionParallelConfig(cfg_parallel_size=1, sequence_parallel_size=1),
cache_backend="cache_dit",
)
elif feature_id == "enforce_eager":
od_config = _make_od_config(enforce_eager=True)
elif feature_id == "unsupported_quantization":
od_config = _make_od_config(quantization_config=SimpleNamespace(quant_method="gguf"))
else:
raise ValueError(f"Unknown feature ID: {feature_id}")
with pytest.raises(NotImplementedError):
DiffusersAdapterPipeline(od_config=od_config)
def test_adapter_load_weights_injects_supported_quantization_config(self, mocker):
(
FakePipelineQuantizationConfig,
FakeTorchAoConfig,
FakeInt8DynamicActivationInt8WeightConfig,
) = _patch_fake_diffusers_quantization_backends(mocker)
class MockPipeline:
def __call__(self, prompt=None):
return None
def to(self, device):
return self
mock_from_pretrained = mocker.patch(
"vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter.DiffusionPipeline.from_pretrained",
return_value=MockPipeline(),
)
mocker.patch(
"vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter.DiffusionPipeline.load_config",
return_value={
"_class_name": "TransformerBackedPipeline",
"scheduler": ["diffusers", "SchedulerMixin"],
"transformer": ["diffusers", "Transformer2DModel"],
"transformer_2": ["diffusers", "Transformer2DModel"],
"vae": ["diffusers", "AutoencoderKL"],
},
)
adapter = DiffusersAdapterPipeline(
od_config=_make_od_config(
quantization_config=SimpleNamespace(quant_method="int8"),
diffusers_pipeline_cls=_TransformerBackedPipeline,
)
)
adapter.load_weights()
pipeline_quant_config = mock_from_pretrained.call_args.kwargs["quantization_config"]
torchao_config = pipeline_quant_config.quant_mapping["transformer"]
assert isinstance(pipeline_quant_config, FakePipelineQuantizationConfig)
assert isinstance(torchao_config, FakeTorchAoConfig)
assert isinstance(torchao_config.quant_type, FakeInt8DynamicActivationInt8WeightConfig)
assert isinstance(
pipeline_quant_config.quant_mapping["transformer_2"].quant_type,
FakeInt8DynamicActivationInt8WeightConfig,
)
@pytest.mark.parametrize(
("pipeline_config", "diffusers_load_kwargs", "diffusers_pipeline_cls"),
[
(
{
"_class_name": "NoTransformerPipeline",
"scheduler": ["diffusers", "SchedulerMixin"],
"unet": ["diffusers", "UNet2DConditionModel"],
"vae": ["diffusers", "AutoencoderKL"],
},
{},
_TransformerBackedPipeline,
),
(
{
"_class_name": "TransformerPipeline",
"scheduler": ["diffusers", "SchedulerMixin"],
"transformer": ["diffusers", "Transformer2DModel"],
"vae": ["diffusers", "AutoencoderKL"],
},
{"transformer": None},
None,
),
],
)
def test_adapter_load_weights_rejects_converted_quantization_without_transformer_component(
self,
pipeline_config,
diffusers_load_kwargs,
diffusers_pipeline_cls,
mocker,
):
_patch_fake_diffusers_quantization_backends(mocker)
mocker.patch(
"vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter.DiffusionPipeline.load_config",
return_value=pipeline_config,
)
mock_from_pretrained = mocker.patch(
"vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter.DiffusionPipeline.from_pretrained"
)
adapter = DiffusersAdapterPipeline(
od_config=_make_od_config(
quantization_config=SimpleNamespace(quant_method="int8"),
diffusers_load_kwargs=diffusers_load_kwargs,
diffusers_pipeline_cls=diffusers_pipeline_cls,
)
)
with pytest.raises(NotImplementedError, match="transformer"):
adapter.load_weights()
mock_from_pretrained.assert_not_called()
def test_adapter_rejects_dduf_file_with_converted_quantization(self):
with pytest.raises(NotImplementedError, match="dduf_file"):
DiffusersAdapterPipeline(
od_config=_make_od_config(
quantization_config=SimpleNamespace(quant_method="int8"),
diffusers_load_kwargs={"dduf_file": "model.dduf"},
)
)
def test_adapter_load_weights_preserves_diffusers_native_quantization_config(self, mocker):
class MockPipeline:
def __call__(self, prompt=None):
return None
def to(self, device):
return self
diffusers_native_quantization_config = object()
mock_from_pretrained = mocker.patch(
"vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter.DiffusionPipeline.from_pretrained",
return_value=MockPipeline(),
)
adapter = DiffusersAdapterPipeline(
od_config=_make_od_config(
quantization_config=SimpleNamespace(quant_method="gguf"),
diffusers_load_kwargs={
"dduf_file": "model.dduf",
"quantization_config": diffusers_native_quantization_config,
},
)
)
adapter.load_weights()
kwargs = mock_from_pretrained.call_args.kwargs
assert kwargs["quantization_config"] is diffusers_native_quantization_config
assert kwargs["dduf_file"] == "model.dduf"
def test_adapter_load_weights_builds_diffusers_native_quantization_config_from_dict(self, mocker):
FakePipelineQuantizationConfig, FakeTorchAoConfig, _ = _patch_fake_diffusers_quantization_backends(mocker)
class MockPipeline:
def __call__(self, prompt=None):
return None
def to(self, device):
return self
mock_from_pretrained = mocker.patch(
"vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter.DiffusionPipeline.from_pretrained",
return_value=MockPipeline(),
)
adapter = DiffusersAdapterPipeline(
od_config=_make_od_config(
diffusers_load_kwargs={
"quantization_config": {
"quant_mapping": {
"transformer": {
"quant_method": "torchao",
"quant_type": "fake-int8-config",
},
},
},
},
)
)
adapter.load_weights()
quant_config = mock_from_pretrained.call_args.kwargs["quantization_config"]
assert isinstance(quant_config, FakePipelineQuantizationConfig)
assert isinstance(quant_config.quant_mapping["transformer"], FakeTorchAoConfig)
assert quant_config.quant_mapping["transformer"].quant_type == "fake-int8-config"
def test_adapter_guard_unknown_output_type(self, mocker):
"""Test that the adapter wraps an unknown output type as-is.
This is useful when `return_dict=True` and the diffusers pipeline returns an OrderedDict subclass."""
adapter = DiffusersAdapterPipeline(od_config=_make_od_config())
raw_output = {"unexpected": "dict-output"}
MockPipeline = type("MockPipeline", (DiffusionPipeline,), {})
adapter._pipeline = MockPipeline()
mocker.patch.object(
MockPipeline,
"__call__",
return_value=raw_output,
)
output = adapter.forward(_make_batch())
assert isinstance(output, DiffusionOutput)
assert output.output == raw_output
def test_adapter_build_call_kwargs(self, mocker):
class MockPipeline:
def __call__(
self,
prompt=None,
negative_prompt=None,
num_inference_steps=None,
# guidance_scale=None, # deliberately not included
height=None,
width=None,
num_frames=None,
num_images_per_prompt=None,
num_videos_per_prompt=None,
output_type=None,
generator=None,
):
"""Need to make the signature match the actual DiffusionPipeline.__call__, because inspect.signature() is used"""
return None
def to(self, device):
return self
mock_from_pretrained = mocker.patch(
"vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter.DiffusionPipeline.from_pretrained",
return_value=MockPipeline(),
)
adapter = DiffusersAdapterPipeline(
od_config=_make_od_config(
diffusers_call_kwargs={
"guidance_scale": 1.25,
"eta": 0.3,
"output_type": "np",
}
)
)
adapter.load_weights()
mock_from_pretrained.assert_called_once()
req = _make_request(
prompt="a cat on mars",
negative_prompt="low quality",
sampling_params=OmniDiffusionSamplingParams(
num_inference_steps=9,
guidance_scale=8.0,
height=320,
width=640,
num_frames=8,
num_outputs_per_prompt=2,
seed=123,
output_type="pil",
),
)
kwargs = adapter._build_call_kwargs(DiffusionRequestBatch(requests=[req]))
assert kwargs["prompt"] == "a cat on mars"
assert kwargs["negative_prompt"] == "low quality"
assert kwargs["num_inference_steps"] == 9
assert "guidance_scale" not in kwargs
assert kwargs["height"] == 320
assert kwargs["width"] == 640
assert kwargs["num_frames"] == 8
assert kwargs["num_images_per_prompt"] == 2
assert kwargs["num_videos_per_prompt"] == 2
assert kwargs["output_type"] == "pil"
assert isinstance(kwargs["generator"], torch.Generator)
assert kwargs["generator"].device.type == "cpu"
assert kwargs["generator"].initial_seed() == 123
def test_adapter_extract_input_reads_negative_prompt_fallback(self):
# 1. test with diffusers_call_kwargs.negative_prompt is a correct list[str]
adapter = DiffusersAdapterPipeline(
od_config=_make_od_config(
diffusers_call_kwargs={
"negative_prompt": [
"fallback negative prompt 0",
"fallback negative prompt 1",
"fallback negative prompt 2",
],
}
)
)
input_kwargs = adapter._extract_input(
[
"a prompt from a string",
{"prompt": "a prompt from a dict"},
{
"prompt": "a prompt with its own negative prompt",
"negative_prompt": "request negative prompt",
},
]
)
assert input_kwargs["prompt"] == [
"a prompt from a string",
"a prompt from a dict",
"a prompt with its own negative prompt",
]
assert input_kwargs["negative_prompt"] == [
"fallback negative prompt 0",
"fallback negative prompt 1",
"request negative prompt",
]
# 2. test with diffusers_call_kwargs.negative_prompt is a single str
adapter = DiffusersAdapterPipeline(
od_config=_make_od_config(diffusers_call_kwargs={"negative_prompt": "fallback negative prompt"})
)
input_kwargs = adapter._extract_input(
[
"a prompt from a string",
{"prompt": "a prompt from a dict"},
{
"prompt": "a prompt with its own negative prompt",
"negative_prompt": "request negative prompt",
},
]
)
assert input_kwargs["prompt"] == [
"a prompt from a string",
"a prompt from a dict",
"a prompt with its own negative prompt",
]
assert input_kwargs["negative_prompt"] == [
"fallback negative prompt",
"fallback negative prompt",
"request negative prompt",
]
# 3. test with diffusers_call_kwargs.negative_prompt is None
adapter = DiffusersAdapterPipeline(od_config=_make_od_config(diffusers_call_kwargs={}))
input_kwargs = adapter._extract_input(
[
"a prompt from a string",
{"prompt": "a prompt from a dict"},
{
"prompt": "a prompt with its own negative prompt",
"negative_prompt": "request negative prompt",
},
]
)
assert input_kwargs["prompt"] == [
"a prompt from a string",
"a prompt from a dict",
"a prompt with its own negative prompt",
]
assert input_kwargs["negative_prompt"] == ["", "", "request negative prompt"]
# 4. test with diffusers_call_kwargs.negative_prompt is a list[str] but its length is less than the number of prompts
adapter = DiffusersAdapterPipeline(
od_config=_make_od_config(
diffusers_call_kwargs={
"negative_prompt": [
"fallback negative prompt 0",
]
}
)
)
with pytest.raises(ValueError):
adapter._extract_input(
[
"a prompt from a string",
{"prompt": "a prompt from a dict"},
{
"prompt": "a prompt with its own negative prompt",
"negative_prompt": "request negative prompt",
},
]
)
# 5. test with diffusers_call_kwargs.negative_prompt is a list[str] but its length is greater than the number of prompts
adapter = DiffusersAdapterPipeline(
od_config=_make_od_config(
diffusers_call_kwargs={
"negative_prompt": [
"fallback negative prompt 0",
"fallback negative prompt 1",
"fallback negative prompt 2",
"fallback negative prompt 3",
]
}
)
)
input_kwargs = adapter._extract_input(
[
"a prompt from a string",
{"prompt": "a prompt from a dict"},
{
"prompt": "a prompt with its own negative prompt",
"negative_prompt": "request negative prompt",
},
]
)
assert input_kwargs["prompt"] == [
"a prompt from a string",
"a prompt from a dict",
"a prompt with its own negative prompt",
]
assert input_kwargs["negative_prompt"] == [
"fallback negative prompt 0",
"fallback negative prompt 1",
"request negative prompt",
]
# 6. test when no negative prompt is provided
adapter = DiffusersAdapterPipeline(od_config=_make_od_config())
input_kwargs = adapter._extract_input(
[
"a prompt from a string",
{"prompt": "a prompt from a dict"},
]
)
assert input_kwargs["prompt"] == [
"a prompt from a string",
"a prompt from a dict",
]
assert "negative_prompt" not in input_kwargs
def test_adapter_load_weights_uses_registered_pipeline_utils(self, mocker):
class WanImageToVideoPipeline:
pass
class MockPipeline:
def __call__(self, prompt=None):
return None
def to(self, device):
return self
od_config = _make_od_config(
diffusers_pipeline_cls=WanImageToVideoPipeline,
boundary_ratio=0.875,
)
mock_from_pretrained = mocker.patch(
"vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter.DiffusionPipeline.from_pretrained",
return_value=MockPipeline(),
)
pipeline = DiffusersAdapterPipeline(od_config=od_config)
pipeline.load_weights()
mock_from_pretrained.assert_called_once()
_, kwargs = mock_from_pretrained.call_args
assert kwargs["boundary_ratio"] == 0.875
problematic_request = _make_request(
prompt="a prompt from a string",
sampling_params=OmniDiffusionSamplingParams(
boundary_ratio=0.875,
),
)
with pytest.raises(ValueError):
pipeline.forward(DiffusionRequestBatch(requests=[problematic_request]))
@pytest.mark.advanced_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
class TestDiffusersBackendEndToEndExecution:
@pytest.mark.parametrize(
"omni_server",
[
OmniServerParams(
model="tiny-random/Qwen-Image",
server_args=[
"--diffusion-load-format",
"diffusers",
"--diffusers-call-kwargs",
'{"height": 512, "width": 0}', # deliberately weird width to be overridden
],
),
],
indirect=True,
)
def test_t2i_random_weights(
self,
omni_server: OmniServer,
openai_client: OpenAIClientHandler,
):
messages = dummy_messages_from_mix_data(content_text="a photo of an astronaut riding a horse on mars")
request_config = {
"model": omni_server.model,
"messages": messages,
"extra_body": {
"width": 512,
"num_inference_steps": 2,
"negative_prompt": "blurry",
"true_cfg_scale": 4.0,
"seed": 42,
},
}
response = openai_client.send_diffusion_request(request_config)
image: Image.Image = response[0].images[0] # pyright: ignore[reportOptionalSubscript]
# Request config has incomplete width/height, so internal assertion in `send_diffusion_request` is incomplete.
assert image.size == (512, 512)