Signed-off-by: Ding Zuhao <e1583181@u.nus.edu> Co-authored-by: Cursor <cursoragent@cursor.com>
177 lines
5.7 KiB
Python
177 lines
5.7 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
|
|
"""
|
|
End-to-end online serving test for Bagel text2img and img2img generation.
|
|
|
|
This test validates that the Bagel model can serve image generation requests
|
|
via the OpenAI-compatible chat completions API.
|
|
|
|
Equivalent to running:
|
|
vllm-omni serve "ByteDance-Seed/BAGEL-7B-MoT" --omni --port 8091
|
|
|
|
# text2img
|
|
python3 examples/online_serving/bagel/openai_chat_client.py \\
|
|
--prompt "A cute cat" --modality text2img
|
|
|
|
# img2img
|
|
python3 examples/online_serving/bagel/openai_chat_client.py \\
|
|
--prompt "Let the woman wear a blue dress" --modality img2img \\
|
|
--image-url women.jpg
|
|
"""
|
|
|
|
import base64
|
|
import os
|
|
from io import BytesIO
|
|
|
|
import pytest
|
|
from PIL import Image
|
|
from vllm.assets.image import ImageAsset
|
|
|
|
from tests.helpers.mark import hardware_test
|
|
from tests.helpers.media import generate_synthetic_image
|
|
from tests.helpers.runtime import OmniServerParams
|
|
from tests.helpers.stage_config import get_deploy_config_path
|
|
|
|
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
|
|
|
MODEL = "ByteDance-Seed/BAGEL-7B-MoT"
|
|
STAGE_CONFIGS_PATH = get_deploy_config_path("ci/bagel.yaml")
|
|
|
|
TEXT2IMG_PROMPT = "A cute cat"
|
|
IMG2IMG_PROMPT = "Change the grass color to red"
|
|
|
|
# Create parameter combinations for model and stage config
|
|
test_params = [
|
|
OmniServerParams(
|
|
model=MODEL,
|
|
stage_config_path=STAGE_CONFIGS_PATH,
|
|
stage_init_timeout=300,
|
|
),
|
|
]
|
|
|
|
|
|
def _build_text2img_messages(prompt: str) -> list[dict]:
|
|
"""Build OpenAI-format messages for text2img generation."""
|
|
return [
|
|
{
|
|
"role": "user",
|
|
"content": [{"type": "text", "text": f"<|im_start|>{prompt}<|im_end|>"}],
|
|
}
|
|
]
|
|
|
|
|
|
def _build_img2img_messages(prompt: str, image_b64: str) -> list[dict]:
|
|
"""Build OpenAI-format messages for img2img generation."""
|
|
return [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": f"<|im_start|>{prompt}<|im_end|>"},
|
|
{
|
|
"type": "image_url",
|
|
"image_url": {"url": f"data:image/jpeg;base64,{image_b64}"},
|
|
},
|
|
],
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.slow
|
|
@pytest.mark.diffusion
|
|
@hardware_test(res={"cuda": "H100"})
|
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
|
def test_bagel_text2img_online(omni_server, online_client) -> None:
|
|
"""Test Bagel text2img via OpenAI-compatible chat completions API."""
|
|
request_config = {
|
|
"model": omni_server.model,
|
|
"messages": _build_text2img_messages(TEXT2IMG_PROMPT),
|
|
"modalities": ["image"],
|
|
"extra_body": {
|
|
"height": 512,
|
|
"width": 512,
|
|
"num_inference_steps": 2,
|
|
"guidance_scale": 0.0,
|
|
"seed": 42,
|
|
},
|
|
}
|
|
|
|
online_client.send_diffusion_request(request_config)
|
|
|
|
|
|
@pytest.mark.slow
|
|
@pytest.mark.diffusion
|
|
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
|
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
|
def test_bagel_img2img_online(omni_server, online_client) -> None:
|
|
"""Test Bagel img2img via OpenAI-compatible chat completions API."""
|
|
input_image = ImageAsset("2560px-Gfp-wisconsin-madison-the-nature-boardwalk").pil_image.convert("RGB")
|
|
buffer = BytesIO()
|
|
input_image.save(buffer, format="JPEG")
|
|
image_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
|
|
|
request_config = {
|
|
"model": omni_server.model,
|
|
"messages": _build_img2img_messages(IMG2IMG_PROMPT, image_b64),
|
|
"modalities": ["image"],
|
|
"extra_body": {
|
|
"num_inference_steps": 2,
|
|
"guidance_scale": 0.0,
|
|
"seed": 42,
|
|
},
|
|
}
|
|
|
|
online_client.send_diffusion_request(request_config)
|
|
|
|
|
|
EDIT_SOURCE_SIZE = (1280, 720)
|
|
EDIT_REQUESTED_SIZE = "1024x1024"
|
|
|
|
|
|
def _edit_source_jpeg() -> tuple[str, bytes, str]:
|
|
synthetic = generate_synthetic_image(*EDIT_SOURCE_SIZE, seed=7287)
|
|
return ("source.jpg", base64.b64decode(synthetic["base64"]), "image/jpeg")
|
|
|
|
|
|
def _build_edit_request(size: str) -> dict:
|
|
return {
|
|
"files": [("image", _edit_source_jpeg())],
|
|
"data": {
|
|
"prompt": IMG2IMG_PROMPT,
|
|
"size": size,
|
|
"num_inference_steps": 2,
|
|
"seed": 42,
|
|
"response_format": "b64_json",
|
|
},
|
|
}
|
|
|
|
|
|
@pytest.mark.slow
|
|
@pytest.mark.diffusion
|
|
@hardware_test(res={"cuda": "H100"})
|
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
|
def test_bagel_image_edit_honors_explicit_size_two_stage(omni_server, online_client) -> None:
|
|
"""An explicit ``size`` wins over the AR stage's source-derived KV image shape (#7283)."""
|
|
(response,) = online_client.send_images_edits_http_request(_build_edit_request(EDIT_REQUESTED_SIZE))
|
|
|
|
payload = response.json_body
|
|
assert isinstance(payload, dict)
|
|
assert payload["size"] == EDIT_REQUESTED_SIZE
|
|
image = Image.open(BytesIO(base64.b64decode(payload["data"][0]["b64_json"])))
|
|
assert image.size == (1024, 1024)
|
|
|
|
|
|
@pytest.mark.slow
|
|
@pytest.mark.diffusion
|
|
@hardware_test(res={"cuda": "H100"})
|
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
|
def test_bagel_image_edit_floors_unaligned_size_two_stage(omni_server, online_client) -> None:
|
|
"""A side that is not a multiple of the latent stride is floored to one (1000x700 -> 992x688)."""
|
|
(response,) = online_client.send_images_edits_http_request(_build_edit_request("1000x700"))
|
|
|
|
payload = response.json_body
|
|
assert isinstance(payload, dict)
|
|
assert payload["size"] == "992x688"
|
|
image = Image.open(BytesIO(base64.b64decode(payload["data"][0]["b64_json"])))
|
|
assert image.size == (992, 688)
|