Files
vllm-omni/tests/engine/test_membership_controller.py
T
05b794f29b [Full Duplex] Feat: Support Full-Duplex realtime runtime & add MiniCPM-o 4.5 demo (#3907)
Signed-off-by: Sy03 <1370724210@qq.com>
Signed-off-by: lishunyang12 <lishunyang03@gmail.com>
Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
Co-authored-by: lishunyang12 <lishunyang03@gmail.com>
Co-authored-by: Yueqian Lin <linyueqian@outlook.com>
Co-authored-by: StormMelody <22371184@zju.edu.cn>
Co-authored-by: Gao Han <hgaoaf@connect.ust.hk>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-07-23 16:45:18 +08:00

191 lines
5.9 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import asyncio
import threading
from types import SimpleNamespace
import pytest
from vllm_omni.distributed.omni_coordinator.messages import ReplicaStatus
from vllm_omni.engine.membership_controller import MembershipController
from vllm_omni.engine.stage_pool import StagePool
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class FakeHub:
def __init__(self, snapshots=None):
self.snapshots = list(snapshots or [])
self.closed = False
def get_replica_list(self):
if self.snapshots:
return self.snapshots.pop(0)
return SimpleNamespace(replicas=[])
def close(self):
self.closed = True
class FakePool:
def __init__(self, stage_id: int):
self.stage_id = stage_id
self.clients = []
self.added = []
self.removed = []
self.invalidated = []
self.hub = None
self.lb = None
def attach_hub(self, hub):
self.hub = hub
def attach_load_balancer(self, lb):
self.lb = lb
def add_client(self, input_addr, client, *, replica_id=None):
self.added.append((input_addr, client, replica_id))
self.clients.append(client)
return len(self.clients) - 1 if replica_id is None else replica_id
def invalidate_addr(self, input_addr):
self.invalidated.append(input_addr)
return ["req-1", "req-2"]
def remove_client(self, input_addr):
self.removed.append(input_addr)
return SimpleNamespace(shutdown=lambda: self.removed.append("shutdown"))
def _snapshot(*replicas):
return SimpleNamespace(replicas=list(replicas))
def _replica(stage_id: int, input_addr: str, status=ReplicaStatus.UP):
return SimpleNamespace(stage_id=stage_id, input_addr=input_addr, status=status)
def _controller(monkeypatch, pool, hub, remote_replica_factory=None):
import vllm_omni.engine.membership_controller as membership_mod
monkeypatch.setattr(membership_mod, "OmniCoordClientForHub", lambda _addr: hub)
if remote_replica_factory is None:
def remote_replica_factory(stage_id, replica_id):
return SimpleNamespace(client_addresses={"input_address": f"tcp://stage-{stage_id}-replica-{replica_id}"})
return MembershipController(
stage_pools=[pool],
coordinator_pub_address="tcp://127.0.0.1:12345",
load_balancer_factory=lambda: object(),
remote_replica_factory=remote_replica_factory,
)
@pytest.mark.asyncio
async def test_watch_replica_list_unregisters_disappeared_replicas(monkeypatch):
pool = FakePool(stage_id=0)
hub = FakeHub(
snapshots=[
_snapshot(_replica(0, "tcp://gone")),
_snapshot(),
]
)
controller = _controller(monkeypatch, pool, hub)
controller.WATCH_INTERVAL_S = 0
unregistered = []
async def _handle_unregister(stage_id, input_addr, *args, **kwargs):
unregistered.append((stage_id, input_addr))
controller._shutdown_event.set()
controller.handle_unregister = _handle_unregister # type: ignore[method-assign]
await asyncio.wait_for(controller._watch_replica_list(), timeout=1)
await controller.drain_tasks(timeout=1)
assert unregistered == [(0, "tcp://gone")]
@pytest.mark.asyncio
async def test_shutdown_closes_hub_then_cancels_watcher(monkeypatch):
pool = FakePool(stage_id=0)
hub = FakeHub()
controller = _controller(monkeypatch, pool, hub)
watcher = asyncio.create_task(asyncio.sleep(10))
controller._watcher_task = watcher
controller.shutdown()
await asyncio.sleep(0)
assert hub.closed is True
assert watcher.cancelled()
@pytest.mark.asyncio
async def test_drain_tasks_waits_for_membership_tasks(monkeypatch):
pool = FakePool(stage_id=0)
controller = _controller(monkeypatch, pool, FakeHub())
completed = []
async def _task():
await asyncio.sleep(0)
completed.append(True)
controller._spawn_task(_task(), label="unit")
await controller.drain_tasks(timeout=1)
assert completed == [True]
@pytest.mark.asyncio
async def test_do_register_offloads_remote_factory(monkeypatch):
pool = FakePool(stage_id=0)
factory_thread_ids = []
def _factory(stage_id, replica_id):
factory_thread_ids.append(threading.get_ident())
return SimpleNamespace(client_addresses={"input_address": f"tcp://stage-{stage_id}-replica-{replica_id}"})
controller = _controller(monkeypatch, pool, FakeHub(), remote_replica_factory=_factory)
await controller._do_register(0, 1)
assert factory_thread_ids
assert factory_thread_ids[0] != threading.get_ident()
assert pool.added[0] == ("tcp://stage-0-replica-1", pool.clients[0], 1)
@pytest.mark.asyncio
async def test_unregister_then_register_restores_coordinator_replica_slot(monkeypatch):
class Client:
def __init__(self, input_addr: str) -> None:
self.client_addresses = {"input_address": input_addr}
self.shutdown_calls = 0
def shutdown(self) -> None:
self.shutdown_calls += 1
old_clients = [Client("tcp://old-0"), Client("tcp://old-1")]
replacement = Client("tcp://new-1")
pool = StagePool(0, old_clients)
def factory(stage_id: int, replica_id: int):
assert (stage_id, replica_id) == (0, 1)
return replacement
controller = _controller(monkeypatch, pool, FakeHub(), remote_replica_factory=factory)
await controller.handle_unregister(0, "tcp://old-0")
await controller.handle_unregister(0, "tcp://old-1")
await controller.handle_register(0, 1)
await controller.drain_tasks(timeout=1)
assert pool.clients == [None, replacement]
assert pool.get_replica_id_by_addr("tcp://new-1") == 1
assert pool.get_replica_id_by_addr("tcp://old-1") is None
assert pool.available_replica_ids() == [1]