Signed-off-by: jader <yjader@foxmail.com> Signed-off-by: JiangJie Zhang <76905040+yJader@users.noreply.github.com> Co-authored-by: SYLAR <125541396+lishunyang12@users.noreply.github.com> Co-authored-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
300 lines
11 KiB
Python
300 lines
11 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
import threading
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
|
|
from vllm_omni.diffusion.data import DiffusionOutput
|
|
from vllm_omni.diffusion.diffusion_engine import DiffusionEngine
|
|
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
|
from vllm_omni.diffusion.sched import (
|
|
DiffusionRequestStatus,
|
|
RequestScheduler,
|
|
Scheduler,
|
|
SchedulerInterface,
|
|
)
|
|
from vllm_omni.diffusion.sched.interface import CachedRequestData, NewRequestData
|
|
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
|
|
|
|
|
def _make_request(req_id: str) -> OmniDiffusionRequest:
|
|
return OmniDiffusionRequest(
|
|
prompts=[f"prompt_{req_id}"],
|
|
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
|
|
request_ids=[req_id],
|
|
)
|
|
|
|
|
|
def _make_request_output(req_id: str, *, error: str | None = None) -> DiffusionOutput:
|
|
del req_id
|
|
return DiffusionOutput(output=None, error=error)
|
|
|
|
|
|
def _new_ids(sched_output) -> list[str]:
|
|
return [req.sched_req_id for req in sched_output.scheduled_new_reqs]
|
|
|
|
|
|
def _cached_ids(sched_output) -> list[str]:
|
|
return list(sched_output.scheduled_cached_reqs.sched_req_ids)
|
|
|
|
|
|
class _StubScheduler(SchedulerInterface):
|
|
def __init__(self, request: OmniDiffusionRequest, output: DiffusionOutput) -> None:
|
|
self._request = request
|
|
self._output = output
|
|
self.initialized_with = None
|
|
self._sched_req_id = request.request_ids[0]
|
|
self._state = None
|
|
self._scheduled = False
|
|
|
|
def initialize(self, od_config) -> None:
|
|
self.initialized_with = od_config
|
|
|
|
def add_request(self, request: OmniDiffusionRequest) -> str:
|
|
assert request is self._request
|
|
self._state = Mock(sched_req_id=self._sched_req_id, req=request)
|
|
return self._sched_req_id
|
|
|
|
def schedule(self):
|
|
if self._scheduled or self._state is None:
|
|
return Mock(
|
|
scheduled_new_reqs=[],
|
|
scheduled_cached_reqs=CachedRequestData.make_empty(),
|
|
scheduled_req_ids=[],
|
|
is_empty=True,
|
|
)
|
|
self._scheduled = True
|
|
return Mock(
|
|
scheduled_new_reqs=[NewRequestData.from_state(self._state)],
|
|
scheduled_cached_reqs=CachedRequestData.make_empty(),
|
|
scheduled_req_ids=[self._state.sched_req_id],
|
|
is_empty=False,
|
|
)
|
|
|
|
def update_from_output(self, sched_output, output: DiffusionOutput) -> set[str]:
|
|
del sched_output
|
|
assert output is self._output
|
|
return {self._sched_req_id}
|
|
|
|
def has_requests(self) -> bool:
|
|
return not self._scheduled
|
|
|
|
def get_request_state(self, sched_req_id: str):
|
|
del sched_req_id
|
|
return self._state
|
|
|
|
def get_sched_req_id(self, request_id: str) -> str | None:
|
|
if request_id in self._request.request_ids:
|
|
return self._sched_req_id
|
|
return None
|
|
|
|
def pop_request_state(self, sched_req_id: str):
|
|
del sched_req_id
|
|
return self._state
|
|
|
|
def preempt_request(self, sched_req_id: str) -> bool:
|
|
del sched_req_id
|
|
return False
|
|
|
|
def finish_requests(self, sched_req_ids, status) -> None:
|
|
del sched_req_ids, status
|
|
return None
|
|
|
|
def close(self) -> None:
|
|
return None
|
|
|
|
|
|
class TestRequestScheduler:
|
|
def setup_method(self) -> None:
|
|
self.scheduler: RequestScheduler = RequestScheduler()
|
|
self.scheduler.initialize(Mock())
|
|
|
|
def test_single_request_success_lifecycle(self) -> None:
|
|
req_id = self.scheduler.add_request(_make_request("a"))
|
|
assert self.scheduler.get_request_state(req_id).status == DiffusionRequestStatus.WAITING
|
|
|
|
sched_output = self.scheduler.schedule()
|
|
assert _new_ids(sched_output) == [req_id]
|
|
assert _cached_ids(sched_output) == []
|
|
assert sched_output.num_running_reqs == 1
|
|
assert sched_output.num_waiting_reqs == 0
|
|
|
|
finished = self.scheduler.update_from_output(sched_output, _make_request_output(req_id))
|
|
assert finished == {req_id}
|
|
assert self.scheduler.get_request_state(req_id).status == DiffusionRequestStatus.FINISHED_COMPLETED
|
|
assert self.scheduler.has_requests() is False
|
|
|
|
def test_error_output_marks_finished_error(self) -> None:
|
|
req_id = self.scheduler.add_request(_make_request("err"))
|
|
|
|
sched_output = self.scheduler.schedule()
|
|
finished = self.scheduler.update_from_output(
|
|
sched_output,
|
|
_make_request_output(req_id, error="worker failed"),
|
|
)
|
|
|
|
assert finished == {req_id}
|
|
state = self.scheduler.get_request_state(req_id)
|
|
assert state.status == DiffusionRequestStatus.FINISHED_ERROR
|
|
assert state.error == "worker failed"
|
|
|
|
def test_empty_output_without_error_marks_completed(self) -> None:
|
|
req_id = self.scheduler.add_request(_make_request("empty"))
|
|
|
|
sched_output = self.scheduler.schedule()
|
|
finished = self.scheduler.update_from_output(sched_output, _make_request_output(req_id))
|
|
|
|
assert finished == {req_id}
|
|
assert self.scheduler.get_request_state(req_id).status == DiffusionRequestStatus.FINISHED_COMPLETED
|
|
|
|
def test_fifo_single_request_scheduling(self) -> None:
|
|
req_id_a = self.scheduler.add_request(_make_request("a"))
|
|
req_id_b = self.scheduler.add_request(_make_request("b"))
|
|
|
|
first = self.scheduler.schedule()
|
|
assert _new_ids(first) == [req_id_a]
|
|
assert _cached_ids(first) == []
|
|
assert first.num_running_reqs == 1
|
|
assert first.num_waiting_reqs == 1
|
|
|
|
# Request A is still running; scheduling again should not pull B.
|
|
second = self.scheduler.schedule()
|
|
assert _new_ids(second) == []
|
|
assert _cached_ids(second) == [req_id_a]
|
|
assert second.num_running_reqs == 1
|
|
assert second.num_waiting_reqs == 1
|
|
|
|
self.scheduler.update_from_output(first, _make_request_output(req_id_a))
|
|
|
|
third = self.scheduler.schedule()
|
|
assert _new_ids(third) == [req_id_b]
|
|
assert _cached_ids(third) == []
|
|
assert third.num_running_reqs == 1
|
|
assert third.num_waiting_reqs == 0
|
|
|
|
def test_abort_request_for_waiting_and_running(self) -> None:
|
|
req_id_a = self.scheduler.add_request(_make_request("a"))
|
|
req_id_b = self.scheduler.add_request(_make_request("b"))
|
|
|
|
# Abort waiting request.
|
|
self.scheduler.finish_requests(req_id_b, DiffusionRequestStatus.FINISHED_ABORTED)
|
|
state_b = self.scheduler.get_request_state(req_id_b)
|
|
assert state_b.status == DiffusionRequestStatus.FINISHED_ABORTED
|
|
|
|
# A should still run normally.
|
|
output_a = self.scheduler.schedule()
|
|
assert _new_ids(output_a) == [req_id_a]
|
|
|
|
# Abort running request.
|
|
self.scheduler.finish_requests(req_id_a, DiffusionRequestStatus.FINISHED_ABORTED)
|
|
state_a = self.scheduler.get_request_state(req_id_a)
|
|
assert state_a.status == DiffusionRequestStatus.FINISHED_ABORTED
|
|
|
|
assert self.scheduler.has_requests() is False
|
|
assert self.scheduler.schedule().scheduled_req_ids == []
|
|
|
|
def test_has_requests_state_transition(self) -> None:
|
|
assert self.scheduler.has_requests() is False
|
|
|
|
req_id = self.scheduler.add_request(_make_request("has"))
|
|
assert self.scheduler.has_requests() is True
|
|
|
|
sched_output = self.scheduler.schedule()
|
|
assert self.scheduler.has_requests() is True
|
|
|
|
self.scheduler.update_from_output(sched_output, _make_request_output(req_id))
|
|
assert self.scheduler.get_request_state(req_id).status == DiffusionRequestStatus.FINISHED_COMPLETED
|
|
assert self.scheduler.has_requests() is False
|
|
|
|
def test_request_id_mapping_lifecycle(self) -> None:
|
|
request = OmniDiffusionRequest(
|
|
prompts=["prompt_map_a", "prompt_map_b"],
|
|
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
|
|
request_ids=["map-a", "map-b"],
|
|
)
|
|
|
|
sched_req_id = self.scheduler.add_request(request)
|
|
|
|
assert self.scheduler.get_sched_req_id("map-a") == sched_req_id
|
|
assert self.scheduler.get_sched_req_id("map-b") == sched_req_id
|
|
|
|
self.scheduler.pop_request_state(sched_req_id)
|
|
|
|
assert self.scheduler.get_sched_req_id("map-a") is None
|
|
assert self.scheduler.get_sched_req_id("map-b") is None
|
|
|
|
|
|
class TestDiffusionEngine:
|
|
def test_add_req_and_wait_for_response_single_path(self) -> None:
|
|
engine = DiffusionEngine.__new__(DiffusionEngine)
|
|
engine.scheduler = RequestScheduler()
|
|
engine.scheduler.initialize(Mock())
|
|
engine.executor = Mock()
|
|
engine._rpc_lock = threading.Lock()
|
|
|
|
request = _make_request("engine")
|
|
expected = DiffusionOutput(output=None)
|
|
engine.executor.add_req.return_value = expected
|
|
|
|
output = engine.add_req_and_wait_for_response(request)
|
|
|
|
assert output is expected
|
|
engine.executor.add_req.assert_called_once_with(request)
|
|
|
|
def test_supports_scheduler_interface_injection(self) -> None:
|
|
request = _make_request("engine_iface")
|
|
expected = DiffusionOutput(output=None)
|
|
scheduler = _StubScheduler(request, expected)
|
|
|
|
engine = DiffusionEngine.__new__(DiffusionEngine)
|
|
engine.scheduler = scheduler
|
|
engine.executor = Mock()
|
|
engine.executor.add_req = Mock(return_value=expected)
|
|
engine._rpc_lock = threading.Lock()
|
|
|
|
output = engine.add_req_and_wait_for_response(request)
|
|
|
|
assert output is expected
|
|
engine.executor.add_req.assert_called_once_with(request)
|
|
|
|
def test_initializes_injected_scheduler(self) -> None:
|
|
request = _make_request("init")
|
|
scheduler = _StubScheduler(request, DiffusionOutput(output=None))
|
|
od_config = Mock(model_class_name="mock_model")
|
|
fake_executor_cls = Mock(return_value=Mock())
|
|
|
|
with (
|
|
patch("vllm_omni.diffusion.diffusion_engine.get_diffusion_post_process_func", return_value=None),
|
|
patch("vllm_omni.diffusion.diffusion_engine.get_diffusion_pre_process_func", return_value=None),
|
|
patch("vllm_omni.diffusion.diffusion_engine.DiffusionExecutor.get_class", return_value=fake_executor_cls),
|
|
patch.object(DiffusionEngine, "_dummy_run", return_value=None),
|
|
):
|
|
DiffusionEngine(od_config, scheduler=scheduler)
|
|
|
|
assert scheduler.initialized_with is od_config
|
|
fake_executor_cls.assert_called_once_with(od_config)
|
|
|
|
def test_scheduler_alias_keeps_default_request_scheduler(self) -> None:
|
|
scheduler = Scheduler()
|
|
scheduler.initialize(Mock())
|
|
|
|
req_id = scheduler.add_request(_make_request("alias"))
|
|
sched_output = scheduler.schedule()
|
|
finished = scheduler.update_from_output(sched_output, _make_request_output(req_id))
|
|
|
|
assert req_id in finished
|
|
assert scheduler.get_request_state(req_id).status == DiffusionRequestStatus.FINISHED_COMPLETED
|
|
|
|
def test_dummy_run_raises_on_output_error(self) -> None:
|
|
engine = DiffusionEngine.__new__(DiffusionEngine)
|
|
engine.od_config = Mock(model_class_name="mock_model")
|
|
engine.pre_process_func = None
|
|
engine.add_req_and_wait_for_response = Mock(return_value=DiffusionOutput(error="boom"))
|
|
|
|
with pytest.raises(RuntimeError, match="Dummy run failed: boom"):
|
|
engine._dummy_run()
|