[Refactor] msgspec standardisation for data entry key names and improved type checks (#3149)

Signed-off-by: Divyansh Singhvi <divyanshsinghvi@gmail.com>
Signed-off-by: dsinghvi <divyanshsinghvi@gmail.com>
This commit is contained in:
dsinghvi authored and GitHub committed 2026-05-12 14:30:13 +08:00
1 parent 9a60e11e3d
commit e40076e872
25 files changed
+1047 -686

No files matched your search

@@ -222,7 +222,7 @@ class TestChunkCoordinatorUpdateRequestMetadata(unittest.TestCase):
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})
self.assertEqual(req._omni_initial_model_buffer, {"meta": {"left_context_size": 25}})
class TestChunkCoordinatorPostprocess(unittest.TestCase):
@@ -12,7 +12,7 @@ from pytest_mock import MockerFixture
from vllm.v1.core.sched.scheduler import Scheduler as VLLMScheduler
from vllm.v1.request import RequestStatus
from vllm_omni.data_entry_keys import OmniPayload
from vllm_omni.data_entry_keys import CodesStruct, MetaStruct, OmniPayload, OmniPayloadStruct
from vllm_omni.distributed.omni_connectors.transfer_adapter.base import OmniTransferAdapterBase
from vllm_omni.distributed.omni_connectors.transfer_adapter.chunk_transfer_adapter import (
OmniChunkTransferAdapter,
@@ -140,11 +140,54 @@ def test_save_async(build_adapter):
assert task["is_finished"] is False
def test_send_single_request_struct_without_meta_does_not_crash(build_adapter, monkeypatch):
"""Producer may return a struct with ``meta=None`` (e.g. payload that
carries only ``embed`` or ``codes``). The sender's ``meta is not None``
guard handles this without AttributeError; ``finished_flag`` is None and
the cleanup path is not triggered.
"""
adapter, _ = build_adapter(stage_id=1)
request = _req("req-no-meta", RequestStatus.WAITING, external_req_id="ext-no-meta")
adapter.custom_process_next_stage_input_func = lambda **kwargs: OmniPayloadStruct(
codes=CodesStruct(audio=torch.tensor([1, 2], dtype=torch.long)),
)
cleanup_calls = []
monkeypatch.setattr(adapter, "cleanup", lambda *a, **kw: cleanup_calls.append((a, kw)))
adapter._send_single_request({"pooling_output": None, "request": request, "is_finished": False})
assert cleanup_calls == [] # no terminal cleanup; meta.finished is unobservable
def test_send_single_request_empty_struct_goes_on_wire(build_adapter, monkeypatch):
"""Pin the contract: an explicitly empty ``OmniPayloadStruct()`` passes
the ``payload_data is None`` check and gets sent. To skip a chunk, the
producer must return ``None``, not an empty struct. (Filtering empty
structs at the adapter would require introspecting all struct fields on
every send and was rejected for cost vs. value.)
"""
adapter, connector = build_adapter(stage_id=1)
request = _req("req-empty", RequestStatus.WAITING, external_req_id="ext-empty")
adapter.custom_process_next_stage_input_func = lambda **kwargs: OmniPayloadStruct()
monkeypatch.setattr(adapter, "cleanup", lambda *a, **kw: None)
adapter._send_single_request({"pooling_output": None, "request": request, "is_finished": False})
assert connector.put.called
sent_payload = connector.put.call_args.kwargs["data"]
assert isinstance(sent_payload, OmniPayloadStruct)
assert sent_payload.meta is None # confirms it's the empty struct on the wire
def test_send_single_request_cleans_up_after_finished_payload(build_adapter, monkeypatch):
adapter, _ = build_adapter(stage_id=1)
request = _req("req-finished", RequestStatus.FINISHED_STOPPED, external_req_id="ext-finished")
adapter.custom_process_next_stage_input_func = lambda **kwargs: {"x": [1], "finished": True}
adapter.custom_process_next_stage_input_func = lambda **kwargs: OmniPayloadStruct(
meta=MetaStruct(finished=torch.tensor(True, dtype=torch.bool))
)
cleanup_calls = []
monkeypatch.setattr(adapter, "cleanup", lambda *a, **kw: cleanup_calls.append((a, kw)))
@@ -604,3 +647,42 @@ def test_omni_ar_scheduler_finish_requests(mocker: MockerFixture):
OmniARScheduler.finish_requests(sched, ["r1"], RequestStatus.FINISHED_ABORTED)
assert order == ["adapter", "super"]
def test_wire_round_trip_struct_to_dict_contract():
"""Pin the wire contract: encoding ``OmniPayloadStruct`` and decoding it
yields a dict equivalent to ``to_dict(struct)``.
The chunk-adapter sender uses struct attribute access while the receiver
uses dict-key access. This works only because ``OmniMsgpackDecoder`` has
no target type and decodes structs back to plain dicts. If this test
breaks, the receiver's dict access will silently drop fields or KeyError.
"""
from vllm_omni.data_entry_keys import CodesStruct, to_dict
from vllm_omni.distributed.omni_connectors.utils.serialization import (
OmniMsgpackDecoder,
OmniMsgpackEncoder,
)
struct = OmniPayloadStruct(
meta=MetaStruct(
finished=torch.tensor(True, dtype=torch.bool),
left_context_size=12,
),
codes=CodesStruct(audio=torch.tensor([1, 2, 3], dtype=torch.int64)),
)
encoded = OmniMsgpackEncoder().encode(struct)
decoded = OmniMsgpackDecoder().decode(encoded)
assert isinstance(decoded, dict)
assert isinstance(decoded["meta"], dict)
assert isinstance(decoded["meta"]["finished"], torch.Tensor)
assert bool(decoded["meta"]["finished"].item()) is True
assert decoded["meta"]["left_context_size"] == 12
assert torch.equal(decoded["codes"]["audio"], torch.tensor([1, 2, 3], dtype=torch.int64))
expected = to_dict(struct)
assert set(decoded.keys()) == set(expected.keys())
assert set(decoded["meta"].keys()) == set(expected["meta"].keys())
assert set(decoded["codes"].keys()) == set(expected["codes"].keys())
@@ -164,11 +164,12 @@ def test_forward_prefers_token_offset_when_present():
runtime_info = [
{
"speech_token": torch.tensor([[1, 2, 3]], dtype=torch.long),
"speech_feat": torch.tensor([[[0.1, 0.2], [0.3, 0.4]]], dtype=torch.float32),
"embedding": torch.tensor([[0.5, 0.6]], dtype=torch.float32),
"token_offset": 2,
"left_context_size": 1,
"embed": {
"speech_token": torch.tensor([[1, 2, 3]], dtype=torch.long),
"speech_feat": torch.tensor([[[0.1, 0.2], [0.3, 0.4]]], dtype=torch.float32),
"embedding": torch.tensor([[0.5, 0.6]], dtype=torch.float32),
},
"meta": {"left_context_size": 2},
}
]
@@ -193,10 +194,12 @@ def test_forward_falls_back_to_left_context_size_for_backward_compat():
runtime_info = [
{
"speech_token": torch.tensor([[1, 2, 3]], dtype=torch.long),
"speech_feat": torch.tensor([[[0.1, 0.2], [0.3, 0.4]]], dtype=torch.float32),
"embedding": torch.tensor([[0.5, 0.6]], dtype=torch.float32),
"left_context_size": 2,
"embed": {
"speech_token": torch.tensor([[1, 2, 3]], dtype=torch.long),
"speech_feat": torch.tensor([[[0.1, 0.2], [0.3, 0.4]]], dtype=torch.float32),
"embedding": torch.tensor([[0.5, 0.6]], dtype=torch.float32),
},
"meta": {"left_context_size": 2},
}
]
@@ -214,10 +217,12 @@ def test_forward_ignores_single_request_padded_tail_tokens():
model = _make_code2wav_model(with_stride_cfg=True)
runtime_info = [
{
"speech_token": torch.tensor([[1, 2, 3]], dtype=torch.long),
"speech_feat": torch.tensor([[[0.1, 0.2], [0.3, 0.4]]], dtype=torch.float32),
"embedding": torch.tensor([[0.5, 0.6]], dtype=torch.float32),
"token_offset": 0,
"embed": {
"speech_token": torch.tensor([[1, 2, 3]], dtype=torch.long),
"speech_feat": torch.tensor([[[0.1, 0.2], [0.3, 0.4]]], dtype=torch.float32),
"embedding": torch.tensor([[0.5, 0.6]], dtype=torch.float32),
},
"meta": {"left_context_size": 0},
}
]
@@ -238,10 +243,12 @@ def test_forward_uses_non_stream_decode_without_chunk_metadata():
runtime_info = [
{
"speech_token": torch.tensor([[1, 2, 3]], dtype=torch.long),
"speech_feat": torch.tensor([[[0.1, 0.2], [0.3, 0.4]]], dtype=torch.float32),
"embedding": torch.tensor([[0.5, 0.6]], dtype=torch.float32),
"prefix_ids": [101, 102],
"embed": {
"speech_token": torch.tensor([[1, 2, 3]], dtype=torch.long),
"speech_feat": torch.tensor([[[0.1, 0.2], [0.3, 0.4]]], dtype=torch.float32),
"embedding": torch.tensor([[0.5, 0.6]], dtype=torch.float32),
},
"ids": {"prompt": [101, 102]},
"generated_len": 3,
}
]
@@ -275,12 +282,16 @@ def test_forward_reuses_streaming_cache_state_between_chunks():
)
runtime_info = [
{
"req_id": ["rid-stream"],
"speech_token": torch.tensor([[1, 2, 3]], dtype=torch.long),
"speech_feat": torch.tensor([[[0.1, 0.2], [0.3, 0.4]]], dtype=torch.float32),
"embedding": torch.tensor([[0.5, 0.6]], dtype=torch.float32),
"token_offset": 0,
"stream_finished": torch.tensor(False),
"embed": {
"speech_token": torch.tensor([[1, 2, 3]], dtype=torch.long),
"speech_feat": torch.tensor([[[0.1, 0.2], [0.3, 0.4]]], dtype=torch.float32),
"embedding": torch.tensor([[0.5, 0.6]], dtype=torch.float32),
},
"meta": {
"req_id": ["rid-stream"],
"stream_finished": torch.tensor(False),
"left_context_size": 0,
},
}
]
@@ -321,12 +332,16 @@ def test_forward_clears_streaming_cache_on_terminal_chunk():
)
runtime_info = [
{
"req_id": ["rid-stream"],
"speech_token": torch.tensor([[1, 2, 3]], dtype=torch.long),
"speech_feat": torch.tensor([[[0.1, 0.2], [0.3, 0.4]]], dtype=torch.float32),
"embedding": torch.tensor([[0.5, 0.6]], dtype=torch.float32),
"token_offset": 0,
"stream_finished": torch.tensor(False),
"embed": {
"speech_token": torch.tensor([[1, 2, 3]], dtype=torch.long),
"speech_feat": torch.tensor([[[0.1, 0.2], [0.3, 0.4]]], dtype=torch.float32),
"embedding": torch.tensor([[0.5, 0.6]], dtype=torch.float32),
},
"meta": {
"req_id": ["rid-stream"],
"stream_finished": torch.tensor(False),
"left_context_size": 0,
},
}
]
@@ -338,7 +353,7 @@ def test_forward_clears_streaming_cache_on_terminal_chunk():
)
assert "rid-stream" in model._stream_vocoder_cache_by_req
runtime_info[0]["stream_finished"] = torch.tensor(True)
runtime_info[0]["meta"]["stream_finished"] = torch.tensor(True)
out = model.forward(
input_ids=torch.tensor([0, 1, 2], dtype=torch.long),
positions=torch.tensor([0, 1, 2], dtype=torch.long),
@@ -64,9 +64,11 @@ def test_talker2code2wav_async_chunk_final_payload_uses_absolute_token_offset():
external_req_id="rid-0",
output_token_ids=[1, 2, 6562, 3],
additional_information={
"speech_token": [torch.tensor([[11, 12, 13]])],
"speech_feat": [torch.tensor([[[0.1, 0.2], [0.3, 0.4]]])],
"embedding": [torch.tensor([[0.5, 0.6]])],
"embed": {
"speech_token": [torch.tensor([[11, 12, 13]])],
"speech_feat": [torch.tensor([[[0.1, 0.2], [0.3, 0.4]]])],
"embedding": [torch.tensor([[0.5, 0.6]])],
},
},
is_finished=lambda: True,
)
@@ -79,15 +81,14 @@ def test_talker2code2wav_async_chunk_final_payload_uses_absolute_token_offset():
)
assert payload is not None
assert payload["meta"]["finished"].item() is True
assert payload["codes"]["audio"] == [1, 2, 3]
assert payload["token_offset"] == 0
assert payload["left_context_size"] == 0
assert payload["req_id"] == ["rid-0"]
assert payload["stream_finished"].item() is True
assert "speech_token" in payload
assert "speech_feat" in payload
assert "embedding" in payload
assert payload.meta.finished.item() is True
assert payload.codes.audio.tolist() == [1, 2, 3]
assert payload.meta.left_context_size == 0
assert payload.meta.req_id == ["rid-0"]
assert payload.meta.stream_finished.item() is True
assert payload.embed.speech_token is not None
assert payload.embed.speech_feat is not None
assert payload.embed.embedding is not None
def test_talker2code2wav_async_chunk_emits_eof_when_finished_without_valid_codes():
@@ -107,8 +108,8 @@ def test_talker2code2wav_async_chunk_emits_eof_when_finished_without_valid_codes
)
assert payload is not None
assert payload["codes"]["audio"] == []
assert payload["meta"]["finished"].item() is True
assert payload.codes.audio.tolist() == []
assert payload.meta.finished.item() is True
def test_talker2code2wav_async_chunk_does_not_reemit_without_new_tokens():
@@ -134,8 +135,8 @@ def test_talker2code2wav_async_chunk_does_not_reemit_without_new_tokens():
)
assert payload1 is not None
assert payload1["codes"]["audio"] == [1, 2]
assert payload1["token_offset"] == 0
assert payload1.codes.audio.tolist() == [1, 2]
assert payload1.meta.left_context_size == 0
assert payload2 is None
@@ -164,9 +165,9 @@ def test_talker2code2wav_async_chunk_waits_for_prelookahead_and_emits_cumulative
assert payload_pending is None
assert payload_ready is not None
assert payload_ready["codes"]["audio"] == [1, 2, 3]
assert payload_ready["token_offset"] == 0
assert payload_ready["meta"]["finished"].item() is False
assert payload_ready.codes.audio.tolist() == [1, 2, 3]
assert payload_ready.meta.left_context_size == 0
assert payload_ready.meta.finished.item() is False
def test_talker2code2wav_async_chunk_final_flush_uses_previous_token_offset():
@@ -193,13 +194,13 @@ def test_talker2code2wav_async_chunk_final_flush_uses_previous_token_offset():
)
assert payload_stream is not None
assert payload_stream["meta"]["finished"].item() is False
assert payload_stream["codes"]["audio"] == [3, 4, 5]
assert payload_stream["token_offset"] == 0
assert payload_stream.meta.finished.item() is False
assert payload_stream.codes.audio.tolist() == [3, 4, 5]
assert payload_stream.meta.left_context_size == 0
assert payload_final is not None
assert payload_final["meta"]["finished"].item() is True
assert payload_final["codes"]["audio"] == [3, 4, 5, 6]
assert payload_final["token_offset"] == 2
assert payload_final.meta.finished.item() is True
assert payload_final.codes.audio.tolist() == [3, 4, 5, 6]
assert payload_final.meta.left_context_size == 2
def test_talker2code2wav_async_chunk_respects_prompt_token_pad_on_first_chunk():
@@ -208,7 +209,7 @@ def test_talker2code2wav_async_chunk_respects_prompt_token_pad_on_first_chunk():
external_req_id="rid-pad",
output_token_ids=[8, 9, 10],
additional_information={
"speech_token": [torch.tensor([[1, 2, 3]])],
"embed": {"speech_token": [torch.tensor([[1, 2, 3]])]},
},
is_finished=lambda: False,
)
@@ -229,8 +230,8 @@ def test_talker2code2wav_async_chunk_respects_prompt_token_pad_on_first_chunk():
assert payload_pending is None
assert payload_ready is not None
assert payload_ready["codes"]["audio"] == [8, 9, 10, 11]
assert payload_ready["token_offset"] == 0
assert payload_ready.codes.audio.tolist() == [8, 9, 10, 11]
assert payload_ready.meta.left_context_size == 0
def test_talker2code2wav_async_chunk_emits_terminal_eof_without_duplicate_audio():
@@ -256,8 +257,8 @@ def test_talker2code2wav_async_chunk_emits_terminal_eof_without_duplicate_audio(
)
assert payload_stream is not None
assert payload_stream["meta"]["finished"].item() is False
assert payload_stream["codes"]["audio"] == [3, 4]
assert payload_stream.meta.finished.item() is False
assert payload_stream.codes.audio.tolist() == [3, 4]
assert payload_final is not None
assert payload_final["meta"]["finished"].item() is True
assert payload_final["codes"]["audio"] == []
assert payload_final.meta.finished.item() is True
assert payload_final.codes.audio.tolist() == []
@@ -20,16 +20,16 @@ def test_flush_remaining_codes_when_no_codes_accumulated_missing_request_id():
"""No entry for request_id: treat as empty, return finished sentinel with empty codes."""
tm = SimpleNamespace(code_prompt_token_ids={})
out = _flush_remaining_codes(tm, "missing", chunk_size=3, left_context_size=3)
assert out["codes"]["audio"] == _sentinel()["codes"]["audio"]
assert out["meta"]["finished"].item() is True
assert out.codes.audio.tolist() == _sentinel()["codes"]["audio"]
assert out.meta.finished.item() is True
def test_flush_remaining_codes_when_no_codes_accumulated_empty_list():
"""Explicit empty accumulation list returns the same sentinel."""
tm = SimpleNamespace(code_prompt_token_ids={"r": []})
out = _flush_remaining_codes(tm, "r", chunk_size=3, left_context_size=3)
assert out["codes"]["audio"] == []
assert out["meta"]["finished"].item() is True
assert out.codes.audio.tolist() == []
assert out.meta.finished.item() is True
def test_flush_remaining_codes_partial_chunk_remaining():
@@ -41,8 +41,8 @@ def test_flush_remaining_codes_partial_chunk_remaining():
code_prompt_token_ids={"r": [[1], [2], [3], [4], [5], [6], [7]]},
)
out = _flush_remaining_codes(tm, "r", chunk_size=3, left_context_size=3)
assert out["meta"]["finished"].item() is True
assert out["codes"]["audio"] == [4, 5, 6, 7]
assert out.meta.finished.item() is True
assert out.codes.audio.tolist() == [4, 5, 6, 7]
def test_flush_remaining_codes_when_length_is_exact_multiple_of_chunk_size():
@@ -52,7 +52,7 @@ def test_flush_remaining_codes_when_length_is_exact_multiple_of_chunk_size():
)
out = _flush_remaining_codes(tm, "r", chunk_size=3, left_context_size=3)
# context_length = chunk_size = 3, end_index = min(6, 6) -> all 6
assert out["codes"]["audio"] == [1, 2, 3, 4, 5, 6]
assert out.codes.audio.tolist() == [1, 2, 3, 4, 5, 6]
@pytest.mark.parametrize(
@@ -74,5 +74,5 @@ def test_flush_remaining_codes_context_window_end_index(
tm = SimpleNamespace(code_prompt_token_ids={"r": accumulated})
out = _flush_remaining_codes(tm, "r", chunk_size=chunk_size, left_context_size=left_context)
expected_flat = list(range(length - expected_end_index, length))
assert out["codes"]["audio"] == expected_flat
assert out["meta"]["finished"].item() is True
assert out.codes.audio.tolist() == expected_flat
assert out.meta.finished.item() is True
@@ -80,8 +80,8 @@ def test_eof_marker_when_finished_empty():
request=_req("r", finished=True),
is_finished=True,
)
assert p["codes"] == {"audio": []}
assert p["meta"]["finished"].item() is True
assert p.codes.audio.tolist() == []
assert p.meta.finished.item() is True
def test_flush_on_finish():
@@ -94,8 +94,8 @@ def test_flush_on_finish():
is_finished=True,
)
assert p is not None
assert p["meta"]["finished"].item() is True
assert len(p["codes"]["audio"]) == _Q * 24
assert p.meta.finished.item() is True
assert len(p.codes.audio) == _Q * 24
_CASES = [
@@ -149,8 +149,8 @@ def test_streaming_phases(config, n_frames, finished, expected):
else:
exp_ctx, exp_window = expected
assert payload is not None
assert payload["meta"]["left_context_size"] == exp_ctx
assert len(payload["codes"]["audio"]) == _Q * exp_window
assert payload.meta.left_context_size == exp_ctx
assert len(payload.codes.audio) == _Q * exp_window
def test_dynamic_ic_adapts_to_load():
@@ -160,14 +160,14 @@ def test_dynamic_ic_adapts_to_load():
# Low load (1/8) -> IC=2 -> emit at 2
p1 = _call(tm, "r", n_frames=2)
assert p1 is not None
assert len(p1["codes"]["audio"]) == _Q * 2
assert len(p1.codes.audio) == _Q * 2
# High load on a new request: active=6/8 -> IC=8 -> emit at 8
for i in range(4):
tm.code_prompt_token_ids[f"other-{i}"] = [[0]]
p2 = _call(tm, "new-high-load", n_frames=8)
assert p2 is not None
assert len(p2["codes"]["audio"]) == _Q * 8
assert len(p2.codes.audio) == _Q * 8
# Requests past initial phase still count in load factor
tm2 = _tm(max_num_seqs=4)
@@ -176,7 +176,7 @@ def test_dynamic_ic_adapts_to_load():
# active=4/4=1.0 -> IC=16
p3 = _call(tm2, "new", n_frames=16)
assert p3 is not None
assert len(p3["codes"]["audio"]) == _Q * 16
assert len(p3.codes.audio) == _Q * 16
def test_ic_load_change_mid_request():
@@ -195,7 +195,7 @@ def test_ic_load_change_mid_request():
assert _call(tm, "r", n_frames=25) is None
p3 = _call(tm, "r", n_frames=27)
assert p3 is not None
assert p3["meta"]["left_context_size"] == 2
assert p3.meta.left_context_size == 2
# A *new* request under high load gets IC=16 (not IC=2).
# Frame 2 would emit under IC=2 but must hold under IC=16.
@@ -214,13 +214,13 @@ def test_connector_initial_chunk_config_overrides_dynamic_ic():
p1 = _call(tm, "r", n_frames=4)
assert p1 is not None
assert len(p1["codes"]["audio"]) == _Q * 4
assert len(p1.codes.audio) == _Q * 4
# Only the first chunk uses the small size; the next emit is 4+25.
assert _call(tm, "r", n_frames=25) is None
p2 = _call(tm, "r", n_frames=29)
assert p2 is not None
assert p2["meta"]["left_context_size"] == 4
assert p2.meta.left_context_size == 4
@pytest.mark.parametrize(
@@ -268,8 +268,8 @@ def test_first_streaming_chunk_prepends_ref_code_context():
)
assert payload is not None
assert payload["meta"]["left_context_size"] == 2
assert len(payload["codes"]["audio"]) == _Q * 12
assert payload.meta.left_context_size == 2
assert len(payload.codes.audio) == _Q * 12
def test_ref_code_context_applies_to_all_streaming_chunks():
@@ -290,8 +290,8 @@ def test_ref_code_context_applies_to_all_streaming_chunks():
assert payload is not None
# ref_code (2 frames) prepended as left context on second chunk too
assert payload["meta"]["left_context_size"] == 10 + 2
assert len(payload["codes"]["audio"]) == _Q * (35 + 2)
assert payload.meta.left_context_size == 10 + 2
assert len(payload.codes.audio) == _Q * (35 + 2)
def test_ref_code_context_can_be_buffered_before_first_emit():
@@ -325,8 +325,8 @@ def test_ref_code_context_can_be_buffered_before_first_emit():
assert payload is not None
# ref_code (2 frames) is kept (not popped) for subsequent chunks
assert payload["meta"]["left_context_size"] == 2
assert len(payload["codes"]["audio"]) == _Q * 12
assert payload.meta.left_context_size == 2
assert len(payload.codes.audio) == _Q * 12
assert rid in tm.request_payload
@@ -353,7 +353,7 @@ def test_non_async_processor_prepends_ref_code_and_sets_trim_context():
assert len(prompts) == 1
prompt = prompts[0]
assert prompt["additional_information"] == {"meta": {"left_context_size": [2]}}
assert prompt["additional_information"] == {"meta": {"left_context_size": 2}}
assert prompt["prompt_token_ids"] == [
9,
8,
@@ -401,4 +401,4 @@ def test_non_async_processor_filters_out_of_range_codec_values():
prompt = prompts[0]
# Only ref_code (1 frame) + 2 valid frames = 3 frames * 4 quantizers = 12 codes
assert len(prompt["prompt_token_ids"]) == 4 * 3
assert prompt["additional_information"] == {"meta": {"left_context_size": [1]}}
assert prompt["additional_information"] == {"meta": {"left_context_size": 1}}
@@ -57,8 +57,8 @@ def test_latent2vae_async_chunk_serializes_latent_payload():
)
assert payload is not None
assert torch.equal(payload["finished"], torch.tensor(False, dtype=torch.bool))
recovered = _decode_serialized_latent(payload["code_predictor_codes"])
assert torch.equal(payload.meta.finished, torch.tensor(False, dtype=torch.bool))
recovered = _decode_serialized_latent(payload.codes.audio.tolist())
torch.testing.assert_close(recovered, latent.to(torch.bfloat16).to(torch.float32).unsqueeze(0))
@@ -70,10 +70,8 @@ def test_latent2vae_async_chunk_returns_terminal_marker_without_latent():
is_finished=False,
)
assert payload == {
"code_predictor_codes": [],
"finished": torch.tensor(True, dtype=torch.bool),
}
assert payload.codes.audio.tolist() == []
assert torch.equal(payload.meta.finished, torch.tensor(True, dtype=torch.bool))
def test_latent2vae_async_chunk_returns_none_for_nonterminal_empty_chunk():
@@ -114,8 +114,8 @@ def test_flush_tail_when_finished():
)
assert payload is not None
assert payload["meta"]["finished"].item() is True
codes = payload["codes"]["audio"]
assert payload.meta.finished.item() is True
codes = payload.codes.audio
# Format: [ctx_frames, context_length, ...flat_codes]
assert len(codes) >= 2 # At least ctx_frames + context_length header
ctx_frames = codes[0]
@@ -137,8 +137,8 @@ def test_eof_marker_when_finished_with_no_frames():
request=request,
)
assert payload["codes"] == {"audio": []}
assert payload["meta"]["finished"].item() is True
assert payload.codes.audio.tolist() == []
assert payload.meta.finished.item() is True
def test_normal_chunk_emission():
@@ -162,7 +162,7 @@ def test_normal_chunk_emission():
# A chunk should be emitted
assert payload is not None
codes = payload["codes"]["audio"]
codes = payload.codes.audio
ctx_frames = codes[0]
context_length = codes[1]
assert ctx_frames == 20 # 25 - 5(chunk_size_at_begin)
@@ -189,7 +189,7 @@ def test_small_initial_chunks():
)
assert payload is not None
codes = payload["codes"]["audio"]
codes = payload.codes.audio
ctx_frames = codes[0]
context_length = codes[1]
assert ctx_frames == 0
@@ -238,19 +238,16 @@ def test_context_handling_format():
)
assert payload is not None
codes = payload["codes"]["audio"]
# First two elements are ctx_frames and context_length
ctx_frames = codes[0]
context_length = codes[1]
codes = payload.codes.audio
# codes is a 1-D long tensor: [ctx_frames, context_length, ...flat_codes]
ctx_frames = int(codes[0].item())
context_length = int(codes[1].item())
flat_codes = codes[2:]
# The window has ctx_frames + context_length frames
assert isinstance(ctx_frames, int)
assert isinstance(context_length, int)
assert ctx_frames >= 0
assert context_length > 0
# flat_codes = total_window_frames * codebook_dim
total_window_frames = ctx_frames + context_length
assert len(flat_codes) == total_window_frames * 2 # codebook_dim=2
assert flat_codes.numel() == total_window_frames * 2 # codebook_dim=2
def test_none_pooling_output_not_finished_returns_none():
+156 -28
View File
@@ -1,41 +1,175 @@
"""Tests for data_entry_keys: TypedDict payload structure, flatten/unflatten, serialize/deserialize."""
"""Tests for data_entry_keys."""
import msgspec
import pytest
import torch
from vllm_omni.data_entry_keys import (
CodesStruct,
EmbeddingsStruct,
HiddenStatesStruct,
IdsStruct,
MetaStruct,
OmniPayload,
OmniPayloadStruct,
deserialize_payload,
flatten_payload,
serialize_payload,
to_dict,
to_struct,
unflatten_payload,
)
from vllm_omni.engine import AdditionalInformationPayload
class TestOmniPayload:
def test_nested_payload_structure(self):
"""Verify OmniPayload can be constructed with nested dicts."""
payload: OmniPayload = {
"hidden_states": {"output": torch.tensor([1.0])},
"embed": {"prefill": torch.tensor([2.0])},
"codes": {"audio": torch.tensor([3.0])},
"ids": {"all": [1, 2, 3]},
"meta": {"finished": torch.tensor(True, dtype=torch.bool)},
class TestOmniPayloadStruct:
"""Runtime-validated mirror of OmniPayload (msgspec.Struct)."""
def test_to_struct_validates_dict(self):
d = {"meta": {"left_context_size": 25, "finished": torch.tensor(False)}}
s = to_struct(d)
assert s.meta.left_context_size == 25
def test_to_struct_rejects_legacy_flat_top_level(self):
with pytest.raises(msgspec.ValidationError, match="unknown field"):
to_struct({"code_predictor_codes": torch.zeros(3, 8)})
def test_to_struct_rejects_legacy_flat_meta_field(self):
# `left_context_size` at top level (legacy) instead of under `meta`
with pytest.raises(msgspec.ValidationError, match="unknown field"):
to_struct({"left_context_size": 25})
def test_to_struct_rejects_typo_in_subkey(self):
with pytest.raises(msgspec.ValidationError, match="unknown field"):
to_struct({"meta": {"finisheed": True}})
def test_to_struct_rejects_wrong_type(self):
with pytest.raises(msgspec.ValidationError, match="Expected"):
to_struct({"meta": {"left_context_size": "not_an_int"}})
def test_round_trip_dict_struct_dict(self):
original = {
"meta": {"left_context_size": 7, "finished": torch.tensor(True)},
"codes": {"audio": torch.zeros(2, 8)},
"hidden_states": {"output": torch.zeros(4, 16)},
}
assert torch.equal(payload["hidden_states"]["output"], torch.tensor([1.0]))
assert torch.equal(payload["embed"]["prefill"], torch.tensor([2.0]))
assert torch.equal(payload["codes"]["audio"], torch.tensor([3.0]))
assert payload["ids"]["all"] == [1, 2, 3]
assert payload["meta"]["finished"].item() is True
s = to_struct(original)
d = to_dict(s)
assert sorted(d.keys()) == sorted(original.keys())
for top, sub in original.items():
assert sorted(d[top].keys()) == sorted(sub.keys())
def test_partial_payload(self):
"""OmniPayload fields are all optional (total=False)."""
payload: OmniPayload = {"meta": {"finished": torch.tensor(False, dtype=torch.bool)}}
assert payload["meta"]["finished"].item() is False
def test_to_dict_drops_unset_fields(self):
s = OmniPayloadStruct(meta=MetaStruct(left_context_size=10))
d = to_dict(s)
assert d == {"meta": {"left_context_size": 10}}
def test_empty_payload(self):
payload: OmniPayload = {}
assert len(payload) == 0
def test_struct_with_all_categories(self):
d = {
"hidden_states": {"output": torch.zeros(1)},
"embed": {"prefill": torch.zeros(1), "tts_bos": torch.zeros(1)},
"ids": {"all": [1, 2], "prompt": [1]},
"codes": {"audio": torch.zeros(1)},
"meta": {"left_context_size": 3, "num_processed_tokens": 7},
}
s = to_struct(d)
assert isinstance(s.hidden_states, HiddenStatesStruct)
assert isinstance(s.embed, EmbeddingsStruct)
assert isinstance(s.ids, IdsStruct)
assert isinstance(s.codes, CodesStruct)
assert isinstance(s.meta, MetaStruct)
assert s.ids.all == [1, 2]
assert s.meta.num_processed_tokens == 7
class TestValidatePayload:
def test_raises_on_unknown_top_level(self):
from vllm_omni.data_entry_keys import validate_payload
with pytest.raises(msgspec.ValidationError, match="unknown field"):
validate_payload({"code_predictor_codes": torch.zeros(3, 8)}, context="test_boundary")
def test_raises_on_unknown_sub_key(self):
from vllm_omni.data_entry_keys import validate_payload
with pytest.raises(msgspec.ValidationError, match="unknown field"):
validate_payload({"meta": {"finisheed": True}})
def test_none_is_ok(self):
from vllm_omni.data_entry_keys import validate_payload
validate_payload(None) # should not raise
def test_valid_payload_passes(self):
from vllm_omni.data_entry_keys import validate_payload
validate_payload({"meta": {"left_context_size": 5}})
def test_context_in_error_message(self):
from vllm_omni.data_entry_keys import validate_payload
with pytest.raises(msgspec.ValidationError, match="my_call_site"):
validate_payload({"bad": 1}, context="my_call_site")
class TestWireEquivalenceStructVsDict:
"""Producer return-side migration invariant: encoding an ``OmniPayloadStruct``
via the connector serializer must decode to the same payload as encoding
the equivalent ``to_dict(struct)``.
Guards against regressions where the wire format diverges between the two
paths (e.g. msgspec adds a struct tag, or ``to_dict`` drops a non-default
sub-field that ``omit_defaults`` retains).
"""
@staticmethod
def _round_trip(obj):
from vllm_omni.distributed.omni_connectors.utils.serialization import (
OmniMsgpackDecoder,
OmniMsgpackEncoder,
)
return OmniMsgpackDecoder().decode(OmniMsgpackEncoder().encode(obj))
@staticmethod
def _assert_decoded_equal(a, b):
if isinstance(a, dict):
assert isinstance(b, dict)
assert sorted(a.keys()) == sorted(b.keys())
for k in a:
TestWireEquivalenceStructVsDict._assert_decoded_equal(a[k], b[k])
elif isinstance(a, torch.Tensor):
assert isinstance(b, torch.Tensor)
assert a.dtype == b.dtype
assert a.shape == b.shape
assert torch.equal(a, b)
elif isinstance(a, list):
assert isinstance(b, list) and len(a) == len(b)
for x, y in zip(a, b, strict=True):
TestWireEquivalenceStructVsDict._assert_decoded_equal(x, y)
else:
assert a == b
def test_basic_payload(self):
struct = OmniPayloadStruct(
codes=CodesStruct(audio=torch.tensor([1, 2, 3], dtype=torch.long)),
meta=MetaStruct(left_context_size=10, finished=torch.tensor(True, dtype=torch.bool)),
)
self._assert_decoded_equal(self._round_trip(struct), self._round_trip(to_dict(struct)))
def test_nested_sub_structs(self):
# Exercises depth-2 sub-struct encoding (embed.*) which was the case that
# exposed schema drift in the #1829 migration.
struct = OmniPayloadStruct(
codes=CodesStruct(audio=torch.tensor([5, 6], dtype=torch.long)),
meta=MetaStruct(finished=torch.tensor(False, dtype=torch.bool)),
embed=EmbeddingsStruct(
speech_token=torch.tensor([[1, 2, 3]]),
speech_feat=torch.tensor([[[0.1, 0.2], [0.3, 0.4]]]),
embedding=torch.tensor([[0.5, 0.6]]),
),
)
self._assert_decoded_equal(self._round_trip(struct), self._round_trip(to_dict(struct)))
class TestFlattenPayload:
@@ -76,9 +210,6 @@ class TestFlattenPayload:
assert torch.equal(flat["hidden_states.layer_24"], torch.tensor([3.0]))
assert "hidden_states.layers" not in flat
def test_empty_payload(self):
assert flatten_payload({}) == {}
def test_mixed_nested_and_top_level(self):
nested: OmniPayload = {
"codes": {"audio": torch.tensor([1.0])},
@@ -118,9 +249,6 @@ class TestUnflattenPayload:
assert torch.equal(nested["hidden_states"]["layers"][0], torch.tensor([2.0]))
assert torch.equal(nested["hidden_states"]["layers"][24], torch.tensor([3.0]))
def test_empty_payload(self):
assert unflatten_payload({}) == {}
class TestFlattenUnflattenRoundTrip:
def test_round_trip_simple(self):
+22 -20
View File
@@ -1044,10 +1044,12 @@ class TestAsyncPayloadLifecycle(unittest.TestCase):
)
host._request_ids_mapping["r1"] = "r1"
payload = {
"thinker_decode_embeddings": torch.ones(1, 2),
"thinker_output_token_ids": [1],
"override_keys": ["thinker_decode_embeddings", "thinker_output_token_ids"],
"finished": torch.tensor(False),
"embed": {"decode": torch.ones(1, 2)},
"ids": {"output": [1]},
"meta": {
"finished": torch.tensor(False),
"override_keys": [["embed", "decode"], ["ids", "output"]],
},
}
host._accumulate_payload("r1", dict(payload))
@@ -1065,15 +1067,16 @@ class TestAsyncPayloadLifecycle(unittest.TestCase):
model_config=_make_model_config(stage_id=1, async_chunk=True, worker_type="ar"),
)
payload = {
"thinker_output_token_ids": [1, 2, 3],
"finished": torch.tensor(False),
"override_keys": [
"thinker_output_token_ids",
"thinker_decode_embeddings_token_start",
"thinker_decode_embeddings_token_end",
],
"thinker_decode_embeddings_token_start": 2,
"thinker_decode_embeddings_token_end": 3,
"ids": {"output": [1, 2, 3]},
"embed": {"decode_token_start": 2, "decode_token_end": 3},
"meta": {
"finished": torch.tensor(False),
"override_keys": [
["ids", "output"],
["embed", "decode_token_start"],
["embed", "decode_token_end"],
],
},
}
self.assertFalse(host._payload_is_consumable(payload))
host.shutdown_omni_connectors()
@@ -1085,9 +1088,9 @@ class TestAsyncPayloadLifecycle(unittest.TestCase):
model_config=_make_model_config(stage_id=1, async_chunk=True, worker_type="ar"),
)
payload = {
"thinker_output_token_ids": [1, 2, 3],
"thinker_decode_embeddings": torch.ones(1, 2),
"finished": torch.tensor(False),
"ids": {"output": [1, 2, 3]},
"embed": {"decode": torch.ones(1, 2)},
"meta": {"finished": torch.tensor(False)},
}
self.assertTrue(host._payload_is_consumable(payload))
host.shutdown_omni_connectors()
@@ -1108,15 +1111,14 @@ class TestAsyncPayloadLifecycle(unittest.TestCase):
host._omni_connector.get.side_effect = [
(
{
"thinker_decode_embeddings": torch.ones(1, 2),
"finished": torch.tensor(False),
"embed": {"decode": torch.ones(1, 2)},
"meta": {"finished": torch.tensor(False)},
},
1,
),
(
{
"next_stage_prompt_len": 7,
"finished": torch.tensor(False),
"meta": {"next_stage_prompt_len": 7, "finished": torch.tensor(False)},
},
1,
),
+180 -68
View File
@@ -1,41 +1,26 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Structured payload types for inter-stage communication.
Adding a new model?
~~~~~~~~~~~~~~~~~~~
Every key you put into the inter-stage payload (``additional_information``,
``multimodal_output``, ``pooling_output``) **must** use the nested
``OmniPayload`` TypedDict structure. For each category, every known
qualifier is an explicit field so misspellings are caught statically.
Categories
Categories under ``OmniPayload``:
hidden_states – intermediate / output hidden-state tensors
embed – embedding tensors (prefill, decode, special tokens)
ids – token-ID sequences
codes – codec / audio code tensors
meta – scalar metadata, control flags, shapes
This module provides:
- Structured ``TypedDict`` types for static type checking (``OmniPayload``)
- ``serialize_payload`` / ``deserialize_payload`` for transport across
process boundaries via ``AdditionalInformationPayload``
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, TypedDict
import msgspec
import numpy as np
import torch
if TYPE_CHECKING:
from vllm_omni.engine import AdditionalInformationEntry, AdditionalInformationPayload
# ── Structured payload types ──
# These are TypedDicts (plain dicts at runtime, zero overhead) that give
# static type checking and IDE autocomplete for inter-stage payloads.
# Every field is optional (total=False) because each stage only populates
# the subset it needs.
class HiddenStates(TypedDict, total=False):
output: torch.Tensor
@@ -54,6 +39,8 @@ class Embeddings(TypedDict, total=False):
tts_pad_projected: torch.Tensor
voice: torch.Tensor
speech_feat: torch.Tensor
speech_token: torch.Tensor
embedding: torch.Tensor
thinker_reply: torch.Tensor
@@ -72,6 +59,8 @@ class Ids(TypedDict, total=False):
class OmniPayloadMeta(TypedDict, total=False):
finished: torch.Tensor
stream_finished: torch.Tensor
req_id: list[str]
left_context_size: int
override_keys: list[tuple[str, str]]
num_processed_tokens: int
@@ -105,45 +94,187 @@ class OmniPayload(TypedDict, total=False):
request_id: str
# ── Keys whose values are nested dicts (TypedDict sub-categories) ──
_NESTED_KEYS = frozenset({"hidden_states", "embed", "ids", "codes", "meta"})
# ── msgspec.Struct mirror of the TypedDicts (runtime-validated) ──
# Sub-TypedDict for each nested category, used by runtime validation.
_NESTED_SCHEMAS: dict[str, type] = {
"hidden_states": HiddenStates,
"embed": Embeddings,
"ids": Ids,
"codes": Codes,
"meta": OmniPayloadMeta,
class _StructBase(msgspec.Struct, omit_defaults=True, kw_only=True, forbid_unknown_fields=True):
pass
class HiddenStatesStruct(_StructBase):
output: torch.Tensor | None = None
output_shape: list[int] | None = None
trailing_text: torch.Tensor | None = None
last: torch.Tensor | None = None
layers: dict[int, torch.Tensor] | None = None
class EmbeddingsStruct(_StructBase):
prefill: torch.Tensor | None = None
prefill_shape: list[int] | None = None
decode: torch.Tensor | None = None
decode_token_start: int | None = None
decode_token_end: int | None = None
cached_decode: torch.Tensor | None = None
tts_bos: torch.Tensor | None = None
tts_eos: torch.Tensor | None = None
tts_pad: torch.Tensor | None = None
tts_pad_projected: torch.Tensor | None = None
voice: torch.Tensor | None = None
speech_feat: torch.Tensor | None = None
speech_token: torch.Tensor | None = None
embedding: torch.Tensor | None = None
thinker_reply: torch.Tensor | None = None
class CodesStruct(_StructBase):
audio: torch.Tensor | None = None
ref: torch.Tensor | None = None
class IdsStruct(_StructBase):
all: list[int] | None = None
prompt: list[int] | None = None
output: list[int] | None = None
speech_token: list[int] | None = None
prior_image: list[int] | None = None
class MetaStruct(_StructBase):
finished: torch.Tensor | None = None
stream_finished: torch.Tensor | None = None
req_id: list[str] | None = None
left_context_size: int | None = None
override_keys: list[tuple[str, str]] | None = None
num_processed_tokens: int | None = None
next_stage_prompt_len: int | None = None
ar_width: int | None = None
eol_token_id: int | None = None
visual_token_start_id: int | None = None
visual_token_end_id: int | None = None
gen_token_mask: torch.Tensor | None = None
omni_task: list[str] | None = None
height: int | None = None
width: int | None = None
decode_flag: bool | None = None
codec_streaming: bool | None = None
ref_code_len: int | None = None
talker_prefill_offset: int | None = None
codec_chunk_frames: int | None = None
codec_left_context_frames: int | None = None
code_flat_numel: int | None = None
omni_final_stage_id: int | None = None
class OmniPayloadStruct(_StructBase):
hidden: torch.Tensor | None = None
hidden_states: HiddenStatesStruct | None = None
embed: EmbeddingsStruct | None = None
ids: IdsStruct | None = None
codes: CodesStruct | None = None
meta: MetaStruct | None = None
latent: torch.Tensor | None = None
generated_len: int | None = None
model_outputs: list[torch.Tensor] | None = None
mtp_inputs: tuple[torch.Tensor, torch.Tensor] | None = None
speaker: list[str] | str | None = None
language: list[str] | str | None = None
request_id: str | None = None
past_key_values: list[int] | None = None
kv_metadata: dict[str, Any] | None = None
_NESTED_STRUCTS: dict[str, type[_StructBase]] = {
"hidden_states": HiddenStatesStruct,
"embed": EmbeddingsStruct,
"ids": IdsStruct,
"codes": CodesStruct,
"meta": MetaStruct,
}
_ROOT_KEYS: frozenset[str] = frozenset(OmniPayload.__annotations__.keys())
_TENSOR_MARKER = "__tensor__"
def assert_payload(payload: dict[str, Any], *, context: str = "payload") -> None:
"""Validate ``payload`` matches the ``OmniPayload`` nested schema.
def _msgspec_dec_hook(typ: type, obj: Any) -> Any:
"""Bridge non-msgspec types when decoding bytes/dicts into Structs."""
if typ is torch.Tensor:
if isinstance(obj, torch.Tensor):
return obj
if isinstance(obj, dict) and obj.get(_TENSOR_MARKER):
arr = np.frombuffer(obj["data"], dtype=np.dtype(obj["dtype"]))
arr = arr.reshape(obj["shape"])
return torch.from_numpy(arr.copy())
raise TypeError(f"cannot decode {type(obj).__name__} into torch.Tensor")
raise NotImplementedError(f"no decoder for {typ}")
TypedDict is a static-only contract in Python; this helper closes the
loop at runtime by rejecting:
* non-dict payloads
* top-level keys not declared on ``OmniPayload``
* nested-category values that aren't dicts
* sub-keys not declared on the matching nested TypedDict
Call at producer/consumer boundaries when a schema violation should
crash the pipeline instead of silently degrading audio quality.
def to_struct(payload: dict[str, Any]) -> OmniPayloadStruct:
"""Convert a payload dict into ``OmniPayloadStruct``, validating types.
Raises ``msgspec.ValidationError`` on:
* unknown top-level keys (typos, legacy flat keys)
* unknown sub-keys under any nested category
* type mismatches (e.g., ``meta.left_context_size`` not an ``int``)
"""
assert isinstance(payload, dict), f"{context}: expected dict, got {type(payload).__name__}"
extra_top = set(payload) - _ROOT_KEYS
assert not extra_top, f"{context}: unknown top-level keys {sorted(extra_top)!r}"
for nested_key, schema in _NESTED_SCHEMAS.items():
if nested_key not in payload:
return msgspec.convert(payload, OmniPayloadStruct, dec_hook=_msgspec_dec_hook)
def validate_payload(payload: dict[str, Any] | None, *, context: str = "payload") -> None:
"""Validate a payload matches the ``OmniPayload`` schema, raising on drift.
Wraps :func:`to_struct` and re-raises ``msgspec.ValidationError`` with
the call-site ``context`` prepended. ``None`` is allowed (treated as
"no payload to check").
"""
if payload is None:
return
try:
to_struct(payload)
except msgspec.ValidationError as exc:
raise msgspec.ValidationError(f"{context}: {exc}") from exc
def to_dict(struct: OmniPayloadStruct) -> dict[str, Any]:
"""Convert ``OmniPayloadStruct`` to a plain dict, dropping ``None`` fields."""
out: dict[str, Any] = {}
for field in OmniPayloadStruct.__struct_fields__:
value = getattr(struct, field)
if value is None:
continue
sub = payload[nested_key]
assert isinstance(sub, dict), f"{context}: payload[{nested_key!r}] must be dict, got {type(sub).__name__}"
known_sub = frozenset(schema.__annotations__.keys())
extra_sub = set(sub) - known_sub
assert not extra_sub, f"{context}: payload[{nested_key!r}] unknown sub-keys {sorted(extra_sub)!r}"
if isinstance(value, _StructBase):
sub: dict[str, Any] = {}
for sk in value.__struct_fields__:
sv = getattr(value, sk)
if sv is not None:
sub[sk] = sv
if sub:
out[field] = sub
else:
out[field] = value
return out
_DTYPE_TO_NAME: dict[torch.dtype, str] = {
torch.float32: "float32",
torch.float16: "float16",
torch.bfloat16: "bfloat16",
torch.float64: "float64",
torch.int64: "int64",
torch.int32: "int32",
torch.int16: "int16",
torch.int8: "int8",
torch.uint8: "uint8",
torch.bool: "bool",
}
def _dtype_to_name(dtype: torch.dtype) -> str:
return _DTYPE_TO_NAME.get(dtype, str(dtype).replace("torch.", ""))
# ── Keys whose values are nested dicts (TypedDict sub-categories) ──
_NESTED_KEYS = frozenset({"hidden_states", "embed", "ids", "codes", "meta"})
def flatten_payload(payload: dict[str, Any]) -> dict[str, Any]:
@@ -192,25 +323,6 @@ def unflatten_payload(flat: dict[str, Any]) -> dict[str, Any]:
return result
# ── dtype helpers ──
_DTYPE_TO_NAME: dict[torch.dtype, str] = {
torch.float32: "float32",
torch.float16: "float16",
torch.bfloat16: "bfloat16",
torch.float64: "float64",
torch.int64: "int64",
torch.int32: "int32",
torch.int16: "int16",
torch.int8: "int8",
torch.uint8: "uint8",
torch.bool: "bool",
}
def _dtype_to_name(dtype: torch.dtype) -> str:
return _DTYPE_TO_NAME.get(dtype, str(dtype).replace("torch.", ""))
def _serialize_tensor(t: torch.Tensor) -> AdditionalInformationEntry:
from vllm_omni.engine import AdditionalInformationEntry
@@ -3,12 +3,13 @@
import importlib
from collections import defaultdict, deque
from collections.abc import Callable
from typing import Any
import torch
from vllm.v1.request import Request, RequestStatus
from vllm_omni.data_entry_keys import unflatten_payload
from vllm_omni.data_entry_keys import OmniPayloadStruct, unflatten_payload
from ..factory import OmniConnectorFactory
from ..utils.config import ConnectorSpec
@@ -43,7 +44,7 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
super().__init__(model_config)
self.model_mode = getattr(model_config, "worker_type", None) or "ar"
# State specific to Chunk management
self.custom_process_next_stage_input_func = None
self.custom_process_next_stage_input_func: Callable[..., OmniPayloadStruct | None] | None = None
custom_process_next_stage_input_func = getattr(model_config, "custom_process_next_stage_input_func", None)
if custom_process_next_stage_input_func:
module_path, func_name = custom_process_next_stage_input_func.rsplit(".", 1)
@@ -162,7 +163,11 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
if meta.get("finished"):
self.finished_requests.add(req_id)
new_ids = payload_data.get("codes", {}).get("audio", [])
new_ids = payload_data.get("codes", {}).get("audio")
if isinstance(new_ids, torch.Tensor):
new_ids = new_ids.tolist()
elif new_ids is None:
new_ids = []
request.prompt_token_ids = new_ids
prev_info = getattr(request, "additional_information", None)
info = dict(prev_info) if isinstance(prev_info, dict) else {}
@@ -202,21 +207,29 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
raw_ok = payload_data.get("meta", {}).pop("override_keys", [])
override_keys = {tuple(k) if isinstance(k, list) else k for k in raw_ok}
for type_key, new_val in payload_data.items():
if not isinstance(new_val, dict):
continue
origin_sub = origin.get(type_key)
if not isinstance(origin_sub, dict):
continue
for qual, value in new_val.items():
if type_key == "meta" and qual == "finished":
for key, value in payload_data.items():
if isinstance(value, dict):
origin_sub = origin.get(key)
if not isinstance(origin_sub, dict):
continue
if (type_key, qual) in override_keys:
for qual, qval in value.items():
if key == "meta" and qual == "finished":
continue
if (key, qual) in override_keys:
continue
osv = origin_sub.get(qual)
if isinstance(qval, torch.Tensor) and isinstance(osv, torch.Tensor):
value[qual] = torch.cat([osv, qval], dim=0)
elif isinstance(qval, list) and isinstance(osv, list):
value[qual] = osv + qval
else:
if key in override_keys:
continue
if isinstance(value, torch.Tensor) and qual in origin_sub:
new_val[qual] = torch.cat([origin_sub[qual], value], dim=0)
elif isinstance(value, list) and qual in origin_sub:
new_val[qual] = origin_sub[qual] + value
ov = origin.get(key)
if isinstance(value, torch.Tensor) and isinstance(ov, torch.Tensor):
payload_data[key] = torch.cat([ov, value], dim=0)
elif isinstance(value, list) and isinstance(ov, list):
payload_data[key] = ov + value
self.request_payload[req_id] = payload_data
return payload_data
@@ -232,7 +245,7 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
chunk_id = self.put_req_chunk[external_req_id]
connector_put_key = f"{external_req_id}_{stage_id}_{chunk_id}"
# Process payload in save_loop thread
payload_data = None
payload_data: OmniPayloadStruct | None = None
if self.custom_process_next_stage_input_func:
try:
payload_data = self.custom_process_next_stage_input_func(
@@ -245,7 +258,7 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
except Exception as e:
logger.error(f"Failed to use custom_process_input_func for payload extraction: {e}")
if not payload_data:
if payload_data is None:
return
success, size, metadata = self.connector.put(
@@ -258,7 +271,12 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
if success:
self.put_req_chunk[external_req_id] += 1
logger.debug(f"[Stage-{stage_id}] Sent {connector_put_key}")
finished_flag = payload_data.get("meta", {}).get("finished", payload_data.get("finished"))
# Sender uses struct attr access here; the receive path in
# `_load_one_request` / `_update_request_payload` reads dict keys.
# That asymmetry is intentional: `OmniMsgpackDecoder` is type-erased
# (no target type), so the wire round-trips struct -> dict. If you
# change the schema, update both ends — see test_wire_round_trip.
finished_flag = payload_data.meta.finished if payload_data.meta is not None else None
is_payload_finished = False
if isinstance(finished_flag, torch.Tensor):
is_payload_finished = finished_flag.numel() == 1 and bool(finished_flag.item())
@@ -32,6 +32,7 @@ from vllm.v1.outputs import SamplerOutput
from vllm.v1.sample.metadata import SamplingMetadata
from vllm.v1.sample.sampler import Sampler
from vllm_omni.data_entry_keys import EmbeddingsStruct, OmniPayloadStruct, to_dict, to_struct
from vllm_omni.model_executor.models.cosyvoice3.config import CosyVoice3Config
from vllm_omni.model_executor.models.cosyvoice3.utils import (
concat_text_with_prompt_ids,
@@ -379,43 +380,6 @@ class CosyVoice3Model(
# Use parent's cache config - critical for PagedAttention to work correctly
return parent_config.with_hf_config(qwen_hf_config, architectures=["Qwen2Model"])
@staticmethod
def _as_tensor(value: object) -> torch.Tensor | None:
"""Extract tensor payload from runtime info fields."""
if isinstance(value, list):
if not value:
return None
value = value[0]
if isinstance(value, torch.Tensor):
return value
return None
@staticmethod
def _as_str(value: object) -> str | None:
"""Extract string payload from runtime info fields."""
if isinstance(value, list):
if not value:
return None
value = value[0]
if value is None:
return None
return str(value)
@staticmethod
def _as_bool(value: object) -> bool:
"""Extract boolean payload from runtime info fields."""
if isinstance(value, list):
if not value:
return False
value = value[0]
if isinstance(value, torch.Tensor):
if value.numel() == 0:
return False
return bool(value.reshape(-1)[0].item())
if value is None:
return False
return bool(value)
@staticmethod
def _cross_fade_audio(audio: torch.Tensor, prev_tail: torch.Tensor) -> torch.Tensor:
"""Blend previous chunk tail into current chunk head using a Hamming window.
@@ -710,12 +674,17 @@ class CosyVoice3Model(
multimodal_outputs = {}
if "speech_token" in kwargs:
# Wrap in lists to pass through gpu_ar_model_runner shape filtering
multimodal_outputs = {
"speech_token": [kwargs.get("speech_token")],
"speech_feat": [kwargs.get("speech_feat")],
"embedding": [kwargs.get("embedding")],
}
# Prompt conditioning tensors for code2wav: live under
# ``embed.*`` per OmniPayloadStruct schema.
multimodal_outputs = to_dict(
OmniPayloadStruct(
embed=EmbeddingsStruct(
speech_token=kwargs.get("speech_token"),
speech_feat=kwargs.get("speech_feat"),
embedding=kwargs.get("embedding"),
),
)
)
return OmniOutput(text_hidden_states=hidden_states, multimodal_outputs=multimodal_outputs)
elif self.model_stage == "cosyvoice3_code2wav":
@@ -738,23 +707,29 @@ class CosyVoice3Model(
runtime_info = []
for idx, req_ids in enumerate(request_ids_list):
info = runtime_info[idx] if idx < len(runtime_info) and isinstance(runtime_info[idx], dict) else {}
req_id = self._as_str(info.get("req_id")) if info else None
stream_finished = self._as_bool(info.get("stream_finished")) if info else False
speech_token = self._as_tensor(info.get("speech_token")) if info else None
speech_feat = self._as_tensor(info.get("speech_feat")) if info else None
embedding = self._as_tensor(info.get("embedding")) if info else None
raw = runtime_info[idx] if idx < len(runtime_info) and isinstance(runtime_info[idx], dict) else {}
payload = to_struct(raw)
meta = payload.meta
embed = payload.embed
req_id = meta.req_id[0] if (meta and meta.req_id) else None
stream_finished = (
bool(meta.stream_finished.item()) if (meta and meta.stream_finished is not None) else False
)
speech_token = embed.speech_token if embed else None
speech_feat = embed.speech_feat if embed else None
embedding = embed.embedding if embed else None
if speech_token is None or speech_feat is None or embedding is None:
if stream_finished and req_id is not None and hasattr(self, "_stream_vocoder_cache_by_req"):
with self._stream_audio_cache_lock:
self._stream_vocoder_cache_by_req.pop(req_id, None)
audios[idx] = self._stitch_stream_audio(req_id, empty_audio, stream_finished)
if (
req_ids.numel() > 0
and info
and ("token_offset" in info or "left_context_size" in info or "generated_len" in info)
if req_ids.numel() > 0 and (
(meta and meta.left_context_size is not None) or payload.generated_len is not None
):
info_keys = ",".join(sorted(info.keys())) if info else ""
info_keys = ",".join(
sorted(f for f in payload.__struct_fields__ if getattr(payload, f) is not None)
)
logger.warning_once(
"CosyVoice3 code2wav missing prompt conditioning for non-empty codec tokens: "
"raw_len=%d info_keys=%s",
@@ -780,18 +755,11 @@ class CosyVoice3Model(
# `generated_len` is injected for many models by the generic
# runner, so only explicit chunk-routing fields should switch
# code2wav into the streaming path.
uses_streaming_decode = bool(info) and (
"stream_finished" in info or "token_offset" in info or "left_context_size" in info
uses_streaming_decode = meta and (
meta.stream_finished is not None or meta.left_context_size is not None
)
if uses_streaming_decode:
token_offset = 0
try:
if info and "token_offset" in info:
token_offset = max(0, int(info.get("token_offset", 0)))
elif info:
token_offset = max(0, int(info.get("left_context_size", 0)))
except (TypeError, ValueError):
token_offset = 0
token_offset = max(0, meta.left_context_size or 0)
cache_state = None
if req_id is not None and hasattr(self, "_stream_vocoder_cache_by_req"):
@@ -137,13 +137,7 @@ class Qwen3TTSCode2Wav(nn.Module):
break
meta = info.get("meta", {})
if "left_context_size" in meta:
# left_context_size may come through serialization as an int, [int], or tensor([int]).
value = meta["left_context_size"]
if isinstance(value, list):
value = value[0] if value else 0
if isinstance(value, torch.Tensor):
value = value.reshape(-1)[0].item() if value.numel() > 0 else 0
left_context_size[i] = int(value)
left_context_size[i] = meta["left_context_size"]
for i, req_ids in enumerate(request_ids_list):
if req_ids.numel() < 1:
parsed.append((0, 0))
@@ -8,9 +8,29 @@ import numpy as np
import torch
from vllm.inputs import TextPrompt
from vllm_omni.data_entry_keys import (
CodesStruct,
EmbeddingsStruct,
MetaStruct,
OmniPayloadStruct,
)
from vllm_omni.inputs.data import OmniTokensPrompt
def _build_prompt_embed_struct(prompt_payload: dict[str, Any]) -> EmbeddingsStruct | None:
"""Wrap prompt_payload's flat speech_token/speech_feat/embedding tensors into EmbeddingsStruct."""
speech_token = prompt_payload.get("speech_token")
speech_feat = prompt_payload.get("speech_feat")
embedding = prompt_payload.get("embedding")
if speech_token is None and speech_feat is None and embedding is None:
return None
return EmbeddingsStruct(
speech_token=speech_token,
speech_feat=speech_feat,
embedding=embedding,
)
def _ensure_list(x: Any) -> list[Any]:
if hasattr(x, "_x"):
return list(x._x)
@@ -88,7 +108,7 @@ def talker2code2wav_async_chunk(
pooling_output: dict[str, Any] | None,
request: Any,
is_finished: bool = False,
) -> dict[str, Any] | None:
) -> OmniPayloadStruct | None:
"""CosyVoice3 async_chunk processor: talker token stream -> code2wav chunks."""
with nullcontext():
request_id = request.external_req_id
@@ -114,16 +134,18 @@ def talker2code2wav_async_chunk(
if not isinstance(request_state, dict) or "_cosyvoice3_async_state" not in request_state:
with nullcontext():
info = _decode_additional_information(getattr(request, "additional_information", None))
info_embed = info.get("embed", {}) if isinstance(info, dict) else {}
prompt_payload = {}
for key in ("speech_token", "speech_feat", "embedding"):
value = _to_cpu_tensor(info.get(key))
value = _to_cpu_tensor(info_embed.get(key))
if value is not None:
prompt_payload[key] = value
if isinstance(pooling_output, dict):
po_embed = pooling_output.get("embed", {}) if isinstance(pooling_output.get("embed"), dict) else {}
for key in ("speech_token", "speech_feat", "embedding"):
if key in prompt_payload:
continue
value = _to_cpu_tensor(pooling_output.get(key))
value = _to_cpu_tensor(po_embed.get(key))
if value is not None:
prompt_payload[key] = value
prompt_token = prompt_payload.get("speech_token")
@@ -176,27 +198,29 @@ def talker2code2wav_async_chunk(
if length <= 0:
if not finished:
return None
payload: dict[str, Any] = {
"codes": {"audio": []},
"meta": {"finished": torch.tensor(True, dtype=torch.bool)},
}
embed_struct = None
if not state.get("sent_prompt", False):
payload.update(state.get("prompt_payload", {}))
embed_struct = _build_prompt_embed_struct(state.get("prompt_payload", {}))
state["sent_prompt"] = True
state["terminal_sent"] = True
return payload
return OmniPayloadStruct(
codes=CodesStruct(audio=torch.empty(0, dtype=torch.long)),
meta=MetaStruct(finished=torch.tensor(True, dtype=torch.bool)),
embed=embed_struct,
)
emitted_token_len = int(state.get("emitted_token_len", 0))
if finished and length <= emitted_token_len:
payload = {
"codes": {"audio": []},
"meta": {"finished": torch.tensor(True, dtype=torch.bool)},
}
embed_struct = None
if not state.get("sent_prompt", False):
payload.update(state.get("prompt_payload", {}))
embed_struct = _build_prompt_embed_struct(state.get("prompt_payload", {}))
state["sent_prompt"] = True
state["terminal_sent"] = True
return payload
return OmniPayloadStruct(
codes=CodesStruct(audio=torch.empty(0, dtype=torch.long)),
meta=MetaStruct(finished=torch.tensor(True, dtype=torch.bool)),
embed=embed_struct,
)
with nullcontext():
token_hop_len = max(1, int(state.get("token_hop_len", chunk_size)))
@@ -220,18 +244,22 @@ def talker2code2wav_async_chunk(
with nullcontext():
code_predictor_codes = [int(frame[0]) for frame in token_frames[:prefix_len]]
payload = {
"codes": {"audio": code_predictor_codes},
"meta": {"finished": torch.tensor(finished, dtype=torch.bool)},
"token_offset": token_offset,
"left_context_size": token_offset,
"req_id": [request_id],
"stream_finished": torch.tensor(finished, dtype=torch.bool),
}
embed_struct = None
if not state.get("sent_prompt", False):
payload.update(state.get("prompt_payload", {}))
embed_struct = _build_prompt_embed_struct(state.get("prompt_payload", {}))
state["sent_prompt"] = True
payload = OmniPayloadStruct(
codes=CodesStruct(audio=torch.tensor(code_predictor_codes, dtype=torch.long)),
meta=MetaStruct(
finished=torch.tensor(finished, dtype=torch.bool),
stream_finished=torch.tensor(finished, dtype=torch.bool),
req_id=[request_id],
left_context_size=token_offset,
),
embed=embed_struct,
)
if not finished:
state["emitted_token_len"] = emitted_token_len + this_token_hop_len
state["token_hop_len"] = min(
@@ -5,6 +5,12 @@ from typing import Any
import torch
from vllm.logger import init_logger
from vllm_omni.data_entry_keys import (
CodesStruct,
MetaStruct,
OmniPayloadStruct,
)
logger = init_logger(__name__)
@@ -59,7 +65,7 @@ def slow_ar_to_dac_decoder_async_chunk(
pooling_output: dict[str, Any] | None,
request: Any,
is_finished: bool = False,
) -> dict[str, Any] | None:
) -> OmniPayloadStruct | None:
"""Async streaming processor: emit code chunks as they are produced.
Accumulates per-step codes and emits fixed-size chunks with left context
@@ -107,10 +113,10 @@ def slow_ar_to_dac_decoder_async_chunk(
if length <= 0:
if finished:
return {
"codes": {"audio": []},
"meta": {"finished": torch.tensor(True, dtype=torch.bool)},
}
return OmniPayloadStruct(
codes=CodesStruct(audio=torch.empty(0, dtype=torch.long)),
meta=MetaStruct(finished=torch.tensor(True, dtype=torch.bool)),
)
return None
in_initial_phase = initial_chunk_size > 0 and length <= chunk_size
@@ -138,9 +144,12 @@ def slow_ar_to_dac_decoder_async_chunk(
# Pack into codebook-major flat codes.
stacked_frames = torch.stack(window_frames, dim=0)
code_predictor_codes = stacked_frames.transpose(0, 1).reshape(-1).tolist()
code_predictor_codes = stacked_frames.transpose(0, 1).reshape(-1)
return {
"codes": {"audio": code_predictor_codes},
"meta": {"left_context_size": left_context_size, "finished": torch.tensor(finished, dtype=torch.bool)},
}
return OmniPayloadStruct(
codes=CodesStruct(audio=code_predictor_codes),
meta=MetaStruct(
left_context_size=left_context_size,
finished=torch.tensor(finished, dtype=torch.bool),
),
)
@@ -4,6 +4,12 @@ import torch
from vllm.inputs import TextPrompt
from vllm.logger import init_logger
from vllm_omni.data_entry_keys import (
CodesStruct,
MetaStruct,
OmniPayload,
OmniPayloadStruct,
)
from vllm_omni.inputs.data import OmniTokensPrompt
from vllm_omni.model_executor.models.mimo_audio.config_mimo_audio import TALKER_CODEC_PAD_TOKEN_ID
@@ -53,9 +59,12 @@ def prepend_and_flatten_colmajor(x: torch.Tensor, pad_vec: torch.Tensor) -> torc
return y_col_major
def _make_finished_sentinel() -> dict[str, Any]:
def _make_finished_sentinel() -> OmniPayloadStruct:
"""Return a minimal payload with finished=True so Stage-1 can end the request."""
return {"codes": {"audio": []}, "meta": {"finished": torch.tensor(True, dtype=torch.bool)}}
return OmniPayloadStruct(
codes=CodesStruct(audio=torch.empty(0, dtype=torch.long)),
meta=MetaStruct(finished=torch.tensor(True, dtype=torch.bool)),
)
def _flush_remaining_codes(
@@ -63,7 +72,7 @@ def _flush_remaining_codes(
request_id: str,
chunk_size: int,
left_context_size: int,
) -> dict[str, Any]:
) -> OmniPayloadStruct:
"""Flush any accumulated but unsent codes when the request finishes."""
accumulated = transfer_manager.code_prompt_token_ids.get(request_id, [])
if not accumulated:
@@ -76,18 +85,18 @@ def _flush_remaining_codes(
# Align with qwen3_omni talker2code2wav_async_chunk: decoder strip uses explicit frame count.
left_ctx_frames = max(0, min(length - context_length, left_context_size))
flat_codes = torch.tensor(accumulated[-end_index:]).reshape(-1).tolist()
flat_codes = torch.tensor(accumulated[-end_index:]).reshape(-1)
return {
"codes": {"audio": flat_codes},
"meta": {
"left_context_size": left_ctx_frames,
"codec_chunk_frames": chunk_size,
"codec_left_context_frames": left_context_size,
"code_flat_numel": len(flat_codes),
"finished": torch.tensor(True, dtype=torch.bool),
},
}
return OmniPayloadStruct(
codes=CodesStruct(audio=flat_codes),
meta=MetaStruct(
left_context_size=left_ctx_frames,
codec_chunk_frames=chunk_size,
codec_left_context_frames=left_context_size,
code_flat_numel=int(flat_codes.numel()),
finished=torch.tensor(True, dtype=torch.bool),
),
)
def _is_codes_empty(codes: Any) -> bool:
@@ -114,10 +123,10 @@ def _to_code_tensor(codes: Any) -> torch.Tensor | None:
def llm2code2wav_async_chunk(
transfer_manager: Any,
pooling_output: dict[str, Any],
pooling_output: OmniPayload,
request: Any,
is_finished: bool = False,
) -> dict[str, Any] | None:
) -> OmniPayloadStruct | None:
"""
Async chunk version: convert stage-0 pooling_output to code2wav payload (pooling / connector accumulation).
@@ -167,18 +176,16 @@ def llm2code2wav_async_chunk(
left_ctx_frames = max(0, min(length - context_length, left_context_size))
flat_codes = torch.tensor(transfer_manager.code_prompt_token_ids[request_id][-end_index:]).reshape(-1).tolist()
return {
"codes": {
"audio": flat_codes,
},
"meta": {
"left_context_size": left_ctx_frames,
"codec_chunk_frames": chunk_size,
"codec_left_context_frames": left_context_size,
"code_flat_numel": len(flat_codes),
"finished": torch.tensor(is_finished, dtype=torch.bool),
},
}
return OmniPayloadStruct(
codes=CodesStruct(audio=torch.tensor(flat_codes)),
meta=MetaStruct(
left_context_size=left_ctx_frames,
codec_chunk_frames=chunk_size,
codec_left_context_frames=left_context_size,
code_flat_numel=len(flat_codes),
finished=torch.tensor(is_finished, dtype=torch.bool),
),
)
def llm2code2wav(
@@ -1,7 +1,14 @@
import torch
from vllm.inputs import TextPrompt
from vllm_omni.data_entry_keys import OmniPayload
from vllm_omni.data_entry_keys import (
EmbeddingsStruct,
HiddenStatesStruct,
IdsStruct,
OmniPayload,
OmniPayloadStruct,
to_dict,
)
from vllm_omni.inputs.data import OmniTokensPrompt
TALKER_CODEC_PAD_TOKEN_ID = 8292
@@ -30,17 +37,15 @@ def thinker2talker(
mm: OmniPayload = output.multimodal_output
latent = mm["latent"]
thinker_hidden_states = latent.clone().detach().to(latent.device)
additional_information = {
"hidden_states": {
"output": thinker_hidden_states[prompt_token_ids_len:].to(torch.float32),
"output_shape": list(thinker_hidden_states[prompt_token_ids_len:].shape),
},
"embed": {
"prefill": thinker_hidden_states[:prompt_token_ids_len].to(torch.float32),
"prefill_shape": list(thinker_hidden_states[:prompt_token_ids_len].shape),
},
"ids": {"prompt": prompt_token_ids, "output": thinker_output_ids},
}
decode_hidden = thinker_hidden_states[prompt_token_ids_len:].to(torch.float32)
prefill_hidden = thinker_hidden_states[:prompt_token_ids_len].to(torch.float32)
additional_information = to_dict(
OmniPayloadStruct(
hidden_states=HiddenStatesStruct(output=decode_hidden, output_shape=list(decode_hidden.shape)),
embed=EmbeddingsStruct(prefill=prefill_hidden, prefill_shape=list(prefill_hidden.shape)),
ids=IdsStruct(prompt=list(prompt_token_ids), output=list(thinker_output_ids)),
)
)
talker_inputs.append(
OmniTokensPrompt(
prompt_token_ids=[TALKER_CODEC_START_TOKEN_ID]
@@ -11,7 +11,16 @@ import torch
from vllm.inputs import TextPrompt
from vllm.platforms import current_platform
from vllm_omni.data_entry_keys import OmniPayload
from vllm_omni.data_entry_keys import (
CodesStruct,
EmbeddingsStruct,
HiddenStatesStruct,
IdsStruct,
MetaStruct,
OmniPayload,
OmniPayloadStruct,
to_dict,
)
from vllm_omni.engine import OmniEngineCoreRequest
from vllm_omni.inputs.data import OmniTokensPrompt
from vllm_omni.model_executor.stage_input_processors.tts_utils import (
@@ -275,10 +284,10 @@ def _get_streaming_codec_delta_len(
def thinker2talker_async_chunk(
transfer_manager: Any,
pooling_output: dict[str, Any],
pooling_output: OmniPayload,
request: OmniEngineCoreRequest,
is_finished: bool = False,
) -> list[dict[str, Any]]:
) -> OmniPayloadStruct | None:
"""
Process thinker outputs to create talker inputs.
1. thinker's text generation outputs (token IDs + hidden states)
@@ -294,7 +303,8 @@ def thinker2talker_async_chunk(
thinker_hs = pooling_output.get("hidden_states", {})
thinker_layers = thinker_hs.get("layers", {}) if isinstance(thinker_hs, dict) else {}
thinker_embed = pooling_output.get("embed", {}) if isinstance(pooling_output.get("embed", {}), dict) else {}
thinker_embed_raw = pooling_output.get("embed", {})
thinker_embed = thinker_embed_raw if isinstance(thinker_embed_raw, dict) else {}
thinker_emb = _layer_tensor(thinker_layers, _EMBED_LAYER_KEY)
thinker_hid = _layer_tensor(thinker_layers, _HIDDEN_LAYER_KEY)
if thinker_emb is None or thinker_hid is None:
@@ -305,82 +315,62 @@ def thinker2talker_async_chunk(
thinker_hid is not None,
)
return None
speaker = extract_speaker_from_request(request)
language = extract_language_from_request(request)
def _maybe_cpu(t: Any) -> torch.Tensor | None:
return t.detach().cpu() if isinstance(t, torch.Tensor) else None
if chunk_id == 0:
all_token_ids = request.all_token_ids # prefill + decode
prompt_token_ids = request.prompt_token_ids
# Convert ConstantList to regular list for OmniSerializer serialization
all_token_ids = _ensure_list(all_token_ids)
prompt_token_ids = _ensure_list(prompt_token_ids)
payload: OmniPayload = {
"embed": {
"prefill": thinker_emb.detach().cpu(),
# Provide thinker-side TTS token embeddings for talker projection
"tts_bos": thinker_embed.get("tts_bos").detach().cpu()
if isinstance(thinker_embed.get("tts_bos"), torch.Tensor)
else None,
"tts_eos": thinker_embed.get("tts_eos").detach().cpu()
if isinstance(thinker_embed.get("tts_eos"), torch.Tensor)
else None,
"tts_pad": thinker_embed.get("tts_pad").detach().cpu()
if isinstance(thinker_embed.get("tts_pad"), torch.Tensor)
else None,
},
"hidden_states": {"output": thinker_hid.detach().cpu()},
"ids": {"all": all_token_ids, "prompt": prompt_token_ids},
"meta": {"finished": torch.tensor(is_finished, dtype=torch.bool)},
}
talker_additional_info = payload
speaker = extract_speaker_from_request(request)
if speaker is not None:
talker_additional_info["speaker"] = speaker
language = extract_language_from_request(request)
if language is not None:
talker_additional_info["language"] = language
all_token_ids = _ensure_list(request.all_token_ids)
prompt_token_ids = _ensure_list(request.prompt_token_ids)
payload = OmniPayloadStruct(
embed=EmbeddingsStruct(
prefill=thinker_emb.detach().cpu(),
tts_bos=_maybe_cpu(thinker_embed.get("tts_bos")),
tts_eos=_maybe_cpu(thinker_embed.get("tts_eos")),
tts_pad=_maybe_cpu(thinker_embed.get("tts_pad")),
),
hidden_states=HiddenStatesStruct(output=thinker_hid.detach().cpu()),
ids=IdsStruct(all=all_token_ids, prompt=prompt_token_ids),
meta=MetaStruct(finished=torch.tensor(is_finished, dtype=torch.bool)),
speaker=speaker,
language=language,
)
if transfer_manager.request_payload.get(request_id) is None:
if not is_finished:
transfer_manager.request_payload[request_id] = talker_additional_info
transfer_manager.request_payload[request_id] = to_dict(payload)
return None
else:
save_payload = transfer_manager.request_payload.pop(request_id)
talker_additional_info["embed"]["prefill"] = torch.cat(
(
save_payload.get("embed", {}).get("prefill"),
talker_additional_info.get("embed", {}).get("prefill"),
),
dim=0,
payload.embed.prefill = torch.cat(
(save_payload.get("embed", {}).get("prefill"), payload.embed.prefill), dim=0
)
talker_additional_info["hidden_states"]["output"] = torch.cat(
(
save_payload.get("hidden_states", {}).get("output"),
talker_additional_info.get("hidden_states", {}).get("output"),
),
dim=0,
payload.hidden_states.output = torch.cat(
(save_payload.get("hidden_states", {}).get("output"), payload.hidden_states.output), dim=0
)
else:
output_token_ids = request.output_token_ids
# Convert ConstantList to regular list for OmniSerializer serialization
output_token_ids = _ensure_list(output_token_ids)
talker_additional_info: OmniPayload = {
"meta": {"finished": torch.tensor(is_finished, dtype=torch.bool)},
}
speaker = extract_speaker_from_request(request)
if speaker is not None:
talker_additional_info["speaker"] = speaker
language = extract_language_from_request(request)
if language is not None:
talker_additional_info["language"] = language
output_token_ids = _ensure_list(request.output_token_ids)
meta = MetaStruct(finished=torch.tensor(is_finished, dtype=torch.bool))
if output_token_ids:
talker_additional_info["meta"]["override_keys"] = [("embed", "decode"), ("ids", "output")]
talker_additional_info["embed"] = {"decode": thinker_emb.detach().cpu()}
talker_additional_info["ids"] = {"output": output_token_ids}
meta.override_keys = [("embed", "decode"), ("ids", "output")]
payload = OmniPayloadStruct(
meta=meta,
embed=EmbeddingsStruct(decode=thinker_emb.detach().cpu()),
ids=IdsStruct(output=output_token_ids),
speaker=speaker,
language=language,
)
else:
# When prefilling a chunked thinker, thinker_hidden_states needs to be updated.
talker_additional_info["embed"] = {"prefill": thinker_emb.detach().cpu()}
talker_additional_info["hidden_states"] = {"output": thinker_hid.detach().cpu()}
return talker_additional_info
payload = OmniPayloadStruct(
meta=meta,
embed=EmbeddingsStruct(prefill=thinker_emb.detach().cpu()),
hidden_states=HiddenStatesStruct(output=thinker_hid.detach().cpu()),
speaker=speaker,
language=language,
)
return payload
def thinker2talker(
@@ -459,36 +449,26 @@ def thinker2talker(
except Exception as exc:
logger.warning("[PD] Could not merge prefill embeddings: %s", exc)
payload: OmniPayload = {
"embed": {
"prefill": thinker_emb,
"tts_bos": _resolve_tts_token_embedding(
payload = OmniPayloadStruct(
embed=EmbeddingsStruct(
prefill=thinker_emb,
tts_bos=_resolve_tts_token_embedding(
"tts_bos", thinker_mm=thinker_mm, prefill_mm=prefill_mm, device=device
),
"tts_eos": _resolve_tts_token_embedding(
tts_eos=_resolve_tts_token_embedding(
"tts_eos", thinker_mm=thinker_mm, prefill_mm=prefill_mm, device=device
),
"tts_pad": _resolve_tts_token_embedding(
tts_pad=_resolve_tts_token_embedding(
"tts_pad", thinker_mm=thinker_mm, prefill_mm=prefill_mm, device=device
),
},
"hidden_states": {
"output": thinker_hid,
},
"ids": {
"all": thinker_sequences,
"prompt": thinker_input_ids,
},
}
info = payload
speaker = extract_speaker_from_prompt(prompt, index=i)
if speaker is not None:
info["speaker"] = speaker
language = extract_language_from_prompt(prompt, index=i)
if language is not None:
info["language"] = language
prompt_len = _compute_talker_prompt_ids_length(payload, device=device)
),
hidden_states=HiddenStatesStruct(output=thinker_hid),
ids=IdsStruct(all=thinker_sequences, prompt=thinker_input_ids),
speaker=extract_speaker_from_prompt(prompt, index=i),
language=extract_language_from_prompt(prompt, index=i),
)
info = to_dict(payload)
prompt_len = _compute_talker_prompt_ids_length(info, device=device)
talker_inputs.append(
OmniTokensPrompt(
@@ -509,10 +489,10 @@ def thinker2talker(
def talker2code2wav_async_chunk(
transfer_manager: Any,
pooling_output: dict[str, Any],
pooling_output: OmniPayload,
request: OmniEngineCoreRequest,
is_finished: bool = False,
):
) -> OmniPayloadStruct | None:
"""
Pooling version.
"""
@@ -565,17 +545,15 @@ def talker2code2wav_async_chunk(
left_context_size = max(0, min(length - context_length, left_context_size_config))
end_index = min(length, left_context_size + context_length)
codes = (
torch.tensor(transfer_manager.code_prompt_token_ids[request_id][-end_index:])
.transpose(0, 1)
.reshape(-1)
.tolist()
)
codes = torch.tensor(transfer_manager.code_prompt_token_ids[request_id][-end_index:]).transpose(0, 1).reshape(-1)
return {
"codes": {"audio": codes},
"meta": {"left_context_size": left_context_size, "finished": torch.tensor(is_finished, dtype=torch.bool)},
}
return OmniPayloadStruct(
codes=CodesStruct(audio=codes),
meta=MetaStruct(
left_context_size=left_context_size,
finished=torch.tensor(is_finished, dtype=torch.bool),
),
)
def talker2code2wav(
@@ -5,6 +5,13 @@ from typing import Any
import torch
from vllm.logger import init_logger
from vllm_omni.data_entry_keys import (
CodesStruct,
MetaStruct,
OmniPayload,
OmniPayloadStruct,
to_dict,
)
from vllm_omni.model_executor.stage_input_processors.chunk_size_utils import (
compute_dynamic_initial_chunk_size,
max_ic_for_chunk_size,
@@ -95,18 +102,13 @@ def talker2code2wav(
ref_code_len = 0
# Code2Wav expects codebook-major flat: [Q*num_frames]
codec_codes = audio_codes.transpose(0, 1).cpu().reshape(-1).tolist()
additional_information: dict[str, Any] = {}
if ref_code_len > 0:
additional_information["meta"] = {"left_context_size": [ref_code_len]}
# Propagate speaker and language from the original prompt so they are
# available as runtime_additional_information in later pipeline stages,
# consistent with qwen3-omni and qwen2.5-omni stage input processors.
speaker = extract_speaker_from_prompt(prompt, index=i)
if speaker is not None:
additional_information["speaker"] = speaker
language = extract_language_from_prompt(prompt, index=i)
if language is not None:
additional_information["language"] = language
additional_information = to_dict(
OmniPayloadStruct(
meta=MetaStruct(left_context_size=ref_code_len) if ref_code_len > 0 else None,
speaker=extract_speaker_from_prompt(prompt, index=i),
language=extract_language_from_prompt(prompt, index=i),
)
)
code2wav_inputs.append(
OmniTokensPrompt(
prompt_token_ids=codec_codes,
@@ -118,7 +120,7 @@ def talker2code2wav(
return code2wav_inputs
def _extract_last_frame(pooling_output: dict[str, Any]) -> torch.Tensor | None:
def _extract_last_frame(pooling_output: OmniPayload) -> torch.Tensor | None:
audio_codes = pooling_output.get("codes", {}).get("audio")
if not isinstance(audio_codes, torch.Tensor) or audio_codes.numel() == 0:
return None
@@ -134,10 +136,10 @@ def _extract_last_frame(pooling_output: dict[str, Any]) -> torch.Tensor | None:
def talker2code2wav_async_chunk(
transfer_manager: Any,
pooling_output: dict[str, Any] | None,
pooling_output: OmniPayload | None,
request: Any,
is_finished: bool = False,
) -> dict[str, Any] | None:
) -> OmniPayloadStruct | None:
request_id = request.external_req_id
finished = bool(is_finished or request.is_finished())
request_payload = getattr(transfer_manager, "request_payload", None)
@@ -209,10 +211,10 @@ def talker2code2wav_async_chunk(
if length <= 0:
if finished:
return {
"codes": {"audio": []},
"meta": {"finished": torch.tensor(True, dtype=torch.bool)},
}
return OmniPayloadStruct(
codes=CodesStruct(audio=torch.empty(0, dtype=torch.long)),
meta=MetaStruct(finished=torch.tensor(True, dtype=torch.bool)),
)
return None
use_first_chunk = initial_chunk_size > 0 and initial_chunk_size < chunk_size
@@ -249,19 +251,17 @@ def talker2code2wav_async_chunk(
num_quantizers = len(window_frames[0])
num_frames = len(window_frames)
code_predictor_codes = [window_frames[f][q] for q in range(num_quantizers) for f in range(num_frames)]
code_predictor_codes = torch.tensor(
[window_frames[f][q] for q in range(num_quantizers) for f in range(num_frames)],
dtype=torch.long,
)
info: dict[str, Any] = {
"codes": {"audio": code_predictor_codes},
"meta": {"left_context_size": left_context_size, "finished": torch.tensor(finished, dtype=torch.bool)},
}
# Propagate speaker and language from the request so they are available
# as runtime_additional_information in subsequent pipeline stages, consistent
# with qwen3-omni and qwen2.5-omni stage input processors.
speaker = extract_speaker_from_request(request)
if speaker is not None:
info["speaker"] = speaker
language = extract_language_from_request(request)
if language is not None:
info["language"] = language
return info
return OmniPayloadStruct(
codes=CodesStruct(audio=code_predictor_codes),
meta=MetaStruct(
left_context_size=left_context_size,
finished=torch.tensor(finished, dtype=torch.bool),
),
speaker=extract_speaker_from_request(request),
language=extract_language_from_request(request),
)
@@ -5,6 +5,11 @@ from typing import Any
import torch
from vllm.inputs import TextPrompt
from vllm_omni.data_entry_keys import (
CodesStruct,
MetaStruct,
OmniPayloadStruct,
)
from vllm_omni.inputs.data import OmniTokensPrompt
_VOXCPM_LATENT_MAGIC = 131071
@@ -74,12 +79,19 @@ def latent2vae(
return vae_inputs
def _eof_payload() -> OmniPayloadStruct:
return OmniPayloadStruct(
codes=CodesStruct(audio=torch.empty(0, dtype=torch.long)),
meta=MetaStruct(finished=torch.tensor(True, dtype=torch.bool)),
)
def latent2vae_async_chunk(
transfer_manager: Any,
pooling_output: dict[str, Any] | None,
request: Any,
is_finished: bool = False,
) -> dict[str, Any] | None:
) -> OmniPayloadStruct | None:
"""Stage-0 latent → stage-1 VAE under ``async_chunk`` (connector payload)."""
# Kept for callback signature compatibility with OmniChunkTransferAdapter.
_ = transfer_manager
@@ -87,28 +99,17 @@ def latent2vae_async_chunk(
if callable(getattr(request, "is_finished", None)):
finished_request = finished_request or _coerce_finished_flag(request.is_finished())
if not isinstance(pooling_output, dict):
if finished_request:
return {
"code_predictor_codes": [],
"finished": torch.tensor(True, dtype=torch.bool),
}
return None
return _eof_payload() if finished_request else None
latent = pooling_output.get("latent_audio_feat")
if isinstance(latent, torch.Tensor) and latent.numel() == 0:
latent = None
if latent is None:
if finished_request:
return {
"code_predictor_codes": [],
"finished": torch.tensor(True, dtype=torch.bool),
}
return None
return _eof_payload() if finished_request else None
serialized_codes = _serialize_latent_to_codes(latent)
out: dict[str, Any] = {
"code_predictor_codes": serialized_codes,
"finished": torch.tensor(finished_request, dtype=torch.bool),
}
return out
return OmniPayloadStruct(
codes=CodesStruct(audio=torch.tensor(serialized_codes, dtype=torch.long)),
meta=MetaStruct(finished=torch.tensor(finished_request, dtype=torch.bool)),
)
@@ -4,6 +4,12 @@ import torch
from vllm.inputs import TextPrompt
from vllm.logger import init_logger
from vllm_omni.data_entry_keys import (
CodesStruct,
MetaStruct,
OmniPayload,
OmniPayloadStruct,
)
from vllm_omni.inputs.data import OmniTokensPrompt
logger = init_logger(__name__)
@@ -29,7 +35,7 @@ def generator2tokenizer(
return tokenizer_inputs
def _extract_last_frame(pooling_output: dict[str, Any]) -> torch.Tensor | None:
def _extract_last_frame(pooling_output: OmniPayload) -> torch.Tensor | None:
audio = pooling_output.get("audio")
if not isinstance(audio, torch.Tensor) or audio.numel() == 0:
return None
@@ -38,10 +44,10 @@ def _extract_last_frame(pooling_output: dict[str, Any]) -> torch.Tensor | None:
def generator2tokenizer_async_chunk(
transfer_manager: Any,
pooling_output: dict[str, Any],
pooling_output: OmniPayload,
request: Any,
is_finished: bool = False,
) -> dict[str, Any] | None:
) -> OmniPayloadStruct | None:
request_id = request.external_req_id
finished = bool(is_finished or request.is_finished())
@@ -71,10 +77,10 @@ def generator2tokenizer_async_chunk(
# finished and nothing was produced, emit an EOF marker.
if length <= 0:
if finished:
return {
"codes": {"audio": []},
"meta": {"finished": torch.tensor(True, dtype=torch.bool)},
}
return OmniPayloadStruct(
codes=CodesStruct(audio=torch.empty(0, dtype=torch.long)),
meta=MetaStruct(finished=torch.tensor(True, dtype=torch.bool)),
)
return None
# Use a small chunk size at begin
@@ -94,7 +100,12 @@ def generator2tokenizer_async_chunk(
# Pack context + chunk into codebook-major flat codes for adapter.
code_predictor_codes = torch.tensor(window_frames).reshape(-1).tolist()
return {
"codes": {"audio": [int(ctx_frames)] + [int(context_length)] + code_predictor_codes},
"meta": {"finished": torch.tensor(finished, dtype=torch.bool)},
}
return OmniPayloadStruct(
codes=CodesStruct(
audio=torch.tensor(
[int(ctx_frames), int(context_length)] + code_predictor_codes,
dtype=torch.long,
),
),
meta=MetaStruct(finished=torch.tensor(finished, dtype=torch.bool)),
)
+25 -20
View File
@@ -28,28 +28,30 @@ def build_mm_cpu(multimodal_outputs: dict) -> dict[str, object]:
if multimodal_outputs:
for k, v in multimodal_outputs.items():
if isinstance(v, torch.Tensor):
mm_cpu[k] = v.detach().to("cpu").contiguous()
elif isinstance(v, dict):
sub_dict: dict[str, torch.Tensor] = {}
for sk, sv in v.items():
if isinstance(sv, torch.Tensor):
sub_dict[str(sk)] = sv.detach().to("cpu").contiguous()
if sub_dict:
mm_cpu[k] = sub_dict
elif isinstance(v, list) and len(v) > 0:
cpu_list = []
for elem in v:
if isinstance(elem, torch.Tensor):
cpu_list.append(elem.detach().to("cpu").contiguous())
else:
cpu_list.append(elem)
mm_cpu[k] = cpu_list
elif v is not None:
mm_cpu[k] = v
cpu_v = _to_cpu(v)
if cpu_v is not None:
mm_cpu[k] = cpu_v
return mm_cpu
def _to_cpu(value):
"""Recursively detach + move tensors to CPU; preserve dict/list nesting."""
if isinstance(value, torch.Tensor):
return value.detach().to("cpu").contiguous()
if isinstance(value, dict):
out = {}
for k, v in value.items():
cpu_v = _to_cpu(v)
if cpu_v is not None:
out[k] = cpu_v
return out or None
if isinstance(value, list):
if not value:
return value
return [_to_cpu(v) for v in value]
return value
def to_payload_element(
element: object, idx: int, start: int, end: int, pass_lists_through: bool = False, seq_len: int | None = None
):
@@ -77,7 +79,10 @@ def to_payload_element(
# Every other case is shared between prefix cache (passthrough data)
# and running a model without prefix caching.
elif isinstance(element, dict):
return {sk: sv[start:end].contiguous() for sk, sv in element.items()}
return {
sk: to_payload_element(sv, idx, start, end, pass_lists_through=pass_lists_through, seq_len=seq_len)
for sk, sv in element.items()
}
elif isinstance(element, list):
# For lists, clone tensors to avoid cross-request aliasing
if pass_lists_through:
+16 -10
View File
@@ -261,7 +261,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
self.omni_prefix_cache.update_omni_tensor_prefix_cache(
hidden_states=hidden_states,
multimodal_outputs=multimodal_outputs,
multimodal_outputs=flatten_payload(multimodal_outputs) if multimodal_outputs else multimodal_outputs,
num_tokens_unpadded=num_tokens_unpadded,
slot_mapping=self.input_batch.block_table[0].slot_mapping.cpu,
num_tokens_padded=num_tokens_padded,
@@ -289,7 +289,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
combined_multimodal_outputs = self.omni_prefix_cache.get_merged_multimodal_states(
query_start_loc=self.query_start_loc.cpu,
input_batch=self.input_batch,
multimodal_outputs=multimodal_outputs,
multimodal_outputs=flatten_payload(multimodal_outputs) if multimodal_outputs else multimodal_outputs,
num_scheduled_tokens=num_scheduled_tokens,
)
return combined_hidden_states, combined_multimodal_outputs
@@ -954,15 +954,21 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
if combined_multimodal_outputs:
# Prefix cache enabled; all items have already been processed
# and split apart for each request as needed, and all tensors
# have already been detached to the CPU. The only exception is
# lists, which we keep as passthrough data for consistent behavior
# in postprocess.
# have already been detached to the CPU. Lists are kept as
# passthrough data for consistent behavior in postprocess.
# Recurse into nested dicts so list-valued sub-keys (e.g.
# embed.tts_bos = [tensor]) are unwrapped to bare tensors
# at the leaves; downstream flatten_payload then yields a
# wire-clean dict[str, torch.Tensor].
def _unwrap_lists(v):
if isinstance(v, list):
return v[idx] if idx < len(v) else v[0]
if isinstance(v, dict):
return {k: _unwrap_lists(sv) for k, sv in v.items()}
return v
for mm_key in combined_multimodal_outputs.keys():
value = combined_multimodal_outputs[mm_key][rid]
if isinstance(value, list):
mm_payload[mm_key] = value[idx] if idx < len(value) else value[0]
else:
mm_payload[mm_key] = value
mm_payload[mm_key] = _unwrap_lists(combined_multimodal_outputs[mm_key][rid])
else:
# Prefix cache disabled; we still need to process the data
@@ -24,18 +24,17 @@ import torch
from vllm.distributed.parallel_state import get_tp_group
from vllm.logger import init_logger
from vllm_omni.data_entry_keys import OmniPayload
from vllm_omni.distributed.omni_connectors.factory import OmniConnectorFactory
from vllm_omni.distributed.omni_connectors.utils.config import ConnectorSpec
from vllm_omni.outputs import OmniConnectorOutput
from vllm_omni.worker.payload_span import (
THINKER_DECODE_EMBEDDINGS_KEY,
THINKER_DECODE_TOKEN_END_KEY,
THINKER_DECODE_TOKEN_START_KEY,
THINKER_OUTPUT_TOKEN_IDS_KEY,
get_tensor_span,
merge_tensor_spans,
)
_EMBED_SPAN_GROUPS: tuple[tuple[str, str, str], ...] = (("decode", "decode_token_start", "decode_token_end"),)
if TYPE_CHECKING:
from vllm_omni.distributed.omni_connectors.connectors.base import (
OmniConnectorBase,
@@ -345,15 +344,15 @@ class OmniConnectorModelRunnerMixin:
# Local payload cache (RFC §2.4 – Model Runner ownership)
# ------------------------------------------------------------------ #
def put_local_stage_payload(self, req_id: str, payload: dict[str, Any]) -> None:
def put_local_stage_payload(self, req_id: str, payload: OmniPayload) -> None:
"""Store a full stage payload in the local cache."""
self._local_stage_payload_cache[req_id] = payload
def get_local_stage_payload(self, req_id: str) -> dict[str, Any] | None:
def get_local_stage_payload(self, req_id: str) -> OmniPayload | None:
"""Read a stage payload without removing it."""
return self._local_stage_payload_cache.get(req_id)
def pop_local_stage_payload(self, req_id: str) -> dict[str, Any] | None:
def pop_local_stage_payload(self, req_id: str) -> OmniPayload | None:
"""Remove and return a stage payload (consume after use)."""
return self._local_stage_payload_cache.pop(req_id, None)
@@ -370,29 +369,39 @@ class OmniConnectorModelRunnerMixin:
# ------------------------------------------------------------------ #
@classmethod
def _extract_scheduling_metadata(cls, payload: dict[str, Any]) -> dict[str, Any]:
def _extract_scheduling_metadata(cls, payload: OmniPayload) -> dict[str, Any]:
"""Extract only the fields the scheduler needs from a full payload."""
extracted: dict[str, Any] = {}
if "next_stage_prompt_len" in payload:
meta = payload.get("meta") if isinstance(payload, dict) else None
meta = meta if isinstance(meta, dict) else {}
if "next_stage_prompt_len" in meta:
extracted["next_stage_prompt_len"] = meta["next_stage_prompt_len"]
elif "next_stage_prompt_len" in payload:
logger.warning_once(
"legacy flat 'next_stage_prompt_len' key in payload; expected 'meta.next_stage_prompt_len'"
)
extracted["next_stage_prompt_len"] = payload["next_stage_prompt_len"]
audio_codes = cls._payload_audio_codes(payload)
if audio_codes is not None:
extracted["code_predictor_codes"] = audio_codes
meta = payload.get("meta")
if isinstance(meta, dict) and "left_context_size" in meta:
if "left_context_size" in meta:
extracted["left_context_size"] = meta["left_context_size"]
elif "left_context_size" in payload:
logger.warning_once("legacy flat 'left_context_size' key in payload; expected 'meta.left_context_size'")
return extracted
_NON_CONSUMABLE_PAYLOAD_KEYS = {
"finished",
"override_keys",
"next_stage_prompt_len",
"left_context_size",
THINKER_OUTPUT_TOKEN_IDS_KEY,
THINKER_DECODE_TOKEN_START_KEY,
THINKER_DECODE_TOKEN_END_KEY,
_NON_CONSUMABLE_PAYLOAD_KEYS: set[tuple[str, str]] = {
("meta", "finished"),
("meta", "override_keys"),
("meta", "next_stage_prompt_len"),
("meta", "left_context_size"),
("ids", "output"),
("embed", "decode_token_start"),
("embed", "decode_token_end"),
}
@staticmethod
@@ -431,7 +440,7 @@ class OmniConnectorModelRunnerMixin:
return None
@classmethod
def _payload_is_consumable(cls, payload: dict[str, Any] | None) -> bool:
def _payload_is_consumable(cls, payload: OmniPayload | None) -> bool:
"""Return True when an async payload can drive a real forward step.
Metadata-only wake-ups should not transition WAITING_FOR_CHUNK requests
@@ -442,23 +451,29 @@ class OmniConnectorModelRunnerMixin:
if not isinstance(payload, dict) or not payload:
return False
decode_embeddings = payload.get(THINKER_DECODE_EMBEDDINGS_KEY)
if isinstance(decode_embeddings, torch.Tensor):
if decode_embeddings.ndim == 0:
return True
return decode_embeddings.numel() > 0 and decode_embeddings.shape[0] > 0
embed = payload.get("embed")
if isinstance(embed, dict):
decode_embeddings = embed.get("decode")
if isinstance(decode_embeddings, torch.Tensor):
if decode_embeddings.ndim == 0:
return True
return decode_embeddings.numel() > 0 and decode_embeddings.shape[0] > 0
audio_codes = cls._payload_audio_codes(payload)
if audio_codes is not None:
if isinstance(audio_codes, torch.Tensor):
return audio_codes.numel() > 0
# Codec code 0 is valid; non-empty code payloads are consumable.
if hasattr(audio_codes, "__len__"):
return len(audio_codes) > 0
return True
for key, value in payload.items():
if key in cls._NON_CONSUMABLE_PAYLOAD_KEYS:
if isinstance(value, dict):
for sk, sv in value.items():
if (key, sk) in cls._NON_CONSUMABLE_PAYLOAD_KEYS:
continue
if cls._payload_value_has_content(sv):
return True
continue
if cls._payload_value_has_content(value):
return True
@@ -1796,85 +1811,66 @@ class OmniConnectorModelRunnerMixin:
# Payload accumulation (ported from OmniChunkTransferAdapter)
# ------------------------------------------------------------------ #
def _accumulate_payload(self, req_id: str, payload_data: dict[str, Any]) -> dict[str, Any]:
"""Accumulate chunk payloads (concat tensors, extend lists).
Returns a **shallow copy** of the accumulated state so callers
(e.g. ``_poll_single_request``) can store it in
``_local_stage_payload_cache`` without aliasing the authoritative
``_send_side_request_payload`` dict.
"""
def _accumulate_payload(self, req_id: str, payload_data: OmniPayload) -> OmniPayload:
"""Accumulate chunk payloads (concat tensors, extend lists)."""
if req_id not in self._send_side_request_payload:
self._send_side_request_payload[req_id] = dict(payload_data)
return dict(self._send_side_request_payload[req_id])
origin = self._send_side_request_payload[req_id]
merged = dict(origin)
override_keys = payload_data.get("override_keys", ())
drop_decode_span = False
decode_span_handled = False
raw_ok = payload_data.get("meta", {}).get("override_keys", []) if isinstance(payload_data, dict) else []
override_keys = {tuple(k) if isinstance(k, list) else k for k in raw_ok}
for key, value in payload_data.items():
if key == "finished":
merged[key] = value
continue
if key == THINKER_DECODE_EMBEDDINGS_KEY:
merged_span = merge_tensor_spans(
get_tensor_span(
origin,
tensor_key=THINKER_DECODE_EMBEDDINGS_KEY,
start_key=THINKER_DECODE_TOKEN_START_KEY,
end_key=THINKER_DECODE_TOKEN_END_KEY,
),
get_tensor_span(
payload_data,
tensor_key=THINKER_DECODE_EMBEDDINGS_KEY,
start_key=THINKER_DECODE_TOKEN_START_KEY,
end_key=THINKER_DECODE_TOKEN_END_KEY,
),
)
if merged_span is not None:
merged[key], merged[THINKER_DECODE_TOKEN_START_KEY], merged[THINKER_DECODE_TOKEN_END_KEY] = (
merged_span
)
decode_span_handled = True
continue
if isinstance(value, torch.Tensor) and key in origin:
if (
THINKER_DECODE_TOKEN_START_KEY in origin
or THINKER_DECODE_TOKEN_END_KEY in origin
or THINKER_DECODE_TOKEN_START_KEY in payload_data
or THINKER_DECODE_TOKEN_END_KEY in payload_data
):
logger.warning(
"[Stage-%s] req=%s falling back to legacy thinker decode "
"merge due to missing/invalid/non-contiguous span "
"metadata",
self._stage_id,
req_id,
if isinstance(value, dict):
origin_sub = origin.get(key)
merged_sub = dict(origin_sub) if isinstance(origin_sub, dict) else {}
span_handled: set[str] = set()
if key == "embed" and isinstance(origin_sub, dict):
for tk, sk, ek in _EMBED_SPAN_GROUPS:
if tk not in value or (key, tk) in override_keys:
continue
span = merge_tensor_spans(
get_tensor_span(origin_sub, tensor_key=tk, start_key=sk, end_key=ek),
get_tensor_span(value, tensor_key=tk, start_key=sk, end_key=ek),
)
drop_decode_span = True
merged[key] = torch.cat([origin[key], value], dim=0)
if span is None:
continue
t, s, e = span
merged_sub[tk] = t
merged_sub[sk] = s
merged_sub[ek] = e
span_handled |= {tk, sk, ek}
for qual, qval in value.items():
if qual in span_handled:
continue
if key == "meta" and qual == "finished":
merged_sub[qual] = qval
continue
if (key, qual) in override_keys:
merged_sub[qual] = qval
continue
osv = merged_sub.get(qual)
if isinstance(qval, torch.Tensor) and isinstance(osv, torch.Tensor):
merged_sub[qual] = torch.cat([osv, qval], dim=0)
elif isinstance(qval, list) and isinstance(osv, list):
merged_sub[qual] = osv + qval
else:
merged_sub[qual] = qval
merged[key] = merged_sub
else:
if key in override_keys:
merged[key] = value
continue
merged[key] = value
continue
if key in {THINKER_DECODE_TOKEN_START_KEY, THINKER_DECODE_TOKEN_END_KEY}:
if decode_span_handled or drop_decode_span:
continue
merged[key] = value
continue
if key in override_keys:
merged[key] = value
continue
if isinstance(value, torch.Tensor) and key in origin:
merged[key] = torch.cat([origin[key], value], dim=0)
elif isinstance(value, list) and key in origin:
merged[key] = origin[key] + value
else:
merged[key] = value
ov = origin.get(key)
if isinstance(value, torch.Tensor) and isinstance(ov, torch.Tensor):
merged[key] = torch.cat([ov, value], dim=0)
elif isinstance(value, list) and isinstance(ov, list):
merged[key] = ov + value
else:
merged[key] = value
if drop_decode_span:
merged.pop(THINKER_DECODE_TOKEN_START_KEY, None)
merged.pop(THINKER_DECODE_TOKEN_END_KEY, None)
self._send_side_request_payload[req_id] = merged
return dict(merged)