[Bugfix] Reclaim resumable async-chunk requests on finish (#6360)
Signed-off-by: natureofnature <wzliu@connect.hku.hk> Co-authored-by: Ruirui Yang | Rein <73573651+R2-Y@users.noreply.github.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
5 files changed
+301
-34
No files matched your search
@@ -3,8 +3,8 @@
|
||||
|
||||
import threading
|
||||
from collections import deque
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from types import MethodType, SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
@@ -13,6 +13,7 @@ from vllm.v1.core.sched.scheduler import Scheduler as VLLMScheduler
|
||||
from vllm.v1.metrics.stats import PrefillStats, PromptTokenStats
|
||||
from vllm.v1.request import RequestStatus
|
||||
|
||||
from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler
|
||||
from vllm_omni.core.sched.omni_generation_scheduler import OmniGenerationScheduler
|
||||
from vllm_omni.data_entry_keys import CodesStruct, MetaStruct, OmniPayload, OmniPayloadStruct
|
||||
from vllm_omni.distributed.omni_connectors.adapter import construct_next_stage_streaming_input_prompt
|
||||
@@ -32,9 +33,14 @@ class DummyWaitingQueue(list):
|
||||
def add_request(self, request):
|
||||
self.append(request)
|
||||
|
||||
def remove_requests(self, requests):
|
||||
remove = set(requests)
|
||||
self[:] = [request for request in self if request not in remove]
|
||||
|
||||
|
||||
def _req(req_id: str, status: RequestStatus, external_req_id: str | None = None):
|
||||
return SimpleNamespace(
|
||||
request = Mock(
|
||||
client_index=0,
|
||||
request_id=req_id,
|
||||
external_req_id=external_req_id or req_id,
|
||||
status=status,
|
||||
@@ -44,8 +50,10 @@ def _req(req_id: str, status: RequestStatus, external_req_id: str | None = None)
|
||||
num_output_placeholders=0,
|
||||
prefill_stats=None,
|
||||
additional_information=None,
|
||||
is_finished=lambda: status == RequestStatus.FINISHED_STOPPED,
|
||||
resumable=False,
|
||||
)
|
||||
request.is_finished = lambda: RequestStatus.is_finished(request.status)
|
||||
return request
|
||||
|
||||
|
||||
def test_streaming_payload_can_replace_placeholder_prompt(mocker: MockerFixture) -> None:
|
||||
@@ -1006,12 +1014,30 @@ def test_finish_requests_restores_status(build_adapter):
|
||||
prior = RequestStatus.RUNNING
|
||||
request = _req(req_id, RequestStatus.WAITING_FOR_CHUNK)
|
||||
adapter.requests_origin_status[req_id] = prior
|
||||
adapter.waiting_for_chunk_running_requests.append(request)
|
||||
requests_map = {req_id: request}
|
||||
|
||||
adapter.finish_requests([req_id], RequestStatus.FINISHED_ABORTED, requests_map)
|
||||
|
||||
assert request.status == prior
|
||||
assert req_id not in adapter.requests_origin_status
|
||||
assert not adapter.waiting_for_chunk_running_requests
|
||||
|
||||
|
||||
def test_finish_requests_does_not_restore_stale_status_without_connector_ownership(build_adapter):
|
||||
adapter, _ = build_adapter(stage_id=1)
|
||||
request = _req("req-restored", RequestStatus.WAITING_FOR_STREAMING_REQ)
|
||||
request.resumable = True
|
||||
adapter.requests_origin_status[request.request_id] = RequestStatus.RUNNING
|
||||
|
||||
adapter.finish_requests(
|
||||
[request.request_id],
|
||||
RequestStatus.FINISHED_ABORTED,
|
||||
{request.request_id: request},
|
||||
)
|
||||
|
||||
assert request.status == RequestStatus.WAITING_FOR_STREAMING_REQ
|
||||
assert request.request_id not in adapter.requests_origin_status
|
||||
|
||||
|
||||
def test_finish_requests_removes_zombies_from_chunk_waiting_deques(build_adapter):
|
||||
@@ -1061,6 +1087,108 @@ def test_finish_requests_releases_active_stream_slot(build_adapter):
|
||||
assert waiting.status == RequestStatus.WAITING_FOR_CHUNK
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scheduler_cls", [OmniGenerationScheduler, OmniARScheduler])
|
||||
@pytest.mark.parametrize(
|
||||
("placement", "origin_status", "initial_status", "streaming_counter_owned"),
|
||||
[
|
||||
("hidden", RequestStatus.RUNNING, RequestStatus.FINISHED_STOPPED, False),
|
||||
("running", RequestStatus.WAITING, RequestStatus.FINISHED_STOPPED, False),
|
||||
("waiting", RequestStatus.WAITING, RequestStatus.FINISHED_STOPPED, False),
|
||||
("skipped", RequestStatus.WAITING, RequestStatus.FINISHED_STOPPED, True),
|
||||
("skipped_streaming", RequestStatus.RUNNING, RequestStatus.WAITING_FOR_STREAMING_REQ, True),
|
||||
("skipped_hidden", RequestStatus.RUNNING, RequestStatus.FINISHED_STOPPED, True),
|
||||
("skipped_non_streaming", RequestStatus.WAITING, RequestStatus.WAITING, False),
|
||||
],
|
||||
)
|
||||
def test_finish_requests_reclaims_resumable_segment_and_reuses_capacity(
|
||||
build_adapter,
|
||||
scheduler_cls,
|
||||
placement,
|
||||
origin_status,
|
||||
initial_status,
|
||||
streaming_counter_owned,
|
||||
):
|
||||
adapter, _ = build_adapter(stage_id=1, active_stream_window=2)
|
||||
request = _req(
|
||||
"req-segment-stop",
|
||||
initial_status,
|
||||
external_req_id="ext-segment-stop",
|
||||
)
|
||||
request.resumable = True
|
||||
adapter.requests_origin_status[request.request_id] = origin_status
|
||||
if placement in {"hidden", "skipped_hidden"}:
|
||||
adapter.waiting_for_chunk_running_requests.append(request)
|
||||
adapter._active_streams[request.request_id] = request
|
||||
|
||||
scheduler = scheduler_cls.__new__(scheduler_cls)
|
||||
scheduler.chunk_transfer_adapter = adapter
|
||||
scheduler.input_coordinator = None
|
||||
scheduler.requests = {request.request_id: request}
|
||||
scheduler.running = [request] if placement == "running" else []
|
||||
scheduler.waiting = DummyWaitingQueue([request] if placement == "waiting" else [])
|
||||
scheduler.skipped_waiting = DummyWaitingQueue([request] if placement.startswith("skipped") else [])
|
||||
scheduler.num_waiting_for_streaming_input = int(streaming_counter_owned)
|
||||
|
||||
freed = []
|
||||
|
||||
def fake_free_request(self, live, delay_free_blocks=False):
|
||||
del delay_free_blocks
|
||||
freed.append(live.request_id)
|
||||
self.requests.pop(live.request_id)
|
||||
return None, None
|
||||
|
||||
scheduler._free_request = MethodType(fake_free_request, scheduler)
|
||||
|
||||
first = scheduler_cls.finish_requests(scheduler, [request.request_id], RequestStatus.FINISHED_ABORTED)
|
||||
second = scheduler_cls.finish_requests(scheduler, [request.request_id], RequestStatus.FINISHED_ABORTED)
|
||||
|
||||
assert len(first) == 1
|
||||
assert second == []
|
||||
assert freed == [request.request_id]
|
||||
assert request.request_id not in scheduler.requests
|
||||
assert request.request_id not in adapter._active_streams
|
||||
assert not adapter.waiting_for_chunk_running_requests
|
||||
assert scheduler.running == []
|
||||
assert list(scheduler.waiting) == []
|
||||
assert list(scheduler.skipped_waiting) == []
|
||||
assert scheduler.num_waiting_for_streaming_input == 0
|
||||
|
||||
fresh_a = _req("req-fresh-a", RequestStatus.WAITING)
|
||||
fresh_b = _req("req-fresh-b", RequestStatus.WAITING)
|
||||
assert adapter._ensure_active_stream(fresh_a)
|
||||
assert adapter._ensure_active_stream(fresh_b)
|
||||
assert set(adapter._active_streams) == {fresh_a.request_id, fresh_b.request_id}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scheduler_cls", [OmniGenerationScheduler, OmniARScheduler])
|
||||
def test_finish_requests_does_not_reopen_off_queue_resumable_segment(build_adapter, scheduler_cls):
|
||||
adapter, _ = build_adapter(stage_id=1, active_stream_window=2)
|
||||
request = _req("req-off-queue", RequestStatus.FINISHED_STOPPED)
|
||||
request.resumable = True
|
||||
adapter.requests_origin_status[request.request_id] = RequestStatus.RUNNING
|
||||
|
||||
scheduler = scheduler_cls.__new__(scheduler_cls)
|
||||
scheduler.chunk_transfer_adapter = adapter
|
||||
scheduler.input_coordinator = None
|
||||
scheduler.requests = {request.request_id: request}
|
||||
scheduler.running = []
|
||||
scheduler.waiting = DummyWaitingQueue()
|
||||
scheduler.skipped_waiting = DummyWaitingQueue()
|
||||
scheduler._free_request = lambda *args, **kwargs: pytest.fail("off-queue request was freed")
|
||||
|
||||
assert (
|
||||
scheduler_cls.finish_requests(
|
||||
scheduler,
|
||||
[request.request_id],
|
||||
RequestStatus.FINISHED_ABORTED,
|
||||
)
|
||||
== []
|
||||
)
|
||||
assert scheduler.requests == {request.request_id: request}
|
||||
assert request.status == RequestStatus.FINISHED_STOPPED
|
||||
assert request.request_id not in adapter.requests_origin_status
|
||||
|
||||
|
||||
def test_restore_queues_skips_requests_missing_from_scheduler_requests(build_adapter):
|
||||
adapter, _ = build_adapter(stage_id=1)
|
||||
zombie = _req("req-zombie", RequestStatus.WAITING_FOR_CHUNK)
|
||||
|
||||
Reference in new issue
Block a user