[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:
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(
|
||||
|
||||
+14
-6
@@ -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],
|
||||
|
||||
Reference in new issue
Block a user