[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:
authored and GitHub committed 2026-08-20 15:59:39 +08:00
1 parent 480e10ba9c
commit b57dc70ec4
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)