[diffusion] Fix GLM-Image resolution alignment (#32999)

Co-authored-by: AuFlow <AuFlow@users.noreply.github.com>
This commit is contained in:
AuFlow
2026-08-06 15:41:45 +03:00
committed by GitHub
co-authored by AuFlow
parent 3654740347
commit e8d0fe92e9
8 changed files with 402 additions and 18 deletions
@@ -1,6 +1,11 @@
from dataclasses import dataclass from dataclasses import dataclass
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
GLM_IMAGE_RESOLUTION_ALIGNMENT = 32
@dataclass @dataclass
@@ -10,3 +15,39 @@ class GlmImageSamplingParams(SamplingParams):
num_frames: int = 1 num_frames: int = 1
guidance_scale: float = 1.5 guidance_scale: float = 1.5
num_inference_steps: int = 30 num_inference_steps: int = 30
def _adjust(self, server_args):
requested_width = self.width
requested_height = self.height
if self.width is not None and self.height is not None:
self.width, self.height = align_glm_image_resolution(
self.width, self.height
)
if (self.width, self.height) != (
requested_width,
requested_height,
):
logger.warning(
"GLM-Image requires dimensions divisible by %s; adjusted "
"requested resolution from %sx%s to %sx%s",
GLM_IMAGE_RESOLUTION_ALIGNMENT,
requested_width,
requested_height,
self.width,
self.height,
)
super()._adjust(server_args)
def align_glm_image_dimension(value: int) -> int:
"""Round a GLM-Image dimension up to a supported multiple."""
return max(
GLM_IMAGE_RESOLUTION_ALIGNMENT,
(value + GLM_IMAGE_RESOLUTION_ALIGNMENT - 1)
// GLM_IMAGE_RESOLUTION_ALIGNMENT
* GLM_IMAGE_RESOLUTION_ALIGNMENT,
)
def align_glm_image_resolution(width: int, height: int) -> tuple[int, int]:
return align_glm_image_dimension(width), align_glm_image_dimension(height)
@@ -19,8 +19,13 @@ from fastapi import (
UploadFile, UploadFile,
) )
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from PIL import Image
from sglang.multimodal_gen.configs.sample.sampling_params import generate_request_id from sglang.multimodal_gen.configs.sample.glmimage import GlmImageSamplingParams
from sglang.multimodal_gen.configs.sample.sampling_params import (
SamplingParams,
generate_request_id,
)
from sglang.multimodal_gen.runtime.entrypoints.openai.protocol import ( from sglang.multimodal_gen.runtime.entrypoints.openai.protocol import (
ImageGenerationsRequest, ImageGenerationsRequest,
ImageResponse, ImageResponse,
@@ -170,6 +175,7 @@ def _build_image_response_kwargs(
fallback_url: str | None = None, fallback_url: str | None = None,
fallback_urls: list[str] | None = None, fallback_urls: list[str] | None = None,
is_persistent: bool = True, is_persistent: bool = True,
resize: str | None = None,
) -> dict: ) -> dict:
"""Build ImageResponse data list. """Build ImageResponse data list.
@@ -186,6 +192,7 @@ def _build_image_response_kwargs(
b64_json=b64, b64_json=b64,
revised_prompt=prompt, revised_prompt=prompt,
file_path=os.path.abspath(path) if is_persistent else None, file_path=os.path.abspath(path) if is_persistent else None,
resize=resize,
) )
for b64, path in zip(b64_list, save_file_path_list) for b64, path in zip(b64_list, save_file_path_list)
] ]
@@ -210,6 +217,7 @@ def _build_image_response_kwargs(
url=url, url=url,
revised_prompt=prompt, revised_prompt=prompt,
file_path=os.path.abspath(path) if is_persistent else None, file_path=os.path.abspath(path) if is_persistent else None,
resize=resize,
) )
) )
@@ -231,6 +239,28 @@ def _build_image_response_kwargs(
return ret return ret
def _get_response_resize(
sampling_params: SamplingParams, output_path: str | None = None
) -> str | None:
"""Return a generated GLM-Image output's actual size as WIDTHxHEIGHT."""
if not isinstance(sampling_params, GlmImageSamplingParams):
return None
if output_path is not None:
try:
with Image.open(output_path) as output_image:
width, height = output_image.size
return f"{width}x{height}"
except (OSError, ValueError):
# Fall back to the aligned sampling canvas if the output cannot be
# inspected (for example, for a custom output transport).
pass
if sampling_params.width is None or sampling_params.height is None:
return None
return sampling_params.output_size_str()
@router.post("/generations", response_model=ImageResponse) @router.post("/generations", response_model=ImageResponse)
async def generations( async def generations(
request: ImageGenerationsRequest, request: ImageGenerationsRequest,
@@ -308,6 +338,7 @@ async def generations(
async_scheduler_client, batch async_scheduler_client, batch
) )
save_file_path = save_file_path_list[0] save_file_path = save_file_path_list[0]
response_resize = _get_response_resize(sampling, save_file_path)
resp_format = (request.response_format or "b64_json").lower() resp_format = (request.response_format or "b64_json").lower()
if ( if (
is_cosmos3 is_cosmos3
@@ -359,6 +390,7 @@ async def generations(
cloud_urls=cloud_urls, cloud_urls=cloud_urls,
fallback_urls=fallback_urls, fallback_urls=fallback_urls,
is_persistent=is_persistent, is_persistent=is_persistent,
resize=response_resize,
) )
return ImageResponse(**response_kwargs) return ImageResponse(**response_kwargs)
@@ -462,6 +494,7 @@ async def edits(
async_scheduler_client, batch async_scheduler_client, batch
) )
save_file_path = save_file_path_list[0] save_file_path = save_file_path_list[0]
response_resize = _get_response_resize(sampling, save_file_path)
resp_format = (response_format or "b64_json").lower() resp_format = (response_format or "b64_json").lower()
# read b64 before cloud upload may delete the local file # read b64 before cloud upload may delete the local file
@@ -510,6 +543,7 @@ async def edits(
cloud_urls=cloud_urls, cloud_urls=cloud_urls,
fallback_urls=fallback_urls, fallback_urls=fallback_urls,
is_persistent=is_persistent, is_persistent=is_persistent,
resize=response_resize,
) )
return ImageResponse(**response_kwargs) return ImageResponse(**response_kwargs)
@@ -13,6 +13,7 @@ class ImageResponseData(BaseModel):
url: Optional[str] = None url: Optional[str] = None
revised_prompt: Optional[str] = None revised_prompt: Optional[str] = None
file_path: Optional[str] = None file_path: Optional[str] = None
resize: Optional[str] = None
class ImagePromptTokensDetails(BaseModel): class ImagePromptTokensDetails(BaseModel):
@@ -11,6 +11,10 @@ import torch
from diffusers.image_processor import VaeImageProcessor from diffusers.image_processor import VaeImageProcessor
from diffusers.utils.torch_utils import randn_tensor from diffusers.utils.torch_utils import randn_tensor
from sglang.multimodal_gen.configs.sample.glmimage import (
GLM_IMAGE_RESOLUTION_ALIGNMENT,
align_glm_image_resolution,
)
from sglang.multimodal_gen.runtime.distributed import get_local_torch_device from sglang.multimodal_gen.runtime.distributed import get_local_torch_device
from sglang.multimodal_gen.runtime.managers.forward_context import set_forward_context from sglang.multimodal_gen.runtime.managers.forward_context import set_forward_context
from sglang.multimodal_gen.runtime.managers.memory_managers.component_manager import ( from sglang.multimodal_gen.runtime.managers.memory_managers.component_manager import (
@@ -118,6 +122,26 @@ def image_path_to_list(image_path: Union[str, List[str]]) -> List[str]:
return image_path if isinstance(image_path, list) else [image_path] return image_path if isinstance(image_path, list) else [image_path]
def resize_glm_image_to_alignment(image: PIL.Image.Image) -> PIL.Image.Image:
"""Resize an image up so both dimensions use GLM-Image's D32 grid."""
width, height = image.size
aligned_width, aligned_height = align_glm_image_resolution(width, height)
if (aligned_width, aligned_height) == (width, height):
return image
return image.resize((aligned_width, aligned_height), PIL.Image.Resampling.LANCZOS)
def _validate_glm_image_resolution_alignment(width: int, height: int) -> None:
if (
height % GLM_IMAGE_RESOLUTION_ALIGNMENT != 0
or width % GLM_IMAGE_RESOLUTION_ALIGNMENT != 0
):
raise ValueError(
"GLM-Image dimensions must be aligned before AR token generation, "
f"got {width}x{height}"
)
def pooled_image_features_to_tensor(image_features) -> torch.Tensor: def pooled_image_features_to_tensor(image_features) -> torch.Tensor:
pooler_output = getattr(image_features, "pooler_output", None) pooler_output = getattr(image_features, "pooler_output", None)
if pooler_output is not None: if pooler_output is not None:
@@ -337,7 +361,6 @@ class GlmImageAR(PipelineStage):
width: int, width: int,
server_args: ServerArgs, server_args: ServerArgs,
image: Optional[List[PIL.Image.Image]] = None, image: Optional[List[PIL.Image.Image]] = None,
factor: int = 32,
seed: Optional[int] = None, seed: Optional[int] = None,
) -> Tuple[torch.Tensor, Optional[List[torch.Tensor]], Optional[dict[str, int]]]: ) -> Tuple[torch.Tensor, Optional[List[torch.Tensor]], Optional[dict[str, int]]]:
""" """
@@ -348,14 +371,11 @@ class GlmImageAR(PipelineStage):
condition_images: Optional list of condition images for i2i condition_images: Optional list of condition images for i2i
Returns: Returns:
Tuple of (prior_token_ids, pixel_height, pixel_width) Tuple of the D16 prior token IDs, optional source-image token IDs,
- prior_token_ids: Upsampled to d16 format, shape [1, token_h*token_w*4] and optional usage statistics returned by an external AR server.
- pixel_height: Image height in pixels
- pixel_width: Image width in pixels
""" """
device = get_local_torch_device() device = get_local_torch_device()
height = (height // factor) * factor _validate_glm_image_resolution_alignment(width, height)
width = (width // factor) * factor
is_text_to_image = image is None or len(image) == 0 is_text_to_image = image is None or len(image) == 0
# Build messages for processor # Build messages for processor
@@ -456,11 +476,9 @@ class GlmImageAR(PipelineStage):
height: int, height: int,
width: int, width: int,
server_args: ServerArgs, server_args: ServerArgs,
factor: int = 32,
) -> tuple[list[torch.Tensor], list[dict[str, int] | None]]: ) -> tuple[list[torch.Tensor], list[dict[str, int] | None]]:
device = get_local_torch_device() device = get_local_torch_device()
height = (height // factor) * factor _validate_glm_image_resolution_alignment(width, height)
width = (width // factor) * factor
input_ids = [] input_ids = []
image_data = [] image_data = []
@@ -650,7 +668,7 @@ class GlmImageAR(PipelineStage):
width = batch.width width = batch.width
if batch.image_path is not None: if batch.image_path is not None:
ar_condition_images = [ ar_condition_images = [
load_image(img_path) resize_glm_image_to_alignment(load_image(img_path))
for img_path in image_path_to_list(batch.image_path) for img_path in image_path_to_list(batch.image_path)
] ]
else: else:
@@ -662,6 +680,20 @@ class GlmImageAR(PipelineStage):
height = height or ar_condition_images[0].height height = height or ar_condition_images[0].height
width = width or ar_condition_images[0].width width = width or ar_condition_images[0].width
requested_width = width
requested_height = height
width, height = align_glm_image_resolution(width, height)
if (width, height) != (requested_width, requested_height):
logger.warning(
"GLM-Image requires dimensions divisible by %s; adjusted "
"runtime resolution from %sx%s to %sx%s",
GLM_IMAGE_RESOLUTION_ALIGNMENT,
requested_width,
requested_height,
width,
height,
)
time_start = time.time() time_start = time.time()
num_outputs = _num_outputs_per_prompt(batch) num_outputs = _num_outputs_per_prompt(batch)
seed = getattr(batch, "seed", None) seed = getattr(batch, "seed", None)
@@ -1037,7 +1069,7 @@ class GlmImageBeforeDenoisingStage(PipelineStage):
num_inference_steps = batch.num_inference_steps num_inference_steps = batch.num_inference_steps
if batch.image_path is not None: if batch.image_path is not None:
ar_condition_images = [ ar_condition_images = [
load_image(img_path) resize_glm_image_to_alignment(load_image(img_path))
for img_path in image_path_to_list(batch.image_path) for img_path in image_path_to_list(batch.image_path)
] ]
else: else:
@@ -1108,9 +1140,9 @@ class GlmImageBeforeDenoisingStage(PipelineStage):
if isinstance(img, PIL.Image.Image) if isinstance(img, PIL.Image.Image)
else img.shape[:2] else img.shape[:2]
) )
multiple_of = self.vae_scale_factor * self.transformer.config.patch_size image_width, image_height = align_glm_image_resolution(
image_height = (image_height // multiple_of) * multiple_of image_width, image_height
image_width = (image_width // multiple_of) * multiple_of )
img = self.image_processor.preprocess( img = self.image_processor.preprocess(
img, height=image_height, width=image_width img, height=image_height, width=image_width
) )
@@ -1,13 +1,15 @@
import unittest import unittest
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import patch from unittest.mock import MagicMock, patch
import torch import torch
from PIL import Image
from sglang.multimodal_gen.configs.sample.glmimage import GlmImageSamplingParams
from sglang.multimodal_gen.runtime.entrypoints.openai.image_api import ( from sglang.multimodal_gen.runtime.entrypoints.openai.image_api import (
_build_image_response_kwargs, _build_image_response_kwargs,
) )
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch, Req
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.glm_image import ( from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.glm_image import (
GlmImageAR, GlmImageAR,
) )
@@ -200,6 +202,120 @@ class TestGlmImageARSrtBackend(unittest.TestCase):
server_args=self._server_args(), server_args=self._server_args(),
) )
@patch(
"sglang.multimodal_gen.runtime.pipelines_core.stages."
"model_specific_stages.glm_image.get_local_torch_device",
return_value=torch.device("cpu"),
)
def test_forward_aligns_runtime_dimensions_before_ar_generation(self, _mock_device):
stage = GlmImageAR(processor=_FakeProcessor(), vision_language_encoder=None)
stage.generate_prior_tokens = MagicMock(
return_value=(torch.zeros((1, 1), dtype=torch.long), None, None)
)
cases = [
((500, 500), (512, 512)),
((550, 1009), (576, 1024)),
((1280, 720), (1280, 736)),
]
for requested, expected in cases:
with self.subTest(requested=requested):
stage.generate_prior_tokens.reset_mock()
sampling = GlmImageSamplingParams(
prompt="A simple product sketch",
width=requested[0],
height=requested[1],
)
sampling.seed = None
batch = Req(sampling_params=sampling)
stage.forward(batch, self._server_args())
self.assertEqual((batch.width, batch.height), expected)
stage.generate_prior_tokens.assert_called_once_with(
prompt="A simple product sketch",
image=None,
height=expected[1],
width=expected[0],
server_args=self._server_args(),
)
@patch(
"sglang.multimodal_gen.runtime.pipelines_core.stages."
"model_specific_stages.glm_image.get_local_torch_device",
return_value=torch.device("cpu"),
)
@patch(
"sglang.multimodal_gen.runtime.pipelines_core.stages."
"model_specific_stages.glm_image.load_image",
return_value=Image.new("RGB", (1280, 720)),
)
def test_forward_resizes_edit_image_up_to_d32_grid(
self, _mock_load_image, _mock_device
):
stage = GlmImageAR(processor=_FakeProcessor(), vision_language_encoder=None)
stage.generate_prior_tokens = MagicMock(
return_value=(torch.zeros((1, 1), dtype=torch.long), None, None)
)
sampling = GlmImageSamplingParams(
prompt="Edit this image",
width=1280,
height=720,
image_path="input.png",
)
sampling.seed = None
batch = Req(sampling_params=sampling)
stage.forward(batch, self._server_args())
call_kwargs = stage.generate_prior_tokens.call_args.kwargs
self.assertEqual((batch.width, batch.height), (1280, 736))
self.assertEqual(call_kwargs["image"][0].size, (1280, 736))
self.assertEqual((call_kwargs["width"], call_kwargs["height"]), (1280, 736))
@patch(
"sglang.multimodal_gen.runtime.pipelines_core.stages."
"model_specific_stages.glm_image.get_local_torch_device",
return_value=torch.device("cpu"),
)
def test_generate_prior_tokens_rejects_unaligned_internal_dimensions(
self, _mock_device
):
stage = GlmImageAR(processor=_FakeProcessor(), vision_language_encoder=None)
with self.assertRaisesRegex(
ValueError,
"GLM-Image dimensions must be aligned before AR token generation",
):
stage.generate_prior_tokens(
prompt="A simple product sketch",
height=1024,
width=550,
server_args=self._server_args(),
)
@patch(
"sglang.multimodal_gen.runtime.pipelines_core.stages."
"model_specific_stages.glm_image.get_local_torch_device",
return_value=torch.device("cpu"),
)
def test_generate_prior_tokens_batch_rejects_unaligned_internal_dimensions(
self, _mock_device
):
stage = GlmImageAR(processor=_FakeProcessor(), vision_language_encoder=None)
with self.assertRaisesRegex(
ValueError,
"GLM-Image dimensions must be aligned before AR token generation",
):
stage.generate_prior_tokens_batch(
prompts=["A simple product sketch"],
seeds=[42],
height=1024,
width=550,
server_args=self._server_args(),
)
def test_image_response_adds_image_count_to_usage(self): def test_image_response_adds_image_count_to_usage(self):
set_global_server_args(SimpleNamespace(enable_cache_report=False)) set_global_server_args(SimpleNamespace(enable_cache_report=False))
response = _build_image_response_kwargs( response = _build_image_response_kwargs(
@@ -1,10 +1,14 @@
import os import os
from fastapi import HTTPException from fastapi import HTTPException
from PIL import Image
from sglang.multimodal_gen.configs.sample.glmimage import GlmImageSamplingParams
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
from sglang.multimodal_gen.runtime.entrypoints.openai.image_api import ( from sglang.multimodal_gen.runtime.entrypoints.openai.image_api import (
_build_image_response_kwargs, _build_image_response_kwargs,
_fallback_image_urls, _fallback_image_urls,
_get_response_resize,
_raise_if_image_variant_not_found, _raise_if_image_variant_not_found,
_select_image_variant_cloud_url, _select_image_variant_cloud_url,
_select_image_variant_path, _select_image_variant_path,
@@ -36,6 +40,46 @@ def test_url_response_returns_one_item_per_output_path():
] ]
def test_image_response_includes_resize_for_every_output():
response = _build_image_response_kwargs(
["first.png", "second.png"],
"b64_json",
"a lantern",
"req-123",
OutputBatch(),
b64_list=["first", "second"],
resize="1280x736",
)
assert [item.resize for item in response["data"]] == [
"1280x736",
"1280x736",
]
def test_response_resize_is_only_populated_for_glm_image():
glm_sampling = GlmImageSamplingParams(width=1280, height=736)
assert _get_response_resize(glm_sampling) == "1280x736"
assert _get_response_resize(SamplingParams(width=1280, height=736)) is None
def test_response_resize_uses_actual_generated_image_size(tmp_path):
output_path = tmp_path / "output.png"
Image.new("RGB", (1280, 736)).save(output_path)
glm_sampling = GlmImageSamplingParams(image_path="input.png")
assert _get_response_resize(glm_sampling, str(output_path)) == "1280x736"
def test_response_resize_prefers_final_output_over_sampling_canvas(tmp_path):
output_path = tmp_path / "upscaled.png"
Image.new("RGB", (2560, 1472)).save(output_path)
glm_sampling = GlmImageSamplingParams(width=1280, height=736)
assert _get_response_resize(glm_sampling, str(output_path)) == "2560x1472"
def test_url_response_uses_variant_fallback_urls_for_multiple_persistent_outputs(): def test_url_response_uses_variant_fallback_urls_for_multiple_persistent_outputs():
paths = ["first.png", "second.png"] paths = ["first.png", "second.png"]
@@ -2,13 +2,17 @@
import asyncio import asyncio
import io import io
from types import SimpleNamespace
from starlette.datastructures import UploadFile as StarletteUploadFile from starlette.datastructures import UploadFile as StarletteUploadFile
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
from sglang.multimodal_gen.runtime.entrypoints.openai import utils as openai_utils
from sglang.multimodal_gen.runtime.entrypoints.openai.utils import ( from sglang.multimodal_gen.runtime.entrypoints.openai.utils import (
_parse_size_or_raise, _parse_size_or_raise,
_save_upload_to_path, _save_upload_to_path,
_validate_positive_int, _validate_positive_int,
build_sampling_params,
) )
@@ -57,3 +61,49 @@ def test_validate_positive_int_rejects_non_positive_sampling_fields():
assert "num_frames must be positive" in exc.detail assert "num_frames must be positive" in exc.detail
else: else:
raise AssertionError("expected bad request") raise AssertionError("expected bad request")
def test_build_sampling_params_resolves_size_and_explicit_dimensions(monkeypatch):
server_args = SimpleNamespace(model_path="zai-org/GLM-Image")
monkeypatch.setattr(openai_utils, "get_global_server_args", lambda: server_args)
captured = {}
def fake_from_user_sampling_params_args(**kwargs):
captured.update(kwargs)
return SimpleNamespace()
monkeypatch.setattr(
SamplingParams,
"from_user_sampling_params_args",
fake_from_user_sampling_params_args,
)
cases = [
(
{"size": "500x500", "width": None, "height": None},
(500, 500),
),
(
{"size": "1024x1024", "width": None, "height": 600},
(1024, 600),
),
(
{"size": "500x500", "width": None, "height": 600},
(500, 600),
),
(
{"size": "500x500", "width": 600, "height": None},
(600, 500),
),
(
{"size": "500x500", "width": 600, "height": 700},
(600, 700),
),
]
for request_fields, expected in cases:
captured.clear()
build_sampling_params("request-id", **request_fields)
assert (captured["width"], captured["height"]) == expected
@@ -4,6 +4,9 @@ import unittest
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from sglang.multimodal_gen.configs.pipeline_configs.glm_image import (
GlmImagePipelineConfig,
)
from sglang.multimodal_gen.configs.pipeline_configs.ltx_2 import ( from sglang.multimodal_gen.configs.pipeline_configs.ltx_2 import (
LTX2PipelineConfig, LTX2PipelineConfig,
is_ltx23_native_variant, is_ltx23_native_variant,
@@ -17,6 +20,10 @@ from sglang.multimodal_gen.configs.sample.flux import (
Flux2SamplingParams, Flux2SamplingParams,
FluxSamplingParams, FluxSamplingParams,
) )
from sglang.multimodal_gen.configs.sample.glmimage import (
GlmImageSamplingParams,
align_glm_image_dimension,
)
from sglang.multimodal_gen.configs.sample.qwenimage import QwenImageSamplingParams from sglang.multimodal_gen.configs.sample.qwenimage import QwenImageSamplingParams
from sglang.multimodal_gen.configs.sample.sampling_params import ( from sglang.multimodal_gen.configs.sample.sampling_params import (
SamplingParams, SamplingParams,
@@ -100,6 +107,65 @@ class TestSamplingParamsValidate(unittest.TestCase):
class TestSamplingParamsSubclass(unittest.TestCase): class TestSamplingParamsSubclass(unittest.TestCase):
def test_glm_image_rounds_resolution_up_to_multiple_of_32(self):
server_args = SimpleNamespace(
pipeline_config=GlmImagePipelineConfig(),
output_path=None,
comfyui_mode=True,
)
cases = [
((500, 500), (512, 512)),
((1024, 600), (1024, 608)),
((500, 600), (512, 608)),
((550, 1009), (576, 1024)),
((1280, 720), (1280, 736)),
]
for requested, expected in cases:
with self.subTest(requested=requested):
params = GlmImageSamplingParams(
width=requested[0],
height=requested[1],
)
with patch(
"sglang.multimodal_gen.configs.sample.glmimage.logger.warning"
) as mock_warning:
params._adjust(server_args)
self.assertEqual((params.width, params.height), expected)
mock_warning.assert_called_once_with(
"GLM-Image requires dimensions divisible by %s; adjusted "
"requested resolution from %sx%s to %sx%s",
32,
requested[0],
requested[1],
expected[0],
expected[1],
)
def test_glm_image_resolution_rounds_up(self):
self.assertEqual(align_glm_image_dimension(560), 576)
def test_glm_image_resolution_keeps_minimum_alignment(self):
self.assertEqual(align_glm_image_dimension(0), 32)
self.assertEqual(align_glm_image_dimension(-1), 32)
def test_glm_image_does_not_warn_for_aligned_resolution(self):
server_args = SimpleNamespace(
pipeline_config=GlmImagePipelineConfig(),
output_path=None,
comfyui_mode=True,
)
params = GlmImageSamplingParams(width=1024, height=1024)
with patch(
"sglang.multimodal_gen.configs.sample.glmimage.logger.warning"
) as mock_warning:
params._adjust(server_args)
mock_warning.assert_not_called()
def test_flux_defaults_resolution_when_not_provided(self): def test_flux_defaults_resolution_when_not_provided(self):
params = FluxSamplingParams() params = FluxSamplingParams()