From 01a2f93256975c7ff9565c5414b0fe0225bc4765 Mon Sep 17 00:00:00 2001 From: Hongsheng Liu Date: Sun, 13 Sep 2026 10:05:56 +0800 Subject: [PATCH] [Bugfix][Frontend] Honor output_compression on the image generations route (#7447) Signed-off-by: hsliu_ustc Signed-off-by: amy-why-3459 Co-authored-by: amy-why-3459 --- recipes/Boogu/Boogu-Image.md | 33 ++++++++++++ .../openai_api/test_image_api_utils.py | 54 +++++++++++++++++++ vllm_omni/entrypoints/openai/api_server.py | 4 +- .../entrypoints/openai/protocol/images.py | 11 ++++ 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 tests/entrypoints/openai_api/test_image_api_utils.py diff --git a/recipes/Boogu/Boogu-Image.md b/recipes/Boogu/Boogu-Image.md index 0353b9778..9a5ce0397 100644 --- a/recipes/Boogu/Boogu-Image.md +++ b/recipes/Boogu/Boogu-Image.md @@ -452,6 +452,39 @@ curl -s http://localhost:8091/v1/chat/completions \ - **Known limitations:** the same single-GPU limitations as Base and Edit apply; CPU offload, Cache-DiT, and multi-GPU parallelism are not yet validated. +## Output format and compression + +Image requests accept `output_format` (`png`, `jpeg`, or `webp`) and +`output_compression` (0-100). The default is `png` with the fastest, +least-compressed PNG encode, which is lossless but produces multi-megabyte +payloads. For latency-sensitive clients, `output_format: "jpeg"` removes +almost the entire response-encoding overhead: on a 1024x1024 Turbo request +the client-observed e2e latency drops from ~900 ms to ~766 ms and the +payload shrinks from ~3.1 MB to ~0.8 MB. Use `output_compression` to trade +encode time against payload size when PNG must stay lossless: lower values +(e.g. 1) compress harder and produce smaller payloads at proportionally +longer encode times. + +## Attention backend + +The diffusion attention backend defaults to `FLASH_ATTN` (FlashAttention-2). +On Hopper-class GPUs (SM90) pass `--diffusion-attention-backend CUDNN_ATTN` +for a measured ~12% engine-time reduction on Boogu-Image workloads: + +```bash +vllm serve Boogu/Boogu-Image-0.1-Turbo \ + --omni \ + --diffusion-attention-backend CUDNN_ATTN \ + --port 8091 +``` + +Measured on a 143 GB SM90 card (1024x1024, 4 steps, 5-run mean): stage time +673 ms with `CUDNN_ATTN` vs 763 ms with the default. Outputs are numerically +different from FLASH_ATTN (different attention kernel) but visually +equivalent, deterministic per seed, and decode identically. On datacenter +Blackwell (SM100 / SM103) use `--diffusion-attention-backend TRTLLM_ATTN` +instead; it is rejected on SM90. + ## Performance validation The checked-in single-device A40 measurements remain the current Boogu diff --git a/tests/entrypoints/openai_api/test_image_api_utils.py b/tests/entrypoints/openai_api/test_image_api_utils.py new file mode 100644 index 000000000..31bc36c5e --- /dev/null +++ b/tests/entrypoints/openai_api/test_image_api_utils.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + +"""L1 tests for the image response encoder's compression contract.""" + +import base64 +import io + +import pytest +from PIL import Image + +from vllm_omni.entrypoints.openai.image_api_utils import ( + encode_image_base64_with_compression, +) + +pytestmark = [pytest.mark.core_model, pytest.mark.cpu] + + +@pytest.fixture(scope="module") +def image(): + # Photographic-content proxy: noise-plus-gradient content compresses very + # differently across PNG compress levels, which is what the mapping under + # test controls. + import numpy as np + + rng = np.random.default_rng(0) + grad = np.linspace(0, 255, 256, dtype=np.float32) + arr = np.clip(grad[None, :, None] + rng.integers(-24, 25, (256, 256, 3)), 0, 255).astype(np.uint8) + return Image.fromarray(arr, mode="RGB") + + +def test_png_output_compression_maps_to_compress_level(image): + """output_compression 100 -> compress_level 0, 1 -> compress_level 9.""" + + def encode(compression): + buffer = io.BytesIO() + image.save(buffer, format="PNG", compress_level=max(0, min(9, 9 - compression // 11))) + return len(base64.b64encode(buffer.getvalue())) + + fast = encode_image_base64_with_compression(image, format="png", output_compression=100) + small = encode_image_base64_with_compression(image, format="png", output_compression=1) + + # Level-0 PNG barely compresses; the mapped level-9 encode is smaller. + assert len(small) < len(fast) + # The mapping must produce the same sizes as the equivalent compress_level. + assert len(fast) == encode(100) + assert len(small) == encode(1) + + +def test_jpeg_output_compression_maps_to_quality(image): + high = base64.b64decode(encode_image_base64_with_compression(image, format="jpeg", output_compression=100)) + low = base64.b64decode(encode_image_base64_with_compression(image, format="jpeg", output_compression=10)) + + assert len(high) > len(low) diff --git a/vllm_omni/entrypoints/openai/api_server.py b/vllm_omni/entrypoints/openai/api_server.py index 565362312..f4f22f95f 100644 --- a/vllm_omni/entrypoints/openai/api_server.py +++ b/vllm_omni/entrypoints/openai/api_server.py @@ -1605,7 +1605,9 @@ def _build_image_generation_response( output_format = _choose_output_format(request.output_format or "png", None) image_data = [ ImageData( - b64_json=encode_image_base64_with_compression(image, format=output_format), + b64_json=encode_image_base64_with_compression( + image, format=output_format, output_compression=request.output_compression + ), revised_prompt=None, ) for image in images diff --git a/vllm_omni/entrypoints/openai/protocol/images.py b/vllm_omni/entrypoints/openai/protocol/images.py index 0215e1ce0..b7800c13e 100644 --- a/vllm_omni/entrypoints/openai/protocol/images.py +++ b/vllm_omni/entrypoints/openai/protocol/images.py @@ -175,6 +175,17 @@ class ImageGenerationRequest(BaseModel): default=None, description="Output image format: 'png', 'jpeg', or 'webp'. Defaults to 'png'.", ) + output_compression: int = Field( + default=100, + ge=0, + le=100, + description=( + "Compression level 0-100. For 'jpeg'/'webp' this is the encoder " + "quality. For 'png' 100 keeps the fastest, least-compressed " + "encode and lower values trade encode time for smaller payloads " + "(100 -> compress_level 0, 1 -> compress_level 9)." + ), + ) return_stage_metrics: bool | None = Field( default=None, description="Return stage metrics for benchmark clients.",