Files
vllm-omni/tests/entrypoints/test_serve.py
T

611 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Unit tests for the Omni serve CLI helpers."""
from __future__ import annotations
import argparse
from types import SimpleNamespace
import pytest
from pytest_mock import MockerFixture
from vllm_omni.entrypoints.cli.serve import OmniServeCommand, run_headless
from vllm_omni.entrypoints.utils import parse_stage_overrides
from vllm_omni.utils.tracking_parser import TrackingArgumentParser, TrackingNamespace
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_serve_parser_accepts_no_async_chunk_and_marks_it_explicit() -> None:
"""``--no-async-chunk`` should parse to ``async_chunk=False`` and mark the
shared deploy-level dest as explicitly provided by the user."""
parser = TrackingArgumentParser()
subparsers = parser.add_subparsers(dest="subcommand")
cmd = OmniServeCommand()
cmd.subparser_init(subparsers)
argv = ["serve", "fake-model", "--omni", "--no-async-chunk"]
args = parser.parse_args(argv)
assert args.async_chunk is False
explicit = args.get_explicit_kwargs_dict()
assert args.get_explicit_kwargs_dict()
assert not explicit["async_chunk"]
def test_serve_parser_accepts_strategy_config() -> None:
"""``--strategy-config`` must parse onto the ``strategy_config`` dest and be
forwarded as an explicit kwarg so the engine can overlay the strategy."""
parser = TrackingArgumentParser()
subparsers = parser.add_subparsers(dest="subcommand")
cmd = OmniServeCommand()
cmd.subparser_init(subparsers)
argv = ["serve", "fake-model", "--omni", "--strategy-config", "/tmp/strategy.yaml"]
args = parser.parse_args(argv)
assert args.strategy_config == "/tmp/strategy.yaml"
assert args.get_explicit_kwargs_dict()["strategy_config"] == "/tmp/strategy.yaml"
def test_serve_parser_accepts_deploy_config() -> None:
parser = TrackingArgumentParser()
subparsers = parser.add_subparsers(dest="subcommand")
OmniServeCommand().subparser_init(subparsers)
args = parser.parse_args(["serve", "fake-model", "--omni", "--deploy-config", "/tmp/deploy.yaml"])
assert args.deploy_config == "/tmp/deploy.yaml"
assert args.get_explicit_kwargs_dict()["deploy_config"] == "/tmp/deploy.yaml"
def test_serve_parser_rejects_stage_configs_path() -> None:
parser = TrackingArgumentParser()
subparsers = parser.add_subparsers(dest="subcommand")
OmniServeCommand().subparser_init(subparsers)
with pytest.raises(SystemExit):
parser.parse_args(["serve", "fake-model", "--omni", "--stage-configs-path", "/tmp/stages.yaml"])
def test_serve_parser_accepts_four_way_cfg_parallelism() -> None:
parser = TrackingArgumentParser()
subparsers = parser.add_subparsers(dest="subcommand")
OmniServeCommand().subparser_init(subparsers)
args = parser.parse_args(["serve", "fake-model", "--omni", "--cfg-parallel-size", "4"])
assert args.cfg_parallel_size == 4
def test_serve_parser_accepts_ulysses_a2a_permute() -> None:
parser = TrackingArgumentParser()
subparsers = parser.add_subparsers(dest="subcommand")
OmniServeCommand().subparser_init(subparsers)
args = parser.parse_args(["serve", "fake-model", "--omni", "--ulysses-a2a-permute"])
assert args.ulysses_a2a_permute is True
assert args.get_explicit_kwargs_dict()["ulysses_a2a_permute"] is True
def _make_headless_args(**kwargs) -> TrackingNamespace:
defaults = {
"model": "fake-model",
"stage_id": 0,
"replica_id": 0,
"omni_master_address": "127.0.0.1",
"omni_master_port": 26000,
"omni_replica_address": None,
"omni_dp_size_local": 1,
"worker_backend": "multi_process",
"deploy_config": None,
"log_stats": False,
"disable_log_stats": False,
"stage_init_timeout": 600,
"tokenizer": None,
}
ns_kwargs = {**defaults, **kwargs}
ns = argparse.Namespace(**ns_kwargs)
return TrackingNamespace(
unfiltered_ns=ns,
explicit_keys=frozenset(ns.__dict__.keys()),
)
def test_run_headless_requires_stage_id() -> None:
args = _make_headless_args(stage_id=None)
with pytest.raises(ValueError, match="--stage-id is required"):
run_headless(args)
def test_run_headless_requires_master_address() -> None:
args = _make_headless_args(omni_master_address=None)
with pytest.raises(ValueError, match="--omni-master-address and --omni-master-port"):
run_headless(args)
def test_run_headless_requires_master_port() -> None:
args = _make_headless_args(omni_master_port=None)
with pytest.raises(ValueError, match="--omni-master-address and --omni-master-port"):
run_headless(args)
def test_run_headless_rejects_non_multiprocess_worker_backend() -> None:
args = _make_headless_args(worker_backend="ray")
with pytest.raises(ValueError, match="worker_backend=multi_process"):
run_headless(args)
# ---------------------------------------------------------------------------
# --stage-overrides parsing parity (headless vs standard path)
# ---------------------------------------------------------------------------
def test_parse_stage_overrides_valid_json() -> None:
"""A valid JSON string is parsed into the nested per-stage dict."""
parsed = parse_stage_overrides('{"0": {"devices": "0,1"}, "1": {"devices": "2"}}')
assert parsed == {"0": {"devices": "0,1"}, "1": {"devices": "2"}}
def test_parse_stage_overrides_none_and_empty_return_none() -> None:
"""No overrides (None / empty string) resolve to ``None``."""
assert parse_stage_overrides(None) is None
assert parse_stage_overrides("") is None
def test_parse_stage_overrides_empty_dict_returns_none() -> None:
"""An empty dict is falsy and must resolve to ``None``, locking in parity
with the original standard-path ``if stage_overrides_json:`` falsy check."""
assert parse_stage_overrides({}) is None
def test_parse_stage_overrides_passes_through_non_str() -> None:
"""An already-parsed mapping is returned unchanged (identity)."""
overrides = {"0": {"devices": "0"}}
assert parse_stage_overrides(overrides) is overrides
def test_parse_stage_overrides_invalid_json_raises() -> None:
"""Invalid JSON raises ValueError whose message matches the standard path
verbatim: the ``--stage-overrides is not valid JSON:`` prefix AND the
``Got: <repr>`` suffix echoing the raw input."""
bad = "{not valid json}"
with pytest.raises(ValueError) as excinfo:
parse_stage_overrides(bad)
message = str(excinfo.value)
assert message.startswith("--stage-overrides is not valid JSON:")
assert f"Got: {bad!r}" in message
def test_parse_stage_overrides_rejects_non_dict_top_level() -> None:
"""Top-level must be a JSON object (dict). A list, scalar, or non-dict
mapping is rejected with a ValueError naming ``--stage-overrides`` and
pointing at the bad shape. Without this guard, ``json.loads`` happily
returns a list/scalar and the override silently never applies."""
for bad in ("[1, 2, 3]", '"oops"', "42"):
with pytest.raises(ValueError, match="must be a JSON object"):
parse_stage_overrides(bad)
def test_parse_stage_overrides_rejects_non_integer_stage_id() -> None:
"""Stage-id keys must be non-negative ASCII integer strings. Letters,
signs, floats, Unicode digit classes, and integer (non-string) keys all
fail. ``str.isdigit() and str.isascii()`` is the minimal check: it
rejects ``"-1"``, ``"abc"``, ``"1.5"``, fullwidth ``"0"``, and bare ``1``.
Note: ``json.loads`` normalizes integer object keys (``{"1": {}}``) into
the string ``"1"`` and would pass our digit check, so the integer-key case
is exercised via the already-parsed-dict code path (``parse_stage_overrides({1: {}})``)."""
bad_string_keys = ('"abc"', '"-1"', '"1.5"', '"\uff10"')
for bad_key in bad_string_keys:
with pytest.raises(ValueError, match="non-negative integer stage ids"):
parse_stage_overrides("{" + bad_key + ": {}}")
# Integer (non-string) key: must reach the structural check, not the JSON
# parser, so pass an already-parsed mapping directly.
with pytest.raises(ValueError, match="non-negative integer stage ids"):
parse_stage_overrides({1: {}})
def test_parse_stage_overrides_accepts_stage_merge_extras_and_engine_args() -> None:
"""Three classes of keys pass through the shape-only parser:
- ``extras`` is read by the default-diffusion fallback
(``async_omni_engine.py``); registered pipelines carry it on
``StagePipelineConfig.extras`` directly.
- Engine arguments (``kv_cache_dtype``, ``stage_connector_spec``, ...)
are forwarded as ``stage_<id>_<key>`` and applied via
``OmniEngineArgs``.
- Unknown keys parse through and are dropped with a warning at
``filter_dataclass_kwargs`` (see
``tests/entrypoints/test_utils.py::TestFilterDataclassKwargs``).
``typo_field_xyz`` in the payload exercises the third case.
"""
parsed = parse_stage_overrides(
'{"0": {"extras": {"ltx2_use_conv_vae": true},'
' "kv_cache_dtype": "fp8", "seed": 42,'
' "stage_connector_spec": {"name": "SharedMemoryConnector", "extra": {}},'
' "typo_field_xyz": 1}}'
)
assert parsed == {
"0": {
"extras": {"ltx2_use_conv_vae": True},
"kv_cache_dtype": "fp8",
"seed": 42,
"stage_connector_spec": {"name": "SharedMemoryConnector", "extra": {}},
"typo_field_xyz": 1,
},
}
# Already-parsed mapping path carries the same trust.
assert parse_stage_overrides(dict(parsed)) == parsed
def test_parse_stage_overrides_accepts_empty_inner_dict() -> None:
"""Per-stage overrides may be empty (``{}``): shape stays valid, every
stage simply receives no per-stage tweaks. Locks in the
``not parsed`` -> ``None`` short-circuit's twin: an outer
non-empty dict with empty inner dicts is a valid no-op pass-through."""
parsed = parse_stage_overrides('{"0": {}, "1": {}}')
assert parsed == {"0": {}, "1": {}}
def test_run_headless_parses_and_forwards_stage_overrides(mocker: MockerFixture) -> None:
"""Regression: the headless path must parse ``--stage-overrides`` (a JSON
string) and forward the parsed dict to ``load_and_resolve_stage_configs``,
mirroring the standard engine path. Previously it was dropped entirely,
silently producing a different per-stage device layout."""
captured: dict = {}
def _fake_resolve(*args, **kwargs):
captured["args"] = args
captured.update(kwargs)
# Return a stage that does NOT match stage_id=0 so run_headless stops
# right after the resolver call (we only care about how it was called).
return ("/fake/stages.yaml", [SimpleNamespace(stage_id=99)], None)
mocker.patch(
"vllm_omni.entrypoints.utils.load_and_resolve_stage_configs",
side_effect=_fake_resolve,
)
args = _make_headless_args(
stage_id=0,
deploy_config="/tmp/deploy.yaml",
strategy_config="/tmp/strategy.yaml",
stage_overrides='{"0": {"devices": "0,1"}, "1": {"devices": "2"}}',
)
with pytest.raises(ValueError, match="No stage config found for stage_id=0"):
run_headless(args)
assert len(captured["args"]) == 2
assert captured["deploy_config_path"] == "/tmp/deploy.yaml"
assert captured["stage_overrides"] == {"0": {"devices": "0,1"}, "1": {"devices": "2"}}
assert captured["strategy_config_path"] == "/tmp/strategy.yaml"
def test_run_headless_invalid_stage_overrides_raises(mocker: MockerFixture) -> None:
"""Invalid ``--stage-overrides`` JSON in headless mode fails fast with the
shared ValueError instead of being silently ignored."""
mocker.patch(
"vllm_omni.entrypoints.utils.load_and_resolve_stage_configs",
return_value=("/fake/stages.yaml", [SimpleNamespace(stage_id=0)], None),
)
args = _make_headless_args(stage_id=0, stage_overrides="{not valid json}")
with pytest.raises(ValueError, match="--stage-overrides is not valid JSON"):
run_headless(args)
def test_run_headless_raises_when_stage_id_not_in_configs(mocker: MockerFixture) -> None:
"""Headless looks up its assigned stage_id in the loaded deploy YAML and
fails fast when the launcher's --stage-id doesn't match any entry."""
other_stage = SimpleNamespace(stage_id=99)
mocker.patch(
"vllm_omni.entrypoints.utils.load_and_resolve_stage_configs",
return_value=("/fake/stages.yaml", [other_stage], None),
)
args = _make_headless_args(stage_id=0)
with pytest.raises(ValueError, match="No stage config found for stage_id=0"):
run_headless(args)
# ---------------------------------------------------------------------------
# run_headless happy paths
# ---------------------------------------------------------------------------
def _make_stage_cfg(stage_id: int, stage_type: str) -> SimpleNamespace:
"""Build a stage config that satisfies every attribute run_headless reads.
Notably ``engine_args`` is a real dict (not a Mock) so
``get_stage_devices_per_replica`` can call ``.get("tensor_parallel_size")``
and feed the result through ``int()`` without TypeError.
"""
return SimpleNamespace(
stage_id=stage_id,
stage_type=stage_type,
# No "devices" key -> split_devices_for_replicas skipped, each replica
# inherits the launcher's CUDA_VISIBLE_DEVICES.
runtime=None,
engine_args={},
)
def test_run_headless_llm_registers_with_auto_assigned_replica_id(mocker: MockerFixture) -> None:
"""LLM headless: each loop iteration registers with auto-assigned
replica_id (master picks a free slot) and spawns one
``StageEngineCoreProcManager`` per local replica."""
from vllm_omni.engine.stage_engine_startup import StageRegistrationResponse
stage_cfg = _make_stage_cfg(0, stage_type="llm")
stage_cfg.engine_args["async_chunk"] = True
parallel_config = SimpleNamespace(
data_parallel_size_local=1,
data_parallel_rank=0,
data_parallel_rank_local=0,
node_rank_within_dp=0,
)
vllm_config = SimpleNamespace(parallel_config=parallel_config, needs_dp_coordinator=False)
engine_manager = mocker.Mock()
mocker.patch(
"vllm_omni.entrypoints.utils.load_and_resolve_stage_configs",
return_value=("/fake/stages.yaml", [stage_cfg], None),
)
mocker.patch("vllm_omni.engine.stage_init_utils.prepare_engine_environment")
mocker.patch("vllm_omni.engine.stage_init_utils.load_omni_transfer_config_for_model", return_value=None)
mocker.patch(
"vllm_omni.distributed.omni_connectors.utils.initialization.resolve_omni_kv_config_for_stage",
return_value=(None, None, None),
)
mock_connector_spec = mocker.patch(
"vllm_omni.engine.stage_init_utils.get_stage_connector_spec",
return_value={},
)
mocker.patch("vllm_omni.engine.stage_init_utils.build_engine_args_dict", return_value={})
mocker.patch(
"vllm_omni.engine.stage_init_utils.build_vllm_config",
return_value=(vllm_config, object),
)
mock_register = mocker.patch(
"vllm_omni.engine.stage_engine_startup.register_stage_with_omni_master",
return_value=StageRegistrationResponse(
handshake_address="tcp://127.0.0.1:26001",
input_address="tcp://127.0.0.1:26002",
output_address="tcp://127.0.0.1:26003",
replica_id=0,
coordinator_router_address="tcp://127.0.0.1:26100",
),
)
mock_manager_cls = mocker.patch(
"vllm_omni.engine.stage_engine_core_proc_manager.StageEngineCoreProcManager",
return_value=engine_manager,
)
mocker.patch("signal.signal")
run_headless(_make_headless_args(stage_id=0))
# The launcher must request auto-assignment (replica_id=None) and the
# full response so it can wire the master-allocated coordinator into the
# spawned subprocess. LLM uses head-owned sockets: the head binds all
# three sockets (handshake, input, output) and the worker connects.
assert mock_register.call_count == 1
kwargs = mock_register.call_args.kwargs
assert kwargs["omni_master_address"] == "127.0.0.1"
assert kwargs["omni_master_port"] == 26000
assert kwargs["omni_stage_id"] == 0
assert kwargs["omni_stage_config"] is stage_cfg
assert kwargs["replica_id"] is None
assert "socket_ownership" not in kwargs
assert mock_connector_spec.call_args.kwargs["async_chunk"] is True
assert mock_manager_cls.call_count == 1
mgr_kwargs = mock_manager_cls.call_args.kwargs
assert mgr_kwargs["local_engine_count"] == 1
assert mgr_kwargs["local_client"] is False
assert mgr_kwargs["handshake_address"] == "tcp://127.0.0.1:26001"
assert mgr_kwargs["omni_stage_id"] == 0
assert mgr_kwargs["omni_coordinator_address"] == "tcp://127.0.0.1:26100"
assert mgr_kwargs["omni_replica_base_id"] == 0
engine_manager.monitor_engine_liveness.assert_called_once_with()
engine_manager.shutdown.assert_called_once_with()
def test_run_headless_llm_launches_one_manager_per_omni_dp_size_local(mocker: MockerFixture) -> None:
"""``--omni-dp-size-local=N`` must spawn N managers, each with its own
master-assigned replica_id, and join all of them before returning."""
from vllm_omni.engine.stage_engine_startup import StageRegistrationResponse
stage_cfg = _make_stage_cfg(0, stage_type="llm")
parallel_config = SimpleNamespace(
data_parallel_size_local=1,
data_parallel_rank=0,
data_parallel_rank_local=0,
node_rank_within_dp=0,
)
vllm_config = SimpleNamespace(parallel_config=parallel_config, needs_dp_coordinator=False)
manager_a = mocker.Mock()
manager_b = mocker.Mock()
mocker.patch(
"vllm_omni.entrypoints.utils.load_and_resolve_stage_configs",
return_value=("/fake/stages.yaml", [stage_cfg], None),
)
mocker.patch("vllm_omni.engine.stage_init_utils.prepare_engine_environment")
mocker.patch("vllm_omni.engine.stage_init_utils.load_omni_transfer_config_for_model", return_value=None)
mocker.patch(
"vllm_omni.distributed.omni_connectors.utils.initialization.resolve_omni_kv_config_for_stage",
return_value=(None, None, None),
)
mocker.patch("vllm_omni.engine.stage_init_utils.get_stage_connector_spec", return_value={})
mocker.patch("vllm_omni.engine.stage_init_utils.build_engine_args_dict", return_value={})
mocker.patch(
"vllm_omni.engine.stage_init_utils.build_vllm_config",
return_value=(vllm_config, object),
)
mocker.patch(
"vllm_omni.engine.stage_engine_startup.register_stage_with_omni_master",
side_effect=[
StageRegistrationResponse(
handshake_address=f"tcp://127.0.0.1:2700{idx}",
input_address=f"tcp://127.0.0.1:2710{idx}",
output_address=f"tcp://127.0.0.1:2720{idx}",
replica_id=idx,
coordinator_router_address=None,
)
for idx in (0, 1)
],
)
mock_manager_cls = mocker.patch(
"vllm_omni.engine.stage_engine_core_proc_manager.StageEngineCoreProcManager",
side_effect=[manager_a, manager_b],
)
mocker.patch("signal.signal")
run_headless(_make_headless_args(stage_id=0, omni_dp_size_local=2))
assert mock_manager_cls.call_count == 2
assigned_ids = [call.kwargs["omni_replica_base_id"] for call in mock_manager_cls.call_args_list]
assert assigned_ids == [0, 1]
# Multi-replica path joins the monitor threads instead of calling
# ``monitor_engine_liveness`` synchronously on the main thread, but every
# manager must still be shut down in the finally block.
manager_a.shutdown.assert_called_once_with()
manager_b.shutdown.assert_called_once_with()
def test_run_headless_diffusion_registers_and_spawns_proc(mocker: MockerFixture) -> None:
"""Diffusion headless: registers as auto-assign, spawns a single
``StageDiffusionProc`` per local replica, and waits for it via
``multiprocessing.connection.wait``."""
from vllm_omni.engine.stage_engine_startup import StageRegistrationResponse
stage_cfg = _make_stage_cfg(1, stage_type="diffusion")
od_config = mocker.Mock()
proc = mocker.Mock(sentinel=object(), exitcode=0)
proc.is_alive.return_value = False
mocker.patch(
"vllm_omni.entrypoints.utils.load_and_resolve_stage_configs",
return_value=("/fake/stages.yaml", [stage_cfg], None),
)
mocker.patch("vllm_omni.engine.stage_init_utils.prepare_engine_environment")
mocker.patch("vllm_omni.engine.stage_init_utils.load_omni_transfer_config_for_model", return_value=None)
mocker.patch(
"vllm_omni.distributed.omni_connectors.utils.initialization.resolve_omni_kv_config_for_stage",
return_value=(None, None, None),
)
mocker.patch(
"vllm_omni.engine.stage_init_utils.extract_legacy_stage_metadata",
return_value=SimpleNamespace(stage_id=1, stage_type="diffusion"),
)
mock_inject = mocker.patch("vllm_omni.engine.stage_init_utils.inject_kv_stage_info")
mocker.patch("vllm_omni.engine.stage_init_utils.build_diffusion_config", return_value=od_config)
mock_register = mocker.patch(
"vllm_omni.engine.stage_engine_startup.register_stage_with_omni_master",
return_value=StageRegistrationResponse(
handshake_address="tcp://127.0.0.1:26001",
input_address="tcp://127.0.0.1:26002",
output_address="tcp://127.0.0.1:26003",
replica_id=0,
coordinator_router_address="tcp://127.0.0.1:26100",
),
)
fake_manager = SimpleNamespace(
proc=proc,
addresses=SimpleNamespace(
inputs=["tcp://127.0.0.1:26002"],
outputs=["tcp://127.0.0.1:26003"],
),
shutdown=mocker.Mock(),
)
mock_manager = mocker.patch(
"vllm_omni.diffusion.stage_diffusion_proc.StageDiffusionProcManager.launch_headless",
return_value=fake_manager,
)
# Replace the blocking wait with one that returns the only proc's sentinel
# immediately so the test does not hang.
mocker.patch(
"multiprocessing.connection.wait",
side_effect=lambda sentinels: [sentinels[0]],
)
mocker.patch("signal.signal")
run_headless(_make_headless_args(stage_id=1))
mock_inject.assert_called_once()
assert mock_inject.call_args.args[0] is stage_cfg
assert mock_inject.call_args.args[1] == 1
assert mock_inject.call_args.args[2] == [stage_cfg]
reg_kwargs = mock_register.call_args.kwargs
assert reg_kwargs["omni_master_address"] == "127.0.0.1"
assert reg_kwargs["omni_master_port"] == 26000
assert reg_kwargs["omni_stage_id"] == 1
assert reg_kwargs["omni_stage_config"] is stage_cfg
assert reg_kwargs["replica_id"] is None
assert "socket_ownership" not in reg_kwargs
manager_kwargs = mock_manager.call_args.kwargs
assert manager_kwargs["handshake_address"] == "tcp://127.0.0.1:26001"
assert manager_kwargs["addresses"].inputs == ["tcp://127.0.0.1:26002"]
assert manager_kwargs["addresses"].outputs == ["tcp://127.0.0.1:26003"]
assert manager_kwargs["omni_coordinator_address"] == "tcp://127.0.0.1:26100"
assert manager_kwargs["omni_stage_id"] == 1
assert manager_kwargs["omni_replica_id"] == 0
def test_run_headless_diffusion_raises_on_nonzero_proc_exit(mocker: MockerFixture) -> None:
"""A diffusion replica that exits with a non-zero code must surface as a
RuntimeError from ``run_headless`` (the head needs the signal to roll
back its own stage init)."""
from vllm_omni.engine.stage_engine_startup import StageRegistrationResponse
stage_cfg = _make_stage_cfg(1, stage_type="diffusion")
proc = mocker.Mock(sentinel=object(), exitcode=137, name="proc-stage1-rep0")
proc.is_alive.return_value = False
mocker.patch(
"vllm_omni.entrypoints.utils.load_and_resolve_stage_configs",
return_value=("/fake/stages.yaml", [stage_cfg], None),
)
mocker.patch("vllm_omni.engine.stage_init_utils.prepare_engine_environment")
mocker.patch("vllm_omni.engine.stage_init_utils.load_omni_transfer_config_for_model", return_value=None)
mocker.patch(
"vllm_omni.distributed.omni_connectors.utils.initialization.resolve_omni_kv_config_for_stage",
return_value=(None, None, None),
)
mocker.patch(
"vllm_omni.engine.stage_init_utils.extract_legacy_stage_metadata",
return_value=SimpleNamespace(stage_id=1, stage_type="diffusion"),
)
mocker.patch("vllm_omni.engine.stage_init_utils.inject_kv_stage_info")
mocker.patch("vllm_omni.engine.stage_init_utils.build_diffusion_config", return_value=mocker.Mock())
mocker.patch(
"vllm_omni.engine.stage_engine_startup.register_stage_with_omni_master",
return_value=StageRegistrationResponse(
handshake_address="tcp://127.0.0.1:26001",
input_address="tcp://127.0.0.1:26002",
output_address="tcp://127.0.0.1:26003",
replica_id=0,
coordinator_router_address=None,
),
)
mocker.patch(
"vllm_omni.diffusion.stage_diffusion_proc.StageDiffusionProcManager.launch_headless",
return_value=SimpleNamespace(proc=proc, shutdown=mocker.Mock()),
)
mocker.patch(
"multiprocessing.connection.wait",
side_effect=lambda sentinels: [sentinels[0]],
)
mocker.patch("signal.signal")
with pytest.raises(RuntimeError, match=r"exited with code 137"):
run_headless(_make_headless_args(stage_id=1))