2026-05-06 05:47:13 +08:00
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
import queue
|
|
|
|
|
import threading
|
|
|
|
|
import time
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class DiffusionSchedulerOutput:
|
|
|
|
|
step_id: int
|
|
|
|
|
scheduled_new_reqs: list = field(default_factory=list)
|
|
|
|
|
scheduled_cached_reqs: Any = None
|
|
|
|
|
finished_req_ids: set = field(default_factory=set)
|
|
|
|
|
num_running_reqs: int = 0
|
|
|
|
|
num_waiting_reqs: int = 0
|
|
|
|
|
|
|
|
|
|
@property
|
2026-05-28 14:56:56 +08:00
|
|
|
def scheduled_request_ids(self):
|
2026-05-06 05:47:13 +08:00
|
|
|
ids = [req.request_id for req in self.scheduled_new_reqs]
|
|
|
|
|
if self.scheduled_cached_reqs:
|
2026-05-28 14:56:56 +08:00
|
|
|
ids.extend(self.scheduled_cached_reqs.request_ids)
|
2026-05-06 05:47:13 +08:00
|
|
|
return ids
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_empty(self):
|
2026-05-28 14:56:56 +08:00
|
|
|
return len(self.scheduled_request_ids) == 0
|
2026-05-06 05:47:13 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class MockScheduler:
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self._waiting_queue = []
|
|
|
|
|
self._step_id = 0
|
|
|
|
|
|
|
|
|
|
def add_request(self, request):
|
|
|
|
|
self._waiting_queue.append(request)
|
|
|
|
|
return request.request_id
|
|
|
|
|
|
|
|
|
|
def has_requests(self):
|
|
|
|
|
return len(self._waiting_queue) > 0
|
|
|
|
|
|
|
|
|
|
def schedule(self) -> DiffusionSchedulerOutput:
|
|
|
|
|
if not self._waiting_queue:
|
|
|
|
|
return DiffusionSchedulerOutput(step_id=self._step_id)
|
|
|
|
|
|
|
|
|
|
batch = []
|
|
|
|
|
while self._waiting_queue:
|
|
|
|
|
req = self._waiting_queue.pop(0)
|
|
|
|
|
batch.append(SimpleNamespace(request_id=req.request_id))
|
|
|
|
|
|
|
|
|
|
output = DiffusionSchedulerOutput(step_id=self._step_id, scheduled_new_reqs=batch)
|
|
|
|
|
self._step_id += 1
|
|
|
|
|
return output
|
|
|
|
|
|
|
|
|
|
def update_from_output(self, sched_output, runner_output):
|
|
|
|
|
# assume all new req finished
|
|
|
|
|
return [req.request_id for req in sched_output.scheduled_new_reqs]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_async_add_req_and_wait_for_response():
|
|
|
|
|
from vllm_omni.diffusion.diffusion_engine import DiffusionEngine
|
|
|
|
|
|
|
|
|
|
engine = object.__new__(DiffusionEngine)
|
|
|
|
|
engine.scheduler = MockScheduler()
|
|
|
|
|
engine._out_queue = {}
|
|
|
|
|
engine.abort_queue: queue.Queue[str] = queue.Queue()
|
2026-05-28 14:56:56 +08:00
|
|
|
engine._rpc_queue = queue.Queue()
|
|
|
|
|
engine._rpc_lock = threading.RLock()
|
|
|
|
|
engine._cv = threading.Condition(engine._rpc_lock)
|
|
|
|
|
engine._init_lock = asyncio.Lock()
|
|
|
|
|
engine._closed = False
|
|
|
|
|
engine._loop_started = False
|
|
|
|
|
engine.main_loop = None
|
2026-05-06 05:47:13 +08:00
|
|
|
|
|
|
|
|
engine._finalize_finished_request = lambda rid, out, err: out.result
|
|
|
|
|
|
|
|
|
|
def mock_execute_batch(sched_output):
|
2026-05-28 14:56:56 +08:00
|
|
|
request_ids = sched_output.scheduled_request_ids
|
2026-05-06 05:47:13 +08:00
|
|
|
|
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
|
|
class MockRunnerOutput:
|
|
|
|
|
def __init__(self, ids):
|
2026-05-28 14:56:56 +08:00
|
|
|
self.request_id = ids
|
2026-05-06 05:47:13 +08:00
|
|
|
self.step_index = 0
|
|
|
|
|
self.finished = True
|
|
|
|
|
self._results = {rid: SimpleNamespace(result_data=f"data_{rid}") for rid in ids}
|
|
|
|
|
|
2026-05-28 14:56:56 +08:00
|
|
|
def get_request_output(self, rid):
|
2026-05-06 05:47:13 +08:00
|
|
|
return SimpleNamespace(result=self._results[rid], step_index=0, finished=True)
|
|
|
|
|
|
2026-05-28 14:56:56 +08:00
|
|
|
return MockRunnerOutput(request_ids)
|
2026-05-06 05:47:13 +08:00
|
|
|
|
|
|
|
|
engine.execute_fn = mock_execute_batch
|
|
|
|
|
|
2026-05-28 14:56:56 +08:00
|
|
|
await engine._check_and_start_background_loop()
|
2026-05-06 05:47:13 +08:00
|
|
|
|
|
|
|
|
async def run_task(rid):
|
|
|
|
|
req = SimpleNamespace(request_id=rid)
|
|
|
|
|
start = time.time()
|
|
|
|
|
res = await engine.async_add_req_and_wait_for_response(req)
|
|
|
|
|
return rid, res, time.time() - start
|
|
|
|
|
|
|
|
|
|
task_ids = [f"req_{i}" for i in range(5)]
|
|
|
|
|
tasks = [run_task(rid) for rid in task_ids]
|
2026-05-28 14:56:56 +08:00
|
|
|
try:
|
|
|
|
|
results = await asyncio.gather(*tasks)
|
|
|
|
|
finally:
|
|
|
|
|
with engine._cv:
|
|
|
|
|
engine.stop_event.set()
|
|
|
|
|
engine._cv.notify_all()
|
|
|
|
|
engine.worker_thread.join(timeout=5)
|
2026-05-06 05:47:13 +08:00
|
|
|
assert len(results) == 5
|
|
|
|
|
for rid, res, elapsed in results:
|
|
|
|
|
assert rid in res.result_data
|
|
|
|
|
|
|
|
|
|
eps = 0.5
|
|
|
|
|
latencies = [r[2] for r in results]
|
|
|
|
|
assert max(latencies) - min(latencies) < eps
|