diff --git a/tests/model_executor/models/minicpmo_4_5/test_hf_model_path.py b/tests/model_executor/models/minicpmo_4_5/test_hf_model_path.py new file mode 100644 index 000000000..dc1599e6b --- /dev/null +++ b/tests/model_executor/models/minicpmo_4_5/test_hf_model_path.py @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Regression test for HF model-ID resolution and path caching in MiniCPM-o 4.5 TTS.""" + +from types import SimpleNamespace + +import pytest +import torch + +from vllm_omni.model_executor.models.minicpmo_4_5.minicpmo_4_5_omni_tts import ( + MiniCPMO45OmniTTSForConditionalGeneration, +) + +pytestmark = [pytest.mark.core_model, pytest.mark.cpu] + + +def test_hf_model_id_path_caching(mocker, tmp_path): + """Verify HF model-ID path is resolved via download_weights_from_hf_specific + once during _lazy_init_tts, cached in self._model_path, and reused in generate_speech. + """ + resolved_path = str(tmp_path / "resolved_hf_model") + assets_dir = tmp_path / "resolved_hf_model" / "assets" + assets_dir.mkdir(parents=True, exist_ok=True) + ref_audio_file = assets_dir / "HT_ref_audio.wav" + ref_audio_file.write_bytes(b"dummy wav content") + + mock_download = mocker.patch( + "vllm_omni.model_executor.models.minicpmo_4_5.minicpmo_4_5_omni_tts.download_weights_from_hf_specific", + return_value=resolved_path, + ) + + # Mock dynamic class loading + fake_tts_class = mocker.MagicMock() + mocker.patch( + "transformers.dynamic_module_utils.get_class_from_dynamic_module", + return_value=fake_tts_class, + ) + + # Build fake vllm_config with HF model ID + model_id = "openbmb/MiniCPM-o-4_5" + tts_cfg = SimpleNamespace( + audio_bos_token_id=151687, + text_eos_token_id=151692, + num_audio_tokens=6562, + hidden_size=768, + normalize_projected_hidden=True, + top_p=0.8, + top_k=100, + repetition_penalty=1.02, + attn_implementation="sdpa", + ) + hf_config = SimpleNamespace(tts_config=tts_cfg) + vllm_config = SimpleNamespace( + model_config=SimpleNamespace( + model=model_id, + hf_config=hf_config, + ) + ) + + tts_model = MiniCPMO45OmniTTSForConditionalGeneration(vllm_config=vllm_config) + + # 1. Trigger lazy init + tts_model._lazy_init_tts() + + # Verify download_weights_from_hf_specific was called with HF model ID and cached + mock_download.assert_called_once_with(model_id, None, ["*"]) + assert tts_model._model_path == resolved_path + assert tts_model._assets_loaded is True + + # 2. Mock TTS object generate method and audio tokenizer for generate_speech + fake_tts_obj = mocker.MagicMock() + fake_tts_obj.emb_text.weight.device = torch.device("cpu") + fake_tts_obj.emb_text.side_effect = lambda t: torch.zeros(t.shape[0], 768) + fake_tts_obj.projector_semantic.side_effect = lambda h, **kw: torch.zeros(h.shape[0], 768) + fake_tts_obj.config = tts_cfg + fake_tts_obj.audio_bos_token_id = 151687 + + fake_output = SimpleNamespace(new_ids=torch.tensor([[[100]]])) + fake_tts_obj.generate.return_value = fake_output + + tts_model.tts_obj = fake_tts_obj + fake_tokenizer = mocker.MagicMock(return_value=b"dummy_wav_bytes") + tts_model.audio_tokenizer = fake_tokenizer + + # Mock sf.read to return fake numpy audio + mocker.patch( + "soundfile.read", + return_value=(torch.zeros(16000).numpy(), 24000), + ) + + # Call generate_speech + tts_token_ids = torch.tensor([1, 2, 3]) + tts_hidden_states = torch.zeros(3, 768) + waveform = tts_model.generate_speech(tts_token_ids, tts_hidden_states) + + # Verify download_weights_from_hf_specific was NOT called a second time + assert mock_download.call_count == 1 + assert waveform is not None diff --git a/vllm_omni/model_executor/models/minicpmo_4_5/minicpmo_4_5_omni_tts.py b/vllm_omni/model_executor/models/minicpmo_4_5/minicpmo_4_5_omni_tts.py index 71681c222..f109eb737 100644 --- a/vllm_omni/model_executor/models/minicpmo_4_5/minicpmo_4_5_omni_tts.py +++ b/vllm_omni/model_executor/models/minicpmo_4_5/minicpmo_4_5_omni_tts.py @@ -26,6 +26,9 @@ import torch.nn.functional as F from vllm.config import VllmConfig from vllm.model_executor.models.interfaces import SupportsPP +from vllm_omni.model_executor.model_loader.weight_utils import ( + download_weights_from_hf_specific, +) from vllm_omni.platforms import current_omni_platform # Preserve the established external vocoder on CUDA. Ascend uses the in-tree @@ -105,6 +108,7 @@ class MiniCPMO45OmniTTSForConditionalGeneration(nn.Module, SupportsPP): self.tts = None self.audio_tokenizer = None self._assets_loaded = False + self._model_path = None tts_config = getattr(config, "tts_config", None) if tts_config is not None: @@ -121,7 +125,8 @@ class MiniCPMO45OmniTTSForConditionalGeneration(nn.Module, SupportsPP): if self._assets_loaded or self._tts_config is None: return try: - model_path = self.vllm_config.model_config.model + model_path = download_weights_from_hf_specific(self.vllm_config.model_config.model, None, ["*"]) + self._model_path = model_path if model_path not in sys.path: sys.path.insert(0, model_path) @@ -279,7 +284,9 @@ class MiniCPMO45OmniTTSForConditionalGeneration(nn.Module, SupportsPP): import torchaudio - model_path = self.vllm_config.model_config.model + model_path = self._model_path or download_weights_from_hf_specific( + self.vllm_config.model_config.model, None, ["*"] + ) default_ref = os.path.join(model_path, "assets", "HT_ref_audio.wav") prompt_wav_path = default_ref if os.path.exists(default_ref) else None