From 4be6bbe0e4e91f49e67300a080989bf1d5915cac Mon Sep 17 00:00:00 2001 From: chickeyton Date: Fri, 25 Sep 2026 22:57:23 +0800 Subject: [PATCH] [Bugfix][MiniCPM-o] Build the Stage-0 duplex runtime at load time (#8146) Signed-off-by: chickeyton Co-authored-by: Claude Opus 5.5 (1M context) --- .../duplex/test_stage0_runtime_load.py | 61 +++++++++++++++++++ .../models/minicpmo_4_5/minicpmo_4_5_omni.py | 10 +++ 2 files changed, 71 insertions(+) create mode 100644 tests/model_executor/models/minicpmo_4_5/duplex/test_stage0_runtime_load.py diff --git a/tests/model_executor/models/minicpmo_4_5/duplex/test_stage0_runtime_load.py b/tests/model_executor/models/minicpmo_4_5/duplex/test_stage0_runtime_load.py new file mode 100644 index 000000000..364e56f8f --- /dev/null +++ b/tests/model_executor/models/minicpmo_4_5/duplex/test_stage0_runtime_load.py @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project +"""The MiniCPM-o 4.5 Stage-0 duplex runtime is built with the model, not in the first session.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest +import torch + +from vllm_omni.model_executor.models.minicpmo_4_5 import minicpmo_4_5_omni +from vllm_omni.model_executor.models.minicpmo_4_5.duplex import compat +from vllm_omni.model_executor.models.minicpmo_4_5.minicpmo_4_5_omni import ( + MiniCPMO45OmniForConditionalGeneration, +) + +pytestmark = [pytest.mark.core_model, pytest.mark.cpu] + + +class _StageModel(torch.nn.Module): + def make_empty_intermediate_tensors(self): + return None + + +@pytest.mark.parametrize( + ("model_stage", "session_mode", "expected_builds"), + [ + ("llm", "duplex", 1), + ("llm", "turn", 0), + ("tts", "duplex", 0), + ], +) +def test_init_builds_duplex_runtime_only_for_duplex_thinker( + monkeypatch: pytest.MonkeyPatch, + model_stage: str, + session_mode: str, + expected_builds: int, +) -> None: + monkeypatch.setattr(minicpmo_4_5_omni, "init_vllm_registered_model", lambda **kwargs: _StageModel()) + monkeypatch.setattr(compat, "patch_minicpmo_remote_config", lambda config: None) + build_devices: list[torch.device] = [] + monkeypatch.setattr( + MiniCPMO45OmniForConditionalGeneration, + "_duplex_data_plane_helper", + lambda self: build_devices.append(torch.get_default_device()), + ) + vllm_config = SimpleNamespace( + model_config=SimpleNamespace( + hf_config=SimpleNamespace(), + multimodal_config=None, + model_stage=model_stage, + session_mode=session_mode, + ) + ) + + # The loader constructs the model under the target-device context. + with torch.device("meta"): + MiniCPMO45OmniForConditionalGeneration(vllm_config=vllm_config) + + assert build_devices == [torch.device("cpu")] * expected_builds diff --git a/vllm_omni/model_executor/models/minicpmo_4_5/minicpmo_4_5_omni.py b/vllm_omni/model_executor/models/minicpmo_4_5/minicpmo_4_5_omni.py index 16269d893..69655209d 100644 --- a/vllm_omni/model_executor/models/minicpmo_4_5/minicpmo_4_5_omni.py +++ b/vllm_omni/model_executor/models/minicpmo_4_5/minicpmo_4_5_omni.py @@ -144,6 +144,16 @@ class MiniCPMO45OmniForConditionalGeneration(nn.Module, SupportsMultiModal, Supp # embeddings and initializes request-local codec generation state. self.has_preprocess = self.model_stage in {"llm", "tts"} + if self.model_stage == "llm" and getattr(vllm_config.model_config, "session_mode", "turn") == "duplex": + # Build the Stage-0 duplex runtime (remote-code processor and + # tokenizer) with the model. Built lazily, it costs several seconds + # inside the first session's first audio unit, and the session then + # runs that far behind the real-time input stream. The loader + # constructs the model under the target-device context; the + # processor is CPU preprocessing, so keep its tensors on the CPU. + with torch.device("cpu"): + self._duplex_data_plane_helper() + @cached_property def sampler(self): if hasattr(self.model, "sampler"):