diff --git a/tests/diffusion/models/qwen_image/test_qwen_image_size_utils.py b/tests/diffusion/models/qwen_image/test_qwen_image_size_utils.py new file mode 100644 index 000000000..7ba8f108a --- /dev/null +++ b/tests/diffusion/models/qwen_image/test_qwen_image_size_utils.py @@ -0,0 +1,26 @@ +import pytest + +from vllm_omni.diffusion.utils.size_utils import ( + normalize_min_aligned_size, +) + +pytestmark = [pytest.mark.core_model, pytest.mark.cpu] + + +@pytest.mark.parametrize( + ("height", "width", "expected"), + [ + (1, 1, (16, 16)), + (15, 15, (16, 16)), + (17, 17, (16, 16)), + (31, 33, (16, 32)), + (64, 80, (64, 80)), + ], +) +def test_normalize_min_aligned_size_clamps_to_minimum_aligned_shape(height, width, expected): + assert normalize_min_aligned_size(height, width, alignment=16) == expected + + +def test_normalize_min_aligned_size_rejects_invalid_alignment(): + with pytest.raises(ValueError, match="positive alignment"): + normalize_min_aligned_size(16, 16, alignment=0) diff --git a/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image.py b/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image.py index 5056b5342..9f75c8453 100644 --- a/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image.py +++ b/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image.py @@ -34,6 +34,9 @@ from vllm_omni.diffusion.models.qwen_image.qwen_image_transformer import ( ) from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin from vllm_omni.diffusion.request import OmniDiffusionRequest +from vllm_omni.diffusion.utils.size_utils import ( + normalize_min_aligned_size, +) from vllm_omni.diffusion.utils.tf_utils import get_transformer_config_kwargs if TYPE_CHECKING: @@ -938,6 +941,7 @@ class QwenImagePipeline(nn.Module, QwenImageCFGParallelMixin, DiffusionPipelineP height = req.sampling_params.height or self.default_sample_size * self.vae_scale_factor width = req.sampling_params.width or self.default_sample_size * self.vae_scale_factor + height, width = normalize_min_aligned_size(height, width, self.vae_scale_factor * 2) num_inference_steps = req.sampling_params.num_inference_steps or num_inference_steps sigmas = req.sampling_params.sigmas or sigmas max_sequence_length = req.sampling_params.max_sequence_length or max_sequence_length diff --git a/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image_edit.py b/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image_edit.py index 3d0cd2a6d..dd77d71b1 100644 --- a/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image_edit.py +++ b/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image_edit.py @@ -37,6 +37,9 @@ from vllm_omni.diffusion.models.qwen_image.qwen_image_transformer import ( ) from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin from vllm_omni.diffusion.request import OmniDiffusionRequest +from vllm_omni.diffusion.utils.size_utils import ( + normalize_min_aligned_size, +) from vllm_omni.diffusion.utils.tf_utils import get_transformer_config_kwargs from vllm_omni.inputs.data import OmniTextPrompt from vllm_omni.model_executor.model_loader.weight_utils import ( @@ -97,9 +100,7 @@ def get_qwen_image_edit_pre_process_func( width = request.sampling_params.width or calculated_width # Ensure dimensions are multiples of vae_scale_factor * 2 - multiple_of = vae_scale_factor * 2 - height = height // multiple_of * multiple_of - width = width // multiple_of * multiple_of + height, width = normalize_min_aligned_size(height, width, vae_scale_factor * 2) # Store calculated dimensions in request prompt["additional_information"]["calculated_height"] = calculated_height @@ -661,9 +662,7 @@ class QwenImageEditPipeline(nn.Module, SupportImageInput, QwenImageCFGParallelMi height = height or calculated_height width = width or calculated_width - multiple_of = self.vae_scale_factor * 2 - width = width // multiple_of * multiple_of - height = height // multiple_of * multiple_of + height, width = normalize_min_aligned_size(height, width, self.vae_scale_factor * 2) if image is not None and not (isinstance(image, torch.Tensor) and image.size(1) == self.latent_channels): image = self.image_processor.resize(image, calculated_height, calculated_width) diff --git a/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image_edit_plus.py b/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image_edit_plus.py index cb5a36579..6f6c9d2ba 100644 --- a/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image_edit_plus.py +++ b/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image_edit_plus.py @@ -40,6 +40,9 @@ from vllm_omni.diffusion.models.qwen_image.qwen_image_transformer import ( ) from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin from vllm_omni.diffusion.request import OmniDiffusionRequest +from vllm_omni.diffusion.utils.size_utils import ( + normalize_min_aligned_size, +) from vllm_omni.diffusion.utils.tf_utils import get_transformer_config_kwargs from vllm_omni.inputs.data import OmniTextPrompt from vllm_omni.model_executor.model_loader.weight_utils import ( @@ -99,9 +102,7 @@ def get_qwen_image_edit_plus_pre_process_func( width = request.sampling_params.width or calculated_width # Ensure dimensions are multiples of vae_scale_factor * 2 - multiple_of = vae_scale_factor * 2 - height = height // multiple_of * multiple_of - width = width // multiple_of * multiple_of + height, width = normalize_min_aligned_size(height, width, vae_scale_factor * 2) # Store calculated dimensions in request prompt["additional_information"]["calculated_height"] = calculated_height @@ -604,9 +605,7 @@ class QwenImageEditPlusPipeline( height = height or calculated_height width = width or calculated_width - multiple_of = self.vae_scale_factor * 2 - width = width // multiple_of * multiple_of - height = height // multiple_of * multiple_of + height, width = normalize_min_aligned_size(height, width, self.vae_scale_factor * 2) condition_images = [] vae_images = [] diff --git a/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image_layered.py b/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image_layered.py index f1d28f068..38866d89c 100644 --- a/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image_layered.py +++ b/vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image_layered.py @@ -36,6 +36,9 @@ from vllm_omni.diffusion.models.qwen_image.qwen_image_transformer import ( ) from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin from vllm_omni.diffusion.request import OmniDiffusionRequest +from vllm_omni.diffusion.utils.size_utils import ( + normalize_min_aligned_size, +) from vllm_omni.diffusion.utils.tf_utils import get_transformer_config_kwargs from vllm_omni.inputs.data import OmniTextPrompt from vllm_omni.model_executor.model_loader.weight_utils import ( @@ -109,9 +112,7 @@ def get_qwen_image_layered_pre_process_func( height = calculated_height width = calculated_width - multiple_of = vae_scale_factor * 2 - width = width // multiple_of * multiple_of - height = height // multiple_of * multiple_of + height, width = normalize_min_aligned_size(height, width, vae_scale_factor * 2) # Store calculated dimensions in request prompt["additional_information"]["calculated_height"] = calculated_height @@ -665,9 +666,7 @@ the image\n<|vision_start|><|image_pad|><|vision_end|><|im_end|>\n<|im_start|>as height = calculated_height width = calculated_width - multiple_of = self.vae_scale_factor * 2 - width = width // multiple_of * multiple_of - height = height // multiple_of * multiple_of + height, width = normalize_min_aligned_size(height, width, self.vae_scale_factor * 2) if image is not None and not (isinstance(image, torch.Tensor) and image.size(1) == self.latent_channels): image = self.image_processor.resize(image, calculated_height, calculated_width) diff --git a/vllm_omni/diffusion/utils/size_utils.py b/vllm_omni/diffusion/utils/size_utils.py new file mode 100644 index 000000000..030e542f1 --- /dev/null +++ b/vllm_omni/diffusion/utils/size_utils.py @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +"""Shared size normalization helpers for diffusion pipelines.""" + + +def normalize_min_aligned_size(height: int, width: int, alignment: int) -> tuple[int, int]: + """Clamp dimensions to the minimum valid aligned size. + + This preserves floor-to-alignment behavior for normal requests while + preventing very small dimensions from collapsing to zero after alignment. + """ + + alignment = int(alignment) + if alignment <= 0: + raise ValueError(f"Expected positive alignment, got {alignment}") + + normalized_height = max(alignment, (int(height) // alignment) * alignment) + normalized_width = max(alignment, (int(width) // alignment) * alignment) + return normalized_height, normalized_width