[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
@@ -0,0 +1,380 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Scheduling-side coordination for chunk and full_payload input waiting.
|
||||
|
||||
Manages WAITING_FOR_CHUNK and WAITING_FOR_INPUT state transitions
|
||||
based on readiness signals from OmniConnectorOutput, without ever
|
||||
calling connector.put()/get().
|
||||
|
||||
This replaces the scheduling half of OmniChunkTransferAdapter; the
|
||||
transport half lives in OmniConnectorModelRunnerMixin.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from collections import deque
|
||||
from typing import Any
|
||||
|
||||
from vllm.logger import init_logger
|
||||
from vllm.v1.request import Request, RequestStatus
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class OmniSchedulingCoordinator:
|
||||
"""Pure-scheduling coordinator for chunk and full_payload input waiting.
|
||||
|
||||
The Scheduler owns an instance of this class. It consumes readiness
|
||||
signals produced by the Model Runner's ``OmniConnectorModelRunnerMixin``
|
||||
(via ``OmniConnectorOutput``) and manages ``WAITING_FOR_CHUNK`` and
|
||||
``WAITING_FOR_INPUT`` state transitions accordingly.
|
||||
"""
|
||||
|
||||
def __init__(self, scheduler_max_num_seqs: int, stage_id: int = 0, async_chunk: bool = False):
|
||||
self._stage_id = stage_id
|
||||
self._scheduler_max_num_seqs = scheduler_max_num_seqs
|
||||
self._async_chunk = async_chunk
|
||||
|
||||
self.finished_requests: set[str] = set()
|
||||
self.requests_with_ready_chunks: set[str] = set()
|
||||
self._full_payload_input_received: set[str] = set()
|
||||
|
||||
self._waiting_for_chunk_waiting: deque[Any] = deque()
|
||||
self._waiting_for_chunk_running: deque[Any] = deque()
|
||||
|
||||
# Request IDs that were newly registered for chunk recv this cycle.
|
||||
# The engine/Model Runner should call register_chunk_recv() for these
|
||||
# so the bg thread starts polling.
|
||||
self.pending_chunk_registrations: list[Any] = []
|
||||
|
||||
# Requests waiting for full_payload stage input (WAITING_FOR_INPUT).
|
||||
self._waiting_for_input: deque[Any] = deque()
|
||||
self.pending_input_registrations: list[Any] = []
|
||||
|
||||
# Monotonic timestamp recording when each request first entered
|
||||
# WAITING_FOR_CHUNK or WAITING_FOR_INPUT. Used by
|
||||
# collect_timed_out_request_ids() to detect orphaned waits.
|
||||
self._waiting_since: dict[str, float] = {}
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Core scheduling methods
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def process_pending_chunks(
|
||||
self,
|
||||
waiting_queue: Any,
|
||||
running_queue: list[Request],
|
||||
chunk_ready_req_ids: set[str],
|
||||
chunk_finished_req_ids: set[str],
|
||||
) -> None:
|
||||
"""Transition requests whose chunks have arrived.
|
||||
|
||||
Args:
|
||||
waiting_queue: Scheduler's waiting request queue.
|
||||
running_queue: Scheduler's running request list.
|
||||
chunk_ready_req_ids: IDs with a newly arrived chunk this cycle.
|
||||
chunk_finished_req_ids: IDs whose final chunk has arrived.
|
||||
"""
|
||||
if self._stage_id == 0 or not self._async_chunk:
|
||||
return
|
||||
|
||||
terminal_ready_req_ids = chunk_ready_req_ids.intersection(chunk_finished_req_ids)
|
||||
self.finished_requests.update(chunk_finished_req_ids - terminal_ready_req_ids)
|
||||
self.pending_chunk_registrations = []
|
||||
|
||||
self._process_chunk_queue(
|
||||
waiting_queue,
|
||||
self._waiting_for_chunk_waiting,
|
||||
RequestStatus.WAITING,
|
||||
chunk_ready_req_ids,
|
||||
)
|
||||
self._process_chunk_queue(
|
||||
running_queue,
|
||||
self._waiting_for_chunk_running,
|
||||
RequestStatus.RUNNING,
|
||||
chunk_ready_req_ids,
|
||||
)
|
||||
self.finished_requests.update(terminal_ready_req_ids)
|
||||
|
||||
while len(running_queue) > self._scheduler_max_num_seqs:
|
||||
request = running_queue.pop()
|
||||
# Must reset status to WAITING so the scheduler treats it as
|
||||
# schedulable work. KV blocks are NOT freed here (unlike a
|
||||
# real preemption), so PREEMPTED would be incorrect.
|
||||
request.status = RequestStatus.WAITING
|
||||
waiting_queue.prepend_requests([request])
|
||||
|
||||
def process_pending_full_payload_inputs(
|
||||
self,
|
||||
waiting_queue: Any,
|
||||
running_queue: list[Request],
|
||||
stage_recv_req_ids: set[str],
|
||||
) -> None:
|
||||
"""Manage WAITING_FOR_INPUT lifecycle for full_payload_mode.
|
||||
|
||||
For non-Stage-0 stages in full_payload_mode (``async_chunk=False``):
|
||||
1. Fresh WAITING requests are transitioned to WAITING_FOR_INPUT
|
||||
and registered for bg-thread polling.
|
||||
2. WAITING_FOR_INPUT requests whose data has arrived (in
|
||||
``stage_recv_req_ids``) are transitioned back to WAITING.
|
||||
"""
|
||||
if self._stage_id == 0:
|
||||
return
|
||||
|
||||
self._full_payload_input_received.update(stage_recv_req_ids)
|
||||
if not self._async_chunk and stage_recv_req_ids:
|
||||
self.finished_requests.update(stage_recv_req_ids)
|
||||
logger.debug(
|
||||
"[Coordinator stage-%s] full_payload recv -> finished_requests: %s",
|
||||
self._stage_id,
|
||||
stage_recv_req_ids,
|
||||
)
|
||||
self.pending_input_registrations = []
|
||||
|
||||
remaining: deque[Any] = deque()
|
||||
for request in self._waiting_for_input:
|
||||
if request.request_id in stage_recv_req_ids:
|
||||
request.status = RequestStatus.WAITING
|
||||
self._waiting_since.pop(request.request_id, None)
|
||||
waiting_queue.add_request(request)
|
||||
else:
|
||||
remaining.append(request)
|
||||
self._waiting_for_input = remaining
|
||||
|
||||
if not self._async_chunk:
|
||||
to_remove: list[Any] = []
|
||||
queue_snapshot = list(waiting_queue)
|
||||
for request in queue_snapshot:
|
||||
if request.status == RequestStatus.WAITING:
|
||||
if request.request_id in self._full_payload_input_received:
|
||||
continue
|
||||
if request.request_id in self.requests_with_ready_chunks:
|
||||
continue
|
||||
if request.request_id in self.finished_requests:
|
||||
continue
|
||||
request.status = RequestStatus.WAITING_FOR_INPUT
|
||||
self._waiting_since.setdefault(request.request_id, time.monotonic())
|
||||
to_remove.append(request)
|
||||
self._waiting_for_input.append(request)
|
||||
self.pending_input_registrations.append(request)
|
||||
elif request.status == RequestStatus.WAITING_FOR_INPUT:
|
||||
if request.request_id in stage_recv_req_ids:
|
||||
request.status = RequestStatus.WAITING
|
||||
self._waiting_since.pop(request.request_id, None)
|
||||
else:
|
||||
to_remove.append(request)
|
||||
self._waiting_for_input.append(request)
|
||||
self.pending_input_registrations.append(request)
|
||||
for request in to_remove:
|
||||
waiting_queue.remove(request)
|
||||
|
||||
def process_pending_full_payload_inputs_legacy(
|
||||
self,
|
||||
waiting_queue: Any,
|
||||
running_queue: list[Request],
|
||||
stage_recv_req_ids: set[str],
|
||||
) -> None:
|
||||
"""Compatibility wrapper for ``process_pending_full_payload_inputs``."""
|
||||
self.process_pending_full_payload_inputs(waiting_queue, running_queue, stage_recv_req_ids)
|
||||
|
||||
def free_finished_request(self, request_id: str) -> None:
|
||||
"""Prune internal tracking sets for a freed request to prevent unbounded growth."""
|
||||
self._full_payload_input_received.discard(request_id)
|
||||
self.finished_requests.discard(request_id)
|
||||
self.requests_with_ready_chunks.discard(request_id)
|
||||
self._waiting_since.pop(request_id, None)
|
||||
|
||||
def collect_timed_out_request_ids(
|
||||
self,
|
||||
timeout_s: float,
|
||||
) -> set[str]:
|
||||
"""Return IDs of requests that have been waiting longer than *timeout_s*.
|
||||
|
||||
Uses ``_waiting_since`` timestamps (always up-to-date) to detect
|
||||
timed-out requests. This method is safe to call at any point in
|
||||
the scheduling cycle — it does **not** rely on coordinator internal
|
||||
queues (which are empty after ``restore_queues()``).
|
||||
|
||||
Clears ``_waiting_since`` for timed-out IDs and defensively removes
|
||||
them from coordinator internal queues if present. The caller
|
||||
(scheduler) should then remove the requests from its queues,
|
||||
set ``FINISHED_ERROR``, and call ``_free_request()`` so that
|
||||
``cleanup_finished_request()`` fires in the model runner mixin.
|
||||
"""
|
||||
if timeout_s <= 0:
|
||||
return set()
|
||||
now = time.monotonic()
|
||||
timed_out_ids: set[str] = set()
|
||||
for req_id, start_time in self._waiting_since.items():
|
||||
if now - start_time > timeout_s:
|
||||
timed_out_ids.add(req_id)
|
||||
if not timed_out_ids:
|
||||
return set()
|
||||
|
||||
# Defensively remove from coordinator internal queues (may already
|
||||
# be empty if restore_queues() has run).
|
||||
for queue_attr in (
|
||||
"_waiting_for_chunk_waiting",
|
||||
"_waiting_for_chunk_running",
|
||||
"_waiting_for_input",
|
||||
):
|
||||
queue = getattr(self, queue_attr)
|
||||
remaining: deque[Any] = deque()
|
||||
for request in queue:
|
||||
if request.request_id not in timed_out_ids:
|
||||
remaining.append(request)
|
||||
setattr(self, queue_attr, remaining)
|
||||
|
||||
for req_id in timed_out_ids:
|
||||
self._waiting_since.pop(req_id, None)
|
||||
logger.warning(
|
||||
"[Coordinator stage-%s] Request %s timed out waiting for chunk/input (waited > %.0fs)",
|
||||
self._stage_id,
|
||||
req_id,
|
||||
timeout_s,
|
||||
)
|
||||
|
||||
return timed_out_ids
|
||||
|
||||
def restore_queues(
|
||||
self,
|
||||
waiting_queue: Any,
|
||||
running_queue: list[Request],
|
||||
) -> None:
|
||||
"""Return waiting-for-chunk/input requests to scheduling queues."""
|
||||
for request in self._waiting_for_chunk_waiting:
|
||||
waiting_queue.add_request(request)
|
||||
self._waiting_for_chunk_waiting = deque()
|
||||
|
||||
if self._waiting_for_chunk_running:
|
||||
running_queue.extend(self._waiting_for_chunk_running)
|
||||
self._waiting_for_chunk_running = deque()
|
||||
|
||||
for request in self._waiting_for_input:
|
||||
waiting_queue.add_request(request)
|
||||
self._waiting_for_input = deque()
|
||||
|
||||
def update_request_metadata(
|
||||
self,
|
||||
requests: dict[str, Request],
|
||||
request_metadata: dict[str, dict[str, Any]],
|
||||
model_mode: str = "ar",
|
||||
) -> None:
|
||||
"""Apply received scheduling metadata to request objects.
|
||||
|
||||
For AR mode: only scheduler-visible metadata is applied locally.
|
||||
For Generation mode: updates ``request.prompt_token_ids``.
|
||||
|
||||
Additionally, if the payload contains ``next_stage_prompt_len``,
|
||||
updates the request's ``prompt_token_ids`` to the correct length.
|
||||
"""
|
||||
for req_id, metadata in request_metadata.items():
|
||||
request = requests.get(req_id)
|
||||
if request is None:
|
||||
continue
|
||||
|
||||
# Handle next_stage_prompt_len if present (for models like Qwen3-Omni).
|
||||
# Only apply when the request has not started decoding yet
|
||||
# (no output tokens). Resetting a mid-decode request would
|
||||
# destroy generated tokens and desync KV cache state.
|
||||
if "next_stage_prompt_len" in metadata:
|
||||
next_len = metadata["next_stage_prompt_len"]
|
||||
if isinstance(next_len, int) and next_len > 0:
|
||||
output_token_ids = getattr(request, "_output_token_ids", None)
|
||||
has_decode_output = output_token_ids is not None and len(output_token_ids) > 0
|
||||
if has_decode_output:
|
||||
logger.debug(
|
||||
"[Coordinator stage-%s] Skipping prompt resize for req %s: "
|
||||
"request already has %s output tokens",
|
||||
self._stage_id,
|
||||
req_id,
|
||||
len(output_token_ids),
|
||||
)
|
||||
else:
|
||||
current_prompt_ids = getattr(request, "prompt_token_ids", []) or []
|
||||
current_prompt_len = len(current_prompt_ids)
|
||||
if current_prompt_len != next_len or getattr(request, "num_prompt_tokens", None) != next_len:
|
||||
new_prompt = [0] * next_len
|
||||
request.prompt_token_ids = new_prompt
|
||||
request.num_prompt_tokens = next_len
|
||||
request._all_token_ids.clear()
|
||||
request._all_token_ids.extend(new_prompt)
|
||||
request._output_token_ids.clear()
|
||||
request.num_computed_tokens = 0
|
||||
logger.debug(
|
||||
"[Coordinator stage-%s] Updated prompt_token_ids length to %s for req %s",
|
||||
self._stage_id,
|
||||
next_len,
|
||||
req_id,
|
||||
)
|
||||
|
||||
if model_mode != "ar":
|
||||
new_ids = metadata.get("code_predictor_codes", [])
|
||||
runtime_seed = None
|
||||
if "left_context_size" in metadata:
|
||||
runtime_seed = {
|
||||
"left_context_size": metadata["left_context_size"],
|
||||
}
|
||||
request._omni_initial_model_buffer = runtime_seed
|
||||
if new_ids:
|
||||
request.prompt_token_ids = new_ids
|
||||
request.num_computed_tokens = 0
|
||||
|
||||
def postprocess_scheduler_output(
|
||||
self,
|
||||
scheduler_output: Any,
|
||||
requests: dict[str, Request] | None = None,
|
||||
) -> None:
|
||||
"""Clear per-cycle ready state after scheduler output is materialized."""
|
||||
self._clear_chunk_ready(scheduler_output)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Internal helpers
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _process_chunk_queue(
|
||||
self,
|
||||
queue: Any,
|
||||
waiting_for_chunk_list: deque[Any],
|
||||
target_status: RequestStatus,
|
||||
chunk_ready_req_ids: set[str],
|
||||
) -> None:
|
||||
queue_snapshot = list(queue)
|
||||
for request in queue_snapshot:
|
||||
if request.status != RequestStatus.WAITING_FOR_CHUNK:
|
||||
if request.request_id in self.requests_with_ready_chunks:
|
||||
continue
|
||||
if request.request_id in self.finished_requests:
|
||||
continue
|
||||
if request.status == RequestStatus.WAITING_FOR_INPUT:
|
||||
continue
|
||||
if request.request_id in chunk_ready_req_ids:
|
||||
self.requests_with_ready_chunks.add(request.request_id)
|
||||
continue
|
||||
self.pending_chunk_registrations.append(request)
|
||||
request.status = RequestStatus.WAITING_FOR_CHUNK
|
||||
self._waiting_since.setdefault(request.request_id, time.monotonic())
|
||||
else:
|
||||
if request.request_id in chunk_ready_req_ids:
|
||||
request.status = target_status
|
||||
self.requests_with_ready_chunks.add(request.request_id)
|
||||
self._waiting_since.pop(request.request_id, None)
|
||||
continue
|
||||
queue.remove(request)
|
||||
waiting_for_chunk_list.append(request)
|
||||
|
||||
def _clear_chunk_ready(self, scheduler_output: Any) -> None:
|
||||
if scheduler_output.scheduled_new_reqs:
|
||||
for req_data in scheduler_output.scheduled_new_reqs:
|
||||
self.requests_with_ready_chunks.discard(
|
||||
getattr(req_data, "req_id", None),
|
||||
)
|
||||
|
||||
if scheduler_output.scheduled_cached_reqs:
|
||||
for req_id in scheduler_output.scheduled_cached_reqs.req_ids:
|
||||
self.requests_with_ready_chunks.discard(req_id)
|
||||
|
||||
|
||||
# Backward-compatible alias
|
||||
ChunkSchedulingCoordinator = OmniSchedulingCoordinator
|
||||
@@ -35,11 +35,12 @@ from vllm_omni.diffusion.sched.interface import DiffusionSchedulerOutput
|
||||
from vllm_omni.diffusion.worker.utils import DiffusionRequestState, RunnerOutput
|
||||
from vllm_omni.distributed.omni_connectors.kv_transfer_manager import OmniKVTransferManager
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
from vllm_omni.worker.omni_connector_model_runner_mixin import OmniConnectorModelRunnerMixin
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class DiffusionModelRunner:
|
||||
class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
|
||||
"""
|
||||
Model runner that handles model loading and execution for diffusion models.
|
||||
|
||||
|
||||
@@ -9,6 +9,33 @@ from vllm.v1.outputs import ModelRunnerOutput
|
||||
from vllm_omni.inputs.data import OmniPromptType
|
||||
|
||||
|
||||
@dataclass
|
||||
class OmniConnectorOutput:
|
||||
"""Communication results from Model Runner to Scheduler.
|
||||
|
||||
Carries transfer readiness signals so the Scheduler can make scheduling
|
||||
decisions without ever calling connector.put()/get() directly.
|
||||
|
||||
Attributes:
|
||||
chunk_ready_req_ids: Request IDs with newly arrived chunks this cycle.
|
||||
chunk_finished_req_ids: Request IDs whose final chunk has arrived.
|
||||
request_metadata: Lightweight scheduling metadata keyed by request ID
|
||||
(e.g. next_stage_prompt_len, code_predictor_codes, left_context_size).
|
||||
Full payloads are owned by the Model Runner's local cache.
|
||||
kv_sent_req_ids: Request IDs whose KV cache was successfully sent.
|
||||
stage_recv_req_ids: Request IDs that received batch stage inputs.
|
||||
has_pending_kv_work: True if the mixin has pending, active, or
|
||||
completed KV transfers that the scheduler should account for.
|
||||
"""
|
||||
|
||||
chunk_ready_req_ids: set[str] = field(default_factory=set)
|
||||
chunk_finished_req_ids: set[str] = field(default_factory=set)
|
||||
request_metadata: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
kv_sent_req_ids: list[str] = field(default_factory=list)
|
||||
stage_recv_req_ids: set[str] = field(default_factory=set)
|
||||
has_pending_kv_work: bool = False
|
||||
|
||||
|
||||
class OmniModelRunnerOutput(ModelRunnerOutput):
|
||||
"""Model runner output for omni models.
|
||||
|
||||
@@ -24,6 +51,7 @@ class OmniModelRunnerOutput(ModelRunnerOutput):
|
||||
# IDs of requests whose KV cache has been extracted from GPU/NPU to CPU.
|
||||
# The Scheduler can safely free the block tables for these requests.
|
||||
kv_extracted_req_ids: list[str] | None = None
|
||||
omni_connector_output: OmniConnectorOutput | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -40,6 +40,7 @@ from vllm.v1.worker.utils import is_residual_scattered_for_sp
|
||||
from vllm_omni.distributed.omni_connectors.kv_transfer_manager import OmniKVTransferManager
|
||||
from vllm_omni.outputs import OmniModelRunnerOutput
|
||||
from vllm_omni.worker.gpu_model_runner import OmniGPUModelRunner
|
||||
from vllm_omni.worker.omni_connector_model_runner_mixin import OmniConnectorModelRunnerMixin
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -60,7 +61,7 @@ class ExecuteModelState(NamedTuple):
|
||||
slot_mappings: dict[str, torch.Tensor] | list[dict[str, torch.Tensor]] | None = None
|
||||
|
||||
|
||||
class GPUARModelRunner(OmniGPUModelRunner):
|
||||
class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
|
||||
"""Autoregressive GPU model runner that returns hidden states per request.
|
||||
|
||||
Follows the v0.12 two-phase execute/sample flow from GPUModelRunner, and
|
||||
|
||||
@@ -39,11 +39,12 @@ from vllm.v1.worker.utils import sanity_check_mm_encoder_outputs
|
||||
from vllm_omni.outputs import OmniModelRunnerOutput
|
||||
from vllm_omni.worker.gpu_ar_model_runner import ExecuteModelState
|
||||
from vllm_omni.worker.gpu_model_runner import OmniGPUModelRunner
|
||||
from vllm_omni.worker.omni_connector_model_runner_mixin import OmniConnectorModelRunnerMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GPUGenerationModelRunner(OmniGPUModelRunner):
|
||||
class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
|
||||
"""Generation model runner for vLLM-Omni (non-autoregressive).
|
||||
|
||||
- Reuses GPUModelRunner preparation, multimodal handling, and TP/PP/DP glue.
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,64 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Helpers for explicit thinker decode span metadata."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
THINKER_DECODE_EMBEDDINGS_KEY = "thinker_decode_embeddings"
|
||||
THINKER_OUTPUT_TOKEN_IDS_KEY = "thinker_output_token_ids"
|
||||
THINKER_DECODE_TOKEN_START_KEY = "thinker_decode_embeddings_token_start"
|
||||
THINKER_DECODE_TOKEN_END_KEY = "thinker_decode_embeddings_token_end"
|
||||
|
||||
CACHED_THINKER_DECODE_EMBEDDINGS_KEY = "cached_thinker_decode_embeddings"
|
||||
CACHED_THINKER_DECODE_TOKEN_START_KEY = "cached_thinker_decode_embeddings_token_start"
|
||||
CACHED_THINKER_DECODE_TOKEN_END_KEY = "cached_thinker_decode_embeddings_token_end"
|
||||
|
||||
TensorSpan = tuple[torch.Tensor, int, int]
|
||||
|
||||
|
||||
def get_tensor_span(payload: Mapping[str, Any], *, tensor_key: str, start_key: str, end_key: str) -> TensorSpan | None:
|
||||
tensor = payload.get(tensor_key)
|
||||
start = payload.get(start_key)
|
||||
end = payload.get(end_key)
|
||||
if not isinstance(tensor, torch.Tensor):
|
||||
return None
|
||||
if not isinstance(start, int) or not isinstance(end, int):
|
||||
return None
|
||||
if start < 0 or end < start or (end - start) != int(tensor.shape[0]):
|
||||
return None
|
||||
return tensor, start, end
|
||||
|
||||
|
||||
def merge_tensor_spans(existing_span: TensorSpan | None, incoming_span: TensorSpan | None) -> TensorSpan | None:
|
||||
if existing_span is None or incoming_span is None:
|
||||
return None
|
||||
|
||||
existing_tensor, existing_start, existing_end = existing_span
|
||||
incoming_tensor, incoming_start, incoming_end = incoming_span
|
||||
if incoming_tensor.device != existing_tensor.device or incoming_tensor.dtype != existing_tensor.dtype:
|
||||
incoming_tensor = incoming_tensor.to(device=existing_tensor.device, dtype=existing_tensor.dtype)
|
||||
if incoming_start == existing_end:
|
||||
return torch.cat([existing_tensor, incoming_tensor], dim=0), existing_start, incoming_end
|
||||
if incoming_start < existing_end:
|
||||
overlap = existing_end - incoming_start
|
||||
if overlap >= int(incoming_tensor.shape[0]):
|
||||
return existing_tensor, existing_start, existing_end
|
||||
trimmed_tensor = incoming_tensor[overlap:]
|
||||
return (
|
||||
torch.cat([existing_tensor, trimmed_tensor], dim=0),
|
||||
existing_start,
|
||||
existing_end + int(trimmed_tensor.shape[0]),
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def get_tensor_span_row(span: TensorSpan | None, index: int) -> torch.Tensor | None:
|
||||
if span is None:
|
||||
return None
|
||||
tensor, start, end = span
|
||||
if index < start or index >= end:
|
||||
return None
|
||||
return tensor[index - start]
|
||||
Reference in new issue
Block a user