diff --git a/tests/config/test_config_import_cycle.py b/tests/config/test_config_import_cycle.py new file mode 100644 index 000000000..d73f6265e --- /dev/null +++ b/tests/config/test_config_import_cycle.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project +"""Guard the NPU pytest circular import through DiffusionOutput.""" + +from __future__ import annotations + +import ast +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +pytestmark = [pytest.mark.core_model, pytest.mark.cpu] +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def _top_level_import_modules(path: Path) -> set[str]: + tree = ast.parse(path.read_text(encoding="utf-8")) + modules: set[str] = set() + for node in tree.body: + if isinstance(node, ast.ImportFrom) and node.module: + modules.add(node.module) + elif isinstance(node, ast.Import): + modules.update(alias.name for alias in node.names) + return modules + + +def _assert_isolated_import_succeeds(script: str) -> None: + env = os.environ.copy() + env.pop("CUDA_VISIBLE_DEVICES", None) + env.pop("HIP_VISIBLE_DEVICES", None) + result = subprocess.run( + [sys.executable, "-c", script], + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + timeout=120, + check=False, + ) + assert result.returncode == 0, result.stderr + + +def test_config_package_does_not_eagerly_import_pipeline_registry() -> None: + imported = _top_level_import_modules(REPO_ROOT / "vllm_omni" / "config" / "__init__.py") + assert "vllm_omni.config.config_factory" not in imported + assert "vllm_omni.config.pipeline_registry" not in imported + + +def test_config_package_lazy_exports_still_resolve() -> None: + from vllm_omni.config import StageConfigFactory, register_pipeline + from vllm_omni.config.config_factory import StageConfigFactory as DirectStageConfigFactory + from vllm_omni.config.pipeline_registry import register_pipeline as direct_register_pipeline + + assert StageConfigFactory is DirectStageConfigFactory + assert register_pipeline is direct_register_pipeline + + +def test_npu_platform_defers_minimax_h3_encoder_patch() -> None: + src = (REPO_ROOT / "vllm_omni" / "platforms" / "npu" / "platform.py").read_text(encoding="utf-8") + init_start = src.index("def __init__(self) -> None:") + runtime_start = src.index("def init_diffusion_model_runner_runtime") + init_src = src[init_start:runtime_start] + runtime_src = src[runtime_start:] + assert "apply_minimax_h3_qwen3vl_patch" not in init_src + assert "apply_minimax_h3_qwen3vl_patch" in runtime_src + assert "apply_minimax_h3_qwen3vl_swiglu_patch" in runtime_src + + +def test_lora_config_import_does_not_load_pipeline_registry() -> None: + """Import LoRAConfig and diffusion.data in a fresh interpreter. + + The NPU failure was ``data.py`` importing LoRAConfig while still loading, + then ``vllm_omni.config`` re-entering ``pipeline_registry`` → PI0 → + DiffusionOutput. This child process must not preload those exports. + """ + _assert_isolated_import_succeeds( + """ +import sys + +from vllm_omni.config.lora import LoRAConfig + +if "vllm_omni.config.pipeline_registry" in sys.modules: + raise SystemExit("LoRAConfig import loaded pipeline_registry") +if "vllm_omni.diffusion.models.pi0.pipeline_pi0" in sys.modules: + raise SystemExit("LoRAConfig import loaded PI0_PIPELINE") + +from vllm_omni.diffusion.data import DiffusionOutput + +if DiffusionOutput is None or LoRAConfig is None: + raise SystemExit("expected LoRAConfig and DiffusionOutput") +""" + ) diff --git a/vllm_omni/config/__init__.py b/vllm_omni/config/__init__.py index 3069025be..f53a1ef74 100644 --- a/vllm_omni/config/__init__.py +++ b/vllm_omni/config/__init__.py @@ -2,7 +2,6 @@ Configuration module for vLLM-Omni. """ -from vllm_omni.config.config_factory import StageConfigFactory from vllm_omni.config.lora import LoRAConfig from vllm_omni.config.model import OmniModelConfig from vllm_omni.config.omni_config import ( @@ -22,7 +21,6 @@ from vllm_omni.config.omni_config import ( VllmOmniGenerationStageConfig, VllmOmniOrchestratorConfig, ) -from vllm_omni.config.pipeline_registry import register_pipeline from vllm_omni.config.stage_config import ( PIPELINE_WIDE_ENGINE_FIELDS, DeployConfig, @@ -42,6 +40,15 @@ from vllm_omni.config.yaml_util import ( to_dict, ) +# StageConfigFactory / register_pipeline pull pipeline_registry, which eagerly +# imports PI0_PIPELINE → diffusion.data. Keep those lazy so +# `from vllm_omni.config.lora import LoRAConfig` (used while data.py is still +# loading) cannot close a circular import through DiffusionOutput. +_LAZY_ATTRS = { + "StageConfigFactory": ("vllm_omni.config.config_factory", "StageConfigFactory"), + "register_pipeline": ("vllm_omni.config.pipeline_registry", "register_pipeline"), +} + __all__ = [ # Legacy model-level configs. "LoRAConfig", @@ -82,3 +89,19 @@ __all__ = [ "merge_configs", "to_dict", ] + + +def __getattr__(name: str): + spec = _LAZY_ATTRS.get(name) + if spec is None: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + module_name, attr_name = spec + import importlib + + value = getattr(importlib.import_module(module_name), attr_name) + globals()[name] = value + return value + + +def __dir__() -> list[str]: + return sorted(set(__all__) | set(globals())) diff --git a/vllm_omni/platforms/npu/platform.py b/vllm_omni/platforms/npu/platform.py index 38c35f1f7..9a6a278e4 100644 --- a/vllm_omni/platforms/npu/platform.py +++ b/vllm_omni/platforms/npu/platform.py @@ -39,7 +39,6 @@ class NPUOmniPlatform(OmniPlatform, NPUPlatform): from vllm_omni.platforms.npu.models.minicpmo_4_5_code2wav import ( apply_minicpmo_4_5_code2wav_patch, ) - from vllm_omni.platforms.npu.models.minimax_h3 import apply_minimax_h3_qwen3vl_patch from vllm_omni.platforms.npu.models.qwen3_tts_code2wav import ( apply_qwen3_tts_code2wav_patch, ) @@ -48,7 +47,6 @@ class NPUOmniPlatform(OmniPlatform, NPUPlatform): ) adapt_patch(is_global_patch=True) - apply_minimax_h3_qwen3vl_patch() apply_minicpmo_4_5_code2wav_patch() apply_qwen3_tts_code2wav_patch() apply_qwen3_tts_tokenizer_v2_patch() @@ -87,12 +85,17 @@ class NPUOmniPlatform(OmniPlatform, NPUPlatform): from vllm_ascend.ascend_forward_context import set_mc2_mask, set_mc2_tokens_capacity from vllm_omni.platforms.npu.models.minimax_h3 import ( + apply_minimax_h3_qwen3vl_patch, apply_minimax_h3_qwen3vl_swiglu_patch, ) - # The patch imports the MiniMax encoder, which depends on - # current_omni_platform. Run it only after platform construction has - # completed, but before the diffusion pipeline is loaded. + # Both patches import the MiniMax encoder package, whose __init__ loads + # pipeline_minimax_h3 → diffusion.data. Doing that during platform + # construction races vllm_omni/__init__.py (patch before config) and + # closes a cycle through pipeline_registry → PI0_PIPELINE → + # DiffusionOutput. Apply them only after the platform exists, before + # the diffusion pipeline is loaded. + apply_minimax_h3_qwen3vl_patch() apply_minimax_h3_qwen3vl_swiglu_patch() set_mc2_tokens_capacity(vllm_config, od_config.max_num_seqs, 1) set_mc2_mask(vllm_config, device)