Signed-off-by: lishunyang <lishunyang12@163.com> Signed-off-by: xiaohajiayou <923390377@qq.com> Co-authored-by: lishunyang <lishunyang12@163.com>
996 lines
33 KiB
Python
996 lines
33 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import queue
|
|
from collections.abc import Callable
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from vllm.entrypoints.openai.models.protocol import BaseModelPath
|
|
from vllm.entrypoints.openai.models.serving import OpenAIServingModels
|
|
from vllm.sampling_params import RequestOutputKind, SamplingParams
|
|
from vllm.v1.engine.exceptions import EngineDeadError, EngineGenerateError
|
|
|
|
from vllm_omni.entrypoints.async_omni import AsyncOmni
|
|
from vllm_omni.entrypoints.omni import Omni
|
|
from vllm_omni.entrypoints.omni_base import OmniEngineDeadError
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
|
|
|
|
THREE_STAGE_META = [
|
|
{"stage_type": "llm", "final_output": True, "final_output_type": "text"},
|
|
{"stage_type": "llm", "final_output": False, "final_output_type": None},
|
|
{"stage_type": "diffusion", "final_output": True, "final_output_type": "image"},
|
|
]
|
|
|
|
DIFFUSION_ONLY_META = [
|
|
{"stage_type": "diffusion", "final_output": True, "final_output_type": "image"},
|
|
]
|
|
|
|
LLM_DIFFUSION_META = [
|
|
{"stage_type": "llm", "final_output": True, "final_output_type": "text"},
|
|
{"stage_type": "diffusion", "final_output": True, "final_output_type": "image"},
|
|
]
|
|
|
|
|
|
class FakeEngineOutput:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
payload: str,
|
|
finished: bool,
|
|
images: list[str] | None = None,
|
|
stage_durations: dict[str, float] | None = None,
|
|
) -> None:
|
|
self.payload = payload
|
|
self.finished = finished
|
|
self.images = images or []
|
|
self.stage_durations = stage_durations or {}
|
|
|
|
|
|
def make_output_msg(
|
|
request_id: str,
|
|
stage_id: int,
|
|
*,
|
|
payload: str,
|
|
output_finished: bool,
|
|
finished: bool | None = None,
|
|
images: list[str] | None = None,
|
|
metrics: Any = None,
|
|
) -> dict[str, Any]:
|
|
if finished is None:
|
|
finished = output_finished
|
|
return {
|
|
"type": "output",
|
|
"request_id": request_id,
|
|
"stage_id": stage_id,
|
|
"engine_outputs": FakeEngineOutput(
|
|
payload=payload,
|
|
finished=output_finished,
|
|
images=images,
|
|
),
|
|
"finished": finished,
|
|
"metrics": metrics,
|
|
}
|
|
|
|
|
|
class FakeAsyncOmniEngine:
|
|
def __init__(
|
|
self,
|
|
model: str = "dummy-model",
|
|
*,
|
|
stage_metadata: list[dict[str, Any]] | None = None,
|
|
default_sampling_params_list: list[Any] | None = None,
|
|
on_add_request: Callable[[FakeAsyncOmniEngine, dict[str, Any]], None] | None = None,
|
|
rpc_results: list[Any] | None = None,
|
|
**_: Any,
|
|
) -> None:
|
|
self.model = model
|
|
self.config_path = None
|
|
self.stage_configs: list[Any] = []
|
|
self.stage_metadata = stage_metadata or [THREE_STAGE_META[-1]]
|
|
self.num_stages = len(self.stage_metadata)
|
|
self.default_sampling_params_list = default_sampling_params_list or [
|
|
SamplingParams(max_tokens=8) for _ in range(self.num_stages)
|
|
]
|
|
self.supported_tasks = ("generate",)
|
|
self.stage_clients = [SimpleNamespace(is_comprehension=False) for _ in range(self.num_stages)]
|
|
self.stage_vllm_configs = [None for _ in range(self.num_stages)]
|
|
self.output_processors = [SimpleNamespace(tokenizer=None) for _ in range(self.num_stages)]
|
|
self.input_processor = None
|
|
|
|
self.output_q: queue.Queue[dict[str, Any]] = queue.Queue()
|
|
self.submitted: list[dict[str, Any]] = []
|
|
self.aborted: list[list[str]] = []
|
|
self.rpc_results = rpc_results or []
|
|
self.on_add_request = on_add_request
|
|
self.shutdown_called = False
|
|
self._alive = True
|
|
|
|
def add_request(
|
|
self,
|
|
request_id: str,
|
|
prompt: Any,
|
|
sampling_params_list: list[Any] | None = None,
|
|
final_stage_id: int = 0,
|
|
arrival_time: float | None = None,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
msg = {
|
|
"request_id": request_id,
|
|
"prompt": prompt,
|
|
"sampling_params_list": sampling_params_list,
|
|
"final_stage_id": final_stage_id,
|
|
"arrival_time": arrival_time,
|
|
}
|
|
self.submitted.append(msg)
|
|
if self.on_add_request is not None:
|
|
self.on_add_request(self, msg)
|
|
|
|
async def add_request_async(self, *args, **kwargs) -> None:
|
|
self.add_request(*args, **kwargs)
|
|
|
|
def try_get_output(self, timeout: float = 0.001) -> dict[str, Any] | None:
|
|
try:
|
|
return self.output_q.get_nowait()
|
|
except queue.Empty:
|
|
return None
|
|
|
|
async def try_get_output_async(self) -> dict[str, Any] | None:
|
|
return self.try_get_output()
|
|
|
|
def get_stage_metadata(self, stage_id: int) -> dict[str, Any]:
|
|
return self.stage_metadata[stage_id]
|
|
|
|
def abort(self, request_ids: list[str]) -> None:
|
|
self.aborted.append(list(request_ids))
|
|
|
|
async def abort_async(self, request_ids: list[str]) -> None:
|
|
self.abort(request_ids)
|
|
|
|
async def collective_rpc_async(self, **_: Any) -> list[Any]:
|
|
return list(self.rpc_results)
|
|
|
|
def is_alive(self) -> bool:
|
|
return self._alive
|
|
|
|
def shutdown(self) -> None:
|
|
self.shutdown_called = True
|
|
self._alive = False
|
|
|
|
|
|
def _patch_engine(monkeypatch: pytest.MonkeyPatch, engine: FakeAsyncOmniEngine) -> None:
|
|
monkeypatch.setattr("vllm_omni.entrypoints.omni_base.AsyncOmniEngine", lambda *args, **kwargs: engine)
|
|
monkeypatch.setattr("vllm_omni.entrypoints.omni_base.omni_snapshot_download", lambda model: model)
|
|
|
|
|
|
def test_from_cli_args_only_nulls_untyped_override_fields(monkeypatch: pytest.MonkeyPatch):
|
|
from vllm_omni.entrypoints.omni import Omni
|
|
|
|
captured: dict[str, Any] = {}
|
|
|
|
def fake_engine(*args: Any, **kwargs: Any) -> FakeAsyncOmniEngine:
|
|
captured.update(kwargs)
|
|
return FakeAsyncOmniEngine()
|
|
|
|
monkeypatch.setattr("vllm_omni.entrypoints.omni_base.AsyncOmniEngine", fake_engine)
|
|
monkeypatch.setattr("vllm_omni.entrypoints.omni_base.omni_snapshot_download", lambda model: model)
|
|
monkeypatch.setattr("sys.argv", ["prog"])
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--gpu-memory-utilization", type=float, default=0.9)
|
|
parser.add_argument("--hsdp-shard-size", type=int, default=-1)
|
|
args = parser.parse_args([])
|
|
args.model = "fake-model"
|
|
|
|
Omni.from_cli_args(args, parser=parser)
|
|
|
|
assert captured["gpu_memory_utilization"] is None
|
|
assert captured["hsdp_shard_size"] == -1
|
|
|
|
|
|
def _make_base():
|
|
from vllm_omni.entrypoints.omni_base import OmniBase
|
|
|
|
obj = object.__new__(OmniBase)
|
|
obj.engine = MagicMock()
|
|
obj.request_states = {}
|
|
return obj
|
|
|
|
|
|
def _stage_spec(
|
|
stage_id: int,
|
|
*,
|
|
payloads: list[str],
|
|
finished: bool = False,
|
|
image_payloads: list[str] | None = None,
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"stage_id": stage_id,
|
|
"payloads": payloads,
|
|
"finished": finished,
|
|
"image_payloads": image_payloads or [],
|
|
}
|
|
|
|
|
|
def _enqueue_outputs(
|
|
engine: FakeAsyncOmniEngine,
|
|
msg: dict[str, Any],
|
|
*,
|
|
stage_specs: list[dict[str, Any]],
|
|
) -> None:
|
|
request_id = msg["request_id"]
|
|
for spec in stage_specs:
|
|
payloads = spec["payloads"]
|
|
image_payloads = spec.get("image_payloads", [])
|
|
last_idx = len(payloads) - 1
|
|
|
|
for idx, payload_tmpl in enumerate(payloads):
|
|
images = []
|
|
if idx < len(image_payloads):
|
|
images = [image_payloads[idx].format(request_id=request_id, idx=idx)]
|
|
|
|
engine.output_q.put_nowait(
|
|
make_output_msg(
|
|
request_id,
|
|
spec["stage_id"],
|
|
payload=payload_tmpl.format(request_id=request_id, idx=idx),
|
|
output_finished=(idx == last_idx),
|
|
finished=bool(spec.get("finished")) and idx == last_idx,
|
|
images=images,
|
|
)
|
|
)
|
|
|
|
|
|
def _enqueue_omni_final_only_outputs(engine: FakeAsyncOmniEngine, msg: dict[str, Any]) -> None:
|
|
sampling_params_list = msg["sampling_params_list"]
|
|
llm_streaming = any(params.output_kind != RequestOutputKind.FINAL_ONLY for params in sampling_params_list[:2])
|
|
stage0_count = 3 if llm_streaming else 1
|
|
stage1_count = 3 if llm_streaming else 1
|
|
|
|
_enqueue_outputs(
|
|
engine,
|
|
msg,
|
|
stage_specs=[
|
|
_stage_spec(0, payloads=[f"{{request_id}}-stage0-{idx}" for idx in range(stage0_count)]),
|
|
_stage_spec(1, payloads=[f"{{request_id}}-stage1-{idx}" for idx in range(stage1_count)]),
|
|
_stage_spec(
|
|
2,
|
|
payloads=["{request_id}-stage2-final"],
|
|
finished=True,
|
|
image_payloads=["{request_id}-img-final"],
|
|
),
|
|
],
|
|
)
|
|
|
|
|
|
def _enqueue_omni_llm_diffusion_outputs(engine: FakeAsyncOmniEngine, msg: dict[str, Any]) -> None:
|
|
sampling_params_list = msg["sampling_params_list"]
|
|
llm_streaming = sampling_params_list[0].output_kind != RequestOutputKind.FINAL_ONLY
|
|
stage0_count = 3 if llm_streaming else 1
|
|
|
|
_enqueue_outputs(
|
|
engine,
|
|
msg,
|
|
stage_specs=[
|
|
_stage_spec(0, payloads=[f"{{request_id}}-text-{idx}" for idx in range(stage0_count)]),
|
|
_stage_spec(
|
|
1,
|
|
payloads=["{request_id}-image-final"],
|
|
finished=True,
|
|
image_payloads=["{request_id}-image"],
|
|
),
|
|
],
|
|
)
|
|
|
|
|
|
def _enqueue_async_three_stage_outputs(engine: FakeAsyncOmniEngine, msg: dict[str, Any]) -> None:
|
|
_enqueue_outputs(
|
|
engine,
|
|
msg,
|
|
stage_specs=[
|
|
_stage_spec(0, payloads=[f"{{request_id}}-stage0-{idx}" for idx in range(3)]),
|
|
_stage_spec(1, payloads=[f"{{request_id}}-stage1-{idx}" for idx in range(3)]),
|
|
_stage_spec(
|
|
2,
|
|
payloads=[f"{{request_id}}-stage2-{idx}" for idx in range(3)],
|
|
finished=True,
|
|
image_payloads=[f"{{request_id}}-img-{idx}" for idx in range(3)],
|
|
),
|
|
],
|
|
)
|
|
|
|
|
|
def _enqueue_async_finish_outputs(engine: FakeAsyncOmniEngine, msg: dict[str, Any]) -> None:
|
|
_enqueue_outputs(
|
|
engine,
|
|
msg,
|
|
stage_specs=[
|
|
_stage_spec(0, payloads=["{request_id}-stage0"]),
|
|
_stage_spec(
|
|
2,
|
|
payloads=["{request_id}-stage2-final"],
|
|
finished=True,
|
|
image_payloads=["{request_id}-img-final"],
|
|
),
|
|
],
|
|
)
|
|
|
|
|
|
def _enqueue_async_diffusion_only_output(engine: FakeAsyncOmniEngine, msg: dict[str, Any]) -> None:
|
|
_enqueue_outputs(
|
|
engine,
|
|
msg,
|
|
stage_specs=[
|
|
_stage_spec(
|
|
0,
|
|
payloads=["{request_id}-diffusion-final"],
|
|
finished=True,
|
|
image_payloads=["{request_id}-image"],
|
|
)
|
|
],
|
|
)
|
|
|
|
|
|
def _enqueue_async_llm_diffusion_outputs(engine: FakeAsyncOmniEngine, msg: dict[str, Any]) -> None:
|
|
_enqueue_outputs(
|
|
engine,
|
|
msg,
|
|
stage_specs=[
|
|
_stage_spec(0, payloads=[f"{{request_id}}-text-{idx}" for idx in range(3)]),
|
|
_stage_spec(
|
|
1,
|
|
payloads=["{request_id}-image-final"],
|
|
finished=True,
|
|
image_payloads=["{request_id}-image"],
|
|
),
|
|
],
|
|
)
|
|
|
|
|
|
def _enqueue_error_message(engine: FakeAsyncOmniEngine, msg: dict[str, Any]) -> None:
|
|
engine.output_q.put_nowait(
|
|
{
|
|
"type": "error",
|
|
"request_id": msg["request_id"],
|
|
"stage_id": 0,
|
|
"error": "engine boom",
|
|
}
|
|
)
|
|
|
|
|
|
def _enqueue_fatal_error_message(engine: FakeAsyncOmniEngine, msg: dict[str, Any]) -> None:
|
|
engine.output_q.put_nowait(
|
|
{
|
|
"type": "error",
|
|
"fatal": True,
|
|
"request_id": msg["request_id"],
|
|
"stage_id": 2,
|
|
"error": "engine dead",
|
|
}
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_supported_tasks_returns_engine_supported_tasks():
|
|
omni = object.__new__(AsyncOmni)
|
|
omni.engine = SimpleNamespace(supported_tasks=("generate", "speech"))
|
|
|
|
supported_tasks = await omni.get_supported_tasks()
|
|
|
|
assert supported_tasks == ("generate", "speech")
|
|
|
|
|
|
def test_model_config_and_vllm_config_forward_from_comprehension_stage():
|
|
model_config = SimpleNamespace(model="Qwen/Qwen3-TTS")
|
|
vllm_config = SimpleNamespace(model_config=model_config)
|
|
renderer = SimpleNamespace(name="renderer")
|
|
input_processor = SimpleNamespace(renderer=renderer)
|
|
io_processor = SimpleNamespace(name="io-processor")
|
|
omni = object.__new__(AsyncOmni)
|
|
omni.engine = SimpleNamespace(
|
|
stage_clients=[SimpleNamespace(is_comprehension=False), SimpleNamespace(is_comprehension=True)],
|
|
stage_vllm_configs=[None, vllm_config],
|
|
)
|
|
omni.input_processor = input_processor
|
|
omni.io_processor = io_processor
|
|
|
|
assert omni.vllm_config is vllm_config
|
|
assert omni.model_config is model_config
|
|
assert omni.renderer is renderer
|
|
assert omni.input_processor is input_processor
|
|
assert omni.io_processor is io_processor
|
|
|
|
|
|
def test_openai_serving_models_can_consume_async_omni_compat_attrs():
|
|
model_config = SimpleNamespace(model="Qwen/Qwen3-TTS", max_model_len=32768)
|
|
vllm_config = SimpleNamespace(model_config=model_config)
|
|
renderer = SimpleNamespace(name="renderer")
|
|
input_processor = SimpleNamespace(renderer=renderer)
|
|
io_processor = SimpleNamespace(name="io-processor")
|
|
omni = object.__new__(AsyncOmni)
|
|
omni.engine = SimpleNamespace(
|
|
stage_clients=[SimpleNamespace(is_comprehension=True)],
|
|
stage_vllm_configs=[vllm_config],
|
|
)
|
|
omni.input_processor = input_processor
|
|
omni.io_processor = io_processor
|
|
|
|
serving_models = OpenAIServingModels(
|
|
engine_client=omni,
|
|
base_model_paths=[BaseModelPath(name="tts-model", model_path="Qwen/Qwen3-TTS")],
|
|
)
|
|
|
|
assert serving_models.model_config is model_config
|
|
assert serving_models.renderer is renderer
|
|
assert serving_models.io_processor is io_processor
|
|
assert serving_models.input_processor is input_processor
|
|
|
|
|
|
def test_get_diffusion_od_config_returns_diffusion_stage_config():
|
|
diffusion_od_config = object()
|
|
omni = object.__new__(AsyncOmni)
|
|
omni.engine = SimpleNamespace(
|
|
stage_clients=[
|
|
SimpleNamespace(stage_type="llm"),
|
|
SimpleNamespace(stage_type="diffusion", _engine=SimpleNamespace(od_config=diffusion_od_config)),
|
|
]
|
|
)
|
|
|
|
assert omni.get_diffusion_od_config() is diffusion_od_config
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_omni_yields_only_final_stage_outputs(monkeypatch: pytest.MonkeyPatch):
|
|
engine = FakeAsyncOmniEngine(
|
|
stage_metadata=THREE_STAGE_META,
|
|
on_add_request=lambda eng, msg: _enqueue_outputs(
|
|
eng,
|
|
msg,
|
|
stage_specs=[
|
|
_stage_spec(1, payloads=["non-final"]),
|
|
_stage_spec(2, payloads=["final"], finished=True, image_payloads=["final-img"]),
|
|
],
|
|
),
|
|
)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = AsyncOmni("dummy-model")
|
|
try:
|
|
outputs = []
|
|
async for output in app.generate(prompt="hello", request_id="req-1"):
|
|
outputs.append(output)
|
|
finally:
|
|
app.shutdown()
|
|
|
|
assert [output.stage_id for output in outputs] == [2]
|
|
assert [output.request_output.payload for output in outputs] == ["final"]
|
|
assert "req-1" not in app.request_states
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_omni_accepts_multiple_final_stage_streams(monkeypatch: pytest.MonkeyPatch):
|
|
engine = FakeAsyncOmniEngine(stage_metadata=THREE_STAGE_META, on_add_request=_enqueue_async_three_stage_outputs)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = AsyncOmni("dummy-model")
|
|
try:
|
|
outputs = []
|
|
async for output in app.generate(prompt="hello", request_id="req-1"):
|
|
outputs.append(output)
|
|
finally:
|
|
app.shutdown()
|
|
|
|
assert [output.stage_id for output in outputs] == [0, 0, 0, 2, 2, 2]
|
|
assert [output.request_output.payload for output in outputs] == [
|
|
"req-1-stage0-0",
|
|
"req-1-stage0-1",
|
|
"req-1-stage0-2",
|
|
"req-1-stage2-0",
|
|
"req-1-stage2-1",
|
|
"req-1-stage2-2",
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_omni_stops_on_final_stage_finished(monkeypatch: pytest.MonkeyPatch):
|
|
# Intentionally jump from stage 0 to stage 2: stage 1 is a non-final stage
|
|
# and should be filtered out from the client-visible output stream.
|
|
engine = FakeAsyncOmniEngine(stage_metadata=THREE_STAGE_META, on_add_request=_enqueue_async_finish_outputs)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = AsyncOmni("dummy-model")
|
|
try:
|
|
outputs = []
|
|
async for output in app.generate(prompt="hello", request_id="req-1"):
|
|
outputs.append(output)
|
|
finally:
|
|
app.shutdown()
|
|
|
|
assert [output.request_output.payload for output in outputs] == [
|
|
"req-1-stage0",
|
|
"req-1-stage2-final",
|
|
]
|
|
assert "req-1" not in app.request_states
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_omni_diffusion_only_yields_single_image_output(monkeypatch: pytest.MonkeyPatch):
|
|
engine = FakeAsyncOmniEngine(
|
|
stage_metadata=DIFFUSION_ONLY_META,
|
|
on_add_request=_enqueue_async_diffusion_only_output,
|
|
)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = AsyncOmni("dummy-model")
|
|
try:
|
|
outputs = []
|
|
async for output in app.generate(prompt="hello", request_id="req-1"):
|
|
outputs.append(output)
|
|
finally:
|
|
app.shutdown()
|
|
|
|
assert len(outputs) == 1
|
|
assert outputs[0].stage_id == 0
|
|
assert outputs[0].final_output_type == "image"
|
|
assert outputs[0].images == ["req-1-image"]
|
|
assert outputs[0].request_output.payload == "req-1-diffusion-final"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_omni_llm_diffusion_yields_text_stream_then_image(monkeypatch: pytest.MonkeyPatch):
|
|
engine = FakeAsyncOmniEngine(
|
|
stage_metadata=LLM_DIFFUSION_META,
|
|
on_add_request=_enqueue_async_llm_diffusion_outputs,
|
|
)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = AsyncOmni("dummy-model")
|
|
try:
|
|
outputs = []
|
|
async for output in app.generate(prompt="hello", request_id="req-1"):
|
|
outputs.append(output)
|
|
finally:
|
|
app.shutdown()
|
|
|
|
assert [output.stage_id for output in outputs] == [0, 0, 0, 1]
|
|
assert [output.final_output_type for output in outputs] == ["text", "text", "text", "image"]
|
|
assert [output.request_output.payload for output in outputs] == [
|
|
"req-1-text-0",
|
|
"req-1-text-1",
|
|
"req-1-text-2",
|
|
"req-1-image-final",
|
|
]
|
|
assert outputs[-1].images == ["req-1-image"]
|
|
assert "req-1" not in app.request_states
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_omni_abort_forwards_to_engine(monkeypatch: pytest.MonkeyPatch):
|
|
engine = FakeAsyncOmniEngine(stage_metadata=THREE_STAGE_META)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = AsyncOmni("dummy-model")
|
|
try:
|
|
app.request_states["req-1"] = object()
|
|
await app.abort("req-1")
|
|
finally:
|
|
app.shutdown()
|
|
|
|
assert engine.aborted == [["req-1"]]
|
|
assert "req-1" not in app.request_states
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_omni_propagates_fatal_error_context(monkeypatch: pytest.MonkeyPatch):
|
|
engine = FakeAsyncOmniEngine(stage_metadata=THREE_STAGE_META, on_add_request=_enqueue_fatal_error_message)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = AsyncOmni("dummy-model")
|
|
try:
|
|
with pytest.raises(EngineDeadError, match="engine dead") as exc_info:
|
|
async for _ in app.generate(prompt="hello", request_id="req-1"):
|
|
pass
|
|
finally:
|
|
app.shutdown()
|
|
|
|
assert isinstance(exc_info.value, OmniEngineDeadError)
|
|
assert str(exc_info.value) == "engine dead"
|
|
assert getattr(exc_info.value, "error_stage_id") == 2
|
|
|
|
|
|
def test_omni_generate_py_generator_yields_final_outputs_for_each_request(monkeypatch: pytest.MonkeyPatch):
|
|
sampling_params = [SamplingParams(max_tokens=8) for _ in range(3)]
|
|
engine = FakeAsyncOmniEngine(
|
|
stage_metadata=THREE_STAGE_META,
|
|
default_sampling_params_list=sampling_params,
|
|
on_add_request=_enqueue_omni_final_only_outputs,
|
|
)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = Omni("dummy-model")
|
|
outputs = list(app.generate(["p1", "p2"], py_generator=True, use_tqdm=False))
|
|
|
|
assert len(outputs) == 4
|
|
assert [output.stage_id for output in outputs] == [0, 2, 0, 2]
|
|
assert [output.request_output.payload for output in outputs] == [
|
|
f"{engine.submitted[0]['request_id']}-stage0-0",
|
|
f"{engine.submitted[0]['request_id']}-stage2-final",
|
|
f"{engine.submitted[1]['request_id']}-stage0-0",
|
|
f"{engine.submitted[1]['request_id']}-stage2-final",
|
|
]
|
|
assert engine.shutdown_called is True
|
|
|
|
|
|
def test_omni_generate_returns_list_when_not_using_generator(monkeypatch: pytest.MonkeyPatch):
|
|
sampling_params = [SamplingParams(max_tokens=8) for _ in range(3)]
|
|
engine = FakeAsyncOmniEngine(
|
|
stage_metadata=THREE_STAGE_META,
|
|
default_sampling_params_list=sampling_params,
|
|
on_add_request=_enqueue_omni_final_only_outputs,
|
|
)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = Omni("dummy-model")
|
|
try:
|
|
outputs = app.generate(["p1", "p2"], py_generator=False, use_tqdm=False)
|
|
finally:
|
|
app.shutdown()
|
|
|
|
assert isinstance(outputs, list)
|
|
assert len(outputs) == 4
|
|
assert [output.stage_id for output in outputs] == [0, 2, 0, 2]
|
|
|
|
|
|
def test_omni_generate_diffusion_only_yields_single_image_per_request(monkeypatch: pytest.MonkeyPatch):
|
|
sampling_params = [SamplingParams(max_tokens=8)]
|
|
engine = FakeAsyncOmniEngine(
|
|
stage_metadata=DIFFUSION_ONLY_META,
|
|
default_sampling_params_list=sampling_params,
|
|
on_add_request=_enqueue_async_diffusion_only_output,
|
|
)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = Omni("dummy-model")
|
|
outputs = list(app.generate(["p1", "p2"], py_generator=True, use_tqdm=False))
|
|
|
|
assert len(outputs) == 2
|
|
assert [output.stage_id for output in outputs] == [0, 0]
|
|
assert [output.final_output_type for output in outputs] == ["image", "image"]
|
|
assert [output.request_output.payload for output in outputs] == [
|
|
f"{engine.submitted[0]['request_id']}-diffusion-final",
|
|
f"{engine.submitted[1]['request_id']}-diffusion-final",
|
|
]
|
|
assert [output.images for output in outputs] == [
|
|
[f"{engine.submitted[0]['request_id']}-image"],
|
|
[f"{engine.submitted[1]['request_id']}-image"],
|
|
]
|
|
assert engine.shutdown_called is True
|
|
|
|
|
|
def test_omni_generate_llm_diffusion_yields_final_text_then_image_per_request(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
):
|
|
sampling_params = [SamplingParams(max_tokens=8) for _ in range(2)]
|
|
engine = FakeAsyncOmniEngine(
|
|
stage_metadata=LLM_DIFFUSION_META,
|
|
default_sampling_params_list=sampling_params,
|
|
on_add_request=_enqueue_omni_llm_diffusion_outputs,
|
|
)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = Omni("dummy-model")
|
|
outputs = list(app.generate(["p1", "p2"], py_generator=True, use_tqdm=False))
|
|
|
|
assert len(outputs) == 4
|
|
assert [output.stage_id for output in outputs] == [0, 1, 0, 1]
|
|
assert [output.final_output_type for output in outputs] == ["text", "image", "text", "image"]
|
|
assert [output.request_output.payload for output in outputs] == [
|
|
f"{engine.submitted[0]['request_id']}-text-0",
|
|
f"{engine.submitted[0]['request_id']}-image-final",
|
|
f"{engine.submitted[1]['request_id']}-text-0",
|
|
f"{engine.submitted[1]['request_id']}-image-final",
|
|
]
|
|
assert [output.images for output in outputs] == [
|
|
[],
|
|
[f"{engine.submitted[0]['request_id']}-image"],
|
|
[],
|
|
[f"{engine.submitted[1]['request_id']}-image"],
|
|
]
|
|
assert engine.submitted[0]["sampling_params_list"][0].output_kind == RequestOutputKind.FINAL_ONLY
|
|
assert engine.shutdown_called is True
|
|
|
|
|
|
def test_omni_abort_forwards_to_engine(monkeypatch: pytest.MonkeyPatch):
|
|
engine = FakeAsyncOmniEngine(stage_metadata=THREE_STAGE_META)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = Omni("dummy-model")
|
|
try:
|
|
app.request_states["req-1"] = object()
|
|
app.abort("req-1")
|
|
finally:
|
|
app.shutdown()
|
|
|
|
assert engine.aborted == [["req-1"]]
|
|
assert "req-1" not in app.request_states
|
|
|
|
|
|
def test_omni_forces_final_only_on_llm_stages(monkeypatch: pytest.MonkeyPatch):
|
|
sampling_params = [SamplingParams(max_tokens=8) for _ in range(3)]
|
|
original_diffusion_output_kind = sampling_params[2].output_kind
|
|
engine = FakeAsyncOmniEngine(
|
|
stage_metadata=THREE_STAGE_META,
|
|
default_sampling_params_list=sampling_params,
|
|
on_add_request=_enqueue_omni_final_only_outputs,
|
|
)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = Omni("dummy-model")
|
|
try:
|
|
outputs = list(app.generate(["p1"], py_generator=True, use_tqdm=False))
|
|
finally:
|
|
if not engine.shutdown_called:
|
|
app.shutdown()
|
|
|
|
submitted_params = engine.submitted[0]["sampling_params_list"]
|
|
assert submitted_params[0].output_kind == RequestOutputKind.FINAL_ONLY
|
|
assert submitted_params[1].output_kind == RequestOutputKind.FINAL_ONLY
|
|
assert submitted_params[2].output_kind == original_diffusion_output_kind
|
|
assert len(outputs) == 2
|
|
|
|
|
|
def test_fatal_error_raises_engine_dead():
|
|
base = _make_base()
|
|
msg = {"type": "error", "error": "orchestrator crashed", "fatal": True}
|
|
|
|
with pytest.raises(EngineDeadError, match="orchestrator crashed"):
|
|
base._handle_output_message(msg)
|
|
|
|
|
|
def test_non_fatal_error_raises_runtime():
|
|
base = _make_base()
|
|
msg = {"type": "error", "error": "something wrong"}
|
|
|
|
with pytest.raises(RuntimeError, match="something wrong"):
|
|
base._handle_output_message(msg)
|
|
|
|
|
|
def test_async_omni_errored_property_alive():
|
|
omni = object.__new__(AsyncOmni)
|
|
omni.engine = SimpleNamespace(
|
|
is_alive=lambda: True,
|
|
stage_clients=[SimpleNamespace(is_comprehension=False)],
|
|
)
|
|
|
|
assert omni.errored is False
|
|
|
|
|
|
def test_async_omni_errored_property_dead_engine():
|
|
omni = object.__new__(AsyncOmni)
|
|
omni.engine = SimpleNamespace(
|
|
is_alive=lambda: False,
|
|
stage_clients=[SimpleNamespace(is_comprehension=False)],
|
|
)
|
|
|
|
assert omni.errored is True
|
|
|
|
|
|
def test_async_omni_errored_property_dead_stage():
|
|
omni = object.__new__(AsyncOmni)
|
|
dead_stage = SimpleNamespace(is_comprehension=False, _engine_dead=True)
|
|
omni.engine = SimpleNamespace(
|
|
is_alive=lambda: True,
|
|
stage_clients=[dead_stage],
|
|
)
|
|
|
|
assert omni.errored is True
|
|
|
|
|
|
def _enqueue_stage_error(
|
|
engine: FakeAsyncOmniEngine,
|
|
msg,
|
|
*,
|
|
error_text: str,
|
|
kill_engine: bool = False,
|
|
):
|
|
"""Enqueue a stage error output, optionally killing the engine."""
|
|
if kill_engine:
|
|
engine._alive = False
|
|
engine.output_q.put_nowait(
|
|
{
|
|
"type": "output",
|
|
"request_id": msg["request_id"],
|
|
"stage_id": 0,
|
|
"engine_outputs": SimpleNamespace(
|
|
payload="",
|
|
finished=True,
|
|
images=[],
|
|
stage_durations={},
|
|
error=error_text,
|
|
),
|
|
"finished": False,
|
|
}
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_omni_propagates_engine_dead_error(monkeypatch: pytest.MonkeyPatch):
|
|
"""When the engine is dead and an error output arrives, ``generate()``
|
|
must raise ``EngineDeadError`` (not plain ``RuntimeError``)."""
|
|
|
|
engine = FakeAsyncOmniEngine(
|
|
stage_metadata=THREE_STAGE_META,
|
|
on_add_request=lambda eng, msg: _enqueue_stage_error(eng, msg, error_text="worker OOM", kill_engine=True),
|
|
)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = AsyncOmni("dummy-model")
|
|
try:
|
|
with pytest.raises(EngineDeadError, match="worker OOM"):
|
|
async for _ in app.generate(prompt="hello", request_id="req-dead"):
|
|
pass
|
|
finally:
|
|
app.shutdown()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_omni_propagates_engine_generate_error(monkeypatch: pytest.MonkeyPatch):
|
|
"""When the engine is alive but a stage error occurs, ``generate()``
|
|
must raise ``EngineGenerateError`` (recoverable, not ``EngineDeadError``)."""
|
|
|
|
engine = FakeAsyncOmniEngine(
|
|
stage_metadata=THREE_STAGE_META,
|
|
on_add_request=lambda eng, msg: _enqueue_stage_error(eng, msg, error_text="diffusion step failed"),
|
|
)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = AsyncOmni("dummy-model")
|
|
try:
|
|
with pytest.raises(EngineGenerateError):
|
|
async for _ in app.generate(prompt="hello", request_id="req-recover"):
|
|
pass
|
|
finally:
|
|
app.shutdown()
|
|
|
|
|
|
# ───────── OmniBase.check_health() aggregation ─────────
|
|
|
|
|
|
def test_check_health_passes_when_all_healthy():
|
|
base = _make_base()
|
|
healthy_stage = MagicMock()
|
|
healthy_stage.check_health = MagicMock()
|
|
base.engine.is_alive.return_value = True
|
|
base.engine.stage_clients = [healthy_stage]
|
|
base.check_health() # should not raise
|
|
|
|
|
|
def test_check_health_raises_when_stage_dead():
|
|
base = _make_base()
|
|
dead_stage = MagicMock()
|
|
dead_stage.check_health = MagicMock(side_effect=EngineDeadError("Stage-1 dead"))
|
|
base.engine.is_alive.return_value = True
|
|
base.engine.stage_clients = [dead_stage]
|
|
with pytest.raises(EngineDeadError, match="Stage-1 dead"):
|
|
base.check_health()
|
|
|
|
|
|
def test_check_health_raises_when_orchestrator_dead():
|
|
base = _make_base()
|
|
base.engine.is_alive.return_value = False
|
|
base.engine.stage_clients = []
|
|
with pytest.raises(EngineDeadError, match="not alive"):
|
|
base.check_health()
|
|
|
|
|
|
# ───────── OmniBase.errored property ─────────
|
|
|
|
|
|
def test_omni_base_errored_false_when_alive():
|
|
base = _make_base()
|
|
base.engine.is_alive.return_value = True
|
|
base.engine.stage_clients = [SimpleNamespace()]
|
|
assert base.errored is False
|
|
|
|
|
|
def test_omni_base_errored_true_when_orchestrator_dead():
|
|
base = _make_base()
|
|
base.engine.is_alive.return_value = False
|
|
base.engine.stage_clients = []
|
|
assert base.errored is True
|
|
|
|
|
|
def test_omni_base_errored_true_when_stage_engine_dead():
|
|
base = _make_base()
|
|
base.engine.is_alive.return_value = True
|
|
dead_stage = SimpleNamespace(_engine_dead=True)
|
|
base.engine.stage_clients = [dead_stage]
|
|
assert base.errored is True
|
|
|
|
|
|
def test_omni_base_errored_true_when_stage_resources_engine_dead():
|
|
base = _make_base()
|
|
base.engine.is_alive.return_value = True
|
|
dead_stage = SimpleNamespace(resources=SimpleNamespace(engine_dead=True))
|
|
base.engine.stage_clients = [dead_stage]
|
|
assert base.errored is True
|
|
|
|
|
|
# ───────── Omni (sync) EngineDeadError / EngineGenerateError ─────────
|
|
|
|
|
|
def test_omni_propagates_engine_dead_error(monkeypatch: pytest.MonkeyPatch):
|
|
"""When the engine is dead and a stage error output arrives,
|
|
``Omni.generate()`` must raise ``EngineDeadError``."""
|
|
engine = FakeAsyncOmniEngine(
|
|
stage_metadata=THREE_STAGE_META,
|
|
on_add_request=lambda eng, msg: _enqueue_stage_error(eng, msg, error_text="worker OOM", kill_engine=True),
|
|
)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = Omni("dummy-model")
|
|
try:
|
|
with pytest.raises(EngineDeadError, match="worker OOM"):
|
|
list(app.generate(["hello"], py_generator=False, use_tqdm=False))
|
|
finally:
|
|
app.shutdown()
|
|
|
|
|
|
def test_omni_propagates_engine_generate_error(monkeypatch: pytest.MonkeyPatch):
|
|
"""When the engine is alive but a stage error occurs,
|
|
``Omni.generate()`` must raise ``EngineGenerateError`` (recoverable)."""
|
|
engine = FakeAsyncOmniEngine(
|
|
stage_metadata=THREE_STAGE_META,
|
|
on_add_request=lambda eng, msg: _enqueue_stage_error(eng, msg, error_text="diffusion step failed"),
|
|
)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = Omni("dummy-model")
|
|
try:
|
|
with pytest.raises(EngineGenerateError):
|
|
list(app.generate(["hello"], py_generator=False, use_tqdm=False))
|
|
finally:
|
|
app.shutdown()
|
|
|
|
|
|
def test_omni_errored_property_alive(monkeypatch: pytest.MonkeyPatch):
|
|
"""Omni.errored (inherited from OmniBase) returns False when healthy."""
|
|
engine = FakeAsyncOmniEngine(stage_metadata=THREE_STAGE_META)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = Omni("dummy-model")
|
|
try:
|
|
assert app.errored is False
|
|
finally:
|
|
app.shutdown()
|
|
|
|
|
|
def test_omni_errored_property_dead_engine(monkeypatch: pytest.MonkeyPatch):
|
|
"""Omni.errored returns True when the orchestrator is dead."""
|
|
engine = FakeAsyncOmniEngine(stage_metadata=THREE_STAGE_META)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = Omni("dummy-model")
|
|
try:
|
|
engine._alive = False
|
|
assert app.errored is True
|
|
finally:
|
|
app.shutdown()
|
|
|
|
|
|
def test_omni_errored_property_dead_stage(monkeypatch: pytest.MonkeyPatch):
|
|
"""Omni.errored returns True when a stage client is marked dead."""
|
|
engine = FakeAsyncOmniEngine(stage_metadata=THREE_STAGE_META)
|
|
_patch_engine(monkeypatch, engine)
|
|
|
|
app = Omni("dummy-model")
|
|
try:
|
|
engine.stage_clients[0]._engine_dead = True
|
|
assert app.errored is True
|
|
finally:
|
|
app.shutdown()
|