Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82aa524c8e |
No files matched your search
+12
-58
@@ -659,14 +659,6 @@ class OmniRunner:
|
|||||||
self.seed = seed
|
self.seed = seed
|
||||||
self._prompt_len_estimate_cache: dict[str, Any] = {}
|
self._prompt_len_estimate_cache: dict[str, Any] = {}
|
||||||
self.omni: Any = None
|
self.omni: Any = None
|
||||||
self._runner_process = psutil.Process()
|
|
||||||
self._initial_child_ids: set[tuple[int, float]] = set()
|
|
||||||
self._owned_engine_processes: dict[int, float] = {}
|
|
||||||
for child in self._runner_process.children(recursive=True):
|
|
||||||
try:
|
|
||||||
self._initial_child_ids.add((child.pid, child.create_time()))
|
|
||||||
except psutil.Error:
|
|
||||||
pass
|
|
||||||
try:
|
try:
|
||||||
from vllm_omni.entrypoints.omni import Omni
|
from vllm_omni.entrypoints.omni import Omni
|
||||||
|
|
||||||
@@ -678,14 +670,12 @@ class OmniRunner:
|
|||||||
deploy_config=deploy_config,
|
deploy_config=deploy_config,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
self._remember_engine_processes(startup=True)
|
|
||||||
startup_s = time.perf_counter() - startup_t0
|
startup_s = time.perf_counter() - startup_t0
|
||||||
if log_stats:
|
if log_stats:
|
||||||
print(f"OmniRunner startup took {startup_s:.3f}s (model={model_name})", flush=True)
|
print(f"OmniRunner startup took {startup_s:.3f}s (model={model_name})", flush=True)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
# ``with OmniRunner(...)`` never reaches ``__enter__``/``__exit__``
|
# ``with OmniRunner(...)`` never reaches ``__enter__``/``__exit__``
|
||||||
# when construction fails after worker processes have started.
|
# when construction fails after worker processes have started.
|
||||||
self._remember_engine_processes(startup=True)
|
|
||||||
self.__exit__(None, None, None)
|
self.__exit__(None, None, None)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -909,51 +899,18 @@ class OmniRunner:
|
|||||||
def stop_profile(self, stages: list[int] | None = None) -> list[Any]:
|
def stop_profile(self, stages: list[int] | None = None) -> list[Any]:
|
||||||
return self.omni.stop_profile(stages=stages)
|
return self.omni.stop_profile(stages=stages)
|
||||||
|
|
||||||
def _remember_engine_processes(self, *, startup: bool = False) -> None:
|
|
||||||
"""Track startup workers, then only descendants of those workers.
|
|
||||||
|
|
||||||
Existing children may belong to another runner or a shared transcriber.
|
|
||||||
Record identities before close() can reparent surviving workers.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if startup:
|
|
||||||
candidates = self._runner_process.children(recursive=True)
|
|
||||||
else:
|
|
||||||
candidates = []
|
|
||||||
for pid, created in list(self._owned_engine_processes.items()):
|
|
||||||
try:
|
|
||||||
proc = psutil.Process(pid)
|
|
||||||
if proc.create_time() == created:
|
|
||||||
candidates.extend(proc.children(recursive=True))
|
|
||||||
except psutil.Error:
|
|
||||||
pass
|
|
||||||
for proc in candidates:
|
|
||||||
try:
|
|
||||||
identity = (proc.pid, proc.create_time())
|
|
||||||
if identity in self._initial_child_ids:
|
|
||||||
continue
|
|
||||||
if startup and any(
|
|
||||||
(parent.pid, parent.create_time()) in self._initial_child_ids for parent in proc.parents()
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
cmdline = " ".join(proc.cmdline()).lower()
|
|
||||||
if "enginecore" in cmdline or "enginecore" in proc.name().lower():
|
|
||||||
self._owned_engine_processes[identity[0]] = identity[1]
|
|
||||||
except psutil.Error:
|
|
||||||
pass
|
|
||||||
except psutil.Error as error:
|
|
||||||
print(f"Warning: could not inspect runner workers: {error}")
|
|
||||||
|
|
||||||
def _cleanup_process(self):
|
def _cleanup_process(self):
|
||||||
try:
|
try:
|
||||||
|
keywords = ["enginecore"]
|
||||||
matched = []
|
matched = []
|
||||||
for pid, created in self._owned_engine_processes.items():
|
for proc in psutil.process_iter(["pid", "name", "cmdline", "username"]):
|
||||||
try:
|
try:
|
||||||
proc = psutil.Process(pid)
|
cmdline = " ".join(proc.cmdline()).lower() if proc.cmdline() else ""
|
||||||
if proc.create_time() == created:
|
name = proc.name().lower()
|
||||||
print(f"Found owned vllm process: PID={proc.pid}")
|
if any(k in cmdline for k in keywords) or any(k in name for k in keywords):
|
||||||
|
print(f"Found vllm process: PID={proc.pid}, cmd={cmdline[:100]}")
|
||||||
matched.append(proc)
|
matched.append(proc)
|
||||||
except psutil.Error:
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
pass
|
pass
|
||||||
for proc in matched:
|
for proc in matched:
|
||||||
try:
|
try:
|
||||||
@@ -981,14 +938,11 @@ class OmniRunner:
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
self._remember_engine_processes()
|
omni = getattr(self, "omni", None)
|
||||||
try:
|
if omni is not None and hasattr(omni, "close"):
|
||||||
omni = getattr(self, "omni", None)
|
omni.close()
|
||||||
if omni is not None and hasattr(omni, "close"):
|
self._cleanup_process()
|
||||||
omni.close()
|
cleanup_test_environment()
|
||||||
finally:
|
|
||||||
self._cleanup_process()
|
|
||||||
cleanup_test_environment()
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
"""Tests for OmniRunner startup rollback and worker ownership."""
|
"""Tests for OmniRunner startup rollback."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from collections.abc import Generator
|
|
||||||
|
|
||||||
import psutil
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests.helpers.runtime import OmniRunner
|
from tests.helpers.runtime import OmniRunner
|
||||||
@@ -43,114 +38,3 @@ def test_runner_init_rolls_back_on_omni_startup_failure(monkeypatch: pytest.Monk
|
|||||||
assert cleaned.count("process") == 1
|
assert cleaned.count("process") == 1
|
||||||
# Once at the start of ``__init__``, once from the constructor rollback.
|
# Once at the start of ``__init__``, once from the constructor rollback.
|
||||||
assert cleaned.count("env") == 2
|
assert cleaned.count("env") == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def live_workers() -> Generator[list[subprocess.Popen[bytes]], None, None]:
|
|
||||||
workers: list[subprocess.Popen[bytes]] = []
|
|
||||||
try:
|
|
||||||
yield workers
|
|
||||||
finally:
|
|
||||||
for worker in workers:
|
|
||||||
if worker.poll() is None:
|
|
||||||
worker.kill()
|
|
||||||
worker.wait(timeout=10)
|
|
||||||
if worker.stdout is not None:
|
|
||||||
worker.stdout.close()
|
|
||||||
|
|
||||||
|
|
||||||
def _start_engine_worker(
|
|
||||||
workers: list[subprocess.Popen[bytes]], *, ignore_terminate: bool = False
|
|
||||||
) -> subprocess.Popen[bytes]:
|
|
||||||
script = "import signal, time; "
|
|
||||||
if ignore_terminate:
|
|
||||||
script += "signal.signal(signal.SIGTERM, signal.SIG_IGN); "
|
|
||||||
script += "print('ready', flush=True); time.sleep(90)"
|
|
||||||
worker = subprocess.Popen([sys.executable, "-c", script, "enginecore"], stdout=subprocess.PIPE)
|
|
||||||
workers.append(worker)
|
|
||||||
assert worker.stdout is not None
|
|
||||||
assert worker.stdout.readline() == b"ready\n"
|
|
||||||
return worker
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("close_raises", [False, True])
|
|
||||||
def test_runner_cleanup_preserves_unrelated_engine_workers(
|
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
live_workers: list[subprocess.Popen[bytes]],
|
|
||||||
close_raises: bool,
|
|
||||||
) -> None:
|
|
||||||
existing = _start_engine_worker(live_workers)
|
|
||||||
owned: list[subprocess.Popen[bytes]] = []
|
|
||||||
late_unrelated: list[subprocess.Popen[bytes]] = []
|
|
||||||
monkeypatch.setattr("tests.helpers.runtime.cleanup_test_environment", lambda: None)
|
|
||||||
|
|
||||||
class _OwnedOmni:
|
|
||||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
|
||||||
owned.append(_start_engine_worker(live_workers))
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
late_unrelated.append(_start_engine_worker(live_workers))
|
|
||||||
if close_raises:
|
|
||||||
raise RuntimeError("close failed")
|
|
||||||
|
|
||||||
monkeypatch.setattr("vllm_omni.entrypoints.omni.Omni", _OwnedOmni)
|
|
||||||
if close_raises:
|
|
||||||
with pytest.raises(RuntimeError, match="close failed"), OmniRunner("fake-model"):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
with OmniRunner("fake-model"):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert owned[0].poll() is not None
|
|
||||||
assert existing.poll() is None
|
|
||||||
assert late_unrelated[0].poll() is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_runner_startup_failure_cleans_only_new_workers(
|
|
||||||
monkeypatch: pytest.MonkeyPatch, live_workers: list[subprocess.Popen[bytes]]
|
|
||||||
) -> None:
|
|
||||||
unrelated = _start_engine_worker(live_workers)
|
|
||||||
owned: list[subprocess.Popen[bytes]] = []
|
|
||||||
monkeypatch.setattr("tests.helpers.runtime.cleanup_test_environment", lambda: None)
|
|
||||||
|
|
||||||
class _FailingOmni:
|
|
||||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
|
||||||
owned.append(_start_engine_worker(live_workers))
|
|
||||||
raise RuntimeError("startup failed")
|
|
||||||
|
|
||||||
monkeypatch.setattr("vllm_omni.entrypoints.omni.Omni", _FailingOmni)
|
|
||||||
with pytest.raises(RuntimeError, match="startup failed"):
|
|
||||||
OmniRunner("fake-model")
|
|
||||||
|
|
||||||
assert owned[0].poll() is not None
|
|
||||||
assert unrelated.poll() is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_runner_cleanup_rejects_stale_process_identity(live_workers: list[subprocess.Popen[bytes]]) -> None:
|
|
||||||
unrelated = _start_engine_worker(live_workers)
|
|
||||||
runner = object.__new__(OmniRunner)
|
|
||||||
runner._owned_engine_processes = {unrelated.pid: psutil.Process(unrelated.pid).create_time() + 1}
|
|
||||||
|
|
||||||
runner._cleanup_process()
|
|
||||||
|
|
||||||
assert unrelated.poll() is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_runner_cleanup_kills_owned_worker_ignoring_terminate(
|
|
||||||
monkeypatch: pytest.MonkeyPatch, live_workers: list[subprocess.Popen[bytes]]
|
|
||||||
) -> None:
|
|
||||||
owned: list[subprocess.Popen[bytes]] = []
|
|
||||||
monkeypatch.setattr("tests.helpers.runtime.cleanup_test_environment", lambda: None)
|
|
||||||
|
|
||||||
class _StubbornOmni:
|
|
||||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
|
||||||
owned.append(_start_engine_worker(live_workers, ignore_terminate=True))
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
monkeypatch.setattr("vllm_omni.entrypoints.omni.Omni", _StubbornOmni)
|
|
||||||
with OmniRunner("fake-model"):
|
|
||||||
assert owned[0].poll() is None
|
|
||||||
|
|
||||||
assert owned[0].poll() is not None
|
|
||||||
@@ -13,6 +13,7 @@ from vllm import SamplingParams
|
|||||||
from vllm.config.compilation import CUDAGraphMode
|
from vllm.config.compilation import CUDAGraphMode
|
||||||
from vllm.v1.core.sched.output import SchedulerOutput
|
from vllm.v1.core.sched.output import SchedulerOutput
|
||||||
from vllm.v1.worker.gpu.model_runner import GPUModelRunner
|
from vllm.v1.worker.gpu.model_runner import GPUModelRunner
|
||||||
|
from vllm.v1.worker.gpu.model_states.default import DefaultModelState
|
||||||
from vllm.v1.worker.gpu.sample.sampler import Sampler
|
from vllm.v1.worker.gpu.sample.sampler import Sampler
|
||||||
|
|
||||||
from vllm_omni.config.model import OmniModelConfig
|
from vllm_omni.config.model import OmniModelConfig
|
||||||
@@ -163,6 +164,28 @@ def test_init_model_state_factory_dispatches_omni_only(monkeypatch, flag, expect
|
|||||||
assert state is upstream.return_value
|
assert state is upstream.return_value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("omni_stage", [False, True])
|
||||||
|
@pytest.mark.parametrize("specialized", [False, True])
|
||||||
|
def test_auxiliary_default_state_has_omni_lifecycle_but_preserves_upstream_states(monkeypatch, omni_stage, specialized):
|
||||||
|
class SpecializedState(DefaultModelState):
|
||||||
|
pass
|
||||||
|
|
||||||
|
state_cls = SpecializedState if specialized else DefaultModelState
|
||||||
|
upstream_state = object.__new__(state_cls)
|
||||||
|
monkeypatch.setattr("vllm_omni.worker_v2.model_states._upstream_init_model_state", lambda *_args: upstream_state)
|
||||||
|
monkeypatch.setattr(OmniModelState, "__init__", lambda *_args: None)
|
||||||
|
model_config = object.__new__(OmniModelConfig) if omni_stage else SimpleNamespace()
|
||||||
|
state = init_omni_model_state(
|
||||||
|
SimpleNamespace(model_config=model_config), SimpleNamespace(), None, torch.device("cpu")
|
||||||
|
)
|
||||||
|
if omni_stage and not specialized:
|
||||||
|
assert isinstance(state, OmniModelState)
|
||||||
|
assert callable(state.run_preprocess)
|
||||||
|
assert callable(state.postprocess_model_output)
|
||||||
|
else:
|
||||||
|
assert state is upstream_state
|
||||||
|
|
||||||
|
|
||||||
def test_finish_requests_notifies_model_and_cleans_only_known_slots(monkeypatch):
|
def test_finish_requests_notifies_model_and_cleans_only_known_slots(monkeypatch):
|
||||||
runner = _make_runner()
|
runner = _make_runner()
|
||||||
calls = []
|
calls = []
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"""Omni-aware ``init_model_state`` factory.
|
"""Omni-aware ``init_model_state`` factory.
|
||||||
|
|
||||||
Extends the upstream v2 factory with Omni architecture dispatch.
|
Extends the upstream v2 factory with Omni architecture dispatch.
|
||||||
Non-Omni architectures fall through to the upstream ``init_model_state``.
|
Non-Omni architectures use upstream dispatch; its default state acquires
|
||||||
|
Omni lifecycle support when owned by an Omni stage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -16,8 +17,11 @@ from vllm.v1.worker.gpu.mm.encoder_cache import EncoderCache
|
|||||||
from vllm.v1.worker.gpu.model_states import (
|
from vllm.v1.worker.gpu.model_states import (
|
||||||
init_model_state as _upstream_init_model_state,
|
init_model_state as _upstream_init_model_state,
|
||||||
)
|
)
|
||||||
|
from vllm.v1.worker.gpu.model_states.default import DefaultModelState
|
||||||
from vllm.v1.worker.gpu.model_states.interface import ModelState
|
from vllm.v1.worker.gpu.model_states.interface import ModelState
|
||||||
|
|
||||||
|
from vllm_omni.config.model import OmniModelConfig
|
||||||
|
|
||||||
|
|
||||||
def init_omni_model_state(
|
def init_omni_model_state(
|
||||||
vllm_config: VllmConfig,
|
vllm_config: VllmConfig,
|
||||||
@@ -28,8 +32,8 @@ def init_omni_model_state(
|
|||||||
"""Create the appropriate ``ModelState`` for *model*.
|
"""Create the appropriate ``ModelState`` for *model*.
|
||||||
|
|
||||||
Returns an ``OmniModelState`` when the model declares Omni lifecycle
|
Returns an ``OmniModelState`` when the model declares Omni lifecycle
|
||||||
capabilities; otherwise
|
capabilities. Otherwise delegates to the upstream v2 factory, wrapping
|
||||||
delegates to the upstream v2 factory.
|
its default state for auxiliary Omni stages.
|
||||||
"""
|
"""
|
||||||
factory = getattr(model, "create_omni_model_state", None)
|
factory = getattr(model, "create_omni_model_state", None)
|
||||||
if factory is not None:
|
if factory is not None:
|
||||||
@@ -49,4 +53,12 @@ def init_omni_model_state(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return OmniModelState(vllm_config, model, encoder_cache, device)
|
return OmniModelState(vllm_config, model, encoder_cache, device)
|
||||||
return _upstream_init_model_state(vllm_config, model, encoder_cache, device)
|
state = _upstream_init_model_state(vllm_config, model, encoder_cache, device)
|
||||||
|
if type(state) is DefaultModelState and isinstance(getattr(vllm_config, "model_config", None), OmniModelConfig):
|
||||||
|
# Auxiliary stages such as the forced aligner have no model-owned
|
||||||
|
# hooks, but Omni's runner still needs its request/output lifecycle.
|
||||||
|
# Keep specialized upstream states and plain vLLM configs intact.
|
||||||
|
from vllm_omni.worker_v2.model_states.omni_model_state import OmniModelState
|
||||||
|
|
||||||
|
return OmniModelState(vllm_config, model, encoder_cache, device)
|
||||||
|
return state
|
||||||
Reference in new issue
Block a user