[2/5] [core]refactor communication layer: PR 2 of 5 Qwen3 Omni non async (#2677)
Signed-off-by: natureofnature <wzliu@connect.hku.hk> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
c7178d89bb
commit
82a0b3a467
20 files changed
+1223
-100
No files matched your search
@@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from vllm.v1.request import RequestStatus
|
||||
|
||||
import vllm_omni.core.sched.omni_ar_scheduler as ar_sched_mod
|
||||
import vllm_omni.core.sched.omni_generation_scheduler as gen_sched_mod
|
||||
from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler
|
||||
from vllm_omni.core.sched.omni_generation_scheduler import OmniGenerationScheduler
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
class FakeInputCoordinator:
|
||||
def __init__(self) -> None:
|
||||
self.freed: list[str] = []
|
||||
|
||||
def free_finished_request(self, request_id: str) -> None:
|
||||
self.freed.append(request_id)
|
||||
|
||||
|
||||
class DummyRequest:
|
||||
request_id = "req-free"
|
||||
client_index = 0
|
||||
additional_information = None
|
||||
|
||||
def is_finished(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("scheduler_cls", "scheduler_mod"),
|
||||
[
|
||||
(OmniARScheduler, ar_sched_mod),
|
||||
(OmniGenerationScheduler, gen_sched_mod),
|
||||
],
|
||||
)
|
||||
def test_finish_requests_cleans_input_coordinator_for_finished_ids(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
scheduler_cls,
|
||||
scheduler_mod,
|
||||
) -> None:
|
||||
coordinator = FakeInputCoordinator()
|
||||
scheduler = scheduler_cls.__new__(scheduler_cls)
|
||||
scheduler.chunk_transfer_adapter = None
|
||||
scheduler.input_coordinator = coordinator
|
||||
|
||||
def fake_finish_requests(self, request_ids, finished_status):
|
||||
assert request_ids == ["req-a", "req-b"]
|
||||
assert finished_status == RequestStatus.FINISHED_STOPPED
|
||||
return [("req-a", 0), ("req-b", 1)]
|
||||
|
||||
monkeypatch.setattr(scheduler_mod.VLLMScheduler, "finish_requests", fake_finish_requests)
|
||||
|
||||
result = scheduler_cls.finish_requests(
|
||||
scheduler,
|
||||
["req-a", "req-b"],
|
||||
RequestStatus.FINISHED_STOPPED,
|
||||
)
|
||||
|
||||
assert result == [("req-a", 0), ("req-b", 1)]
|
||||
assert coordinator.freed == ["req-a", "req-b"]
|
||||
|
||||
|
||||
def test_ar_free_request_cleans_input_coordinator_on_normal_free() -> None:
|
||||
coordinator = FakeInputCoordinator()
|
||||
scheduler = OmniARScheduler.__new__(OmniARScheduler)
|
||||
scheduler.input_coordinator = coordinator
|
||||
scheduler._omits_kv_transfer_cache = {"req-free": True}
|
||||
scheduler.encoder_cache_manager = SimpleNamespace(free=lambda request: None)
|
||||
scheduler.finished_req_ids = set()
|
||||
scheduler.finished_req_ids_dict = None
|
||||
scheduler._new_prompt_len_snapshot = {"req-free": 3}
|
||||
scheduler._connector_finished = lambda request: (False, None)
|
||||
scheduler._should_transfer_kv_for_request = lambda request_id: False
|
||||
scheduler._free_blocks = lambda request: None
|
||||
|
||||
result = OmniARScheduler._free_request(scheduler, DummyRequest())
|
||||
|
||||
assert result is None
|
||||
assert coordinator.freed == ["req-free"]
|
||||
assert scheduler._omits_kv_transfer_cache == {}
|
||||
assert scheduler._new_prompt_len_snapshot == {}
|
||||
|
||||
|
||||
def test_generation_free_request_cleans_input_coordinator(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
coordinator = FakeInputCoordinator()
|
||||
scheduler = OmniGenerationScheduler.__new__(OmniGenerationScheduler)
|
||||
scheduler.input_coordinator = coordinator
|
||||
|
||||
def fake_free_request(self, request, delay_free_blocks=False):
|
||||
assert delay_free_blocks is True
|
||||
+137
-11
@@ -1,6 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Unit tests for OmniSchedulingCoordinator (formerly ChunkSchedulingCoordinator).
|
||||
"""Unit tests for OmniSchedulingCoordinator.
|
||||
|
||||
These tests use mock request objects and mock queues. They do not require
|
||||
GPU, vLLM runtime, or any connector.
|
||||
@@ -11,10 +11,12 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
import torch
|
||||
|
||||
import vllm_omni.core.sched.omni_scheduling_coordinator as coord_mod
|
||||
from vllm_omni.core.sched.omni_scheduling_coordinator import (
|
||||
ChunkSchedulingCoordinator,
|
||||
OmniSchedulingCoordinator,
|
||||
uses_qwen3_omni_full_payload_input_coordinator,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -89,11 +91,55 @@ class MockQueue:
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
class TestFullPayloadCoordinatorSelection(unittest.TestCase):
|
||||
def test_qwen3_omni_talker_and_code2wav_use_full_payload_input_coordinator(self):
|
||||
for model_stage in ("talker", "code2wav"):
|
||||
model_config = SimpleNamespace(
|
||||
stage_id=1,
|
||||
async_chunk=False,
|
||||
model_arch="Qwen3OmniMoeForConditionalGeneration",
|
||||
model_stage=model_stage,
|
||||
)
|
||||
|
||||
self.assertTrue(uses_qwen3_omni_full_payload_input_coordinator(model_config))
|
||||
|
||||
def test_async_chunk_and_non_qwen3_omni_do_not_use_full_payload_input_coordinator(self):
|
||||
cases = [
|
||||
SimpleNamespace(
|
||||
stage_id=1,
|
||||
async_chunk=True,
|
||||
model_arch="Qwen3OmniMoeForConditionalGeneration",
|
||||
model_stage="talker",
|
||||
),
|
||||
SimpleNamespace(
|
||||
stage_id=1,
|
||||
async_chunk=False,
|
||||
model_arch="Qwen3TTSForConditionalGeneration",
|
||||
model_stage="code2wav",
|
||||
),
|
||||
SimpleNamespace(
|
||||
stage_id=1,
|
||||
async_chunk=False,
|
||||
model_arch="Qwen2_5OmniForConditionalGeneration",
|
||||
model_stage="talker",
|
||||
),
|
||||
SimpleNamespace(
|
||||
stage_id=0,
|
||||
async_chunk=False,
|
||||
model_arch="Qwen3OmniMoeForConditionalGeneration",
|
||||
model_stage="thinker",
|
||||
),
|
||||
]
|
||||
|
||||
for model_config in cases:
|
||||
self.assertFalse(uses_qwen3_omni_full_payload_input_coordinator(model_config))
|
||||
|
||||
|
||||
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)
|
||||
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])
|
||||
@@ -110,7 +156,7 @@ class TestChunkCoordinatorStateTransition(unittest.TestCase):
|
||||
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)
|
||||
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])
|
||||
@@ -126,7 +172,7 @@ class TestChunkCoordinatorStateTransition(unittest.TestCase):
|
||||
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)
|
||||
coord = OmniSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=0)
|
||||
req = _make_request("r1")
|
||||
waiting = MockQueue([req])
|
||||
running: list = []
|
||||
@@ -144,7 +190,7 @@ 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)
|
||||
coord = OmniSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
|
||||
r1 = _make_request("r1")
|
||||
r2 = _make_request("r2")
|
||||
@@ -166,7 +212,7 @@ 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)
|
||||
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])
|
||||
@@ -187,7 +233,7 @@ class TestChunkCoordinatorUpdateRequestMetadata(unittest.TestCase):
|
||||
|
||||
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)
|
||||
coord = OmniSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
|
||||
req = _make_request("r1")
|
||||
requests = {"r1": req}
|
||||
@@ -204,10 +250,14 @@ class TestChunkCoordinatorUpdateRequestMetadata(unittest.TestCase):
|
||||
self.assertIsNone(getattr(req, "additional_information", None))
|
||||
|
||||
def test_generation_mode(self):
|
||||
coord = ChunkSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
coord = OmniSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
|
||||
req = _make_request("r1")
|
||||
req.prompt_token_ids = [0, 0, 0]
|
||||
req.num_prompt_tokens = 3
|
||||
req.num_computed_tokens = 3
|
||||
req._all_token_ids = [0, 0, 0, 99]
|
||||
req._output_token_ids = [99]
|
||||
requests = {"r1": req}
|
||||
|
||||
request_metadata = {
|
||||
@@ -220,16 +270,61 @@ class TestChunkCoordinatorUpdateRequestMetadata(unittest.TestCase):
|
||||
coord.update_request_metadata(requests, request_metadata, model_mode="generation")
|
||||
|
||||
self.assertEqual(req.prompt_token_ids, [10, 20, 30])
|
||||
self.assertEqual(req.num_prompt_tokens, 3)
|
||||
self.assertEqual(req.num_computed_tokens, 0)
|
||||
self.assertEqual(req._all_token_ids, [10, 20, 30])
|
||||
self.assertEqual(req._output_token_ids, [])
|
||||
self.assertIsNone(req.additional_information)
|
||||
self.assertEqual(req._omni_initial_model_buffer, {"meta": {"left_context_size": 25}})
|
||||
|
||||
def test_generation_mode_flattens_tensor_code_predictor_codes(self):
|
||||
coord = OmniSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
|
||||
req = _make_request("r1")
|
||||
req.prompt_token_ids = [9]
|
||||
req.num_prompt_tokens = 1
|
||||
req._all_token_ids = [9, 8]
|
||||
req._output_token_ids = [8]
|
||||
requests = {"r1": req}
|
||||
|
||||
coord.update_request_metadata(
|
||||
requests,
|
||||
{"r1": {"code_predictor_codes": torch.tensor([[1, 2, 3]], dtype=torch.long)}},
|
||||
model_mode="generation",
|
||||
)
|
||||
|
||||
self.assertEqual(req.prompt_token_ids, [1, 2, 3])
|
||||
self.assertEqual(req.num_prompt_tokens, 3)
|
||||
self.assertEqual(req._all_token_ids, [1, 2, 3])
|
||||
self.assertEqual(req._output_token_ids, [])
|
||||
|
||||
def test_generation_mode_flattens_nested_code_predictor_codes(self):
|
||||
coord = OmniSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
|
||||
req = _make_request("r1")
|
||||
req.prompt_token_ids = [9]
|
||||
req.num_prompt_tokens = 1
|
||||
req._all_token_ids = [9, 8]
|
||||
req._output_token_ids = [8]
|
||||
requests = {"r1": req}
|
||||
|
||||
coord.update_request_metadata(
|
||||
requests,
|
||||
{"r1": {"code_predictor_codes": [[1, 2], [3, 4]]}},
|
||||
model_mode="generation",
|
||||
)
|
||||
|
||||
self.assertEqual(req.prompt_token_ids, [1, 2, 3, 4])
|
||||
self.assertEqual(req.num_prompt_tokens, 4)
|
||||
self.assertEqual(req._all_token_ids, [1, 2, 3, 4])
|
||||
self.assertEqual(req._output_token_ids, [])
|
||||
|
||||
|
||||
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 = OmniSchedulingCoordinator(scheduler_max_num_seqs=10, stage_id=1)
|
||||
coord.requests_with_ready_chunks = {"r1", "r2"}
|
||||
|
||||
new_req = SimpleNamespace(req_id="r1")
|
||||
@@ -366,6 +461,33 @@ class TestWaitingForInputTransition(unittest.TestCase):
|
||||
self.assertEqual(len(coord.pending_input_registrations), 1)
|
||||
self.assertEqual(coord.pending_input_registrations[0].request_id, "r1")
|
||||
|
||||
def test_idle_cycles_retain_received_marker_before_request_appears(self):
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=10,
|
||||
stage_id=1,
|
||||
async_chunk=False,
|
||||
)
|
||||
coord._full_payload_input_received.add("late")
|
||||
coord.finished_requests.add("late")
|
||||
|
||||
waiting = MockQueue()
|
||||
running: list = []
|
||||
|
||||
coord.process_pending_full_payload_inputs(waiting, running, stage_recv_req_ids=set())
|
||||
|
||||
self.assertIn("late", coord._full_payload_input_received)
|
||||
self.assertIn("late", coord.finished_requests)
|
||||
|
||||
late_req = _make_request("late", status=RequestStatus.WAITING)
|
||||
waiting.add_request(late_req)
|
||||
|
||||
coord.process_pending_full_payload_inputs(waiting, running, stage_recv_req_ids=set())
|
||||
|
||||
self.assertEqual(late_req.status, RequestStatus.WAITING)
|
||||
self.assertEqual(coord.pending_input_registrations, [])
|
||||
self.assertIn("late", coord._full_payload_input_received)
|
||||
self.assertIn("late", coord.finished_requests)
|
||||
|
||||
|
||||
class TestTimeoutDetection(unittest.TestCase):
|
||||
"""Regression tests for orphaned pending-recv timeout detection.
|
||||
@@ -508,14 +630,18 @@ class TestTimeoutDetection(unittest.TestCase):
|
||||
self.assertEqual(len(coord._waiting_for_input), 0)
|
||||
|
||||
def test_free_finished_request_clears_waiting_since(self):
|
||||
"""free_finished_request clears _waiting_since."""
|
||||
"""free_finished_request clears coordinator lifecycle markers."""
|
||||
coord = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=10,
|
||||
stage_id=1,
|
||||
)
|
||||
coord._waiting_since["r1"] = 0.0
|
||||
coord._full_payload_input_received.add("r1")
|
||||
coord.finished_requests.add("r1")
|
||||
coord.free_finished_request("r1")
|
||||
self.assertNotIn("r1", coord._waiting_since)
|
||||
self.assertNotIn("r1", coord._full_payload_input_received)
|
||||
self.assertNotIn("r1", coord.finished_requests)
|
||||
|
||||
def test_timeout_from_running_queue_full_lifecycle(self):
|
||||
"""End-to-end: request from running → WAITING_FOR_CHUNK → restore →
|
||||
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import vllm_omni.model_executor.stage_input_processors.qwen3_omni as q3
|
||||
|
||||
@@ -79,3 +80,110 @@ def test_get_streaming_codec_delta_len_increments_and_finishes(_streaming_contex
|
||||
assert d3 == 1
|
||||
state = q3._get_qwen3_streaming_state("c1", _streaming_context)
|
||||
assert state.talker2code2wav_last_seq_len == 0
|
||||
|
||||
|
||||
def test_talker2code2wav_full_payload_filters_by_output_token_ids() -> None:
|
||||
request = SimpleNamespace(
|
||||
request_id="codec",
|
||||
output_token_ids=[4197, 1, 2, 4198, -1, 2048],
|
||||
)
|
||||
rows = torch.tensor(
|
||||
[
|
||||
[100, 101, 102],
|
||||
[10, 11, 12],
|
||||
[20, 21, 22],
|
||||
[30, 31, 32],
|
||||
[40, 41, 42],
|
||||
[50, 51, 52],
|
||||
],
|
||||
dtype=torch.long,
|
||||
)
|
||||
|
||||
payload = q3.talker2code2wav_full_payload(None, {"codes.audio": rows}, request)
|
||||
|
||||
assert payload is not None
|
||||
assert payload["codes"]["audio"] == [10, 20, 11, 21, 12, 22]
|
||||
assert payload["code_predictor_codes"] == payload["codes"]["audio"]
|
||||
|
||||
|
||||
def test_talker2code2wav_full_payload_drops_count_matched_terminal_row() -> None:
|
||||
request = SimpleNamespace(
|
||||
request_id="codec_terminal_row",
|
||||
output_token_ids=[0, 4198],
|
||||
)
|
||||
rows = torch.tensor(
|
||||
[
|
||||
[10, 11, 12],
|
||||
],
|
||||
dtype=torch.long,
|
||||
)
|
||||
|
||||
payload = q3.talker2code2wav_full_payload(None, {"codes.audio": rows}, request)
|
||||
|
||||
assert payload is None
|
||||
|
||||
|
||||
def test_talker2code2wav_full_payload_drops_rows_aligned_to_non_codec_ids() -> None:
|
||||
request = SimpleNamespace(
|
||||
request_id="codec_invalid_ids",
|
||||
output_token_ids=[4197, 0, 4198, 4196, -1, 2048],
|
||||
)
|
||||
rows = torch.tensor(
|
||||
[
|
||||
[91, 92, 93],
|
||||
[0, 0, 0],
|
||||
[81, 82, 83],
|
||||
[71, 72, 73],
|
||||
[61, 62, 63],
|
||||
[51, 52, 53],
|
||||
],
|
||||
dtype=torch.long,
|
||||
)
|
||||
|
||||
payload = q3.talker2code2wav_full_payload(None, {"codes.audio": rows}, request)
|
||||
|
||||
assert payload is not None
|
||||
assert payload["codes"]["audio"] == [0, 0, 0]
|
||||
assert payload["code_predictor_codes"] == payload["codes"]["audio"]
|
||||
|
||||
|
||||
def test_talker2code2wav_full_payload_keeps_all_zero_codec_rows() -> None:
|
||||
request = SimpleNamespace(
|
||||
request_id="codec_zero",
|
||||
output_token_ids=[0, 1],
|
||||
)
|
||||
rows = torch.tensor(
|
||||
[
|
||||
[0, 0, 0],
|
||||
[7, 8, 9],
|
||||
],
|
||||
dtype=torch.long,
|
||||
)
|
||||
|
||||
payload = q3.talker2code2wav_full_payload(None, {"codes.audio": rows}, request)
|
||||
|
||||
assert payload is not None
|
||||
assert payload["codes"]["audio"] == [0, 7, 0, 8, 0, 9]
|
||||
assert payload["code_predictor_codes"] == payload["codes"]["audio"]
|
||||
|
||||
|
||||
def test_thinker2talker_full_payload_packs_complete_tensors() -> None:
|
||||
request = SimpleNamespace(
|
||||
request_id="thinker",
|
||||
prompt_token_ids=[151644, 872],
|
||||
output_token_ids=[3],
|
||||
all_token_ids=[151644, 872, 3],
|
||||
)
|
||||
pooling_output = {
|
||||
"hidden_states.layer_0": torch.ones(3, 2),
|
||||
"hidden_states.layer_24": torch.full((3, 2), 2.0),
|
||||
"embed.tts_bos": torch.zeros(1, 2),
|
||||
}
|
||||
|
||||
payload = q3.thinker2talker_full_payload(None, pooling_output, request)
|
||||
|
||||
assert payload is not None
|
||||
assert payload["ids"]["all"] == [151644, 872, 3]
|
||||
assert payload["embed"]["prefill"].device.type == "cpu"
|
||||
assert payload["hidden_states"]["output"].device.type == "cpu"
|
||||
assert payload["next_stage_prompt_len"] > 0
|
||||
@@ -1504,7 +1504,7 @@ class TestSentinelDefaultPrecedence:
|
||||
def test_none_value_skipped_yaml_wins(self):
|
||||
stages = self._stages({"max_num_seqs": None})
|
||||
assert stages[2].runtime_overrides.get("max_num_seqs") is None
|
||||
assert "max_num_seqs" not in stages[2].yaml_engine_args
|
||||
assert stages[2].yaml_engine_args["max_num_seqs"] == 64
|
||||
|
||||
def test_empty_kwargs_yaml_only(self):
|
||||
stages = self._stages({})
|
||||
@@ -1598,6 +1598,41 @@ class TestSentinelDefaultPrecedence:
|
||||
assert sync_stages[1].custom_process_input_func is not None
|
||||
assert sync_stages[1].custom_process_input_func.endswith("talker2code2wav")
|
||||
|
||||
def test_async_chunk_dispatches_qwen3_omni_processors(self):
|
||||
import runpy
|
||||
from pathlib import Path
|
||||
|
||||
from vllm_omni.config.stage_config import DeployConfig, merge_pipeline_deploy
|
||||
|
||||
pipeline_path = (
|
||||
Path(__file__).parent.parent / "vllm_omni" / "model_executor" / "models" / "qwen3_omni" / "pipeline.py"
|
||||
)
|
||||
pipeline = runpy.run_path(str(pipeline_path))["QWEN3_OMNI_PIPELINE"]
|
||||
|
||||
async_stages = merge_pipeline_deploy(pipeline, DeployConfig(async_chunk=True))
|
||||
assert (
|
||||
async_stages[0]
|
||||
.yaml_engine_args["custom_process_next_stage_input_func"]
|
||||
.endswith("thinker2talker_async_chunk")
|
||||
)
|
||||
assert (
|
||||
async_stages[1]
|
||||
.yaml_engine_args["custom_process_next_stage_input_func"]
|
||||
.endswith("talker2code2wav_async_chunk")
|
||||
)
|
||||
|
||||
sync_stages = merge_pipeline_deploy(pipeline, DeployConfig(async_chunk=False))
|
||||
assert (
|
||||
sync_stages[0]
|
||||
.yaml_engine_args["custom_process_next_stage_input_func"]
|
||||
.endswith("thinker2talker_full_payload")
|
||||
)
|
||||
assert (
|
||||
sync_stages[1]
|
||||
.yaml_engine_args["custom_process_next_stage_input_func"]
|
||||
.endswith("talker2code2wav_full_payload")
|
||||
)
|
||||
|
||||
|
||||
class TestSamplingConstraintsPrecedence:
|
||||
"""Test that pipeline sampling_constraints override deploy defaults."""
|
||||
|
||||
@@ -5,6 +5,7 @@ import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.worker.gpu_model_runner import OmniGPUModelRunner
|
||||
from vllm_omni.worker.omni_connector_model_runner_mixin import OmniConnectorModelRunnerMixin
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
@@ -330,6 +331,117 @@ def test_update_intermediate_buffer_skips_unknown_req_id():
|
||||
assert "unknown_req" not in runner.model_intermediate_buffer
|
||||
|
||||
|
||||
def _make_full_payload_accumulation_runner(
|
||||
model_arch="Qwen3OmniMoeForConditionalGeneration",
|
||||
model_stage="talker",
|
||||
async_chunk=False,
|
||||
):
|
||||
runner = object.__new__(OmniConnectorModelRunnerMixin)
|
||||
runner.model_config = SimpleNamespace(
|
||||
model_arch=model_arch,
|
||||
model_stage=model_stage,
|
||||
async_chunk=async_chunk,
|
||||
)
|
||||
runner._custom_process_func = object()
|
||||
runner._pending_full_payload_send = {}
|
||||
runner._stage_id = 1
|
||||
return runner
|
||||
|
||||
|
||||
def test_accumulate_full_payload_output_preserves_aligned_all_zero_qwen3_omni_codec_rows():
|
||||
runner = _make_full_payload_accumulation_runner()
|
||||
request = SimpleNamespace(output_token_ids=[0, 1])
|
||||
codes = torch.zeros((2, 3), dtype=torch.long)
|
||||
|
||||
OmniConnectorModelRunnerMixin.accumulate_full_payload_output(runner, "r1", {"codes.audio": codes}, request)
|
||||
|
||||
stored, _ = OmniConnectorModelRunnerMixin._materialize_full_payload_entry(runner._pending_full_payload_send["r1"])
|
||||
assert torch.equal(stored["codes.audio"], codes)
|
||||
|
||||
|
||||
def test_accumulate_full_payload_output_keeps_misaligned_all_zero_qwen3_omni_codec_rows():
|
||||
# After removing the sender-side zero filter, the accumulator keeps every
|
||||
# codec row including misaligned all-zero rows. The downstream consumer
|
||||
# (_extract_qwen3_full_payload_codec_rows) is the authoritative crop and
|
||||
# filters by output_token_ids.
|
||||
runner = _make_full_payload_accumulation_runner()
|
||||
request = SimpleNamespace(output_token_ids=[0, 1])
|
||||
codes = torch.zeros((1, 3), dtype=torch.long)
|
||||
|
||||
OmniConnectorModelRunnerMixin.accumulate_full_payload_output(runner, "r1", {"codes.audio": codes}, request)
|
||||
|
||||
stored, _ = OmniConnectorModelRunnerMixin._materialize_full_payload_entry(runner._pending_full_payload_send["r1"])
|
||||
assert "codes.audio" in stored
|
||||
assert torch.equal(stored["codes.audio"], codes)
|
||||
|
||||
|
||||
def test_accumulate_full_payload_output_preserves_incremental_aligned_all_zero_qwen3_omni_codec_rows():
|
||||
runner = _make_full_payload_accumulation_runner()
|
||||
request = SimpleNamespace(output_token_ids=[0, 1])
|
||||
runner._pending_full_payload_send["r1"] = (
|
||||
{"codes.audio": torch.ones((1, 3), dtype=torch.long)},
|
||||
request,
|
||||
)
|
||||
codes = torch.zeros((1, 3), dtype=torch.long)
|
||||
|
||||
OmniConnectorModelRunnerMixin.accumulate_full_payload_output(runner, "r1", {"codes.audio": codes}, request)
|
||||
|
||||
stored, _ = OmniConnectorModelRunnerMixin._materialize_full_payload_entry(runner._pending_full_payload_send["r1"])
|
||||
assert stored["codes.audio"].shape == (2, 3)
|
||||
assert torch.equal(stored["codes.audio"][1], torch.zeros(3, dtype=torch.long))
|
||||
|
||||
|
||||
def test_accumulate_full_payload_output_keeps_all_zero_qwen3_omni_prefill_placeholder():
|
||||
# Prefill placeholder rows (output_token_ids empty) are no longer dropped
|
||||
# at the sender. The consumer-side crop trims them off using
|
||||
# output_token_ids, so the end-to-end semantics are unchanged.
|
||||
runner = _make_full_payload_accumulation_runner()
|
||||
request = SimpleNamespace(output_token_ids=[])
|
||||
codes = torch.zeros((2, 3), dtype=torch.long)
|
||||
|
||||
OmniConnectorModelRunnerMixin.accumulate_full_payload_output(runner, "r1", {"codes.audio": codes}, request)
|
||||
|
||||
stored, _ = OmniConnectorModelRunnerMixin._materialize_full_payload_entry(runner._pending_full_payload_send["r1"])
|
||||
assert "codes.audio" in stored
|
||||
assert torch.equal(stored["codes.audio"], codes)
|
||||
|
||||
|
||||
def test_full_payload_output_accumulation_hook_matrix():
|
||||
assert _make_full_payload_accumulation_runner(model_stage="thinker")._should_accumulate_full_payload_output()
|
||||
assert _make_full_payload_accumulation_runner(model_stage="talker")._should_accumulate_full_payload_output()
|
||||
assert not _make_full_payload_accumulation_runner(model_stage="code2wav")._should_accumulate_full_payload_output()
|
||||
assert not _make_full_payload_accumulation_runner(
|
||||
model_stage="talker", async_chunk=True
|
||||
)._should_accumulate_full_payload_output()
|
||||
assert not _make_full_payload_accumulation_runner(
|
||||
model_arch="Qwen3TTSForConditionalGeneration"
|
||||
)._should_accumulate_full_payload_output()
|
||||
assert not _make_full_payload_accumulation_runner(
|
||||
model_arch="Qwen2_5OmniForConditionalGeneration"
|
||||
)._should_accumulate_full_payload_output()
|
||||
|
||||
|
||||
def test_sync_local_stage_payloads_retains_payload_until_request_is_active():
|
||||
runner = object.__new__(OmniGPUModelRunner)
|
||||
payload = {"codes": {"audio": [1, 2, 3]}}
|
||||
runner._local_stage_payload_cache = {"late": payload}
|
||||
runner._full_payload_pending_broadcast_req_ids = set()
|
||||
runner.requests = {}
|
||||
runner.model_intermediate_buffer = {}
|
||||
|
||||
OmniGPUModelRunner._sync_local_stage_payloads(runner)
|
||||
|
||||
assert runner._local_stage_payload_cache == {"late": payload}
|
||||
assert runner.model_intermediate_buffer == {}
|
||||
|
||||
runner.requests = {"late": DummyReqState()}
|
||||
OmniGPUModelRunner._sync_local_stage_payloads(runner)
|
||||
|
||||
assert runner._local_stage_payload_cache == {}
|
||||
assert runner.model_intermediate_buffer["late"] == payload
|
||||
assert runner.requests["late"].additional_information_cpu == payload
|
||||
|
||||
|
||||
def test_maybe_attach_mimo_audio_req_infos_enriches_dict():
|
||||
runner = _make_runner_for_mimo()
|
||||
req_id = "r_mimo"
|
||||
|
||||
@@ -20,11 +20,16 @@ from vllm.v1.request import Request, RequestStatus, StreamingUpdate
|
||||
from vllm.v1.spec_decode.metrics import SpecDecodingStats
|
||||
|
||||
from vllm_omni.core.sched.omni_scheduler_mixin import OmniSchedulerMixin
|
||||
from vllm_omni.core.sched.omni_scheduling_coordinator import (
|
||||
OmniSchedulingCoordinator,
|
||||
uses_qwen3_omni_full_payload_input_coordinator,
|
||||
)
|
||||
from vllm_omni.core.sched.output import OmniSchedulerOutput
|
||||
from vllm_omni.distributed.omni_connectors.transfer_adapter.chunk_transfer_adapter import (
|
||||
OmniChunkTransferAdapter,
|
||||
)
|
||||
from vllm_omni.engine.serialization import deserialize_additional_information
|
||||
from vllm_omni.outputs import OmniConnectorOutput
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -75,6 +80,14 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
self.chunk_transfer_adapter = None
|
||||
if getattr(model_config, "async_chunk", False):
|
||||
self.chunk_transfer_adapter = OmniChunkTransferAdapter(self.vllm_config)
|
||||
self.input_coordinator: OmniSchedulingCoordinator | None = None
|
||||
if uses_qwen3_omni_full_payload_input_coordinator(model_config):
|
||||
self.input_coordinator = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=self.vllm_config.scheduler_config.max_num_seqs,
|
||||
stage_id=getattr(model_config, "stage_id", 0),
|
||||
async_chunk=False,
|
||||
)
|
||||
self._latest_omni_connector_output: OmniConnectorOutput | None = None
|
||||
# Snapshot prompt length for each streaming input update
|
||||
self._new_prompt_len_snapshot: dict[str, int] = {}
|
||||
|
||||
@@ -190,6 +203,19 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
return False
|
||||
|
||||
def schedule(self) -> SchedulerOutput: # type: ignore[override]
|
||||
connector_output = self._latest_omni_connector_output
|
||||
self._latest_omni_connector_output = None
|
||||
if self.input_coordinator:
|
||||
if connector_output and connector_output.request_metadata:
|
||||
self.input_coordinator.update_request_metadata(
|
||||
self.requests, connector_output.request_metadata, model_mode="ar"
|
||||
)
|
||||
self.input_coordinator.process_pending_full_payload_inputs(
|
||||
self.waiting,
|
||||
self.running,
|
||||
connector_output.stage_recv_req_ids if connector_output else set(),
|
||||
)
|
||||
|
||||
if self.chunk_transfer_adapter:
|
||||
self.chunk_transfer_adapter.process_pending_chunks(self.waiting, self.running)
|
||||
|
||||
@@ -199,6 +225,8 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
if self.chunk_transfer_adapter:
|
||||
# Add request waiting for chunk to the waiting and running queue
|
||||
self.chunk_transfer_adapter.restore_queues(self.waiting, self.running)
|
||||
if self.input_coordinator:
|
||||
self.input_coordinator.restore_queues(self.waiting, self.running)
|
||||
try:
|
||||
# Late import to avoid circulars in some launch modes
|
||||
from .output import OmniNewRequestData
|
||||
@@ -239,9 +267,11 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
# Wrap in omni scheduler output to carry transfer metadata.
|
||||
base_fields = SchedulerOutput.__dataclass_fields__.keys()
|
||||
base_data = {name: getattr(scheduler_output, name) for name in base_fields}
|
||||
input_regs = self.input_coordinator.pending_input_registrations if self.input_coordinator else []
|
||||
return OmniSchedulerOutput(
|
||||
**base_data,
|
||||
finished_requests_needing_kv_transfer=finished_reqs,
|
||||
pending_input_registrations=input_regs,
|
||||
)
|
||||
|
||||
def update_from_output(
|
||||
@@ -534,6 +564,16 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
engine_core_outputs[0] = eco = EngineCoreOutputs()
|
||||
eco.scheduler_stats = stats
|
||||
|
||||
omni_output = getattr(model_runner_output, "omni_connector_output", None)
|
||||
if omni_output is not None:
|
||||
self._latest_omni_connector_output = omni_output
|
||||
if self.input_coordinator and omni_output.request_metadata:
|
||||
self.input_coordinator.update_request_metadata(
|
||||
self.requests,
|
||||
omni_output.request_metadata,
|
||||
model_mode="ar",
|
||||
)
|
||||
|
||||
# Free blocks that were held for transfer (kv_ready and
|
||||
# active_kv_transfers updates already done before the per-request loop).
|
||||
if kv_extracted_ids:
|
||||
@@ -572,7 +612,12 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
if self.chunk_transfer_adapter:
|
||||
self.chunk_transfer_adapter.finish_requests(request_ids, finished_status, self.requests)
|
||||
|
||||
return super().finish_requests(request_ids, finished_status)
|
||||
finished = super().finish_requests(request_ids, finished_status)
|
||||
input_coordinator = getattr(self, "input_coordinator", None)
|
||||
if input_coordinator is not None:
|
||||
for request_id, _ in finished:
|
||||
self._free_input_coordinator_request(request_id)
|
||||
return finished
|
||||
|
||||
def _update_request_as_session(self, session: Request, update: StreamingUpdate) -> None:
|
||||
"""
|
||||
@@ -616,10 +661,14 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
# It triggered but hasn't finished yet. We MUST wait.
|
||||
logger.debug(f"[Omni] Request {request_id} finished but transfer is still ACTIVE. Waiting.")
|
||||
self.waiting_for_transfer_free.add(request_id)
|
||||
if self.input_coordinator is not None:
|
||||
self._free_input_coordinator_request(request_id)
|
||||
kv_xfer_params = None
|
||||
return kv_xfer_params
|
||||
elif request_id in self.waiting_for_transfer_free:
|
||||
# Blocks held until KV extraction completes in a future step.
|
||||
if self.input_coordinator is not None:
|
||||
self._free_input_coordinator_request(request_id)
|
||||
return None
|
||||
else:
|
||||
logger.debug(
|
||||
@@ -654,10 +703,14 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
if isinstance(add_info, dict):
|
||||
add_info.update(kv_xfer_params)
|
||||
|
||||
if self.input_coordinator is not None:
|
||||
self._free_input_coordinator_request(request_id)
|
||||
return kv_xfer_params
|
||||
|
||||
# 3. Standard Freeing
|
||||
delay_free_blocks |= connector_delay_free_blocks
|
||||
if self.input_coordinator is not None:
|
||||
self._free_input_coordinator_request(request_id)
|
||||
if not delay_free_blocks:
|
||||
self._free_blocks(request)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from typing import Any
|
||||
|
||||
from vllm.compilation.cuda_graph import CUDAGraphStat
|
||||
from vllm.distributed.kv_events import KVEventBatch
|
||||
@@ -23,11 +24,15 @@ from vllm.v1.request import Request, RequestStatus, StreamingUpdate
|
||||
from vllm.v1.spec_decode.metrics import SpecDecodingStats
|
||||
|
||||
from vllm_omni.core.sched.omni_scheduler_mixin import OmniSchedulerMixin
|
||||
from vllm_omni.core.sched.output import OmniCachedRequestData, OmniNewRequestData
|
||||
from vllm_omni.core.sched.omni_scheduling_coordinator import (
|
||||
OmniSchedulingCoordinator,
|
||||
uses_qwen3_omni_full_payload_input_coordinator,
|
||||
)
|
||||
from vllm_omni.core.sched.output import OmniCachedRequestData, OmniNewRequestData, OmniSchedulerOutput
|
||||
from vllm_omni.distributed.omni_connectors.transfer_adapter.chunk_transfer_adapter import (
|
||||
OmniChunkTransferAdapter,
|
||||
)
|
||||
from vllm_omni.outputs import OmniModelRunnerOutput
|
||||
from vllm_omni.outputs import OmniConnectorOutput, OmniModelRunnerOutput
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -39,6 +44,14 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
self.chunk_transfer_adapter = None
|
||||
if getattr(model_config, "async_chunk", False):
|
||||
self.chunk_transfer_adapter = OmniChunkTransferAdapter(self.vllm_config)
|
||||
self.input_coordinator: OmniSchedulingCoordinator | None = None
|
||||
if uses_qwen3_omni_full_payload_input_coordinator(model_config):
|
||||
self.input_coordinator = OmniSchedulingCoordinator(
|
||||
scheduler_max_num_seqs=self.vllm_config.scheduler_config.max_num_seqs,
|
||||
stage_id=getattr(model_config, "stage_id", 0),
|
||||
async_chunk=False,
|
||||
)
|
||||
self._latest_omni_connector_output: OmniConnectorOutput | None = None
|
||||
|
||||
def schedule(self) -> SchedulerOutput:
|
||||
"""Diffusion fast path:
|
||||
@@ -68,6 +81,18 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
# Temporary queue: preserve waiting order, do not disturb non-diffusion requests
|
||||
skipped_waiting_requests = create_request_queue(self.policy)
|
||||
req_index = 0
|
||||
connector_output = self._latest_omni_connector_output
|
||||
self._latest_omni_connector_output = None
|
||||
if self.input_coordinator:
|
||||
if connector_output and connector_output.request_metadata:
|
||||
self.input_coordinator.update_request_metadata(
|
||||
self.requests, connector_output.request_metadata, model_mode="generation"
|
||||
)
|
||||
self.input_coordinator.process_pending_full_payload_inputs(
|
||||
self.waiting,
|
||||
self.running,
|
||||
connector_output.stage_recv_req_ids if connector_output else set(),
|
||||
)
|
||||
if self.chunk_transfer_adapter:
|
||||
self.chunk_transfer_adapter.process_pending_chunks(self.waiting, self.running)
|
||||
|
||||
@@ -201,7 +226,16 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
self.chunk_transfer_adapter.restore_queues(self.waiting, self.running)
|
||||
else:
|
||||
res = super().schedule()
|
||||
return res
|
||||
if self.input_coordinator:
|
||||
self.input_coordinator.restore_queues(self.waiting, self.running)
|
||||
base_fields = SchedulerOutput.__dataclass_fields__.keys()
|
||||
base_data = {name: getattr(res, name) for name in base_fields}
|
||||
return OmniSchedulerOutput(
|
||||
**base_data,
|
||||
pending_input_registrations=(
|
||||
self.input_coordinator.pending_input_registrations if self.input_coordinator else []
|
||||
),
|
||||
)
|
||||
|
||||
# Compute common prefix blocks (aligned with v1)
|
||||
num_common_prefix_blocks = [0] * len(self.kv_cache_config.kv_cache_groups)
|
||||
@@ -321,8 +355,17 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
# internal deques and never scheduled again.
|
||||
if self.chunk_transfer_adapter:
|
||||
self.chunk_transfer_adapter.restore_queues(self.waiting, self.running)
|
||||
if self.input_coordinator:
|
||||
self.input_coordinator.restore_queues(self.waiting, self.running)
|
||||
|
||||
return scheduler_output
|
||||
base_fields = SchedulerOutput.__dataclass_fields__.keys()
|
||||
base_data = {name: getattr(scheduler_output, name) for name in base_fields}
|
||||
return OmniSchedulerOutput(
|
||||
**base_data,
|
||||
pending_input_registrations=(
|
||||
self.input_coordinator.pending_input_registrations if self.input_coordinator else []
|
||||
),
|
||||
)
|
||||
|
||||
def finish_requests(self, request_ids, finished_status: RequestStatus) -> list[tuple[str, int]]:
|
||||
"""Handles the finish signal from outside the scheduler.
|
||||
@@ -340,7 +383,20 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
if self.chunk_transfer_adapter:
|
||||
self.chunk_transfer_adapter.finish_requests(request_ids, finished_status, self.requests)
|
||||
|
||||
return super().finish_requests(request_ids, finished_status)
|
||||
finished = super().finish_requests(request_ids, finished_status)
|
||||
if self.input_coordinator is not None:
|
||||
for request_id, _ in finished:
|
||||
self._free_input_coordinator_request(request_id)
|
||||
return finished
|
||||
|
||||
def _free_request(self, request: Request, delay_free_blocks: bool = False) -> dict[str, Any] | None:
|
||||
if self.input_coordinator is None:
|
||||
return super()._free_request(request, delay_free_blocks)
|
||||
|
||||
try:
|
||||
return super()._free_request(request, delay_free_blocks)
|
||||
finally:
|
||||
self._free_input_coordinator_request(request.request_id)
|
||||
|
||||
"""
|
||||
Scheduler for the diffusion model.
|
||||
@@ -589,6 +645,16 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
engine_core_outputs[0] = eco = EngineCoreOutputs()
|
||||
eco.scheduler_stats = stats
|
||||
|
||||
omni_output = getattr(model_runner_output, "omni_connector_output", None)
|
||||
if omni_output is not None:
|
||||
self._latest_omni_connector_output = omni_output
|
||||
if self.input_coordinator and omni_output.request_metadata:
|
||||
self.input_coordinator.update_request_metadata(
|
||||
self.requests,
|
||||
omni_output.request_metadata,
|
||||
model_mode="generation",
|
||||
)
|
||||
|
||||
return engine_core_outputs
|
||||
|
||||
def _update_request_as_session(self, session: Request, update: StreamingUpdate) -> None:
|
||||
|
||||
@@ -7,6 +7,12 @@ from vllm.v1.request import Request, RequestStatus, StreamingUpdate
|
||||
class OmniSchedulerMixin:
|
||||
"""Shared scheduler helpers for omni-specific request handling."""
|
||||
|
||||
def _free_input_coordinator_request(self, request_id: str) -> None:
|
||||
"""Prune full-payload coordinator state for a completed request."""
|
||||
input_coordinator = getattr(self, "input_coordinator", None)
|
||||
if input_coordinator is not None:
|
||||
input_coordinator.free_finished_request(request_id)
|
||||
|
||||
def _replace_session_with_streaming_update(
|
||||
self,
|
||||
session: Request,
|
||||
|
||||
@@ -22,6 +22,15 @@ from vllm.v1.request import Request, RequestStatus
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def uses_qwen3_omni_full_payload_input_coordinator(model_config: Any) -> bool:
|
||||
return (
|
||||
getattr(model_config, "stage_id", 0) > 0
|
||||
and not getattr(model_config, "async_chunk", False)
|
||||
and getattr(model_config, "model_arch", None) == "Qwen3OmniMoeForConditionalGeneration"
|
||||
and getattr(model_config, "model_stage", None) in {"talker", "code2wav"}
|
||||
)
|
||||
|
||||
|
||||
class OmniSchedulingCoordinator:
|
||||
"""Pure-scheduling coordinator for chunk and full_payload input waiting.
|
||||
|
||||
@@ -166,8 +175,10 @@ class OmniSchedulingCoordinator:
|
||||
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)
|
||||
if to_remove:
|
||||
# Use the bulk-remove helper: one O(N) sweep instead of N
|
||||
# repeated O(N) removes from a list-backed queue.
|
||||
waiting_queue.remove_requests(to_remove)
|
||||
|
||||
def process_pending_full_payload_inputs_legacy(
|
||||
self,
|
||||
@@ -255,6 +266,30 @@ class OmniSchedulingCoordinator:
|
||||
waiting_queue.add_request(request)
|
||||
self._waiting_for_input = deque()
|
||||
|
||||
@staticmethod
|
||||
def _flatten_prompt_token_ids(value: Any) -> list[int]:
|
||||
"""Normalize connector metadata into flat prompt token ids."""
|
||||
if value is None:
|
||||
return []
|
||||
if hasattr(value, "detach") and hasattr(value, "cpu") and hasattr(value, "tolist"):
|
||||
value = value.detach().cpu().tolist()
|
||||
elif hasattr(value, "tolist") and not isinstance(value, (list, tuple)):
|
||||
value = value.tolist()
|
||||
|
||||
if isinstance(value, (list, tuple)):
|
||||
flattened: list[int] = []
|
||||
for item in value:
|
||||
if hasattr(item, "detach") and hasattr(item, "cpu") and hasattr(item, "tolist"):
|
||||
item = item.detach().cpu().tolist()
|
||||
elif hasattr(item, "tolist") and not isinstance(item, (list, tuple)):
|
||||
item = item.tolist()
|
||||
if isinstance(item, (list, tuple)):
|
||||
flattened.extend(int(token_id) for token_id in item)
|
||||
else:
|
||||
flattened.append(int(item))
|
||||
return flattened
|
||||
return [int(value)]
|
||||
|
||||
def update_request_metadata(
|
||||
self,
|
||||
requests: dict[str, Request],
|
||||
@@ -310,7 +345,7 @@ class OmniSchedulingCoordinator:
|
||||
)
|
||||
|
||||
if model_mode != "ar":
|
||||
new_ids = metadata.get("code_predictor_codes", [])
|
||||
new_ids = self._flatten_prompt_token_ids(metadata.get("code_predictor_codes"))
|
||||
runtime_seed = None
|
||||
if "left_context_size" in metadata:
|
||||
runtime_seed = {
|
||||
@@ -319,6 +354,10 @@ class OmniSchedulingCoordinator:
|
||||
request._omni_initial_model_buffer = runtime_seed
|
||||
if new_ids:
|
||||
request.prompt_token_ids = new_ids
|
||||
request.num_prompt_tokens = len(new_ids)
|
||||
request._all_token_ids.clear()
|
||||
request._all_token_ids.extend(new_ids)
|
||||
request._output_token_ids.clear()
|
||||
request.num_computed_tokens = 0
|
||||
|
||||
def postprocess_scheduler_output(
|
||||
@@ -374,7 +413,3 @@ class OmniSchedulingCoordinator:
|
||||
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
|
||||
@@ -1,4 +1,5 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from vllm.v1.core.sched.output import CachedRequestData, NewRequestData, SchedulerOutput
|
||||
from vllm.v1.request import Request
|
||||
@@ -75,3 +76,4 @@ class OmniSchedulerOutput(SchedulerOutput):
|
||||
"""Scheduler output with omni-specific transfer metadata."""
|
||||
|
||||
finished_requests_needing_kv_transfer: dict[str, dict] = field(default_factory=dict)
|
||||
pending_input_registrations: list[Any] = field(default_factory=list)
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import json
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -403,7 +404,16 @@ def build_stage_connectors(
|
||||
connectors_config: dict[str, Any],
|
||||
purpose: str = "request_forwarding",
|
||||
) -> dict[tuple[str, str], Any] | None:
|
||||
"""Instantiate OmniConnectors for a stage based on config."""
|
||||
"""Instantiate OmniConnectors for a stage based on config.
|
||||
|
||||
Deprecated: prefer ``get_stage_connector_config`` plus the unified
|
||||
connector factory. Kept as a thin shim so legacy callers keep working.
|
||||
"""
|
||||
warnings.warn(
|
||||
"build_stage_connectors is deprecated; use get_stage_connector_config and OmniConnectorFactory instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if not connectors_config:
|
||||
return {}
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ class RealtimeConnection(VllmRealtimeConnection):
|
||||
full_text = ""
|
||||
prompt_token_ids_len = 0
|
||||
completion_tokens_len = 0
|
||||
last_prompt_token_ids_len = 0 # detect Stage-0 segment rollover
|
||||
self._realtime_audio_ref = None
|
||||
|
||||
# Coerce cumulative outputs to delta outputs; this ensures
|
||||
@@ -144,6 +145,18 @@ class RealtimeConnection(VllmRealtimeConnection):
|
||||
new_token_ids = list(first_output.token_ids)
|
||||
new_tokens_len = len(new_token_ids)
|
||||
|
||||
cur_prompt_token_ids_len = len(output.prompt_token_ids or [])
|
||||
# Stage-0 segment rollover: buffer_realtime_audio may yield
|
||||
# multiple TokensPrompt segments and the second segment's
|
||||
# prompt_token_ids include the first segment's decoded output.
|
||||
# Clear accumulated full_text at the boundary so the
|
||||
# carried-over prefix is not duplicated in the final
|
||||
# TranscriptionDone.text.
|
||||
stage_id = getattr(output, "stage_id", None)
|
||||
if stage_id == 0 and cur_prompt_token_ids_len > last_prompt_token_ids_len > 0:
|
||||
full_text = ""
|
||||
last_prompt_token_ids_len = cur_prompt_token_ids_len
|
||||
|
||||
if not prompt_token_ids_len and output.prompt_token_ids:
|
||||
prompt_token_ids_len = len(output.prompt_token_ids)
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ QWEN3_OMNI_PIPELINE = PipelineConfig(
|
||||
requires_multimodal_data=True,
|
||||
hf_config_name="thinker_config",
|
||||
engine_output_type="latent",
|
||||
custom_process_next_stage_input_func=(f"{_PROC}.thinker2talker_async_chunk"),
|
||||
custom_process_next_stage_input_func=(f"{_PROC}.thinker2talker_full_payload"),
|
||||
async_chunk_process_next_stage_input_func=(f"{_PROC}.thinker2talker_async_chunk"),
|
||||
sampling_constraints={"detokenize": True},
|
||||
),
|
||||
StagePipelineConfig(
|
||||
@@ -41,7 +42,9 @@ QWEN3_OMNI_PIPELINE = PipelineConfig(
|
||||
hf_config_name="talker_config",
|
||||
engine_output_type="latent",
|
||||
custom_process_input_func=f"{_PROC}.thinker2talker",
|
||||
custom_process_next_stage_input_func=(f"{_PROC}.talker2code2wav_async_chunk"),
|
||||
sync_process_input_func=f"{_PROC}.thinker2talker_token_only",
|
||||
custom_process_next_stage_input_func=(f"{_PROC}.talker2code2wav_full_payload"),
|
||||
async_chunk_process_next_stage_input_func=(f"{_PROC}.talker2code2wav_async_chunk"),
|
||||
sampling_constraints={
|
||||
"detokenize": False,
|
||||
"stop_token_ids": [2150],
|
||||
|
||||
@@ -237,14 +237,22 @@ class Qwen3OmniMoeForConditionalGeneration(
|
||||
|
||||
prompt_token_ids = tokenizer.encode(prompt_template)
|
||||
|
||||
# In non-async-chunk (full-payload) mode the engine treats each
|
||||
# streaming TokensPrompt as a fresh decode, so mid-stream segment
|
||||
# yields cause the thinker to emit duplicate responses and the
|
||||
# talker to emit duplicate audio. Defer all audio to the final
|
||||
# flush so the thinker sees one complete prompt.
|
||||
async_chunk = getattr(model_config, "async_chunk", False)
|
||||
|
||||
async for audio_chunk in audio_stream:
|
||||
buffer.write_audio(audio_chunk)
|
||||
|
||||
while (segment := buffer.read_audio()) is not None:
|
||||
yield TokensPrompt(
|
||||
prompt_token_ids=prompt_token_ids,
|
||||
multi_modal_data={"audio": segment},
|
||||
)
|
||||
if async_chunk:
|
||||
while (segment := buffer.read_audio()) is not None:
|
||||
yield TokensPrompt(
|
||||
prompt_token_ids=prompt_token_ids,
|
||||
multi_modal_data={"audio": segment},
|
||||
)
|
||||
|
||||
remaining = buffer.flush()
|
||||
if remaining is not None and len(remaining) > 0:
|
||||
|
||||
@@ -35,6 +35,10 @@ logger = logging.getLogger(__name__)
|
||||
# Pooling output layer keys: "0" = word embedding, "24" = accept_hidden_layer
|
||||
_EMBED_LAYER_KEY = "0"
|
||||
_HIDDEN_LAYER_KEY = "24"
|
||||
_QWEN3_CODEC_CODEBOOK_SIZE = 2048
|
||||
_QWEN3_CODEC_PAD_TOKEN_ID = 4196
|
||||
_QWEN3_CODEC_BOS_TOKEN_ID = 4197
|
||||
_QWEN3_CODEC_EOS_TOKEN_ID = 4198
|
||||
|
||||
|
||||
def _layer_tensor(layers: dict[Any, Any], key: str) -> torch.Tensor | None:
|
||||
@@ -99,6 +103,84 @@ def _ensure_list(x):
|
||||
return list(x)
|
||||
|
||||
|
||||
def _as_tensor_or_none(value: Any) -> torch.Tensor | None:
|
||||
if isinstance(value, torch.Tensor):
|
||||
return value.detach().cpu()
|
||||
if isinstance(value, list) and value and isinstance(value[0], torch.Tensor):
|
||||
return value[0].detach().cpu()
|
||||
return None
|
||||
|
||||
|
||||
def _is_valid_qwen3_codec_token_id(token_id: Any) -> bool:
|
||||
try:
|
||||
token_id = int(token_id)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return 0 <= token_id < _QWEN3_CODEC_CODEBOOK_SIZE
|
||||
|
||||
|
||||
def should_accumulate_qwen3_omni_full_payload_output(
|
||||
model_config: Any,
|
||||
custom_process_func: Any,
|
||||
) -> bool:
|
||||
"""Return whether Qwen3-Omni should accumulate full-payload outputs."""
|
||||
return (
|
||||
custom_process_func is not None
|
||||
and not getattr(model_config, "async_chunk", False)
|
||||
and getattr(model_config, "model_arch", None) == "Qwen3OmniMoeForConditionalGeneration"
|
||||
and getattr(model_config, "model_stage", None) in {"thinker", "talker"}
|
||||
)
|
||||
|
||||
|
||||
def _extract_qwen3_full_payload_codec_rows(
|
||||
code_predictor_codes: torch.Tensor,
|
||||
output_token_ids: list[int],
|
||||
) -> tuple[torch.Tensor, dict[str, int]]:
|
||||
"""Filter full-payload codec rows by the authoritative output ids."""
|
||||
if code_predictor_codes.ndim != 2 or code_predictor_codes.numel() == 0:
|
||||
return code_predictor_codes, {
|
||||
"raw_rows": int(code_predictor_codes.shape[0]) if code_predictor_codes.ndim > 0 else 0,
|
||||
"aligned_rows": 0,
|
||||
"valid_rows": 0,
|
||||
"trailing_placeholder_count": 0,
|
||||
}
|
||||
|
||||
trailing_placeholder_count = 0
|
||||
while (
|
||||
trailing_placeholder_count < len(output_token_ids) and output_token_ids[-1 - trailing_placeholder_count] == -1
|
||||
):
|
||||
trailing_placeholder_count += 1
|
||||
|
||||
aligned_len = min(int(code_predictor_codes.shape[0]), len(output_token_ids))
|
||||
if aligned_len <= 0:
|
||||
return code_predictor_codes[:0], {
|
||||
"raw_rows": int(code_predictor_codes.shape[0]),
|
||||
"aligned_rows": 0,
|
||||
"valid_rows": 0,
|
||||
"trailing_placeholder_count": trailing_placeholder_count,
|
||||
}
|
||||
|
||||
aligned_rows = code_predictor_codes[-aligned_len:]
|
||||
aligned_token_ids = output_token_ids[-aligned_len:]
|
||||
aligned_token_mask = torch.tensor(
|
||||
[_is_valid_qwen3_codec_token_id(token_id) for token_id in aligned_token_ids],
|
||||
dtype=torch.bool,
|
||||
device=aligned_rows.device,
|
||||
)
|
||||
row_valid_mask = (aligned_rows.max(dim=1).values < _QWEN3_CODEC_CODEBOOK_SIZE) & (
|
||||
aligned_rows.min(dim=1).values >= 0
|
||||
)
|
||||
filtered_rows = aligned_rows[aligned_token_mask & row_valid_mask]
|
||||
if filtered_rows.numel() == 0:
|
||||
filtered_rows = aligned_rows[:0]
|
||||
return filtered_rows, {
|
||||
"raw_rows": int(code_predictor_codes.shape[0]),
|
||||
"aligned_rows": aligned_len,
|
||||
"valid_rows": int(filtered_rows.shape[0]) if filtered_rows.ndim > 0 else 0,
|
||||
"trailing_placeholder_count": trailing_placeholder_count,
|
||||
}
|
||||
|
||||
|
||||
# =========================
|
||||
# PD disaggregation helpers
|
||||
# =========================
|
||||
@@ -373,6 +455,79 @@ def thinker2talker_async_chunk(
|
||||
return payload
|
||||
|
||||
|
||||
def thinker2talker_full_payload(
|
||||
transfer_manager: Any,
|
||||
pooling_output: dict[str, Any],
|
||||
request: OmniEngineCoreRequest,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Pack complete thinker output for the non-async connector path."""
|
||||
if not isinstance(pooling_output, dict):
|
||||
return None
|
||||
|
||||
layers = {
|
||||
0: pooling_output.get("hidden_states.layer_0"),
|
||||
24: pooling_output.get("hidden_states.layer_24"),
|
||||
}
|
||||
thinker_emb = _layer_tensor(layers, _EMBED_LAYER_KEY)
|
||||
thinker_hid = _layer_tensor(layers, _HIDDEN_LAYER_KEY)
|
||||
if thinker_emb is None:
|
||||
hidden = pooling_output.get("hidden")
|
||||
thinker_emb = hidden if isinstance(hidden, torch.Tensor) else None
|
||||
if thinker_emb is None or thinker_hid is None:
|
||||
logger.debug(
|
||||
"thinker2talker_full_payload: missing thinker tensors for req=%s (embed=%s hidden=%s)",
|
||||
getattr(request, "request_id", None),
|
||||
thinker_emb is not None,
|
||||
thinker_hid is not None,
|
||||
)
|
||||
return None
|
||||
|
||||
prompt_token_ids = _ensure_list(getattr(request, "prompt_token_ids", []) or [])
|
||||
all_token_ids = _ensure_list(getattr(request, "all_token_ids", None) or [])
|
||||
if not all_token_ids:
|
||||
output_token_ids = _ensure_list(getattr(request, "output_token_ids", []) or [])
|
||||
all_token_ids = list(prompt_token_ids) + list(output_token_ids)
|
||||
|
||||
# Trim the trailing stop-token row from the accumulated thinker output.
|
||||
# The accumulator captures one hidden-state row per executed thinker
|
||||
# forward (prefill + every decode step including the one that emitted
|
||||
# the stop_token), so for a finished request thinker_emb has exactly one
|
||||
# row more than the rows the talker should consume. async_chunk's
|
||||
# chunk-0 path naturally captures only the prefill / non-stop portion,
|
||||
# which is why the [async_chunk] parametrization passes while [default]
|
||||
# over-generates one codec frame on short outputs (e.g.
|
||||
# test_one_word_prompt_001[default]: audio extends "London" with
|
||||
# spurious phonemes).
|
||||
if isinstance(thinker_emb, torch.Tensor) and thinker_emb.shape[0] > 0:
|
||||
thinker_emb_prefill = thinker_emb[:-1]
|
||||
else:
|
||||
thinker_emb_prefill = thinker_emb
|
||||
if isinstance(thinker_hid, torch.Tensor) and thinker_hid.shape[0] > 0:
|
||||
thinker_hid_prefill = thinker_hid[:-1]
|
||||
else:
|
||||
thinker_hid_prefill = thinker_hid
|
||||
|
||||
payload: OmniPayload = {
|
||||
"embed": {
|
||||
"prefill": thinker_emb_prefill.detach().cpu(),
|
||||
"tts_bos": _as_tensor_or_none(pooling_output.get("embed.tts_bos")),
|
||||
"tts_eos": _as_tensor_or_none(pooling_output.get("embed.tts_eos")),
|
||||
"tts_pad": _as_tensor_or_none(pooling_output.get("embed.tts_pad")),
|
||||
},
|
||||
"hidden_states": {"output": thinker_hid_prefill.detach().cpu()},
|
||||
"ids": {"all": list(all_token_ids), "prompt": list(prompt_token_ids)},
|
||||
"meta": {"finished": torch.tensor(True, dtype=torch.bool)},
|
||||
}
|
||||
payload["next_stage_prompt_len"] = _compute_talker_prompt_ids_length(payload, device="cpu")
|
||||
speaker = extract_speaker_from_request(request)
|
||||
if speaker is not None:
|
||||
payload["speaker"] = speaker
|
||||
language = extract_language_from_request(request)
|
||||
if language is not None:
|
||||
payload["language"] = language
|
||||
return payload
|
||||
|
||||
|
||||
def thinker2talker(
|
||||
source_outputs: list[Any],
|
||||
prompt: OmniTokensPrompt | TextPrompt | None = None,
|
||||
@@ -482,6 +637,85 @@ def thinker2talker(
|
||||
return talker_inputs
|
||||
|
||||
|
||||
def thinker2talker_token_only(
|
||||
source_outputs: list[Any],
|
||||
prompt: OmniTokensPrompt | TextPrompt | None = None,
|
||||
requires_multimodal_data: bool = False,
|
||||
streaming_context: Any | None = None,
|
||||
) -> list[OmniTokensPrompt]:
|
||||
"""Non-async-chunk Stage-1 input builder for the connector data plane.
|
||||
|
||||
The worker connector (Stage-0 ``thinker2talker_full_payload`` →
|
||||
``_sync_local_stage_payloads``) supplies the bulk talker conditioning
|
||||
tensors (embed / hidden_states / ids) via ``model_intermediate_buffer``.
|
||||
The orchestrator only needs to ship a placeholder prefill prompt of the
|
||||
correct length so the scheduler can allocate KV-cache slots.
|
||||
|
||||
Small per-request voice metadata (``speaker`` / ``language``) is forwarded
|
||||
here from the user prompt so the worker's line-408 buffer seed picks it
|
||||
up. The connector-side ``extract_speaker_from_request`` reads the
|
||||
strongly-typed ``request.additional_information.entries["speaker"]`` which
|
||||
currently does not always round-trip the user-supplied voice; until that
|
||||
plumbing is normalized, providing the small fields directly preserves
|
||||
voice selection (regression discovered on Buildkite 9668:
|
||||
``test_speaker_002[default]`` lost the preset voice).
|
||||
"""
|
||||
talker_inputs: list[OmniTokensPrompt] = []
|
||||
for i, thinker_output in enumerate(source_outputs):
|
||||
output = thinker_output.outputs[0]
|
||||
req_id = str(getattr(thinker_output, "request_id", f"idx-{i}"))
|
||||
# Skip-on-missing parity with thinker2talker_full_payload: if the
|
||||
# connector builder would drop this request (no MM dict or missing
|
||||
# hidden-state layers), do the same here so the worker buffer
|
||||
# presence agrees with the orchestrator's scheduling decision.
|
||||
thinker_mm_raw = getattr(output, "multimodal_output", None)
|
||||
if not isinstance(thinker_mm_raw, dict):
|
||||
logger.debug("thinker2talker_token_only: skip req=%s due to empty multimodal_output", req_id)
|
||||
continue
|
||||
mm_hs = thinker_mm_raw.get("hidden_states", {})
|
||||
mm_layers = mm_hs.get("layers", {}) if isinstance(mm_hs, dict) else {}
|
||||
if _layer_tensor(mm_layers, _EMBED_LAYER_KEY) is None or _layer_tensor(mm_layers, _HIDDEN_LAYER_KEY) is None:
|
||||
logger.debug("thinker2talker_token_only: skip req=%s due to missing hidden-state layers", req_id)
|
||||
continue
|
||||
prompt_token_ids = _ensure_list(thinker_output.prompt_token_ids)
|
||||
output_ids = _ensure_list(output.cumulative_token_ids)
|
||||
is_streaming_session = bool(getattr(streaming_context, "enabled", False))
|
||||
if is_streaming_session:
|
||||
prompt_token_ids, output_ids, thinker_sequences, thinker_input_ids = _get_streaming_talker_tokens(
|
||||
req_id,
|
||||
prompt_token_ids,
|
||||
output_ids,
|
||||
getattr(streaming_context, "new_prompt_len_snapshot", None),
|
||||
streaming_context,
|
||||
clear_state=bool(getattr(thinker_output, "finished", False)),
|
||||
)
|
||||
else:
|
||||
thinker_sequences = prompt_token_ids + output_ids
|
||||
thinker_input_ids = prompt_token_ids
|
||||
info_for_len = {"ids": {"all": thinker_sequences, "prompt": thinker_input_ids}}
|
||||
prompt_len = _compute_talker_prompt_ids_length(info_for_len, device="cpu")
|
||||
|
||||
# Forward only small voice metadata; bulk tensors come from the
|
||||
# connector path via _sync_local_stage_payloads.
|
||||
small_info: dict[str, Any] = {}
|
||||
speaker = extract_speaker_from_prompt(prompt, index=i)
|
||||
if speaker is not None:
|
||||
small_info["speaker"] = speaker
|
||||
language = extract_language_from_prompt(prompt, index=i)
|
||||
if language is not None:
|
||||
small_info["language"] = language
|
||||
|
||||
talker_inputs.append(
|
||||
OmniTokensPrompt(
|
||||
prompt_token_ids=[0] * prompt_len,
|
||||
additional_information=(small_info if small_info else None),
|
||||
multi_modal_data=None,
|
||||
mm_processor_kwargs=None,
|
||||
)
|
||||
)
|
||||
return talker_inputs
|
||||
|
||||
|
||||
# =========================
|
||||
# Talker -> Code2Wav
|
||||
# =========================
|
||||
@@ -556,6 +790,56 @@ def talker2code2wav_async_chunk(
|
||||
)
|
||||
|
||||
|
||||
def talker2code2wav_full_payload(
|
||||
transfer_manager: Any,
|
||||
pooling_output: dict[str, Any],
|
||||
request: OmniEngineCoreRequest,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Pack complete talker codec output for the non-async connector path."""
|
||||
if not isinstance(pooling_output, dict):
|
||||
return None
|
||||
code_predictor_codes = pooling_output.get("codes.audio")
|
||||
if code_predictor_codes is None:
|
||||
codes = pooling_output.get("codes")
|
||||
if isinstance(codes, dict):
|
||||
code_predictor_codes = codes.get("audio")
|
||||
if code_predictor_codes is None:
|
||||
return None
|
||||
if not isinstance(code_predictor_codes, torch.Tensor):
|
||||
code_predictor_codes = torch.as_tensor(code_predictor_codes)
|
||||
if code_predictor_codes.numel() == 0:
|
||||
return None
|
||||
|
||||
output_token_ids = _ensure_list(getattr(request, "output_token_ids", []) or [])
|
||||
raw_shape = tuple(code_predictor_codes.shape)
|
||||
code_predictor_codes, codec_stats = _extract_qwen3_full_payload_codec_rows(
|
||||
code_predictor_codes.to(torch.long),
|
||||
list(output_token_ids),
|
||||
)
|
||||
if code_predictor_codes.numel() == 0:
|
||||
return None
|
||||
|
||||
codec_codes = code_predictor_codes.transpose(0, 1).cpu().reshape(-1).tolist()
|
||||
logger.debug(
|
||||
"talker2code2wav_full_payload: raw_shape=%s output_ids_len=%s aligned_rows=%s "
|
||||
"valid_rows=%s placeholders=%s flattened_len=%s pad4196=%s bos4197=%s eos4198=%s",
|
||||
raw_shape,
|
||||
len(output_token_ids),
|
||||
codec_stats["aligned_rows"],
|
||||
codec_stats["valid_rows"],
|
||||
codec_stats["trailing_placeholder_count"],
|
||||
len(codec_codes),
|
||||
sum(1 for tid in output_token_ids if tid == _QWEN3_CODEC_PAD_TOKEN_ID),
|
||||
sum(1 for tid in output_token_ids if tid == _QWEN3_CODEC_BOS_TOKEN_ID),
|
||||
sum(1 for tid in output_token_ids if tid == _QWEN3_CODEC_EOS_TOKEN_ID),
|
||||
)
|
||||
return {
|
||||
"codes": {"audio": codec_codes},
|
||||
"code_predictor_codes": codec_codes,
|
||||
"meta": {"finished": torch.tensor(True, dtype=torch.bool)},
|
||||
}
|
||||
|
||||
|
||||
def talker2code2wav(
|
||||
source_outputs: list[Any],
|
||||
_prompt: OmniTokensPrompt | TextPrompt | None = None,
|
||||
|
||||
@@ -102,6 +102,11 @@ if not hasattr(RequestStatus, "WAITING_FOR_CHUNK"):
|
||||
# as a non-finished state and remains compatible with existing comparisons.
|
||||
extend_enum(RequestStatus, "WAITING_FOR_CHUNK", -1)
|
||||
|
||||
if not hasattr(RequestStatus, "WAITING_FOR_INPUT"):
|
||||
# Full-payload stage handoff uses a distinct waiting state so the
|
||||
# scheduler can restore the request once non-stage-0 inputs arrive.
|
||||
extend_enum(RequestStatus, "WAITING_FOR_INPUT", -2)
|
||||
|
||||
# Snapshot sys.modules: `hasattr` below can trigger lazy submodule imports
|
||||
# (e.g. transformers' `_LazyModule.__getattr__`), which mutate sys.modules
|
||||
# during iteration and raise `dictionary changed size during iteration`.
|
||||
|
||||
@@ -81,6 +81,16 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
|
||||
self.inputs_embeds = self._make_buffer(self.max_num_tokens, self.hidden_size, dtype=self.dtype, numpy=False)
|
||||
# Initialize KV cache manager (preserve vllm_config fallback behavior)
|
||||
self.kv_transfer_manager = OmniKVTransferManager.from_vllm_config(self.vllm_config, self.model_config)
|
||||
# Only Qwen3-Omni currently consumes the connector-based full-payload
|
||||
# handoff added in this PR. Other model architectures (e.g. Bagel
|
||||
# diffusion) retain their pre-existing runner behavior so this PR
|
||||
# does not perturb them.
|
||||
if getattr(self.model_config, "model_arch", None) == "Qwen3OmniMoeForConditionalGeneration":
|
||||
self.init_omni_connectors(
|
||||
vllm_config=self.vllm_config,
|
||||
model_config=self.model_config,
|
||||
kv_transfer_manager=self.kv_transfer_manager,
|
||||
)
|
||||
self._downstream_payload_cache: dict[str, bool] = {}
|
||||
|
||||
def _make_buffer(self, *size, dtype, numpy=True):
|
||||
@@ -337,6 +347,16 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
|
||||
request_id_resolver=self._resolve_global_request_id,
|
||||
)
|
||||
|
||||
if hasattr(self, "_omni_connector"):
|
||||
for request in getattr(scheduler_output, "pending_input_registrations", []):
|
||||
self.register_chunk_recv(request)
|
||||
self.recv_full_payload_inputs(scheduler_output)
|
||||
if self._pending_full_payload_send:
|
||||
flush_ids = set(getattr(scheduler_output, "finished_req_ids", set()))
|
||||
flush_ids.update({rid for rid in self._pending_full_payload_send if rid not in self.requests})
|
||||
if flush_ids:
|
||||
self.flush_full_payload_outputs(flush_ids)
|
||||
|
||||
if self.routed_experts_initialized:
|
||||
capturer = RoutedExpertsCapturer.get_instance()
|
||||
if capturer is not None:
|
||||
@@ -375,7 +395,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
|
||||
if kv_ids:
|
||||
output = copy(output)
|
||||
output.kv_extracted_req_ids = kv_ids
|
||||
return output
|
||||
return self.attach_omni_connector_output(output)
|
||||
|
||||
if not num_scheduled_tokens:
|
||||
if (
|
||||
@@ -399,7 +419,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
|
||||
output = copy(output)
|
||||
output.kv_extracted_req_ids = kv_ids
|
||||
|
||||
return output
|
||||
return self.attach_omni_connector_output(output)
|
||||
|
||||
if self.cache_config.kv_sharing_fast_prefill:
|
||||
assert not self.num_prompt_logprobs, (
|
||||
@@ -735,11 +755,11 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
|
||||
# In case of PP with kv transfer, we need to pass through the
|
||||
# kv_connector_output
|
||||
if kv_connector_output.is_empty():
|
||||
return EMPTY_MODEL_RUNNER_OUTPUT
|
||||
return self.attach_omni_connector_output(EMPTY_MODEL_RUNNER_OUTPUT)
|
||||
|
||||
output = copy(EMPTY_MODEL_RUNNER_OUTPUT)
|
||||
output.kv_connector_output = kv_connector_output
|
||||
return output
|
||||
return self.attach_omni_connector_output(output)
|
||||
|
||||
# Unpack ephemeral state.
|
||||
(
|
||||
@@ -985,6 +1005,13 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
|
||||
# Flatten nested dicts to dotted keys so pooling_output
|
||||
# stays dict[str, torch.Tensor] for msgspec serialization.
|
||||
pooler_output.append(flatten_payload(payload))
|
||||
|
||||
if pooler_output and self._should_accumulate_full_payload_output():
|
||||
for i, rid in enumerate(req_ids_output_copy):
|
||||
req_state = self.requests.get(rid)
|
||||
if req_state is not None and pooler_output[i]:
|
||||
self.accumulate_full_payload_output(rid, pooler_output[i], req_state)
|
||||
|
||||
with record_function_or_nullcontext("gpu_model_runner: ModelRunnerOutput"):
|
||||
if self.routed_experts_initialized:
|
||||
capturer = RoutedExpertsCapturer.get_instance()
|
||||
@@ -1005,6 +1032,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
|
||||
cudagraph_stats=cudagraph_stats,
|
||||
)
|
||||
output.kv_extracted_req_ids = kv_extracted_req_ids
|
||||
output.omni_connector_output = self.get_omni_connector_output()
|
||||
|
||||
if not self.use_async_scheduling:
|
||||
return output
|
||||
|
||||
@@ -53,6 +53,17 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
|
||||
- Executes generation process and returns tensors via `pooler_output`.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Scope full-payload connector init to Qwen3-Omni: other generation
|
||||
# models (e.g. Bagel DiT) retain their pre-existing runner setup
|
||||
# so this refactor does not perturb them.
|
||||
if getattr(self.model_config, "model_arch", None) == "Qwen3OmniMoeForConditionalGeneration":
|
||||
self.init_omni_connectors(
|
||||
vllm_config=self.vllm_config,
|
||||
model_config=self.model_config,
|
||||
)
|
||||
|
||||
def _update_request_states(self, scheduler_output: SchedulerOutput):
|
||||
# remove requests
|
||||
for req_id in scheduler_output.finished_req_ids:
|
||||
@@ -89,6 +100,16 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
|
||||
if self.execute_model_state is not None:
|
||||
raise RuntimeError("State error: sample_tokens() must be called after execute_model() returns None.")
|
||||
|
||||
if hasattr(self, "_omni_connector"):
|
||||
for request in getattr(scheduler_output, "pending_input_registrations", []):
|
||||
self.register_chunk_recv(request)
|
||||
self.recv_full_payload_inputs(scheduler_output)
|
||||
if self._pending_full_payload_send:
|
||||
flush_ids = set(getattr(scheduler_output, "finished_req_ids", set()))
|
||||
flush_ids.update({rid for rid in self._pending_full_payload_send if rid not in self.requests})
|
||||
if flush_ids:
|
||||
self.flush_full_payload_outputs(flush_ids)
|
||||
|
||||
if self.routed_experts_initialized:
|
||||
capturer = RoutedExpertsCapturer.get_instance()
|
||||
if capturer is not None:
|
||||
@@ -110,7 +131,7 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
|
||||
self._update_request_states(scheduler_output)
|
||||
deferred_state_corrections_fn = self._update_states(scheduler_output)
|
||||
if not scheduler_output.total_num_scheduled_tokens:
|
||||
return EMPTY_MODEL_RUNNER_OUTPUT
|
||||
return self.attach_omni_connector_output(EMPTY_MODEL_RUNNER_OUTPUT)
|
||||
|
||||
if has_ec_transfer() and not get_ec_transfer().is_consumer:
|
||||
with self.maybe_get_ec_connector_output(
|
||||
@@ -118,7 +139,7 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
|
||||
encoder_cache=self.encoder_cache,
|
||||
) as ec_connector_output:
|
||||
self._execute_mm_encoder(scheduler_output)
|
||||
return make_empty_encoder_model_runner_output(scheduler_output)
|
||||
return self.attach_omni_connector_output(make_empty_encoder_model_runner_output(scheduler_output))
|
||||
|
||||
if not num_scheduled_tokens:
|
||||
if (
|
||||
@@ -134,9 +155,10 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
|
||||
self._dummy_run(1)
|
||||
if not has_kv_transfer_group():
|
||||
# Return empty ModelRunnerOutput if no work to do.
|
||||
return EMPTY_MODEL_RUNNER_OUTPUT
|
||||
return self.attach_omni_connector_output(EMPTY_MODEL_RUNNER_OUTPUT)
|
||||
|
||||
return self.kv_connector_no_forward(scheduler_output, self.vllm_config)
|
||||
result = self.kv_connector_no_forward(scheduler_output, self.vllm_config)
|
||||
return self.attach_omni_connector_output(result)
|
||||
|
||||
if self.cache_config.kv_sharing_fast_prefill:
|
||||
assert not self.num_prompt_logprobs, (
|
||||
@@ -338,11 +360,11 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
|
||||
# In case of PP with kv transfer, we need to pass through the
|
||||
# kv_connector_output
|
||||
if kv_connector_output.is_empty():
|
||||
return EMPTY_MODEL_RUNNER_OUTPUT
|
||||
return self.attach_omni_connector_output(EMPTY_MODEL_RUNNER_OUTPUT)
|
||||
|
||||
output = copy(EMPTY_MODEL_RUNNER_OUTPUT)
|
||||
output.kv_connector_output = kv_connector_output
|
||||
return output
|
||||
return self.attach_omni_connector_output(output)
|
||||
|
||||
# Unpack ephemeral state.
|
||||
(
|
||||
@@ -403,6 +425,12 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
|
||||
# [Omni] Copy req_id mappings to avoid async scheduling mutation.
|
||||
req_ids_output_copy = self.input_batch.req_ids.copy()
|
||||
req_id_to_index_output_copy = self.input_batch.req_id_to_index.copy()
|
||||
if self._should_accumulate_full_payload_output():
|
||||
for i, rid in enumerate(req_ids_output_copy):
|
||||
req_state = self.requests.get(rid)
|
||||
if req_state is not None and pooler_output[i]:
|
||||
self.accumulate_full_payload_output(rid, pooler_output[i], req_state)
|
||||
|
||||
output = OmniModelRunnerOutput(
|
||||
req_ids=req_ids_output_copy,
|
||||
req_id_to_index=req_id_to_index_output_copy,
|
||||
@@ -415,6 +443,7 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
|
||||
cudagraph_stats=cudagraph_stats,
|
||||
ec_connector_output=ec_connector_output if self.supports_mm_inputs else None,
|
||||
)
|
||||
output.omni_connector_output = self.get_omni_connector_output()
|
||||
|
||||
if not self.use_async_scheduling:
|
||||
return output
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import contextlib
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import numpy as np
|
||||
@@ -264,6 +265,14 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
self.omni_prefix_cache.reset_prefix_cached_new_req_ids()
|
||||
|
||||
# Remove finished requests from the cached states.
|
||||
# cleanup_finished_request lives on OmniConnectorModelRunnerMixin and
|
||||
# is only safe to call once init_omni_connectors() has populated the
|
||||
# mixin state. Archs that inherit the method via MRO without running
|
||||
# that init must be skipped, so probe a mixin-owned attribute as the
|
||||
# "state initialized" gate.
|
||||
cleanup_finished_request = (
|
||||
getattr(self, "cleanup_finished_request", None) if hasattr(self, "_request_ids_mapping") else None
|
||||
)
|
||||
for req_id in scheduler_output.finished_req_ids:
|
||||
self.requests.pop(req_id, None)
|
||||
self.model_intermediate_buffer.pop(req_id, None)
|
||||
@@ -272,6 +281,8 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
self._downstream_payload_cache.pop(req_id, None)
|
||||
if hasattr(self, "_talker_mtp_generators"):
|
||||
self._talker_mtp_generators.pop(req_id, None)
|
||||
if cleanup_finished_request is not None:
|
||||
cleanup_finished_request(req_id)
|
||||
|
||||
if hasattr(self, "late_interaction_runner"):
|
||||
self.late_interaction_runner.on_requests_finished(scheduler_output.finished_req_ids)
|
||||
@@ -1033,8 +1044,31 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
req_token_spans.append((start_offset, start_offset + sched_tokens))
|
||||
return req_token_spans
|
||||
|
||||
def _sync_local_stage_payloads(self) -> None:
|
||||
"""Move received full-payload stage inputs into model_intermediate_buffer."""
|
||||
cache = getattr(self, "_local_stage_payload_cache", None)
|
||||
if not cache:
|
||||
return
|
||||
lock = getattr(self, "_lock", None)
|
||||
ctx = lock if lock is not None else contextlib.nullcontext()
|
||||
with ctx:
|
||||
if not cache:
|
||||
return
|
||||
active_req_ids = set(getattr(self, "requests", {}))
|
||||
pending = set(getattr(self, "_full_payload_pending_broadcast_req_ids", set()))
|
||||
staged = {
|
||||
req_id: payload
|
||||
for req_id, payload in cache.items()
|
||||
if req_id not in pending and req_id in active_req_ids and isinstance(payload, dict)
|
||||
}
|
||||
for req_id in staged:
|
||||
cache.pop(req_id, None)
|
||||
for req_id, payload in staged.items():
|
||||
self._update_intermediate_buffer(req_id, payload)
|
||||
|
||||
def _build_model_kwargs_extra(self) -> dict:
|
||||
"""Build extra keyword arguments passed to the model for this step."""
|
||||
self._sync_local_stage_payloads()
|
||||
model_kwargs_extra: dict[str, object] = {}
|
||||
try:
|
||||
buffer_map = self._gather_runtime_additional_information()
|
||||
@@ -1299,6 +1333,13 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
if hasattr(self.model, "has_preprocess") or hasattr(self.model, "enable_update_additional_information"):
|
||||
if self.vllm_config.model_config.async_chunk:
|
||||
self._update_additional_information(scheduler_output)
|
||||
else:
|
||||
# In full-payload (non-async-chunk) mode, connector-delivered
|
||||
# stage payloads must override any earlier engine-level
|
||||
# additional_information written by the legacy
|
||||
# custom_process_input_func codec, so talker_preprocess reads
|
||||
# the full thinker payload.
|
||||
self._sync_local_stage_payloads()
|
||||
|
||||
if hasattr(self.model, "has_preprocess") and self.model.has_preprocess:
|
||||
# Overlay custom prompt_embeds per request for the prompt portion;
|
||||
|
||||
@@ -153,7 +153,7 @@ class OmniConnectorModelRunnerMixin:
|
||||
|
||||
# -- full_payload_mode: accumulate latest pooler_output per request,
|
||||
# send only when the request finishes (next-cycle flush) --
|
||||
self._pending_full_payload_send: dict[str, tuple[Any, Any]] = {}
|
||||
self._pending_full_payload_send: dict[str, tuple[Any, ...]] = {}
|
||||
|
||||
# -- KV sent accumulator --
|
||||
self._kv_sent_req_ids: list[str] = []
|
||||
@@ -206,21 +206,33 @@ class OmniConnectorModelRunnerMixin:
|
||||
"""Clean up per-request state after a request is fully finished.
|
||||
|
||||
Call this when a request is freed from the model runner to prevent
|
||||
memory leaks in the mixin's tracking dicts/sets. The external
|
||||
request ID is resolved before cleaning up ``_put_req_chunk`` which
|
||||
is keyed by external ID.
|
||||
memory leaks in the mixin's tracking dicts/sets.
|
||||
|
||||
Two senders use different keys: ``send_chunk`` keys per-request
|
||||
state under the EXTERNAL id (after mapping resolution), while
|
||||
``send_full_payload_outputs`` keys under the INTERNAL id. To cover
|
||||
both modes (and forward compat with id-rename scenarios) we attempt
|
||||
cleanup against both keys; the entry that doesn't exist for the
|
||||
active mode is a no-op pop. Only the key that actually has pending
|
||||
saves is added to ``_deferred_send_cleanup`` so the bg save's
|
||||
decrement path drains it without leaving orphans.
|
||||
"""
|
||||
ext_id = self._request_ids_mapping.pop(req_id, None)
|
||||
send_req_id = ext_id if ext_id is not None else req_id
|
||||
keys_to_clean: list[str] = [req_id]
|
||||
if ext_id is not None and ext_id != req_id:
|
||||
keys_to_clean.append(ext_id)
|
||||
|
||||
with self._lock:
|
||||
if self._pending_save_counts.get(send_req_id, 0):
|
||||
self._deferred_send_cleanup.add(send_req_id)
|
||||
else:
|
||||
self._put_req_chunk.pop(send_req_id, None)
|
||||
self._send_side_request_payload.pop(send_req_id, None)
|
||||
self._code_prompt_token_ids.pop(send_req_id, None)
|
||||
self._cached_ic.pop(send_req_id, None)
|
||||
keys_pending = [k for k in keys_to_clean if self._pending_save_counts.get(k, 0)]
|
||||
for k in keys_pending:
|
||||
self._deferred_send_cleanup.add(k)
|
||||
for k in keys_to_clean:
|
||||
if k in keys_pending:
|
||||
continue
|
||||
self._put_req_chunk.pop(k, None)
|
||||
self._send_side_request_payload.pop(k, None)
|
||||
self._code_prompt_token_ids.pop(k, None)
|
||||
self._cached_ic.pop(k, None)
|
||||
self._kv_pending_transfers.pop(req_id, None)
|
||||
self._kv_active_transfers.discard(req_id)
|
||||
self._kv_completed_transfers.discard(req_id)
|
||||
@@ -628,6 +640,16 @@ class OmniConnectorModelRunnerMixin:
|
||||
that has arrived, or ``None`` if nothing is ready. Stores full
|
||||
payloads in the local cache and extracts scheduling metadata.
|
||||
"""
|
||||
# Fast path: when TP is trivial (no peer ranks waiting on a broadcast)
|
||||
# and the bg recv thread has not staged anything, skip the lock + TP
|
||||
# broadcast cycle entirely. _broadcast_tp_payload_packet already
|
||||
# returns its input unchanged under the same world_size<=1 condition,
|
||||
# so the original code path was a no-op here on every empty step.
|
||||
tp_group = self._get_local_tp_group()
|
||||
if (
|
||||
tp_group is None or getattr(tp_group, "world_size", 1) <= 1
|
||||
) and not self._full_payload_pending_broadcast_req_ids:
|
||||
return None
|
||||
with self._lock:
|
||||
results = self._collect_full_payload_results_locked() if self.is_data_transfer_rank() else None
|
||||
results = self._broadcast_tp_payload_packet(results)
|
||||
@@ -649,10 +671,63 @@ class OmniConnectorModelRunnerMixin:
|
||||
)
|
||||
return results
|
||||
|
||||
def _get_model_config(self) -> Any:
|
||||
model_config = getattr(self, "model_config", None)
|
||||
if model_config is not None:
|
||||
return model_config
|
||||
return getattr(getattr(self, "vllm_config", None), "model_config", None)
|
||||
|
||||
def _should_accumulate_full_payload_output(self) -> bool:
|
||||
"""Gate send-side full-payload output accumulation only.
|
||||
|
||||
Cached per instance: the result depends only on model_config /
|
||||
_custom_process_func, both of which are set at init time. Avoid
|
||||
the per-step dynamic import inside the model decode loop.
|
||||
"""
|
||||
cached = getattr(self, "_should_accumulate_full_payload_output_cached", None)
|
||||
if cached is not None:
|
||||
return cached
|
||||
model_config = self._get_model_config()
|
||||
if model_config is None:
|
||||
self._should_accumulate_full_payload_output_cached = False
|
||||
return False
|
||||
if getattr(model_config, "model_arch", None) == "Qwen3OmniMoeForConditionalGeneration":
|
||||
from vllm_omni.model_executor.stage_input_processors.qwen3_omni import (
|
||||
should_accumulate_qwen3_omni_full_payload_output,
|
||||
)
|
||||
|
||||
result = should_accumulate_qwen3_omni_full_payload_output(
|
||||
model_config,
|
||||
getattr(self, "_custom_process_func", None),
|
||||
)
|
||||
self._should_accumulate_full_payload_output_cached = result
|
||||
return result
|
||||
self._should_accumulate_full_payload_output_cached = False
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _is_all_zero_tensor(t: Any) -> bool:
|
||||
"""Return True if *t* is a torch.Tensor whose elements are all zero."""
|
||||
return isinstance(t, torch.Tensor) and t.numel() > 0 and not t.any()
|
||||
def _new_full_payload_accumulator(output: dict[str, Any]):
|
||||
chunks: dict[str, list[torch.Tensor]] = {}
|
||||
latest: dict[str, Any] = {}
|
||||
rows: dict[str, int] = {}
|
||||
for k, v in output.items():
|
||||
if isinstance(v, torch.Tensor) and v.dim() >= 2:
|
||||
chunks[k] = [v]
|
||||
rows[k] = int(v.shape[0])
|
||||
else:
|
||||
latest[k] = v
|
||||
return chunks, latest, rows
|
||||
|
||||
@staticmethod
|
||||
def _materialize_full_payload_entry(entry):
|
||||
if len(entry) == 2:
|
||||
return entry
|
||||
chunks, latest, _rows, request = entry
|
||||
output = dict(latest)
|
||||
for k, tensors in chunks.items():
|
||||
if tensors:
|
||||
output[k] = tensors[0] if len(tensors) == 1 else torch.cat(tensors, dim=0)
|
||||
return output, request
|
||||
|
||||
def accumulate_full_payload_output(
|
||||
self,
|
||||
@@ -666,55 +741,45 @@ class OmniConnectorModelRunnerMixin:
|
||||
along dim-0. Scalar / global tensors (1-D or 0-D) are replaced
|
||||
with the latest value.
|
||||
|
||||
All-zero tensors (e.g. ``code_predictor_codes`` emitted during
|
||||
prefill) are dropped so that they do not pollute downstream stages
|
||||
with garbage / noise frames.
|
||||
Note: codec rows are NOT filtered for zero placeholders here. The
|
||||
downstream consumer ``_extract_qwen3_full_payload_codec_rows`` crops
|
||||
codec rows using ``output_token_ids`` as the authoritative source,
|
||||
which makes any sender-side zero filtering redundant. Skipping the
|
||||
sender-side ``t.any()`` scan also avoids a per-tensor GPU->CPU device
|
||||
sync that stalled the decode pipeline.
|
||||
|
||||
The data is actually sent when ``flush_full_payload_outputs`` is called
|
||||
with the finished request IDs from the next scheduler cycle.
|
||||
"""
|
||||
# ---- Filter out all-zero tensors from the incoming pooler_output ----
|
||||
filtered: dict[str, Any] = {}
|
||||
dropped_zero_keys: list[tuple[str, tuple[int, ...]]] = []
|
||||
for k, v in pooler_output.items():
|
||||
if self._is_all_zero_tensor(v):
|
||||
dropped_zero_keys.append((k, tuple(v.shape)))
|
||||
continue # skip prefill zero-filled placeholders
|
||||
filtered[k] = v
|
||||
if dropped_zero_keys:
|
||||
logger.info(
|
||||
"[Stage-%s] accumulate_full_payload_output: req=%s dropped_zero_keys=%s",
|
||||
self._stage_id,
|
||||
req_id,
|
||||
dropped_zero_keys,
|
||||
)
|
||||
pooler_output = filtered
|
||||
|
||||
existing = self._pending_full_payload_send.get(req_id)
|
||||
|
||||
if existing is None:
|
||||
self._pending_full_payload_send[req_id] = (pooler_output, request)
|
||||
chunks, latest, rows = self._new_full_payload_accumulator(pooler_output)
|
||||
self._pending_full_payload_send[req_id] = (chunks, latest, rows, request)
|
||||
return
|
||||
|
||||
prev_output, _ = existing
|
||||
merged: dict[str, Any] = {}
|
||||
for k in set(prev_output) | set(pooler_output):
|
||||
v_new = pooler_output.get(k)
|
||||
v_old = prev_output.get(k)
|
||||
if v_new is None:
|
||||
merged[k] = v_old
|
||||
elif v_old is None:
|
||||
merged[k] = v_new
|
||||
elif (
|
||||
isinstance(v_new, torch.Tensor)
|
||||
and isinstance(v_old, torch.Tensor)
|
||||
and v_new.dim() >= 2
|
||||
and v_old.dim() >= 2
|
||||
and v_new.shape[1:] == v_old.shape[1:]
|
||||
):
|
||||
merged[k] = torch.cat([v_old, v_new], dim=0)
|
||||
if len(existing) == 2:
|
||||
chunks, latest, rows = self._new_full_payload_accumulator(existing[0])
|
||||
else:
|
||||
chunks, latest, rows, _ = existing
|
||||
|
||||
for k, v in pooler_output.items():
|
||||
if v is None:
|
||||
continue
|
||||
if isinstance(v, torch.Tensor) and v.dim() >= 2:
|
||||
if k in chunks and chunks[k] and v.shape[1:] == chunks[k][0].shape[1:]:
|
||||
chunks[k].append(v)
|
||||
rows[k] += int(v.shape[0])
|
||||
else:
|
||||
latest.pop(k, None)
|
||||
chunks[k] = [v]
|
||||
rows[k] = int(v.shape[0])
|
||||
else:
|
||||
merged[k] = v_new
|
||||
self._pending_full_payload_send[req_id] = (merged, request)
|
||||
chunks.pop(k, None)
|
||||
rows.pop(k, None)
|
||||
latest[k] = v
|
||||
|
||||
self._pending_full_payload_send[req_id] = (chunks, latest, rows, request)
|
||||
|
||||
def flush_full_payload_outputs(self, finished_req_ids: set[str]) -> None:
|
||||
"""Send accumulated full_payload outputs for requests that just finished."""
|
||||
@@ -728,7 +793,7 @@ class OmniConnectorModelRunnerMixin:
|
||||
for req_id in finished_req_ids:
|
||||
entry = self._pending_full_payload_send.pop(req_id, None)
|
||||
if entry is not None:
|
||||
to_send[req_id] = entry
|
||||
to_send[req_id] = self._materialize_full_payload_entry(entry)
|
||||
logger.info("[Stage-%s] flush_full_payload_outputs: to_send=%s", self._stage_id, list(to_send.keys()))
|
||||
if to_send:
|
||||
self.send_full_payload_outputs(scheduler_output=None, outputs=to_send)
|
||||
@@ -1476,7 +1541,7 @@ class OmniConnectorModelRunnerMixin:
|
||||
logger.warning("Error receiving data for %s", req_id, exc_info=True)
|
||||
|
||||
if not made_progress and not self._stop_event.is_set():
|
||||
self._work_available.wait(timeout=0.001)
|
||||
self._work_available.wait(timeout=0.005)
|
||||
self._work_available.clear()
|
||||
|
||||
_MAX_SEND_RETRIES = 3
|
||||
|
||||
Reference in new issue
Block a user