[Bugfix] Fix image quality in /v1/images/generations for multi-stage pipeline (#2267)
Signed-off-by: Lancer <maruixiang6688@gmail.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
edb4f2fdcf
commit
cf75ae6ed5
4 files changed
+559
-14
No files matched your search
@@ -106,10 +106,13 @@ def test_encode_image_base64():
|
||||
|
||||
|
||||
class MockGenerationResult:
|
||||
"""Mock result object from AsyncOmni.generate()"""
|
||||
"""Mock result object compatible with current diffusion output shape."""
|
||||
|
||||
def __init__(self, images):
|
||||
self.images = images
|
||||
self.request_output = SimpleNamespace(images=images)
|
||||
self.stage_durations = {}
|
||||
self.peak_memory_mb = 0.0
|
||||
|
||||
|
||||
class FakeAsyncOmni:
|
||||
@@ -117,20 +120,26 @@ class FakeAsyncOmni:
|
||||
|
||||
def __init__(self, images=None):
|
||||
self.stage_configs = [
|
||||
SimpleNamespace(stage_type="llm"),
|
||||
SimpleNamespace(stage_type="diffusion"),
|
||||
SimpleNamespace(stage_type="llm", is_comprehension=True),
|
||||
SimpleNamespace(stage_type="diffusion", is_comprehension=False),
|
||||
]
|
||||
self.default_sampling_params_list = [SamplingParams(temperature=0.1), OmniDiffusionSamplingParams()]
|
||||
self.captured_sampling_params_list = None
|
||||
self.captured_prompt = None
|
||||
self._images = images or [Image.new("RGB", (64, 64), color="green")]
|
||||
|
||||
async def generate(self, prompt, request_id, sampling_params_list):
|
||||
self.captured_sampling_params_list = sampling_params_list
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None):
|
||||
if sampling_params_list is not None:
|
||||
self.captured_sampling_params_list = sampling_params_list
|
||||
else:
|
||||
self.captured_sampling_params_list = [sampling_params]
|
||||
self.captured_prompt = prompt
|
||||
images = [img.copy() for img in self._images]
|
||||
yield MockGenerationResult(images)
|
||||
|
||||
def __class_getitem__(cls, item):
|
||||
return cls
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_async_diffusion(mocker: MockerFixture):
|
||||
@@ -189,12 +198,49 @@ def async_omni_test_client():
|
||||
"""Create test client with mocked AsyncOmni engine."""
|
||||
from fastapi import FastAPI
|
||||
|
||||
from vllm_omni.entrypoints.async_omni import AsyncOmni
|
||||
from vllm_omni.entrypoints.openai.api_server import router
|
||||
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
|
||||
|
||||
class FakeAsyncOmniClass(AsyncOmni):
|
||||
def __init__(self):
|
||||
self.stage_configs = [
|
||||
SimpleNamespace(stage_type="llm", is_comprehension=True),
|
||||
SimpleNamespace(stage_type="diffusion", is_comprehension=False),
|
||||
]
|
||||
self.default_sampling_params_list = [
|
||||
SamplingParams(temperature=0.1),
|
||||
OmniDiffusionSamplingParams(),
|
||||
]
|
||||
self.captured_sampling_params_list = None
|
||||
self.captured_prompt = None
|
||||
self._images = [Image.new("RGB", (64, 64), color="green")]
|
||||
self.od_config = SimpleNamespace(supports_multimodal_inputs=True)
|
||||
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None):
|
||||
if sampling_params_list is not None:
|
||||
self.captured_sampling_params_list = sampling_params_list
|
||||
else:
|
||||
self.captured_sampling_params_list = [sampling_params]
|
||||
self.captured_prompt = prompt
|
||||
images = [img.copy() for img in self._images]
|
||||
yield MockGenerationResult(images)
|
||||
|
||||
def __class_getitem__(cls, item):
|
||||
return cls
|
||||
|
||||
def get_diffusion_od_config(self):
|
||||
return self.od_config
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
|
||||
app.state.engine_client = FakeAsyncOmni()
|
||||
engine = FakeAsyncOmniClass()
|
||||
chat_handler = object.__new__(OmniOpenAIServingChat)
|
||||
chat_handler.engine_client = engine
|
||||
chat_handler._diffusion_engine = None
|
||||
app.state.openai_serving_chat = chat_handler
|
||||
app.state.engine_client = engine
|
||||
app.state.stage_configs = [
|
||||
SimpleNamespace(stage_type="llm"),
|
||||
SimpleNamespace(stage_type="diffusion"),
|
||||
@@ -211,12 +257,49 @@ def async_omni_rgba_test_client():
|
||||
"""Create test client with mocked AsyncOmni engine returning RGBA output."""
|
||||
from fastapi import FastAPI
|
||||
|
||||
from vllm_omni.entrypoints.async_omni import AsyncOmni
|
||||
from vllm_omni.entrypoints.openai.api_server import router
|
||||
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
|
||||
|
||||
class FakeAsyncOmniClass(AsyncOmni):
|
||||
def __init__(self):
|
||||
self.stage_configs = [
|
||||
SimpleNamespace(stage_type="llm", is_comprehension=True),
|
||||
SimpleNamespace(stage_type="diffusion", is_comprehension=False),
|
||||
]
|
||||
self.default_sampling_params_list = [
|
||||
SamplingParams(temperature=0.1),
|
||||
OmniDiffusionSamplingParams(),
|
||||
]
|
||||
self.captured_sampling_params_list = None
|
||||
self.captured_prompt = None
|
||||
self._images = [Image.new("RGBA", (64, 64), color=(0, 255, 0, 128))]
|
||||
self.od_config = SimpleNamespace(supports_multimodal_inputs=True)
|
||||
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None):
|
||||
if sampling_params_list is not None:
|
||||
self.captured_sampling_params_list = sampling_params_list
|
||||
else:
|
||||
self.captured_sampling_params_list = [sampling_params]
|
||||
self.captured_prompt = prompt
|
||||
images = [img.copy() for img in self._images]
|
||||
yield MockGenerationResult(images)
|
||||
|
||||
def __class_getitem__(cls, item):
|
||||
return cls
|
||||
|
||||
def get_diffusion_od_config(self):
|
||||
return self.od_config
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
|
||||
app.state.engine_client = FakeAsyncOmni(images=[Image.new("RGBA", (64, 64), color=(0, 255, 0, 128))])
|
||||
engine = FakeAsyncOmniClass()
|
||||
chat_handler = object.__new__(OmniOpenAIServingChat)
|
||||
chat_handler.engine_client = engine
|
||||
chat_handler._diffusion_engine = None
|
||||
app.state.openai_serving_chat = chat_handler
|
||||
app.state.engine_client = engine
|
||||
app.state.stage_configs = [
|
||||
SimpleNamespace(stage_type="llm"),
|
||||
SimpleNamespace(stage_type="diffusion"),
|
||||
@@ -233,16 +316,50 @@ def async_omni_stage_configs_only_client():
|
||||
"""Create test client with refactored AsyncOmni compatibility surface only."""
|
||||
from fastapi import FastAPI
|
||||
|
||||
from vllm_omni.entrypoints.async_omni import AsyncOmni
|
||||
from vllm_omni.entrypoints.openai.api_server import router
|
||||
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
|
||||
|
||||
class FakeAsyncOmniClass(AsyncOmni):
|
||||
def __init__(self):
|
||||
self.stage_configs = [
|
||||
SimpleNamespace(stage_type="llm", is_comprehension=True),
|
||||
SimpleNamespace(stage_type="diffusion", is_comprehension=False),
|
||||
]
|
||||
self.default_sampling_params_list = [
|
||||
SamplingParams(temperature=0.1),
|
||||
OmniDiffusionSamplingParams(),
|
||||
]
|
||||
self.captured_sampling_params_list = None
|
||||
self.captured_prompt = None
|
||||
self._images = [Image.new("RGB", (64, 64), color="green")]
|
||||
self.od_config = SimpleNamespace(supports_multimodal_inputs=True)
|
||||
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None):
|
||||
if sampling_params_list is not None:
|
||||
self.captured_sampling_params_list = sampling_params_list
|
||||
else:
|
||||
self.captured_sampling_params_list = [sampling_params]
|
||||
self.captured_prompt = prompt
|
||||
images = [img.copy() for img in self._images]
|
||||
yield MockGenerationResult(images)
|
||||
|
||||
def __class_getitem__(cls, item):
|
||||
return cls
|
||||
|
||||
def get_diffusion_od_config(self):
|
||||
return self.od_config
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
|
||||
engine = FakeAsyncOmni()
|
||||
engine = FakeAsyncOmniClass()
|
||||
assert not hasattr(engine, "stage_list")
|
||||
app.state.engine_client = engine
|
||||
# Intentionally do not populate app.state.stage_configs. Refactored
|
||||
# AsyncOmni exposes stage_configs on the engine instance.
|
||||
chat_handler = object.__new__(OmniOpenAIServingChat)
|
||||
chat_handler.engine_client = engine
|
||||
chat_handler._diffusion_engine = None
|
||||
app.state.openai_serving_chat = chat_handler
|
||||
app.state.args = Namespace(
|
||||
default_sampling_params='{"1": {"num_inference_steps":4, "guidance_scale":7.5, "generator_device":"cpu"}}',
|
||||
max_generated_image_size=1024 * 1792,
|
||||
@@ -306,6 +423,9 @@ def test_models_endpoint_no_engine():
|
||||
|
||||
def test_generate_single_image(test_client):
|
||||
"""Test generating a single image"""
|
||||
# Single-stage path should not require openai_serving_chat.
|
||||
assert not hasattr(test_client.app.state, "openai_serving_chat")
|
||||
|
||||
response = test_client.post(
|
||||
"/v1/images/generations",
|
||||
json={
|
||||
@@ -374,6 +494,43 @@ def test_generate_images_async_omni_stage_configs_only(async_omni_stage_configs_
|
||||
assert captured[1].seed == 11
|
||||
|
||||
|
||||
def test_multistage_images_async_omni_construction(async_omni_test_client):
|
||||
"""Regression: multistage image generation builds the expected chat-style payload."""
|
||||
response = async_omni_test_client.post(
|
||||
"/v1/images/generations",
|
||||
json={
|
||||
"prompt": "a cat",
|
||||
"n": 2,
|
||||
"size": "128x256",
|
||||
"seed": 7,
|
||||
"num_inference_steps": 12,
|
||||
"guidance_scale": 6.5,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
engine = async_omni_test_client.app.state.engine_client
|
||||
captured_prompt = engine.captured_prompt
|
||||
assert captured_prompt["prompt"] == "a cat"
|
||||
assert captured_prompt["modalities"] == ["image"]
|
||||
assert captured_prompt["mm_processor_kwargs"] == {
|
||||
"target_h": 256,
|
||||
"target_w": 128,
|
||||
}
|
||||
|
||||
captured = engine.captured_sampling_params_list
|
||||
assert captured is not None
|
||||
assert len(captured) == 2
|
||||
assert captured[0].temperature == 0.1
|
||||
assert captured[0].seed == 7
|
||||
assert captured[1].num_outputs_per_prompt == 2
|
||||
assert captured[1].width == 128
|
||||
assert captured[1].height == 256
|
||||
assert captured[1].seed == 7
|
||||
assert captured[1].num_inference_steps == 12
|
||||
assert captured[1].guidance_scale == 6.5
|
||||
|
||||
|
||||
def test_image_edits_async_omni_stage_configs_only(async_omni_stage_configs_only_client):
|
||||
"""Regression: image edits accepts refactored AsyncOmni without stage_list."""
|
||||
img_bytes = make_test_image_bytes((16, 16))
|
||||
|
||||
Reference in new issue
Block a user