[Core][Model] Enable Qwen3-TTS MRV2 and optimize the TTS pipeline (#7781)

Signed-off-by: Sy03 <1370724210@qq.com>
This commit is contained in:
Sy03 authored and GitHub committed 2026-09-20 23:03:08 -07:00
1 parent d139c600c7
commit a9e2117f5e
103 files changed
+10232 -661

No files matched your search

@@ -682,6 +682,7 @@ class TestCodePredictorGraphReplay:
return_proj_buf=False,
)
predictor._prefix_graphs_enabled = False
predictor._prefix_reprefill_enabled = False
predictor._bucket_pos_ids = {1: torch.arange(4).unsqueeze(0)}
graph = mocker.Mock()
graph_output = torch.zeros(1, 4, 4)
@@ -1362,3 +1363,38 @@ class TestCodePredictorFusedProjections:
with pytest.raises(RuntimeError, match="missing fused parameters"):
model.load_weights(weights)
class TestMTPExecutionBuckets:
"""Outer MRv2 MTP graph buckets must align with the predictor bucket set."""
def test_configure_mtp_execution_buckets(self, mocker: MockerFixture, loaded_target_classes) -> None:
_ = loaded_target_classes
common_mod = sys.modules["vllm_omni.model_executor.models.common.qwen3_code_predictor"]
mocker.patch.object(common_mod.current_omni_platform, "is_npu", return_value=False)
cp_config, _ = _make_tiny_config(loaded_target_classes)
vllm_config = _make_vllm_config(mocker, max_num_seqs=4)
wrapper = common_mod.CodePredictorWrapper(
vllm_config=vllm_config,
cp_config=cp_config,
wrapper_config=common_mod.CodePredictorWrapperConfig(use_cuda_graphs=False),
talker_hidden_size=cp_config.hidden_size,
)
# Default keeps the legacy power-of-two derivation.
assert wrapper._execution_batch_buckets is None
assert wrapper._batch_bucket_sizes() == [1, 2, 4]
wrapper.configure_mtp_execution_buckets([3, 1, 3, 0, 9])
assert wrapper._batch_bucket_sizes() == [1, 3, 4] # deduplicate/filter declaration and retain max
with pytest.raises(RuntimeError, match="before the first warmup"):
wrapper2 = common_mod.CodePredictorWrapper(
vllm_config=vllm_config,
cp_config=cp_config,
wrapper_config=common_mod.CodePredictorWrapperConfig(use_cuda_graphs=False),
talker_hidden_size=cp_config.hidden_size,
)
wrapper2._bucket_sizes = [1, 2, 4] # stand-in for warmed-up state
wrapper2.configure_mtp_execution_buckets([3])
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""
Tests for CUDA Graph decoder wrapper numerical equivalence.
@@ -142,6 +142,28 @@ def test_padded_output_shape_and_length(decoder, wrapper, seq_len):
assert graph_out.shape[-1] == expected_len
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_padded_batch_size_uses_larger_capture_bucket(decoder):
"""Non-exact batch sizes should replay the next captured batch bucket."""
w = CUDAGraphDecoderWrapper(
decoder=decoder,
capture_sizes=[25],
capture_batch_sizes=[1, 4],
num_quantizers=NUM_QUANTIZERS,
enabled=True,
)
w.warmup(DEVICE)
codes = _random_codes(25, batch_size=3)
with torch.no_grad():
eager_out = decoder(codes)
graph_out = w.decode(codes)
assert (4, 25) in w.graphs
assert graph_out.shape == eager_out.shape
torch.testing.assert_close(graph_out, eager_out, atol=0, rtol=0)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("seq_len", [10, 30, 47, 73, 99])
def test_padded_interior_positions_close(decoder, wrapper, seq_len):
@@ -0,0 +1,55 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Output metadata must not wait for the preceding model forward on CUDA."""
from types import SimpleNamespace
import pytest
import torch
from tests.helpers.mark import hardware_test
from vllm_omni.model_executor.models.qwen3_tts.qwen3_tts_talker import (
Qwen3TTSTalkerForConditionalGeneration,
)
from vllm_omni.worker.gpu_ar_model_runner import _snapshot_tensor_payload_to_cpu_async
pytestmark = [pytest.mark.core_model]
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.parametrize("validity_device", ["cpu", "cuda"])
@torch.inference_mode()
def test_output_validity_preserves_values_without_waiting_for_forward(validity_device):
talker = Qwen3TTSTalkerForConditionalGeneration.__new__(Qwen3TTSTalkerForConditionalGeneration)
talker.vllm_config = SimpleNamespace(model_config=SimpleNamespace(async_chunk=True))
hidden = torch.zeros((3, 8), device="cuda")
infos = [
{
"codes": {"audio": torch.zeros((frames, 16), device="cuda", dtype=torch.long)},
"meta": {"codec_frame_valid": torch.tensor(valid, device=validity_device)},
}
for frames, valid in [(2, True), (1, False)]
]
# Warm allocation paths before checking the stream-ordering contract.
talker.make_omni_output(hidden, model_intermediate_buffer=infos)
copy_stream = torch.cuda.Stream()
torch.accelerator.synchronize()
preceding_forward = torch.cuda.Event()
torch.cuda._sleep(500_000_000)
preceding_forward.record()
output = talker.make_omni_output(hidden, model_intermediate_buffer=infos)
waited_for_forward = preceding_forward.query()
validity = output.multimodal_outputs["meta"]["codec_frame_valid"]
snapshot = _snapshot_tensor_payload_to_cpu_async(
output.multimodal_outputs, copy_stream=copy_stream, pin_memory=True
)
snapshot.wait()
assert not waited_for_forward, "output construction synchronized the preceding forward"
assert validity.device.type == "cuda"
assert validity.dtype == torch.int8
cpu_validity = snapshot.payload["meta"]["codec_frame_valid"]
assert cpu_validity.device.type == "cpu"
assert cpu_validity.tolist() == [1, 1, 0]
assert all(info["meta"]["codec_frame_valid"].device.type == validity_device for info in infos)
@@ -0,0 +1,100 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Exercise actual stateless decoder capture, not a mocked capture indicator."""
import pytest
import torch
from tests.helpers.mark import hardware_marks
from vllm_omni.model_executor.models.qwen3_tts.tokenizer_12hz.configuration_qwen3_tts_tokenizer_v2 import (
Qwen3TTSTokenizerV2DecoderConfig,
)
from vllm_omni.model_executor.models.qwen3_tts.tokenizer_12hz.modeling_qwen3_tts_tokenizer_v2 import (
Qwen3TTSTokenizerV2DecoderTransformerModel,
)
pytestmark = [pytest.mark.core_model, *hardware_marks(res={"cuda": "L4"}, num_cards=1)]
@pytest.mark.parametrize("batch_size", [1, 2])
@pytest.mark.parametrize("input_mode", ["implicit", "positions"])
@torch.inference_mode()
def test_stateless_decoder_capture_replays_new_inputs(batch_size, input_mode):
sequence_length = 8
torch.manual_seed(42)
config = Qwen3TTSTokenizerV2DecoderConfig(
hidden_size=32,
latent_dim=16,
max_position_embeddings=512,
num_attention_heads=4,
num_key_value_heads=4,
intermediate_size=64,
num_hidden_layers=1,
num_quantizers=2,
sliding_window=16,
decoder_dim=32,
upsample_rates=(2,),
upsampling_ratios=(2,),
)
config._attn_implementation = "sdpa"
model = Qwen3TTSTokenizerV2DecoderTransformerModel._from_config(config).cuda().eval()
inputs = torch.randn(batch_size, sequence_length, config.latent_dim, device="cuda")
positions = {}
if input_mode == "positions":
cache_position = torch.arange(sequence_length, device="cuda")
positions = {"cache_position": cache_position, "position_ids": cache_position.unsqueeze(0)}
stream = torch.cuda.Stream()
stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(stream):
for _ in range(3):
model(inputs_embeds=inputs, use_cache=False, **positions)
torch.cuda.current_stream().wait_stream(stream)
torch.accelerator.synchronize()
# No wrapper or eager fallback: an illegal capture must fail this test.
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
captured_output = model(inputs_embeds=inputs, use_cache=False, **positions).last_hidden_state
outputs = []
for _ in range(3):
new_inputs = torch.randn_like(inputs)
expected = model(inputs_embeds=new_inputs, use_cache=False, **positions).last_hidden_state
if input_mode == "implicit":
# Compare the cached mask against the independent live-mask path,
# not only against eager execution using the same cached mask.
reference = model(
inputs_embeds=new_inputs,
use_cache=False,
position_ids=torch.arange(sequence_length, device="cuda").unsqueeze(0),
).last_hidden_state
torch.testing.assert_close(expected, reference, atol=1e-5, rtol=1e-4)
inputs.copy_(new_inputs)
graph.replay()
torch.accelerator.synchronize()
torch.testing.assert_close(captured_output, expected, atol=1e-5, rtol=1e-4)
outputs.append(captured_output.clone())
assert not torch.equal(outputs[0], outputs[1]), "replay must consume the new input"
@pytest.mark.parametrize("batch_size", [1, 2])
@torch.inference_mode()
def test_stateless_codec_wrapper_captures_and_replays_waveform(batch_size):
from tests.model_executor.models.qwen3_tts.test_qwen3_tts_incremental_decode import _make_small_decoder
from vllm_omni.model_executor.models.qwen3_tts.segmented_graph_wrapper import CUDAGraphDecoderWrapper
torch.manual_seed(42)
decoder = _make_small_decoder().cuda().eval()
wrapper = CUDAGraphDecoderWrapper(decoder, num_quantizers=2, async_chunk=False)
# Exercise the production capture method, including quantization and
# waveform convolutions, without accepting warmup's eager fallback.
wrapper._capture_stateless(batch_size, 8, torch.device("cuda"), torch.long)
state = wrapper.stateless_states[(batch_size, 8)]
for _ in range(3):
codes = torch.randint(0, 32, (batch_size, 2, 8), device="cuda")
expected = decoder._forward_exact(codes)
state["input"]["codes"].copy_(codes)
state["graph"].replay()
torch.accelerator.synchronize()
torch.testing.assert_close(state["output"], expected, atol=1e-5, rtol=1e-4)
assert torch.isfinite(state["output"]).all()
@@ -1,5 +1,9 @@
from collections import OrderedDict
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from collections import OrderedDict, UserDict
from types import SimpleNamespace
from typing import Any
import numpy as np
import pytest
@@ -36,6 +40,7 @@ def _make_minimal_talker(
"""
model = Qwen3TTSTalkerForConditionalGeneration.__new__(Qwen3TTSTalkerForConditionalGeneration)
model.talker_config = SimpleNamespace(codec_pad_id=7, num_code_groups=16)
model._codebook_vocab_size = 2048
model._embedding_dtype = torch.bfloat16
if tts_pad_embed is None:
tts_pad_embed = torch.zeros((1, 4), dtype=torch.bfloat16)
@@ -51,6 +56,22 @@ def _make_minimal_talker(
return model
def test_postprocess_batch_gathers_each_request_tail():
from vllm_omni.model_executor.models.output_templates import OwnedBatchTensor
model = _make_minimal_talker()
hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]])
key, values = model.postprocess_batch_mrv2(
hidden_states=hidden,
last_token_indices=torch.tensor([1, 3]),
)
assert key == ("hidden_states", "last")
assert isinstance(values, OwnedBatchTensor) # freshly allocated rows transfer ownership
assert torch.equal(values.tensor, torch.tensor([[3.0, 4.0], [7.0, 8.0]]))
def _make_minimal_builder(
*,
config: SimpleNamespace | None = None,
@@ -95,6 +116,7 @@ def _make_minimal_builder(
)
builder._speaker_cache = None
builder._text_tokenizer = None
builder._projected_token_cache = {}
builder._embedding_dtype = torch.bfloat16
builder._ref_audio_artifact_cache_max_entries = 256
builder._ref_audio_artifact_cache = OrderedDict()
@@ -259,6 +281,30 @@ def test_decode_compacts_long_trailing_text_after_large_offset():
assert torch.equal(update["hidden_states"]["trailing_text"], trailing_text[65:])
def test_decode_marks_codec_frame_validity_from_processed_input_token():
model = _make_minimal_talker()
model.embed_input_ids = lambda input_ids: input_ids.to(torch.float32).reshape(1, 1, 1).expand(1, 1, 4)
common = {
"input_embeds": None,
"text": ["hello"],
"task_type": ["CustomVoice"],
"hidden_states": {
"trailing_text": torch.ones((2, 4)),
"last": torch.ones(4),
},
"meta": {"talker_text_offset": 0},
"_omni_is_prefill": False,
"_omni_num_computed_tokens": 2,
"_omni_prompt_len": 2,
}
_, _, valid_update = model.preprocess(input_ids=torch.tensor([123]), **common)
_, _, eos_update = model.preprocess(input_ids=torch.tensor([4198]), **common)
assert valid_update["meta"]["codec_frame_valid"].item() is True
assert eos_update["meta"]["codec_frame_valid"].item() is False
def test_decode_replay_span_embeds_all_tokens_without_mutating_decode_state():
model = _make_minimal_talker()
@@ -286,14 +332,17 @@ def test_decode_replay_span_embeds_all_tokens_without_mutating_decode_state():
out_embeds.cpu(),
torch.tensor([[101.0] * 4, [202.0] * 4, [303.0] * 4], dtype=torch.bfloat16),
)
assert update == {"meta": {"codec_streaming": True}}
assert update["meta"]["codec_streaming"] is True
assert update["meta"]["codec_frame_valid"].item() is False
def test_decode_batch_preprocess_matches_decode_state_updates():
@pytest.mark.parametrize("mrv2", [False, True])
def test_decode_batch_preprocess_matches_decode_state_updates(mrv2):
tts_pad = torch.full((1, 4), -1.0, dtype=torch.bfloat16)
model = _make_minimal_talker(tts_pad_embed=tts_pad)
def fake_embed_input_ids(input_ids):
assert not mrv2, "MRV2 must reuse the prepared token embeddings"
return input_ids.to(torch.float32).reshape(-1, 1, 1).expand(-1, 1, 4)
model.embed_input_ids = fake_embed_input_ids
@@ -302,7 +351,10 @@ def test_decode_batch_preprocess_matches_decode_state_updates():
last_a = torch.full((4,), 2.0, dtype=torch.float32)
last_b = torch.full((4,), 3.0, dtype=torch.float32)
out_ids, out_embeds, past_hidden, text_step, updates = model.preprocess_decode_batch(
preprocess = model.preprocess_decode_batch_mrv2 if mrv2 else model.preprocess_decode_batch
prepared = torch.tensor([[101.0] * 4, [202.0] * 4], dtype=torch.bfloat16)
out_ids, out_embeds, past_hidden, text_step, updates = preprocess(
**({"input_embeds": prepared} if mrv2 else {}),
input_ids=torch.tensor([101, 202], dtype=torch.long),
req_infos=[
{
@@ -321,15 +373,19 @@ def test_decode_batch_preprocess_matches_decode_state_updates():
)
assert out_ids.tolist() == [101, 202]
if mrv2:
assert out_embeds.data_ptr() == prepared.data_ptr()
assert torch.equal(out_embeds.cpu(), torch.tensor([[101.0] * 4, [202.0] * 4], dtype=torch.bfloat16))
assert torch.equal(past_hidden.cpu(), torch.stack([last_a, last_b]).to(torch.bfloat16))
assert torch.equal(text_step[0].cpu(), trailing_a[1].to(torch.bfloat16))
assert torch.equal(text_step[1].cpu(), tts_pad.reshape(-1).to(torch.bfloat16))
assert updates[0]["meta"]["talker_text_offset"] == 2
assert updates[0]["meta"]["codec_streaming"] is True
assert updates[0]["meta"]["codec_frame_valid"].item() is True
assert "hidden_states" not in updates[0]
assert updates[1]["meta"]["talker_text_offset"] == 0
assert updates[1]["meta"]["codec_streaming"] is False
assert updates[1]["meta"]["codec_frame_valid"].item() is True
assert updates[1]["hidden_states"]["trailing_text"].numel() == 0
@@ -449,11 +505,11 @@ def test_base_voice_clone_batch_preprocess_encodes_ref_code_by_sample_rate():
def __call__(self, texts, *, padding=False):
self.calls.append((texts, padding))
return {"input_ids": [[idx + 1, idx + 2, idx + 3] for idx, _ in enumerate(texts)]}
return UserDict({"input_ids": [[1, 2, 3], [2, 3, 4, 5]]})
text_tok = FakeTextTokenizer()
builder._text_tokenizer = text_tok
buf = {
buf: dict[str, dict[str, Any]] = {
"r1": {
"task_type": ["Base"],
"text": ["one"],
@@ -488,9 +544,17 @@ def test_base_voice_clone_batch_preprocess_encodes_ref_code_by_sample_rate():
assert buf["r2"][NORMALIZED_REF_AUDIO_KEY][0] is wav2
assert len(text_tok.calls) == 2
assert torch.equal(buf["r1"][PRECOMPUTED_TEXT_IDS_KEY], torch.tensor([1, 2, 3]))
assert torch.equal(buf["r2"][PRECOMPUTED_TEXT_IDS_KEY], torch.tensor([2, 3, 4]))
assert torch.equal(buf["r2"][PRECOMPUTED_TEXT_IDS_KEY], torch.tensor([2, 3, 4, 5]))
assert torch.equal(buf["r1"][PRECOMPUTED_REF_IDS_KEY], torch.tensor([1, 2, 3]))
assert torch.equal(buf["r2"][PRECOMPUTED_REF_IDS_KEY], torch.tensor([2, 3, 4]))
assert torch.equal(buf["r2"][PRECOMPUTED_REF_IDS_KEY], torch.tensor([2, 3, 4, 5]))
assert (
buf["r1"][PRECOMPUTED_TEXT_IDS_KEY].untyped_storage().data_ptr()
== buf["r2"][PRECOMPUTED_TEXT_IDS_KEY].untyped_storage().data_ptr()
)
assert (
buf["r1"][PRECOMPUTED_REF_IDS_KEY].untyped_storage().data_ptr()
== buf["r2"][PRECOMPUTED_REF_IDS_KEY].untyped_storage().data_ptr()
)
def test_base_voice_clone_batch_preprocess_reuses_singleton_normalized_audio_without_speech_tokenizer():
@@ -506,7 +570,7 @@ def test_base_voice_clone_batch_preprocess_reuses_singleton_normalized_audio_wit
return {"input_ids": [[7, 8, 9] for _ in texts]}
builder._text_tokenizer = FakeTextTokenizer()
buf = {
buf: dict[str, dict[str, Any]] = {
"r1": {
"task_type": ["Base"],
"text": ["one"],
@@ -539,7 +603,7 @@ def test_base_voice_clone_batch_preprocess_skips_after_initial_prefill_state_exi
builder._encode_ref_audio_batch_fn = lambda *a, **kw: (_ for _ in ()).throw(
AssertionError("speech tokenizer not expected")
)
buf = {
buf: dict[str, dict[str, Any]] = {
"r1": {
"task_type": ["Base"],
"text": ["one"],
@@ -574,7 +638,7 @@ def test_base_voice_clone_batch_preprocess_uses_serving_artifact_cache_key_witho
return {"input_ids": [[7, 8, 9] for _ in texts]}
builder._text_tokenizer = FakeTextTokenizer()
buf = {
buf: dict[str, dict[str, Any]] = {
"r1": {
"task_type": ["Base"],
"text": ["one"],
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Regression tests for cross-request ``codes.ref`` leakage (#4370).
``make_omni_output`` used to collapse every request's reference codec
@@ -11,11 +11,14 @@ vocoder chunks then decoded with another request's reference voice as
context (audible onset timbre deformation at any concurrency > 1).
"""
from types import SimpleNamespace
import pytest
import torch
from vllm_omni.model_executor.models.qwen3_tts.qwen3_tts_talker import (
Qwen3TTSTalkerForConditionalGeneration,
_qwen3_tts_gpu_resident_buffer_keys,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -23,6 +26,18 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
_NUM_CODE_GROUPS = 16
def test_large_tts_prefill_artifacts_stay_gpu_resident_only_on_mrv2() -> None:
v1_keys = _qwen3_tts_gpu_resident_buffer_keys(False)
v2_keys = _qwen3_tts_gpu_resident_buffer_keys(True)
assert ("embed", "prefill") not in v1_keys
assert ("codes", "ref") not in v1_keys
assert ("meta", "codec_frame_valid") not in v1_keys
assert ("embed", "prefill") in v2_keys
assert ("codes", "ref") in v2_keys
assert ("meta", "codec_frame_valid") in v2_keys
def _make_talker() -> Qwen3TTSTalkerForConditionalGeneration:
# make_omni_output reads no instance state, so skip __init__ (no real
# config / checkpoint needed), same as the preprocess tests.
@@ -89,3 +104,42 @@ def test_make_omni_output_keeps_hidden_states_for_replay_spans_without_audio_cod
assert torch.equal(out.text_hidden_states, hidden)
assert out.multimodal_outputs["codes"]["audio"].shape == (1, _NUM_CODE_GROUPS)
def test_make_omni_output_preserves_per_request_codec_frame_validity() -> None:
talker = _make_talker()
talker.vllm_config = SimpleNamespace(model_config=SimpleNamespace(async_chunk=True))
valid = _info(1, None)
invalid = _info(1, None)
valid["meta"]["codec_frame_valid"] = True
invalid["meta"]["codec_frame_valid"] = False
out = talker.make_omni_output(
torch.zeros((2, 8)),
model_intermediate_buffer=[valid, invalid],
)
assert out.multimodal_outputs["meta"]["codec_frame_valid"].tolist() == [1, 0]
def test_make_omni_output_publishes_ref_once_for_kv_resumed_first_decode() -> None:
talker = _make_talker()
talker.vllm_config = SimpleNamespace(
model_config=SimpleNamespace(async_chunk=True),
)
ref_code = torch.ones((5, _NUM_CODE_GROUPS), dtype=torch.long)
info = _info(1, ref_code)
info["_omni_num_computed_tokens"] = 9
info["_omni_prompt_len"] = 5
first = talker.make_omni_output(
torch.zeros((1, 8)),
model_intermediate_buffer=[info],
)
second = talker.make_omni_output(
torch.zeros((1, 8)),
model_intermediate_buffer=[info],
)
assert first.multimodal_outputs["codes"]["ref"][0] is ref_code
assert "ref" not in second.multimodal_outputs["codes"]
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Unit tests for the Qwen3-TTS leading-silence ban (#4966, #5048).
``compute_logits`` suppresses the checkpoint-derived silence codec vocabulary
@@ -59,6 +59,7 @@ def _make_talker(*, ban_frames: int, logits: torch.Tensor, modes=None):
object.__setattr__(talker, "_silence_mask", silence_mask)
object.__setattr__(talker, "_codec_disallowed_mask", torch.zeros((_VOCAB,), dtype=torch.bool))
object.__setattr__(talker, "_batch_req_ids", req_ids)
object.__setattr__(talker, "_mrv2_silence_ban_mask", None)
object.__setattr__(
talker,
"_req_x_vector_only",
@@ -78,7 +79,26 @@ def _banned(row: torch.Tensor) -> set[int]:
return {int(i) for i in torch.nonzero(torch.isinf(row) & (row < 0)).flatten()}
# -------------------- the step gate --------------------
def test_mrv2_silence_ban_tracks_batch_order_scope_and_decode_window() -> None:
talker = _make_talker(ban_frames=3, logits=torch.zeros((3, _VOCAB)), modes=[True, False, True])
with talker.mrv2_sampling_context(
req_ids=["req2", "req1", "req0"],
num_output_tokens=torch.tensor([3, 0, 1]),
):
logits = talker.compute_logits(torch.zeros((3, 4)))
assert _banned(logits[0]) == set()
assert _banned(logits[1]) == set()
assert _banned(logits[2]) == set(_SILENCE_IDS)
# Prompt logprobs are computed after sampling and must remain unmasked.
assert not torch.isneginf(talker.compute_logits(torch.zeros((3, 4)))).any()
with talker.mrv2_sampling_context(
req_ids=["req2", "req1", "req0"],
num_output_tokens=torch.tensor([4, 1, 3]),
):
assert not torch.isneginf(talker.compute_logits(torch.zeros((3, 4)))).any()
def test_ban_disabled_leaves_logits_unchanged() -> None: