94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
269 lines
10 KiB
Python
269 lines
10 KiB
Python
"""Unit tests for InputValidationStage.preprocess_condition_image resolution logic."""
|
||
|
||
import unittest
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import numpy as np
|
||
import torch
|
||
from diffusers.pipelines.flux2.image_processor import Flux2ImageProcessor
|
||
from PIL import Image
|
||
|
||
from sglang.multimodal_gen.configs.pipeline_configs.base import ModelTaskType
|
||
from sglang.multimodal_gen.configs.pipeline_configs.flux import Flux2PipelineConfig
|
||
from sglang.multimodal_gen.configs.pipeline_configs.wan import (
|
||
WanI2V480PConfig,
|
||
WanI2V720PConfig,
|
||
)
|
||
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
|
||
from sglang.multimodal_gen.runtime.pipelines.flux_2 import Flux2Pipeline
|
||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
|
||
from sglang.multimodal_gen.runtime.pipelines_core.stages.input_validation import (
|
||
InputValidationStage,
|
||
)
|
||
|
||
|
||
def _make_batch(condition_image: Image.Image, width=None, height=None) -> Req:
|
||
"""Create a minimal Req with a condition image and optional user dimensions."""
|
||
sp = SamplingParams(
|
||
seed=42,
|
||
num_outputs_per_prompt=1,
|
||
width=width,
|
||
height=height,
|
||
)
|
||
batch = Req(sampling_params=sp, condition_image=condition_image)
|
||
return batch
|
||
|
||
|
||
def _make_server_args(pipeline_config):
|
||
"""Create a mock ServerArgs with the given pipeline config."""
|
||
sa = MagicMock()
|
||
sa.pipeline_config = pipeline_config
|
||
return sa
|
||
|
||
|
||
class _DummyTI2IConfig:
|
||
task_type = ModelTaskType.TI2I
|
||
|
||
def __init__(self):
|
||
self.vae_config = MagicMock()
|
||
self.vae_config.get_vae_scale_factor.return_value = 8
|
||
|
||
def preprocess_vae_image(self, batch, vae_image_processor):
|
||
return None
|
||
|
||
def calculate_condition_image_size(self, image, width, height):
|
||
return None
|
||
|
||
def preprocess_condition_image(
|
||
self, image, target_width, target_height, vae_image_processor
|
||
):
|
||
return image, (target_width, target_height)
|
||
|
||
def prepare_calculated_size(self, image):
|
||
return image.size
|
||
|
||
|
||
class TestCalculateDimensionsFromArea(unittest.TestCase):
|
||
"""Tests for InputValidationStage._calculate_dimensions_from_area."""
|
||
|
||
def test_square_aspect_ratio(self):
|
||
# area=921600, aspect=1.0, mod=16 → sqrt(921600)=~960
|
||
w, h = InputValidationStage._calculate_dimensions_from_area(921600, 1.0, 16)
|
||
self.assertEqual(w % 16, 0)
|
||
self.assertEqual(h % 16, 0)
|
||
self.assertEqual((w, h), (960, 960))
|
||
|
||
def test_16_9_aspect_ratio(self):
|
||
# aspect = 720/1280 = 0.5625
|
||
w, h = InputValidationStage._calculate_dimensions_from_area(921600, 9 / 16, 16)
|
||
self.assertEqual(w % 16, 0)
|
||
self.assertEqual(h % 16, 0)
|
||
self.assertEqual((w, h), (1280, 720))
|
||
|
||
def test_9_16_aspect_ratio(self):
|
||
w, h = InputValidationStage._calculate_dimensions_from_area(921600, 16 / 9, 16)
|
||
self.assertEqual(w % 16, 0)
|
||
self.assertEqual(h % 16, 0)
|
||
self.assertEqual((w, h), (720, 1280))
|
||
|
||
def test_mod_alignment(self):
|
||
# Ensure dimensions are always multiples of mod_value
|
||
w, h = InputValidationStage._calculate_dimensions_from_area(500000, 1.3, 16)
|
||
self.assertEqual(w % 16, 0)
|
||
self.assertEqual(h % 16, 0)
|
||
|
||
|
||
class TestPreprocessConditionImageResolution(unittest.TestCase):
|
||
"""Tests for the WanI2V480PConfig branch of preprocess_condition_image.
|
||
|
||
Verifies that:
|
||
- Aspect ratio always comes from the condition image
|
||
- User-specified width/height controls target area (scale)
|
||
- Output is clamped to max_area when user dimensions exceed it
|
||
- Dimensions are always mod-aligned
|
||
"""
|
||
|
||
def setUp(self):
|
||
self.stage = InputValidationStage()
|
||
|
||
def _run(self, config, img_w, img_h, user_w=None, user_h=None):
|
||
"""Run preprocess_condition_image and return (batch.width, batch.height)."""
|
||
img = Image.new("RGB", (img_w, img_h), color="red")
|
||
batch = _make_batch(img, width=user_w, height=user_h)
|
||
server_args = _make_server_args(config)
|
||
self.stage.preprocess_condition_image(batch, server_args, img_w, img_h)
|
||
return batch.width, batch.height
|
||
|
||
def test_720p_no_user_dims_16_9_image(self):
|
||
"""16:9 image, no user dims → 1280×720."""
|
||
w, h = self._run(WanI2V720PConfig(), 1920, 1080)
|
||
self.assertEqual((w, h), (1280, 720))
|
||
|
||
def test_720p_no_user_dims_9_16_image(self):
|
||
"""9:16 image, no user dims → 720×1280."""
|
||
w, h = self._run(WanI2V720PConfig(), 1080, 1920)
|
||
self.assertEqual((w, h), (720, 1280))
|
||
|
||
def test_720p_no_user_dims_square_image(self):
|
||
"""Square image, no user dims → ~960×960 (max_area=921600, sqrt≈960)."""
|
||
w, h = self._run(WanI2V720PConfig(), 1024, 1024)
|
||
self.assertEqual((w, h), (960, 960))
|
||
self.assertEqual(w % 16, 0)
|
||
|
||
def test_720p_user_dims_equal_max_area_16_9_image(self):
|
||
"""16:9 image + user 1280×720 (=max_area) → 1280×720."""
|
||
w, h = self._run(WanI2V720PConfig(), 1920, 1080, 1280, 720)
|
||
self.assertEqual((w, h), (1280, 720))
|
||
|
||
def test_720p_user_dims_equal_max_area_square_image(self):
|
||
"""Square image + user 1280×720 → still square (~960×960) because
|
||
aspect ratio comes from image, not from user dimensions."""
|
||
w, h = self._run(WanI2V720PConfig(), 1024, 1024, 1280, 720)
|
||
self.assertEqual((w, h), (960, 960))
|
||
|
||
def test_720p_user_dims_smaller_area(self):
|
||
"""Square image + user 832×480 → smaller square (target_area=399360)."""
|
||
w, h = self._run(WanI2V720PConfig(), 1024, 1024, 832, 480)
|
||
self.assertEqual((w, h), (624, 624))
|
||
self.assertEqual(w % 16, 0)
|
||
|
||
def test_720p_user_dims_exceed_max_area(self):
|
||
"""4K request clamped to max_area."""
|
||
w, h = self._run(WanI2V720PConfig(), 1920, 1080, 3840, 2160)
|
||
self.assertEqual(w % 16, 0)
|
||
self.assertEqual(h % 16, 0)
|
||
self.assertEqual((w, h), (1280, 720))
|
||
|
||
def test_480p_no_user_dims_16_9_image(self):
|
||
"""480p config, 16:9 image → area-based calc from max_area=399360."""
|
||
w, h = self._run(WanI2V480PConfig(), 1920, 1080)
|
||
# max_area=480*832=399360, aspect=9/16 → (832, 464) due to rounding
|
||
self.assertEqual(w % 16, 0)
|
||
self.assertEqual(h % 16, 0)
|
||
self.assertEqual((w, h), (832, 464))
|
||
|
||
def test_condition_image_resized_to_output_dims(self):
|
||
"""Condition image is resized to match output dimensions."""
|
||
img = Image.new("RGB", (1920, 1080), color="blue")
|
||
batch = _make_batch(img)
|
||
server_args = _make_server_args(WanI2V720PConfig())
|
||
self.stage.preprocess_condition_image(batch, server_args, 1920, 1080)
|
||
self.assertEqual(batch.condition_image.size, (batch.width, batch.height))
|
||
|
||
def test_list_condition_image_takes_first(self):
|
||
"""List of condition images → uses first one."""
|
||
img1 = Image.new("RGB", (1920, 1080), color="red")
|
||
img2 = Image.new("RGB", (800, 600), color="green")
|
||
batch = _make_batch(img1)
|
||
batch.condition_image = [img1, img2]
|
||
server_args = _make_server_args(WanI2V720PConfig())
|
||
self.stage.preprocess_condition_image(batch, server_args, 1920, 1080)
|
||
self.assertIsInstance(batch.condition_image, Image.Image)
|
||
self.assertEqual((batch.width, batch.height), (1280, 720))
|
||
|
||
|
||
class TestFlux2ConditionImagePreprocess(unittest.TestCase):
|
||
def test_matches_official_flux2_image_processor(self):
|
||
config = Flux2PipelineConfig()
|
||
config.vae_config.arch_config.vae_scale_factor = 8
|
||
processor = Flux2ImageProcessor(vae_scale_factor=16)
|
||
image = Image.fromarray(
|
||
np.arange(1792 * 1216 * 3, dtype=np.uint8).reshape(1216, 1792, 3),
|
||
mode="RGB",
|
||
)
|
||
|
||
size = config.calculate_condition_image_size(image, image.width, image.height)
|
||
self.assertEqual(size, (1232, 832))
|
||
|
||
processed, processed_size = config.preprocess_condition_image(
|
||
image, size[0], size[1], processor
|
||
)
|
||
|
||
official_image = processor._resize_to_target_area(image, 1024 * 1024)
|
||
expected_width = (official_image.width // 16) * 16
|
||
expected_height = (official_image.height // 16) * 16
|
||
expected = processor.preprocess(
|
||
official_image,
|
||
height=expected_height,
|
||
width=expected_width,
|
||
resize_mode="crop",
|
||
)
|
||
|
||
self.assertEqual(processed_size, (expected_width, expected_height))
|
||
self.assertTrue(torch.equal(processed, expected))
|
||
|
||
@patch.object(Flux2Pipeline, "add_standard_ti2i_stages")
|
||
def test_runtime_pipeline_uses_flux2_image_processor(self, mock_add_stages):
|
||
pipeline = object.__new__(Flux2Pipeline)
|
||
server_args = MagicMock()
|
||
server_args.pipeline_config.vae_config.arch_config.vae_scale_factor = 8
|
||
|
||
Flux2Pipeline.create_pipeline_stages(pipeline, server_args)
|
||
|
||
processor = mock_add_stages.call_args.kwargs["vae_image_processor"]
|
||
self.assertIsInstance(processor, Flux2ImageProcessor)
|
||
self.assertIs(
|
||
processor,
|
||
mock_add_stages.call_args.kwargs["image_vae_stage_kwargs"][
|
||
"vae_image_processor"
|
||
],
|
||
)
|
||
|
||
|
||
class TestFlux2TI2ISizeResolution(unittest.TestCase):
|
||
def setUp(self):
|
||
self.stage = InputValidationStage()
|
||
self.config = _DummyTI2IConfig()
|
||
|
||
def test_uses_condition_image_size_when_width_height_not_explicit(self):
|
||
image = Image.new("RGB", (1255, 833), color="red")
|
||
batch = _make_batch(image)
|
||
batch.extra = {}
|
||
|
||
self.stage.preprocess_condition_image(
|
||
batch,
|
||
_make_server_args(self.config),
|
||
image.width,
|
||
image.height,
|
||
)
|
||
|
||
self.assertEqual((batch.width, batch.height), (1248, 832))
|
||
|
||
def test_preserves_explicit_width_height_for_ti2i(self):
|
||
image = Image.new("RGB", (1255, 833), color="red")
|
||
batch = _make_batch(image, width=768, height=512)
|
||
batch.extra = {"explicit_fields": ["width", "height"]}
|
||
|
||
self.stage.preprocess_condition_image(
|
||
batch,
|
||
_make_server_args(self.config),
|
||
image.width,
|
||
image.height,
|
||
)
|
||
|
||
self.assertEqual((batch.width, batch.height), (768, 512))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|