Files
vllm-omni/tests/worker_v2/test_omni_generation_model_runner.py
T
Taichang ZhouandClaude Opus 4.8 9678ae58cf [MR V2] Migrate to Model Runner V2
Squashed migration rebased onto current main. Collapses the 46-commit
dev/migrate-MR-v2 history (10 merge commits + superseded pre-rebase work)
into a single commit, so the diff reflects only the genuine MR-V2 feature
(worker_v2/ runners + model_states, omni schedulers, stage engine pipeline,
pooling_output serialization bridge). Content already merged to main
separately (step_audio2, tts_adapters, npu/_310p, etc.) drops out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 07:19:40 +00:00

261 lines
9.4 KiB
Python

"""Tests for OmniGenerationModelRunner.sample_tokens (V2).
Covers the core pooler_output construction paths via _build_pooler_output:
- OmniOutput with batched tensor multimodal_outputs → per-request slicing
- OmniOutput with list multimodal_outputs → direct mapping (including None)
- OmniOutput with dict scalar values → broadcast to all requests
- None model output → returns None
- Non-dict multimodal_outputs → [None] * num_reqs
- sampled_token_ids always emits empty lists per request (no token sampling)
- req_states.num_computed_tokens updated to prompt_len after sample_tokens
"""
import unittest
from unittest.mock import MagicMock
import numpy as np
import torch
from vllm_omni.model_executor.models.output_templates import OmniOutput
from vllm_omni.outputs import OmniModelRunnerOutput
pytestmark = []
class _FakeInputBatch:
"""Minimal input batch for sample_tokens."""
def __init__(self, num_reqs: int = 1, req_ids: list[str] | None = None):
self.num_reqs = num_reqs
self.req_ids = req_ids or [f"req-{i}" for i in range(num_reqs)]
self.idx_mapping_np = np.arange(num_reqs, dtype=np.int32)
class _FakeStagedField:
"""Minimal mock for req_states fields that support staged writes."""
def __init__(self, data: np.ndarray):
self.np = data
self._staged: list[tuple[int, int]] = []
def stage_write_elem(self, idx: int, value: int) -> None:
self._staged.append((idx, value))
def apply_write(self) -> None:
for idx, value in self._staged:
self.np[idx] = value
self._staged.clear()
class _FakeNpField:
"""Minimal mock for req_states fields with .np attribute."""
def __init__(self, data: np.ndarray):
self.np = data
def _make_omni_output(multimodal_outputs: dict | None = None) -> OmniOutput:
"""Create an OmniOutput with given multimodal_outputs."""
return OmniOutput(
text_hidden_states=torch.zeros(1),
multimodal_outputs=multimodal_outputs,
)
def _make_runner(
model_output,
num_reqs: int = 1,
prompt_len: int = 10,
):
"""Build a minimal OmniGenerationModelRunner for sample_tokens testing."""
from vllm_omni.worker_v2.omni_generation_model_runner import (
OmniGenerationModelRunner,
)
runner = object.__new__(OmniGenerationModelRunner)
runner.device = torch.device("cpu")
mc = MagicMock()
del mc.eos_token_id
mc.hf_text_config = None
runner.model_config = mc
runner.postprocess = lambda *a, **kw: None
input_batch = _FakeInputBatch(num_reqs)
runner._gen_model_output = model_output
runner._gen_input_batch = input_batch
runner._gen_kv_connector_output = None
runner.execute_model_state = None
req_states = MagicMock()
req_states.prompt_len = _FakeNpField(
np.full(num_reqs, prompt_len, dtype=np.int32),
)
req_states.num_computed_tokens = _FakeStagedField(
np.zeros(num_reqs, dtype=np.int32),
)
runner.req_states = req_states
return runner
class TestSampleTokensTensorOutput(unittest.TestCase):
def test_single_request(self):
from vllm_omni.worker_v2.omni_generation_model_runner import OmniGenerationModelRunner
output = _make_omni_output({"model_outputs": torch.randn(1, 4, 8)})
runner = _make_runner(output, num_reqs=1)
result = OmniGenerationModelRunner.sample_tokens(runner)
assert isinstance(result, OmniModelRunnerOutput)
assert len(result.pooler_output) == 1
assert result.pooler_output[0]["model_outputs"].shape == (4, 8)
def test_multi_request(self):
from vllm_omni.worker_v2.omni_generation_model_runner import OmniGenerationModelRunner
output = _make_omni_output({"model_outputs": torch.randn(3, 2, 5)})
runner = _make_runner(output, num_reqs=3)
result = OmniGenerationModelRunner.sample_tokens(runner)
assert len(result.pooler_output) == 3
for i in range(3):
assert result.pooler_output[i]["model_outputs"].shape == (2, 5)
class TestSampleTokensListOutput(unittest.TestCase):
def test_list_of_tensors(self):
from vllm_omni.worker_v2.omni_generation_model_runner import OmniGenerationModelRunner
output = _make_omni_output({"model_outputs": [torch.randn(3, 2)]})
runner = _make_runner(output, num_reqs=1)
result = OmniGenerationModelRunner.sample_tokens(runner)
assert len(result.pooler_output) == 1
assert result.pooler_output[0]["model_outputs"].shape == (3, 2)
def test_list_with_none(self):
from vllm_omni.worker_v2.omni_generation_model_runner import OmniGenerationModelRunner
output = _make_omni_output({"model_outputs": [None]})
runner = _make_runner(output, num_reqs=1)
result = OmniGenerationModelRunner.sample_tokens(runner)
assert len(result.pooler_output) == 1
assert result.pooler_output[0]["model_outputs"] is None
class TestSampleTokensDictOutput(unittest.TestCase):
def test_dict_with_batched_tensor(self):
from vllm_omni.worker_v2.omni_generation_model_runner import OmniGenerationModelRunner
output = _make_omni_output({"audio": torch.randn(2, 16000), "sr": 24000})
runner = _make_runner(output, num_reqs=2)
result = OmniGenerationModelRunner.sample_tokens(runner)
assert len(result.pooler_output) == 2
assert result.pooler_output[0]["audio"].shape == (16000,)
assert result.pooler_output[1]["audio"].shape == (16000,)
assert result.pooler_output[0]["sr"] == 24000
def test_dict_with_list_values(self):
from vllm_omni.worker_v2.omni_generation_model_runner import OmniGenerationModelRunner
output = _make_omni_output({"chunks": [torch.randn(10), torch.randn(20)]})
runner = _make_runner(output, num_reqs=2)
result = OmniGenerationModelRunner.sample_tokens(runner)
assert len(result.pooler_output) == 2
assert result.pooler_output[0]["chunks"].shape == (10,)
assert result.pooler_output[1]["chunks"].shape == (20,)
class TestSampleTokensNoneOutput(unittest.TestCase):
def test_none_model_output(self):
from vllm_omni.worker_v2.omni_generation_model_runner import OmniGenerationModelRunner
runner = _make_runner(None, num_reqs=1)
result = OmniGenerationModelRunner.sample_tokens(runner)
assert result is None
class TestNonDictMultimodalOutputs(unittest.TestCase):
"""When multimodal_outputs is None or non-dict, pooler_output is [None]*num_reqs."""
def test_none_multimodal_outputs(self):
from vllm_omni.worker_v2.omni_generation_model_runner import OmniGenerationModelRunner
output = _make_omni_output(multimodal_outputs=None)
runner = _make_runner(output, num_reqs=2)
result = OmniGenerationModelRunner.sample_tokens(runner)
assert len(result.pooler_output) == 2
assert result.pooler_output[0] is None
assert result.pooler_output[1] is None
class TestSampledTokenIds(unittest.TestCase):
def test_empty_sampled_token_ids_per_request(self):
"""Generation models emit empty sampled_token_ids (no token sampling)."""
from vllm_omni.worker_v2.omni_generation_model_runner import OmniGenerationModelRunner
output = _make_omni_output({"model_outputs": torch.randn(3, 2)})
runner = _make_runner(output, num_reqs=3)
result = OmniGenerationModelRunner.sample_tokens(runner)
assert len(result.sampled_token_ids) == 3
for ids in result.sampled_token_ids:
assert ids == []
class TestReqStatesUpdate(unittest.TestCase):
"""Verify that sample_tokens marks all tokens as computed."""
def test_num_computed_tokens_set_to_prompt_len(self):
from vllm_omni.worker_v2.omni_generation_model_runner import OmniGenerationModelRunner
prompt_len = 15
output = _make_omni_output({"model_outputs": torch.randn(2, 4)})
runner = _make_runner(output, num_reqs=2, prompt_len=prompt_len)
OmniGenerationModelRunner.sample_tokens(runner)
for i in range(2):
assert runner.req_states.num_computed_tokens.np[i] == prompt_len
class TestMultimodalOutputsPassthrough(unittest.TestCase):
"""multimodal_outputs is a per-request list (tensor-only) on OmniModelRunnerOutput.
OmniGenerationScheduler indexes it as mm_outputs[req_index], so it must be a
list (not the raw dict). Each entry mirrors the per-request pooler payload.
"""
def test_multimodal_outputs_on_result(self):
from vllm_omni.worker_v2.omni_generation_model_runner import OmniGenerationModelRunner
mm = {"audio": [torch.randn(10)]}
output = _make_omni_output(mm)
runner = _make_runner(output, num_reqs=1)
result = OmniGenerationModelRunner.sample_tokens(runner)
assert isinstance(result.multimodal_outputs, list)
assert len(result.multimodal_outputs) == 1
assert "audio" in result.multimodal_outputs[0]
assert torch.is_tensor(result.multimodal_outputs[0]["audio"])
def test_none_multimodal_outputs_becomes_empty_dict(self):
from vllm_omni.worker_v2.omni_generation_model_runner import OmniGenerationModelRunner
output = _make_omni_output(multimodal_outputs=None)
runner = _make_runner(output, num_reqs=1)
result = OmniGenerationModelRunner.sample_tokens(runner)
# No multimodal data -> one empty dict per request.
assert result.multimodal_outputs == [{}]
if __name__ == "__main__":
unittest.main()