[Refactor] Output Processor Phase 2: separate multimodal output channel (#1601) (#2744)

Signed-off-by: Megha Agarwal <agarwalmegha1308@gmail.com>
Signed-off-by: meghaagr13 <divyanshsinghvi@gmail.com>
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Co-authored-by: meghaagr13 <divyanshsinghvi@gmail.com>
Co-authored-by: Boao Shi <aoibosh@connect.hku.hk>
Co-authored-by: Taichang Zhou <tzhouam@connect.ust.hk>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
authored and GitHub committed 2026-06-12 15:19:14 +08:00
1 parent 1be1a71156
commit 8f869c8ea5
49 files changed
+985 -327

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for _ensure_tensor_values boundary enforcement."""
import pytest
import torch
from vllm_omni.worker.gpu_ar_model_runner import _ensure_tensor_values
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_tensors_pass_through():
"""Tensor values pass through unchanged."""
t = torch.randn(10)
result = _ensure_tensor_values({"audio": t})
assert torch.equal(result["audio"], t)
def test_scalar_int_wrapped():
"""Integer scalars are wrapped in torch.tensor."""
result = _ensure_tensor_values({"sr": 24000})
assert isinstance(result["sr"], torch.Tensor)
assert result["sr"].item() == 24000
def test_scalar_float_wrapped():
"""Float scalars are wrapped in torch.tensor."""
result = _ensure_tensor_values({"gain": 0.5})
assert isinstance(result["gain"], torch.Tensor)
assert result["gain"].item() == pytest.approx(0.5)
def test_list_converted():
"""Lists of numbers are converted to tensors."""
result = _ensure_tensor_values({"ids": [1, 2, 3]})
assert isinstance(result["ids"], torch.Tensor)
assert list(result["ids"]) == [1, 2, 3]
def test_non_tensorizable_dropped():
"""Values that cannot be tensorized are dropped with warning."""
result = _ensure_tensor_values({"ok": torch.randn(5), "bad": {"nested": "dict"}})
assert "ok" in result
assert "bad" not in result
def test_mixed_payload():
"""Mixed tensor + scalar + list payload is properly sanitized."""
payload = {
"audio": torch.randn(100),
"sr": 24000,
"codes": [1, 2, 3],
"hidden": torch.randn(10, 64),
}
result = _ensure_tensor_values(payload)
assert len(result) == 4
for v in result.values():
assert isinstance(v, torch.Tensor)
+2 -2
View File
@@ -81,7 +81,7 @@ def test_multimodal_payload_and_completion_output():
assert p is not None
assert "waveform" in p.tensors and torch.equal(p.primary_tensor, data["waveform"])
assert p.metadata["sample_rate"] == 16000
assert not p.is_empty and len(p) == 1
assert not p.is_empty and len(p) == 2 # 1 tensor + 1 metadata
# None/empty returns None
assert MultimodalPayload.from_dict(None) is None
@@ -125,7 +125,7 @@ def test_output_modality_printed_examples(capsys):
print(f" tensors keys : {list(p.tensors.keys())}")
print(f" primary_tensor: shape={p.primary_tensor.shape}, dtype={p.primary_tensor.dtype}")
print(f" metadata : {p.metadata}")
print(f" is_empty={p.is_empty}, len={len(p)}")
print(f" is_empty={p.is_empty}, len={len(p)}") # len = tensors + metadata
print(f" from_dict(None) -> {MultimodalPayload.from_dict(None)}")
print(f" from_dict({{}}) -> {MultimodalPayload.from_dict({})}")
+61
View File
@@ -209,3 +209,64 @@ def test_cumulative_token_ids_is_a_copy():
s = _make_state(RequestOutputKind.DELTA)
out = s._new_completion_output([42], None, None)
assert out.cumulative_token_ids is not _DETOK.output_token_ids
# ---------------------------------------------------------------------------
# No-detokenizer regression tests (Concern 2)
# ---------------------------------------------------------------------------
_NO_DETOK_STATE_KWARGS = {
**_DEFAULT_STATE_KWARGS,
"detokenizer": None,
"logprobs_processor": None,
}
def _make_no_detok_state(output_kind: RequestOutputKind):
return OmniRequestState(**_NO_DETOK_STATE_KWARGS, output_kind=output_kind)
def test_no_detokenizer_completion_output():
"""_new_completion_output works when detokenizer is None (generation stages)."""
s = _make_no_detok_state(RequestOutputKind.CUMULATIVE)
s.add_multimodal_tensor(torch.randn(10), mm_type=AUDIO)
out = s._new_completion_output([1, 2], None, None)
# Should still produce output with empty text and the token_ids as-is
assert out.text == ""
assert list(out.token_ids) == [1, 2]
assert hasattr(out, "cumulative_token_ids")
assert list(out.cumulative_token_ids) == [1, 2]
# Multimodal data should be attached
assert AUDIO in out.multimodal_output
def test_no_detokenizer_make_request_output():
"""make_request_output works without detokenizer when multimodal data is present."""
s = _make_no_detok_state(RequestOutputKind.CUMULATIVE)
s.add_multimodal_tensor(torch.randn(10), mm_type=AUDIO)
result = s.make_request_output([], None, FinishReason.STOP, None)
assert result is not None
assert not isinstance(result, PoolingRequestOutput)
assert AUDIO in result.outputs[0].multimodal_output
def test_no_detokenizer_stream_interval_skipped():
"""stream_interval > 1 does not crash when detokenizer is None."""
kwargs = {**_NO_DETOK_STATE_KWARGS, "stream_interval": 5}
s = OmniRequestState(**kwargs, output_kind=RequestOutputKind.DELTA)
s.add_multimodal_tensor(torch.randn(10), mm_type=AUDIO)
# Should not assert; stream_interval logic is simply skipped
result = s.make_request_output([], None, FinishReason.STOP, None)
assert result is not None
def test_no_detokenizer_final_only():
"""FINAL_ONLY with no detokenizer returns None until finish."""
s = _make_no_detok_state(RequestOutputKind.FINAL_ONLY)
s.add_multimodal_tensor(torch.randn(10), mm_type=AUDIO)
# Non-finish step should return None
assert s.make_request_output([], None, None, None) is None
# Finish step should return output
result = s.make_request_output([], None, FinishReason.STOP, None)
assert result is not None
assert AUDIO in result.outputs[0].multimodal_output
@@ -0,0 +1,88 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Msgpack roundtrip tests for OmniEngineCoreOutputs with multimodal_output.
Validates that tensor-only payloads survive msgspec encode/decode and that
non-tensor values fail decoding (enforcing the wire invariant).
"""
import pytest
import torch
from vllm.v1.serial_utils import MsgpackDecoder, MsgpackEncoder
from vllm_omni.engine import OmniEngineCoreOutput, OmniEngineCoreOutputs
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _roundtrip(outputs: OmniEngineCoreOutputs) -> OmniEngineCoreOutputs:
encoder = MsgpackEncoder()
encoded = encoder.encode(outputs)
decoder = MsgpackDecoder(OmniEngineCoreOutputs)
return decoder.decode(encoded)
def test_tensor_only_roundtrip():
"""Tensor-only multimodal_output survives msgpack roundtrip."""
audio = torch.randn(16000)
sr = torch.tensor(24000)
eco = OmniEngineCoreOutput(
request_id="req-1",
new_token_ids=[1, 2, 3],
finish_reason=None,
multimodal_output={"audio": audio, "sr": sr},
)
decoded = _roundtrip(OmniEngineCoreOutputs(outputs=[eco]))
assert len(decoded.outputs) == 1
out = decoded.outputs[0]
assert out.request_id == "req-1"
assert isinstance(out.multimodal_output, dict)
assert torch.allclose(out.multimodal_output["audio"], audio)
assert out.multimodal_output["sr"].item() == 24000
def test_empty_multimodal_roundtrip():
"""None multimodal_output roundtrips correctly."""
eco = OmniEngineCoreOutput(
request_id="req-2",
new_token_ids=[4],
finish_reason=None,
multimodal_output=None,
)
decoded = _roundtrip(OmniEngineCoreOutputs(outputs=[eco]))
assert decoded.outputs[0].multimodal_output is None
def test_multiple_tensor_keys_roundtrip():
"""Multiple tensor keys survive roundtrip."""
hidden = torch.randn(10, 4096)
codes = torch.randint(0, 1024, (1, 100))
eco = OmniEngineCoreOutput(
request_id="req-3",
new_token_ids=[],
finish_reason=None,
multimodal_output={"hidden": hidden, "codes": codes},
)
decoded = _roundtrip(OmniEngineCoreOutputs(outputs=[eco]))
out = decoded.outputs[0]
assert torch.allclose(out.multimodal_output["hidden"], hidden)
assert torch.equal(out.multimodal_output["codes"], codes)
def test_non_tensor_value_fails_decode():
"""Non-tensor values in multimodal_output cause decode failure.
This validates that the wire field must be tensor-only and that
the _ensure_tensor_values boundary in model runners is necessary.
"""
eco = OmniEngineCoreOutput(
request_id="req-bad",
new_token_ids=[],
finish_reason=None,
multimodal_output={"audio": torch.randn(10), "sr": 24000}, # scalar!
)
encoder = MsgpackEncoder()
encoded = encoder.encode(OmniEngineCoreOutputs(outputs=[eco]))
decoder = MsgpackDecoder(OmniEngineCoreOutputs)
with pytest.raises(Exception):
decoder.decode(encoded)