[Refactor] Normalize diffusion request extra arguments in Chat serving (#5171)
Signed-off-by: TaffyOfficial <2324465096@qq.com> Co-authored-by: TaffyOfficial <2324465096@qq.com>
This commit is contained in:
1 parent
1d6c0816a4
commit
0c88f8a0b6
7 files changed
+669
-176
No files matched your search
@@ -0,0 +1,78 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from vllm_omni.diffusion.models.bagel.pipeline_bagel import BagelPipeline
|
||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
class _NegativePromptPreparedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("prompt_negative", "extra_negative", "expected"),
|
||||
[
|
||||
("prompt negative", "legacy negative", "prompt negative"),
|
||||
(None, "legacy negative", "legacy negative"),
|
||||
],
|
||||
ids=["prompt-owner", "legacy-extra-args"],
|
||||
)
|
||||
def test_forward_reads_negative_prompt_from_prompt_with_legacy_fallback(
|
||||
mocker: MockerFixture,
|
||||
prompt_negative: str | None,
|
||||
extra_negative: str,
|
||||
expected: str,
|
||||
) -> None:
|
||||
pipeline = object.__new__(BagelPipeline)
|
||||
torch.nn.Module.__init__(pipeline)
|
||||
pipeline.device = torch.device("cpu")
|
||||
pipeline.od_config = mocker.Mock(dtype=torch.float32)
|
||||
pipeline.tokenizer = mocker.sentinel.tokenizer
|
||||
pipeline.new_token_ids = {}
|
||||
pipeline.language_model = mocker.Mock(vocab_size=10)
|
||||
pipeline.image_processor = None
|
||||
pipeline.vae = None
|
||||
|
||||
bagel = mocker.MagicMock()
|
||||
bagel.max_latent_size = 32
|
||||
bagel.latent_downsample = 8
|
||||
bagel.config.llm_config.num_hidden_layers = 1
|
||||
prepared_prompts: list[str] = []
|
||||
|
||||
def prepare_prompts(*, prompts, **_):
|
||||
prepared_prompts.append(prompts[0])
|
||||
if len(prepared_prompts) == 1:
|
||||
return {"packed_text_ids": torch.tensor([1])}, [1], [1]
|
||||
raise _NegativePromptPreparedError
|
||||
|
||||
bagel.prepare_prompts.side_effect = prepare_prompts
|
||||
pipeline.bagel = bagel
|
||||
|
||||
prompt = {
|
||||
"prompt": "positive",
|
||||
"negative_prompt": prompt_negative,
|
||||
}
|
||||
request = DiffusionRequestBatch(
|
||||
requests=[
|
||||
OmniDiffusionRequest(
|
||||
prompt=prompt,
|
||||
sampling_params=OmniDiffusionSamplingParams(
|
||||
extra_args={"negative_prompt": extra_negative},
|
||||
),
|
||||
request_id="test",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
with pytest.raises(_NegativePromptPreparedError):
|
||||
pipeline.forward(request)
|
||||
|
||||
assert prepared_prompts == ["positive", expected]
|
||||
@@ -35,7 +35,7 @@ from vllm.outputs import CompletionOutput, RequestOutput
|
||||
|
||||
from vllm_omni.entrypoints.async_omni import AsyncOmni as RealAsyncOmni
|
||||
from vllm_omni.entrypoints.cli.serve import OmniServeCommand
|
||||
from vllm_omni.inputs.data import OmniSamplingParams
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniSamplingParams
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
|
||||
|
||||
@@ -417,7 +417,7 @@ def mock_async_omni(
|
||||
mock_instance.stage_configs = server_case.stage_configs
|
||||
mock_instance.output_modalities = _build_output_modalities(server_case.stage_configs)
|
||||
mock_instance.default_sampling_params_list = [
|
||||
SamplingParams() if _stage_type(stage) != "diffusion" else mocker.MagicMock()
|
||||
OmniDiffusionSamplingParams() if _stage_type(stage) == "diffusion" else SamplingParams()
|
||||
for stage in server_case.stage_configs
|
||||
]
|
||||
mock_instance.errored = False
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm_omni.entrypoints.openai.diffusion_request_utils import (
|
||||
apply_normalized_diffusion_request_extra_args,
|
||||
normalize_diffusion_request_args,
|
||||
)
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
REQUEST_SOURCES = (
|
||||
"root.declared",
|
||||
"root.extra_args",
|
||||
"root.extra_params",
|
||||
"nested.declared",
|
||||
"nested.extra_args",
|
||||
"nested.extra_params",
|
||||
)
|
||||
|
||||
|
||||
def _source_args(source: str, value: object) -> dict[str, object]:
|
||||
scope, container = source.split(".")
|
||||
payload = {"solver": value} if container == "declared" else {container: {"solver": value}}
|
||||
kwargs: dict[str, object] = {scope: payload}
|
||||
if container == "declared":
|
||||
kwargs["registered_extra_fields"] = {"solver"}
|
||||
return kwargs
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", REQUEST_SOURCES)
|
||||
@pytest.mark.parametrize(("value", "expected"), [("ddim", "ddim"), (None, "euler")])
|
||||
def test_normalizer_routes_values_without_erasing_defaults(
|
||||
source: str,
|
||||
value: object,
|
||||
expected: str,
|
||||
) -> None:
|
||||
normalized, _ = normalize_diffusion_request_args(**_source_args(source, value))
|
||||
sampling_params = OmniDiffusionSamplingParams(
|
||||
extra_args={"solver": "euler", "stage_default": True},
|
||||
)
|
||||
apply_normalized_diffusion_request_extra_args(sampling_params, normalized)
|
||||
|
||||
assert sampling_params.extra_args == {"solver": expected, "stage_default": True}
|
||||
|
||||
|
||||
def test_non_overlapping_legacy_and_canonical_values_are_merged(mocker) -> None:
|
||||
warning_once = mocker.patch("vllm_omni.entrypoints.openai.diffusion_request_utils.logger.warning_once")
|
||||
|
||||
normalized, _ = normalize_diffusion_request_args(
|
||||
root={"extra_args": {"cfg_text_scale": 7.0}},
|
||||
nested={"extra_params": {"sample_solver": "euler"}},
|
||||
)
|
||||
|
||||
assert normalized == {"cfg_text_scale": 7.0, "sample_solver": "euler"}
|
||||
warning_once.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"kwargs, expected_sources",
|
||||
[
|
||||
(
|
||||
{
|
||||
"root": {"seed": 1, "extra_args": {"seed": 2}},
|
||||
"serving_root_fields": {"seed"},
|
||||
},
|
||||
"request.seed, request.extra_args.seed",
|
||||
),
|
||||
(
|
||||
{
|
||||
"root": {"cfg_scale": 7.0},
|
||||
"nested": {"true_cfg_scale": 2.0},
|
||||
"serving_root_fields": {"true_cfg_scale"},
|
||||
"root_field_aliases": {"cfg_scale": "true_cfg_scale"},
|
||||
},
|
||||
"request.cfg_scale, request.extra_body.true_cfg_scale",
|
||||
),
|
||||
(
|
||||
{
|
||||
"root": {
|
||||
"extra_args": {"solver": "euler"},
|
||||
"extra_params": {"solver": "ddim"},
|
||||
}
|
||||
},
|
||||
"request.extra_args.solver, request.extra_params.solver",
|
||||
),
|
||||
(
|
||||
{
|
||||
"root": {"cfg_text_scale": 7.0},
|
||||
"nested": {"extra_args": {"cfg_text_scale": 8.0}},
|
||||
"registered_extra_fields": {"cfg_text_scale"},
|
||||
},
|
||||
"request.cfg_text_scale, request.extra_body.extra_args.cfg_text_scale",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_duplicate_sources_are_rejected(
|
||||
kwargs: dict[str, object],
|
||||
expected_sources: str,
|
||||
mocker,
|
||||
) -> None:
|
||||
warning_once = mocker.patch("vllm_omni.entrypoints.openai.diffusion_request_utils.logger.warning_once")
|
||||
|
||||
with pytest.raises(ValueError, match=expected_sources):
|
||||
normalize_diffusion_request_args(**kwargs)
|
||||
|
||||
warning_once.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("scope", "name"),
|
||||
(
|
||||
("root", "extra_args"),
|
||||
("root", "extra_params"),
|
||||
("nested", "extra_args"),
|
||||
("nested", "extra_params"),
|
||||
),
|
||||
)
|
||||
def test_public_containers_must_be_objects(scope: str, name: str) -> None:
|
||||
with pytest.raises(ValueError, match="must be a JSON object"):
|
||||
normalize_diffusion_request_args(**{scope: {name: ["not", "an", "object"]}})
|
||||
|
||||
|
||||
def test_container_keys_must_be_strings() -> None:
|
||||
with pytest.raises(ValueError, match="must be a JSON object with string keys"):
|
||||
normalize_diffusion_request_args(root={"extra_args": {1: "invalid"}})
|
||||
@@ -6,10 +6,12 @@ Tests that standard OpenAI API parameters (max_tokens, temperature, etc.)
|
||||
are correctly applied to the comprehension stage while preserving YAML defaults.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
|
||||
from vllm.sampling_params import SamplingParams
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
@@ -86,6 +88,233 @@ def serving_chat(mock_engine_client):
|
||||
return instance
|
||||
|
||||
|
||||
def test_serving_boundary_normalizes_declared_root_and_nested_extras(serving_chat):
|
||||
serving_chat._diffusion_extra_body_params = frozenset({"cfg_text_scale", "negative_prompt"})
|
||||
request = ChatCompletionRequest(
|
||||
model="test",
|
||||
messages=[],
|
||||
modalities=["image"],
|
||||
num_inference_steps=7,
|
||||
size="768x512",
|
||||
negative_prompt="avoid blur",
|
||||
lora={"name": "adapter"},
|
||||
cfg_text_scale=7.0,
|
||||
extra_body={"extra_args": {"sample_solver": "euler"}},
|
||||
)
|
||||
|
||||
normalized_extra_args, diffusion_request_args = serving_chat._normalize_diffusion_request_args(request)
|
||||
|
||||
assert normalized_extra_args == {
|
||||
"cfg_text_scale": 7.0,
|
||||
"sample_solver": "euler",
|
||||
}
|
||||
assert diffusion_request_args["cfg_text_scale"] == 7.0
|
||||
assert diffusion_request_args["num_inference_steps"] == 7
|
||||
assert diffusion_request_args["size"] == "768x512"
|
||||
assert diffusion_request_args["negative_prompt"] == "avoid blur"
|
||||
assert diffusion_request_args["lora"] == {"name": "adapter"}
|
||||
assert diffusion_request_args["modalities"] == ["image"]
|
||||
|
||||
|
||||
def test_unknown_root_extra_does_not_claim_canonical_extra(serving_chat):
|
||||
serving_chat._diffusion_extra_body_params = frozenset()
|
||||
request = ChatCompletionRequest(
|
||||
model="test",
|
||||
messages=[],
|
||||
pipeline_option="ignored-root-value",
|
||||
extra_args={"pipeline_option": "canonical-value"},
|
||||
)
|
||||
|
||||
normalized_extra_args, diffusion_request_args = serving_chat._normalize_diffusion_request_args(request)
|
||||
|
||||
assert normalized_extra_args == {
|
||||
"pipeline_option": "canonical-value",
|
||||
}
|
||||
assert "pipeline_option" not in diffusion_request_args
|
||||
|
||||
|
||||
def test_unregistered_cfg_scale_aliases_common_true_cfg_scale(serving_chat):
|
||||
serving_chat._diffusion_extra_body_params = frozenset()
|
||||
request = ChatCompletionRequest(
|
||||
model="test",
|
||||
messages=[],
|
||||
cfg_scale=7.0,
|
||||
)
|
||||
|
||||
normalized_extra_args, diffusion_request_args = serving_chat._normalize_diffusion_request_args(request)
|
||||
|
||||
assert normalized_extra_args == {}
|
||||
assert diffusion_request_args == {"true_cfg_scale": 7.0}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("registered", "request_kwargs"),
|
||||
[
|
||||
({"cfg_scale"}, {"cfg_scale": 7.0, "extra_args": {"cfg_scale": 8.0}}),
|
||||
(set(), {"cfg_scale": 7.0, "true_cfg_scale": 2.0}),
|
||||
],
|
||||
ids=["registered-model-extra", "unregistered-common-alias"],
|
||||
)
|
||||
def test_cfg_scale_owner_conflicts_are_rejected(serving_chat, registered, request_kwargs):
|
||||
serving_chat._diffusion_extra_body_params = frozenset(registered)
|
||||
request = ChatCompletionRequest(model="test", messages=[], **request_kwargs)
|
||||
|
||||
with pytest.raises(ValueError, match="provided more than once"):
|
||||
serving_chat._normalize_diffusion_request_args(request)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("diffusion_mode", [True, False], ids=["pure", "mixed"])
|
||||
def test_duplicate_extras_return_the_same_bad_request_before_dispatch(
|
||||
serving_chat,
|
||||
mocker: MockerFixture,
|
||||
diffusion_mode: bool,
|
||||
):
|
||||
serving_chat._diffusion_mode = diffusion_mode
|
||||
serving_chat._diffusion_extra_body_params = frozenset({"sample_solver"})
|
||||
serving_chat.engine_client.stage_configs = [SimpleNamespace(stage_type="diffusion")]
|
||||
check_model = mocker.patch.object(serving_chat, "_check_model", new=mocker.AsyncMock())
|
||||
pure_dispatch = mocker.patch.object(
|
||||
serving_chat,
|
||||
"_create_diffusion_chat_completion",
|
||||
new=mocker.AsyncMock(),
|
||||
)
|
||||
request = ChatCompletionRequest(
|
||||
model="test",
|
||||
messages=[],
|
||||
sample_solver="euler",
|
||||
extra_args={"sample_solver": "ddim"},
|
||||
)
|
||||
|
||||
response = asyncio.run(serving_chat._create_chat_completion(request))
|
||||
|
||||
assert response.error.code == 400
|
||||
assert response.error.message == (
|
||||
'Diffusion request parameters were provided more than once: "sample_solver": '
|
||||
"request.sample_solver, request.extra_args.sample_solver."
|
||||
)
|
||||
check_model.assert_not_awaited()
|
||||
pure_dispatch.assert_not_awaited()
|
||||
|
||||
|
||||
def test_pure_consumer_preserves_defaults_and_separate_cfg_owners(serving_chat, mocker: MockerFixture):
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def generate(**kwargs):
|
||||
captured.update(kwargs)
|
||||
if False:
|
||||
yield None
|
||||
|
||||
serving_chat._diffusion_model_name = "test"
|
||||
serving_chat._diffusion_engine = SimpleNamespace(
|
||||
stage_configs=[SimpleNamespace(stage_type="diffusion")],
|
||||
default_sampling_params_list=[
|
||||
OmniDiffusionSamplingParams(extra_args={"solver": "euler", "stage_default": True}),
|
||||
],
|
||||
generate=generate,
|
||||
)
|
||||
serving_chat._diffusion_mode = True
|
||||
serving_chat._diffusion_extra_body_params = frozenset({"cfg_scale"})
|
||||
serving_chat._extract_diffusion_prompt_and_media = mocker.Mock(return_value=("prompt", [], [], []))
|
||||
request = ChatCompletionRequest(
|
||||
model="test",
|
||||
messages=[],
|
||||
modalities=["image"],
|
||||
num_inference_steps=7,
|
||||
size="768x512",
|
||||
negative_prompt="avoid blur",
|
||||
cfg_scale=7.0,
|
||||
true_cfg_scale=2.0,
|
||||
extra_body={"extra_args": {"solver": "ddim"}},
|
||||
)
|
||||
|
||||
response = asyncio.run(serving_chat._create_chat_completion(request))
|
||||
|
||||
(sampling_params,) = captured["sampling_params_list"]
|
||||
assert sampling_params.extra_args == {
|
||||
"cfg_scale": 7.0,
|
||||
"solver": "ddim",
|
||||
"stage_default": True,
|
||||
}
|
||||
assert sampling_params.true_cfg_scale == 2.0
|
||||
assert sampling_params.num_inference_steps == 7
|
||||
assert (sampling_params.height, sampling_params.width) == (512, 768)
|
||||
assert captured["prompt"]["negative_prompt"] == "avoid blur"
|
||||
assert response.error.message == "No output generated from AsyncOmni"
|
||||
|
||||
|
||||
def test_mixed_consumer_keeps_root_common_args_with_nested_extras(serving_chat, mocker: MockerFixture):
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
serving_chat.engine_client.stage_configs = [
|
||||
SimpleNamespace(stage_type="llm", is_comprehension=True),
|
||||
SimpleNamespace(stage_type="diffusion", is_comprehension=False),
|
||||
]
|
||||
serving_chat.engine_client.default_sampling_params_list = [
|
||||
SamplingParams(),
|
||||
OmniDiffusionSamplingParams(),
|
||||
]
|
||||
serving_chat.engine_client.output_modalities = ["image"]
|
||||
serving_chat.engine_client.errored = False
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def results():
|
||||
if False:
|
||||
yield None
|
||||
|
||||
def generate(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return results()
|
||||
|
||||
serving_chat.engine_client.generate = generate
|
||||
serving_chat.__dict__.update(
|
||||
_diffusion_mode=False,
|
||||
_diffusion_extra_body_params=frozenset(),
|
||||
models=SimpleNamespace(model_name=lambda _: "test"),
|
||||
renderer=SimpleNamespace(get_tokenizer=lambda: object()),
|
||||
online_renderer=SimpleNamespace(validate_chat_template=lambda **_: None),
|
||||
parser_cls=None,
|
||||
use_harmony=False,
|
||||
enable_auto_tools=False,
|
||||
exclude_tools_when_tool_choice_none=False,
|
||||
trust_request_chat_template=True,
|
||||
chat_template=None,
|
||||
chat_template_content_format="auto",
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
serving_chat,
|
||||
_check_model=mocker.AsyncMock(return_value=None),
|
||||
_maybe_get_adapters=mocker.Mock(return_value=None),
|
||||
_effective_chat_template_kwargs=mocker.Mock(return_value={}),
|
||||
_preprocess_chat=mocker.AsyncMock(return_value=([], [{"prompt": "raw"}])),
|
||||
_base_request_id=mocker.Mock(return_value="test"),
|
||||
_extract_diffusion_prompt_and_images_from_messages=mocker.Mock(return_value=("prompt", [])),
|
||||
_log_inputs=mocker.Mock(),
|
||||
chat_completion_full_generator=mocker.AsyncMock(return_value="done"),
|
||||
)
|
||||
request = ChatCompletionRequest(
|
||||
model="test",
|
||||
messages=[{"role": "user", "content": "prompt"}],
|
||||
modalities=["image"],
|
||||
num_inference_steps=7,
|
||||
size="768x512",
|
||||
negative_prompt="avoid blur",
|
||||
extra_body={"extra_args": {"solver": "euler"}},
|
||||
)
|
||||
|
||||
assert asyncio.run(serving_chat._create_chat_completion(request)) == "done"
|
||||
|
||||
sampling_params_list = captured["sampling_params_list"]
|
||||
assert sampling_params_list[0].extra_args == {"target_h": 512, "target_w": 768}
|
||||
diffusion_params = sampling_params_list[1]
|
||||
assert diffusion_params.num_inference_steps == 7
|
||||
assert (diffusion_params.height, diffusion_params.width) == (512, 768)
|
||||
assert diffusion_params.extra_args == {"solver": "euler"}
|
||||
assert captured["prompt"]["negative_prompt"] == "avoid blur"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_request(mocker: MockerFixture):
|
||||
"""Create a mock request with all OpenAI sampling params set to None."""
|
||||
@@ -616,124 +845,3 @@ class TestResolveHeightWidth:
|
||||
h, w = OmniOpenAIServingChat._resolve_height_width_from_extra_body({"size": "invalid"})
|
||||
assert h is None
|
||||
assert w is None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tests for _apply_request_overrides with GLM-Image (target_h/w injection)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestApplyRequestOverridesGLMImage:
|
||||
"""Test target_h/w injection for GLM-Image AR stage.
|
||||
|
||||
max_tokens is NOT computed dynamically — it comes from the deploy YAML
|
||||
default (e.g. 4353). _apply_request_overrides only injects target_h/w
|
||||
into extra_args so the model can build M-RoPE position grids.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def glm_serving_chat(self, mock_engine_client, mocker: MockerFixture):
|
||||
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
|
||||
|
||||
instance = object.__new__(OmniOpenAIServingChat)
|
||||
instance.engine_client = mock_engine_client
|
||||
instance._extract_diffusion_prompt_and_images_from_messages = mocker.MagicMock(return_value=("a cat", []))
|
||||
return instance
|
||||
|
||||
@pytest.fixture
|
||||
def glm_request(self, mocker: MockerFixture):
|
||||
req = mocker.MagicMock()
|
||||
req.temperature = None
|
||||
req.top_p = None
|
||||
req.top_k = None
|
||||
req.max_tokens = None
|
||||
req.min_tokens = None
|
||||
req.seed = None
|
||||
req.ignore_eos = None
|
||||
req.stop = None
|
||||
req.stop_token_ids = None
|
||||
req.frequency_penalty = None
|
||||
req.presence_penalty = None
|
||||
req.extra_body = {"height": 1024, "width": 1024}
|
||||
req.model_fields_set = set()
|
||||
return req
|
||||
|
||||
def test_t2i_injects_target_h_w(self, glm_serving_chat, glm_request, default_comprehension_params):
|
||||
"""t2i mode: target_h/w injected into extra_args, max_tokens unchanged."""
|
||||
result = glm_serving_chat._apply_request_overrides(default_comprehension_params, glm_request)
|
||||
assert result.extra_args["target_h"] == 1024
|
||||
assert result.extra_args["target_w"] == 1024
|
||||
# max_tokens stays at YAML default (not dynamically computed)
|
||||
assert result.max_tokens == 4353
|
||||
|
||||
def test_i2i_injects_target_h_w(
|
||||
self, glm_serving_chat, glm_request, default_comprehension_params, mocker: MockerFixture
|
||||
):
|
||||
"""i2i mode: target_h/w injected, max_tokens unchanged."""
|
||||
glm_serving_chat._extract_diffusion_prompt_and_images_from_messages = mocker.MagicMock(
|
||||
return_value=("edit this", ["fake_image"])
|
||||
)
|
||||
result = glm_serving_chat._apply_request_overrides(default_comprehension_params, glm_request)
|
||||
assert result.extra_args["target_h"] == 1024
|
||||
assert result.extra_args["target_w"] == 1024
|
||||
# max_tokens stays at YAML default regardless of t2i/i2i
|
||||
assert result.max_tokens == 4353
|
||||
|
||||
def test_user_max_tokens_preserved(self, glm_serving_chat, glm_request, default_comprehension_params):
|
||||
"""User-provided max_tokens is respected (not overridden by dynamic computation)."""
|
||||
glm_request.max_tokens = 500
|
||||
glm_request.model_fields_set = {"max_tokens"}
|
||||
|
||||
result = glm_serving_chat._apply_request_overrides(default_comprehension_params, glm_request)
|
||||
assert result.max_tokens == 500
|
||||
assert result.extra_args["target_h"] == 1024
|
||||
assert result.extra_args["target_w"] == 1024
|
||||
|
||||
def test_no_height_width_preserves_default(
|
||||
self, glm_serving_chat, mocker: MockerFixture, default_comprehension_params
|
||||
):
|
||||
"""When no height/width in extra_body, keep YAML default max_tokens, no target_h/w."""
|
||||
req = mocker.MagicMock()
|
||||
req.temperature = None
|
||||
req.top_p = None
|
||||
req.top_k = None
|
||||
req.max_tokens = None
|
||||
req.min_tokens = None
|
||||
req.seed = None
|
||||
req.ignore_eos = None
|
||||
req.stop = None
|
||||
req.stop_token_ids = None
|
||||
req.frequency_penalty = None
|
||||
req.presence_penalty = None
|
||||
req.extra_body = {}
|
||||
req.model_fields_set = set()
|
||||
|
||||
result = glm_serving_chat._apply_request_overrides(default_comprehension_params, req)
|
||||
assert result.max_tokens == 4353 # YAML default
|
||||
# No target_h/w injected when dimensions not provided
|
||||
assert not result.extra_args or "target_h" not in (result.extra_args or {})
|
||||
|
||||
def test_size_string_parsed_for_glm_image(
|
||||
self, glm_serving_chat, mocker: MockerFixture, default_comprehension_params
|
||||
):
|
||||
"""'size' in extra_body is parsed as fallback for height/width."""
|
||||
req = mocker.MagicMock()
|
||||
req.temperature = None
|
||||
req.top_p = None
|
||||
req.top_k = None
|
||||
req.max_tokens = None
|
||||
req.min_tokens = None
|
||||
req.seed = None
|
||||
req.ignore_eos = None
|
||||
req.stop = None
|
||||
req.stop_token_ids = None
|
||||
req.frequency_penalty = None
|
||||
req.presence_penalty = None
|
||||
req.extra_body = {"size": "512x512"}
|
||||
req.model_fields_set = set()
|
||||
|
||||
result = glm_serving_chat._apply_request_overrides(default_comprehension_params, req)
|
||||
assert result.extra_args["target_h"] == 512
|
||||
assert result.extra_args["target_w"] == 512
|
||||
# max_tokens stays at YAML default (not dynamically computed)
|
||||
assert result.max_tokens == 4353
|
||||
@@ -610,7 +610,8 @@ class BagelPipeline(nn.Module, SupportsComponentDiscovery, DiffusionPipelineProf
|
||||
# cfg_text_context: update with negative prompt (no text condition).
|
||||
# When empty, keep cfg_text_context as-is (kv_lens=0) to match
|
||||
# original BAGEL.
|
||||
neg_prompt = extra_args.get("negative_prompt", "")
|
||||
prompt_negative = first_prompt.get("negative_prompt") if isinstance(first_prompt, dict) else None
|
||||
neg_prompt = prompt_negative if prompt_negative is not None else extra_args.get("negative_prompt", "")
|
||||
if neg_prompt:
|
||||
neg_input, neg_newlens, neg_rope = self.bagel.prepare_prompts(
|
||||
curr_kvlens=cfg_text_context["kv_lens"],
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Collection, Mapping
|
||||
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def _request_mapping(name: str, value: object | None) -> dict[str, object]:
|
||||
if value is None:
|
||||
return {}
|
||||
if not isinstance(value, Mapping):
|
||||
raise ValueError(f"{name} must be a JSON object.")
|
||||
if any(not isinstance(key, str) for key in value):
|
||||
raise ValueError(f"{name} must be a JSON object with string keys.")
|
||||
return dict(value)
|
||||
|
||||
|
||||
def normalize_diffusion_request_args(
|
||||
*,
|
||||
root: object | None = None,
|
||||
nested: object | None = None,
|
||||
explicit_root_args: object | None = None,
|
||||
serving_root_fields: Collection[str] = (),
|
||||
registered_extra_fields: Collection[str] = (),
|
||||
root_field_aliases: Mapping[str, str] | None = None,
|
||||
) -> tuple[dict[str, object], dict[str, object]]:
|
||||
"""Validate request sources and project diffusion consumer arguments."""
|
||||
root_args = _request_mapping("request", root)
|
||||
root_args.update(_request_mapping("explicit_root_args", explicit_root_args))
|
||||
nested_args = _request_mapping("request.extra_body", nested)
|
||||
|
||||
registered = set(registered_extra_fields)
|
||||
aliases = {alias: canonical for alias, canonical in (root_field_aliases or {}).items() if alias not in registered}
|
||||
serving_fields = set(serving_root_fields) | aliases.keys()
|
||||
declared_fields = registered - serving_fields
|
||||
consumer_fields = serving_fields | declared_fields
|
||||
|
||||
root_declared = {key: root_args[key] for key in declared_fields if key in root_args}
|
||||
nested_declared = {key: nested_args[key] for key in declared_fields if key in nested_args}
|
||||
canonical = _request_mapping("extra_args", root_args.get("extra_args"))
|
||||
legacy = _request_mapping("extra_params", root_args.get("extra_params"))
|
||||
nested_canonical = _request_mapping("extra_body.extra_args", nested_args.get("extra_args"))
|
||||
nested_legacy = _request_mapping("extra_body.extra_params", nested_args.get("extra_params"))
|
||||
|
||||
sources_by_key: dict[str, list[str]] = {}
|
||||
sources = (
|
||||
((root_args.keys() & serving_fields) | root_declared.keys(), "request"),
|
||||
((nested_args.keys() & serving_fields) | nested_declared.keys(), "request.extra_body"),
|
||||
(canonical, "request.extra_args"),
|
||||
(legacy, "request.extra_params"),
|
||||
(nested_canonical, "request.extra_body.extra_args"),
|
||||
(nested_legacy, "request.extra_body.extra_params"),
|
||||
)
|
||||
for keys, prefix in sources:
|
||||
for key in sorted(keys):
|
||||
sources_by_key.setdefault(aliases.get(key, key), []).append(f"{prefix}.{key}")
|
||||
|
||||
conflicts = {key: list(dict.fromkeys(paths)) for key, paths in sources_by_key.items() if len(set(paths)) > 1}
|
||||
if conflicts:
|
||||
details = "; ".join(f'"{key}": {", ".join(conflicts[key])}' for key in sorted(conflicts))
|
||||
raise ValueError(f"Diffusion request parameters were provided more than once: {details}.")
|
||||
|
||||
if root_args.get("extra_params") is not None or nested_args.get("extra_params") is not None:
|
||||
logger.warning_once(
|
||||
"extra_params is deprecated; use extra_args for model-specific diffusion request parameters."
|
||||
)
|
||||
|
||||
# Duplicate request keys were rejected above, so ordering is not precedence.
|
||||
normalized_extra_args = {
|
||||
**nested_declared,
|
||||
**root_declared,
|
||||
**legacy,
|
||||
**nested_legacy,
|
||||
**canonical,
|
||||
**nested_canonical,
|
||||
}
|
||||
normalized_extra_args = {key: value for key, value in normalized_extra_args.items() if value is not None}
|
||||
|
||||
request_args = {
|
||||
**{key: nested_args[key] for key in consumer_fields if key in nested_args},
|
||||
**{key: root_args[key] for key in consumer_fields if key in root_args},
|
||||
}
|
||||
for alias, canonical_name in aliases.items():
|
||||
if alias in request_args:
|
||||
request_args[canonical_name] = request_args.pop(alias)
|
||||
return normalized_extra_args, request_args
|
||||
|
||||
|
||||
def apply_normalized_diffusion_request_extra_args(
|
||||
sampling_params: OmniDiffusionSamplingParams,
|
||||
normalized_extra_args: Mapping[str, object],
|
||||
) -> None:
|
||||
"""Overlay request extras without discarding stage defaults."""
|
||||
if normalized_extra_args:
|
||||
sampling_params.extra_args = {
|
||||
**(sampling_params.extra_args or {}),
|
||||
**normalized_extra_args,
|
||||
}
|
||||
@@ -25,6 +25,10 @@ from vllm.entrypoints.chat_utils import (
|
||||
|
||||
from vllm_omni.diffusion.utils.param_utils import apply_declared_extra_args
|
||||
from vllm_omni.entrypoints.async_omni import AsyncOmni
|
||||
from vllm_omni.entrypoints.openai.diffusion_request_utils import (
|
||||
apply_normalized_diffusion_request_extra_args,
|
||||
normalize_diffusion_request_args,
|
||||
)
|
||||
from vllm_omni.entrypoints.openai.protocol.chat_completion import OmniChatCompletionResponse
|
||||
from vllm_omni.entrypoints.utils import coerce_param_message_types
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt
|
||||
@@ -33,7 +37,10 @@ from vllm_omni.metrics.modality import (
|
||||
observe_audio_first_packet,
|
||||
observe_audio_streaming_finalize,
|
||||
)
|
||||
from vllm_omni.model_extras import get_extra_body_params, get_extra_output_params
|
||||
from vllm_omni.model_extras import (
|
||||
get_extra_body_params,
|
||||
get_extra_output_params,
|
||||
)
|
||||
|
||||
try:
|
||||
import soundfile
|
||||
@@ -95,7 +102,10 @@ from vllm.utils.collection_utils import as_list
|
||||
from vllm.v1.engine.exceptions import EngineDeadError
|
||||
|
||||
from vllm_omni.entrypoints.openai.audio_utils_mixin import AudioMixin
|
||||
from vllm_omni.entrypoints.openai.image_api_utils import encode_image_base64_with_compression, validate_layered_layers
|
||||
from vllm_omni.entrypoints.openai.image_api_utils import (
|
||||
encode_image_base64_with_compression,
|
||||
validate_layered_layers,
|
||||
)
|
||||
from vllm_omni.entrypoints.openai.protocol import OmniChatCompletionStreamResponse
|
||||
from vllm_omni.entrypoints.openai.protocol.audio import (
|
||||
DEFAULT_AUDIO_FORMAT,
|
||||
@@ -152,6 +162,33 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
_supported_speakers: set[str] | None = None
|
||||
_diffusion_extra_body_params: frozenset[str] | None = None
|
||||
_diffusion_extra_output_params: frozenset[str] | None = None
|
||||
# Common fields stay at the request root, but still participate in
|
||||
# conflicts with model-specific compatibility forms.
|
||||
_diffusion_common_root_fields = frozenset(
|
||||
{
|
||||
"height",
|
||||
"width",
|
||||
"num_outputs_per_prompt",
|
||||
"seed",
|
||||
"num_inference_steps",
|
||||
"guidance_scale",
|
||||
"true_cfg_scale",
|
||||
"num_frames",
|
||||
"guidance_scale_2",
|
||||
"layers",
|
||||
"resolution",
|
||||
}
|
||||
)
|
||||
_diffusion_existing_control_fields = frozenset(
|
||||
{
|
||||
"size",
|
||||
"negative_prompt",
|
||||
"lora",
|
||||
"modalities",
|
||||
}
|
||||
)
|
||||
_diffusion_root_field_aliases = {"cfg_scale": "true_cfg_scale"}
|
||||
_diffusion_serving_root_fields = _diffusion_common_root_fields | _diffusion_existing_control_fields
|
||||
|
||||
# Harmony flag (always False for vllm-omni models)
|
||||
use_harmony: bool = False
|
||||
@@ -291,6 +328,30 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
self._diffusion_extra_body_params = params
|
||||
return params
|
||||
|
||||
def _normalize_diffusion_request_args(
|
||||
self,
|
||||
request: ChatCompletionRequest,
|
||||
) -> tuple[dict[str, object], dict[str, Any]]:
|
||||
"""Normalize extras and preserve validated common root fields."""
|
||||
root = dict(request.model_extra or {})
|
||||
nested = root.pop("extra_body", None)
|
||||
if nested is None:
|
||||
nested = getattr(request, "extra_body", None)
|
||||
explicit = getattr(request, "model_fields_set", None)
|
||||
if explicit is None:
|
||||
explicit = getattr(request, "__fields_set__", ())
|
||||
explicit_root_args = {
|
||||
key: getattr(request, key) for key in explicit if key != "extra_body" and hasattr(request, key)
|
||||
}
|
||||
return normalize_diffusion_request_args(
|
||||
root=root,
|
||||
nested=nested,
|
||||
explicit_root_args=explicit_root_args,
|
||||
serving_root_fields=self._diffusion_serving_root_fields,
|
||||
registered_extra_fields=self._get_diffusion_extra_body_params(),
|
||||
root_field_aliases=self._diffusion_root_field_aliases,
|
||||
)
|
||||
|
||||
def _get_diffusion_extra_output_params(
|
||||
self,
|
||||
output: object,
|
||||
@@ -386,9 +447,26 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
request: ChatCompletionRequest,
|
||||
raw_request: Request | None = None,
|
||||
) -> AsyncGenerator[str, None] | ChatCompletionResponse | ErrorResponse:
|
||||
stage_configs = getattr(self.engine_client, "stage_configs", ()) or ()
|
||||
serves_diffusion = self._diffusion_mode or any(
|
||||
get_stage_type(stage_config) == "diffusion" for stage_config in stage_configs
|
||||
)
|
||||
normalized_extra_args: dict[str, object] = {}
|
||||
diffusion_request_args: dict[str, Any] = {}
|
||||
if serves_diffusion:
|
||||
try:
|
||||
normalized_extra_args, diffusion_request_args = self._normalize_diffusion_request_args(request)
|
||||
except ValueError as exc:
|
||||
return self._create_error_response(str(exc), status_code=400)
|
||||
|
||||
# Handle diffusion mode
|
||||
if self._diffusion_mode:
|
||||
return await self._create_diffusion_chat_completion(request, raw_request)
|
||||
return await self._create_diffusion_chat_completion(
|
||||
request,
|
||||
normalized_extra_args,
|
||||
diffusion_request_args,
|
||||
raw_request,
|
||||
)
|
||||
|
||||
request_timestamp = time.time()
|
||||
if raw_request is not None:
|
||||
@@ -530,7 +608,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
return audio_format_check
|
||||
|
||||
num_inference_steps = None
|
||||
extra_body: dict[str, Any] = {}
|
||||
extra_body = diffusion_request_args
|
||||
# Omni multistage image generation: Stage-0 (AR) should receive a clean
|
||||
# text prompt (and optional conditioning image/size) so the model's own
|
||||
# processor can construct the correct inputs.
|
||||
@@ -544,12 +622,6 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
if not extracted_prompt:
|
||||
return self.create_error_response("No text prompt found in messages")
|
||||
|
||||
# [NOTE] When sending request via openai client Python library,
|
||||
# `extra_body` is flattented and merged into the payload's root.
|
||||
# These extra fields are accessible via `model_extra` property (from Pydantic base class).
|
||||
# When sending raw request with curl, no flattening happens. Directly read the `extra_body` dict.
|
||||
extra_body = getattr(request, "extra_body", None) or request.model_extra or {}
|
||||
|
||||
height, width = self._resolve_height_width_from_extra_body(extra_body)
|
||||
|
||||
num_inference_steps = extra_body.get("num_inference_steps")
|
||||
@@ -668,15 +740,32 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
output_modalities,
|
||||
)
|
||||
|
||||
comprehension_idx = (
|
||||
self._get_comprehension_stage_index()
|
||||
if _image_gen_height is not None and _image_gen_width is not None
|
||||
else None
|
||||
)
|
||||
# Apply user-specified overrides to diffusion stage(s) for image generation
|
||||
for idx, sp in enumerate(sampling_params_list):
|
||||
if idx == comprehension_idx:
|
||||
extra_args = dict(getattr(sp, "extra_args", {}) or {})
|
||||
extra_args["target_h"] = int(_image_gen_height)
|
||||
extra_args["target_w"] = int(_image_gen_width)
|
||||
sp.extra_args = extra_args
|
||||
if hasattr(sp, "height") and _image_gen_height is not None:
|
||||
sp.height = _image_gen_height
|
||||
if hasattr(sp, "width") and _image_gen_width is not None:
|
||||
sp.width = _image_gen_width
|
||||
if hasattr(sp, "num_inference_steps") and num_inference_steps is not None:
|
||||
sp.num_inference_steps = num_inference_steps
|
||||
apply_declared_extra_args(sp, self._get_diffusion_extra_body_params(), extra_body)
|
||||
if isinstance(sp, OmniDiffusionSamplingParams):
|
||||
apply_normalized_diffusion_request_extra_args(sp, normalized_extra_args)
|
||||
else:
|
||||
apply_declared_extra_args(
|
||||
sp,
|
||||
self._get_diffusion_extra_body_params(),
|
||||
extra_body,
|
||||
)
|
||||
|
||||
self._log_inputs(
|
||||
request_id,
|
||||
@@ -1166,8 +1255,6 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
Starts with YAML defaults and only overrides fields that the user
|
||||
explicitly provided (non-None values) in the request.
|
||||
|
||||
For models needing spatial metadata (e.g. GLM-Image), target_h/w is
|
||||
injected into extra_args so the runner can build M-RoPE position grids.
|
||||
max_tokens is NOT computed dynamically — it uses the deploy YAML default.
|
||||
|
||||
Args:
|
||||
@@ -1193,19 +1280,6 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
if (value is not None and not isinstance(value, list)) or (isinstance(value, list) and len(value) > 0):
|
||||
setattr(params, field_name, value)
|
||||
|
||||
# For GLM-Image: compute max_tokens from height/width with mode-aware
|
||||
# budgeting (t2i vs i2i).
|
||||
extra_body = getattr(request, "extra_body", {}) or {}
|
||||
height, width = self._resolve_height_width_from_extra_body(extra_body)
|
||||
|
||||
if height is not None and width is not None:
|
||||
# Keep target size in stage-0 sampling params so runner/model can
|
||||
# build deterministic M-RoPE grids for t2i (no MM features).
|
||||
extra_args = dict(getattr(params, "extra_args", {}) or {})
|
||||
extra_args["target_h"] = int(height)
|
||||
extra_args["target_w"] = int(width)
|
||||
params.extra_args = extra_args
|
||||
|
||||
return params
|
||||
|
||||
@staticmethod
|
||||
@@ -3366,12 +3440,16 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
async def _create_diffusion_chat_completion(
|
||||
self,
|
||||
request: ChatCompletionRequest,
|
||||
normalized_extra_args: dict[str, object],
|
||||
diffusion_request_args: dict[str, Any],
|
||||
raw_request: Request | None = None,
|
||||
) -> ChatCompletionResponse | ErrorResponse:
|
||||
"""Generate images via chat completion interface for diffusion models.
|
||||
|
||||
Args:
|
||||
request: Chat completion request
|
||||
normalized_extra_args: Validated model-specific request arguments
|
||||
diffusion_request_args: Compatibility arguments plus validated common root fields
|
||||
raw_request: Raw FastAPI request object
|
||||
|
||||
Returns:
|
||||
@@ -3396,15 +3474,8 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
messages
|
||||
)
|
||||
|
||||
# Extract generation parameters from extra_body (preferred)
|
||||
# Reference: text_to_image.py and text_to_video.py for supported parameters
|
||||
# [NOTE] When sending request via openai client Python library,
|
||||
# `extra_body` is flattented and merged into the payload's root.
|
||||
# These extra fields are accessible via `model_extra` property (from Pydantic base class).
|
||||
# When sending raw request with curl, no flattening happens. Directly read the `extra_body` dict.
|
||||
extra_body = getattr(request, "extra_body", None)
|
||||
if not extra_body:
|
||||
extra_body = request.model_extra or {}
|
||||
extra_body = diffusion_request_args
|
||||
|
||||
# Parse size if provided (supports "1024x1024" format)
|
||||
height, width = self._resolve_height_width_from_extra_body(extra_body)
|
||||
@@ -3415,7 +3486,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
# not provide a value.
|
||||
num_inference_steps = extra_body.get("num_inference_steps")
|
||||
guidance_scale = extra_body.get("guidance_scale")
|
||||
true_cfg_scale = extra_body.get("true_cfg_scale") or extra_body.get("cfg_scale")
|
||||
true_cfg_scale = extra_body.get("true_cfg_scale")
|
||||
seed = extra_body.get("seed")
|
||||
if seed is None:
|
||||
seed = getattr(request, "seed", None)
|
||||
@@ -3430,18 +3501,17 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
# Qwen-Image-Layered parameters
|
||||
layers = extra_body.get("layers")
|
||||
resolution = extra_body.get("resolution")
|
||||
|
||||
try:
|
||||
layers = validate_layered_layers(layers)
|
||||
except ValueError as e:
|
||||
return self._create_error_response(str(e), status_code=400)
|
||||
except ValueError as exc:
|
||||
return self._create_error_response(str(exc), status_code=400)
|
||||
|
||||
logger.info(
|
||||
"Diffusion chat request %s: prompt=%r, ref_images=%d, params=%s",
|
||||
request_id,
|
||||
prompt[:50] + "..." if len(prompt) > 50 else prompt,
|
||||
len(reference_images),
|
||||
{k: v for k, v in extra_body.items() if v is not None},
|
||||
{key: value for key, value in extra_body.items() if value is not None},
|
||||
)
|
||||
|
||||
# Decode reference images if provided
|
||||
@@ -3473,7 +3543,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
gen_params.guidance_scale = guidance_scale
|
||||
if true_cfg_scale is not None:
|
||||
gen_params.true_cfg_scale = true_cfg_scale
|
||||
apply_declared_extra_args(gen_params, self._get_diffusion_extra_body_params(), extra_body)
|
||||
apply_normalized_diffusion_request_extra_args(gen_params, normalized_extra_args)
|
||||
if num_frames is not None:
|
||||
gen_params.num_frames = num_frames
|
||||
if guidance_scale_2 is not None:
|
||||
@@ -3483,14 +3553,6 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
if resolution is not None:
|
||||
gen_params.resolution = resolution
|
||||
|
||||
# Pipeline-agnostic escape hatch (mirrors ``extra_params`` on the /v1/videos
|
||||
# endpoint in ``serving_video.py``): a single reserved ``extra_args`` dict in
|
||||
# ``extra_body`` flows straight into ``gen_params.extra_args``, with no keys
|
||||
# hardcoded here.
|
||||
extra_args_body = extra_body.get("extra_args")
|
||||
if isinstance(extra_args_body, dict):
|
||||
gen_params.extra_args.update(extra_args_body)
|
||||
|
||||
# Parse per-request LoRA.
|
||||
if lora_body and isinstance(lora_body, dict):
|
||||
try:
|
||||
@@ -3499,8 +3561,8 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
gen_params.lora_request = lora_req
|
||||
if lora_scale is not None:
|
||||
gen_params.lora_scale = lora_scale
|
||||
except Exception as e: # pragma: no cover - safeguard
|
||||
logger.warning("Failed to parse LoRA request: %s", e)
|
||||
except Exception as exc: # pragma: no cover - safeguard
|
||||
logger.warning("Failed to parse LoRA request: %s", exc)
|
||||
|
||||
# Route text modality for single-stage diffusion (img2text / text2text)
|
||||
requested_modalities = extra_body.get("modalities") or []
|
||||
@@ -3539,13 +3601,22 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
# Generate image or audio (e.g. AudioX) via AsyncOmni
|
||||
diffusion_engine = cast(AsyncOmni, self._diffusion_engine)
|
||||
stage_configs = list(getattr(diffusion_engine, "stage_configs", []) or [])
|
||||
default_sampling_params_list = get_default_sampling_params_list(diffusion_engine)
|
||||
sampling_params_list = build_stage_sampling_params_list(
|
||||
stage_configs,
|
||||
get_default_sampling_params_list(diffusion_engine),
|
||||
default_sampling_params_list,
|
||||
diffusion_params=gen_params,
|
||||
replace_diffusion_params=True,
|
||||
)
|
||||
|
||||
for index, (stage_config, sampling_params) in enumerate(zip(stage_configs, sampling_params_list)):
|
||||
if get_stage_type(stage_config) != "diffusion" or index >= len(default_sampling_params_list):
|
||||
continue
|
||||
default_extra_args = getattr(default_sampling_params_list[index], "extra_args", None)
|
||||
if default_extra_args:
|
||||
sampling_params.extra_args = {
|
||||
**default_extra_args,
|
||||
**(getattr(sampling_params, "extra_args", None) or {}),
|
||||
}
|
||||
if not sampling_params_list:
|
||||
sampling_params_list = [gen_params]
|
||||
|
||||
|
||||
Reference in new issue
Block a user