Merge branch 'main' into fix/pr4709-review-followup

This commit is contained in:
Gao Han authored and GitHub committed 2026-07-18 11:28:29 +08:00
commit e8193787cc
525 files changed
+38790 -11106

No files matched your search

@@ -18,8 +18,11 @@ import types
import pytest
import torch
import torch.nn.functional as F
from pytest_mock import MockerFixture
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
# Direct file import to avoid vllm_omni.__init__ patch dependencies.
_MODELS = os.path.join(
os.path.dirname(__file__),
@@ -46,6 +49,11 @@ def _load_module(name: str, filename: str):
def _build_mock_modules(mocker: MockerFixture) -> dict[str, object]:
"""Build the dict of modules to inject into sys.modules."""
class NativeCustomOp(torch.nn.Module):
def forward(self, *args, **kwargs):
return self.forward_native(*args, **kwargs)
platforms_mock = mocker.MagicMock()
platforms_mock.current_omni_platform.supports_torch_inductor.return_value = False
platforms_mock.current_omni_platform.is_npu.return_value = False
@@ -73,6 +81,8 @@ def _build_mock_modules(mocker: MockerFixture) -> dict[str, object]:
vllm_parallel_mock = mocker.MagicMock()
vllm_parallel_mock.VocabParallelEmbedding = torch.nn.Embedding
custom_op_mock = types.ModuleType("vllm_omni.diffusion.layers.custom_op")
custom_op_mock.CustomOp = NativeCustomOp
return {
"vllm_omni": mocker.MagicMock(),
@@ -82,6 +92,7 @@ def _build_mock_modules(mocker: MockerFixture) -> dict[str, object]:
"vllm.config.vllm": vllm_config_mod,
"vllm.model_executor.model_loader.weight_utils": weight_utils_mock,
"vllm.model_executor.layers.vocab_parallel_embedding": vllm_parallel_mock,
"vllm_omni.diffusion.layers.custom_op": custom_op_mock,
"vllm_omni.model_executor": types.ModuleType("vllm_omni.model_executor"),
"vllm_omni.model_executor.models": models_pkg,
"vllm_omni.model_executor.models.common": common_pkg,
@@ -165,6 +176,38 @@ def _make_vllm_config(mocker: MockerFixture, max_num_seqs: int = 4):
return vllm_config
def test_npu_custom_ops_use_fused_norm_and_cached_rope(mocker: MockerFixture, loaded_target_classes) -> None:
common_mod = sys.modules["vllm_omni.model_executor.models.common.qwen3_code_predictor"]
cp_config, _ = _make_tiny_config(loaded_target_classes)
rms_calls = []
def npu_rms_norm(hidden_states, weight, epsilon):
rms_calls.append((hidden_states, weight, epsilon))
return hidden_states + 1, None
mocker.patch.object(common_mod.current_omni_platform, "is_npu", return_value=True)
mocker.patch.object(
common_mod,
"torch_npu",
types.SimpleNamespace(npu_rms_norm=npu_rms_norm),
create=True,
)
hidden_states = torch.zeros(1, 2, cp_config.hidden_size, dtype=torch.float16)
norm = common_mod._RMSNorm(cp_config.hidden_size, eps=cp_config.rms_norm_eps)
torch.testing.assert_close(norm.forward_npu(hidden_states), hidden_states + 1)
assert len(rms_calls) == 1
assert rms_calls[0][2] == cp_config.rms_norm_eps
rotary = common_mod._RotaryEmbedding(cp_config)
assert rotary.cos_cached.shape == (cp_config.num_code_groups + 1, cp_config.head_dim)
assert rotary.sin_cached.shape == rotary.cos_cached.shape
position_ids = torch.tensor([[0, 2, 4]])
cos, sin = rotary.forward_npu(hidden_states, position_ids)
torch.testing.assert_close(cos, rotary.cos_cached[position_ids].to(torch.float16))
torch.testing.assert_close(sin, rotary.sin_cached[position_ids].to(torch.float16))
class TestCodePredictorDtypeAlignment:
"""Test that code predictor buffers match model parameter dtype."""
@@ -339,6 +382,91 @@ class TestCodePredictorDtypeAlignment:
assert not torch.equal(first[:, 1:], different[:, 1:])
class TestCodePredictorPerRowGenerators:
"""Seeded requests must stay deterministic inside a multi-row batch (#4883)."""
def _make_predictor(self, mocker: MockerFixture, loaded_target_classes):
_, _, code_predictor_wrapper, _, _ = 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, talker_config = _make_tiny_config(loaded_target_classes)
vllm_config = _make_vllm_config(mocker, max_num_seqs=4)
predictor = code_predictor_wrapper(
vllm_config=vllm_config,
config=cp_config,
talker_config=talker_config,
)
predictor._wrapper_config.use_cuda_graphs = False
return predictor, talker_config
def test_multinomial_per_row_matches_single_row_draws(self, mocker: MockerFixture, loaded_target_classes) -> None:
common_mod = sys.modules["vllm_omni.model_executor.models.common.qwen3_code_predictor"]
multinomial = common_mod.CodePredictorWrapper._multinomial
torch.manual_seed(7)
probs = torch.softmax(torch.randn(3, 64), dim=-1)
def seeded(seed: int) -> torch.Generator:
generator = torch.Generator(device=probs.device)
generator.manual_seed(seed)
return generator
batched = multinomial(probs, None, [seeded(11), None, seeded(22)])
assert batched.shape == (3, 1)
# Each seeded row must reproduce a standalone draw from the same seed,
# independent of what the other rows in the batch consume.
assert torch.equal(batched[0:1], torch.multinomial(probs[0:1], num_samples=1, generator=seeded(11)))
assert torch.equal(batched[2:3], torch.multinomial(probs[2:3], num_samples=1, generator=seeded(22)))
def test_forward_per_row_generators_are_row_independent(self, mocker: MockerFixture, loaded_target_classes) -> None:
predictor, talker_config = self._make_predictor(mocker, loaded_target_classes)
hidden = talker_config.hidden_size
torch.manual_seed(123)
row_embed = torch.randn(1, hidden)
row_hidden = torch.randn(1, hidden)
# Three identical rows: two share a seed, one differs.
layer0_code = torch.zeros(3, dtype=torch.long)
layer0_embed = row_embed.expand(3, hidden).contiguous()
last_talker_hidden = row_hidden.expand(3, hidden).contiguous()
def seeded(seed: int) -> torch.Generator:
generator = torch.Generator(device=layer0_code.device)
generator.manual_seed(seed)
return generator
def run(generators):
return predictor(
layer0_code=layer0_code,
layer0_embed=layer0_embed,
last_talker_hidden=last_talker_hidden,
do_sample=True,
temperature=0.9,
top_k=50,
top_p=1.0,
generators=generators,
)
first = run([seeded(1234), seeded(1234), seeded(4321)])
second = run([seeded(1234), seeded(1234), seeded(4321)])
# Same per-row seeds -> the whole batch reproduces across calls.
assert torch.equal(first, second)
# Identical inputs + identical seeds -> identical rows within a call.
assert torch.equal(first[0], first[1])
# A different seed on the same inputs must diverge in the residual layers.
assert not torch.equal(first[0, 1:], first[2, 1:])
def test_forward_rejects_mismatched_generators_length(self, mocker: MockerFixture, loaded_target_classes) -> None:
predictor, talker_config = self._make_predictor(mocker, loaded_target_classes)
hidden = talker_config.hidden_size
with pytest.raises(ValueError, match="one entry per row"):
predictor(
layer0_code=torch.zeros(2, dtype=torch.long),
layer0_embed=torch.randn(2, hidden),
last_talker_hidden=torch.randn(2, hidden),
generators=[torch.Generator()],
)
class TestCodePredictorModelDtype:
"""Test the inner model forward with different dtypes."""
@@ -457,3 +585,92 @@ class TestCodePredictorWrapperConfig:
wrapper_config=common_mod.CodePredictorWrapperConfig(use_cuda_graphs=True),
)
assert graph_wrapper._prefix_graphs_enabled is True
class TestGumbelMaxSampling:
"""Sanity tests for the shared Gumbel-max sampling helper."""
@staticmethod
def _sample(logits: torch.Tensor, generator=None) -> torch.Tensor:
common_mod = sys.modules["vllm_omni.model_executor.models.common.qwen3_code_predictor"]
return common_mod.CodePredictorWrapper._sample_codes_gumbel(logits, generator=generator)
def test_gumbel_max_picks_unique_high_logit(self, loaded_target_classes) -> None:
"""If one logit is dominant, Gumbel-max must always pick it."""
torch.manual_seed(0)
bsz, vocab = 4, 64
logits = torch.full((bsz, vocab), -1e4)
logits[:, 7] = 0.0 # only finite entry
out = self._sample(logits)
assert out.shape == (bsz, 1)
assert torch.equal(out.squeeze(-1), torch.full((bsz,), 7, dtype=torch.long))
def test_gumbel_max_handles_single_finite_entry_rows(self, loaded_target_classes) -> None:
"""Degenerate rows with one surviving finite entry should still return
a valid index instead of tripping multinomial-style asserts."""
bsz, vocab = 2, 8
logits = torch.full((bsz, vocab), float("-inf"))
logits[0, 3] = 0.0
logits[1, 5] = 0.0
out = self._sample(logits)
assert out[0].item() == 3
assert out[1].item() == 5
def test_gumbel_max_generator_makes_results_reproducible(self, loaded_target_classes) -> None:
"""Same ``Generator.manual_seed`` should produce identical samples."""
torch.manual_seed(0)
logits = torch.randn(8, 32)
g1 = torch.Generator()
g1.manual_seed(123)
g2 = torch.Generator()
g2.manual_seed(123)
g3 = torch.Generator()
g3.manual_seed(456)
s1 = self._sample(logits, generator=g1)
s2 = self._sample(logits, generator=g2)
s3 = self._sample(logits, generator=g3)
assert torch.equal(s1, s2)
# Different seeds must (almost surely) yield a different sequence.
assert not torch.equal(s1, s3)
def test_gumbel_max_per_row_generators_match_individual_rows(self, loaded_target_classes) -> None:
"""A per-row generator list should preserve row independence."""
torch.manual_seed(11)
logits = torch.randn(3, 16)
seeds = [101, 202, 303]
batched_generators = []
expected_rows = []
for row, seed in enumerate(seeds):
row_generator = torch.Generator()
row_generator.manual_seed(seed)
batched_generators.append(row_generator)
solo_generator = torch.Generator()
solo_generator.manual_seed(seed)
expected_rows.append(self._sample(logits[row : row + 1], generator=solo_generator))
batched = self._sample(logits, generator=batched_generators)
expected = torch.cat(expected_rows, dim=0)
assert torch.equal(batched, expected)
def test_gumbel_max_rejects_generator_length_mismatch(self, loaded_target_classes) -> None:
logits = torch.randn(2, 8)
generators = [torch.Generator()]
with pytest.raises(ValueError, match="Expected 2 per-row generators"):
self._sample(logits, generator=generators)
def test_gumbel_max_distribution_matches_softmax(self, loaded_target_classes) -> None:
"""Empirical frequency under Gumbel-max should be close to the
softmax probability mass. Loose tolerance because we draw a
finite number of samples."""
torch.manual_seed(7)
vocab = 16
logits = torch.randn(vocab) * 2.0
target = F.softmax(logits, dim=-1, dtype=torch.float32)
n_draws = 20000
batched_logits = logits.unsqueeze(0).expand(n_draws, vocab).contiguous()
samples = self._sample(batched_logits).squeeze(-1)
empirical = torch.bincount(samples, minlength=vocab).float() / n_draws
# L1 distance under 0.03 with 20k draws is comfortable for vocab=16.
assert (empirical - target).abs().sum().item() < 0.05
@@ -0,0 +1,148 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Regression tests for vLLM's KV-cache scale mapper load path."""
from collections.abc import Iterable
from pathlib import Path
from types import SimpleNamespace
import pytest
import torch
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
_REPO_ROOT = Path(__file__).resolve().parents[3]
_STALE_API_FILES = [
"vllm_omni/model_executor/models/hunyuan_image3/hunyuan_image3.py",
"vllm_omni/model_executor/models/mammoth_moda2/mammoth_moda2.py",
"vllm_omni/model_executor/models/mimo_audio/mimo_audio_llm.py",
"vllm_omni/model_executor/models/qwen2_5_omni/qwen2_old.py",
]
_SOURCE_SCALE_NAME = "layers.0.self_attn.k_proj.output_scale"
_MAPPED_SCALE_NAME = "layers.0.self_attn.attn.k_scale"
class _RecordingCacheScaleMapper:
def __init__(self) -> None:
self.called = False
def apply(
self,
weights: Iterable[tuple[str, torch.Tensor]],
) -> Iterable[tuple[str, torch.Tensor]]:
self.called = True
for name, weight in weights:
yield name.replace(".k_proj.output_scale", ".attn.k_scale"), weight
class _QuantConfigWithoutOldCacheScale:
def __init__(self) -> None:
self.mapper = _RecordingCacheScaleMapper()
def get_cache_scale_mapper(self) -> _RecordingCacheScaleMapper:
return self.mapper
class _FakeModel:
def __init__(self, quant_config: _QuantConfigWithoutOldCacheScale) -> None:
self.quant_config = quant_config
self.param = torch.nn.Parameter(torch.zeros(()), requires_grad=False)
def named_parameters(
self,
remove_duplicate: bool = True,
) -> Iterable[tuple[str, torch.nn.Parameter]]:
del remove_duplicate
return [(_MAPPED_SCALE_NAME, self.param)]
class _FakeHunyuanModel(_FakeModel):
def __init__(self, quant_config: _QuantConfigWithoutOldCacheScale) -> None:
super().__init__(quant_config)
self.config = SimpleNamespace(
tie_word_embeddings=False,
num_attention_heads=1,
num_key_value_heads=1,
use_cla=False,
)
def get_expert_mapping(self) -> tuple[list[object], dict[str, object]]:
return [], {}
def _split_qkv_weight(self, weight: torch.Tensor) -> torch.Tensor:
return weight
@pytest.mark.parametrize("rel_path", _STALE_API_FILES)
def test_model_loaders_do_not_call_removed_get_cache_scale(rel_path: str) -> None:
"""Affected loaders must not call the removed vLLM get_cache_scale API."""
source = (_REPO_ROOT / rel_path).read_text(encoding="utf-8")
assert ".get_cache_scale(" not in source
@pytest.mark.parametrize(
("module_name", "class_name", "fake_model_cls"),
[
(
"vllm_omni.model_executor.models.hunyuan_image3.hunyuan_image3",
"HunyuanModel",
_FakeHunyuanModel,
),
(
"vllm_omni.model_executor.models.qwen2_5_omni.qwen2_old",
"Qwen2Model",
_FakeModel,
),
],
)
def test_auto_weights_loader_delegated_loaders_accept_mapped_cache_scale(
module_name: str,
class_name: str,
fake_model_cls: type[_FakeModel],
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Delegated loaders must not expect the removed get_cache_scale method."""
module = __import__(module_name, fromlist=[class_name])
monkeypatch.setattr(module, "is_pp_missing_parameter", lambda *_: False)
fake_model = fake_model_cls(_QuantConfigWithoutOldCacheScale())
load_weights = getattr(module, class_name).load_weights
loaded = load_weights(fake_model, [(_MAPPED_SCALE_NAME, torch.ones(()))])
assert loaded == {_MAPPED_SCALE_NAME}
torch.testing.assert_close(fake_model.param.data, torch.ones(()))
@pytest.mark.parametrize(
("module_name", "class_name"),
[
(
"vllm_omni.model_executor.models.mammoth_moda2.mammoth_moda2",
"MammothModa2Qwen2ForCausalLM",
),
(
"vllm_omni.model_executor.models.mimo_audio.mimo_audio_llm",
"MiMoAudioLLMForConditionalGeneration",
),
],
)
def test_direct_custom_loaders_apply_cache_scale_mapper(
module_name: str,
class_name: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Direct custom loaders must apply the new mapper before fall-through load."""
module = __import__(module_name, fromlist=[class_name])
monkeypatch.setattr(module, "is_pp_missing_parameter", lambda *_: False)
quant_config = _QuantConfigWithoutOldCacheScale()
fake_model = _FakeModel(quant_config)
load_weights = getattr(module, class_name).load_weights
loaded = load_weights(fake_model, [(_SOURCE_SCALE_NAME, torch.ones(()))])
assert quant_config.mapper.called
assert loaded == {_MAPPED_SCALE_NAME}
torch.testing.assert_close(fake_model.param.data, torch.ones(()))
@@ -0,0 +1,208 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Static regression checks for VoxCPM2 runner-assisted unified graph hooks.
These tests intentionally avoid importing torch/vLLM so they can run in a
lightweight local checkout. Runtime audio quality still requires a CUDA test.
"""
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[4]
RUNNER = REPO_ROOT / "vllm_omni" / "worker" / "gpu_ar_model_runner.py"
RUNNER_ASSISTED_METADATA = REPO_ROOT / "vllm_omni" / "worker" / "runner_assisted_metadata.py"
TALKER = REPO_ROOT / "vllm_omni" / "model_executor" / "models" / "voxcpm2" / "voxcpm2_talker.py"
VOXCPM2_RUNTIME_CONFIG = REPO_ROOT / "vllm_omni" / "model_executor" / "models" / "voxcpm2" / "runtime_config.py"
VOXCPM2_PIPELINE = REPO_ROOT / "vllm_omni" / "model_executor" / "models" / "voxcpm2" / "pipeline.py"
VOXCPM2_SCHEDULER = REPO_ROOT / "vllm_omni" / "model_executor" / "models" / "voxcpm2" / "scheduler.py"
OMNI_AR_SCHEDULER = REPO_ROOT / "vllm_omni" / "core" / "sched" / "omni_ar_scheduler.py"
DEPLOY = REPO_ROOT / "vllm_omni" / "deploy" / "voxcpm2.yaml"
def test_ar_runner_exposes_runner_assisted_full_metadata_hook():
source = RUNNER.read_text()
assert "Models without this hook keep the normal runner path" in source
assert "get_runner_assisted_full_attention_metadata_request" in source
assert "set_runner_assisted_full_attention_metadata_context" in source
assert "runner_assisted_full_attn" in source
assert "runner_assisted_full_attn_capture" in source
assert "pad_attn = cudagraph_mode == CUDAGraphMode.FULL or runner_assisted_full_attn" in source
assert "for_cudagraph_capture=runner_assisted_full_attn_capture" in source
assert "cudagraph_runtime_mode=(" in source
assert "CUDAGraphMode.FULL if runner_assisted_full_attn else cudagraph_mode" in source
refresh_source = source[source.index("def _refresh_runner_assisted_full_attention_metadata_buffers") :]
refresh_source = refresh_source[: refresh_source.index("def _set_runner_assisted_full_attention_metadata_context")]
assert "num_computed_tokens_cpu" in refresh_source
assert "num_scheduled_tokens_np" in refresh_source
assert "optimistic_seq_lens_cpu[:num_reqs]=1" not in "".join(refresh_source.split())
padding_source = source[
source.index("runner_assisted_full_attn_request = self._get_runner_assisted_full_attention_metadata_request") :
]
padding_source = padding_source[: padding_source.index("ubatch_slices, ubatch_slices_padded")]
assert "runner_assisted_full_attn_request.num_reqs_padded" in padding_source
assert "runner_assisted_full_attn_request.for_cudagraph_capture" in padding_source
assert "num_reqs_padded * max_num_scheduled_tokens" in padding_source
def test_ar_runner_without_model_hook_stays_on_normal_path():
source = RUNNER.read_text()
contract_source = RUNNER_ASSISTED_METADATA.read_text()
request_source = source[source.index("def _get_runner_assisted_full_attention_metadata_request") :]
request_source = request_source[
: request_source.index("def _refresh_runner_assisted_full_attention_metadata_buffers")
]
compact_request_source = "".join(request_source.split())
assert "Models without this hook keep the normal runner path" in request_source
assert "class RunnerAssistedAttentionMetadataProvider(Protocol)" in contract_source
assert "class RunnerAssistedFullAttentionMetadataRequest(NamedTuple)" in contract_source
assert "tuple[int, bool]" not in contract_source
assert 'hook=getattr(self.model,"get_runner_assisted_full_attention_metadata_request",None)' in (
compact_request_source
)
assert "ifnotcallable(hook):returnNone" in compact_request_source
assert ".tolist()" not in compact_request_source
assert "isinstance(request,RunnerAssistedFullAttentionMetadataRequest)" in compact_request_source
assert "request.num_reqs_padded" in request_source
assert "request.for_cudagraph_capture" in request_source
assert "exceptException" not in compact_request_source
set_context_source = source[source.index("def _set_runner_assisted_full_attention_metadata_context") :]
set_context_source = set_context_source[: set_context_source.index("def _deferred_prefix_cache_mm_keys")]
assert "except Exception" not in set_context_source
def test_voxcpm2_graph_paths_fail_closed_and_preserve_deterministic_noise():
source = TALKER.read_text()
runtime_config_source = VOXCPM2_RUNTIME_CONFIG.read_text()
compact_source = "".join(source.split())
assert "_voxcpm2_compile_without_inductor_cudagraphs" in source
assert "def _compile_without_inductor_cudagraphs" not in source
assert "_voxcpm2_unwrap_torch_compile" in source
assert "_orig_mod" in source
compile_source = source[source.index("def _voxcpm2_compile_without_inductor_cudagraphs") :]
compile_source = compile_source[: compile_source.index("def _setup_torch_compile")]
assert "torch._inductor.config" not in compile_source
assert '"triton.cudagraphs": False' in compile_source
assert '"triton.cudagraph_trees": False' in compile_source
assert "self._enable_unified_decode_graph=self._runtime_config.unified_decode_graph_available(" in compact_source
assert "andnotself.deterministic_cfm_noise" in "".join(runtime_config_source.split())
assert "decode_tail_graph" not in source
unified_source = source[source.index("def _forward_unified_decode") :]
unified_source = unified_source[: unified_source.index("# -------------------- vllm hooks")]
assert "except Exception:" not in unified_source
assert "_failed_unified_graph_sizes" not in source
assert "self._enable_unified_decode_graph = False" not in unified_source
assert "_forward_unified_decode_fallback" not in source
assert "capture_failed" not in compact_source
assert "record_fallback" not in source
assert "fallbacks" not in source
assert "**info_dict: Unpack[VoxCPM2PreprocessInput]" in source
assert "**info: Unpack[VoxCPM2PostprocessInput]" in source
assert "reference_audio: Any" not in source
assert "prompt_audio: Any" not in source
assert "def _nullify_volatile_metadata(ctx: _ForwardContextLike) -> _ForwardContextLike" in source
assert "class _PrefillInputs(NamedTuple)" in source
assert "-> _PrefillInputs" in source
assert 'inputs["audio_feat"]' not in source
assert 'inputs["text_token"]' not in source
capture_source = source[source.index("def _capture_unified_decode_graph") :]
capture_source = capture_source[: capture_source.index("def _unified_decode_graph_skip_reason")]
assert "_voxcpm2_compile_unified_capture_estimator" in capture_source
assert "_voxcpm2_compile_unified_capture_feat_encoder" in capture_source
graph_body = capture_source[capture_source.index("with torch.cuda.graph") :]
assert "g.cfm_noise.normal_()" not in graph_body
assert "g.cfm_noise.normal_()" in unified_source
assert "_pre_capture_unified_graphs" not in source
assert "_unified_graph_pre_capture_sizes" not in source
assert "unified_decode_graph_pre_capture_sizes" not in source
def test_voxcpm2_batch_unified_graph_requires_runner_metadata_marker():
source = TALKER.read_text()
compact_source = "".join(source.split())
assert "enable_runner_assisted_unified_decode_graph" not in source
assert "allow_unified_decode_graph_batch_attention" not in source
assert "get_runner_assisted_full_attention_metadata_request" in source
assert "set_runner_assisted_full_attention_metadata_context" in source
assert "RunnerAssistedFullAttentionMetadataRequest" in source
assert "_runner_assisted_unified_decode_graph_active" in source
assert "runner_full_metadata_missing" in source
assert "_build_unified_graph_bucket_sizes" in source
assert "_select_unified_graph_bucket_size" in source
capture_source = source[source.index("def _capture_unified_decode_graph") :]
capture_source = capture_source[: capture_source.index("def _unified_decode_graph_skip_reason")]
assert "override_forward_context" in capture_source
assert "_nullify_volatile_metadata" in capture_source
assert "capture_context = override_forward_context(self._nullify_volatile_metadata(ctx))" in capture_source
assert "ifsize>1:continue" not in compact_source
forward_source = source[source.index("def _forward_unified_decode") :]
forward_source = forward_source[: forward_source.index("# -------------------- vllm hooks")]
assert "graph_size = self._select_unified_graph_bucket_size(num_reqs)" in forward_source
assert "self._unified_graphs[graph_size]" in forward_source
assert "g.input_embeds[num_reqs:graph_size].zero_()" not in forward_source
assert "padded rows as valid duplicates" in forward_source
assert "g.input_embeds[last : last + 1].expand" in forward_source
assert "ifnum_reqs>1andnotself._runner_assisted_unified_decode_graph_active" in compact_source
assert "andnotcfg.enable_runner_assisted_unified_decode_graph" not in compact_source
needs_source = source[source.index("def get_runner_assisted_full_attention_metadata_request") :]
needs_source = needs_source[: needs_source.index("def set_runner_assisted_full_attention_metadata_context")]
assert "_select_unified_graph_bucket_size(num_reqs)" in needs_source
assert "_should_use_decode_graph(num_reqs)" not in needs_source
assert "RunnerAssistedFullAttentionMetadataRequest(" in needs_source
def test_voxcpm2_unified_skip_preserves_segmented_decode_graphs():
source = TALKER.read_text()
compact_source = "".join(source.split())
forward_source = source[source.index("def forward(") :]
forward_source = forward_source[: forward_source.index("# -------------------- prefill / decode helpers")]
assert "use_segmented_decode_graph" in forward_source
assert "unified_skip_reasonisnotNone" in compact_source
assert "notself._enable_unified_decode_graphorunified_skip_reasonisnotNone" in compact_source
assert "ifuse_segmented_decode_graph:" in compact_source
assert "anduse_segmented_decode_graph" in compact_source
def test_voxcpm2_scheduler_policy_stays_model_local():
common_source = OMNI_AR_SCHEDULER.read_text()
voxcpm2_scheduler_source = VOXCPM2_SCHEDULER.read_text()
pipeline_source = VOXCPM2_PIPELINE.read_text()
assert "VoxCPM2TalkerForConditionalGeneration" not in common_source
assert "voxcpm2_runtime_config" not in common_source
assert "pure_decode_graph" not in common_source
assert "_schedule_with_optional_waiting_deferral" not in common_source
assert "def _should_defer_waiting_admission(self) -> bool:" in common_source
assert "class VoxCPM2OmniARAsyncScheduler(OmniARAsyncScheduler)" in voxcpm2_scheduler_source
assert "from .runtime_config import _VoxCPM2RuntimeConfig" in voxcpm2_scheduler_source
assert "_VoxCPM2RuntimeConfig.from_vllm_config(self.vllm_config)" in voxcpm2_scheduler_source
assert "unified_decode_graph_available(use_cuda_graph=current_omni_platform.is_cuda())" in voxcpm2_scheduler_source
assert "_should_defer_waiting_for_unified_decode_graph" in voxcpm2_scheduler_source
assert "def schedule(" not in voxcpm2_scheduler_source
assert "create_request_queue" not in voxcpm2_scheduler_source
assert "original_waiting.prepend_requests(deferred_waiting)" not in voxcpm2_scheduler_source
assert "return self._should_defer_waiting_for_unified_decode_graph()" in voxcpm2_scheduler_source
assert "vllm_omni.model_executor.models.voxcpm2.scheduler.VoxCPM2OmniARAsyncScheduler" in pipeline_source
def test_voxcpm2_deploy_defaults_to_full_unified_graph_only():
source = DEPLOY.read_text()
assert "max_num_seqs: 8" in source
assert "enable_unified_decode_graph: true" in source
assert "unified_decode_graph_max_batch_size: 8" in source
assert "unified_decode_graph_pre_capture_sizes" not in source
assert "enable_runner_assisted_unified_decode_graph" not in source
assert "allow_unified_decode_graph_batch_attention" not in source
assert "decode_graph_capture_policy" not in source
@@ -52,6 +52,10 @@ def _make_bare_talker():
talker._cfm_buffers = None
talker._last_audio_output_req_ids = []
talker._batched_fsq_fusion_max_batch = 32
# Added in VoxCPM2TalkerForConditionalGeneration.__init__; set to
# default (False) to avoid AttributeError when test exercises forward
# path that checks this flag.
talker._enable_unified_decode_graph = False
return talker
@@ -329,23 +333,27 @@ class TestDecodeBatchContract:
for (expected_res, expected_meta), (actual_res, actual_meta) in zip(expected, actual, strict=True):
torch.testing.assert_close(actual_res, expected_res)
torch.testing.assert_close(actual_meta["new_lm_hidden"], expected_meta["new_lm_hidden"])
torch.testing.assert_close(actual_meta.new_lm_hidden, expected_meta.new_lm_hidden)
assert [state.decode_step_count for state in batch_states] == [1, 1]
def test_batch_decode_matches_per_request_state_updates(self) -> None:
from vllm_omni.model_executor.models.voxcpm2.voxcpm2_talker import (
_DecodeResidualMeta,
)
seq_talker = self._make_decode_talker()
batch_talker = self._make_decode_talker()
seq_states = [self._make_decode_state("req-0", 0.0), self._make_decode_state("req-1", 10.0)]
batch_states = [self._make_decode_state("req-0", 0.0), self._make_decode_state("req-1", 10.0)]
metas = [
{"new_lm_hidden": torch.tensor([[1.0, 2.0, 3.0, 4.0]])},
{"new_lm_hidden": torch.tensor([[5.0, 6.0, 7.0, 8.0]])},
_DecodeResidualMeta(new_lm_hidden=torch.tensor([[1.0, 2.0, 3.0, 4.0]])),
_DecodeResidualMeta(new_lm_hidden=torch.tensor([[5.0, 6.0, 7.0, 8.0]])),
]
batch_out = torch.tensor([[0.5, 1.0, 1.5, 2.0], [2.5, 3.0, 3.5, 4.0]])
for state, meta, res_out in zip(seq_states, metas, batch_out, strict=True):
seq_talker._finish_decode(state, meta, res_out.unsqueeze(0), torch.device("cpu"))
seq_talker._finish_decode(state, meta, res_out.unsqueeze(0))
batch_talker._finish_decode_batch(
[(state, False, meta) for state, meta in zip(batch_states, metas, strict=True)],
@@ -360,12 +368,16 @@ class TestDecodeBatchContract:
torch.testing.assert_close(actual.last_audio_patch_gpu, expected.last_audio_patch_gpu)
def test_batch_decode_preserves_per_request_cfm_rng_order(self) -> None:
from vllm_omni.model_executor.models.voxcpm2.voxcpm2_talker import (
_DecodeResidualMeta,
)
talker = self._make_decode_talker()
talker._enable_batched_cfm = False
states = [self._make_decode_state("req-0", 0.0), self._make_decode_state("req-1", 10.0)]
metas = [
{"new_lm_hidden": torch.tensor([[1.0, 2.0, 3.0, 4.0]])},
{"new_lm_hidden": torch.tensor([[5.0, 6.0, 7.0, 8.0]])},
_DecodeResidualMeta(new_lm_hidden=torch.tensor([[1.0, 2.0, 3.0, 4.0]])),
_DecodeResidualMeta(new_lm_hidden=torch.tensor([[5.0, 6.0, 7.0, 8.0]])),
]
batch_out = torch.tensor([[0.5, 1.0, 1.5, 2.0], [2.5, 3.0, 3.5, 4.0]])
@@ -0,0 +1,78 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import functools
import pytest
import torch
import torch.nn as nn
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@functools.lru_cache(maxsize=1)
def _voxtral_tts_model_cls():
from tests.model_executor.helpers import bootstrap_vllm_layer_custom_op_modules
bootstrap_vllm_layer_custom_op_modules()
import vllm.model_executor.models.utils # noqa: F401
from vllm_omni.model_executor.models.voxtral_tts.voxtral_tts import (
VoxtralTTSForConditionalGeneration,
)
return VoxtralTTSForConditionalGeneration
class FakeAudioGeneration(nn.Module):
def __init__(self):
super().__init__()
self.embed_multimodal_calls = []
def embed_multimodal(self, **kwargs):
self.embed_multimodal_calls.append(kwargs)
return [torch.full((1, 4), 7.0)]
def _make_voxtral_tts_model():
model_cls = _voxtral_tts_model_cls()
model = model_cls.__new__(model_cls)
nn.Module.__init__(model)
model.model_stage = "audio_generation"
model.model = FakeAudioGeneration()
model._audio_token_id = 42
return model
def test_tts_preprocess_consumes_nested_codes_audio_feedback():
model = _make_voxtral_tts_model()
input_ids = torch.tensor([model._audio_token_id])
input_embeds = torch.zeros((1, 4))
audio_tokens = torch.tensor([[1, 2, 3, 4]])
_, output_embeds, _ = model.tts_preprocess(
input_ids=input_ids,
input_embeds=input_embeds,
codes={"audio": audio_tokens},
)
assert len(model.model.embed_multimodal_calls) == 1
torch.testing.assert_close(model.model.embed_multimodal_calls[0]["audio_tokens"], audio_tokens)
torch.testing.assert_close(output_embeds, torch.full((1, 4), 7.0))
def test_tts_preprocess_keeps_legacy_top_level_audio_feedback():
model = _make_voxtral_tts_model()
input_ids = torch.tensor([model._audio_token_id])
input_embeds = torch.zeros((1, 4))
audio_tokens = torch.tensor([[5, 6, 7, 8]])
_, output_embeds, _ = model.tts_preprocess(
input_ids=input_ids,
input_embeds=input_embeds,
audio=audio_tokens,
)
assert len(model.model.embed_multimodal_calls) == 1
torch.testing.assert_close(model.model.embed_multimodal_calls[0]["audio_tokens"], audio_tokens)
torch.testing.assert_close(output_embeds, torch.full((1, 4), 7.0))
@@ -8,7 +8,6 @@ import torch
from vllm_omni.model_executor.stage_input_processors.cosyvoice3 import (
talker2code2wav_async_chunk,
text2flow,
text2flow_full_payload,
text2flow_token_only,
)
@@ -49,13 +48,13 @@ def _transfer_manager(
)
def test_text2flow_supports_batched_source_outputs():
def test_text2flow_token_only_supports_batched_source_outputs():
source_outputs = [
_source_output("req-0", [10, 11], [1, 2, 3], {"speech_token": torch.tensor([[8, 9]])}),
_source_output("req-1", [20, 21], [4, 5], {"speech_token": torch.tensor([[6, 7]])}),
]
outputs = text2flow(source_outputs=source_outputs, prompt=None)
outputs = text2flow_token_only(source_outputs=source_outputs, prompt=None)
assert len(outputs) == 2
assert outputs[0]["prompt_token_ids"] == [1, 2, 3]
@@ -64,16 +63,6 @@ def test_text2flow_supports_batched_source_outputs():
assert outputs[1]["additional_information"]["ids"]["prompt"] == [20, 21]
def test_text2flow_strips_reference_speech_prefix_from_cumulative_ids():
source_outputs = [
_source_output("req-0", [10, 11], [8, 9, 1, 2, 3], {"speech_token": torch.tensor([[8, 9]])}),
]
outputs = text2flow(source_outputs=source_outputs, prompt=None)
assert outputs[0]["prompt_token_ids"] == [1, 2, 3]
def test_text2flow_token_only_strips_reference_speech_prefix_from_cumulative_ids():
source_outputs = [
_source_output(
@@ -45,6 +45,33 @@ def test_truncate_at_cot_end_strips_tail_after_recaption_marker():
assert text == "body text</recaption>"
def test_ar2diffusion_returns_one_request_payload_for_request_level_batching():
result = ar2diffusion(
[_source_output([100], text="thought")],
prompt={"prompt": "edit"},
)
assert isinstance(result, dict)
assert result["prompt"] == "edit"
def test_ar2diffusion_uses_parent_output_when_companions_are_present():
result = ar2diffusion(
[
_source_output([100], text="parent thought"),
_source_output([200], text="companion thought"),
],
prompt={"prompt": "edit"},
)
assert result is not None
assert result["extra"]["ar_generated_text"] == "parent thought"
def test_ar2diffusion_returns_none_without_parent_output():
assert ar2diffusion([], prompt={"prompt": "edit"}) is None
def test_ar2diffusion_applies_ratio_and_truncates_tail_without_tokenizer(monkeypatch: pytest.MonkeyPatch):
real_import = builtins.__import__
@@ -67,10 +94,10 @@ def test_ar2diffusion_applies_ratio_and_truncates_tail_without_tokenizer(monkeyp
prompt=[{"prompt": "edit", "height": 64, "width": 64}],
)
assert len(result) == 1
assert (result[0]["height"], result[0]["width"]) == (512, 2048)
assert result[0]["extra"]["ar_generated_text"] == "decoded without special tokens"
assert "ar_token_ids" not in result[0]["extra"]
assert result is not None
assert (result["height"], result["width"]) == (512, 2048)
assert result["extra"]["ar_generated_text"] == "decoded without special tokens"
assert "ar_token_ids" not in result["extra"]
def test_ar2diffusion_forwards_custom_system_prompt_body():
@@ -88,5 +115,6 @@ def test_ar2diffusion_forwards_custom_system_prompt_body():
],
)
assert result[0]["use_system_prompt"] == "custom"
assert result[0]["system_prompt"] == marker
assert result is not None
assert result["use_system_prompt"] == "custom"
assert result["system_prompt"] == marker
@@ -7,7 +7,6 @@ import torch
from vllm_omni.model_executor.stage_input_processors.indextts2 import (
_strip_stop_token,
talker2s2mel,
talker2s2mel_full_payload,
talker2s2mel_token_only,
)
@@ -108,118 +107,6 @@ def test_strip_stop_token_all_stop():
assert lat.shape[1] == 0
# ── talker2s2mel ──
def test_talker2s2mel_basic():
mel_codes = torch.tensor([10, 20, 30, STOP_MEL_TOKEN])
latent = torch.randn(4, LATENT_DIM)
s_ref = torch.randn(1, 128)
ref_mel = torch.randn(1, 80, 200)
style = torch.randn(1, 256)
outputs = talker2s2mel(
source_outputs=[
_make_talker_output(
mel_codes=mel_codes,
latent=latent,
s_ref=s_ref,
ref_mel=ref_mel,
style=style,
)
]
)
assert len(outputs) == 1
info = outputs[0]["additional_information"]
assert info["mel_codes"].shape == (1, 3)
assert info["latent"].shape == (1, 3, LATENT_DIM)
assert info["code_lens"].tolist() == [3]
assert "S_ref" in info
assert "ref_mel" in info
assert "style" in info
assert outputs[0]["prompt_token_ids"] == [0]
def test_talker2s2mel_skips_unfinished():
mel_codes = torch.tensor([10, 20])
latent = torch.randn(2, LATENT_DIM)
outputs = talker2s2mel(source_outputs=[_make_talker_output(mel_codes=mel_codes, latent=latent, finished=False)])
assert len(outputs) == 0
def test_talker2s2mel_skips_empty_mel():
outputs = talker2s2mel(
source_outputs=[
_make_talker_output(
mel_codes=torch.tensor([]),
latent=torch.randn(0, LATENT_DIM),
)
]
)
assert len(outputs) == 0
def test_talker2s2mel_missing_optional_meta():
mel_codes = torch.tensor([10, 20])
latent = torch.randn(2, LATENT_DIM)
outputs = talker2s2mel(source_outputs=[_make_talker_output(mel_codes=mel_codes, latent=latent)])
assert len(outputs) == 1
info = outputs[0]["additional_information"]
assert "S_ref" not in info
assert "ref_mel" not in info
assert "style" not in info
def test_talker2s2mel_batch():
out0 = _make_talker_output(
mel_codes=torch.tensor([1, 2, STOP_MEL_TOKEN]),
latent=torch.randn(3, LATENT_DIM),
s_ref=torch.randn(1, 64),
)
out1 = _make_talker_output(
mel_codes=torch.tensor([5, 6, 7, 8]),
latent=torch.randn(4, LATENT_DIM),
ref_mel=torch.randn(1, 80, 100),
)
outputs = talker2s2mel(source_outputs=[out0, out1])
assert len(outputs) == 2
assert outputs[0]["additional_information"]["code_lens"].tolist() == [2]
assert outputs[1]["additional_information"]["code_lens"].tolist() == [4]
def test_talker2s2mel_tensors_on_cpu():
device = torch.device("cpu")
mel_codes = torch.tensor([10, 20], device=device)
latent = torch.randn(2, LATENT_DIM, device=device)
s_ref = torch.randn(1, 64, device=device)
outputs = talker2s2mel(source_outputs=[_make_talker_output(mel_codes=mel_codes, latent=latent, s_ref=s_ref)])
info = outputs[0]["additional_information"]
assert info["latent"].device.type == "cpu"
assert info["mel_codes"].device.type == "cpu"
assert info["S_ref"].device.type == "cpu"
def test_talker2s2mel_no_multimodal_dict():
output = SimpleNamespace(
finished=True,
outputs=[SimpleNamespace(multimodal_output="not_a_dict")],
)
results = talker2s2mel(source_outputs=[output])
assert len(results) == 0
# ── full-payload connector path ──
@@ -11,59 +11,40 @@ import torch
from vllm_omni.model_executor.stage_input_processors import mimo_audio as sip
from vllm_omni.model_executor.stage_input_processors.mimo_audio import (
MAX_CODE2WAV_TOKENS,
llm2code2wav,
llm2code2wav_full_payload,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _make_source_outputs(codec_codes: torch.Tensor, request_id: str = "req-0"):
output = SimpleNamespace(multimodal_output={"codes": {"audio": codec_codes}})
"""Build minimal source_outputs with one talker output carrying codec codes."""
talker_output = SimpleNamespace(outputs=[output], request_id=request_id)
return [talker_output]
def test_llm2code2wav_truncates_when_flat_exceeds_max(caplog):
"""Flat codec sequences longer than MAX_CODE2WAV_TOKENS must be truncated, not passed through."""
# prepend_and_flatten_colmajor produces 36 ids per (8, 4) codec frame:
# pad adds one row -> (9, 4) per frame, permuted and flattened.
# Pick enough frames to comfortably exceed the cap.
def test_llm2code2wav_full_payload_truncates_when_flat_exceeds_max(caplog):
"""Flat codec sequences longer than MAX_CODE2WAV_TOKENS must be truncated."""
frames = (MAX_CODE2WAV_TOKENS // 36) + 100
codec_codes = torch.ones(frames, 1, 8, 4, dtype=torch.long)
request = SimpleNamespace(request_id="req-long")
source_outputs = _make_source_outputs(codec_codes, request_id="req-long")
# Attach caplog's handler directly to the module logger so the warning is
# captured regardless of propagation (vllm's logger configuration can
# interact badly with caplog.at_level's default root-handler path).
target_logger = logging.getLogger("vllm_omni.model_executor.stage_input_processors.mimo_audio")
target_logger.addHandler(caplog.handler)
prev_level = target_logger.level
target_logger.setLevel(logging.WARNING)
try:
prompts = llm2code2wav(source_outputs)
payload = llm2code2wav_full_payload(None, {"codes.audio": codec_codes}, request)
finally:
target_logger.removeHandler(caplog.handler)
target_logger.setLevel(prev_level)
assert len(prompts) == 1
assert len(prompts[0]["prompt_token_ids"]) == MAX_CODE2WAV_TOKENS
assert any("truncating" in rec.getMessage() for rec in caplog.records), (
f"Expected a 'truncating' warning; captured records: {[r.getMessage() for r in caplog.records]}"
)
assert payload is not None
assert len(payload["codes"]["audio"]) == MAX_CODE2WAV_TOKENS
def test_llm2code2wav_short_sequence_unchanged():
"""Short codec sequences are returned without truncation."""
def test_llm2code2wav_full_payload_short_sequence_unchanged():
codec_codes = torch.ones(4, 1, 8, 4, dtype=torch.long)
source_outputs = _make_source_outputs(codec_codes, request_id="req-short")
request = SimpleNamespace(request_id="req-short")
prompts = llm2code2wav(source_outputs)
payload = llm2code2wav_full_payload(None, {"codes.audio": codec_codes}, request)
assert len(prompts) == 1
# 4 frames + 1 pad row, flattened col-major → well below the cap
assert 0 < len(prompts[0]["prompt_token_ids"]) <= MAX_CODE2WAV_TOKENS
assert payload is not None
assert 0 < len(payload["codes"]["audio"]) <= MAX_CODE2WAV_TOKENS
def test_llm2code2wav_truncation_boundary_constant_matches_yaml():
@@ -0,0 +1,102 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from collections import defaultdict
from types import SimpleNamespace
import pytest
import torch
from vllm_omni.model_executor.models.ming_tts.constants import LATENT_DIM, PATCH_SIZE
from vllm_omni.model_executor.stage_input_processors.ming_tts import (
MING_EMIT_PATCH_COUNT_KEY,
llm2audio_vae_async_chunk,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _make_transfer_manager(*, chunk_size: int = 5, initial_chunk_size: int = 2):
return SimpleNamespace(
code_prompt_token_ids=defaultdict(list),
put_req_chunk=defaultdict(int),
request_payload={},
connector=SimpleNamespace(
config={
"extra": {
"latent_chunk_size": chunk_size,
"initial_latent_chunk_size": initial_chunk_size,
"latent_left_context": 0,
}
}
),
)
def _make_request(req_id: str = "req", *, finished: bool = False):
return SimpleNamespace(
external_req_id=req_id,
is_finished=lambda: finished,
)
def _make_output(value: float):
return {
"ming_has_patch": torch.tensor([1], dtype=torch.bool),
"ming_latent_patch": torch.full((1, PATCH_SIZE, LATENT_DIM), value),
}
def _append_patch(tm, req_id: str, idx: int, *, finished: bool = False):
return llm2audio_vae_async_chunk(
tm,
_make_output(float(idx)),
_make_request(req_id, finished=finished),
is_finished=finished,
)
def test_ming_async_chunk_emits_small_initial_chunk_then_steady_chunks():
tm = _make_transfer_manager(chunk_size=5, initial_chunk_size=2)
assert _append_patch(tm, "req", 0) is None
first = _append_patch(tm, "req", 1)
assert first is not None
assert first.latent.shape == (2, PATCH_SIZE, LATENT_DIM)
assert first.kv_metadata[MING_EMIT_PATCH_COUNT_KEY] == 2
assert first.meta.finished.item() is False
for idx in range(2, 6):
assert _append_patch(tm, "req", idx) is None
second = _append_patch(tm, "req", 6)
assert second is not None
assert second.latent.shape == (5, PATCH_SIZE, LATENT_DIM)
assert second.kv_metadata[MING_EMIT_PATCH_COUNT_KEY] == 5
def test_ming_async_chunk_flushes_short_final_chunk():
tm = _make_transfer_manager(chunk_size=5, initial_chunk_size=2)
payload = _append_patch(tm, "req", 0, finished=True)
assert payload is not None
assert payload.latent.shape == (1, PATCH_SIZE, LATENT_DIM)
assert payload.kv_metadata[MING_EMIT_PATCH_COUNT_KEY] == 1
assert payload.meta.finished.item() is True
def test_ming_async_chunk_flushes_leftover_after_initial_chunk():
tm = _make_transfer_manager(chunk_size=5, initial_chunk_size=2)
assert _append_patch(tm, "req", 0) is None
assert _append_patch(tm, "req", 1) is not None
assert _append_patch(tm, "req", 2) is None
final = _append_patch(tm, "req", 3, finished=True)
assert final is not None
assert final.latent.shape == (2, PATCH_SIZE, LATENT_DIM)
assert final.kv_metadata[MING_EMIT_PATCH_COUNT_KEY] == 2
assert final.meta.finished.item() is True
@@ -163,7 +163,7 @@ def test_talker2code2wav_full_payload_filters_by_output_token_ids() -> None:
assert payload is not None
assert payload["codes"]["audio"] == [10, 20, 11, 21, 12, 22]
assert payload["code_predictor_codes"] == payload["codes"]["audio"]
assert "code_predictor_codes" not in payload
def test_talker2code2wav_full_payload_drops_count_matched_terminal_row() -> None:
@@ -204,7 +204,7 @@ def test_talker2code2wav_full_payload_drops_rows_aligned_to_non_codec_ids() -> N
assert payload is not None
assert payload["codes"]["audio"] == [0, 0, 0]
assert payload["code_predictor_codes"] == payload["codes"]["audio"]
assert "code_predictor_codes" not in payload
def test_talker2code2wav_full_payload_keeps_all_zero_codec_rows() -> None:
@@ -224,7 +224,7 @@ def test_talker2code2wav_full_payload_keeps_all_zero_codec_rows() -> None:
assert payload is not None
assert payload["codes"]["audio"] == [0, 7, 0, 8, 0, 9]
assert payload["code_predictor_codes"] == payload["codes"]["audio"]
assert "code_predictor_codes" not in payload
def test_thinker2talker_full_payload_packs_complete_tensors() -> None:
@@ -247,11 +247,33 @@ def test_thinker2talker_full_payload_packs_complete_tensors() -> None:
assert payload["ids"]["all"] == [151644, 872, 3]
assert payload["embed"]["prefill"].device.type == "cpu"
assert payload["hidden_states"]["output"].device.type == "cpu"
assert payload["next_stage_prompt_len"] > 0
assert payload["embed"]["prefill"].shape[0] == 2
assert payload["hidden_states"]["output"].shape[0] == 2
def test_thinker2talker_token_only_preserves_voice_metadata() -> None:
source_outputs = [
SimpleNamespace(
request_id="req-1",
prompt_token_ids=[1, 2],
outputs=[SimpleNamespace(cumulative_token_ids=[3])],
)
]
prompt = {
"additional_information": {
"speaker": ["ethan"],
"language": ["English"],
}
}
[talker_prompt] = q3.thinker2talker_token_only(source_outputs, prompt)
assert talker_prompt["additional_information"] == {
"speaker": ["ethan"],
"language": ["English"],
}
def test_accumulator_replaces_keys_in_replace_set() -> None:
"""REPLACE-key semantics: subsequent emissions of the same key replace, not append."""
from vllm_omni.worker.omni_connector_model_runner_mixin import OmniConnectorModelRunnerMixin
@@ -12,9 +12,9 @@ from vllm_omni.model_executor.stage_input_processors.chunk_size_utils import (
max_ic_for_chunk_size,
)
from vllm_omni.model_executor.stage_input_processors.qwen3_tts import (
talker2code2wav,
talker2code2wav_async_chunk,
talker2code2wav_full_payload,
talker2code2wav_token_only,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -368,7 +368,15 @@ def test_ref_code_context_can_be_buffered_before_first_emit():
assert rid in tm.request_payload
def test_non_async_processor_prepends_ref_code_and_sets_trim_context():
def test_non_async_token_only_sizes_placeholder_for_ref_and_audio_frames():
"""``talker2code2wav_token_only`` only allocates placeholder prompt slots.
After the connector refactor, actual codec flattening (ref prepend +
codebook-major layout) is performed by ``talker2code2wav_full_payload`` on
the worker data plane. The orchestrator hook still derives
``left_context_size`` from stage-0 multimodal_output so Code2Wav can trim
reference frames once the connector payload arrives.
"""
ref_code = torch.tensor([[9, 9, 9, 9], [8, 8, 8, 8]], dtype=torch.long)
audio_codes = torch.tensor(
[
@@ -387,12 +395,38 @@ def test_non_async_processor_prepends_ref_code_and_sets_trim_context():
engine_outputs=[SimpleNamespace(outputs=[output], finished=True)],
)
prompts = talker2code2wav(stage.engine_outputs)
prompts = talker2code2wav_token_only(stage.engine_outputs)
assert len(prompts) == 1
prompt = prompts[0]
assert prompt["additional_information"] == {"meta": {"left_context_size": 2}}
assert prompt["prompt_token_ids"] == [
# 2 ref frames + 2 valid audio frames (zero row filtered), 4 quantizers.
assert prompt["prompt_token_ids"] == [0] * (_Q * (2 + 2))
def test_full_payload_prepends_ref_code_and_flattens_codebook_major():
"""Worker producer is authoritative for ref prepend + codec flatten."""
ref_code = torch.tensor([[9, 9, 9, 9], [8, 8, 8, 8]], dtype=torch.long)
audio_codes = torch.tensor(
[
[0, 0, 0, 0],
[1, 2, 3, 4],
[5, 6, 7, 8],
],
dtype=torch.long,
)
pooling_output = {
"codes.audio": audio_codes,
"codes.ref": ref_code,
"meta.ref_code_len": 2,
}
request = SimpleNamespace(request_id="r", output_token_ids=list(range(3)))
payload = talker2code2wav_full_payload(transfer_manager=None, pooling_output=pooling_output, request=request)
assert payload is not None
assert payload["meta"]["left_context_size"] == 2
assert payload["codes"]["audio"].tolist() == [
9,
8,
1,
@@ -433,7 +467,7 @@ def test_non_async_processor_filters_out_of_range_codec_values():
engine_outputs=[SimpleNamespace(outputs=[output], finished=True)],
)
prompts = talker2code2wav(stage.engine_outputs)
prompts = talker2code2wav_token_only(stage.engine_outputs)
assert len(prompts) == 1
prompt = prompts[0]
@@ -8,61 +8,12 @@ import pytest
import torch
from vllm_omni.model_executor.stage_input_processors.voxtral_tts import (
generator2tokenizer,
generator2tokenizer_async_chunk,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
# ---- Helpers for generator2tokenizer (non-streaming) ----
def _make_stage(engine_outputs):
return SimpleNamespace(engine_outputs=engine_outputs)
def _make_output(audio_tensors):
"""Build a single generator output with multimodal_output codes.audio."""
return SimpleNamespace(
outputs=[SimpleNamespace(multimodal_output={"codes": {"audio": audio_tensors}})],
)
# ---- Tests for generator2tokenizer ----
class TestGenerator2Tokenizer:
def test_single_output_single_chunk(self):
"""Single output with one audio tensor produces correct prompt_token_ids."""
audio = torch.tensor([10, 20, 30, 40])
stage = _make_stage([_make_output([audio])])
result = generator2tokenizer(stage.engine_outputs)
assert len(result) == 1
assert result[0]["prompt_token_ids"] == [10, 20, 30, 40]
assert result[0]["multi_modal_data"] is None
def test_2d_audio_tensors_flattened(self):
"""2D audio tensors (e.g., multi-codebook frames) are flattened."""
audio = torch.tensor([[1, 2, 3], [4, 5, 6]]) # (2, 3)
stage = _make_stage([_make_output([audio])])
result = generator2tokenizer(stage.engine_outputs)
assert result[0]["prompt_token_ids"] == [1, 2, 3, 4, 5, 6]
def test_no_outputs_yet_raises(self):
"""Stage with engine_outputs=None raises RuntimeError."""
stage = _make_stage(engine_outputs=None)
with pytest.raises(TypeError):
generator2tokenizer(stage.engine_outputs)
# ---- Helpers for generator2tokenizer_async_chunk (streaming) ----
def _req(external_req_id: str, *, finished: bool):
return SimpleNamespace(
external_req_id=external_req_id,