# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """Unit tests for ModuleDiscovery and SupportsComponentDiscovery.""" from typing import ClassVar import pytest from torch import nn from vllm_omni.diffusion.models.interface import SupportsComponentDiscovery from vllm_omni.diffusion.offloader.module_collector import ModuleDiscovery pytestmark = [pytest.mark.diffusion, pytest.mark.cpu, pytest.mark.core_model] # NOTE: tests for skipped/warned attributes verify the *behavioral* # outcome (attribute excluded from results) but do not assert on log # output. vllm's logger sets propagate=False, preventing caplog from # capturing records. See https://github.com/pytest-dev/pytest/issues/3697 # --------------------------------------------------------------------------- # Test pipelines # --------------------------------------------------------------------------- class FallbackPipeline(nn.Module): """Pipeline with standard attribute names (no protocol).""" def __init__(self): super().__init__() self.transformer = nn.Linear(10, 10) self.text_encoder = nn.Linear(10, 10) self.text_encoder_2 = nn.Linear(10, 10) self.vae = nn.Linear(10, 10) class NonModuleAttrPipeline(nn.Module): """Pipeline where an attribute is not an nn.Module (fallback path).""" def __init__(self): super().__init__() self.transformer = nn.Linear(10, 10) self.text_encoder = "not_a_module" self.vae = nn.Linear(10, 10) class DuplicateAttrPipeline(nn.Module): """Pipeline where two encoder attrs point to the same module.""" def __init__(self): super().__init__() self.transformer = nn.Linear(10, 10) encoder = nn.Linear(10, 10) self.text_encoder = encoder self.text_encoder_2 = encoder self.vae = nn.Linear(10, 10) class ProtocolPipeline(nn.Module, SupportsComponentDiscovery): """Pipeline with non-standard names, using the protocol.""" _dit_modules: ClassVar[list[str]] = ["gen_transformer"] _encoder_modules: ClassVar[list[str]] = ["mllm", "vision_model"] _vae_modules: ClassVar[list[str]] = ["gen_vae"] def __init__(self): super().__init__() self.gen_transformer = nn.Linear(10, 10) self.mllm = nn.Linear(10, 10) self.vision_model = nn.Linear(10, 10) self.gen_vae = nn.Linear(10, 10) # Standard name present but NOT declared — should be ignored self.transformer = nn.Linear(10, 10) class MissingAttrPipeline(nn.Module, SupportsComponentDiscovery): """Pipeline that declares a non-existent attribute.""" _dit_modules: ClassVar[list[str]] = ["transformer"] _encoder_modules: ClassVar[list[str]] = ["nonexistent_encoder"] _vae_modules: ClassVar[list[str]] = ["vae"] def __init__(self): super().__init__() self.transformer = nn.Linear(10, 10) self.vae = nn.Linear(10, 10) class MissingIntermediatePipeline(nn.Module, SupportsComponentDiscovery): """Pipeline with dotted path referencing non-existent intermediate.""" _dit_modules: ClassVar[list[str]] = ["nonexistent.transformer"] _encoder_modules: ClassVar[list[str]] = [] _vae_modules: ClassVar[list[str]] = [] def __init__(self): super().__init__() class NestedPipeline(nn.Module, SupportsComponentDiscovery): """Pipeline with nested modules accessed via dotted paths.""" _dit_modules: ClassVar[list[str]] = ["pipe.transformer"] _encoder_modules: ClassVar[list[str]] = ["pipe.text_encoder"] _vae_modules: ClassVar[list[str]] = ["vae"] def __init__(self): super().__init__() self.pipe = nn.Module() self.pipe.transformer = nn.Linear(10, 10) self.pipe.text_encoder = nn.Linear(10, 10) self.vae = nn.Linear(10, 10) class ResidentPipeline(nn.Module, SupportsComponentDiscovery): """Pipeline with resident modules that must stay on GPU.""" _dit_modules: ClassVar[list[str]] = ["language_model.model"] _encoder_modules: ClassVar[list[str]] = [] _vae_modules: ClassVar[list[str]] = ["vae"] _resident_modules: ClassVar[list[str]] = [ "bagel.time_embedder", "bagel.vae2llm", ] def __init__(self): super().__init__() self.language_model = nn.Module() self.language_model.model = nn.Linear(10, 10) self.bagel = nn.Module() self.bagel.time_embedder = nn.Linear(10, 10) self.bagel.vae2llm = nn.Linear(10, 10) self.vae = nn.Linear(10, 10) class MultiVaePipeline(nn.Module, SupportsComponentDiscovery): """Pipeline with multiple VAEs.""" _dit_modules: ClassVar[list[str]] = ["transformer"] _encoder_modules: ClassVar[list[str]] = ["text_encoder"] _vae_modules: ClassVar[list[str]] = ["vae", "audio_vae"] def __init__(self): super().__init__() self.transformer = nn.Linear(10, 10) self.text_encoder = nn.Linear(10, 10) self.vae = nn.Linear(10, 10) self.audio_vae = nn.Linear(10, 10) # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- class TestFallbackDiscovery: """Test the fallback attribute scan (no SupportsComponentDiscovery).""" def test_discovers_standard_attrs(self): pipeline = FallbackPipeline() result = ModuleDiscovery.discover(pipeline) assert not isinstance(pipeline, SupportsComponentDiscovery) assert result.dit_names == ["transformer"] assert result.dits[0] is pipeline.transformer assert result.encoder_names == ["text_encoder", "text_encoder_2"] assert result.vaes[0] is pipeline.vae assert result.resident_modules == [] def test_deduplicates_encoders(self): pipeline = DuplicateAttrPipeline() result = ModuleDiscovery.discover(pipeline) assert len(result.encoders) == 1 assert result.encoder_names == ["text_encoder"] def test_skips_non_module_attr(self): pipeline = NonModuleAttrPipeline() result = ModuleDiscovery.discover(pipeline) assert len(result.encoders) == 0 class TestProtocolDiscovery: """Test discovery via SupportsComponentDiscovery protocol.""" def test_discovers_declared_attrs_and_ignores_undeclared(self): pipeline = ProtocolPipeline() result = ModuleDiscovery.discover(pipeline) assert isinstance(pipeline, SupportsComponentDiscovery) assert result.dit_names == ["gen_transformer"] assert result.encoder_names == ["mllm", "vision_model"] assert len(result.vaes) == 1 # self.transformer exists but is NOT in _dit_modules assert "transformer" not in result.dit_names # No _resident_modules declared — defaults to empty assert result.resident_modules == [] def test_skips_missing_attr(self): pipeline = MissingAttrPipeline() result = ModuleDiscovery.discover(pipeline) assert len(result.encoders) == 0 def test_skips_missing_intermediate(self): result = ModuleDiscovery.discover(MissingIntermediatePipeline()) assert len(result.dits) == 0 def test_dotted_path_resolves_nested_modules(self): pipeline = NestedPipeline() result = ModuleDiscovery.discover(pipeline) assert result.dit_names == ["pipe.transformer"] assert result.dits[0] is pipeline.pipe.transformer assert result.encoder_names == ["pipe.text_encoder"] assert result.encoders[0] is pipeline.pipe.text_encoder assert result.vaes[0] is pipeline.vae def test_resident_modules(self): pipeline = ResidentPipeline() result = ModuleDiscovery.discover(pipeline) assert result.resident_names == [ "bagel.time_embedder", "bagel.vae2llm", ] assert result.resident_modules[0] is pipeline.bagel.time_embedder assert result.resident_modules[1] is pipeline.bagel.vae2llm assert result.dits[0] is pipeline.language_model.model def test_multiple_vaes(self): pipeline = MultiVaePipeline() result = ModuleDiscovery.discover(pipeline) assert len(result.vaes) == 2 assert result.vaes[0] is pipeline.vae assert result.vaes[1] is pipeline.audio_vae