[core]refactor communication layer: PR1(Added Refactor Infra Only) (#1555)
Signed-off-by: natureofnature <wzliu@connect.hku.hk> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
2b70e89535
commit
0d4e975e1b
9 files changed
+4712
-3
No files matched your search
@@ -0,0 +1,690 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Unit tests for OmniSchedulingCoordinator (formerly ChunkSchedulingCoordinator).
|
||||
|
||||
These tests use mock request objects and mock queues. They do not require
|
||||
GPU, vLLM runtime, or any connector.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
import vllm_omni.core.sched.omni_scheduling_coordinator as coord_mod
|
||||
from vllm_omni.core.sched.omni_scheduling_coordinator import (
|
||||
ChunkSchedulingCoordinator,
|
||||
OmniSchedulingCoordinator,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Mock helpers
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
class _RequestStatus:
|
||||
WAITING = "waiting"
|
||||
RUNNING = "running"
|
||||
WAITING_FOR_CHUNK = "waiting_for_chunk"
|
||||
WAITING_FOR_INPUT = "waiting_for_input"
|
||||
FINISHED_STOPPED = "finished_stopped"
|
||||
|
||||
|
||||
# Patch RequestStatus for tests that don't import vllm
|
||||
try:
|
||||
from vllm.v1.request import RequestStatus
|
||||
except ImportError:
|
||||
RequestStatus = _RequestStatus # type: ignore[misc,assignment]
|
||||
|
||||
if not hasattr(RequestStatus, "WAITING_FOR_INPUT"):
|
||||
coord_mod.RequestStatus = _RequestStatus # type: ignore[assignment]
|
||||
RequestStatus = _RequestStatus # type: ignore[misc,assignment]
|
||||
|
||||
|
||||
def _make_request(req_id: str, status: str = "waiting") -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
request_id=req_id,
|
||||
external_req_id=req_id,
|
||||
status=status,
|
||||
additional_information=None,
|
||||
prompt_token_ids=[],
|
||||
num_prompt_tokens=0,
|
||||
num_computed_tokens=0,
|
||||
_all_token_ids=[],
|
||||
_output_token_ids=[],
|
||||
)
|
||||
|
||||
|
||||
class MockQueue:
|
||||
"""Simplified queue that mimics the Scheduler waiting queue interface."""
|
||||
|
||||
def __init__(self, items: list | None = None):
|
||||
self._items: list = list(items or [])
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._items)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._items)
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self._items
|
||||
|
||||
def add_request(self, request):
|
||||
self._items.append(request)
|
||||
|
||||
def prepend_requests(self, requests):
|
||||
self._items = list(requests) + self._items
|
||||
|
||||
def remove(self, request):
|
||||
self._items.remove(request)
|
||||
|
||||
def remove_requests(self, requests):
|
||||
remove_set = set(id(r) for r in requests)
|
||||
self._items = [r for r in self._items if id(r) not in remove_set]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Tests
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
class TestChunkCoordinatorStateTransition(unittest.TestCase):
|
||||
"""Test 5: process_pending_chunks transitions WAITING_FOR_CHUNK → target."""
|
||||
|
||||
def test_ready_request_transitions_to_waiting(self):
|
||||
coord = ChunkSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1, async_chunk=True)
|
||||
|
||||
req = _make_request("r1", status=RequestStatus.WAITING_FOR_CHUNK)
|
||||
waiting = MockQueue([req])
|
||||
running: list = []
|
||||
|
||||
coord.process_pending_chunks(
|
||||
waiting,
|
||||
running,
|
||||
chunk_ready_req_ids={"r1"},
|
||||
chunk_finished_req_ids=set(),
|
||||
)
|
||||
|
||||
self.assertEqual(req.status, RequestStatus.WAITING)
|
||||
self.assertIn("r1", coord.requests_with_ready_chunks)
|
||||
|
||||
def test_non_ready_stays_waiting_for_chunk(self):
|
||||
coord = ChunkSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1, async_chunk=True)
|
||||
|
||||
req = _make_request("r1", status=RequestStatus.WAITING_FOR_CHUNK)
|
||||
waiting = MockQueue([req])
|
||||
running: list = []
|
||||
|
||||
coord.process_pending_chunks(
|
||||
waiting,
|
||||
running,
|
||||
chunk_ready_req_ids=set(),
|
||||
chunk_finished_req_ids=set(),
|
||||
)
|
||||
|
||||
self.assertEqual(req.status, RequestStatus.WAITING_FOR_CHUNK)
|
||||
|
||||
def test_stage_0_is_noop(self):
|
||||
coord = ChunkSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=0)
|
||||
req = _make_request("r1")
|
||||
waiting = MockQueue([req])
|
||||
running: list = []
|
||||
|
||||
coord.process_pending_chunks(
|
||||
waiting,
|
||||
running,
|
||||
chunk_ready_req_ids={"r1"},
|
||||
chunk_finished_req_ids=set(),
|
||||
)
|
||||
self.assertNotEqual(req.status, RequestStatus.WAITING_FOR_CHUNK)
|
||||
|
||||
|
||||
class TestChunkCoordinatorRestoreQueues(unittest.TestCase):
|
||||
"""Test 6: restore_queues returns waiting-for-chunk requests."""
|
||||
|
||||
def test_restore(self):
|
||||
coord = ChunkSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
|
||||
r1 = _make_request("r1")
|
||||
r2 = _make_request("r2")
|
||||
coord._waiting_for_chunk_waiting.append(r1)
|
||||
coord._waiting_for_chunk_running.append(r2)
|
||||
|
||||
waiting = MockQueue()
|
||||
running: list = []
|
||||
|
||||
coord.restore_queues(waiting, running)
|
||||
|
||||
self.assertIn(r1, waiting)
|
||||
self.assertIn(r2, running)
|
||||
self.assertEqual(len(coord._waiting_for_chunk_waiting), 0)
|
||||
self.assertEqual(len(coord._waiting_for_chunk_running), 0)
|
||||
|
||||
|
||||
class TestChunkCoordinatorFinishedSignal(unittest.TestCase):
|
||||
"""Test 8: chunk_finished_req_ids → finished_requests."""
|
||||
|
||||
def test_finished_signal(self):
|
||||
coord = ChunkSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1, async_chunk=True)
|
||||
|
||||
req = _make_request("r1", status=RequestStatus.WAITING_FOR_CHUNK)
|
||||
waiting = MockQueue([req])
|
||||
running: list = []
|
||||
|
||||
coord.process_pending_chunks(
|
||||
waiting,
|
||||
running,
|
||||
chunk_ready_req_ids={"r1"},
|
||||
chunk_finished_req_ids={"r1"},
|
||||
)
|
||||
|
||||
self.assertIn("r1", coord.finished_requests)
|
||||
|
||||
|
||||
class TestChunkCoordinatorUpdateRequestMetadata(unittest.TestCase):
|
||||
"""Test update_request_metadata applies scheduling metadata to requests."""
|
||||
|
||||
def test_ar_mode_no_longer_sets_additional_information(self):
|
||||
"""AR mode only processes scheduling metadata, not full payloads."""
|
||||
coord = ChunkSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
|
||||
req = _make_request("r1")
|
||||
requests = {"r1": req}
|
||||
|
||||
# Only scheduling metadata is passed now (full payload stays in model runner)
|
||||
request_metadata = {"r1": {"next_stage_prompt_len": 50}}
|
||||
|
||||
coord.update_request_metadata(requests, request_metadata, model_mode="ar")
|
||||
|
||||
# next_stage_prompt_len should update prompt_token_ids
|
||||
self.assertEqual(len(req.prompt_token_ids), 50)
|
||||
self.assertEqual(req.num_prompt_tokens, 50)
|
||||
# additional_information should NOT be set
|
||||
self.assertIsNone(getattr(req, "additional_information", None))
|
||||
|
||||
def test_generation_mode(self):
|
||||
coord = ChunkSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
|
||||
req = _make_request("r1")
|
||||
req.prompt_token_ids = [0, 0, 0]
|
||||
requests = {"r1": req}
|
||||
|
||||
request_metadata = {
|
||||
"r1": {
|
||||
"code_predictor_codes": [10, 20, 30],
|
||||
"left_context_size": 25,
|
||||
}
|
||||
}
|
||||
|
||||
coord.update_request_metadata(requests, request_metadata, model_mode="generation")
|
||||
|
||||
self.assertEqual(req.prompt_token_ids, [10, 20, 30])
|
||||
self.assertEqual(req.num_computed_tokens, 0)
|
||||
self.assertIsNone(req.additional_information)
|
||||
self.assertEqual(req._omni_initial_model_buffer, {"left_context_size": 25})
|
||||
|
||||
|
||||
class TestChunkCoordinatorPostprocess(unittest.TestCase):
|
||||
"""Test postprocess_scheduler_output clears ready chunks."""
|
||||
|
||||
def test_clear_ready(self):
|
||||
coord = ChunkSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
coord.requests_with_ready_chunks = {"r1", "r2"}
|
||||
|
||||
new_req = SimpleNamespace(req_id="r1")
|
||||
cached_reqs = SimpleNamespace(req_ids=["r2"])
|
||||
scheduler_output = SimpleNamespace(
|
||||
scheduled_new_reqs=[new_req],
|
||||
scheduled_cached_reqs=cached_reqs,
|
||||
)
|
||||
|
||||
coord.postprocess_scheduler_output(scheduler_output)
|
||||
|
||||
self.assertEqual(coord.requests_with_ready_chunks, set())
|
||||
|
||||
|
||||
class TestWaitingForInputTransition(unittest.TestCase):
|
||||
"""Test B8: process_pending_full_payload_inputs transitions WAITING_FOR_INPUT."""
|
||||
|
||||
def test_transition_on_recv(self):
|
||||
coord = OmniSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
|
||||
req = _make_request("r1", status=RequestStatus.WAITING_FOR_INPUT)
|
||||
waiting = MockQueue([req])
|
||||
running: list = []
|
||||
|
||||
coord.process_pending_full_payload_inputs(
|
||||
waiting,
|
||||
running,
|
||||
stage_recv_req_ids={"r1"},
|
||||
)
|
||||
|
||||
self.assertEqual(req.status, RequestStatus.WAITING)
|
||||
|
||||
def test_stays_waiting_for_input_if_not_received(self):
|
||||
coord = OmniSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
|
||||
req = _make_request("r1", status=RequestStatus.WAITING_FOR_INPUT)
|
||||
waiting = MockQueue([req])
|
||||
running: list = []
|
||||
|
||||
coord.process_pending_full_payload_inputs(
|
||||
waiting,
|
||||
running,
|
||||
stage_recv_req_ids=set(),
|
||||
)
|
||||
|
||||
self.assertEqual(req.status, RequestStatus.WAITING_FOR_INPUT)
|
||||
self.assertEqual(len(coord._waiting_for_input), 1)
|
||||
|
||||
def test_stage_0_is_noop(self):
|
||||
coord = OmniSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=0)
|
||||
|
||||
req = _make_request("r1", status=RequestStatus.WAITING_FOR_INPUT)
|
||||
waiting = MockQueue([req])
|
||||
running: list = []
|
||||
|
||||
coord.process_pending_full_payload_inputs(
|
||||
waiting,
|
||||
running,
|
||||
stage_recv_req_ids={"r1"},
|
||||
)
|
||||
self.assertEqual(req.status, RequestStatus.WAITING_FOR_INPUT)
|
||||
|
||||
def test_restore_queues_includes_waiting_for_input(self):
|
||||
coord = OmniSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
|
||||
r1 = _make_request("r1")
|
||||
coord._waiting_for_input.append(r1)
|
||||
|
||||
waiting = MockQueue()
|
||||
running: list = []
|
||||
|
||||
coord.restore_queues(waiting, running)
|
||||
|
||||
self.assertIn(r1, waiting)
|
||||
self.assertEqual(len(coord._waiting_for_input), 0)
|
||||
|
||||
def test_full_payload_mode_auto_transitions_waiting_to_waiting_for_input(self):
|
||||
"""In full_payload_mode (async_chunk=False), fresh WAITING requests on
|
||||
non-Stage-0 should be transitioned to WAITING_FOR_INPUT."""
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=10,
|
||||
stage_id=1,
|
||||
async_chunk=False,
|
||||
)
|
||||
|
||||
req = _make_request("r1", status=RequestStatus.WAITING)
|
||||
waiting = MockQueue([req])
|
||||
running: list = []
|
||||
|
||||
coord.process_pending_full_payload_inputs(
|
||||
waiting,
|
||||
running,
|
||||
stage_recv_req_ids=set(),
|
||||
)
|
||||
|
||||
self.assertEqual(req.status, RequestStatus.WAITING_FOR_INPUT)
|
||||
self.assertEqual(len(coord._waiting_for_input), 1)
|
||||
self.assertEqual(len(coord.pending_input_registrations), 1)
|
||||
|
||||
def test_async_chunk_mode_does_not_auto_transition(self):
|
||||
"""In async_chunk mode, fresh WAITING requests should NOT be
|
||||
transitioned to WAITING_FOR_INPUT."""
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=10,
|
||||
stage_id=1,
|
||||
async_chunk=True,
|
||||
)
|
||||
|
||||
req = _make_request("r1", status=RequestStatus.WAITING)
|
||||
waiting = MockQueue([req])
|
||||
running: list = []
|
||||
|
||||
coord.process_pending_full_payload_inputs(
|
||||
waiting,
|
||||
running,
|
||||
stage_recv_req_ids=set(),
|
||||
)
|
||||
|
||||
self.assertEqual(req.status, RequestStatus.WAITING)
|
||||
|
||||
def test_pending_input_registrations(self):
|
||||
coord = OmniSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
|
||||
req = _make_request("r1", status=RequestStatus.WAITING_FOR_INPUT)
|
||||
waiting = MockQueue([req])
|
||||
running: list = []
|
||||
|
||||
coord.process_pending_full_payload_inputs(
|
||||
waiting,
|
||||
running,
|
||||
stage_recv_req_ids=set(),
|
||||
)
|
||||
|
||||
self.assertEqual(len(coord.pending_input_registrations), 1)
|
||||
self.assertEqual(coord.pending_input_registrations[0].request_id, "r1")
|
||||
|
||||
|
||||
class TestTimeoutDetection(unittest.TestCase):
|
||||
"""Regression tests for orphaned pending-recv timeout detection.
|
||||
|
||||
Covers the full lifecycle:
|
||||
1. Request enters WAITING_FOR_CHUNK from either waiting or running queue
|
||||
2. restore_queues() moves it back to the scheduler queue
|
||||
3. Timeout fires via collect_timed_out_request_ids()
|
||||
4. Scheduler removes from both queues and calls _free_request()
|
||||
"""
|
||||
|
||||
def test_waiting_since_recorded_on_chunk_wait(self):
|
||||
"""_waiting_since is set when a request enters WAITING_FOR_CHUNK."""
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=10,
|
||||
stage_id=1,
|
||||
async_chunk=True,
|
||||
)
|
||||
req = _make_request("r1", status=RequestStatus.WAITING)
|
||||
waiting = MockQueue([req])
|
||||
|
||||
coord.process_pending_chunks(
|
||||
waiting,
|
||||
[],
|
||||
chunk_ready_req_ids=set(),
|
||||
chunk_finished_req_ids=set(),
|
||||
)
|
||||
|
||||
self.assertIn("r1", coord._waiting_since)
|
||||
self.assertEqual(req.status, RequestStatus.WAITING_FOR_CHUNK)
|
||||
|
||||
def test_waiting_since_cleared_on_chunk_arrival(self):
|
||||
"""_waiting_since is cleared when a chunk arrives."""
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=10,
|
||||
stage_id=1,
|
||||
async_chunk=True,
|
||||
)
|
||||
req = _make_request("r1", status=RequestStatus.WAITING_FOR_CHUNK)
|
||||
waiting = MockQueue([req])
|
||||
|
||||
coord.process_pending_chunks(
|
||||
waiting,
|
||||
[],
|
||||
chunk_ready_req_ids={"r1"},
|
||||
chunk_finished_req_ids=set(),
|
||||
)
|
||||
|
||||
self.assertNotIn("r1", coord._waiting_since)
|
||||
|
||||
def test_waiting_since_recorded_on_input_wait(self):
|
||||
"""_waiting_since is set when a request enters WAITING_FOR_INPUT."""
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=10,
|
||||
stage_id=1,
|
||||
async_chunk=False,
|
||||
)
|
||||
req = _make_request("r1", status=RequestStatus.WAITING)
|
||||
waiting = MockQueue([req])
|
||||
|
||||
coord.process_pending_full_payload_inputs(
|
||||
waiting,
|
||||
[],
|
||||
stage_recv_req_ids=set(),
|
||||
)
|
||||
|
||||
self.assertIn("r1", coord._waiting_since)
|
||||
|
||||
def test_waiting_since_cleared_on_input_arrival(self):
|
||||
"""_waiting_since is cleared when input data arrives."""
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=10,
|
||||
stage_id=1,
|
||||
async_chunk=False,
|
||||
)
|
||||
req = _make_request("r1", status=RequestStatus.WAITING_FOR_INPUT)
|
||||
coord._waiting_for_input.append(req)
|
||||
coord._waiting_since["r1"] = 0.0
|
||||
|
||||
waiting = MockQueue()
|
||||
coord.process_pending_full_payload_inputs(
|
||||
waiting,
|
||||
[],
|
||||
stage_recv_req_ids={"r1"},
|
||||
)
|
||||
|
||||
self.assertNotIn("r1", coord._waiting_since)
|
||||
self.assertEqual(req.status, RequestStatus.WAITING)
|
||||
|
||||
def test_collect_timed_out_request_ids_no_timeout(self):
|
||||
"""No IDs returned when nothing has timed out."""
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=10,
|
||||
stage_id=1,
|
||||
)
|
||||
import time
|
||||
|
||||
coord._waiting_since["r1"] = time.monotonic()
|
||||
|
||||
result = coord.collect_timed_out_request_ids(timeout_s=300.0)
|
||||
self.assertEqual(result, set())
|
||||
|
||||
def test_collect_timed_out_request_ids_expired(self):
|
||||
"""Timed-out IDs are returned and _waiting_since is cleared."""
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=10,
|
||||
stage_id=1,
|
||||
)
|
||||
coord._waiting_since["r1"] = 0.0 # epoch → definitely expired
|
||||
coord._waiting_since["r2"] = 0.0
|
||||
|
||||
import time
|
||||
|
||||
coord._waiting_since["r3"] = time.monotonic() + 9999 # far future
|
||||
|
||||
result = coord.collect_timed_out_request_ids(timeout_s=1.0)
|
||||
|
||||
self.assertEqual(result, {"r1", "r2"})
|
||||
self.assertNotIn("r1", coord._waiting_since)
|
||||
self.assertNotIn("r2", coord._waiting_since)
|
||||
self.assertIn("r3", coord._waiting_since)
|
||||
|
||||
def test_collect_removes_from_coordinator_queues(self):
|
||||
"""Timed-out requests are defensively removed from internal queues."""
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=10,
|
||||
stage_id=1,
|
||||
)
|
||||
r1 = _make_request("r1")
|
||||
r2 = _make_request("r2")
|
||||
coord._waiting_for_chunk_waiting.append(r1)
|
||||
coord._waiting_for_input.append(r2)
|
||||
coord._waiting_since["r1"] = 0.0
|
||||
coord._waiting_since["r2"] = 0.0
|
||||
|
||||
result = coord.collect_timed_out_request_ids(timeout_s=1.0)
|
||||
|
||||
self.assertEqual(result, {"r1", "r2"})
|
||||
self.assertEqual(len(coord._waiting_for_chunk_waiting), 0)
|
||||
self.assertEqual(len(coord._waiting_for_input), 0)
|
||||
|
||||
def test_free_finished_request_clears_waiting_since(self):
|
||||
"""free_finished_request clears _waiting_since."""
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=10,
|
||||
stage_id=1,
|
||||
)
|
||||
coord._waiting_since["r1"] = 0.0
|
||||
coord.free_finished_request("r1")
|
||||
self.assertNotIn("r1", coord._waiting_since)
|
||||
|
||||
def test_timeout_from_running_queue_full_lifecycle(self):
|
||||
"""End-to-end: request from running → WAITING_FOR_CHUNK → restore →
|
||||
timeout → removed from running list.
|
||||
|
||||
This is the critical regression case: WAITING_FOR_CHUNK requests
|
||||
that originated from self.running are placed back into self.running
|
||||
by restore_queues(), but their status remains WAITING_FOR_CHUNK.
|
||||
The scheduler must remove from BOTH queues unconditionally.
|
||||
"""
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=10,
|
||||
stage_id=1,
|
||||
async_chunk=True,
|
||||
)
|
||||
|
||||
# 1) Request starts in running queue with WAITING status
|
||||
req = _make_request("r1", status=RequestStatus.WAITING)
|
||||
running = [req]
|
||||
waiting = MockQueue()
|
||||
|
||||
# 2) process_pending_chunks: moves to WAITING_FOR_CHUNK
|
||||
coord.process_pending_chunks(
|
||||
waiting,
|
||||
running,
|
||||
chunk_ready_req_ids=set(),
|
||||
chunk_finished_req_ids=set(),
|
||||
)
|
||||
self.assertEqual(req.status, RequestStatus.WAITING_FOR_CHUNK)
|
||||
self.assertIn("r1", coord._waiting_since)
|
||||
self.assertEqual(len(coord._waiting_for_chunk_running), 1)
|
||||
|
||||
# 3) restore_queues: back to running (status stays WAITING_FOR_CHUNK)
|
||||
coord.restore_queues(waiting, running)
|
||||
self.assertIn(req, running)
|
||||
self.assertEqual(len(coord._waiting_for_chunk_running), 0)
|
||||
self.assertEqual(req.status, RequestStatus.WAITING_FOR_CHUNK)
|
||||
|
||||
# 4) Force timeout by setting _waiting_since to epoch
|
||||
coord._waiting_since["r1"] = 0.0
|
||||
|
||||
timed_out_ids = coord.collect_timed_out_request_ids(timeout_s=1.0)
|
||||
self.assertEqual(timed_out_ids, {"r1"})
|
||||
|
||||
# 5) Scheduler removes from both queues (simulating the scheduler path)
|
||||
timed_out_id_set = {id(req)}
|
||||
running = [r for r in running if id(r) not in timed_out_id_set]
|
||||
waiting.remove_requests([req])
|
||||
|
||||
self.assertNotIn(req, running)
|
||||
self.assertEqual(len(waiting), 0)
|
||||
|
||||
def test_timeout_from_waiting_queue_full_lifecycle(self):
|
||||
"""End-to-end: request from waiting → WAITING_FOR_CHUNK → restore →
|
||||
timeout → removed from waiting queue."""
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=10,
|
||||
stage_id=1,
|
||||
async_chunk=True,
|
||||
)
|
||||
|
||||
req = _make_request("r1", status=RequestStatus.WAITING)
|
||||
waiting = MockQueue([req])
|
||||
running: list = []
|
||||
|
||||
coord.process_pending_chunks(
|
||||
waiting,
|
||||
running,
|
||||
chunk_ready_req_ids=set(),
|
||||
chunk_finished_req_ids=set(),
|
||||
)
|
||||
self.assertEqual(len(coord._waiting_for_chunk_waiting), 1)
|
||||
|
||||
coord.restore_queues(waiting, running)
|
||||
self.assertIn(req, waiting)
|
||||
|
||||
coord._waiting_since["r1"] = 0.0
|
||||
timed_out_ids = coord.collect_timed_out_request_ids(timeout_s=1.0)
|
||||
self.assertEqual(timed_out_ids, {"r1"})
|
||||
|
||||
waiting.remove_requests([req])
|
||||
self.assertEqual(len(waiting), 0)
|
||||
|
||||
|
||||
class TestOverflowPreemption(unittest.TestCase):
|
||||
"""Tests for P1-1: overflow requests must get WAITING status.
|
||||
|
||||
Overflow happens when multiple WAITING_FOR_CHUNK requests in
|
||||
``_waiting_for_chunk_running`` receive their chunk in the same cycle.
|
||||
``_process_chunk_queue`` restores them to RUNNING (``continue``
|
||||
path) while RUNNING requests without chunks are moved out. If the
|
||||
net result exceeds ``scheduler_max_num_seqs``, the tail is pushed
|
||||
to ``waiting_queue`` and must have status == WAITING.
|
||||
"""
|
||||
|
||||
def test_overflow_sets_waiting_status(self):
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=1,
|
||||
stage_id=1,
|
||||
async_chunk=True,
|
||||
)
|
||||
|
||||
# r1 is currently RUNNING in the queue.
|
||||
# r2, r3 were previously moved to _waiting_for_chunk_running.
|
||||
r1 = _make_request("r1", status=RequestStatus.RUNNING)
|
||||
r2 = _make_request("r2", status=RequestStatus.WAITING_FOR_CHUNK)
|
||||
r3 = _make_request("r3", status=RequestStatus.WAITING_FOR_CHUNK)
|
||||
|
||||
running = [r1]
|
||||
waiting = MockQueue([])
|
||||
coord._waiting_for_chunk_running.extend([r2, r3])
|
||||
|
||||
# restore_queues puts r2, r3 back into running
|
||||
coord.restore_queues(waiting, running)
|
||||
self.assertEqual(len(running), 3)
|
||||
|
||||
# Now process_pending_chunks with r2, r3 chunks ready:
|
||||
# _process_chunk_queue will:
|
||||
# r1 (RUNNING) → no chunk → move to _waiting_for_chunk_running
|
||||
# r2 (WAITING_FOR_CHUNK, chunk ready) → set RUNNING, stay in running
|
||||
# r3 (WAITING_FOR_CHUNK, chunk ready) → set RUNNING, stay in running
|
||||
# running = [r2, r3], len=2 > max=1 → overflow
|
||||
coord.process_pending_chunks(
|
||||
waiting,
|
||||
running,
|
||||
chunk_ready_req_ids={"r2", "r3"},
|
||||
chunk_finished_req_ids=set(),
|
||||
)
|
||||
|
||||
self.assertEqual(len(running), 1)
|
||||
self.assertEqual(len(waiting), 1)
|
||||
overflow_req = list(waiting)[0]
|
||||
self.assertEqual(
|
||||
overflow_req.status,
|
||||
RequestStatus.WAITING,
|
||||
f"Overflowed request should have WAITING status, got {overflow_req.status}",
|
||||
)
|
||||
|
||||
def test_overflow_does_not_strand_request(self):
|
||||
"""Without the fix, the overflowed request would keep its
|
||||
RUNNING status in the waiting queue and never be re-scheduled."""
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=1,
|
||||
stage_id=1,
|
||||
async_chunk=True,
|
||||
)
|
||||
|
||||
r1 = _make_request("r1", status=RequestStatus.WAITING_FOR_CHUNK)
|
||||
r2 = _make_request("r2", status=RequestStatus.WAITING_FOR_CHUNK)
|
||||
coord._waiting_for_chunk_running.extend([r1, r2])
|
||||
|
||||
running: list = []
|
||||
waiting = MockQueue([])
|
||||
|
||||
coord.restore_queues(waiting, running)
|
||||
self.assertEqual(len(running), 2)
|
||||
|
||||
coord.process_pending_chunks(
|
||||
waiting,
|
||||
running,
|
||||
chunk_ready_req_ids={"r1", "r2"},
|
||||
chunk_finished_req_ids=set(),
|
||||
)
|
||||
|
||||
self.assertEqual(len(running), 1)
|
||||
self.assertEqual(len(waiting), 1)
|
||||
for req in waiting:
|
||||
self.assertNotEqual(req.status, RequestStatus.RUNNING, "Overflowed request must not keep RUNNING status")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user