[Test] Unify L2/L3 test layout, Buildkite steps, and test helpers (#2556)

Signed-off-by: wangyu <410167048@qq.com>
Signed-off-by: Huang, Zeyu <11222265+fhfuih@users.noreply.github.com>
Signed-off-by: wangyu <53896905+yenuo26@users.noreply.github.com>
Co-authored-by: Huang, Zeyu <11222265+fhfuih@users.noreply.github.com>
This commit is contained in:
wangyuandHuang, Zeyu authored and GitHub committed 2026-05-08 14:12:23 +08:00
1 parent b85833eeec
commit 6f2ad7b403
134 files changed
+4904 -4185

No files matched your search

@@ -1,8 +1,12 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import functools
from threading import Lock
from types import SimpleNamespace
from typing import TYPE_CHECKING
import pytest
import torch
@@ -11,11 +15,20 @@ from vllm.v1.outputs import SamplerOutput
from vllm.v1.sample.logits_processor.state import LogitsProcessors
from vllm.v1.sample.metadata import SamplingMetadata
from vllm_omni.model_executor.models.cosyvoice3.cosyvoice3 import CosyVoice3Model
from vllm_omni.worker.gpu_ar_model_runner import GPUARModelRunner
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
if TYPE_CHECKING:
from vllm_omni.model_executor.models.cosyvoice3.cosyvoice3 import CosyVoice3Model
@functools.lru_cache(maxsize=1)
def _cosyvoice3_model_and_runner():
"""Defer heavy Omni/vLLM imports until a test runs (avoids duplicate CustomOp init)."""
from vllm_omni.model_executor.models.cosyvoice3.cosyvoice3 import CosyVoice3Model
from vllm_omni.worker.gpu_ar_model_runner import GPUARModelRunner
return CosyVoice3Model, GPUARModelRunner
class _DummyCode2Wav:
def __init__(
@@ -59,6 +72,7 @@ def _make_code2wav_model(
num_samples: int = 32,
outputs: list[tuple[torch.Tensor, dict[str, object] | None]] | None = None,
) -> CosyVoice3Model:
CosyVoice3Model, _ = _cosyvoice3_model_and_runner()
model = object.__new__(CosyVoice3Model)
nn.Module.__init__(model)
model.model_stage = "cosyvoice3_code2wav"
@@ -79,6 +93,7 @@ def _make_code2wav_model(
def _make_talker_model() -> CosyVoice3Model:
CosyVoice3Model, _ = _cosyvoice3_model_and_runner()
model = object.__new__(CosyVoice3Model)
nn.Module.__init__(model)
model.model_stage = "cosyvoice3_talker"
@@ -124,12 +139,14 @@ def _make_sampling_metadata(
def test_split_request_ids_uses_seq_token_counts():
CosyVoice3Model, _ = _cosyvoice3_model_and_runner()
ids = torch.tensor([10, 11, 12, 13, 14], dtype=torch.long)
chunks = CosyVoice3Model._split_request_ids(ids, [2, 2, 2])
assert [c.tolist() for c in chunks] == [[10, 11], [12, 13], [14]]
def test_split_request_ids_honors_single_request_seq_token_counts():
CosyVoice3Model, _ = _cosyvoice3_model_and_runner()
ids = torch.tensor([10, 11, 12, 13, 14], dtype=torch.long)
chunks = CosyVoice3Model._split_request_ids(ids, [3])
assert [c.tolist() for c in chunks] == [[10, 11, 12]]
@@ -376,6 +393,7 @@ def test_gpu_ar_model_runner_prefers_model_sampler_when_opted_in():
def update_async_output_token_ids(self):
self.updated = True
_, GPUARModelRunner = _cosyvoice3_model_and_runner()
runner = object.__new__(GPUARModelRunner)
runner.input_batch = _DummyInputBatch()
runner.model = SimpleNamespace(
@@ -407,6 +425,7 @@ def test_gpu_ar_model_runner_supplies_req_output_history_to_model_sampler():
def update_async_output_token_ids(self):
raise AssertionError("fallback async repair should not run for model sampler path")
_, GPUARModelRunner = _cosyvoice3_model_and_runner()
runner = object.__new__(GPUARModelRunner)
runner.input_batch = _DummyInputBatch()
runner.model = SimpleNamespace(
@@ -446,6 +465,7 @@ def test_gpu_ar_model_runner_repairs_async_placeholders_for_model_sampler():
def update_async_output_token_ids(self):
raise AssertionError("fallback async repair should not run for model sampler path")
_, GPUARModelRunner = _cosyvoice3_model_and_runner()
runner = object.__new__(GPUARModelRunner)
runner.input_batch = _DummyInputBatch()
runner.model = SimpleNamespace(
@@ -1,17 +1,23 @@
import functools
import math
import pytest
import torch
from vllm_omni.model_executor.models.fish_speech import fish_speech_slow_ar as slow_ar_module
from vllm_omni.model_executor.models.fish_speech.fish_speech_dac_decoder import FishSpeechDACDecoder
from vllm_omni.model_executor.models.fish_speech.fish_speech_slow_ar import (
FishSpeechSlowARForConditionalGeneration,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@functools.lru_cache(maxsize=1)
def _fish_speech_regression_modules():
from vllm_omni.model_executor.models.fish_speech import fish_speech_slow_ar as slow_ar_module
from vllm_omni.model_executor.models.fish_speech.fish_speech_dac_decoder import FishSpeechDACDecoder
from vllm_omni.model_executor.models.fish_speech.fish_speech_slow_ar import (
FishSpeechSlowARForConditionalGeneration,
)
return slow_ar_module, FishSpeechDACDecoder, FishSpeechSlowARForConditionalGeneration
class _FakeCodec:
def decode(self, codes_bqf: torch.Tensor, feature_lengths: torch.Tensor):
del codes_bqf, feature_lengths
@@ -30,6 +36,7 @@ class _FakeTokenizer:
def test_dac_decoder_mixed_batch_empty_request_does_not_misalign_indices():
_, FishSpeechDACDecoder, _ = _fish_speech_regression_modules()
decoder = object.__new__(FishSpeechDACDecoder)
torch.nn.Module.__init__(decoder)
decoder._codec = _FakeCodec()
@@ -56,6 +63,7 @@ def test_dac_decoder_mixed_batch_empty_request_does_not_misalign_indices():
def test_structured_voice_clone_prefill_adds_full_codebooks_with_decode_scale(monkeypatch):
slow_ar_module, _, FishSpeechSlowARForConditionalGeneration = _fish_speech_regression_modules()
model = object.__new__(FishSpeechSlowARForConditionalGeneration)
torch.nn.Module.__init__(model)
model._num_codebooks = 2
@@ -1,34 +1,50 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import functools
from types import SimpleNamespace
import pytest
import torch
from pytest_mock import MockerFixture
from vllm_omni.model_executor.models.mimo_audio.config_mimo_audio import TALKER_CODEC_PAD_TOKEN_ID
from vllm_omni.model_executor.models.mimo_audio.mimo_audio_code2wav import (
AudioStreamerConfig,
MiMoAudioToken2WavForConditionalGenerationVLLM,
flat_codec_group_element_count,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
_GROUP = 4
_AC = 8
_GROUP_WIDTH = flat_codec_group_element_count(_GROUP, _AC)
_FTP = 2 * 2 * 240 # frames_per_token from mocked tokenizer.config
@functools.lru_cache(maxsize=1)
def _mimo_code2wav_deps():
"""Defer mimo code2wav (pulls vLLM model_executor) until first use."""
from vllm_omni.model_executor.models.mimo_audio.config_mimo_audio import TALKER_CODEC_PAD_TOKEN_ID
from vllm_omni.model_executor.models.mimo_audio.mimo_audio_code2wav import (
AudioStreamerConfig,
MiMoAudioToken2WavForConditionalGenerationVLLM,
flat_codec_group_element_count,
)
return (
TALKER_CODEC_PAD_TOKEN_ID,
AudioStreamerConfig,
MiMoAudioToken2WavForConditionalGenerationVLLM,
flat_codec_group_element_count,
)
def _mimo_t2w_cls():
return _mimo_code2wav_deps()[2]
def _codes_ns(empty: int = 555, eostm: int = 666):
return SimpleNamespace(empty=empty, eostm=eostm)
def _make_dummy_code_tensor() -> torch.Tensor:
"""Pad-only talker dummy; matches _check_dummy_code_tensor."""
t = torch.zeros(flat_codec_group_element_count(_GROUP, _AC), dtype=torch.long)
TALKER_CODEC_PAD_TOKEN_ID, _, _, fcec = _mimo_code2wav_deps()
t = torch.zeros(fcec(_GROUP, _AC), dtype=torch.long)
t = t.view(_GROUP, _AC + 1)
t[:, 0] = TALKER_CODEC_PAD_TOKEN_ID
return t.view(-1)
@@ -53,6 +69,7 @@ def _make_invalid_flat_immediate_eostm(eostm_id: int = 666) -> torch.Tensor:
def _minimal_model(mocker: MockerFixture):
"""Avoid __init__ (HF tokenizer paths); only fields used by _batch_decode_waveforms."""
_, AudioStreamerConfig, MiMoAudioToken2WavForConditionalGenerationVLLM, _ = _mimo_code2wav_deps()
model = object.__new__(MiMoAudioToken2WavForConditionalGenerationVLLM)
model.device = torch.device("cpu")
model.config = SimpleNamespace(group_size=_GROUP, audio_channels=_AC)
@@ -81,7 +98,7 @@ def _minimal_model(mocker: MockerFixture):
def test_batch_decode_waveforms_empty_input_list(mocker: MockerFixture):
"""Empty input list returns a single zero-length float32 tensor on model device."""
model, _ = _minimal_model(mocker)
out = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, [])
out = _mimo_t2w_cls()._batch_decode_waveforms(model, [])
assert len(out) == 1
assert out[0].dtype == torch.float32
assert out[0].numel() == 0
@@ -96,7 +113,7 @@ def test_batch_decode_waveforms_single_vs_multiple_decoder_shapes(mocker: Mocker
# Single valid request: decoder output rank-3 for double squeeze path
flat = _make_valid_flat_codes(1)
decoder.return_value = torch.ones(1, 1, 4 * _FTP + 100, dtype=torch.float32)
out1 = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, [flat])
out1 = _mimo_t2w_cls()._batch_decode_waveforms(model, [flat])
decoder.assert_called_once()
packed_hs, input_lengths = decoder.call_args[0]
assert packed_hs.shape == (4, 7) # T from decode_vq mock
@@ -108,7 +125,7 @@ def test_batch_decode_waveforms_single_vs_multiple_decoder_shapes(mocker: Mocker
a = _make_valid_flat_codes(1)
b = _make_valid_flat_codes(2)
decoder.return_value = torch.ones(2, 1, 8 * _FTP + 100, dtype=torch.float32)
out2 = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, [a, b])
out2 = _mimo_t2w_cls()._batch_decode_waveforms(model, [a, b])
decoder.assert_called_once()
packed_hs2, input_lengths2 = decoder.call_args[0]
assert packed_hs2.shape == (4 + 8, 7) # 1 group -> T=4; 2 groups -> T=8
@@ -138,7 +155,7 @@ def test_batch_decode_waveforms_mixed_valid_invalid_requests(mocker: MockerFixtu
valid_a,
valid_b,
]
out = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, inputs)
out = _mimo_t2w_cls()._batch_decode_waveforms(model, inputs)
assert len(out) == len(inputs)
for i in range(5):
@@ -154,7 +171,7 @@ def test_batch_decode_waveforms_mixed_valid_invalid_requests(mocker: MockerFixtu
def test_batch_decode_waveforms_all_invalid_returns_per_request_empty(mocker: MockerFixture):
"""All-invalid batch skips decoder entirely and returns empty tensors for every slot."""
model, audio_tok = _minimal_model(mocker)
out = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(
out = _mimo_t2w_cls()._batch_decode_waveforms(
model,
[None, _make_dummy_code_tensor(), torch.tensor([], dtype=torch.long)],
)
@@ -169,7 +186,7 @@ def test_batch_decode_waveforms_output_shape_trim_when_decoder_returns_extra_sam
flat = _make_valid_flat_codes(1)
# Longer than valid_len so branch wav = wav[:valid_len] runs
audio_tok.decoder.return_value = torch.ones(1, 1, 10_000, dtype=torch.float32)
out = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, [flat])
out = _mimo_t2w_cls()._batch_decode_waveforms(model, [flat])
assert out[0].dim() == 1
assert out[0].numel() == 4 * _FTP
assert out[0].dtype == torch.float32
@@ -181,7 +198,7 @@ def test_batch_decode_waveforms_multi_request_trims_each_row_when_decoder_return
a = _make_valid_flat_codes(1)
b = _make_valid_flat_codes(2)
audio_tok.decoder.return_value = torch.ones(2, 1, 10_000, dtype=torch.float32)
out = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, [a, b])
out = _mimo_t2w_cls()._batch_decode_waveforms(model, [a, b])
assert len(out) == 2
assert out[0].shape == (4 * _FTP,)
assert out[1].shape == (8 * _FTP,)
@@ -201,7 +218,7 @@ def test_batch_decode_waveforms_valid_only_at_edges_maps_to_correct_indices(mock
last,
]
audio_tok.decoder.return_value = torch.ones(2, 1, 10_000, dtype=torch.float32)
out = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, inputs)
out = _mimo_t2w_cls()._batch_decode_waveforms(model, inputs)
assert len(out) == 4
assert out[0].numel() == 4 * _FTP
assert out[1].numel() == 0
@@ -217,7 +234,7 @@ def test_batch_decode_waveforms_output_shapes_1d_float32_for_all_slots(mocker: M
model, audio_tok = _minimal_model(mocker)
inputs = [_make_valid_flat_codes(1), None, _make_valid_flat_codes(1)]
audio_tok.decoder.return_value = torch.ones(2, 1, 5000, dtype=torch.float32)
out = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, inputs)
out = _mimo_t2w_cls()._batch_decode_waveforms(model, inputs)
assert len(out) == 3
for i, t in enumerate(out):
assert t.dim() == 1, f"slot {i}"
@@ -10,16 +10,23 @@ Regression test for: https://github.com/vllm-project/vllm/issues/34506
- Interleaved (use_audio_in_video) should also work correctly.
"""
import functools
import pytest
import torch
from pytest_mock import MockerFixture
from vllm.model_executor.models.qwen2_5_omni_thinker import (
check_interleaved_audio_video,
merge_interleaved_embeddings,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@functools.lru_cache(maxsize=1)
def _qwen2_5_omni_thinker_mod():
"""Load vLLM thinker only at first use so collection does not import model_executor too early."""
import vllm.model_executor.models.qwen2_5_omni_thinker as m
return m
# Fake token IDs
AUDIO_TOKEN_ID = 1001
IMAGE_TOKEN_ID = 1002
@@ -74,31 +81,35 @@ def make_interleaved_seq(video_chunks: list[int], audio_chunks: list[int], text_
class TestCheckInterleavedAudioVideo:
def test_non_interleaved_audio_then_video(self):
"""Audio entirely before video -> not interleaved."""
m = _qwen2_5_omni_thinker_mod()
input_ids, is_multimodal = make_token_seq(5, 0, 4)
is_video = is_multimodal & (input_ids == VIDEO_TOKEN_ID)
is_audio = is_multimodal & (input_ids == AUDIO_TOKEN_ID)
assert not check_interleaved_audio_video(is_video, is_audio, is_video.sum().item(), is_audio.sum().item())
assert not m.check_interleaved_audio_video(is_video, is_audio, is_video.sum().item(), is_audio.sum().item())
def test_non_interleaved_with_image(self):
"""Audio + image + video (the mixed_modalities case) -> not interleaved."""
m = _qwen2_5_omni_thinker_mod()
input_ids, is_multimodal = make_token_seq(5, 4, 6)
is_video = is_multimodal & (input_ids == VIDEO_TOKEN_ID)
is_audio = is_multimodal & (input_ids == AUDIO_TOKEN_ID)
assert not check_interleaved_audio_video(is_video, is_audio, is_video.sum().item(), is_audio.sum().item())
assert not m.check_interleaved_audio_video(is_video, is_audio, is_video.sum().item(), is_audio.sum().item())
def test_no_audio(self):
"""Video only -> not interleaved."""
m = _qwen2_5_omni_thinker_mod()
input_ids, is_multimodal = make_token_seq(0, 0, 6)
is_video = is_multimodal & (input_ids == VIDEO_TOKEN_ID)
is_audio = is_multimodal & (input_ids == AUDIO_TOKEN_ID)
assert not check_interleaved_audio_video(is_video, is_audio, is_video.sum().item(), is_audio.sum().item())
assert not m.check_interleaved_audio_video(is_video, is_audio, is_video.sum().item(), is_audio.sum().item())
def test_interleaved(self):
"""V A V A interleaved -> True."""
m = _qwen2_5_omni_thinker_mod()
input_ids, is_multimodal = make_interleaved_seq([4, 4], [3, 3])
is_video = is_multimodal & (input_ids == VIDEO_TOKEN_ID)
is_audio = is_multimodal & (input_ids == AUDIO_TOKEN_ID)
assert check_interleaved_audio_video(is_video, is_audio, is_video.sum().item(), is_audio.sum().item())
assert m.check_interleaved_audio_video(is_video, is_audio, is_video.sum().item(), is_audio.sum().item())
# ---------------------------------------------------------------------------
@@ -111,9 +122,8 @@ def make_mock_model(mocker: MockerFixture, hidden: int = 8):
Return a minimal mock of Qwen2_5OmniThinkerForConditionalGeneration
that has enough structure to run embed_input_ids.
"""
from vllm.model_executor.models.qwen2_5_omni_thinker import (
Qwen2_5OmniThinkerForConditionalGeneration,
)
m = _qwen2_5_omni_thinker_mod()
Qwen2_5OmniThinkerForConditionalGeneration = m.Qwen2_5OmniThinkerForConditionalGeneration
model = mocker.Mock(spec=Qwen2_5OmniThinkerForConditionalGeneration)
@@ -145,7 +155,9 @@ def make_mock_model(mocker: MockerFixture, hidden: int = 8):
is_multimodal=is_multimodal,
)
model.embed_input_ids = lambda *a, **kw: Qwen2_5OmniThinkerForConditionalGeneration.embed_input_ids(model, *a, **kw)
model.embed_input_ids = lambda *a, **kw: m.Qwen2_5OmniThinkerForConditionalGeneration.embed_input_ids( # noqa: E501
model, *a, **kw
)
model._super_embed_input_ids = fake_super_embed
@@ -285,6 +297,7 @@ class TestEmbedInputIds:
class TestMergeInterleavedEmbeddings:
def test_basic_interleaved(self):
"""Video chunks + audio chunks scattered to correct positions."""
m = _qwen2_5_omni_thinker_mod()
hidden = 4
input_ids, is_multimodal = make_interleaved_seq([3, 3], [2, 2])
@@ -299,7 +312,7 @@ class TestMergeInterleavedEmbeddings:
torch.full((num_audio, hidden), 10.0),
]
result = merge_interleaved_embeddings(
result = m.merge_interleaved_embeddings(
inputs_embeds,
mm_embeds,
is_video,
@@ -7,19 +7,46 @@ For modelopt FP8/FP4/MXFP8 checkpoints the Thinker LM is the only
quantized component. Vision and audio encoder weights are BF16 with no
FP8 scale tensors — passing quant_config to them causes FP8 kernels to
run on BF16 weights, producing garbage embeddings.
**Import note (pytest + ``tests/model_executor`` only):** :mod:`vllm_omni.quantization`
:file:`__init__` pulls ``factory``/``inc`` and reproduces vLLM CustomOp double
registration. We load the same :file:`component_config.py` via
:data:`importlib` under a *non-package* name so the package ``__init__`` is
never executed for this file.
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from vllm_omni.quantization.component_config import (
PRE_QUANTIZED_METHODS,
ComponentQuantizationConfig,
resolve_encoder_quant_config,
)
_CC_MODULE_NAME = "_vllm_omni_tests_component_config_encoder_quant"
def _load_component_config():
if _CC_MODULE_NAME in sys.modules:
return sys.modules[_CC_MODULE_NAME]
root = Path(__file__).resolve().parents[3]
path = root / "vllm_omni" / "quantization" / "component_config.py"
if not path.is_file():
raise FileNotFoundError(f"expected component_config at {path}")
spec = importlib.util.spec_from_file_location(_CC_MODULE_NAME, path)
if spec is None or spec.loader is None:
raise RuntimeError("failed to create import spec for component_config")
mod = importlib.util.module_from_spec(spec)
sys.modules[_CC_MODULE_NAME] = mod
spec.loader.exec_module(mod)
return mod
_cc = _load_component_config()
PRE_QUANTIZED_METHODS = _cc.PRE_QUANTIZED_METHODS
ComponentQuantizationConfig = _cc.ComponentQuantizationConfig
resolve_encoder_quant_config = _cc.resolve_encoder_quant_config
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -22,17 +22,18 @@ from vllm.config.multimodal import (
VideoDummyOptions,
)
from vllm.inputs import MultiModalDataDict, MultiModalInput
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.multimodal.cache import MultiModalProcessorOnlyCache
from vllm.multimodal.inputs import batched_tensors_equal
from vllm.multimodal.processing import BaseMultiModalProcessor, InputProcessingContext
from vllm.tokenizers import TokenizerLike, cached_tokenizer_from_config
from tests.model_executor.helpers import bootstrap_vllm_layer_custom_op_modules
from tests.model_executor.models.registry import (
_MULTIMODAL_OMNI_EXAMPLE_MODELS,
_OmniExamplesInfo,
)
from vllm_omni.config import OmniModelConfig
from vllm_omni.model_executor.models.registry import OmniModelRegistry
def random_image(rng: np.random.RandomState, min_wh: int, max_wh: int):
@@ -128,6 +129,36 @@ def _build_model_config(model_arch: str, info: _OmniExamplesInfo) -> OmniModelCo
return model_config
def _get_model_class_for_omni_processing(
model_config: OmniModelConfig,
):
"""Resolve the multimodal model class for processor tests.
vLLM's ``MULTIMODAL_REGISTRY._get_model_cls`` uses
:func:`get_model_architecture`, which re-imports
:mod:`vllm.model_executor.model_loader` and can re-register the same
custom Torch / CustomOp names when tests run in isolation
(``tests/model_executor/``). Omni-registered classes are loaded
unambiguously via :meth:`OmniModelRegistry._try_load_model_cls`.
:func:`bootstrap_vllm_layer_custom_op_modules` also runs (see
:file:`../helpers.py` and :file:`../conftest.py`) so vLLM layer modules are in :data:`sys.modules`
before shims like ``qwen2_5_omni_thinker`` import
:mod:`vllm.model_executor.models.qwen2_5_omni_thinker` (avoids duplicate
``@CustomOp.register`` e.g. ``fatrelu_and_mul``).
"""
bootstrap_vllm_layer_custom_op_modules()
if not model_config.model_arch:
raise ValueError("OmniModelConfig.model_arch is required for processing tests")
model_cls = OmniModelRegistry._try_load_model_cls(model_config.model_arch)
if model_cls is None:
raise RuntimeError(
f"OmniModelRegistry has no class registered for {model_config.model_arch!r}; "
"add it to vllm_omni.model_executor.models or fix the test matrix."
)
return model_cls
def _test_processing_correctness(
model_arch: str,
hit_rate: float,
@@ -141,7 +172,7 @@ def _test_processing_correctness(
model_config = _build_model_config(model_arch, info)
model_cls = MULTIMODAL_REGISTRY._get_model_cls(model_config)
model_cls = _get_model_class_for_omni_processing(model_config)
factories = model_cls._processor_factory
ctx = InputProcessingContext(
model_config,
@@ -4,17 +4,26 @@
from __future__ import annotations
import functools
import pytest
torch = pytest.importorskip("torch")
from vllm_omni.model_executor.models.voxcpm2.voxcpm2_talker import ( # noqa: E402
VoxCPM2TalkerForConditionalGeneration,
_RequestState,
)
@functools.lru_cache(maxsize=1)
def _voxcpm2_talker_mod():
"""Defer talker import (pulls vLLM model_executor) until first use."""
from vllm_omni.model_executor.models.voxcpm2.voxcpm2_talker import (
VoxCPM2TalkerForConditionalGeneration,
_RequestState,
)
return VoxCPM2TalkerForConditionalGeneration, _RequestState
def _make_bare_talker() -> VoxCPM2TalkerForConditionalGeneration:
def _make_bare_talker():
VoxCPM2TalkerForConditionalGeneration, _ = _voxcpm2_talker_mod()
talker = VoxCPM2TalkerForConditionalGeneration.__new__(VoxCPM2TalkerForConditionalGeneration)
talker._active_states = {}
talker._current_request_id = None
@@ -28,7 +37,8 @@ def _make_bare_talker() -> VoxCPM2TalkerForConditionalGeneration:
return talker
def _seed_cached_decode(talker, req_id: str) -> _RequestState:
def _seed_cached_decode(talker, req_id: str):
_, _RequestState = _voxcpm2_talker_mod()
state = _RequestState(request_id=req_id)
state.prefill_completed = True
state.decode_step_count = 5
@@ -109,8 +119,9 @@ class TestLeakWarnGuard:
talker = _make_bare_talker()
talker._active_state_warn_threshold = 3
_, RState = _voxcpm2_talker_mod()
for i in range(4):
talker._active_states[f"seed-{i}"] = _RequestState(request_id=f"seed-{i}")
talker._active_states[f"seed-{i}"] = RState(request_id=f"seed-{i}")
talker._get_or_create_state("new-1")
talker._get_or_create_state("new-2")
@@ -8,16 +8,33 @@ produced by generator2tokenizer_async_chunk and cut leading context samples
from decoded audio arrays.
"""
import functools
import pytest
import torch
from vllm_omni.model_executor.models.voxtral_tts.voxtral_tts import (
apply_ctx_frames_cutting,
parse_batched_audio_input,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@functools.lru_cache(maxsize=1)
def _voxtral_tts_parsers():
# ``vllm_omni...voxtral_tts`` does ``from vllm.model_executor.models.utils import ...``.
# If that module was first pulled in under a different import context in the
# same process, ``direct_register_custom_op`` for ``sequence_parallel_chunk_impl``
# can run twice. Match ``tests.model_executor.helpers`` + pin ``utils`` first.
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 (
apply_ctx_frames_cutting,
parse_batched_audio_input,
)
return parse_batched_audio_input, apply_ctx_frames_cutting
NUM_CODEBOOKS = 37
@@ -37,6 +54,7 @@ def _make_input_ids(*requests: tuple[int, int, torch.Tensor]) -> torch.Tensor:
def test_parse_single_request():
"""Single request is parsed correctly."""
parse_batched_audio_input, _ = _voxtral_tts_parsers()
ctx_frames = 3
context_length = 5
total_frames = ctx_frames + context_length
@@ -54,6 +72,7 @@ def test_parse_single_request():
def test_parse_multiple_requests():
"""Multiple requests in a batch are parsed correctly."""
parse_batched_audio_input, _ = _voxtral_tts_parsers()
req1 = (2, 4, torch.ones((6, NUM_CODEBOOKS), dtype=torch.long))
req2 = (0, 3, torch.full((3, NUM_CODEBOOKS), 7, dtype=torch.long))
input_ids = _make_input_ids(req1, req2)
@@ -70,6 +89,7 @@ def test_parse_multiple_requests():
def test_parse_zero_ctx_frames():
"""ctx_frames=0 means no context, just chunk data."""
parse_batched_audio_input, _ = _voxtral_tts_parsers()
ctx_frames = 0
context_length = 10
tokens = torch.randint(0, 100, (context_length, NUM_CODEBOOKS))
@@ -83,6 +103,7 @@ def test_parse_zero_ctx_frames():
def test_parse_fails_on_misaligned_input():
"""Input not divisible by num_codebooks should raise AssertionError."""
parse_batched_audio_input, _ = _voxtral_tts_parsers()
# 2 header elements + 5 data elements (not divisible by 37)
input_ids = torch.tensor([0, 1] + [0] * 5)
with pytest.raises(AssertionError, match="divisible by"):
@@ -91,6 +112,7 @@ def test_parse_fails_on_misaligned_input():
def test_parse_custom_num_codebooks():
"""Works with a non-default num_codebooks value."""
parse_batched_audio_input, _ = _voxtral_tts_parsers()
cb = 4
ctx_frames = 1
context_length = 2
@@ -111,6 +133,7 @@ def test_parse_custom_num_codebooks():
def test_cut_removes_leading_samples():
"""Context frames are removed from the front of the audio array."""
_, apply_ctx_frames_cutting = _voxtral_tts_parsers()
downsample_factor = 240
ctx_frames = 5
total_samples = 2400 # 10 frames * 240
@@ -126,6 +149,7 @@ def test_cut_removes_leading_samples():
def test_cut_zero_ctx_frames_unchanged():
"""ctx_frames=0 leaves the audio unchanged."""
_, apply_ctx_frames_cutting = _voxtral_tts_parsers()
downsample_factor = 240
audio = torch.randn(2400)
@@ -137,6 +161,7 @@ def test_cut_zero_ctx_frames_unchanged():
def test_cut_multiple_requests():
"""Each request in the batch gets its own ctx_frames cut."""
_, apply_ctx_frames_cutting = _voxtral_tts_parsers()
downsample_factor = 100
audio1 = torch.arange(1000, dtype=torch.float32) # 10 frames
audio2 = torch.arange(500, dtype=torch.float32) # 5 frames
@@ -152,6 +177,7 @@ def test_cut_multiple_requests():
def test_cut_all_frames_returns_empty():
"""Cutting all frames returns an empty tensor."""
_, apply_ctx_frames_cutting = _voxtral_tts_parsers()
downsample_factor = 100
ctx_frames = 5
audio = torch.randn(500) # exactly 5 frames
@@ -12,8 +12,7 @@ Euler ODE with CFG. The synthetic model here uses simple linear layers
to exercise the wrapper mechanism while keeping the test lightweight.
"""
import importlib.util
import os
import functools
import pytest
import torch
@@ -32,50 +31,18 @@ N_ACOUSTIC_CODEBOOK = 7
SEMANTIC_CODEBOOK_SIZE = 128
ACOUSTIC_EMBEDDINGS_LEVELS = 1024
# Load CUDAGraphAcousticTransformerWrapper: try package import first, fall back to direct file load
try:
@functools.lru_cache(maxsize=1)
def _voxtral_cudagraph_deps():
"""Load voxtral CUDA graph helpers only when CUDA tests run (avoids re-exec + duplicate vLLM op registration)."""
from vllm_omni.model_executor.models.voxtral_tts.cuda_graph_acoustic_transformer_wrapper import (
CUDAGraphAcousticTransformerWrapper,
)
from vllm_omni.model_executor.models.voxtral_tts.voxtral_tts_audio_generation import (
AudioSpecialTokens,
)
except Exception:
_WRAPPER_PATH = os.path.join(
os.path.dirname(__file__),
os.pardir,
os.pardir,
os.pardir,
os.pardir,
"vllm_omni",
"model_executor",
"models",
"voxtral_tts",
"cuda_graph_acoustic_transformer_wrapper.py",
)
_spec = importlib.util.spec_from_file_location(
"cuda_graph_acoustic_transformer_wrapper", os.path.abspath(_WRAPPER_PATH)
)
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)
CUDAGraphAcousticTransformerWrapper = _mod.CUDAGraphAcousticTransformerWrapper
_AUDIO_GEN_PATH = os.path.join(
os.path.dirname(__file__),
os.pardir,
os.pardir,
os.pardir,
os.pardir,
"vllm_omni",
"model_executor",
"models",
"voxtral_tts",
"voxtral_tts_audio_generation.py",
)
_spec2 = importlib.util.spec_from_file_location("voxtral_tts_audio_generation", os.path.abspath(_AUDIO_GEN_PATH))
_mod2 = importlib.util.module_from_spec(_spec2)
_spec2.loader.exec_module(_mod2)
AudioSpecialTokens = _mod2.AudioSpecialTokens
return CUDAGraphAcousticTransformerWrapper, AudioSpecialTokens
class SyntheticAcousticTransformerArgs:
@@ -102,6 +69,7 @@ class SyntheticAcousticTransformer(nn.Module):
def __init__(self):
super().__init__()
_, AudioSpecialTokens = _voxtral_cudagraph_deps()
self.model_args = SyntheticModelArgs()
self.acoustic_transformer_args = SyntheticAcousticTransformerArgs()
self.acoustic_embeddings_levels = ACOUSTIC_EMBEDDINGS_LEVELS
@@ -128,6 +96,7 @@ class SyntheticModel(nn.Module):
def __init__(self):
super().__init__()
_, AudioSpecialTokens = _voxtral_cudagraph_deps()
self.acoustic_transformer = SyntheticAcousticTransformer()
end_audio_id = AudioSpecialTokens.id(AudioSpecialTokens.end_audio)
empty_audio_id = AudioSpecialTokens.id(AudioSpecialTokens.empty_audio)
@@ -140,6 +109,7 @@ class SyntheticModel(nn.Module):
cfg_alpha: torch.Tensor,
):
"""Eager fallback path: replicate what the wrapper does."""
_, AudioSpecialTokens = _voxtral_cudagraph_deps()
at = self.acoustic_transformer
B = hidden_states.shape[0]
@@ -200,6 +170,7 @@ def model():
@pytest.fixture(scope="module")
def wrapper(model):
"""Create a warmed-up CUDAGraphAcousticTransformerWrapper."""
CUDAGraphAcousticTransformerWrapper, _ = _voxtral_cudagraph_deps()
w = CUDAGraphAcousticTransformerWrapper(
model=model,
capture_sizes=[1, 2, 4, 8, 16, 32],