[Model] Optimize MOSS Local 1.5 MRV2 serving and progressive chunks (#8213)

Signed-off-by: Sy03 <1370724210@qq.com>
Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
Co-authored-by: Yueqian Lin <linyueqian@outlook.com>
This commit is contained in:
Sy03andYueqian Lin authored and GitHub committed 2026-10-02 18:03:14 +00:00
1 parent 527982d886
commit 3bc3f1a7d0
70 files changed
+8519 -347

No files matched your search

@@ -0,0 +1,338 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Waveform ownership across graph replays and asynchronous output copies."""
from types import SimpleNamespace
import pytest
import torch
from torch import nn
from vllm_omni.model_executor.models.moss_tts.modeling_moss_tts_codec import MossTTSCodecDecoder
from vllm_omni.model_executor.output_snapshot import PackedOutputSnapshot
pytestmark = [pytest.mark.core_model]
class CausalCodec(nn.Module):
downsample_rate = 1
def __init__(self, channels):
super().__init__()
self.weight = nn.Parameter(torch.zeros(()))
self.channels = channels
self.config = SimpleNamespace(codebook_size=1024)
def initialize_decoder_state_pool(self, capacity, scratch):
self.state = torch.zeros(capacity + scratch, device=self.weight.device)
def reset_decoder_state_slots(self, slots):
self.state[slots] = 0
def decode_streaming_batch(self, codes, lengths, slots, valid_rows):
audio = codes.sum(0).float().cumsum(-1) + self.state[slots, None]
self.state[slots] = audio[:, -1]
channels = torch.arange(1, self.channels + 1, device=audio.device)
return SimpleNamespace(audio=audio[:, None] * channels[None, :, None], audio_lengths=lengths)
class ReplayGraph:
"""A real CUDA graph with deliberately reused output storage."""
batch_sizes = [1]
def __init__(self, codec):
self.codes = torch.ones((2, 1, 3), device="cuda", dtype=torch.long)
self.slots = torch.zeros(1, device="cuda", dtype=torch.long)
self.lengths = torch.full((1,), 3, device="cuda", dtype=torch.long)
self.valid = torch.ones(1, device="cuda", dtype=torch.bool)
stream = torch.cuda.Stream()
stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(stream):
for _ in range(3):
codec.decode_streaming_batch(self.codes, self.lengths, self.slots, self.valid)
torch.cuda.current_stream().wait_stream(stream)
self.graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(self.graph):
self.output = codec.decode_streaming_batch(self.codes, self.lengths, self.slots, self.valid)
codec.state.zero_()
def decode(self, codes, slots, *, allow_frame_padding):
if codes.shape[1:] != (1, 3):
return None
self.codes.copy_(codes)
self.slots.copy_(slots)
self.graph.replay()
return self.output.audio, self.output.audio_lengths, 1
def make_decoder(*, v2=True, channels=2, device="cpu", model_type="moss_tts_local", streaming=True):
config = SimpleNamespace(
model_config=SimpleNamespace(
hf_config=SimpleNamespace(n_vq=2, model_type=model_type),
async_chunk=streaming,
use_v2_model_runner=v2,
),
scheduler_config=SimpleNamespace(max_num_seqs=8),
)
decoder = MossTTSCodecDecoder(vllm_config=config)
decoder._codec = CausalCodec(channels).to(device)
decoder._n_channels = channels
decoder._stream_max_step_frames = 3
return decoder
@pytest.mark.cpu
@pytest.mark.parametrize("ramp,frames", [(None, [1, 15]), ([1, 4, 8, 15], [1, 4, 8, 15]), ([1, 4, 4, 15], [1, 4, 15])])
def test_ramp_chunks_are_captured_at_exact_frame_counts(ramp, frames):
config = SimpleNamespace(
model_config=SimpleNamespace(
hf_config=SimpleNamespace(n_vq=2, model_type="moss_tts_local"),
async_chunk=True,
use_v2_model_runner=True,
stage_connector_config={
"extra": {
"initial_codec_chunk_frames": 1,
"codec_chunk_frames": 15,
"codec_chunk_ramp": ramp,
}
},
),
scheduler_config=SimpleNamespace(max_num_seqs=8),
)
decoder = MossTTSCodecDecoder(vllm_config=config)
assert decoder._streaming_graph_frame_sizes == frames
assert decoder._stream_max_step_frames == 15
@pytest.mark.cpu
@pytest.mark.parametrize(
"v2,streaming,model_type",
[
(False, True, "moss_tts_local"),
(True, False, "moss_tts_local"),
(True, True, "moss_tts_realtime"),
(True, True, "moss_tts_local"),
],
)
def test_gpu_output_requires_local_streaming_mrv2_and_cuda(v2, streaming, model_type):
decoder = make_decoder(v2=v2, streaming=streaming, model_type=model_type)
assert decoder._gpu_stream_output == (v2 and streaming and model_type == "moss_tts_local")
session = decoder._ensure_stream_session()
slot = session.acquire()
assert session.step({slot: torch.ones(2, 3)})[slot].device.type == "cpu"
@pytest.mark.cuda
@pytest.mark.parametrize("v2", [False, True])
@pytest.mark.parametrize("channels", [1, 2])
@pytest.mark.parametrize("graph", [False, True])
@torch.no_grad()
def test_replays_terminal_tails_and_slot_reuse_preserve_pending_waveforms(v2, channels, graph):
from vllm_omni.outputs import OmniModelRunnerOutput
from vllm_omni.worker_v2.omni_generation_model_runner import (
OmniGenerationAsyncOutput,
OmniGenerationModelRunner,
_contains_cuda_tensor,
)
decoder = make_decoder(v2=v2, channels=channels, device="cuda")
# Real Local-v1.5 weights replace the class's 24 kHz default at load time.
decoder._sr_tensor = torch.tensor(48_000, dtype=torch.int32)
session = decoder._ensure_stream_session()
if graph:
session._cudagraph_wrapper = ReplayGraph(decoder._codec)
copy_stream = torch.cuda.Stream()
pending = []
# Do not materialize any output until later forwards have replayed the
# graph, reset terminal/cancelled state, and reused those state slots.
for round_index in range(4):
value = round_index + 1
codes = torch.cat(
[
torch.full((14,), value, device="cuda", dtype=torch.long),
torch.full((6,), value + 10, device="cuda", dtype=torch.long),
]
)
info = [{"request_id": rid, "meta": {"finished": rid == "tail"}} for rid in ["tail", "empty", "live"]]
output = decoder.forward(codes, runtime_additional_information=info, seq_token_counts=[14, 0, 6])
# The runner may overwrite its input buffer before codec completion.
codes.fill_(-100)
payload = output.multimodal_outputs
assert _contains_cuda_tensor(payload) == v2
assert isinstance(payload, PackedOutputSnapshot) == v2
if v2:
# Exactly one waveform slab, regardless of request/chunk count.
assert sum(slab.is_cuda for _, slab in payload._groups) == 1
result = OmniModelRunnerOutput(
req_ids=["tail", "empty", "live"],
req_id_to_index={"tail": 0, "empty": 1, "live": 2},
sampled_token_ids=[[], [], []],
)
pending.append(
OmniGenerationAsyncOutput(
model_runner_output=result,
multimodal_outputs=payload,
num_reqs=3,
main_stream=torch.cuda.current_stream(),
copy_stream=copy_stream,
)
)
else:
pending.append(OmniGenerationModelRunner._build_pooler_output(output, 3))
decoder.on_requests_finished(["live"])
assert decoder._stream_req_slots == {}
assert not session._leased_slots
for round_index, item in enumerate(pending):
rows = item.get_output().multimodal_outputs if v2 else item
for index, (frames, value) in enumerate([(7, round_index + 1), (0, 0), (3, round_index + 11)]):
expected = 2 * value * torch.arange(1, frames + 1, dtype=torch.float32)
if channels > 1:
expected = expected[None] * torch.arange(1, channels + 1)[:, None]
torch.testing.assert_close(rows[index]["model_outputs"], expected, rtol=0, atol=0)
assert rows[index]["sr"].item() == 48_000
@pytest.mark.cuda
@torch.no_grad()
def test_owned_input_copy_clamps_entire_ragged_batch():
from vllm_omni.worker_v2.omni_ar_model_runner import _async_copy_mm
decoder = make_decoder(device="cuda", channels=2)
codes = torch.tensor([-1, 1024, 1, -10, 2, 1025, 5000, -2], device="cuda")
output = decoder.forward(
codes,
runtime_additional_information=[{"request_id": name} for name in ("a", "empty", "b")],
seq_token_counts=[6, 0, 2],
)
codes.fill_(-100)
cpu = _async_copy_mm(output.multimodal_outputs, 0, pin_memory=True)
torch.accelerator.synchronize()
expected = torch.tensor([[0.0, 1025.0, 2049.0], [0.0, 2050.0, 4098.0]])
torch.testing.assert_close(cpu["model_outputs"][0], expected, rtol=0, atol=0)
assert cpu["model_outputs"][1].numel() == 0
torch.testing.assert_close(cpu["model_outputs"][2], torch.tensor([[1023.0], [2046.0]]), rtol=0, atol=0)
decoder.on_requests_finished(["a", "b"])
@pytest.mark.cuda
@torch.no_grad()
def test_pinned_metadata_reuse_waits_for_dma_and_preserves_mixed_terminals():
decoder = make_decoder(device="cuda")
session = decoder._ensure_stream_session()
# Force reuse every call, with an outstanding transfer on the first call.
# This delay tests host-buffer ownership, never performance or overlap.
session._metadata_ring = session._metadata_ring[:1]
slots = [session.acquire() for _ in range(3)]
pending = []
for step in range(8):
order = slots[step % 3 :] + slots[: step % 3]
terminal = {order[1]}
torch.cuda._sleep(2_000_000)
ids, tails = session._stage_slot_ids(order, terminal)
pending.append((ids.clone(), tails.clone(), order, list(terminal)))
session.close()
for ids, tails, expected_ids, expected_tails in pending:
assert ids.cpu().tolist() == expected_ids
assert tails.cpu().tolist() == expected_tails
@pytest.mark.cuda
@torch.no_grad()
def test_mixed_terminal_reset_does_not_reset_live_slot():
decoder = make_decoder(device="cuda", channels=1)
session = decoder._ensure_stream_session()
first, second = session.acquire(), session.acquire()
codes = torch.ones(2, 3, device="cuda")
initial = session.step({first: codes, second: 2 * codes}, terminal_slots={first})
following = session.step({second: codes, first: codes})
torch.testing.assert_close(initial[first].cpu(), torch.tensor([[2.0, 4.0, 6.0]]), rtol=0, atol=0)
torch.testing.assert_close(following[first].cpu(), torch.tensor([[2.0, 4.0, 6.0]]), rtol=0, atol=0)
torch.testing.assert_close(following[second].cpu(), torch.tensor([[14.0, 16.0, 18.0]]), rtol=0, atol=0)
session.close()
@pytest.mark.cuda
@pytest.mark.parametrize("explicit_copy_stream", [False, True])
@torch.no_grad()
def test_output_copy_waits_for_private_codec_stream(explicit_copy_stream):
from vllm_omni.worker_v2.omni_ar_model_runner import _async_copy_mm
decoder = make_decoder(device="cuda", channels=1)
decoder._codec_stream = torch.cuda.Stream()
with torch.cuda.stream(decoder._codec_stream):
torch.cuda._sleep(20_000_000)
codes = torch.ones(6, device="cuda", dtype=torch.long)
output = decoder.forward(
codes,
runtime_additional_information=[{"request_id": "live"}],
seq_token_counts=[6],
)
codes.fill_(-100)
payload = output.multimodal_outputs
assert isinstance(payload, PackedOutputSnapshot)
assert payload.producer_event is not None
# No explicit consumer wait here: the shared copier must honor readiness.
copy_stream = torch.cuda.Stream()
with torch.cuda.stream(copy_stream):
cpu = _async_copy_mm(payload, 0, copy_stream=copy_stream if explicit_copy_stream else None, pin_memory=True)
decoder.on_requests_finished(["live"])
copy_stream.synchronize()
torch.testing.assert_close(cpu["model_outputs"][0], torch.tensor([2.0, 4.0, 6.0]), rtol=0, atol=0)
@pytest.mark.cuda
@pytest.mark.parametrize("channels", [1, 2])
@torch.no_grad()
def test_short_terminal_row_crops_each_channel_into_output_slab(channels):
decoder = make_decoder(device="cuda", channels=channels)
session = decoder._ensure_stream_session()
short, live = session.acquire(), session.acquire()
codes = {short: torch.ones(2, 2, device="cuda"), live: 3 * torch.ones(2, 5, device="cuda")}
slab = torch.empty(channels * 7, device="cuda")
buffers = {
short: slab[: channels * 2].view(channels, 2),
live: slab[channels * 2 :].view(channels, 5),
}
result = session.step(codes, terminal_slots={short}, output_buffers=buffers)
for slot, frames, value in [(short, 2, 1), (live, 5, 3)]:
expected = 2 * value * torch.arange(1, frames + 1, dtype=torch.float32)[None]
expected = expected * torch.arange(1, channels + 1)[:, None]
assert result[slot].data_ptr() == buffers[slot].data_ptr()
torch.testing.assert_close(result[slot].cpu(), expected, rtol=0, atol=0)
session.close()
@pytest.mark.cuda
@pytest.mark.parametrize("channels", [1, 2])
@pytest.mark.parametrize("layout", ["adjacent", "strided", "gapped"])
@torch.no_grad()
def test_batched_output_copy_preserves_rows_and_slab_ownership(channels, layout):
decoder = make_decoder(device="cuda", channels=channels)
session = decoder._ensure_stream_session()
slots = [session.acquire() for _ in range(4)]
slab = torch.full((6 if layout == "gapped" else 4, channels, 6), -1.0, device="cuda")
if layout == "gapped":
buffers = {slot: slab[row] for row, slot in zip([0, 1, 3, 4], slots, strict=True)}
frames = 6
elif layout == "adjacent":
buffers = {slot: slab.reshape(4, channels * 6)[row].view(channels, 6) for row, slot in enumerate(slots)}
frames = 6
else:
# Interleaved with other requests' output: the short views must not
# overwrite the untouched part of another row/channel.
buffers = {slot: slab[row, :, :3] for row, slot in enumerate(slots)}
frames = 3
codes = {slot: torch.full((2, frames), row + 1, device="cuda") for row, slot in enumerate(slots)}
result = session.step(codes, output_buffers=buffers)
for row, slot in enumerate(slots):
expected = 2 * (row + 1) * torch.arange(1, frames + 1, dtype=torch.float32)[None]
expected = expected * torch.arange(1, channels + 1)[:, None]
assert result[slot].data_ptr() == buffers[slot].data_ptr()
torch.testing.assert_close(result[slot].cpu(), expected, rtol=0, atol=0)
if layout == "strided":
assert torch.all(slab[:, :, 3:] == -1)
elif layout == "gapped":
assert torch.all(slab[[2, 5]] == -1)
session.close()
@@ -0,0 +1,146 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Chunked streaming decode must match whole-sequence decode.
``RingKVCache.complete`` writes a whole chunk before attending. With a ring of
exactly ``context`` entries, a chunk of T tokens evicts up to T keys that its
own queries should still see; for T > context it evicts part of the chunk
itself. ``initialize_decoder_state_pool(chunk_frames=...)`` adds one chunk of
headroom per layer so streaming reproduces the non-streaming decoder.
"""
import pytest
import torch
from vllm_omni.model_executor.models.moss_tts.audio_tokenizer_v2 import MossAudioTokenizerModel
from vllm_omni.model_executor.models.moss_tts.configuration_moss_audio_tokenizer_v2 import (
MossAudioTokenizerConfig,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
TRANSFORMER = {
"module_type": "Transformer",
"d_model": 16,
"num_heads": 2,
"num_layers": 1,
"dim_feedforward": 32,
"causal": True,
"norm": "layer_norm",
"positional_embedding": "rope",
"max_period": 10000,
"gating": "none",
"layer_scale": 1.0,
"conv_layout": True,
}
def tiny_codec() -> MossAudioTokenizerModel:
# 64 Hz mono input, 4-sample code frames (16 code frames per second).
# Decoder: transformer @16 Hz (context 0.5 s = 8 tokens) -> x2 -> transformer
# @32 Hz (context 0.25 s = 8 tokens) -> x2 -> 64 Hz. A 6-frame chunk is 12
# tokens at the second transformer, more than its 8-token ring.
config = MossAudioTokenizerConfig(
sampling_rate=64,
downsample_rate=4,
number_channels=1,
enable_channel_interleave=False,
encoder_kwargs=[
{"module_type": "PatchedPretransform", "patch_size": 4},
{**TRANSFORMER, "input_dimension": 4, "output_dimension": 8, "context_duration": 0.5},
],
decoder_kwargs=[
{**TRANSFORMER, "input_dimension": 8, "output_dimension": 16, "context_duration": 0.5},
{"module_type": "PatchedPretransform", "patch_size": 2},
{**TRANSFORMER, "input_dimension": 8, "output_dimension": 2, "context_duration": 0.25},
{"module_type": "PatchedPretransform", "patch_size": 2},
],
quantizer_kwargs={
"input_dim": 8,
"rvq_dim": 8,
"output_dim": 8,
"num_quantizers": 2,
"codebook_size": 16,
"codebook_dim": 4,
"quantizer_type": "rlfq",
},
)
torch.manual_seed(0)
model = MossAudioTokenizerModel(config).eval()
with torch.no_grad():
for parameter in model.parameters():
parameter.copy_(torch.randn_like(parameter) * 0.5)
model.quantizer.build_decode_lut(2, dtype=torch.float32)
return model
def stream_decode(model, codes, chunk_frames, *, headroom):
n_q, _, total = codes.shape
model.initialize_decoder_state_pool(1, 0, chunk_frames=chunk_frames if headroom else 0)
try:
slot = torch.zeros(1, dtype=torch.long)
valid = torch.ones(1, dtype=torch.bool)
parts = []
for start in range(0, total, chunk_frames):
chunk = codes[:, :, start : start + chunk_frames]
lengths = torch.full((1,), chunk.shape[-1], dtype=torch.long)
parts.append(model.decode_streaming_batch(chunk, lengths, slot, valid).audio)
return torch.cat(parts, dim=-1)
finally:
model.close_decoder_state_pool()
@torch.no_grad()
def test_ring_headroom_sizes_each_layer_for_one_chunk():
model = tiny_codec()
transformers = [m for m in model.decoder if hasattr(m, "tokens_per_input_frame")]
assert [m.tokens_per_input_frame for m in transformers] == [1, 2]
assert [m.transformer.layers[0].self_attn.context for m in transformers] == [8, 8]
model.initialize_decoder_state_pool(1, 0, chunk_frames=6)
try:
rings = [m.transformer.layers[0].self_attn._streaming_state.kv_cache.capacity for m in transformers]
assert rings == [8 + 6, 8 + 12]
finally:
model.close_decoder_state_pool()
model.initialize_decoder_state_pool(1, 0)
try:
rings = [m.transformer.layers[0].self_attn._streaming_state.kv_cache.capacity for m in transformers]
assert rings == [8, 8]
finally:
model.close_decoder_state_pool()
@torch.no_grad()
def test_chunked_streaming_matches_whole_sequence_only_with_headroom():
model = tiny_codec()
torch.manual_seed(1)
codes = torch.randint(0, 16, (2, 1, 18))
reference = model._decode_frame(codes).audio
with_headroom = stream_decode(model, codes, 6, headroom=True)
legacy = stream_decode(model, codes, 6, headroom=False)
assert with_headroom.shape == reference.shape == legacy.shape
torch.testing.assert_close(with_headroom, reference, rtol=1e-5, atol=1e-5)
# The legacy context-sized ring lets the 12-token chunk evict keys inside
# its own window, so streaming diverges from whole-sequence decoding.
assert not torch.allclose(legacy, reference, rtol=1e-3, atol=1e-3)
@torch.no_grad()
def test_padded_terminal_tail_matches_exact_tail_with_headroom():
model = tiny_codec()
torch.manual_seed(2)
codes = torch.randint(0, 16, (2, 1, 14))
history, tail = codes[:, :, :12], codes[:, :, 12:]
padded_tail = torch.nn.functional.pad(tail, (0, 4)) # 2 real frames padded to a 6-frame step
outputs = {}
for name, last in [("exact", tail), ("padded", padded_tail)]:
model.initialize_decoder_state_pool(1, 0, chunk_frames=6)
try:
slot, valid = torch.zeros(1, dtype=torch.long), torch.ones(1, dtype=torch.bool)
for chunk in (history[:, :, :6], history[:, :, 6:]):
model.decode_streaming_batch(chunk, torch.tensor([6]), slot, valid)
audio = model.decode_streaming_batch(last, torch.tensor([last.shape[-1]]), slot, valid).audio
outputs[name] = audio[..., : 2 * model.downsample_rate]
finally:
model.close_decoder_state_pool()
torch.testing.assert_close(outputs["padded"], outputs["exact"], rtol=1e-5, atol=1e-5)
@@ -0,0 +1,547 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""The first-chunk fast path emits audio out of band; the main path resumes the stream."""
import queue
import threading
from types import SimpleNamespace
import pytest
import torch
from torch import nn
from tests.model_executor.models.moss_tts.test_streaming_terminal_batch import session as cpu_session
from vllm_omni.model_executor.models.moss_tts.first_chunk_fast_path import MossFirstChunkFastPath, _SlotHandoff
from vllm_omni.model_executor.models.moss_tts.modeling_moss_tts_codec import MossTTSCodecDecoder
from vllm_omni.worker_v2.first_audio_sender import engine_output_queue_sink
pytestmark = pytest.mark.core_model
N_VQ, SAMPLES, CHANNELS = 2, 4, 2
class _Session:
def __init__(self, capacity):
self.free = list(range(capacity))
self.reset_waits = []
def acquire(self, *, oldest=False):
return self.free.pop(0 if oldest else -1) if self.free else None
def order_after_reset(self, slot, stream):
self.reset_waits.append(slot)
def release(self, slot, **kwargs):
self.free.append(slot)
class _Wrapper:
batch_sizes = [1, 2, 4, 8]
def __init__(self, device):
self.state = torch.zeros(16, device=device)
self.calls = []
self.gate = threading.Event()
self.gate.set()
def decode(self, codes, slots):
self.gate.wait()
self.calls.append(int(codes.shape[1]))
base = codes.sum(0)[:, 0].float() + 100 * slots.float()
channel = torch.arange(CHANNELS, device=codes.device).float()
audio = base[:, None, None] + channel[None, :, None] + torch.zeros(1, 1, SAMPLES, device=codes.device)
self.state[slots] = 1
return audio.to(torch.bfloat16), None, int(codes.shape[1])
@pytest.fixture
def cuda():
if not torch.cuda.is_available():
pytest.skip("CUDA required")
return torch.device("cuda")
def _fast(device, capacity=4):
session, wrapper = _Session(capacity), _Wrapper(device)
fast = MossFirstChunkFastPath(
session,
wrapper,
n_vq=N_VQ,
frames=1,
codebook_size=1024,
samples_per_frame=SAMPLES,
n_channels=CHANNELS,
sample_rate=torch.tensor(24000, dtype=torch.int32),
device=device,
)
return fast, session, wrapper
class _CallbackSink:
def __init__(self, callback):
self.callback = callback
def prepare(self, request_ids):
callback = self.callback
class Delivery:
routes = {rid: (3 if rid == "r0" else 0) for rid in request_ids}
def __call__(self, ids, rows, sr):
for rid, row in zip(ids, rows, strict=True):
callback(self.routes[rid], rid, {"model_outputs": row, "sr": sr})
def fail(self, ids):
pass
return Delivery()
@pytest.mark.cuda
def test_emits_each_first_chunk_and_hands_slot_to_main_path(cuda):
fast, session, wrapper = _fast(cuda)
emitted = {}
done = threading.Event()
def sink(client, req_id, payload):
emitted[req_id] = (client, payload["model_outputs"], int(payload["sr"]))
if len(emitted) == 2:
done.set()
slots: dict[str, int] = {}
assert not fast.submit("r0", "k0", [1, 2], slots) # unbound: main path owns it
fast.bind(_CallbackSink(sink))
wrapper.gate.clear()
assert fast.submit("r0", "k0", torch.tensor([1, 2]), slots)
assert fast.submit("r1", "k1", [5, 7], slots)
assert not fast.submit("r0", "k0", [1, 2], slots) # already has a slot
assert not fast.submit("r2", "k2", [1, 2, 3, 4], slots) # not a 1-frame chunk
assert slots == {"k0": 0, "k1": 1} and session.free == [2, 3]
wrapper.gate.set()
assert done.wait(10)
fast.close()
assert wrapper.calls in ([1, 1], [2])
for req_id, client, code_sum, slot in (("r0", 3, 3, 0), ("r1", 0, 12, 1)):
got_client, wav, sr = emitted[req_id]
assert got_client == client and sr == 24000 and wav.device.type == "cpu"
expected = (code_sum + 100 * slot + torch.arange(CHANNELS).float())[:, None].expand(CHANNELS, SAMPLES)
torch.testing.assert_close(wav, expected)
assert sorted(session.reset_waits) == [0, 1]
# The main path consumes the marker once and is ordered after the decode.
assert fast.take_decoded("k0") and not fast.take_decoded("k0")
fast.order_after(0)
fast.order_after(0) # no pending handoff: no-op
assert wrapper.state[:2].tolist() == [1, 1]
@pytest.mark.cuda
def test_order_after_blocks_until_decode_finishes(cuda):
fast, _, wrapper = _fast(cuda)
fast.bind(_CallbackSink(lambda *args: None))
wrapper.gate.clear()
assert fast.submit("r0", "k0", [1, 2], {})
finished = threading.Event()
def wait_for_slot():
fast.order_after(0)
finished.set()
waiter = threading.Thread(target=wait_for_slot)
waiter.start()
assert not finished.wait(0.2)
wrapper.gate.set()
assert finished.wait(10)
waiter.join()
fast.close()
@pytest.mark.cuda
def test_no_free_slot_leaves_chunk_to_main_path(cuda):
fast, _, _ = _fast(cuda, capacity=0)
fast.bind(_CallbackSink(lambda *args: None))
slots: dict[str, int] = {}
assert not fast.submit("r0", "k0", [1, 2], slots)
assert slots == {} and not fast.take_decoded("k0")
fast.close()
class _FakeFast:
frames = 1
def __init__(self, decoded, unsent=None):
self.decoded = set(decoded)
self.unsent = dict(unsent or {})
self.ordered = []
self.forgotten = []
def take_decoded(self, key):
if key in self.decoded:
self.decoded.remove(key)
return True
return False
def get_request_slot(self, key, slots):
return slots.get(key)
def order_after(self, slot):
self.ordered.append(slot)
def take_unsent_audio(self, key):
return self.unsent.pop(key, None)
def forget(self, key):
self.forgotten.append(key)
def gate(self):
self.gated = getattr(self, "gated", 0) + 1
def _decoder(s, fast):
decoder = object.__new__(MossTTSCodecDecoder)
nn.Module.__init__(decoder)
decoder._stream_max_step_frames = 15
decoder._stream_state_capacity = 8
decoder._stream_req_slots = {}
decoder._ensure_stream_session = lambda: s
decoder._first_chunk_fast_path = fast
decoder._stream_session = s
decoder._codec_stream = None
return decoder
@pytest.mark.cpu
def test_main_path_skips_fast_decoded_chunk_and_resumes_state():
s = cpu_session()
reference = cpu_session()
fast = _FakeFast({"a"})
decoder = _decoder(s, fast)
# The fast path leased slot 0 and decoded frame 0 of "a" into it.
slot = s.acquire(oldest=True)
decoder._stream_req_slots["a"] = slot
s.step({slot: torch.full((2, 1), 3.0)})
ref_slot = reference.acquire()
reference.step({ref_slot: torch.full((2, 1), 3.0)})
out = decoder._decode_streaming_batch([(0, "a", torch.full((2, 1), 3.0), False), (1, "b", torch.ones(2, 1), False)])
assert 0 not in out and out[1].tolist() == [[2.0]]
assert fast.ordered == [slot]
out = decoder._decode_streaming_batch([(0, "a", torch.ones(2, 3), True)])
expected = reference.step({ref_slot: torch.ones(2, 3)})[ref_slot]
torch.testing.assert_close(out[0], expected)
assert "a" not in decoder._stream_req_slots and fast.ordered == [slot, slot]
@pytest.mark.cpu
def test_abort_orders_release_after_fast_decode():
s = cpu_session()
fast = _FakeFast({"a"})
decoder = _decoder(s, fast)
slot = decoder._stream_req_slots["a"] = s.acquire(oldest=True)
decoder.on_requests_finished({"a"})
assert fast.ordered == [slot] and fast.forgotten == ["a"] and decoder._stream_req_slots == {}
assert slot in s._free_stream_slots
@pytest.mark.cpu
def test_main_path_returns_unsent_first_chunk_without_decoding_twice():
s = cpu_session()
first = torch.full((1, 1), 7.0)
fast = _FakeFast({"a"}, {"a": first})
decoder = _decoder(s, fast)
slot = s.acquire(oldest=True)
decoder._stream_req_slots["a"] = slot
s.step({slot: torch.full((2, 1), 3.0)})
out = decoder._decode_streaming_batch([(0, "a", torch.full((2, 1), 3.0), False)])
torch.testing.assert_close(out[0], first)
assert fast.unsent == {} and fast.ordered == [slot]
# A payload containing a later frame must concatenate it after the
# preserved first samples, while advancing state only for the later frame.
first = torch.full((1, 1), 7.0)
fast.decoded.add("b")
fast.unsent["b"] = first
slot_b = s.acquire(oldest=True)
decoder._stream_req_slots["b"] = slot_b
s.step({slot_b: torch.full((2, 1), 3.0)})
out = decoder._decode_streaming_batch([(0, "b", torch.full((2, 2), 3.0), False)])
torch.testing.assert_close(out[0][..., :1], first)
assert out[0].shape[-1] == 2
@pytest.mark.cpu
def test_oldest_acquire_reuses_least_recently_released_slot():
s = cpu_session()
a, b = s.acquire(), s.acquire()
s.release(a)
s.release(b)
assert s.acquire() == b
assert s.acquire(oldest=True) == 7 # never-used slots were freed before any release
assert s._free_stream_slots[-1] == a
@pytest.mark.cuda
def test_failed_decode_reports_error_without_replaying_state(cuda):
fast, _, wrapper = _fast(cuda)
def fail(codes, slots):
raise RuntimeError("boom")
wrapper.decode = fail
fast.bind(_CallbackSink(lambda *args: pytest.fail("no output expected")))
slots: dict[str, int] = {}
assert fast.submit("r0", "k0", [1, 2], slots)
with pytest.raises(RuntimeError, match="first-chunk decode failed"):
fast.order_after(slots["k0"])
assert fast.take_decoded("k0")
fast.close()
@pytest.mark.cuda
def test_failed_output_handoff_retains_decoded_samples(cuda):
fast, _, _ = _fast(cuda)
def reject(*args):
raise RuntimeError("output queue closed")
fast.bind(_CallbackSink(reject))
slots: dict[str, int] = {}
assert fast.submit("r0", "k0", [1, 2], slots)
fast.order_after(slots["k0"])
assert fast.take_decoded("k0")
wav = fast.take_unsent_audio("k0")
assert wav is not None and wav.shape == (CHANNELS, SAMPLES)
assert fast.take_unsent_audio("k0") is None
fast.close()
@pytest.mark.cuda
def test_gate_orders_main_stream_after_inflight_fast_decode(cuda):
fast, _, _ = _fast(cuda)
side = torch.cuda.Stream()
with torch.cuda.stream(side):
torch.cuda._sleep(200_000_000)
pending = torch.cuda.Event()
pending.record(side)
fast._inflight = pending
fast.gate() # disabled: no wait
unordered = torch.cuda.Event()
unordered.record()
fast._gate_main = True
fast.gate()
ordered = torch.cuda.Event()
ordered.record()
unordered.synchronize()
assert not pending.query() and not ordered.query()
ordered.synchronize()
assert pending.query()
def _cpu_fast(monkeypatch, *, scheduler=None):
# Replace only CUDA allocations, retaining the real lifecycle and worker.
import vllm_omni.model_executor.models.moss_tts.first_chunk_fast_path as module
monkeypatch.setattr(torch.Tensor, "pin_memory", lambda tensor: tensor)
monkeypatch.setattr(torch.cuda, "Stream", lambda **kwargs: None)
monkeypatch.setattr(torch.cuda, "Event", lambda: None)
monkeypatch.setattr(module, "current_omni_platform", SimpleNamespace(set_device=lambda device: None))
session = _Session(2)
fast = MossFirstChunkFastPath(
session,
SimpleNamespace(batch_sizes=[1, 2]),
n_vq=2,
frames=1,
codebook_size=1024,
samples_per_frame=4,
n_channels=1,
sample_rate=torch.tensor(24000),
device=torch.device("cpu:0"),
handoff_timeout_s=0.01,
)
outputs: queue.Queue = queue.Queue()
scheduler = scheduler or SimpleNamespace(requests={"r": SimpleNamespace(client_index=3)})
sink = engine_output_queue_sink(outputs, scheduler, upstream_first_audio=False)
return fast, session, sink, outputs
def _cancel_during_admission(fast, session, scheduler, sink):
entered, release = threading.Event(), threading.Event()
class PausedSink:
def prepare(self, ids):
delivery = sink.prepare(ids)
if delivery.routes:
entered.set()
assert release.wait(5)
return delivery
fast.bind(PausedSink())
decoder = _decoder(session, fast)
errors = []
finished = threading.Event()
finishing = threading.Event()
def submit():
try:
assert fast.submit("r", "r", [1, 2], decoder._stream_req_slots)
except BaseException as error:
errors.append(error)
def cancel():
try:
finishing.set()
decoder.on_requests_finished({"r"})
finished.set()
except BaseException as error:
errors.append(error)
producer = threading.Thread(target=submit)
cleaner = threading.Thread(target=cancel)
producer.start()
try:
assert entered.wait(5)
assert decoder._stream_req_slots == {}
# Scheduler cancellation retires the route before the runner hook.
scheduler.requests.pop("r")
cleaner.start()
assert finishing.wait(5)
assert not finished.wait(0.1)
release.set()
producer.join(5)
cleaner.join(5)
assert not producer.is_alive() and not cleaner.is_alive()
assert not errors, errors
assert finished.is_set()
assert decoder._stream_req_slots == {}
assert fast._decoded == {} and fast._handoffs == {}
assert sorted(session.free) == [0, 1]
assert not fast.submit("r", "r", [1, 2], decoder._stream_req_slots)
# A fresh request can use and return the same capacity immediately.
scheduler.requests["reuse"] = SimpleNamespace(client_index=3)
release.set()
assert fast.submit("reuse", "reuse", [1, 2], decoder._stream_req_slots)
scheduler.requests.pop("reuse")
decoder.on_requests_finished({"reuse"})
assert decoder._stream_req_slots == {} and sorted(session.free) == [0, 1]
finally:
release.set()
producer.join(5)
if cleaner.ident is not None:
cleaner.join(5)
fast.close()
@pytest.mark.cpu
def test_cancel_waits_for_first_chunk_admission_and_reclaims_slot(monkeypatch):
scheduler = SimpleNamespace(requests={"r": SimpleNamespace(client_index=3)})
fast, session, sink, _ = _cpu_fast(monkeypatch, scheduler=scheduler)
# Keep the actual queue/worker/handoff lifecycle, omitting CUDA decode.
def decode(jobs):
for job in jobs:
job.handoff.done.set()
monkeypatch.setattr(fast, "_decode", decode)
fast._handoff_timeout_s = 5.0
_cancel_during_admission(fast, session, scheduler, sink)
@pytest.mark.cuda
def test_cancel_during_first_chunk_admission_orders_cuda_handoff_and_reuse(cuda):
fast, session, _ = _fast(cuda, capacity=2)
scheduler = SimpleNamespace(requests={"r": SimpleNamespace(client_index=3)})
sink = engine_output_queue_sink(queue.Queue(), scheduler, upstream_first_audio=False)
_cancel_during_admission(fast, session, scheduler, sink)
@pytest.mark.cpu
def test_slot_timeout_keeps_handoff_and_prevents_slot_reuse(monkeypatch):
fast, _, _, _ = _cpu_fast(monkeypatch)
handoff = _SlotHandoff()
fast._handoffs[0] = handoff
for _ in range(2):
with pytest.raises(TimeoutError, match="slot 0"):
fast.order_after(0)
assert fast._handoffs[0] is handoff
handoff.done.set()
fast.order_after(0)
assert fast._handoffs == {}
@pytest.mark.cpu
def test_closed_decoder_rejects_jobs_and_cannot_restart(monkeypatch):
fast, session, sink, _ = _cpu_fast(monkeypatch)
fast.bind(sink)
fast.close()
slots: dict[str, int] = {}
assert not fast.submit("r", "external", [1, 2], slots)
assert slots == {} and session.free == [0, 1]
with pytest.raises(RuntimeError, match="closed"):
fast.bind(sink)
@pytest.mark.cpu
def test_missing_route_does_not_claim_slot(monkeypatch):
fast, session, sink, outputs = _cpu_fast(monkeypatch, scheduler=SimpleNamespace(requests={}))
fast.bind(sink)
try:
assert not fast.submit("gone", "external", [1, 2], {})
assert session.free == [0, 1] and outputs.empty()
finally:
fast.close()
@pytest.mark.cpu
def test_decode_failure_wakes_current_and_queued_handoffs(monkeypatch):
from vllm.v1.engine import FinishReason
fast, _, sink, outputs = _cpu_fast(monkeypatch)
entered, release = threading.Event(), threading.Event()
def fail(jobs):
entered.set()
assert release.wait(2)
raise RuntimeError("partial replay failed")
fast._decode = fail
fast.bind(sink)
slots: dict[str, int] = {}
try:
assert fast.submit("r", "a", [1, 2], slots)
assert entered.wait(2)
assert fast.submit("r", "b", [3, 4], slots)
release.set()
fast._thread.join(2)
assert not fast._thread.is_alive()
for slot in slots.values():
with pytest.raises(RuntimeError, match="decode failed"):
fast.order_after(slot)
assert all(fast.take_decoded(key) for key in slots)
assert not fast.submit("r", "c", [1, 2], slots)
assert outputs.qsize() == 2
while not outputs.empty():
assert outputs.get_nowait()[1].outputs[0].finish_reason == FinishReason.ERROR
finally:
release.set()
fast.close()
@pytest.mark.cpu
def test_platform_failure_wakes_pending_handoff(monkeypatch):
import vllm_omni.model_executor.models.moss_tts.first_chunk_fast_path as module
fast, _, _, _ = _cpu_fast(monkeypatch)
fast._handoffs[0] = _SlotHandoff()
def fail(device):
raise RuntimeError("device unavailable")
monkeypatch.setattr(module, "current_omni_platform", SimpleNamespace(set_device=fail))
fast._run()
assert fast._closed
with pytest.raises(RuntimeError, match="decode failed"):
fast.order_after(0)
@@ -0,0 +1,32 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Per-request code rows keep the runner payload layout with one host copy."""
import pytest
import torch
from vllm_omni.model_executor.models.moss_tts.local_model_state import _CodeRowsSnapshot
from vllm_omni.model_executor.output_snapshot import PackedOutputSnapshot
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_rows_map_to_batch_positions_and_copy_once():
codes = torch.arange(6, dtype=torch.long).reshape(2, 3)
snapshot = _CodeRowsSnapshot(codes, [2, 0], 4)
assert isinstance(snapshot, PackedOutputSnapshot) and snapshot.producer_event is None
rows = snapshot["codes"]["audio"]
assert [tuple(row.shape) for row in rows] == [(1, 3), (0, 3), (1, 3), (0, 3)]
assert torch.equal(rows[2], codes[:1]) and torch.equal(rows[0], codes[1:])
copies = []
def copy(tensor):
copies.append(tensor)
return tensor.clone()
host = snapshot.copy_to_cpu(copy)["codes"]["audio"]
assert len(copies) == 1 and copies[0] is codes
codes.zero_()
assert host[2].tolist() == [[0, 1, 2]] and host[0].tolist() == [[3, 4, 5]]
assert host[1].numel() == 0 and host[1].dtype == torch.long
@@ -0,0 +1,37 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import pytest
import torch
pytestmark = [
pytest.mark.core_model,
pytest.mark.cuda,
pytest.mark.tts,
pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required"),
]
def test_explicit_generator_bypasses_compiled_audio_sampler():
from transformers import GPT2Config
from vllm_omni.model_executor.models.moss_tts.modeling_moss_tts_local_depth import (
MossTTSLocalDepthTransformer,
)
model = MossTTSLocalDepthTransformer(GPT2Config(n_embd=80, n_head=1, n_inner=160)).cuda().bfloat16().eval()
heads = torch.nn.ModuleList([torch.nn.Linear(80, 32) for _ in range(2)]).cuda().bfloat16()
embeddings = torch.nn.ModuleList([torch.nn.Embedding(32, 80) for _ in range(2)]).cuda().bfloat16()
binary = torch.nn.Linear(80, 2).cuda().bfloat16()
hidden = torch.randn(1, 80, device="cuda", dtype=torch.bfloat16)
generator = torch.Generator(device="cuda").manual_seed(17)
expected = model.generate_frame(hidden, heads, embeddings, binary, n_vq=2, generator=generator)
def forbidden(*args, **kwargs):
raise AssertionError("compiled path selected")
model._compiled_audio_sampler = forbidden
generator.manual_seed(17)
actual = model.generate_frame(hidden, heads, embeddings, binary, n_vq=2, generator=generator)
assert all(torch.equal(a, b) for a, b in zip(expected, actual))
with pytest.raises(AssertionError, match="compiled path selected"):
model.generate_frame(hidden, heads, embeddings, binary, n_vq=2)
@@ -0,0 +1,124 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Determined text tokens retain the upstream sampling/counting contract."""
from types import SimpleNamespace
import numpy as np
import pytest
import torch
from vllm.sampling_params import SamplingParams
from vllm.v1.worker.gpu.sample.sampler import Sampler
from vllm.v1.worker.gpu.states import RequestState
from tests.model_executor.models.moss_tts.test_local_model_state import _batch, _state
from vllm_omni.model_executor.models.moss_tts.local_model_state import MossLocalModelState
from vllm_omni.worker_v2.omni_ar_model_runner import OmniARModelRunner
from vllm_omni.worker_v2.omni_model_runner import OmniGPUModelRunner
pytestmark = pytest.mark.core_model
@pytest.mark.cuda
@pytest.mark.parametrize("seed", [None, 17])
@pytest.mark.parametrize("temperature", [0.0, 1.7])
def test_matches_real_sampler_for_mixed_prefill_decode_stop_and_reordering(seed, temperature):
if not torch.cuda.is_available():
pytest.skip("CUDA required")
device = torch.device("cuda")
reqs = RequestState(5, 32, 16, 0, 8, device)
sampler = Sampler(5, 8, device, reqs)
for i, length in enumerate([4, 1, 3]):
reqs.add_request(str(i), length, [2] * length, 0, 8)
slot = reqs.req_id_to_index[str(i)]
sampler.add_request(slot, length, SamplingParams(temperature=temperature, top_k=3, top_p=0.8, seed=seed))
reqs.apply_staged_writes()
sampler.apply_staged_writes()
state = _state(MossLocalModelState, device)
state._direct_tokens = True
state.model._batch_state = None
batch = _batch(device, [2, 4, 3], [1, 2, 1])
batch.seq_lens = torch.tensor([5, 2, 2], device=device, dtype=torch.int32)
batch.cu_num_logits_np = np.arange(4, dtype=np.int32)
batch.cu_num_logits = torch.tensor(batch.cu_num_logits_np, device=device)
batch.expanded_idx_mapping = batch.idx_mapping
batch.expanded_local_pos = torch.zeros(3, dtype=torch.int32, device=device)
batch.logits_indices = batch.query_start_loc[1:].long() - 1
batch.positions = torch.arange(batch.num_tokens, device=device)
state.model._batch_should_continue = torch.tensor([False, True, True], device=device)
with torch.inference_mode():
logits = state.model.compute_logits(torch.ones(3, 4, device=device))
expected = sampler(logits, batch)
actual = state.sample_determined_tokens(batch, sampler)
assert actual is not None
for field in ["sampled_token_ids", "num_sampled", "num_rejected"]:
torch.testing.assert_close(getattr(actual, field), getattr(expected, field), rtol=0, atol=0)
assert actual.num_sampled.tolist() == [1, 0, 1]
snapshot = actual.sampled_token_ids.clone()
state.model._batch_should_continue.logical_not_()
torch.testing.assert_close(actual.sampled_token_ids, snapshot, rtol=0, atol=0)
@pytest.mark.cuda
@pytest.mark.parametrize(
"constraint",
[
{"min_tokens": 2, "stop_token_ids": [1]},
{"allowed_token_ids": [1, 2]},
{"logit_bias": {1: -10.0}},
],
)
def test_real_sampler_constraints_disable_determined_tokens(constraint):
if not torch.cuda.is_available():
pytest.skip("CUDA required")
device = torch.device("cuda")
reqs = RequestState(5, 32, 16, 0, 8, device)
sampler = Sampler(5, 8, device, reqs)
reqs.add_request("constrained", 1, [2], 0, 8)
slot = reqs.req_id_to_index["constrained"]
sampler.add_request(slot, 1, SamplingParams(**constraint))
state = _state(MossLocalModelState, device)
state._direct_tokens = True
# Use the actual upstream request registration, including its combined
# min-token/allowed-ID/bias flag, rather than mocking that flag ourselves.
assert state.sample_determined_tokens(SimpleNamespace(idx_mapping_np=np.array([slot])), sampler) is None
@pytest.mark.cpu
@pytest.mark.parametrize("feature", ["logprobs", "bias", "penalty", "bad_words", "thinking", "trace", "mask", "nans"])
def test_distribution_features_fall_back_before_using_gpu(feature):
sampler = object.__new__(Sampler)
sampler.compute_nans = feature == "nans"
sampler.return_sampling_mask = feature == "mask"
sampler.trace_replay_state = object() if feature == "trace" else None
sampler.get_logprobs_dims = lambda rows: (1, 0) if feature == "logprobs" else None
sampler.logit_bias_state = SimpleNamespace(use_logit_bias=np.array([feature == "bias"]))
sampler.penalties_state = SimpleNamespace(use_penalty=np.array([feature == "penalty"]))
sampler.bad_words_state = SimpleNamespace(num_bad_words=SimpleNamespace(np=np.array([feature == "bad_words"])))
sampler.thinking_budget_state = SimpleNamespace(enabled=True, use_thinking_budget=np.array([feature == "thinking"]))
state = object.__new__(MossLocalModelState)
state._direct_tokens = True
assert state.sample_determined_tokens(SimpleNamespace(idx_mapping_np=np.array([0])), sampler) is None
@pytest.mark.cpu
@pytest.mark.parametrize("feature", ["grammar", "sharding", "pipeline", "speculative", "empty", "declined"])
def test_runner_retains_upstream_path_for_unsupported_execution(mocker, feature):
hook = mocker.Mock(return_value=None)
class State:
def sample_determined_tokens(self, batch, sampler):
return hook(batch, sampler)
runner = object.__new__(OmniARModelRunner)
runner.model_state = State()
runner.sampler = object()
runner.batch_sharder = object() if feature == "sharding" else None
runner.pp_handler = object() if feature == "pipeline" else None
batch = SimpleNamespace(num_draft_tokens=int(feature == "speculative"), num_reqs=0 if feature == "empty" else 1)
grammar = object() if feature == "grammar" else None
fallback = mocker.patch.object(OmniGPUModelRunner, "sample", return_value="upstream")
hidden = object()
assert runner.sample(hidden, batch, grammar) == "upstream"
fallback.assert_called_once_with(hidden, batch, grammar)
assert hook.call_count == int(feature == "declined")
@@ -0,0 +1,166 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Eager MTP emits the same frames one step earlier with identical backbone inputs."""
from types import SimpleNamespace
import numpy as np
import pytest
import torch
from tests.model_executor.models.moss_tts.test_local_model_state import _admit, _batch, _state, _step
from vllm_omni.model_executor.models.moss_tts.local_model_state import MossLocalModelState
pytestmark = pytest.mark.core_model
@pytest.fixture(params=[pytest.param("cpu", marks=pytest.mark.cpu), pytest.param("cuda", marks=pytest.mark.cuda)])
def device(request):
if request.param == "cuda" and not torch.cuda.is_available():
pytest.skip("CUDA required")
return torch.device(request.param)
def _run(state, device, schedule, stop_step):
computed, prompt = np.zeros(5, dtype=np.int32), np.zeros(5, dtype=np.int32)
prompt[3], prompt[0] = 4, 2
req_states = SimpleNamespace(prompt_len=prompt, num_computed_tokens=computed)
embeds = []
frames: dict[int, list[torch.Tensor]] = {3: [], 0: []}
for index, (slots, counts) in enumerate(schedule):
state.model.force_stop = index == stop_step
embed, payload, _ = _step(state, _batch(device, slots, counts), req_states)
embeds.append(embed)
rows = payload.get("codes", {}).get("audio", [])
for slot, row in zip(slots, rows):
if row.numel() and bool(row.ne(state.model.audio_pad_token_id).any()):
frames[slot].append((index, row.clone()))
for slot, count in zip(slots, counts):
computed[slot] += count
return embeds, frames
def test_eager_frames_match_canonical_one_step_earlier(device):
schedule = [([3, 0], [2, 2]), ([3, 0], [2, 1]), ([3, 0], [1, 1]), ([0, 3], [1, 1]), ([3, 0], [1, 1])]
canonical, eager = (_state(MossLocalModelState, device) for _ in range(2))
eager._local_eager_mtp = True
for state in (canonical, eager):
_admit(state, 3, "long", 17)
_admit(state, 0, "short", 17)
# Stop both streams at the canonical step that would draw the 4th step's frames.
c_embeds, c_frames = _run(canonical, device, schedule, stop_step=4)
e_embeds, e_frames = _run(eager, device, schedule, stop_step=3)
for c, e in zip(c_embeds, e_embeds):
torch.testing.assert_close(c, e, rtol=0, atol=0)
for slot in (3, 0):
assert [f for _, f in c_frames[slot]] and len(c_frames[slot]) == len(e_frames[slot])
for (ci, cf), (ei, ef) in zip(c_frames[slot], e_frames[slot]):
assert ei == ci - 1
torch.testing.assert_close(cf, ef, rtol=0, atol=0)
def test_stopped_stream_does_not_emit_again(device):
state = _state(MossLocalModelState, device)
state._local_eager_mtp = True
_admit(state, 0, "short", 17)
computed, prompt = np.zeros(5, dtype=np.int32), np.zeros(5, dtype=np.int32)
prompt[0] = 2
req_states = SimpleNamespace(prompt_len=prompt, num_computed_tokens=computed)
emitted = []
for index, count in enumerate([2, 1, 1, 1]):
state.model.force_stop = index == 1
_, payload, _ = _step(state, _batch(device, [0], [count]), req_states)
row = payload["codes"]["audio"][0]
emitted.append(bool(row.ne(state.model.audio_pad_token_id).any()))
computed[0] += count
state.model.force_stop = False
# Frame at the prefill step, stop at step 1, then asynchronous overrun steps.
assert emitted == [True, False, False, False]
def test_unmarked_decode_row_ignores_stale_slot_state(device):
canonical, eager = (_state(MossLocalModelState, device) for _ in range(2))
eager._local_eager_mtp = True
for state in (canonical, eager):
_admit(state, 0, "short", 17)
# A decode row whose completing prefill was not observed in this state.
eager._keep_pool[0] = False
eager._eager_emb_pool[0].fill_(3.0)
computed, prompt = np.zeros(5, dtype=np.int32), np.zeros(5, dtype=np.int32)
prompt[0], computed[0] = 2, 2
req_states = SimpleNamespace(prompt_len=prompt, num_computed_tokens=computed)
for state in (canonical, eager):
state.intermediate_buffer.buffers[0]["audio_state"] = {"is_stopping": False}
c_embed, c_payload, _ = _step(canonical, _batch(device, [0], [1]), req_states)
e_embed, e_payload, _ = _step(eager, _batch(device, [0], [1]), req_states)
torch.testing.assert_close(c_embed, e_embed, rtol=0, atol=0)
torch.testing.assert_close(c_payload["codes"]["audio"][0], e_payload["codes"]["audio"][0], rtol=0, atol=0)
@pytest.mark.cpu
def test_first_prefill_through_runner_preserves_parent_eager_contract(monkeypatch, mocker):
"""The Local config must not enable the parent's incompatible eager hook."""
from vllm_omni.worker_v2 import omni_ar_model_runner as runner_module
from vllm_omni.worker_v2.model_states.eager_mtp import EagerMTPState
from vllm_omni.worker_v2.model_states.omni_model_state import OmniModelState
from vllm_omni.worker_v2.omni_ar_model_runner import OmniARModelRunner
device = torch.device("cpu")
template = _state(MossLocalModelState, device)
model = template.model
model.config = SimpleNamespace(mrv2_eager_mtp=True)
def initialize_parent(state, *args):
state.__dict__.update(template.__dict__)
state._eager_mtp, state._eager_rows = False, None
state._eager_state = EagerMTPState(state)
monkeypatch.setattr(OmniModelState, "__init__", initialize_parent)
state = MossLocalModelState(SimpleNamespace(), model, None, device)
_admit(state, 0, "req", 17)
batch = _batch(device, [0], [2])
batch.req_ids = ["req"]
computed, prompt = np.zeros(5, dtype=np.int32), np.zeros(5, dtype=np.int32)
prompt[0] = 2
_step(state, batch, SimpleNamespace(prompt_len=prompt, num_computed_tokens=computed))
hook = mocker.spy(state, "run_eager_mtp")
runner = OmniARModelRunner.__new__(OmniARModelRunner)
runner.execute_model_state = SimpleNamespace(
input_batch=batch,
hidden_states=torch.zeros(2, 4),
finished_req_ids=set(),
ec_connector_output=None,
routed_experts=None,
)
runner._kv_extracted_req_ids = runner._last_aux_output = runner._last_multimodal_outputs = None
runner.is_last_pp_rank, runner.pp_handler, runner.check_ep_fault = True, None, False
runner.model_config = SimpleNamespace(async_chunk=False)
runner.vllm_config = SimpleNamespace(model_config=SimpleNamespace(engine_output_type="text"))
runner.model_state, runner.model = state, model
runner.req_states = SimpleNamespace(
all_token_ids=SimpleNamespace(gpu=torch.zeros(5, 2)),
num_computed_tokens=SimpleNamespace(gpu=torch.from_numpy(computed)),
prompt_len=SimpleNamespace(np=prompt, gpu=torch.from_numpy(prompt)),
)
runner.main_stream = runner.output_copy_stream = mocker.Mock()
runner.eplb = runner._finalize_native_data_plane_output = runner._reserve_native_data_plane_outputs = mocker.Mock()
runner.sampler = None
runner.sample = mocker.Mock(
return_value=(
SimpleNamespace(sampled_token_ids=torch.tensor([[2]])),
torch.ones(1),
torch.zeros(1),
)
)
runner.prompt_logprobs_worker = SimpleNamespace(compute_prompt_logprobs=mocker.Mock(return_value={}))
runner.postprocess_sampled = mocker.Mock()
runner.kv_connector = SimpleNamespace(post_forward=mocker.Mock(return_value=None))
mock_out = SimpleNamespace(copy_event=None)
monkeypatch.setattr(runner_module, "OmniAsyncOutput", mocker.Mock(return_value=mock_out))
assert runner.sample_tokens(None) is mock_out
hook.assert_called_once()
assert state._eager_mtp is False and state._eager_rows is None
assert state._local_eager_mtp is True
@@ -0,0 +1,286 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Compare slot lifecycle with canonical Local hooks, including output ownership."""
from contextlib import nullcontext
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import numpy as np
import pytest
import torch
from vllm.v1.worker.gpu.model_states.default import DefaultModelState
from vllm_omni.model_executor.models.moss_tts.local_model_state import MossLocalModelState
from vllm_omni.model_executor.models.moss_tts.modeling_moss_tts_talker import MossTTSLocalTalkerForGeneration
from vllm_omni.worker_v2.model_states import init_omni_model_state
from vllm_omni.worker_v2.model_states.eager_mtp import EagerMTPState
from vllm_omni.worker_v2.model_states.intermediate_buffer import OmniIntermediateBuffer
from vllm_omni.worker_v2.model_states.omni_model_state import OmniModelState
pytestmark = pytest.mark.core_model
@pytest.fixture(params=[pytest.param("cpu", marks=pytest.mark.cpu), pytest.param("cuda", marks=pytest.mark.cuda)])
def device(request):
if request.param == "cuda" and not torch.cuda.is_available():
pytest.skip("CUDA required")
return torch.device(request.param)
def _model(device):
model = MossTTSLocalTalkerForGeneration.__new__(MossTTSLocalTalkerForGeneration)
torch.nn.Module.__init__(model)
dtype = torch.bfloat16 if device.type == "cuda" else torch.float32
model.model = torch.nn.Module()
weight = torch.arange(32, dtype=dtype, device=device).reshape(8, 4) / 16
model.model.embed_tokens = torch.nn.Embedding(8, 4, _weight=weight)
model.hidden_size, model.n_vq = 4, 2
model.audio_pad_token_id, model.text_vocab_size = 8, 8
model.audio_assistant_slot_token_id, model.im_end_token_id = 2, 3
model.talker_mtp_output_key = ("audio_codes", "current")
model.talker_mtp_graph_safe = False
model.gpu_resident_buffer_keys = {("hidden_states", "last"), ("audio_codes", "current")}
model.audio_lm_heads = model.audio_embeddings = model.local_text_lm_head = None
model._audio_embed = lambda codes: codes[:, :1].expand(-1, 4).to(dtype) / 8
model.force_stop = False
def frame(hidden, *_args, generator=None, temperature=None, top_k=None, top_p=None, **_kwargs):
assert (temperature, top_k, top_p) == (1.7, 25, 0.8)
noise = torch.randint(0, 8, (hidden.shape[0], 2), device=device, generator=generator)
codes = (hidden[:, :2].mul(16).long() + noise) % 8
return torch.full((hidden.shape[0],), not model.force_stop, device=device), codes
model.local_transformer = SimpleNamespace(generate_frame=frame)
return model
def _state(cls, device):
state = object.__new__(cls)
state.model = _model(device)
state.device, state.dtype = device, state.model.model.embed_tokens.weight.dtype
state.has_preprocess = state.has_postprocess = state.have_multimodal_outputs = True
state.scheduler_config = SimpleNamespace(max_num_seqs=5)
state.vllm_config = SimpleNamespace()
state.intermediate_buffer = OmniIntermediateBuffer(5)
state._static_inputs_embeds = torch.zeros(16, 4, device=device, dtype=state.dtype)
state._mtp_input_ids = torch.zeros(5, device=device, dtype=torch.long)
state._mtp_offsets = torch.zeros(5, device=device, dtype=torch.long)
for name in ("_mtp_input_embeds", "_mtp_hidden", "_mtp_text_step"):
setattr(state, name, torch.zeros(5, 4, device=device, dtype=state.dtype))
state._mtp_runner = state._mtp_sample_uniforms = None
state._mtp_generators = {}
state._stream_pos = {}
state._stream_decode_event = None
state._eager_state = EagerMTPState(state)
if cls is MossLocalModelState:
state._init_slot_buffers(5, 4, device, state.dtype)
return state
def _admit(state, slot, name, seed):
params = SimpleNamespace(extra_args={"tts_local_seed": seed} if seed is not None else {}, seed=None)
with patch.object(DefaultModelState, "add_request", return_value=None):
state.add_request(slot, SimpleNamespace(req_id=name, mm_features=[], sampling_params=params))
state.intermediate_buffer.buffers[slot]["codes"] = {"ref": torch.tensor([[1, 2], [3, 4], [5, 6], [2, 3]])}
def _batch(device, slots, counts):
starts = np.array([0, *np.cumsum(counts)], dtype=np.int32)
return SimpleNamespace(
idx_mapping_np=np.array(slots),
idx_mapping=torch.tensor(slots, device=device),
num_reqs=len(slots),
num_tokens=int(starts[-1]),
num_scheduled_tokens=np.array(counts),
query_start_loc_np=starts,
query_start_loc=torch.tensor(starts, device=device),
# Upstream MRV2 uses int32 token buffers, unlike the MTP graph inputs.
input_ids=torch.full((int(starts[-1]),), 2, dtype=torch.int32, device=device),
)
def _step(state, batch, req_states, dispatcher=None):
inputs = {"input_ids": batch.input_ids, "inputs_embeds": state._static_inputs_embeds[: batch.num_tokens]}
with (
patch("vllm.forward_context.set_forward_context", return_value=nullcontext()),
patch(
"vllm_omni.model_executor.models.moss_tts.local_model_state.set_forward_context", return_value=nullcontext()
),
torch.inference_mode(),
):
state.run_preprocess(batch, inputs, req_states, dispatcher)
hidden = inputs["inputs_embeds"] + 0.25
state.run_postprocess(hidden, batch)
_, payload = state.postprocess_model_output(hidden, batch, req_states)
last = batch.query_start_loc[1:] - 1
tokens = state.model.compute_logits(hidden.index_select(0, last)).argmax(-1)
result = inputs["inputs_embeds"].clone(), payload, tokens
hidden.fill_(-100) # next graph replay can overwrite its output immediately
return result
@pytest.mark.parametrize("seed", [None, 17])
@pytest.mark.parametrize("batch_prefill", [False, True])
def test_slot_matches_canonical_mixed_prefill_reorder_stop_and_reuse(device, seed, batch_prefill):
reference, candidate = (_state(cls, device) for cls in (OmniModelState, MossLocalModelState))
candidate._batch_prefill = batch_prefill
for state in (reference, candidate):
_admit(state, 3, "long", seed)
_admit(state, 0, "short", seed)
computed, prompt = np.zeros(5, dtype=np.int32), np.zeros(5, dtype=np.int32)
prompt[3], prompt[0] = 4, 2
req_states = SimpleNamespace(prompt_len=prompt, num_computed_tokens=computed)
saved = []
for index, (slots, counts) in enumerate(
[
([3, 0], [2, 2]),
([0, 3], [1, 1]),
([3, 0], [1, 1]),
([3, 0], [1, 1]),
([0, 3], [1, 1]),
([3, 0], [1, 1]),
]
):
if index == 4:
# Cancellation/removal followed by reuse must not retain hidden,
# reference offset, control, or the old request's RNG stream.
for state in (reference, candidate):
state.remove_request("short")
assert "short" not in state._mtp_generators
_admit(state, 0, "replacement", seed)
computed[0], prompt[0] = 0, 1
outputs = []
for state in (reference, candidate):
state.model.force_stop = index == 3
torch.manual_seed(91 + index)
outputs.append(_step(state, _batch(device, slots, counts), req_states))
a, b = outputs
torch.testing.assert_close(a[0], b[0], rtol=0, atol=0)
torch.testing.assert_close(a[2], b[2], rtol=0, atol=0)
if index == 3:
assert b[2].tolist() == [3, 3]
assert a[1].keys() == b[1].keys()
for ar, br in zip(a[1].get("codes", {}).get("audio", []), b[1].get("codes", {}).get("audio", [])):
torch.testing.assert_close(ar, br, rtol=0, atol=0)
saved.append((br, br.clone()))
for slot, count in zip(slots, counts):
computed[slot] += count
old = reference.intermediate_buffer.buffers[slot]
new = candidate.intermediate_buffer.buffers[slot]
assert old["ref_offset"] == new["ref_offset"]
torch.testing.assert_close(candidate._hidden_pool[slot], old["hidden_states"]["last"], rtol=0, atol=0)
assert "hidden_states" not in new and "audio_codes" not in new
candidate._codes_pool.fill_(8)
candidate._hidden_pool.zero_()
for current, snapshot in saved:
torch.testing.assert_close(current, snapshot, rtol=0, atol=0)
def test_slot_honors_external_stopping_control(device):
states = [_state(cls, device) for cls in (OmniModelState, MossLocalModelState)]
for state in states:
_admit(state, 0, "stop", None)
req = SimpleNamespace(prompt_len=np.ones(5), num_computed_tokens=np.zeros(5))
_step(state, _batch(device, [0], [1]), req)
state.intermediate_buffer.buffers[0]["audio_state"]["is_stopping"] = True
req.num_computed_tokens[:] = 1
output = _step(state, _batch(device, [0], [1]), req)
assert output[2].tolist() == [3]
assert output[1]["codes"]["audio"][0].tolist() == [[8, 8]]
@pytest.mark.cuda
def test_slot_mtp_graph_padding_reorder_and_owned_output():
if not torch.cuda.is_available():
pytest.skip("CUDA required")
states = [_state(cls, torch.device("cuda")) for cls in (OmniModelState, MossLocalModelState)]
for state in states:
def frame(hidden, *_args, **_kwargs):
return hidden[:, 0] < 3, hidden[:, :2].mul(16).long() % 8
state.model.local_transformer.generate_frame = frame
for slot in [0, 3, 4]:
_admit(state, slot, f"req{slot}", None)
candidate = states[1]
buffers = [
candidate._mtp_input_ids[:4],
candidate._mtp_input_embeds[:4],
candidate._mtp_hidden[:4],
candidate._mtp_text_step[:4],
]
stream = torch.cuda.Stream()
stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(stream), torch.inference_mode():
for _ in range(3):
candidate.model.mtp(*buffers)
torch.cuda.current_stream().wait_stream(stream)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph), torch.inference_mode():
graph_output = candidate.model.mtp(*buffers)
def replay(*args, **kwargs):
assert all(a.data_ptr() == b.data_ptr() for a, b in zip(args, buffers))
assert args[0].shape[0] == 4
graph.replay()
return graph_output
candidate._mtp_runner = replay
candidate._is_mtp_graph_runner = lambda: True
from vllm.config import CUDAGraphMode
def dispatcher(bsz):
return SimpleNamespace(num_tokens=4, cg_mode=CUDAGraphMode.FULL)
req = SimpleNamespace(prompt_len=np.ones(5, dtype=np.int32), num_computed_tokens=np.zeros(5, dtype=np.int32))
retained = []
for slots in [[0, 3, 4], [3, 0], [4, 0, 3], [3, 4]]:
out = [
_step(
state,
_batch(torch.device("cuda"), slots, [1] * len(slots)),
req,
dispatcher if state is candidate else None,
)
for state in states
]
torch.testing.assert_close(out[0][0], out[1][0], rtol=0, atol=0)
torch.testing.assert_close(out[0][2], out[1][2], rtol=0, atol=0)
for a, b in zip(out[0][1].get("codes", {}).get("audio", []), out[1][1].get("codes", {}).get("audio", [])):
torch.testing.assert_close(a, b, rtol=0, atol=0)
retained.append((b, b.clone()))
req.num_computed_tokens[slots] += 1
with torch.inference_mode():
for tensor in graph_output:
tensor.fill_(7)
for tensor, copy in retained:
torch.testing.assert_close(tensor, copy, rtol=0, atol=0)
@pytest.mark.cpu
def test_factory_requires_explicit_class_capability_and_valid_state(monkeypatch):
monkeypatch.setattr(OmniModelState, "__init__", lambda *args: None)
dynamic = MagicMock(has_preprocess=True, create_omni_model_state=None)
assert isinstance(init_omni_model_state(None, dynamic, None, torch.device("cpu")), OmniModelState)
dynamic.create_mrv2_model_state.assert_not_called()
class Invalid:
has_preprocess = True
def create_mrv2_model_state(self, *args):
return object()
with pytest.raises(TypeError, match="must return a ModelState"):
init_omni_model_state(None, Invalid(), None, torch.device("cpu"))
@pytest.mark.cpu
@pytest.mark.parametrize("enabled", [False, True])
def test_local_factory_default_and_option(monkeypatch, enabled):
monkeypatch.setattr(OmniModelState, "__init__", lambda *args: None)
monkeypatch.setattr(MossLocalModelState, "__init__", lambda *args: None)
model = _model(torch.device("cpu"))
model.config = SimpleNamespace(mrv2_gpu_slot_state=enabled)
state = init_omni_model_state(None, model, None, torch.device("cpu"))
assert type(state) is (MossLocalModelState if enabled else OmniModelState)
@@ -0,0 +1,86 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Reference batching preserves coordinates and asynchronous source ownership."""
from types import SimpleNamespace
import numpy as np
import pytest
import torch
from tests.model_executor.models.moss_tts.test_local_model_state import _batch, _state
from vllm_omni.model_executor.models.moss_tts.local_model_state import MossLocalModelState
from vllm_omni.model_executor.models.moss_tts.modeling_moss_tts_talker import MossTTSLocalTalkerForGeneration
pytestmark = pytest.mark.core_model
@pytest.mark.cpu
@pytest.mark.parametrize("flat", [False, True])
def test_prefill_reuses_text_embeddings_and_noncontiguous_reference_offsets(mocker, flat):
state = _state(MossLocalModelState, torch.device("cpu"))
state._batch_prefill = True
codes = torch.tensor([[1, 2], [3, 4], [5, 6], [2, 3]])
for slot, offset in [(3, 1), (0, 2)]:
state.intermediate_buffer.buffers[slot] = {
"req_id": str(slot),
"codes": {"ref": codes.flatten() if flat else codes},
"ref_offset": offset,
}
batch = _batch(torch.device("cpu"), [3, 0], [2, 1])
req = SimpleNamespace(prompt_len=np.full(5, 8), num_computed_tokens=np.zeros(5))
embeds = state._static_inputs_embeds[:3]
expected = state.model.embed_input_ids(batch.input_ids) + state.model._audio_embed(codes[[1, 2, 2]])
embed = mocker.spy(state.model.model.embed_tokens, "forward")
with torch.inference_mode():
state.run_preprocess(batch, {"input_ids": batch.input_ids, "inputs_embeds": embeds}, req)
torch.testing.assert_close(embeds, expected, rtol=0, atol=0)
assert embed.call_count == 1
assert state.intermediate_buffer.buffers[3]["ref_offset"] == 3
assert state.intermediate_buffer.buffers[0]["ref_offset"] == 3
@pytest.mark.cuda
def test_pending_reference_uploads_survive_staging_reuse_and_overflow():
if not torch.cuda.is_available():
pytest.skip("CUDA required")
state = _state(MossLocalModelState, torch.device("cuda"))
stream = torch.cuda.Stream()
stream.wait_stream(torch.cuda.current_stream())
outputs = []
with torch.cuda.stream(stream), torch.inference_mode():
# Keep the first H2D pending while CPU prepares all subsequent batches.
torch.cuda._sleep(100_000_000)
for value in range(1, 8):
embeds = torch.zeros(4, 4, device="cuda", dtype=state.dtype)
codes = torch.full((2, 2), value)
state._apply_reference_batch(embeds, [(1, codes)])
outputs.append((embeds, value))
codes.fill_(-1)
stream.synchronize()
assert len(state._prefill_staging) <= 3
for output, value in outputs:
expected = torch.zeros_like(output)
expected[1:3] = value / 8
torch.testing.assert_close(output, expected, rtol=0, atol=0)
@pytest.mark.cuda
def test_batched_real_audio_embedding_matches_scalar_bfloat16_reduction():
if not torch.cuda.is_available():
pytest.skip("CUDA required")
state = _state(MossLocalModelState, torch.device("cuda"))
model = state.model
model.n_vq = 12
model.audio_vocab_size = model.audio_pad_token_id = 1024
model._stacked_audio_emb_w = torch.randn(12, 1024, 3072, device="cuda", dtype=torch.bfloat16)
model._audio_embed = MossTTSLocalTalkerForGeneration._audio_embed.__get__(model)
state._static_inputs_embeds = torch.zeros(512, 3072, device="cuda", dtype=torch.bfloat16)
references = [(start, torch.randint(0, 1025, (count, 12))) for start, count in [(0, 169), (201, 11), (220, 257)]]
with torch.inference_mode():
actual = torch.randn_like(state._static_inputs_embeds)
expected = actual.clone()
for start, codes in references:
expected[start : start + len(codes)] += model._audio_embed(codes.to("cuda"))
state._apply_reference_batch(actual, references)
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
@@ -0,0 +1,82 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import pytest
import torch
import torch.nn.functional as F
pytestmark = [
pytest.mark.core_model,
pytest.mark.cuda,
pytest.mark.tts,
pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required"),
]
@pytest.mark.parametrize("batch,heads,dim", [(1, 4, 64), (16, 32, 80), (128, 32, 80), (3, 8, 128)])
def test_all_depth_positions_with_strided_cache_and_dirty_future(batch, heads, dim):
from vllm_omni.model_executor.models.moss_tts.local_short_attention import local_short_attention
torch.manual_seed(43)
query = torch.randn(batch, heads, 1, dim, device="cuda", dtype=torch.bfloat16)
key = torch.randn(batch, heads, 12, dim, device="cuda", dtype=torch.bfloat16)
value = torch.randn_like(key)
for position in range(12):
# Non-contiguous prefixes retain the stride of the full 12-slot cache.
k, v = key[:, :, : position + 1], value[:, :, : position + 1]
before_k, before_v = key.clone(), value.clone()
expected = F.scaled_dot_product_attention(query, k, v)
actual = local_short_attention(query, k, v)
assert torch.equal(key, before_k) and torch.equal(value, before_v)
if position == 0:
assert torch.equal(actual, v)
torch.testing.assert_close(actual, expected, atol=0.015625, rtol=0.01)
# Future cache contents must never affect an earlier position.
key[:, :, position + 1 :] += 100
value[:, :, position + 1 :] -= 100
assert torch.equal(local_short_attention(query, k, v), actual)
key.copy_(before_k)
value.copy_(before_v)
def test_compile_and_graph_replay_reads_fresh_inputs():
from vllm_omni.model_executor.models.moss_tts.local_short_attention import local_short_attention
query = torch.randn(16, 32, 1, 96, device="cuda", dtype=torch.bfloat16)
key = torch.randn(16, 32, 12, 96, device="cuda", dtype=torch.bfloat16)
value = torch.randn_like(key)
compiled = torch.compile(local_short_attention, fullgraph=True)
for _ in range(3):
compiled(query, key, value)
torch.accelerator.synchronize()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
result = compiled(query, key, value)
for _ in range(3):
query.normal_()
key.normal_()
value.normal_()
graph.replay()
torch.testing.assert_close(result, F.scaled_dot_product_attention(query, key, value), atol=0.015625, rtol=0.01)
def test_depth_block_cache_overwrite_and_compiled_positions(monkeypatch):
from transformers import GPT2Config
from vllm_omni.model_executor.models.moss_tts.modeling_moss_tts_local_depth import (
MossTTSLocalDepthTransformer,
)
monkeypatch.setenv("VLLM_OMNI_MOSS_LOCAL_SHORT_ATTN", "1")
model = MossTTSLocalDepthTransformer(GPT2Config(n_embd=320, n_head=4, n_inner=640)).cuda().bfloat16().eval()
attn = model.h[0].attn
attn.prepare_rope_cache(12, torch.device("cuda"), torch.bfloat16)
cache = tuple(torch.empty(4, 4, 12, 80, device="cuda", dtype=torch.bfloat16) for _ in range(2))
forward = torch.compile(model._forward_prefix, fullgraph=True, dynamic=True)
with torch.inference_mode():
for _ in range(2):
inputs = torch.randn(4, 12, 320, device="cuda", dtype=torch.bfloat16)
# The second frame overwrites the previous frame's K/V before use.
for position in range(12):
expected = model._forward_prefix(inputs[:, : position + 1])[:, -1:]
actual = forward(inputs[:, position : position + 1], cache, position)
torch.testing.assert_close(actual, expected, atol=0.03125, rtol=0.02)
@@ -598,3 +598,236 @@ def test_reference_resampler_is_cached_and_matches_functional_resample():
assert info.misses == 1 and info.hits == 1
finally:
_reference_resampler.cache_clear()
# --------------------------------------------------------------------------- #
# Inline data: URI fast path #
# --------------------------------------------------------------------------- #
class _DigestAudio(_FakeAudio):
"""Resolver whose key for data: URIs is the SHA-1 digest, like serving."""
def __init__(self, *, digest_keys=True):
super().__init__()
self._digest_keys = digest_keys
async def resolve(self, ref_str):
wav_list, sr, _ = await super().resolve(ref_str)
if not self._digest_keys:
return wav_list, sr, "rk:" + ref_str
import hashlib
return wav_list, sr, hashlib.sha1(ref_str.encode("utf-8")).hexdigest()
def artifact_key(self, cache_key):
return None
async def test_inline_reference_skips_repeat_resolution(make_encoder):
proc, audio = _FakeProcessor(), _DigestAudio()
ref = "data:audio/wav;base64,QUJD"
audio.register(ref, 5)
enc = make_encoder(proc)
first, key1 = await enc.encode(ref, resolve_ref_audio=audio.resolve, get_artifact_key=audio.artifact_key)
first.fill_(0) # callers own their copy
second, key2 = await enc.encode(ref, resolve_ref_audio=audio.resolve, get_artifact_key=audio.artifact_key)
assert audio.resolve_calls == [ref]
assert key1 == key2
assert torch.equal(second, torch.full((3, _N_VQ), 5, dtype=torch.long))
async def test_inline_index_requires_matching_resolve_key(make_encoder):
proc, audio = _FakeProcessor(), _DigestAudio(digest_keys=False)
ref = "data:audio/wav;base64,REVG"
audio.register(ref, 6)
enc = make_encoder(proc)
for _ in range(2):
await enc.encode(ref, resolve_ref_audio=audio.resolve, get_artifact_key=audio.artifact_key)
assert audio.resolve_calls == [ref, ref]
async def test_inline_reference_shared_across_encoders(make_encoder, tmp_path_factory, monkeypatch):
shared_dir = tmp_path_factory.mktemp("ref")
monkeypatch.setenv("VLLM_OMNI_MOSS_REF_CODES_SHARED_DIR", str(shared_dir))
ref = "data:audio/wav;base64,R0hJ"
first_audio, second_audio = _DigestAudio(), _DigestAudio()
first_audio.register(ref, 9)
second_audio.register(ref, 9)
first, second = make_encoder(_FakeProcessor()), make_encoder(_FakeProcessor())
a, key_a = await first.encode(ref, resolve_ref_audio=first_audio.resolve, get_artifact_key=first_audio.artifact_key)
b, key_b = await second.encode(
ref, resolve_ref_audio=second_audio.resolve, get_artifact_key=second_audio.artifact_key
)
assert first_audio.resolve_calls == [ref] and second_audio.resolve_calls == []
assert key_a == key_b and torch.equal(a, b)
# --------------------------------------------------------------------------- #
# Reference-encoder CUDA graphs: selection and processor-equivalent prep #
# --------------------------------------------------------------------------- #
class _GraphsStub:
def __init__(self, result):
self.result = result
self.calls: list[list[torch.Tensor]] = []
def encode(self, wavs):
self.calls.append(wavs)
return self.result
class _LoudnessProcessor(_FakeProcessor):
@staticmethod
def loudness_normalize(wav):
return wav * 2.0
async def test_encode_prepared_prefers_graphs_and_falls_back_to_processor(make_encoder):
proc = _LoudnessProcessor()
enc = make_encoder(proc)
graphs_codes = [torch.full((3, _N_VQ), 7, dtype=torch.long)]
enc._graphs_enabled, enc._graphs = True, _GraphsStub(graphs_codes)
prepared = [torch.ones(1, 100)]
assert enc._encode_prepared(prepared) is graphs_codes and proc.attempt_sizes == []
enc._graphs = _GraphsStub([None]) # no graph fits this clip
out = enc._encode_prepared(prepared)
assert proc.attempt_sizes == [1] and out[0].tolist() == [[1] * _N_VQ] * 3
async def test_processor_prepare_matches_processor_channel_and_loudness_handling(make_encoder):
enc = make_encoder(_LoudnessProcessor())
mono = enc._processor_prepare(torch.ones(100))
assert mono.shape == (2, 100) and torch.equal(mono, torch.full((2, 100), 2.0))
many = enc._processor_prepare(torch.arange(3 * 4, dtype=torch.float32).reshape(3, 4))
assert torch.equal(many, torch.arange(8, dtype=torch.float32).reshape(2, 4) * 2.0)
async def test_processor_prepare_resamples_to_the_tokenizer_rate(make_encoder):
torchaudio = pytest.importorskip("torchaudio")
proc = _LoudnessProcessor(sampling_rate=48000)
enc = make_encoder(proc) # works at 24 kHz, the Local-v1.5 reference rate
wav = torch.randn(1, 2400)
expected = torchaudio.functional.resample(wav.repeat(2, 1), 24000, 48000) * 2.0
# The encoder resamples the mono clip before duplicating it. Some CPU conv
# kernels round a one-row batch differently from a two-row one, so the two
# orders agree to float32 precision, not bit for bit.
torch.testing.assert_close(enc._processor_prepare(wav), expected, rtol=0, atol=1e-5)
@pytest.mark.parametrize("role", ["", "host", "client"])
async def test_prepare_captures_graphs_or_starts_the_shared_host(make_encoder, monkeypatch, tmp_path, role):
from types import SimpleNamespace
from vllm_omni.model_executor.models.moss_tts import reference_encoder as re_mod
enc = make_encoder(_FakeProcessor())
enc._shares_encoder, enc._shared_codes_dir = True, str(tmp_path)
monkeypatch.setattr(re_mod, "shared_encoder_role", lambda: role)
captured, hosts, ready = [], [], []
monkeypatch.setattr(enc, "_reference_graphs", lambda: captured.append(True))
monkeypatch.setattr(enc, "_host_workers", lambda: [])
helper = SimpleNamespace(wait_until_ready=lambda: ready.append(True))
def make_host(*args, **kwargs):
hosts.append(args)
return helper
monkeypatch.setattr(re_mod, "SharedReferenceEncoderHost", make_host)
monkeypatch.setattr(re_mod, "SharedReferenceEncoderClient", lambda *args: helper)
enc.prepare()
enc.prepare()
# Every API process waits for the shared host before startup completes.
assert (len(captured), len(hosts)) == {"": (2, 0), "host": (0, 1), "client": (0, 0)}[role]
assert len(ready) == (2 if role else 0)
async def test_clips_reach_the_encoder_at_the_tokenizer_rate(make_encoder, monkeypatch):
torchaudio = pytest.importorskip("torchaudio")
proc = _LoudnessProcessor(sampling_rate=48000)
enc = make_encoder(proc) # works at 24 kHz
sent = []
def record_prepared(prepared):
sent.extend(prepared)
return [None] * len(prepared)
monkeypatch.setattr(enc, "_encode_shared_or_local", record_prepared)
wav = [float(v) for v in torch.randn(2400)]
enc._encode_batch_sync([(wav, _SR)])
# Resampled where the request arrived (a shared encoder receives it this way); still one channel.
expected = torchaudio.functional.resample(torch.tensor(wav)[None], 24000, 48000)
torch.testing.assert_close(sent[0], expected, rtol=0, atol=0)
# The eager path is told the clip is already at the tokenizer rate.
calls = []
def record_encoding(wavs, sampling_rate, n_vq=None):
calls.append(sampling_rate)
return [None]
monkeypatch.setattr(proc, "encode_audios_from_wav", record_encoding)
enc._graphs_enabled = False
enc._encode_prepared(sent)
assert calls == [48000]
async def test_batcher_runs_up_to_max_inflight_batches_at_once():
inside = threading.Barrier(2, timeout=5)
seen: list[int] = []
def encode_batch(payload):
inside.wait() # two batches are encoding at the same time
seen.append(len(payload))
return [torch.zeros(1) for _ in payload]
batcher = _RefEncodeBatcher(encode_batch, window_ms=0.0, max_batch=8, max_inflight=2)
try:
first = asyncio.ensure_future(batcher.submit([1.0], _SR))
await asyncio.sleep(0.05) # the first batch is in flight before the second clip arrives
await asyncio.gather(first, batcher.submit([2.0], _SR))
finally:
await batcher.aclose()
assert seen == [1, 1]
def test_reference_stream_priority(monkeypatch):
from vllm_omni.model_executor.models.moss_tts import reference_encoder as re_mod
monkeypatch.delenv(re_mod._REF_ENCODE_PRIORITY_ENV, raising=False)
assert re_mod.reference_stream_priority() == 0
monkeypatch.setenv(re_mod._REF_ENCODE_PRIORITY_ENV, "high")
expected = torch.cuda.Stream.priority_range()[1] if torch.cuda.is_available() else 0
assert re_mod.reference_stream_priority() == expected
async def test_device_prep_resamples_on_the_tokenizer_device(make_encoder, monkeypatch):
torchaudio = pytest.importorskip("torchaudio")
from vllm_omni.model_executor.models.moss_tts import reference_encoder as re_mod
proc = _LoudnessProcessor(sampling_rate=48000)
monkeypatch.setattr(proc, "audio_tokenizer", torch.nn.Linear(1, 1), raising=False) # CPU placement
enc = make_encoder(proc)
enc._gpu_prep = True
wav = torch.randn(1, 2400)
clip = enc._encoder_input(wav)
assert clip.shape == (1, 2400) # left at the working rate for the encoder's device
expected = torchaudio.functional.resample(wav, 24000, 48000).repeat(2, 1) * 2.0
torch.testing.assert_close(enc._finish_prepare(clip), expected, rtol=1e-5, atol=1e-6)
# The eager fallback is told the clip's own rate.
calls = []
def record_encoding(wavs, sampling_rate, n_vq=None):
calls.append(sampling_rate)
return [None]
monkeypatch.setattr(proc, "encode_audios_from_wav", record_encoding)
enc._graphs_enabled = False
enc._encode_prepared([clip])
assert calls == [24000]
assert re_mod._REF_ENCODE_GPU_PREP_ENV == "VLLM_OMNI_MOSS_REF_GPU_PREP"
@@ -0,0 +1,320 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Reference-encoder CUDA graphs reproduce the tokenizer's eager encode."""
from contextlib import nullcontext
import pytest
import torch
import torch.nn.functional as F
from torch import nn
from vllm_omni.model_executor.models.moss_tts.reference_encoder_graphs import (
MossReferenceEncoderGraphs,
_cached_reference_rope,
_reference_rope_factors,
_validate_unmasked_reference_encoder,
_windowed_attention,
install_windowed_attention,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cuda]
FRAME, N_VQ = 8, 3
class _Stage(nn.Module):
"""Causal per-frame stage: frames never mix, like a causal encoder layer."""
def __init__(self, dim_in, dim_out):
super().__init__()
self.proj = nn.Linear(dim_in, dim_out)
def forward(self, x, lengths):
return torch.tanh(self.proj(x)), lengths
class _Quantizer(nn.Module):
def forward(self, hidden, lengths, n_q):
# hidden (B, frames, D): codes (n_q, B, frames) from per-frame argmax slices.
codes = torch.stack([hidden[..., q::n_q].argmax(-1) for q in range(n_q)])
return hidden, codes, lengths
class _Tokenizer(nn.Module):
number_channels = 2
downsample_rate = FRAME
sampling_rate = 64
def __init__(self):
super().__init__()
self.encoder = nn.ModuleList([_Stage(FRAME * 2, 16), _Stage(16, 12)])
self.quantizer = _Quantizer()
def _flatten_channels_for_codec(self, audio, lengths):
batch, channels, samples = audio.shape
frames = samples // FRAME
values = audio.reshape(batch, channels, frames, FRAME).permute(0, 2, 1, 3).reshape(batch, frames, -1)
return values, lengths // FRAME
def _codec_inference_autocast(self):
return nullcontext()
def eager(self, wavs):
out = []
for wav in wavs:
values, lengths = self._flatten_channels_for_codec(wav[None], torch.tensor([wav.shape[-1]]))
for module in self.encoder:
values, lengths = module(values, lengths)
_, codes, _ = self.quantizer(values, lengths, N_VQ)
out.append(codes[:, 0, : wav.shape[-1] // FRAME].transpose(0, 1).long())
return out
@pytest.fixture
def cuda():
if not torch.cuda.is_available():
pytest.skip("CUDA required")
return torch.device("cuda", torch.accelerator.current_device_index())
def test_cached_rope_reuses_longest_table_without_modifying_inputs(cuda):
from types import SimpleNamespace
module = SimpleNamespace(rope=SimpleNamespace(max_period=10000.0))
cache: dict[tuple[int, float, torch.device], tuple[torch.Tensor, torch.Tensor]] = {}
torch.manual_seed(17)
# Prime once; all subsequent sequences and batches use an immutable prefix.
q = torch.randn(1, 3, 128, 32, device=cuda, dtype=torch.bfloat16)
k = torch.randn_like(q)
_cached_reference_rope(module, q, k, cache)
stored = next(iter(cache.values()))
for batch, length in [(1, 17), (4, 64), (2, 128)]:
q = torch.randn(batch, 3, length, 32, device=cuda, dtype=torch.bfloat16)
k = torch.randn_like(q)
before = (q.clone(), k.clone())
got = _cached_reference_rope(module, q, k, cache)
cos, sin = _reference_rope_factors(length, 32, 10000.0, cuda)
torch.testing.assert_close(stored[0][:length], cos, rtol=0, atol=0)
torch.testing.assert_close(stored[1][:length], sin, rtol=0, atol=0)
for value, source, saved in zip(got, (q, k), before):
real, imag = source.float().reshape(batch, 3, length, 16, 2).unbind(-1)
expected = torch.stack((real * cos - imag * sin, real * sin + imag * cos), -1).to(source.dtype)
torch.testing.assert_close(value, expected.reshape_as(source), rtol=0, atol=0)
torch.testing.assert_close(source, saved, rtol=0, atol=0)
assert len(cache) == 1
assert next(iter(cache.values())) is stored
def test_cached_rope_distinguishes_period_and_head_width(cuda):
from types import SimpleNamespace
cache: dict[tuple[int, float, torch.device], tuple[torch.Tensor, torch.Tensor]] = {}
for width, period in [(32, 10000.0), (64, 10000.0), (32, 1000.0)]:
module = SimpleNamespace(rope=SimpleNamespace(max_period=period))
q = torch.randn(1, 2, 32, width, device=cuda, dtype=torch.bfloat16)
_cached_reference_rope(module, q, q, cache)
assert len(cache) == 3
@pytest.mark.parametrize("batched_transfer", [False, True])
def test_graph_codes_match_eager_per_clip_and_fall_back_outside_buckets(cuda, batched_transfer):
torch.manual_seed(0)
tokenizer = _Tokenizer().to(cuda)
# Buckets of 2 s and 4 s at 64 Hz: 128 and 256 samples.
graphs = MossReferenceEncoderGraphs(
tokenizer, n_vq=N_VQ, batch_sizes=(1, 2, 4), bucket_seconds=(2.0, 4.0), batched_transfer=batched_transfer
)
graphs.capture()
assert graphs.captured == [(1, 128), (1, 256), (2, 128), (2, 256), (4, 128), (4, 256)]
assert graphs.max_batch == 4
for count, frames in [(1, [5]), (3, [16, 3, 9]), (4, [32, 1, 20, 17]), (2, [16, 16])]:
wavs = [torch.randn(2, n * FRAME) for n in frames]
codes = graphs.encode(wavs)
expected = tokenizer.eager([w.to(cuda) for w in wavs])
assert len(codes) == count
for got, want in zip(codes, expected):
assert got.device.type == "cpu" and got.dtype == torch.long
torch.testing.assert_close(got, want.cpu(), rtol=0, atol=0)
# Longer than every bucket: left to the caller; the rest still use graphs.
long_clip, short_clip = torch.randn(2, 33 * FRAME), torch.randn(2, 4 * FRAME)
codes = graphs.encode([long_clip, short_clip])
assert codes[0] is None
torch.testing.assert_close(codes[1], tokenizer.eager([short_clip.to(cuda)])[0].cpu(), rtol=0, atol=0)
# Wider than every batch: split, longest first, and returned in input order.
wavs = [torch.randn(2, n * FRAME) for n in (3, 30, 7, 12, 1)]
for got, want in zip(graphs.encode(wavs), tokenizer.eager([w.to(cuda) for w in wavs])):
torch.testing.assert_close(got, want.cpu(), rtol=0, atol=0)
# Result ownership includes the degenerate single-frame shape. Reusing
# the pinned output slab must not overwrite a previous caller's codes.
previous = graphs.encode([torch.randn(2, FRAME)])[0]
snapshot = previous.clone()
graphs.encode([torch.randn(2, 16 * FRAME) for _ in range(4)])
torch.testing.assert_close(previous, snapshot, rtol=0, atol=0)
def test_batch_audio_budget_limits_captured_graphs(cuda):
tokenizer = _Tokenizer().to(cuda)
graphs = MossReferenceEncoderGraphs(
tokenizer, n_vq=N_VQ, batch_sizes=(1, 2, 4), bucket_seconds=(2.0, 4.0), max_batch_seconds=8.0
)
graphs.capture()
# 4 x 4 s exceeds the 8 s budget; a single clip is always captured.
assert graphs.captured == [(1, 128), (1, 256), (2, 128), (2, 256), (4, 128)]
wavs = [torch.randn(2, 30 * FRAME) for _ in range(3)]
for got, want in zip(graphs.encode(wavs), tokenizer.eager([w.to(cuda) for w in wavs])):
torch.testing.assert_close(got, want.cpu(), rtol=0, atol=0)
def test_finer_singleton_buckets_preserve_multiclip_batching(cuda):
tokenizer = _Tokenizer().to(cuda)
graphs = MossReferenceEncoderGraphs(
tokenizer,
n_vq=N_VQ,
batch_sizes=(1, 2, 4),
bucket_seconds=(2.0, 4.0),
singleton_bucket_seconds=(1.0, 3.0),
)
graphs.capture()
assert (1, 64) in graphs.captured and (1, 192) in graphs.captured
assert (2, 64) not in graphs.captured and (4, 192) not in graphs.captured
replay = graphs._replay
shapes = []
def record(entry, wavs):
shapes.append(tuple(entry.audio.shape))
return replay(entry, wavs)
graphs._replay = record
one = torch.randn(2, 20 * FRAME)
out = graphs.encode([one])
assert shapes == [(1, 2, 192)]
torch.testing.assert_close(out[0], tokenizer.eager([one.to(cuda)])[0].cpu(), rtol=0, atol=0)
shapes.clear()
wavs = [one, one[:, : 8 * FRAME], one[:, : 12 * FRAME]]
out = graphs.encode(wavs)
assert shapes == [(4, 2, 256)]
for got, want in zip(out, tokenizer.eager([w.to(cuda) for w in wavs])):
torch.testing.assert_close(got, want.cpu(), rtol=0, atol=0)
def test_compiled_capture_matches_eager(cuda):
torch.manual_seed(2)
tokenizer = _Tokenizer().to(cuda)
graphs = MossReferenceEncoderGraphs(
tokenizer, n_vq=N_VQ, batch_sizes=(1, 4), bucket_seconds=(2.0, 4.0), compile_core=True
)
graphs.capture()
assert graphs.captured == [(1, 128), (1, 256), (4, 128), (4, 256)]
wavs = [torch.randn(2, n * FRAME) for n in (5, 30, 12)]
for got, want in zip(graphs.encode(wavs), tokenizer.eager([w.to(cuda) for w in wavs])):
torch.testing.assert_close(got, want.cpu(), rtol=0, atol=0)
class _Attention(nn.Module):
"""The attention surface the tokenizer's MHA exposes to the windowed kernel."""
def __init__(self, embed_dim=128, heads=2, context=5):
super().__init__()
self.embed_dim, self.num_heads, self.context, self.causal = embed_dim, heads, context, True
self.in_proj = nn.Linear(embed_dim, 3 * embed_dim, bias=False)
def _project_qkv(self, x):
b, t, _ = x.shape
return self.in_proj(x).reshape(b, t, 3, self.num_heads, -1).permute(2, 0, 3, 1, 4).unbind(0)
def _apply_dense_rope(self, q, k):
return q, k
def _forward_non_streaming_sdpa(self, x, input_lengths):
# Reference semantics: causal, distance < context, padded rows zeroed.
b, t, _ = x.shape
q, k, v = self._project_qkv(x)
pos = torch.arange(t, device=x.device)
delta = pos.view(-1, 1) - pos.view(1, -1)
mask = (delta >= 0) & (delta < self.context)
out = F.scaled_dot_product_attention(q, k, v, mask[None, None])
valid = (pos.view(1, t) < input_lengths.view(-1, 1)).view(b, 1, t, 1)
out = torch.where(valid, out, torch.zeros((), device=out.device, dtype=out.dtype))
return out.transpose(1, 2).reshape(b, t, self.embed_dim)
@pytest.mark.parametrize("fa_version", [2, 3])
def test_windowed_attention_matches_masked_sdpa(cuda, fa_version):
if fa_version == 3 and torch.cuda.get_device_capability(cuda)[0] != 9:
pytest.skip("FA3 requires Hopper")
torch.manual_seed(1)
model = nn.Module()
model.encoder = nn.ModuleList([_Attention().to(cuda, torch.bfloat16)])
attention = model.encoder[0]
x = torch.randn(3, 40, 128, device=cuda, dtype=torch.bfloat16)
lengths = torch.tensor([40, 17, 3], device=cuda)
expected = attention._forward_non_streaming_sdpa(x, lengths)
assert install_windowed_attention(model, fa_version=fa_version) == 1
actual = attention._forward_non_streaming_sdpa(x, lengths)
torch.testing.assert_close(actual, expected, rtol=2e-2, atol=2e-2)
assert actual[1, 17:].count_nonzero() == 0 and actual[2, 3:].count_nonzero() == 0
# Non-bf16 inputs keep the tokenizer's own SDPA path.
x32 = x.float()
attention.float()
torch.testing.assert_close(
attention._forward_non_streaming_sdpa(x32, lengths), expected.float(), rtol=3e-2, atol=3e-2
)
@pytest.mark.parametrize("fa_version", [2, 3])
def test_unmasked_causal_queries_preserve_valid_prefix_and_ignore_future_padding(cuda, fa_version):
if fa_version == 3 and torch.cuda.get_device_capability(cuda)[0] != 9:
pytest.skip("FA3 requires Hopper")
torch.manual_seed(44)
attention = _Attention().to(cuda, torch.bfloat16)
x = torch.randn(4, 40, 128, device=cuda, dtype=torch.bfloat16)
lengths = torch.tensor([40, 17, 1, 0], device=cuda)
kwargs = dict(sdpa=attention._forward_non_streaming_sdpa, fa_version=fa_version)
with torch.no_grad():
masked = _windowed_attention(attention, x, lengths, **kwargs)
unmasked = _windowed_attention(attention, x, lengths, skip_padded_query_mask=True, **kwargs)
poisoned = x.clone()
for row, length in enumerate([40, 17, 1, 0]):
poisoned[row, length:] = torch.randn_like(poisoned[row, length:]) * 30
changed = _windowed_attention(attention, poisoned, lengths, skip_padded_query_mask=True, **kwargs)
for row, length in enumerate([40, 17, 1, 0]):
torch.testing.assert_close(unmasked[row, :length], masked[row, :length], rtol=0, atol=0)
torch.testing.assert_close(changed[row, :length], masked[row, :length], rtol=0, atol=0)
assert masked[row, length:].count_nonzero() == 0
assert unmasked[1, 17:].count_nonzero() > 0
# The FP32 fallback retains its original masking contract.
attention.float()
got = _windowed_attention(attention, x.float(), lengths, skip_padded_query_mask=True, **kwargs)
expected = attention._forward_non_streaming_sdpa(x.float(), lengths)
torch.testing.assert_close(got, expected, rtol=0, atol=0)
def test_unmasked_queries_reject_unknown_encoder_before_patching():
tokenizer = _Tokenizer()
with pytest.raises(ValueError, match="patch-downsample"):
install_windowed_attention(tokenizer, skip_padded_query_mask=True)
@pytest.mark.parametrize("bad_part", ["upsample", "noncausal", "quantizer", "convolution"])
def test_unmasked_queries_reject_structures_that_can_read_invalid_rows(bad_part):
from types import SimpleNamespace
patch = type("MossAudioTokenizerPatchedPretransform", (), {})()
patch.is_downsample = bad_part != "upsample"
projected = type("MossAudioTokenizerProjectedTransformer", (), {})()
projected.transformer = SimpleNamespace(
layers=[SimpleNamespace(self_attn=SimpleNamespace(causal=bad_part != "noncausal"))]
)
quantizer_cls = type(
"MossAudioTokenizerResidualVQ" if bad_part != "quantizer" else "UnknownQuantizer", (nn.Module,), {}
)
quantizer = quantizer_cls()
quantizer.proj = nn.Conv1d(2, 2, 3 if bad_part == "convolution" else 1)
tokenizer = SimpleNamespace(encoder=[patch, projected], quantizer=quantizer)
with pytest.raises(ValueError):
_validate_unmasked_reference_encoder(tokenizer)
@@ -0,0 +1,339 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""API processes share one reference encoder through a Unix socket."""
import threading
import pytest
import torch
from vllm_omni.model_executor.models.moss_tts import shared_reference_encoder as shared
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _codes(wav):
# Deterministic per clip: frames from the clip length, values from its first sample.
return torch.full((wav.shape[-1] // 10, 4), int(wav.reshape(-1)[0]), dtype=torch.long)
class _Recorder:
def __init__(self, gate=None):
self.batches = []
self.gate = gate
def __call__(self, wavs):
if self.gate is not None:
self.gate.wait(5)
self.batches.append(len(wavs))
return [RuntimeError("bad clip") if int(w.reshape(-1)[0]) < 0 else _codes(w) for w in wavs]
@pytest.fixture
def host_dir(tmp_path_factory, monkeypatch):
monkeypatch.setattr(shared, "_host_lock_handle", None)
# AF_UNIX limits the full socket path, including pytest directory names.
return str(tmp_path_factory.mktemp("ref"))
def test_only_one_process_per_directory_hosts(host_dir):
assert shared.elect_host(host_dir) is True
assert shared.elect_host(host_dir) is False # a second open file description cannot take the lock
def test_close_releases_queued_and_active_callers(host_dir):
entered, release = threading.Event(), threading.Event()
def encode(wavs):
entered.set()
assert release.wait(5)
return [_codes(wav) for wav in wavs]
host = shared.SharedReferenceEncoderHost(host_dir, [(encode, None)])
results: dict[str, list[torch.Tensor | BaseException]] = {}
threads = [
threading.Thread(target=lambda key=key: results.setdefault(key, host.encode([torch.ones(1, 20)])), daemon=True)
for key in ("active", "queued")
]
try:
threads[0].start()
assert entered.wait(2)
threads[1].start()
with host._ready:
assert host._ready.wait_for(lambda: len(host._pending) == 1, timeout=2)
host.close()
for thread in threads:
thread.join(1)
assert not thread.is_alive(), "closed encoder left a request waiting"
assert all(isinstance(results[key][0], RuntimeError) for key in results)
finally:
release.set()
host.close()
for thread in threads:
if thread.ident is not None:
thread.join(1)
def test_worker_result_count_mismatch_fails_entire_batch(host_dir):
host = shared.SharedReferenceEncoderHost(host_dir, [(lambda wavs: [_codes(wavs[0])], None)])
results = []
caller = threading.Thread(target=lambda: results.extend(host.encode([torch.ones(1, 20)] * 2)), daemon=True)
try:
caller.start()
caller.join(2)
assert not caller.is_alive(), "missing encoder result left a request waiting"
assert len(results) == 2
assert all(isinstance(result, RuntimeError) for result in results)
finally:
host.close()
caller.join(1)
def test_client_round_trip_and_per_clip_errors(host_dir):
host = shared.SharedReferenceEncoderHost(host_dir, [(_Recorder(), None)])
try:
client = shared.SharedReferenceEncoderClient(host_dir)
wavs = [torch.full((1, 40), 3.0), torch.full((1, 70), -1.0), torch.full((1, 20), 5.0)]
out = client.encode(wavs)
assert out[0].dtype == torch.long and torch.equal(out[0], _codes(wavs[0]))
assert isinstance(out[1], RuntimeError) and "bad clip" in str(out[1])
assert torch.equal(out[2], _codes(wavs[2]))
# The host process's own clips skip the socket.
assert torch.equal(host.encode([wavs[2]])[0], _codes(wavs[2]))
finally:
host.close()
def test_host_merges_clips_from_concurrent_clients(host_dir):
gate = threading.Event()
recorder = _Recorder(gate)
host = shared.SharedReferenceEncoderHost(host_dir, [(recorder, None)])
try:
results = {}
def send(name, value):
results[name] = shared.SharedReferenceEncoderClient(host_dir).encode([torch.full((1, 30), value)] * 2)
# Encoding is held until both clients' clips are queued.
warm = threading.Thread(target=lambda: host.encode([torch.full((1, 10), 1.0)]))
warm.start()
threads = [threading.Thread(target=send, args=(n, v)) for n, v in (("a", 2.0), ("b", 4.0))]
for thread in threads:
thread.start()
threading.Timer(0.5, gate.set).start()
for thread in (warm, *threads):
thread.join(10)
# Both clients' clips were encoded together, not one request at a time.
assert sum(recorder.batches) == 5 and max(recorder.batches) >= 4
assert all(int(c[0, 0]) == 2 for c in results["a"]) and all(int(c[0, 0]) == 4 for c in results["b"])
finally:
host.close()
def test_client_without_host_raises(host_dir, monkeypatch):
monkeypatch.setattr(shared, "_CONNECT_TIMEOUT_S", 0.3)
with pytest.raises((FileNotFoundError, ConnectionRefusedError)):
shared.SharedReferenceEncoderClient(host_dir).encode([torch.zeros(1, 10)])
def test_host_window_collects_across_workers_without_reserving_singletons(host_dir):
recorder = _Recorder()
host = shared.SharedReferenceEncoderHost(host_dir, [(recorder, None)] * 3, batch_window_ms=500)
try:
barrier = threading.Barrier(3)
results = []
def send(value):
barrier.wait()
results.extend(host.encode([torch.full((1, 20), value)]))
threads = [threading.Thread(target=send, args=(v,)) for v in (1.0, 2.0)]
for thread in threads:
thread.start()
barrier.wait()
for thread in threads:
thread.join(5)
assert not thread.is_alive()
assert recorder.batches == [2]
assert sorted(int(x[0, 0]) for x in results) == [1, 2]
finally:
host.close()
def test_reference_encoder_client_falls_back_to_local_encode(host_dir, monkeypatch):
from tests.model_executor.models.moss_tts.test_reference_encoder import _N_VQ, _SR, _FakeProcessor
from vllm_omni.model_executor.models.moss_tts import reference_encoder as re_mod
from vllm_omni.utils.speaker_cache import SpeakerEmbeddingCache
monkeypatch.setenv(re_mod._SHARED_CODES_DIR_ENV, host_dir)
monkeypatch.setattr(re_mod, "shared_encoder_role", lambda: "client")
monkeypatch.setattr(shared, "_CONNECT_TIMEOUT_S", 0.3)
proc = _FakeProcessor()
enc = re_mod.MossReferenceEncoder(
proc, variant="local", n_vq=_N_VQ, sr_target=_SR, speaker_cache=SpeakerEmbeddingCache(max_bytes=1 << 20)
)
placed = []
monkeypatch.setattr(enc, "_place_tokenizer_for_local_encode", lambda: placed.append(True))
out = enc._encode_batch_sync([([7.0] * 50, _SR)])
assert placed == [True] and proc.attempt_sizes == [1] and int(out[0][0, 0]) == 7
def test_workers_encode_concurrently_after_their_warmups(host_dir):
inside, release = threading.Barrier(3, timeout=5), threading.Event()
warmed = []
def worker(name):
def encode(wavs):
inside.wait() # both workers are inside an encode at the same time
release.wait(5)
return [_codes(w) for w in wavs]
return encode, lambda: warmed.append(name)
host = shared.SharedReferenceEncoderHost(host_dir, [worker("a"), worker("b")])
try:
out = []
def submit(value):
out.append(host.encode([torch.full((1, 20), value)]))
first = threading.Thread(target=submit, args=(1.0,))
first.start()
# The first worker is busy by now, so the second clip goes to the other one.
threading.Event().wait(0.1)
second = threading.Thread(target=submit, args=(2.0,))
second.start()
inside.wait()
release.set()
for thread in (first, second):
thread.join(5)
assert sorted(warmed) == ["a", "b"] and sorted(int(o[0][0, 0]) for o in out) == [1, 2]
finally:
host.close()
def test_client_requests_in_flight_use_their_own_connections(host_dir):
entered, release = threading.Semaphore(0), threading.Event()
def worker():
def encode(wavs):
entered.release()
release.wait(5)
return [_codes(w) for w in wavs]
return encode, None
host = shared.SharedReferenceEncoderHost(host_dir, [worker(), worker()])
try:
client = shared.SharedReferenceEncoderClient(host_dir)
out = []
threads = [
threading.Thread(target=lambda v=v: out.append(client.encode([torch.full((1, 20), v)]))) for v in (1.0, 2.0)
]
for thread in threads:
thread.start()
# A worker holds this request, still unanswered, before the next is
# sent. Sent together, one free worker may take both as one batch.
assert entered.acquire(timeout=5)
release.set() # both requests reached the host at once
for thread in threads:
thread.join(5)
assert sorted(int(o[0][0, 0]) for o in out) == [1, 2]
assert len(client._idle) == 2 # both connections are kept for reuse
finally:
host.close()
def test_readiness_waits_for_every_worker_warmup(host_dir, monkeypatch):
entered, release = threading.Event(), threading.Event()
recorder = _Recorder()
warmed = []
def last_warmup():
entered.set()
assert release.wait(5)
warmed.append("last")
host = shared.SharedReferenceEncoderHost(
host_dir, [(recorder, lambda: warmed.append("first")), (recorder, last_warmup)]
)
client = shared.SharedReferenceEncoderClient(host_dir)
try:
assert entered.wait(5)
monkeypatch.setattr(shared, "_STARTUP_TIMEOUT_S", 0.03)
with pytest.raises(shared.SharedReferenceEncoderStartupError):
host.wait_until_ready()
with pytest.raises(shared.SharedReferenceEncoderStartupError):
client.wait_until_ready()
assert warmed == ["first"] and recorder.batches == [] and not client._idle
release.set()
monkeypatch.setattr(shared, "_STARTUP_TIMEOUT_S", 5)
host.wait_until_ready()
client.wait_until_ready()
assert warmed == ["first", "last"] and recorder.batches == []
assert len(client._idle) == 1
assert torch.equal(client.encode([torch.ones(1, 20)])[0], _codes(torch.ones(1, 20)))
finally:
release.set()
host.close()
def test_slow_startup_does_not_use_encode_timeout(host_dir, monkeypatch):
release = threading.Event()
monkeypatch.setattr(shared, "_REQUEST_TIMEOUT_S", 0.05)
monkeypatch.setattr(shared, "_STARTUP_TIMEOUT_S", 5, raising=False)
host = shared.SharedReferenceEncoderHost(host_dir, [(_Recorder(), lambda: release.wait(5))])
timer = threading.Timer(0.2, release.set)
timer.start()
try:
# The first socket waits for readiness before starting its much shorter
# encode deadline. No API warmup call is required to protect this path.
client = shared.SharedReferenceEncoderClient(host_dir)
assert torch.equal(client.encode([torch.ones(1, 20)])[0], _codes(torch.ones(1, 20)))
finally:
release.set()
timer.join(5)
host.close()
def test_failed_host_warmup_is_not_ready(host_dir):
def fail():
raise RuntimeError("warmup failed")
recorder = _Recorder()
host = shared.SharedReferenceEncoderHost(host_dir, [(recorder, fail)])
try:
with pytest.raises(shared.SharedReferenceEncoderStartupError):
host.wait_until_ready()
with pytest.raises(shared.SharedReferenceEncoderStartupError):
shared.SharedReferenceEncoderClient(host_dir).wait_until_ready()
with pytest.raises(shared.SharedReferenceEncoderStartupError):
host.encode([torch.ones(1, 20)])
assert recorder.batches == []
finally:
host.close()
def test_startup_failure_does_not_trigger_local_capture(host_dir, monkeypatch):
from unittest.mock import Mock
from tests.model_executor.models.moss_tts.test_reference_encoder import _N_VQ, _SR, _FakeProcessor
from vllm_omni.model_executor.models.moss_tts import reference_encoder as re_mod
from vllm_omni.utils.speaker_cache import SpeakerEmbeddingCache
monkeypatch.setenv(re_mod._SHARED_CODES_DIR_ENV, host_dir)
monkeypatch.setattr(re_mod, "shared_encoder_role", lambda: "client")
enc = re_mod.MossReferenceEncoder(
_FakeProcessor(),
variant="local",
n_vq=_N_VQ,
sr_target=_SR,
speaker_cache=SpeakerEmbeddingCache(max_bytes=1 << 20),
)
enc._shared_client = Mock()
enc._shared_client.encode.side_effect = shared.SharedReferenceEncoderStartupError("still compiling")
local = Mock()
monkeypatch.setattr(enc, "_place_tokenizer_for_local_encode", local)
with pytest.raises(shared.SharedReferenceEncoderStartupError):
enc._encode_shared_or_local([torch.ones(1, 20)])
local.assert_not_called()
@@ -0,0 +1,301 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import copy
import pytest
import torch
import torch.nn.functional as F
from vllm_omni.model_executor.models.moss_tts.audio_tokenizer_v2 import (
MossAudioTokenizerMultiheadAttention,
MossAudioTokenizerRotaryEmbedding,
RingKVCache,
StreamingExecutionContext,
)
from vllm_omni.model_executor.models.moss_tts.slot_attention import slot_ring_attention, slot_ring_attention_rows
pytestmark = [pytest.mark.core_model, pytest.mark.cuda]
@pytest.mark.parametrize(
"frames,capacity", [(1, 125), (2, 250), (15, 125), (30, 250), (32, 400), (60, 400), (480, 400)]
)
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
@pytest.mark.parametrize("execution", ["graph", "compiled"])
def test_empty_tile_skip_matches_static_ring_across_reuse(frames, capacity, dtype, execution):
torch.manual_seed(45)
batch, heads, states, dim = 3, 2, 5, 64
projected = torch.randn(batch, frames, 3, heads, dim, device="cuda", dtype=dtype)
q, k, v = projected.permute(2, 0, 3, 1, 4).unbind(0)
cache = torch.randn(2, states, heads, capacity, dim, device="cuda", dtype=dtype)
expected_cache = cache.clone()
end = torch.zeros(states, 2, device="cuda", dtype=torch.long)[:, 0]
expected_end = end.clone()
slots = torch.tensor([0, 4, 2], device="cuda")
lengths = torch.full((batch, 2), frames, device="cuda", dtype=torch.int32)[:, 0]
def candidate():
return slot_ring_attention(q, k, v, cache, end, slots, lengths, 17, skip_empty_tiles=True)
graph = None
if execution == "compiled":
run = torch.compile(candidate, fullgraph=True)
else:
stream = torch.cuda.Stream()
stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(stream):
candidate()
torch.cuda.current_stream().wait_stream(stream)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph, stream=stream):
actual = candidate()
for step, start in enumerate([0, 1, capacity - 1, capacity + 9, 0, 3 * capacity]):
end.fill_(start)
expected_end.fill_(start)
cache.copy_(expected_cache)
projected.normal_()
slots.copy_(torch.roll(torch.arange(states, device="cuda"), step)[:batch])
lengths.copy_(torch.tensor([frames, max(1, frames - 3), 0], device="cuda", dtype=torch.int32))
expected = slot_ring_attention(q, k, v, expected_cache, expected_end, slots, lengths, 17)
if graph is not None:
graph.replay()
else:
actual = run()
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
torch.testing.assert_close(cache, expected_cache, rtol=0, atol=0)
torch.testing.assert_close(end, expected_end, rtol=0, atol=0)
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
@pytest.mark.parametrize("frames,capacity", [(1, 125), (15, 140), (480, 400), (480, 880)])
def test_all_padding_preserves_state_and_replay_can_become_active(dtype, frames, capacity):
q, k, v = [torch.randn(2, 2, frames, 64, device="cuda", dtype=dtype) for _ in range(3)]
cache = torch.randn(2, 4, 2, capacity, 64, device="cuda", dtype=dtype)
end = torch.tensor([0, capacity + 3, 9, 0], device="cuda")
slots = torch.tensor([1, 3], device="cuda")
lengths = torch.zeros(2, device="cuda", dtype=torch.int32)
before_cache, before_end = cache.clone(), end.clone()
stream = torch.cuda.Stream()
stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(stream):
for _ in range(3):
slot_ring_attention(q, k, v, cache, end, slots, lengths, capacity)
torch.cuda.current_stream().wait_stream(stream)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
out = slot_ring_attention(q, k, v, cache, end, slots, lengths, capacity)
for active in [False, True, False, True]:
lengths.fill_(frames if active else 0)
expected = _ring_reference(q, k, v, before_cache, before_end, slots, lengths, capacity)
graph.replay()
torch.testing.assert_close(cache, before_cache, rtol=0, atol=0)
torch.testing.assert_close(end, before_end, rtol=0, atol=0)
torch.testing.assert_close(out, expected, rtol=0.025, atol=0.025)
if not active:
assert out.count_nonzero() == 0
def test_dense_ring_retains_last_tokens_when_chunk_exceeds_capacity():
torch.manual_seed(19)
capacity, frames = 400, 480
state = RingKVCache(3, 2, 64, capacity, device=torch.device("cuda"), dtype=torch.float32)
slots = torch.tensor([1, 0], device="cuda")
context = StreamingExecutionContext(state_slot_ids=slots, valid_rows=torch.ones(2, device="cuda", dtype=torch.bool))
for start in [0, frames]:
k = torch.randn(2, 2, frames, 64, device="cuda")
v = torch.randn_like(k)
state.complete(k, v, execution_context=context)
retained = torch.arange(frames - capacity, frames, device="cuda")
destinations = (start + retained) % capacity
torch.testing.assert_close(state.cache[0, slots][:, :, destinations], k[:, :, retained], rtol=0, atol=0)
torch.testing.assert_close(state.cache[1, slots][:, :, destinations], v[:, :, retained], rtol=0, atol=0)
torch.testing.assert_close(state.end_offset[slots], torch.full_like(slots, start + frames), rtol=0, atol=0)
def _ring_reference(q, k, v, cache, end, slots, lengths, context):
"""Independent chronological ring oracle with FP32 attention arithmetic."""
capacity = cache.shape[-2]
output = torch.zeros_like(q)
for row, (slot, length) in enumerate(zip(slots.tolist(), lengths.tolist())):
if length == 0:
continue
start = int(end[slot])
for token in range(max(0, length - capacity), length):
cache[0, slot, :, (start + token) % capacity] = k[row, :, token]
cache[1, slot, :, (start + token) % capacity] = v[row, :, token]
stop = start + length
physical_positions = torch.full((capacity,), -1, device=q.device, dtype=torch.long)
available = torch.arange(max(0, stop - capacity), stop, device=q.device)
physical_positions[available % capacity] = available
queries = torch.arange(start, stop, device=q.device)
distance = queries[:, None] - physical_positions[None]
mask = (physical_positions[None] >= 0) & (distance >= 0)
if context > 0:
mask &= distance < context
output[row, :, :length] = F.scaled_dot_product_attention(
q[row, :, :length].float(), cache[0, slot].float(), cache[1, slot].float(), mask[None]
).to(q.dtype)
end[slot] = stop
return output
@pytest.mark.parametrize("frames,capacity,head_dim", [(1, 125, 64), (15, 125, 64), (480, 400, 64), (33, 65, 32)])
@pytest.mark.parametrize("execution", ["eager", "graph", "compiled"])
def test_slot_attention_preserves_ring_state_and_padding(frames, capacity, head_dim, execution):
torch.manual_seed(29)
batch, heads, state_capacity = 3, 2, 5
projected = torch.randn(batch, frames, 3, heads, head_dim, device="cuda", dtype=torch.bfloat16)
q, k, v = projected.permute(2, 0, 3, 1, 4).unbind(0)
cache = torch.randn(2, state_capacity, heads, capacity, head_dim, device="cuda", dtype=q.dtype)
# Strided metadata is allowed by the kernel contract.
end = torch.zeros(state_capacity, 2, device="cuda", dtype=torch.long)[:, 0]
slots = torch.arange(batch * 2, device="cuda")[::2]
lengths = torch.full((batch, 2), frames, device="cuda", dtype=torch.int32)[:, 0]
context = capacity
attention = slot_ring_attention
if execution == "compiled":
# Functionalization must preserve mutations of the persistent ring
# and its offsets between calls, including slot reuse.
attention = torch.compile(slot_ring_attention, backend="inductor", fullgraph=True)
if execution == "graph":
stream = torch.cuda.Stream()
stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(stream):
for _ in range(3):
slot_ring_attention(q, k, v, cache, end, slots, lengths, context)
torch.cuda.current_stream().wait_stream(stream)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
actual = slot_ring_attention(q, k, v, cache, end, slots, lengths, context)
end.zero_()
expected_cache, expected_end = cache.clone(), end.clone()
for step in range(8):
slots.copy_(torch.roll(torch.arange(state_capacity, device="cuda"), step)[:batch])
lengths.copy_(torch.tensor([frames, max(1, frames - 3), 0], device="cuda", dtype=torch.int32))
projected.normal_()
if step == 4:
end[1:3].zero_()
expected_end[1:3].zero_()
expected = _ring_reference(q, k, v, expected_cache, expected_end, slots, lengths, context)
if execution == "graph":
graph.replay()
else:
actual = attention(q, k, v, cache, end, slots, lengths, context)
torch.testing.assert_close(cache, expected_cache, rtol=0, atol=0)
torch.testing.assert_close(end, expected_end, rtol=0, atol=0)
torch.testing.assert_close(actual, expected, rtol=0.025, atol=0.025)
padding = torch.arange(frames, device="cuda")[None] >= lengths[:, None]
assert actual.permute(0, 2, 1, 3)[padding].count_nonzero() == 0
@pytest.mark.parametrize("frames,context", [(1, 9), (3, 9), (15, 125), (480, 400)])
def test_slot_attention_integrates_with_current_mha(frames, context):
torch.manual_seed(9)
reference = MossAudioTokenizerMultiheadAttention(
128,
2,
causal=True,
context=context,
rope=MossAudioTokenizerRotaryEmbedding(),
device="cuda",
dtype=torch.bfloat16,
)
candidate = copy.deepcopy(reference)
candidate._slot_attention = slot_ring_attention
for model in (reference, candidate):
model._streaming_state = model._init_streaming_state(4)
for step in range(8):
slots = torch.tensor([1, 0, 3] if step % 2 else [0, 1, 3], device="cuda")
valid = torch.tensor([True, True, False], device="cuda")
execution_context = StreamingExecutionContext(state_slot_ids=slots, valid_rows=valid)
x = torch.randn(3, frames, 128, device="cuda", dtype=torch.bfloat16)
with torch.inference_mode():
expected = reference(x, x, x, execution_context=execution_context)
actual = candidate(x, x, x, execution_context=execution_context)
torch.testing.assert_close(actual, expected, atol=0.01, rtol=0.02)
cs, rs = candidate._streaming_state, reference._streaming_state
torch.testing.assert_close(cs.offset, rs.offset, rtol=0, atol=0)
torch.testing.assert_close(cs.kv_cache.end_offset, rs.kv_cache.end_offset, rtol=0, atol=0)
torch.testing.assert_close(cs.kv_cache.cache[:, :3], rs.kv_cache.cache[:, :3], rtol=0, atol=0)
if step == 4:
for model in (reference, candidate):
model._streaming_state.reset_slots(torch.tensor([0], device="cuda"))
@pytest.mark.parametrize(
"frames,capacity,head_dim", [(1, 125, 64), (15, 140, 64), (40, 125, 64), (480, 400, 64), (33, 65, 32)]
)
@pytest.mark.parametrize("execution", ["eager", "graph", "compiled"])
def test_fused_rows_attention_matches_three_launch_op_exactly(frames, capacity, head_dim, execution):
torch.manual_seed(31)
batch, heads, state_capacity = 3, 2, 6
projected = torch.randn(batch, frames, 3, heads, head_dim, device="cuda", dtype=torch.bfloat16)
q, k, v = projected.permute(2, 0, 3, 1, 4).unbind(0)
cache = torch.randn(2, state_capacity, heads, capacity, head_dim, device="cuda", dtype=q.dtype)
reference_cache = cache.clone()
end = torch.zeros(state_capacity, device="cuda", dtype=torch.long)
slots = torch.zeros(batch, device="cuda", dtype=torch.long)
lengths = torch.zeros(batch, device="cuda", dtype=torch.int32)
starts = torch.zeros(batch, device="cuda", dtype=torch.long)
attention = slot_ring_attention_rows
if execution == "compiled":
attention = torch.compile(slot_ring_attention_rows, backend="inductor", fullgraph=True)
if execution == "graph":
stream = torch.cuda.Stream()
stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(stream):
for _ in range(3):
slot_ring_attention_rows(q, k, v, cache, starts, slots, lengths, capacity)
torch.cuda.current_stream().wait_stream(stream)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
actual = slot_ring_attention_rows(q, k, v, cache, starts, slots, lengths, capacity)
cache.copy_(reference_cache)
for step in range(8):
slots.copy_(torch.roll(torch.arange(state_capacity, device="cuda"), step)[:batch])
lengths.copy_(torch.tensor([frames, max(1, frames - 3), 0], device="cuda", dtype=torch.int32))
starts.copy_(end[slots])
projected.normal_()
expected = slot_ring_attention(q, k, v, reference_cache, end, slots, lengths, capacity)
if execution == "graph":
graph.replay()
else:
actual = attention(q, k, v, cache, starts, slots, lengths, capacity)
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
torch.testing.assert_close(cache, reference_cache, rtol=0, atol=0)
@pytest.mark.parametrize("frames,context", [(1, 9), (15, 125), (480, 400)])
def test_fused_rows_attention_integrates_with_mha_offsets(frames, context):
torch.manual_seed(11)
reference = MossAudioTokenizerMultiheadAttention(
128,
2,
causal=True,
context=context,
rope=MossAudioTokenizerRotaryEmbedding(),
device="cuda",
dtype=torch.bfloat16,
)
candidate = copy.deepcopy(reference)
reference._slot_attention = slot_ring_attention
candidate._slot_attention_rows = slot_ring_attention_rows
for model in (reference, candidate):
model._streaming_state = model._init_streaming_state(4)
for step in range(8):
slots = torch.tensor([1, 0, 3] if step % 2 else [0, 1, 3], device="cuda")
valid = torch.tensor([True, True, False], device="cuda")
execution_context = StreamingExecutionContext(state_slot_ids=slots, valid_rows=valid)
x = torch.randn(3, frames, 128, device="cuda", dtype=torch.bfloat16)
with torch.inference_mode():
expected = reference(x, x, x, execution_context=execution_context)
actual = candidate(x, x, x, execution_context=execution_context)
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
cs, rs = candidate._streaming_state, reference._streaming_state
torch.testing.assert_close(cs.offset, rs.offset, rtol=0, atol=0)
torch.testing.assert_close(cs.kv_cache.end_offset, rs.kv_cache.end_offset, rtol=0, atol=0)
torch.testing.assert_close(cs.kv_cache.cache, rs.kv_cache.cache, rtol=0, atol=0)
if step == 4:
for model in (reference, candidate):
model._streaming_state.reset_slots(torch.tensor([0], device="cuda"))
@@ -0,0 +1,86 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""A second streaming-decoder graph set can replay concurrently with the first."""
import threading
import pytest
import torch
from torch import nn
from vllm_omni.model_executor.models.moss_tts.cuda_graph_streaming_decoder_wrapper import (
CUDAGraphStreamingDecoderWrapper,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cuda]
CAP, DIM = 8, 64
class _Codec(nn.Module):
def __init__(self, device):
super().__init__()
self.weight = nn.Parameter(torch.randn(DIM, DIM, device=device) / DIM)
self.state = torch.zeros(CAP + 16, DIM, device=device)
def decode_streaming_tensors(self, codes, lengths, slots, valid_rows):
x = codes.float().sum(0)[..., None].expand(-1, -1, DIM) + self.state[slots][:, None]
y = x @ self.weight
self.state.index_copy_(0, slots, y[:, -1])
return y.transpose(1, 2).contiguous(), lengths
def reset_decoder_state_slots(self, slots):
self.state.index_fill_(0, slots.to(self.state.device), 0)
def _wrapper(codec, **kwargs):
return CUDAGraphStreamingDecoderWrapper(
codec,
state_capacity=CAP,
num_quantizers=2,
vllm_config=None,
compiled_decode=codec.decode_streaming_tensors,
**kwargs,
)
def test_private_wrapper_captures_on_its_own_stream_and_replays_concurrently():
if not torch.cuda.is_available():
pytest.skip("CUDA required")
device = torch.device("cuda", torch.accelerator.current_device_index())
codec = _Codec(device)
main = _wrapper(codec, batch_sizes=[4], frame_sizes=[3])
fast = _wrapper(codec, batch_sizes=[1, 2], frame_sizes=[1], scratch_base=CAP + 4, private_pool=True)
main.warmup(device)
fast.warmup(device)
assert main.is_ready and fast.is_ready
assert main._capture_stream is None and fast._capture_stream is not None
codes_main = torch.randint(0, 5, (2, 4, 3), device=device)
codes_fast = torch.randint(0, 5, (2, 2, 1), device=device)
errors = []
def run(wrapper, codes, slots, iters):
stream = torch.cuda.Stream(device=device)
slot_ids = torch.tensor(slots, device=device)
try:
with torch.cuda.stream(stream):
for _ in range(iters):
codec.reset_decoder_state_slots(slot_ids)
audio = wrapper.decode(codes, slot_ids)[0][: len(slots)].clone()
expected = (codes.float().sum(0)[..., None].expand(-1, -1, DIM) @ codec.weight).transpose(1, 2)
stream.synchronize()
torch.testing.assert_close(audio, expected, rtol=1e-3, atol=1e-3)
except Exception as error: # surfaced below
errors.append(error)
threads = [
threading.Thread(target=run, args=(main, codes_main, [0, 1, 2, 3], 200)),
threading.Thread(target=run, args=(fast, codes_fast, [4, 5], 200)),
]
for thread in threads:
thread.start()
for thread in threads:
thread.join(timeout=60)
assert not any(thread.is_alive() for thread in threads), "concurrent graph replay did not finish"
assert not errors, errors
@@ -54,8 +54,19 @@ def test_terminal_padding_preserves_audio_live_state_and_reused_slots():
assert [resumed[i].item() for i in (0, 1)] == [2, 36]
@pytest.mark.parametrize("graph_capacity, expected_calls", [(None, 5), (4, 5), (8, 3)])
def test_only_existing_compatible_graph_buckets_are_coalesced(mocker, graph_capacity, expected_calls):
@pytest.mark.parametrize(
"graph_capacity,expected_groups",
[
# No graph, or graphs that do not cover the state capacity: exact tails.
(None, {3: [0], 7: [1], 8: [2], 1: [3], 15: [4]}),
(4, {3: [0], 7: [1], 8: [2], 1: [3], 15: [4]}),
# Full graph coverage: finished tails (3, 7) join the padded 8-frame
# bucket with the live 8-frame row; the finished 1-frame row keeps the
# exact 1-frame graph and the live 15-frame row stays alone.
(8, {8: [0, 1, 2], 1: [3], 15: [4]}),
],
)
def test_terminal_tails_coalesce_into_padded_frame_buckets(mocker, graph_capacity, expected_groups):
s = session()
s._cudagraph_wrapper = (
None
@@ -81,9 +92,16 @@ def test_only_existing_compatible_graph_buckets_are_coalesced(mocker, graph_capa
for i, (length, done) in enumerate(zip(lengths, finished, strict=True))
]
)
assert step.call_count == expected_calls
# Slots are leased in request order, so slot == row here.
groups = {}
for call in step.call_args_list:
plan = call.args[0]
groups[max(int(codes.shape[1]) for codes in plan.values())] = sorted(plan)
terminal = call.kwargs["terminal_slots"] or set()
assert terminal == {slot for slot in plan if finished[slot]}
assert groups == expected_groups
assert step.call_count == len(expected_groups)
# Output is always cropped to the exact frame count, padded or not.
assert [result[i].shape[-1] for i in range(5)] == lengths
if graph_capacity == 8:
assert list(step.call_args_list[0].args[0]) == [0, 1, 2]
assert step.call_args_list[0].kwargs["terminal_slots"] == {0, 1}
assert list(step.call_args_list[1].args[0]) == [3] # Preserve the exact one-frame graph.
# Finished rows released their slots; live rows keep theirs.
assert set(decoder._stream_req_slots) == {"2", "4"}