Merge main into dev/vllm-align for vLLM 0.31 release

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
This commit is contained in:
tzhouam committed 2026-10-03 21:05:17 +08:00
commit 008fa25263
252 files changed
+27589 -1237

No files matched your search

@@ -45,6 +45,8 @@ def _check_prefill():
kvconfig = KVCacheConfig(num_blocks=1024, kv_cache_tensors=[], kv_cache_groups=[KVCacheGroupSpec(["attn"], spec)])
state = OmniModelState.__new__(OmniModelState)
state.model_config = SimpleNamespace(max_model_len=256)
state.vllm_config = c
state.max_model_len = 256
state.supports_mm_inputs = False
with set_current_vllm_config(c):
group = AttentionGroup(FlashAttentionBackend, ["attn"], spec, 0)
@@ -0,0 +1,99 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Real FA3 graph replay must preserve the decode GQA layout."""
from dataclasses import dataclass, replace
import numpy as np
import pytest
import torch
from transformers import Qwen2Config
from vllm.config import ModelConfig, VllmConfig, set_current_vllm_config
from vllm.config.compilation import CUDAGraphMode
from vllm.v1.attention.backends.flash_attn import FlashAttentionBackend, FlashAttentionImpl
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig, KVCacheGroupSpec
from vllm.v1.worker.gpu.input_batch import InputBatch, InputBuffers
from vllm.v1.worker.utils import AttentionGroup
from tests.helpers.mark import hardware_test
from vllm_omni.worker_v2.model_states.omni_model_state import OmniModelState
pytestmark = [pytest.mark.core_model]
@dataclass
class _AttentionScales:
_q_scale: torch.Tensor
_k_scale: torch.Tensor
_v_scale: torch.Tensor
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.parametrize("graph_mode", [CUDAGraphMode.FULL_AND_PIECEWISE, CUDAGraphMode.FULL_DECODE_ONLY])
def test_full_decode_graph_matches_fa3_aot_schedule(graph_mode, tmp_path):
if not torch.cuda.is_available() or torch.cuda.get_device_capability()[0] < 9:
pytest.skip("requires FA3 on Hopper or newer")
torch.manual_seed(17)
device = torch.device("cuda:0")
c = VllmConfig()
Qwen2Config(
architectures=["Qwen2ForCausalLM"],
hidden_size=1024,
intermediate_size=2048,
num_hidden_layers=1,
num_attention_heads=16,
num_key_value_heads=2,
max_position_embeddings=2048,
).save_pretrained(tmp_path)
c.model_config = ModelConfig(model=str(tmp_path), skip_tokenizer_init=True, dtype="bfloat16")
c.scheduler_config.max_num_seqs = 64
c.compilation_config.cudagraph_mode = graph_mode
c.compilation_config.max_cudagraph_capture_size = 128
c.attention_config.flash_attn_version = 3
spec = FullAttentionSpec(block_size=16, num_kv_heads=2, head_size=64, dtype=torch.bfloat16)
kvconfig = KVCacheConfig(num_blocks=1024, kv_cache_tensors=[], kv_cache_groups=[KVCacheGroupSpec(["attn"], spec)])
state = OmniModelState.__new__(OmniModelState)
state.vllm_config = c
state.max_model_len = 2048
state.supports_mm_inputs = False
with set_current_vllm_config(c):
group = AttentionGroup(FlashAttentionBackend, ["attn"], spec, 0)
group.create_metadata_builders(c, device)
impl = FlashAttentionImpl(16, 64, 64**-0.5, 2, None, None, "auto")
assert impl.vllm_flash_attn_version == 3
buffers = InputBuffers(64, 128, device)
batch = InputBatch.make_dummy(8, 8, buffers, max_query_len=None)
tables = torch.arange(1024, dtype=torch.int32, device=device).reshape(8, 128)
slots = torch.zeros((1, 8), dtype=torch.int64, device=device)
cache = torch.randn(1024, 2, 16, 128, device=device, dtype=torch.bfloat16)
q = torch.randn(8, 16, 64, device=device, dtype=torch.bfloat16)
k = v = torch.empty(8, 2, 64, device=device, dtype=torch.bfloat16)
layer = _AttentionScales(
_q_scale=torch.ones((), device=device),
_k_scale=torch.ones((), device=device),
_v_scale=torch.ones((), device=device),
)
md = state.prepare_attn(batch, CUDAGraphMode.FULL, (tables,), slots, [[group]], kvconfig, for_capture=True)[
"attn"
]
out = torch.full_like(q, float("nan"))
stream = torch.cuda.Stream()
stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(stream):
for _ in range(3):
impl.forward(layer, q, k, v, cache, md, out)
stream.synchronize()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph, stream=stream):
impl.forward(layer, q, k, v, cache, md, out)
lengths = np.arange(37, 45, dtype=np.int32)
batch.seq_lens.copy_(torch.tensor(lengths, device=device))
runtime = replace(batch, max_query_len=1, seq_lens_cpu_upper_bound=torch.tensor(lengths))
eager_md = state.prepare_attn(runtime, CUDAGraphMode.FULL, (tables,), slots, [[group]], kvconfig)["attn"]
expected = torch.empty_like(q)
impl.forward(layer, q, k, v, cache, eager_md, expected)
state.prepare_attn(runtime, CUDAGraphMode.FULL, (tables,), slots, [[group]], kvconfig)
graph.replay()
torch.accelerator.synchronize()
assert md.max_query_len == 1
torch.testing.assert_close(out, expected, atol=0.002, rtol=0.002)
@@ -235,3 +235,19 @@ def test_sender_copy_failure_reports_all_prepared_routes():
assert client == 3
assert [(o.request_id, o.finish_reason) for o in outputs.outputs] == [("r", FinishReason.ERROR)]
assert output_queue.empty()
def test_final_codec_delivery_omits_upstream_marker_and_freezes_route():
from vllm_omni.data_entry_keys import FIRST_AUDIO_KEY
outputs: queue.Queue = queue.Queue()
scheduler = _scheduler(r=3)
delivery = engine_output_queue_sink(outputs, scheduler, upstream_first_audio=False).prepare(["r"])
scheduler.requests.clear()
pcm = torch.ones(4)
delivery(["r"], [pcm], torch.tensor(24000))
client, batch = outputs.get_nowait()
assert client == 3
assert FIRST_AUDIO_KEY not in batch.outputs[0].multimodal_output
assert batch.outputs[0].multimodal_output["model_outputs"] is pcm
assert batch.outputs[0].finish_reason is None
+21
View File
@@ -0,0 +1,21 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import pytest
import torch
from vllm_omni.utils.device_copy import index_to_device
pytestmark = [pytest.mark.core_model, pytest.mark.cuda]
def test_index_staging_survives_later_copies_and_graph_replay():
stream = torch.cuda.Stream()
graph = torch.cuda.CUDAGraph()
with torch.cuda.stream(stream):
with torch.cuda.graph(graph, stream=stream):
captured = index_to_device([17] * 6, "cuda")
for _ in range(32):
index_to_device([99] * 6, "cuda")
graph.replay()
torch.accelerator.synchronize()
assert captured.tolist() == [17] * 6
@@ -18,6 +18,7 @@ from vllm.v1.worker.gpu.sample.prompt_logprob import PromptLogprobsWorker
import vllm_omni.worker_v2.omni_ar_model_runner as omni_ar_model_runner
from tests.helpers.mark import hardware_test
from vllm_omni.model_executor.output_snapshot import PackedOutputSnapshot
from vllm_omni.worker_v2.omni_ar_model_runner import OmniARModelRunner, OmniAsyncOutput
from vllm_omni.worker_v2.output_snapshot import pack_output_snapshot
@@ -28,6 +29,9 @@ class _FakeStream:
def wait_stream(self, _stream) -> None:
pass
def wait_event(self, _event) -> None:
pass
class _FakeEvent:
def record(self, _stream) -> None:
@@ -224,8 +228,10 @@ def test_chunked_fixed_token_scores_are_copied_from_cuda() -> None:
def test_async_mm_snapshot_owns_output_until_copy_finishes() -> None:
runner = OmniARModelRunner.__new__(OmniARModelRunner)
runner.model_config = SimpleNamespace(async_chunk=True)
runner.model = SimpleNamespace()
runner._async_mm_snapshot_slots, runner._async_mm_snapshot_events = [{}], [None]
runner._async_mm_snapshot_pending, runner._async_mm_snapshot_cursor = [False], 0
runner._last_multimodal_snapshot_slot = None
waited: list[object] = []
runner.main_stream = SimpleNamespace(wait_event=waited.append)
source = torch.tensor([[7, 8]], dtype=torch.long)
@@ -243,6 +249,35 @@ def test_async_mm_snapshot_owns_output_until_copy_finishes() -> None:
assert waited == [copy_event]
def test_producer_snapshot_is_not_repacked() -> None:
runner = OmniARModelRunner.__new__(OmniARModelRunner)
runner.model_config = SimpleNamespace(async_chunk=True)
runner.model = SimpleNamespace()
runner._async_mm_snapshot_slots, runner._async_mm_snapshot_events = [{}], [None]
runner._async_mm_snapshot_pending, runner._async_mm_snapshot_cursor = [False], 0
runner._last_multimodal_snapshot_slot = None
source = torch.tensor([[7, 8]], dtype=torch.float32)
snapshot = pack_output_snapshot({"audio": source}, {}, max_buckets=1)
assert isinstance(snapshot, PackedOutputSnapshot)
retained = runner._retain_multimodal_outputs(snapshot)
assert retained is snapshot
assert runner._last_multimodal_snapshot_slot is None
assert runner._async_mm_snapshot_pending == [False]
def test_fresh_per_step_outputs_are_not_repacked() -> None:
runner = OmniARModelRunner.__new__(OmniARModelRunner)
runner.model_config = SimpleNamespace(async_chunk=True)
runner.model = SimpleNamespace(mm_outputs_fresh_per_step=True)
runner._last_multimodal_snapshot_slot = None
outputs = {"model_outputs": torch.ones(2, 4)}
assert runner._retain_multimodal_outputs(outputs) is outputs
assert runner._last_multimodal_snapshot_slot is None
def test_snapshot_slots_bounded_by_shape_and_packed_grouping_isolation(monkeypatch) -> None:
slot: dict[tuple[Any, ...], torch.Tensor] = {}
omni_ar_model_runner._copy_mm_to_snapshot_slot(torch.ones(1, 2), slot)
@@ -399,6 +434,231 @@ def test_request_reference_codes_preserve_local_axis(prefill_first, padded):
assert torch.equal(outputs[i]["codes.audio"], codes[offsets[i] : offsets[i + 1]])
def test_stream_audio_cancel_rejects_inflight_output_and_id_reuse() -> None:
from vllm_omni.worker_v2.streaming_audio import StreamingAudioBuffer
buffer = StreamingAudioBuffer(samples_per_frame=4, chunk_frames=3)
cancelled = buffer.add("request")
buffer.finish({"request"})
replacement = buffer.add("request")
assert cancelled.push(torch.ones(4), False) is None
assert not cancelled.pending
assert replacement.push(torch.full((4,), 2.0), False).tolist() == [2.0] * 4
buffer.finish({"request"})
assert not buffer.requests
def test_stream_audio_length_end_flushes_first_and_partial_chunks() -> None:
from vllm_omni.worker_v2.streaming_audio import StreamingAudioBuffer, StreamingAudioOutput
buffer = StreamingAudioBuffer(samples_per_frame=4, chunk_frames=3)
state = buffer.add("request")
output = StreamingAudioOutput(
torch.ones(1, 4),
torch.tensor([True]),
torch.tensor(24000),
None,
np.array([0, 1]),
[state],
np.array([True]),
)
assert output.get_output()[0]["model_outputs"].numel() == 4
assert not state.active
state = buffer.add("another")
assert state.push(torch.ones(4), False).numel() == 4
assert state.push(torch.ones(4), False) is None
assert state.push(torch.ones(4), True).numel() == 8
@pytest.mark.parametrize("dtype", [torch.float16, torch.float32, torch.float64, torch.bfloat16])
def test_stream_audio_batch_views_retain_pcm_after_output_release(dtype):
from vllm_omni.worker_v2.streaming_audio import StreamingAudioBuffer, StreamingAudioOutput
buffer = StreamingAudioBuffer(samples_per_frame=4, chunk_frames=3)
states = [buffer.add("a"), buffer.add("b")]
expected: list[list[torch.Tensor]] = [[], []]
emitted: list[list[torch.Tensor]] = [[], []]
for step in range(5):
# Mixed prefill/decode spans, invalid rows, and a final partial chunk.
wav = (torch.arange(16).reshape(4, 4) + 100 * step).to(dtype)
valid = torch.tensor([False, True, True, step != 4])
output = StreamingAudioOutput(
wav,
valid,
torch.tensor(24000),
None,
np.array([0, 3, 4]),
states,
np.array([step == 4, step == 4]),
)
for i, (start, end) in enumerate([(0, 3), (3, 4)]):
expected[i].append(wav[start:end][valid[start:end]].reshape(-1).clone())
for i, payload in enumerate(output.get_output()):
if payload is not None:
emitted[i].append(payload["model_outputs"])
del output, wav # Pending views must keep the old host allocation alive.
for i in range(2):
assert torch.equal(torch.cat(emitted[i]), torch.cat(expected[i]))
assert torch.cat(emitted[i]).dtype == dtype
assert not states[i].active and not states[i].pending
assert states[i].pending_samples == 0
def test_stream_audio_numpy_direct_first_cancel_and_mixed_fallback():
from vllm_omni.worker_v2.streaming_audio import StreamingAudioBuffer
buffer = StreamingAudioBuffer(samples_per_frame=4, chunk_frames=3)
state = buffer.add("r")
state.accept_first_audio()
assert state.push(np.arange(8, dtype=np.float32), False) is None
assert state.pending_samples == 4
# A dtype fallback can arrive while a NumPy frame is pending.
chunk = state.push(torch.arange(8, 12, dtype=torch.bfloat16), True)
assert chunk.tolist() == list(range(4, 12))
assert state.pending_samples == 0
buffer.finish({"r"})
replacement = buffer.add("r")
assert state.push(np.ones(4, dtype=np.float32), False) is None
assert replacement.push(np.full(4, 99, dtype=np.float32), False).tolist() == [99] * 4
replacement.push(np.ones(4, dtype=np.float32), False)
assert replacement.pending_samples == 4
buffer.finish({"r"})
assert replacement.pending_samples == 0 and not replacement.pending
def test_stream_audio_prefill_length_end_reaches_output(mocker) -> None:
from vllm.v1.worker.gpu.input_batch import InputBatch
from vllm.v1.worker.gpu.states import RequestState
from vllm_omni.worker_v2.model_states.eager_mtp import EagerMTPState
from vllm_omni.worker_v2.streaming_audio import StreamingAudioBuffer
owner = mocker.Mock()
owner.vllm_config.model_config.max_model_len = 4096
owner._stream_decode_event = None
eager = EagerMTPState(owner)
eager._audio_buffer = StreamingAudioBuffer(4, 25)
eager._audio_buffer.add("request")
eager._frame_requests = {"request"}
batch = InputBatch.__new__(InputBatch)
batch.num_reqs, batch.req_ids = 1, ["request"]
batch.num_computed_tokens_np = np.array([0])
batch.num_scheduled_tokens = np.array([3])
batch.idx_mapping_np = np.array([0])
batch.query_start_loc_np = np.array([0, 3])
batch.is_prefilling_np = np.array([True])
states = RequestState.__new__(RequestState)
states.max_seq_len = np.array([4])
mm = {
"model_outputs": torch.ones(3, 4),
"meta": {"codec_frame_valid": torch.tensor([0, 0, 1])},
"sr": [torch.tensor(24000)],
}
output = eager.prepare_audio_output(batch, states, mm)
assert output.get_output()[0]["model_outputs"].numel() == 4
assert "model_outputs" not in mm
def test_stream_audio_preemption_preserves_position_rng_and_pending_pcm(mocker) -> None:
from vllm_omni.worker_v2.model_states.eager_mtp import EagerMTPState, TalkerInputs
from vllm_omni.worker_v2.streaming_audio import StreamingAudioBuffer
owner = mocker.Mock()
owner._stream_pos = {"request": 37}
owner._eager_embeds = torch.arange(18).reshape(6, 3).float()
owner._eager_ready = {}
owner.intermediate_buffer.buffers = [{} for _ in range(6)]
generator = torch.Generator().manual_seed(42)
owner._mtp_generators = {"request": generator}
saved = [torch.arange(6)]
owner.model.stream_decoder.save_slot.return_value = saved
eager = EagerMTPState(owner)
eager._talker_inputs["request"] = TalkerInputs(torch.ones(20, 3), 10)
eager._side_stream = mocker.Mock()
eager._audio_buffer = StreamingAudioBuffer(4, 25)
request = eager._audio_buffer.add("request")
request.push(torch.ones(4), False)
request.push(torch.ones(4), False)
eager.suspend_audio("request", 2)
owner._stream_pos.clear()
owner._mtp_generators.clear()
eager.resume_audio("request", 5)
assert owner._stream_pos["request"] == 37
assert owner._mtp_generators["request"] is generator
assert eager._restore_audio["request"] is saved
assert sum(part.numel() for part in request.pending) == 4
# A second preemption can happen before the restored slot is used.
eager.suspend_audio("request", 5)
owner.model.stream_decoder.save_slot.assert_called_once_with(2)
eager.finish_audio({"request"})
assert not eager._suspended_audio and not eager._restore_audio
assert not eager._audio_buffer.requests and not request.active
def test_stream_audio_replay_uses_exact_inputs_and_consumes_last_frame_once(mocker):
from vllm_omni.worker_v2.model_states.eager_mtp import EagerMTPState, TalkerInputs
owner = mocker.Mock()
owner._eager_embeds = torch.tensor([[20.0, 30.0]])
owner.model.preprocess.return_value = (None, None, {"mtp_inputs": (None, torch.tensor([[2.0, 3.0]]))})
owner.intermediate_buffer.buffers = [{"req_id": "request"}]
eager = EagerMTPState(owner)
eager._restore_audio["request"] = []
history = torch.arange(8).reshape(4, 2).float()
eager._talker_inputs["request"] = TalkerInputs(history, 4)
first = torch.empty(2, 2)
assert eager.replay_inputs("request", 0, 0, torch.ones(2), first)
torch.testing.assert_close(first, history[:2])
owner.model.preprocess.assert_not_called()
last = torch.empty(3, 2)
assert eager.replay_inputs("request", 0, 2, torch.ones(3), last)
torch.testing.assert_close(last, torch.cat((history[2:], torch.tensor([[22.0, 33.0]]))))
owner.model.preprocess.assert_called_once()
with pytest.raises(RuntimeError, match="exceeds saved inputs"):
eager.replay_inputs("request", 0, 4, torch.ones(2), torch.empty(2, 2))
def test_stream_reference_priming_groups_lengths_without_padding_or_slot_aliasing(mocker):
from vllm_omni.model_executor.models.qwen3_tts.qwen3_tts_talker import Qwen3TTSTalkerForConditionalGeneration
primes = [(4, torch.full((26, 2), 4)), (1, torch.full((3, 2), 1)), (7, torch.full((26, 2), 7))]
calls = []
def stream(codes, slots, pos):
calls.append((codes.clone(), slots.clone(), pos.clone()))
decoder = mocker.Mock(side_effect=stream, device=torch.device("cpu"))
model = mocker.Mock(stream_decoder=decoder, stream_prime_graphs=None, stream_chunk_frames=25)
Qwen3TTSTalkerForConditionalGeneration.prime_stream_decoder(model, primes)
assert len(calls) == 3
for (codes, slots, pos), frames, indices, position in zip(
calls, [25, 1, 3], [[4, 7], [4, 7], [1]], [0, 25, 0], strict=True
):
assert codes.shape == (len(indices), frames, 2)
assert codes.dtype == slots.dtype == pos.dtype == torch.int32
assert slots.tolist() == indices and pos.tolist() == [position] * len(indices)
for row, idx in enumerate(indices):
assert torch.all(codes[row] == idx)
Qwen3TTSTalkerForConditionalGeneration.prime_stream_decoder(model, [])
assert len(calls) == 3
@pytest.mark.parametrize("context_frames", [25, 72])
def test_model_owned_reference_context_respects_configured_boundary(mocker, context_frames):
from vllm_omni.model_executor.models.qwen3_tts.qwen3_tts_talker import Qwen3TTSTalkerForConditionalGeneration
model = mocker.Mock(stream_ref_context_frames=context_frames, talker_config=mocker.Mock(num_code_groups=16))
reference = torch.arange(100 * 16).reshape(100, 16)
info = {"codes": {"ref": reference}}
got = Qwen3TTSTalkerForConditionalGeneration.get_stream_ref_context(model, info)
torch.testing.assert_close(got, reference[-context_frames:])
assert info["codes"]["ref"] is reference
assert Qwen3TTSTalkerForConditionalGeneration.get_stream_ref_context(model, {}) is None
assert (
Qwen3TTSTalkerForConditionalGeneration.get_stream_ref_context(model, {"codes": {"ref": torch.empty(0)}}) is None
)
@pytest.mark.parametrize("streaming", [False, True])
def test_model_owned_audio_finalizer_runs_after_copy_without_hidden(monkeypatch, streaming):
monkeypatch.setattr(torch.cuda, "set_stream", lambda _stream: None)
@@ -434,6 +694,136 @@ def test_model_owned_audio_finalizer_runs_after_copy_without_hidden(monkeypatch,
assert (result.pooler_output is None) == streaming
def test_stream_audio_history_replays_exact_inputs_after_batch_reordering(mocker):
from vllm.v1.worker.gpu.input_batch import InputBatch
from vllm_omni.worker_v2.model_states.eager_mtp import EagerMTPState
owner = mocker.Mock()
owner.vllm_config.model_config.max_model_len = 12
eager = EagerMTPState(owner)
first = torch.arange(9, dtype=torch.float32).reshape(3, 3)
second = torch.arange(9, 18, dtype=torch.float32).reshape(3, 3)
expected = {"a": torch.cat((first[:2], second[2:])), "b": torch.cat((first[2:], second[:2]))}
batch = InputBatch.__new__(InputBatch)
batch.req_ids = ["a", "b"]
batch.query_start_loc_np = np.array([0, 2, 3])
batch.num_computed_tokens_np = np.array([0, 0])
eager.record_inputs(batch, first)
batch.req_ids = ["b", "a"]
batch.num_computed_tokens_np = np.array([1, 2])
eager.record_inputs(batch, second)
# Later input-buffer reuse must not change the conditioned replay history.
first.fill_(-1)
second.fill_(-2)
for request_id, wanted in expected.items():
eager._restore_audio[request_id] = []
replayed = torch.empty_like(wanted)
assert eager.replay_inputs(request_id, 0, 0, torch.zeros(3, dtype=torch.long), replayed)
torch.testing.assert_close(replayed, wanted)
@pytest.mark.parametrize("valid", [False, True])
def test_stream_audio_direct_first_requires_only_real_pcm_on_terminal(valid):
from vllm_omni.data_entry_keys import FIRST_AUDIO_REQUIRED_KEY
from vllm_omni.worker_v2.streaming_audio import StreamingAudioBuffer, StreamingAudioOutput
buffer = StreamingAudioBuffer(samples_per_frame=4, chunk_frames=3)
state = buffer.add("r")
state.accept_first_audio()
first = StreamingAudioOutput(
torch.arange(4).reshape(1, 4),
torch.tensor([valid]),
torch.tensor(24000),
None,
np.array([0, 1]),
[state],
np.array([True]),
)
payload = first.get_output()[0]
if valid:
assert payload == {FIRST_AUDIO_REQUIRED_KEY: torch.tensor(True)}
else:
assert payload is None # EOS has no promised first PCM to wait for.
assert not state.active and not state.pending
buffer.finish({"r"})
replacement = buffer.add("r")
assert replacement.push(torch.full((4,), 99), False).tolist() == [99] * 4
def test_stream_audio_direct_first_drops_one_frame_and_retains_partial_suffix():
from vllm_omni.worker_v2.streaming_audio import AudioRequest
state = AudioRequest(samples_per_frame=4, chunk_frames=3)
state.accept_first_audio()
assert state.push(torch.arange(4), False) is None
assert state.first_audio_required and state.emitted
assert state.push(torch.arange(4, 8), False) is None
chunk = state.push(torch.arange(8, 12), True)
assert chunk.tolist() == list(range(4, 12))
assert not state.active and not state.pending
@pytest.mark.parametrize("consumer", ["pcm_only", "finalizer", "extra"])
def test_stream_audio_snapshot_skips_discarded_code_partition(monkeypatch, mocker, consumer):
from vllm.v1.worker.gpu.input_batch import InputBatch
from vllm_omni.worker_v2.output_snapshot import RequestOutputSnapshot
from vllm_omni.worker_v2.streaming_audio import StreamingAudioBuffer, StreamingAudioOutput
monkeypatch.setattr(torch.cuda, "set_stream", lambda _stream: None)
monkeypatch.setattr(
OmniARModelRunner,
"_build_async_chunk_outputs_from_mm",
lambda *args: pytest.fail("in-stage PCM unnecessarily partitioned code outputs"),
)
batch = InputBatch.__new__(InputBatch)
batch.num_reqs = 3
batch.query_start_loc_np = np.array([0, 1, 2, 4])
batch.num_scheduled_tokens = np.array([1, 1, 2])
batch.num_tokens_after_padding = 4
buffer = StreamingAudioBuffer(samples_per_frame=4, chunk_frames=3)
states = [buffer.add(request_id) for request_id in ["frame", "eos", "prefill"]]
wav = torch.arange(16, dtype=torch.float32).reshape(4, 4)
streaming_audio = StreamingAudioOutput(
wav,
torch.tensor([True, False, True, False]),
torch.tensor(24000),
None,
batch.query_start_loc_np.copy(),
states,
np.array([False, False, True]),
)
copy_mm = mocker.spy(omni_ar_model_runner, "_async_copy_mm")
def finalize(payload, counts):
assert payload["codes"]["audio"].shape == (4, 2)
return RequestOutputSnapshot([None] * batch.num_reqs)
finalizer = mocker.Mock(side_effect=finalize) if consumer == "finalizer" else None
extra = ({"extra": torch.tensor([7])}, _FakeEvent()) if consumer == "extra" else None
result = _async_output(
req_ids=["frame", "eos", "prefill"],
sampler_output=SamplerOutput(torch.tensor([[1], [2], [3]]), None, None, None),
num_sampled_tokens=torch.tensor([1, 1, 1]),
multimodal_outputs={"codes": {"audio": torch.ones(4, 2)}},
input_batch=batch,
async_chunk=True,
streaming_audio=streaming_audio,
finalize_multimodal=finalizer,
extra_multimodal_outputs=extra,
).get_output()
assert copy_mm.call_count == {"pcm_only": 0, "finalizer": 1, "extra": 2}[consumer]
if finalizer is not None:
finalizer.assert_called_once()
wav.fill_(-1)
assert result.inter_stage_outputs is None
assert result.multimodal_outputs[0]["model_outputs"].tolist() == [0, 1, 2, 3]
assert result.multimodal_outputs[1] == {}
assert result.multimodal_outputs[2]["model_outputs"].tolist() == [8, 9, 10, 11]
assert not states[1].active and not states[2].active
@pytest.mark.parametrize("streaming", [False, True])
def test_request_owned_snapshot_skips_generic_partition(monkeypatch, streaming):
from vllm_omni.worker_v2.output_snapshot import RequestOutputSnapshot
+100
View File
@@ -4,13 +4,17 @@
graph/eager paths with per-request seed independence, async snapshot ownership."""
from contextlib import nullcontext
from dataclasses import replace
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import numpy as np
import pytest
import torch
from vllm.v1.worker.gpu.input_batch import InputBatch, InputBuffers
from vllm.v1.worker.gpu.mm.encoder_runner import EncoderRunner
from vllm.v1.worker.gpu.model_states.default import DefaultModelState
from vllm.v1.worker.gpu.states import RequestState
from vllm_omni.model_executor.models.output_templates import OmniOutput, OwnedBatchTensor
from vllm_omni.worker_v2.model_states.omni_model_state import OmniModelState, _make_safe_get_rope
@@ -36,6 +40,7 @@ class _DummyInputBatch:
def _make_state(max_num_reqs=4, has_preprocess=False, has_postprocess=False, have_multimodal_outputs=False):
state = object.__new__(OmniModelState)
model = MagicMock()
model.stream_decoder = None
model.has_preprocess = has_preprocess
model.has_postprocess = has_postprocess
model.have_multimodal_outputs = have_multimodal_outputs
@@ -62,6 +67,10 @@ def _make_state(max_num_reqs=4, has_preprocess=False, has_postprocess=False, hav
state.intermediate_buffer = OmniIntermediateBuffer(max_num_reqs)
state._static_inputs_embeds = None
from vllm_omni.worker_v2.model_states.eager_mtp import EagerMTPState
state._eager_state = EagerMTPState(state)
state._stream_pos = {}
state._mtp_generators = {}
state._mtp_runner = None
for name in ("_mtp_input_ids", "_mtp_input_embeds", "_mtp_hidden", "_mtp_text_step", "_mtp_offsets"):
@@ -215,6 +224,61 @@ def test_static_decode_embeddings_refresh_from_input_ids():
assert OmniModelState._preprocess_result_needs_writeback(original, original.view_as(original)) is True
def test_moss_local_decode_runs_depth_predictor_and_routes_eos(mocker):
"""Exercise MRV2 dispatch through the real Local hook, output and logits."""
from vllm.sampling_params import SamplingParams
from vllm.v1.worker.gpu.states import RequestState
from vllm_omni.model_executor.models.moss_tts.modeling_moss_tts_local_depth import MossTTSLocalDepthTransformer
from vllm_omni.model_executor.models.moss_tts.modeling_moss_tts_talker import (
MossTTSLocalTalkerForGeneration,
)
model = MossTTSLocalTalkerForGeneration.__new__(MossTTSLocalTalkerForGeneration)
torch.nn.Module.__init__(model)
model.model = torch.nn.Module()
model.model.embed_tokens = torch.nn.Embedding(8, 4, _weight=torch.zeros(8, 4))
model.hidden_size = 4
model.n_vq = 2
model.audio_pad_token_id = 8
model.audio_assistant_slot_token_id = 2
model.im_end_token_id = 3
model.text_vocab_size = 8
model.talker_mtp_output_key = ("audio_codes", "current")
model.talker_mtp_graph_safe = False
model.gpu_resident_buffer_keys = set()
model.audio_lm_heads = model.audio_embeddings = model.local_text_lm_head = None
model._audio_embed = lambda codes: codes[:, :1].expand(-1, 4).float()
frame = mocker.Mock(return_value=(torch.tensor([True, False]), torch.tensor([[1, 2], [3, 4]])))
model.local_transformer = mocker.Mock(spec=MossTTSLocalDepthTransformer, generate_frame=frame)
state = _make_state(max_num_reqs=2, has_preprocess=True, have_multimodal_outputs=True)
state.model = model
for idx in range(2):
state.intermediate_buffer.buffers[idx] = {
"req_id": f"r{idx}",
"audio_state": {"is_stopping": False},
"hidden_states": {"last": torch.full((4,), float(idx + 1))},
}
batch = _DummyInputBatch([1, 0], num_computed_tokens_cpu=[1, 1])
inputs = {"input_ids": torch.tensor([2, 2]), "inputs_embeds": torch.zeros(2, 4)}
request_state = mocker.Mock(spec=RequestState, prompt_len=np.array([1, 1]), num_computed_tokens=None)
state.run_preprocess(batch, inputs, request_state)
frame.assert_called_once()
torch.testing.assert_close(frame.call_args.args[0], torch.tensor([[2.0] * 4, [1.0] * 4]))
assert frame.call_args.kwargs["temperature"] == 1.7
assert frame.call_args.kwargs["top_k"] == 25
assert frame.call_args.kwargs["top_p"] == 0.8
torch.testing.assert_close(inputs["inputs_embeds"], torch.tensor([[1.0] * 4, [0.0] * 4]))
_, payload = state.postprocess_model_output(torch.zeros(2, 4), batch, request_state)
assert [codes.tolist() for codes in payload["codes"]["audio"]] == [[[1, 2]], [[8, 8]]]
assert model.compute_logits(torch.zeros(2, 4)).argmax(-1).tolist() == [2, 3]
# An explicit local seed must reach the request-owned MRV2 generator.
params = SamplingParams(extra_args={"tts_local_seed": 17}, seed=99)
assert state._get_mtp_generator("seeded", params, torch.device("cpu")).initial_seed() == 17
@pytest.mark.parametrize("owned", [False, True])
def test_batched_postprocess_gpu_snapshot_writeback(owned):
# batch=[1, 0]: last-token indices follow the reordered batch, the scalar
@@ -264,6 +328,7 @@ def test_seed_independence_resolve_once_and_sampling_kwargs():
# vLLM sampling seed must not produce a talker generator.
cpu = torch.device("cpu")
assert state._get_mtp_generator("r1", SimpleNamespace(extra_args={}, seed=42), cpu) is None
state._mtp_generators.clear() # The following rows are new requests.
# Same model-local seed reproduces identical uniforms regardless of batch makeup.
state._mtp_sample_uniforms = torch.empty((2, 2, 4))
assert torch.equal(
@@ -620,6 +685,12 @@ def test_publish_sampled_embeddings_for_rows_whose_sample_is_kept(monkeypatch) -
assert [tuple(row.shape) for row in sampled] == [(1, 3), (1, 3), (0,), (1, 3)]
assert [row.tolist() for row in sampled if row.numel()] == [[[v] * 3] for v in (11.0, 12.0, 14.0)]
batch.is_prefilling_np[:] = True
batch.num_computed_prefill_tokens_np[:] = 0
batch.num_scheduled_tokens = [1] * 4
extra, _done = state.publish_sampled_embeddings(batch, torch.tensor([[11], [12], [13], [14]]))
assert all(row.numel() == 0 for row in extra["embed"]["sampled"])
# Speculative steps sample several tokens per row: not published.
assert state.publish_sampled_embeddings(batch, torch.tensor([[11, 1], [12, 1], [13, 1], [14, 1]])) is None
@@ -649,3 +720,32 @@ def test_identity_preprocess_skips_only_decode_rows(prefilling):
state.run_preprocess(batch, {"input_ids": torch.tensor([1]), "inputs_embeds": embeds})
assert seen == (["r1"] if prefilling else [])
assert torch.equal(embeds, torch.ones(1, 4))
def test_mm_embeddings_exclude_zero_length_graph_padding_rows():
state = object.__new__(OmniModelState)
state.supports_mm_inputs = True
state.mm_pruner = None
state.prompt_embeds_state = None
state.encoder_runner = MagicMock(spec=EncoderRunner, inputs_embeds=torch.zeros(8, 2))
state.execute_mm_encoder = lambda _: None
state.gather_mm_embeddings = lambda _: ([torch.ones(1, 2)], torch.ones(5, dtype=torch.bool))
def embed(input_ids, *, query_start_loc, multimodal_embeddings, is_multimodal):
assert query_start_loc == [0, 2, 5]
return input_ids[:, None].expand(-1, 2).float()
state.model = MagicMock(supports_embed_input_ids_query_start_loc=True, embed_input_ids=embed)
buffers = InputBuffers(4, 8, torch.device("cpu"))
batch = replace(
InputBatch.make_dummy(2, 5, buffers),
num_reqs=2,
num_reqs_after_padding=4,
num_tokens=5,
num_tokens_after_padding=8,
input_ids=torch.arange(8),
query_start_loc_np=np.array([0, 2, 5, 5, 5], dtype=np.int32),
)
result = state.prepare_inputs_embeds({}, batch, MagicMock(spec=RequestState))
torch.testing.assert_close(result[:5], torch.arange(5).float()[:, None].expand(-1, 2))
assert result.shape == (8, 2)