Files
vllm-omni/tests/diffusion/offloader/test_plan_resolver.py

459 lines
18 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Unit tests for the shared offload plan resolver."""
from typing import ClassVar
import pytest
import torch
from torch import nn
from tests.diffusion.offloader.helpers import (
_DummyBlock,
_PlainEncoder,
_SingleBlockModel,
_StagedEncoder,
_StagedVAE,
patch_offload_runtime,
)
from vllm_omni.diffusion.offloader import layerwise_backend as layerwise_backend_module
from vllm_omni.diffusion.offloader import plan_resolver as plan_resolver_module
from vllm_omni.diffusion.offloader.base import OffloadConfig, OffloadStrategy
from vllm_omni.diffusion.offloader.layerwise_backend import LayerWiseOffloadBackend
from vllm_omni.diffusion.offloader.offload_plan import OffloadPlan
from vllm_omni.diffusion.offloader.plan_resolver import resolve_offload_plan
pytestmark = [pytest.mark.diffusion, pytest.mark.cpu, pytest.mark.core_model]
CPU = torch.device("cpu")
@pytest.fixture
def patched_offload_runtime(monkeypatch):
patch_offload_runtime(monkeypatch, layerwise_backend_module.current_omni_platform)
def _config(
components: set[str] | None = None,
strategy: OffloadStrategy = OffloadStrategy.LAYER_WISE,
**kwargs,
) -> OffloadConfig:
return OffloadConfig(
strategy=strategy,
pin_cpu_memory=False,
components=None if components is None else frozenset(components),
**kwargs,
)
def _stack_of(component):
assert len(component.stacks) == 1
return component.stacks[0]
class _WanStylePipeline(nn.Module):
"""Streamable DiT plus a plan-declared, replicated text encoder."""
_dit_modules: ClassVar[list[str]] = ["transformer"]
_encoder_modules: ClassVar[list[str]] = ["text_encoder"]
_vae_modules: ClassVar[list[str]] = ["vae"]
_resident_modules: ClassVar[list[str]] = []
_offload_plan = OffloadPlan(
encoder_component_types={"text_encoder": "text_encoder"},
encoder_block_attrs={"text_encoder": ("encoder.block",)},
encoder_dlo_weight_replication=frozenset({"text_encoder"}),
)
def __init__(self):
super().__init__()
self.transformer = _SingleBlockModel(num_blocks=4)
self.text_encoder = _PlainEncoder()
self.vae = _StagedVAE()
class _StagedComponentPipeline(nn.Module):
"""MiniMax-H3 shape: two encoder stacks plus pipeline-staged components."""
_dit_modules: ClassVar[list[str]] = ["transformer"]
_encoder_modules: ClassVar[list[str]] = ["text_encoder"]
_vae_modules: ClassVar[list[str]] = ["vae"]
_resident_modules: ClassVar[list[str]] = []
_offload_plan = OffloadPlan(
encoder_component_types={"text_encoder": "text_encoder"},
encoder_block_attrs={"text_encoder": ("vision.blocks", "text_model.layers")},
on_demand_component_paths=frozenset({"text_encoder", "vae"}),
resident_dit_paths=frozenset({"transformer"}),
)
def __init__(self):
super().__init__()
self.transformer = _SingleBlockModel(num_blocks=4)
self.text_encoder = _StagedEncoder()
self.vae = _StagedVAE()
class _LegacyScanPipeline(nn.Module):
"""No SupportsComponentDiscovery declaration and no OffloadPlan."""
def __init__(self):
super().__init__()
self.transformer = _SingleBlockModel(num_blocks=3)
self.text_encoder = _PlainEncoder()
class TestProducerPrecedence:
def test_plan_block_attrs_win_over_class_attributes(self):
class TwoStackDiT(nn.Module):
_layerwise_offload_blocks_attrs = ["blocks"]
def __init__(self):
super().__init__()
self.blocks = nn.ModuleList([_DummyBlock() for _ in range(2)])
self.alt_blocks = nn.ModuleList([_DummyBlock() for _ in range(3)])
class Pipeline(nn.Module):
_offload_plan = OffloadPlan(block_attrs={"transformer": ("alt_blocks",)})
def __init__(self):
super().__init__()
self.transformer = TwoStackDiT()
pipeline = Pipeline()
resolved = resolve_offload_plan(pipeline, _config({"dit"}))
stack = _stack_of(resolved.dits[0])
assert stack.attrs == ("alt_blocks",)
assert list(stack.blocks) == list(pipeline.transformer.alt_blocks)
def test_class_attributes_are_used_without_a_plan(self):
pipeline = _LegacyScanPipeline()
resolved = resolve_offload_plan(pipeline, _config({"dit"}))
assert list(_stack_of(resolved.dits[0]).blocks) == list(pipeline.transformer.blocks)
def test_declared_component_type_wins_over_leaf_name(self):
class Pipeline(nn.Module):
_dit_modules: ClassVar[list[str]] = ["transformer"]
_encoder_modules: ClassVar[list[str]] = ["mllm"]
_vae_modules: ClassVar[list[str]] = []
_resident_modules: ClassVar[list[str]] = []
_offload_plan = OffloadPlan(
encoder_component_types={"mllm": "text_encoder"},
encoder_block_attrs={"mllm": ("encoder.block",)},
)
def __init__(self):
super().__init__()
self.transformer = _SingleBlockModel(num_blocks=2)
self.mllm = _PlainEncoder()
resolved = resolve_offload_plan(Pipeline(), _config({"text_encoder"}))
encoder = resolved.encoders[0]
assert encoder.path == "mllm"
assert encoder.selected
assert len(_stack_of(encoder).blocks) == 2
def test_component_declaration_wins_over_attribute_scan(self):
class Pipeline(nn.Module):
_dit_modules: ClassVar[list[str]] = ["custom_dit"]
_encoder_modules: ClassVar[list[str]] = []
_vae_modules: ClassVar[list[str]] = []
_resident_modules: ClassVar[list[str]] = []
def __init__(self):
super().__init__()
self.custom_dit = _SingleBlockModel(num_blocks=2)
# Would be picked up by the deprecated attribute scan.
self.transformer = _SingleBlockModel(num_blocks=2)
resolved = resolve_offload_plan(Pipeline(), _config({"dit"}))
assert [component.path for component in resolved.dits] == ["custom_dit"]
def test_attribute_scan_warns_once_per_pipeline_class(self, monkeypatch):
warnings: list[str] = []
class _Recorder:
def warning(self, message, *args):
warnings.append(message % args if args else message)
def __getattr__(self, _name):
return lambda *args, **kwargs: None
monkeypatch.setattr(plan_resolver_module, "logger", _Recorder())
class UndeclaredPipeline(nn.Module):
def __init__(self):
super().__init__()
self.transformer = _SingleBlockModel(num_blocks=2)
resolve_offload_plan(UndeclaredPipeline(), _config({"dit"}))
resolve_offload_plan(UndeclaredPipeline(), _config({"dit"}))
scan_warnings = [message for message in warnings if "SupportsComponentDiscovery" in message]
assert len(scan_warnings) == 1
assert "deprecated" in scan_warnings[0]
class TestResolvedTopology:
def test_encoder_plan_resolves_one_stack_per_declared_path(self):
pipeline = _WanStylePipeline()
resolved = resolve_offload_plan(pipeline, _config({"dit", "text_encoder"}))
encoder = resolved.encoders[0]
assert encoder.selected and not encoder.on_demand
assert list(_stack_of(encoder).blocks) == list(pipeline.text_encoder.encoder.block)
assert [component.path for component in resolved.vaes] == ["vae"]
def test_staged_plan_resolves_every_declared_stack(self):
pipeline = _StagedComponentPipeline()
resolved = resolve_offload_plan(pipeline, _config({"text_encoder"}))
encoder = resolved.encoders[0]
assert encoder.on_demand
assert [len(stack.blocks) for stack in encoder.stacks] == [2, 2]
# An explicit selection never stages a VAE through the legacy plan.
assert not resolved.vaes[0].on_demand
# DiT is unselected, so it keeps no streaming topology.
assert resolved.dits[0].selected is False
assert resolved.dits[0].stacks == ()
def test_omitted_selector_keeps_the_legacy_staged_vae(self):
resolved = resolve_offload_plan(_StagedComponentPipeline(), _config())
assert resolved.vaes[0].on_demand
assert resolved.encoders[0].on_demand
assert resolved.dits[0].selected
def test_on_demand_only_plan_resolves_without_stacks(self):
class Pipeline(nn.Module):
_dit_modules: ClassVar[list[str]] = ["transformer"]
_encoder_modules: ClassVar[list[str]] = ["text_encoder"]
_vae_modules: ClassVar[list[str]] = ["vae"]
_resident_modules: ClassVar[list[str]] = []
_offload_plan = OffloadPlan(on_demand_component_paths=frozenset({"text_encoder", "vae"}))
def __init__(self):
super().__init__()
self.transformer = _SingleBlockModel(num_blocks=2)
self.text_encoder = _StagedEncoder()
self.vae = _StagedVAE()
resolved = resolve_offload_plan(Pipeline(), _config({"text_encoder"}))
assert resolved.encoders[0].on_demand
assert resolved.encoders[0].stacks == ()
def test_module_strategy_resolves_selection_without_stacks(self):
resolved = resolve_offload_plan(
_WanStylePipeline(),
_config({"dit", "text_encoder"}, strategy=OffloadStrategy.MODEL_LEVEL),
)
assert [component.path for component in resolved.components] == ["transformer", "text_encoder", "vae"]
assert all(component.stacks == () for component in resolved.components)
assert resolved.encoders[0].selected
def test_module_strategy_leaves_staged_residency_to_the_pipeline(self):
resolved = resolve_offload_plan(
_StagedComponentPipeline(),
_config({"dit", "text_encoder"}, strategy=OffloadStrategy.MODEL_LEVEL),
)
assert not any(component.on_demand for component in resolved.components)
class TestResidentLayers:
def _dlo_config(self, resident_layers: int, components: set[str] | None = None) -> OffloadConfig:
return _config(
components,
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
dlo_transfers={"dit": "rank-local", "text_encoder": "rank-local"},
dlo_resident_layers=resident_layers,
)
def test_resident_head_splits_the_declared_dit(self):
pipeline = _StagedComponentPipeline()
resolved = resolve_offload_plan(pipeline, self._dlo_config(2, {"dit"}))
stack = _stack_of(resolved.dits[0])
assert stack.resident_head == 2
assert list(stack.resident) == list(pipeline.transformer.blocks)[:2]
assert list(stack.streaming) == list(pipeline.transformer.blocks)[2:]
def test_resident_count_beyond_the_stack_keeps_every_block(self):
resolved = resolve_offload_plan(_StagedComponentPipeline(), self._dlo_config(9, {"dit"}))
stack = _stack_of(resolved.dits[0])
assert stack.resident_head == 4
assert stack.streaming == ()
def test_resident_head_leaving_one_streaming_block_is_rejected(self):
with pytest.raises(ValueError, match="leaves only one streaming block"):
resolve_offload_plan(_StagedComponentPipeline(), self._dlo_config(3, {"dit"}))
def test_legacy_single_streaming_block_stays_resident(self):
resolved = resolve_offload_plan(_StagedComponentPipeline(), self._dlo_config(3))
stack = _stack_of(resolved.dits[0])
assert stack.resident_head == 4
assert stack.streaming == ()
def test_resident_layers_without_a_declared_path_is_rejected(self):
with pytest.raises(ValueError, match="no matching resident_dit_paths"):
resolve_offload_plan(_WanStylePipeline(), self._dlo_config(2, {"dit"}))
class TestValidation:
def test_selected_dit_requires_a_discovered_module(self):
pipeline = nn.Module()
pipeline.text_encoder = _PlainEncoder()
with pytest.raises(ValueError, match="No DiT/transformer modules found"):
resolve_offload_plan(pipeline, _config({"dit"}))
def test_model_level_selection_requires_both_swap_sides(self):
pipeline = nn.Module()
pipeline.transformer = _SingleBlockModel(num_blocks=2)
with pytest.raises(ValueError, match="requires an encoder execution stage"):
resolve_offload_plan(pipeline, _config({"dit"}, strategy=OffloadStrategy.MODEL_LEVEL))
def test_selected_text_encoder_requires_a_matching_module(self):
pipeline = nn.Module()
pipeline.transformer = _SingleBlockModel(num_blocks=2)
with pytest.raises(ValueError, match="No text encoder modules found"):
resolve_offload_plan(pipeline, _config({"text_encoder"}))
def test_selected_encoder_requires_a_streamable_or_staged_plan(self):
pipeline = nn.Module()
pipeline.transformer = _SingleBlockModel(num_blocks=2)
pipeline.text_encoder = _PlainEncoder()
with pytest.raises(ValueError, match="Selected text encoder 'text_encoder' requires"):
resolve_offload_plan(pipeline, _config({"text_encoder"}))
def test_staged_component_requires_the_explicit_lifecycle(self):
class Pipeline(nn.Module):
_offload_plan = OffloadPlan(on_demand_component_paths=frozenset({"text_encoder"}))
def __init__(self):
super().__init__()
self.transformer = _SingleBlockModel(num_blocks=2)
self.text_encoder = _PlainEncoder()
with pytest.raises(ValueError, match="must implement load_to_device"):
resolve_offload_plan(Pipeline(), _config({"text_encoder"}))
def test_single_block_dit_is_rejected_for_layer_offload(self):
pipeline = nn.Module()
pipeline.transformer = _SingleBlockModel(num_blocks=1)
with pytest.raises(ValueError, match="requires at least two streamable"):
resolve_offload_plan(pipeline, _config({"dit"}))
def test_encoder_allgather_requires_declared_replication(self):
config = _config(
{"text_encoder"},
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
dp_size=2,
dlo_transfers={"dit": "rank-local", "text_encoder": "allgather"},
)
with pytest.raises(ValueError, match="not declared replicated"):
resolve_offload_plan(_StagedComponentPipeline(), config)
def test_two_dits_cannot_own_the_same_blocks(self):
class Pipeline(nn.Module):
_dit_modules: ClassVar[list[str]] = ["transformer", "transformer_2"]
_encoder_modules: ClassVar[list[str]] = []
_vae_modules: ClassVar[list[str]] = []
_resident_modules: ClassVar[list[str]] = []
def __init__(self):
super().__init__()
self.transformer = _SingleBlockModel(num_blocks=2)
self.transformer_2 = _SingleBlockModel(num_blocks=2)
self.transformer_2.blocks = self.transformer.blocks
with pytest.raises(ValueError, match="claimed by both"):
resolve_offload_plan(Pipeline(), _config({"dit"}))
def test_legacy_selection_keeps_unstreamable_components(self):
pipeline = nn.Module()
pipeline.transformer = _SingleBlockModel(num_blocks=1)
pipeline.text_encoder = _PlainEncoder()
resolved = resolve_offload_plan(pipeline, _config())
assert resolved.dits[0].stacks == ()
assert resolved.encoders[0].stacks == ()
class TestPurity:
def _snapshot(self, pipeline: nn.Module):
return {
"devices": [(name, tensor.device) for name, tensor in pipeline.named_parameters()],
"attrs": {name: sorted(vars(module)) for name, module in pipeline.named_modules()},
}
def test_resolution_does_not_touch_the_model(self):
pipeline = _StagedComponentPipeline()
before = self._snapshot(pipeline)
resolve_offload_plan(pipeline, _config({"dit", "text_encoder"}))
assert self._snapshot(pipeline) == before
assert pipeline.text_encoder.offload_calls == 0
assert pipeline.text_encoder.to_calls == 0
assert pipeline.vae.to_calls == 0
def test_resolution_is_repeatable(self):
pipeline = _WanStylePipeline()
config = _config({"dit", "text_encoder"})
first = resolve_offload_plan(pipeline, config)
second = resolve_offload_plan(pipeline, config)
assert [component.path for component in first.components] == [component.path for component in second.components]
for left, right in zip(first.components, second.components, strict=True):
assert left.selected == right.selected
assert left.on_demand == right.on_demand
assert [list(stack.blocks) for stack in left.stacks] == [list(stack.blocks) for stack in right.stacks]
def test_backend_enable_rejects_before_touching_earlier_components(self, patched_offload_runtime):
class Pipeline(nn.Module):
_dit_modules: ClassVar[list[str]] = ["transformer"]
_encoder_modules: ClassVar[list[str]] = ["text_encoder", "text_encoder_2"]
_vae_modules: ClassVar[list[str]] = ["vae"]
_resident_modules: ClassVar[list[str]] = []
_offload_plan = OffloadPlan(
encoder_component_types={
"text_encoder": "text_encoder",
"text_encoder_2": "text_encoder",
},
encoder_block_attrs={"text_encoder": ("encoder.block",)},
)
def __init__(self):
super().__init__()
self.transformer = _SingleBlockModel(num_blocks=2)
self.text_encoder = _PlainEncoder()
self.text_encoder_2 = _PlainEncoder()
self.vae = _StagedVAE()
pipeline = Pipeline()
backend = LayerWiseOffloadBackend(_config({"text_encoder"}), CPU)
with pytest.raises(ValueError, match="Selected text encoder 'text_encoder_2' requires"):
backend.enable(pipeline)
assert not backend.enabled
assert not getattr(pipeline.text_encoder, "_omni_layerwise_enabled", False)
for block in pipeline.text_encoder.encoder.block:
assert getattr(block, "_hook_registry", None) is None
assert pipeline.vae.to_calls == 0