# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """Unit tests for LayerwiseOffloadHook and LayerWiseOffloadBackend utilities.""" import gc from contextlib import contextmanager import pytest import torch import torch.distributed as dist from torch import nn from torch.distributed.tensor import DeviceMesh, DTensor, Replicate import vllm_omni.diffusion.offloader.layerwise_backend as layerwise_backend_module from tests.helpers.runtime import get_distributed_init_method from vllm_omni.diffusion.offloader.layerwise_backend import LayerWiseOffloadBackend, LayerwiseOffloadHook from vllm_omni.platforms import current_omni_platform pytestmark = [pytest.mark.diffusion, pytest.mark.cpu, pytest.mark.core_model] class DummyStream: def wait_stream(self, _stream) -> None: return None def wait_event(self, _event) -> None: return None class DummyEvent: def record(self, _stream) -> None: return None @contextmanager def dummy_stream(_stream): yield None def _cleanup_distributed() -> None: if dist.is_initialized(): dist.destroy_process_group() gc.collect() if current_omni_platform.is_available(): current_omni_platform.empty_cache() current_omni_platform.synchronize() @pytest.fixture(scope="module") def dist_group(): dist.init_process_group("gloo", rank=0, world_size=1, init_method=get_distributed_init_method()) try: yield finally: _cleanup_distributed() @pytest.fixture def patched_offload_runtime(monkeypatch): monkeypatch.setattr(layerwise_backend_module.current_omni_platform, "Stream", DummyStream) monkeypatch.setattr(layerwise_backend_module.current_omni_platform, "Event", DummyEvent) monkeypatch.setattr(layerwise_backend_module.current_omni_platform, "current_stream", lambda: DummyStream()) monkeypatch.setattr(layerwise_backend_module.current_omni_platform, "stream", dummy_stream) class TinyBlock(nn.Module): def __init__(self, values: torch.Tensor): super().__init__() mesh = DeviceMesh("cpu", [0]) dtensor = DTensor.from_local(values, mesh, [Replicate()]) self.weight = nn.Parameter(dtensor) def _make_values(start: float) -> torch.Tensor: return torch.arange(start, start + 4, dtype=torch.float32) class TestLayerwiseOffloadHook: def test_dtensor_wrapper_is_preserved_across_prefetch_and_offload(self, dist_group, patched_offload_runtime): current_block = TinyBlock(_make_values(1.0)) next_block = TinyBlock(_make_values(10.0)) hook = LayerwiseOffloadHook( next_block=next_block, device=torch.device("cpu"), stream=DummyStream(), pin_memory=False, ) hook.initialize_hook(current_block) assert isinstance(next_block.weight, DTensor) assert next_block.weight.to_local().is_meta assert next_block.weight.to_local().shape == torch.Size([4]) assert hook.dtype_metadata[next_block.weight.dtype][0]["shape"] == torch.Size([4]) hook.prefetch_layer(non_blocking=False) assert isinstance(next_block.weight, DTensor) assert torch.equal(next_block.weight.to_local(), _make_values(10.0)) assert next_block.weight.to_local().shape == torch.Size([4]) hook.offload_layer() assert isinstance(current_block.weight, DTensor) assert current_block.weight.to_local().is_meta assert current_block.weight.to_local().shape == torch.Size([4]) assert not hook.is_materialized def test_prefetch_preserves_transposed_weight_stride(self, patched_offload_runtime): """Online-FP8 Cutlass weights must retain their transposed layout.""" class StridedBlock(nn.Module): def __init__(self, start: float): super().__init__() base = torch.arange(start, start + 12).reshape(3, 4) self.weight = nn.Parameter(base.t(), requires_grad=False) current_block = StridedBlock(1.0) next_block = StridedBlock(20.0) expected = next_block.weight.detach().clone() expected_stride = next_block.weight.stride() assert expected_stride == (1, 4) hook = LayerwiseOffloadHook( next_block=next_block, device=torch.device("cpu"), stream=DummyStream(), pin_memory=False, ) hook.initialize_hook(current_block) hook.prefetch_layer(non_blocking=False) assert next_block.weight.stride() == expected_stride assert torch.equal(next_block.weight, expected) class _DummyBlock(nn.Module): def __init__(self): super().__init__() self.weight = nn.Parameter(torch.randn(10, 10)) class _SingleBlockModel(nn.Module): _layerwise_offload_blocks_attrs = ["blocks"] def __init__(self, num_blocks: int = 3): super().__init__() self.blocks = nn.ModuleList([_DummyBlock() for _ in range(num_blocks)]) class _MultiBlockModel(nn.Module): _layerwise_offload_blocks_attrs = ["transformer_blocks", "single_transformer_blocks"] def __init__(self, num_transformer: int = 2, num_single: int = 2): super().__init__() self.transformer_blocks = nn.ModuleList([_DummyBlock() for _ in range(num_transformer)]) self.single_transformer_blocks = nn.ModuleList([_DummyBlock() for _ in range(num_single)]) class _EmptyBlocksModel(nn.Module): _layerwise_offload_blocks_attrs = ["blocks"] def __init__(self): super().__init__() self.blocks = nn.ModuleList([]) class _InvalidAttrModel(nn.Module): _layerwise_offload_blocks_attrs = ["nonexistent_blocks", "blocks"] def __init__(self, num_blocks: int = 2): super().__init__() self.blocks = nn.ModuleList([_DummyBlock() for _ in range(num_blocks)]) class _DeprecatedSingleAttrModel(nn.Module): _layerwise_offload_blocks_attr = "blocks" def __init__(self, num_blocks: int = 2): super().__init__() self.blocks = nn.ModuleList([_DummyBlock() for _ in range(num_blocks)]) class _NoAttrsModel(nn.Module): def __init__(self, num_blocks: int = 2): super().__init__() self.blocks = nn.ModuleList([_DummyBlock() for _ in range(num_blocks)]) class TestGetBlocksFromDit: def test_get_blocks_from_dit_single_block_attr(self): model = _SingleBlockModel(num_blocks=3) attr_names, blocks = LayerWiseOffloadBackend.get_blocks_from_dit(model) assert attr_names == ["blocks"] assert len(blocks) == 3 assert all(isinstance(b, _DummyBlock) for b in blocks) def test_get_blocks_from_dit_multi_block_attrs(self): model = _MultiBlockModel(num_transformer=2, num_single=3) attr_names, blocks = LayerWiseOffloadBackend.get_blocks_from_dit(model) assert set(attr_names) == {"transformer_blocks", "single_transformer_blocks"} assert len(blocks) == 5 assert all(isinstance(b, _DummyBlock) for b in blocks) def test_get_blocks_from_dit_empty_blocks(self): model = _EmptyBlocksModel() attr_names, blocks = LayerWiseOffloadBackend.get_blocks_from_dit(model) assert attr_names == [] assert blocks == [] def test_get_blocks_from_dit_invalid_attr_name(self): model = _InvalidAttrModel(num_blocks=2) with pytest.raises( AttributeError, match="Attribute 'nonexistent_blocks' declared in _layerwise_offload_blocks_attrs does not exist", ): LayerWiseOffloadBackend.get_blocks_from_dit(model) def test_get_blocks_from_dit_no_attrs_defined(self): model = _NoAttrsModel(num_blocks=3) attr_names, blocks = LayerWiseOffloadBackend.get_blocks_from_dit(model) assert attr_names == [] assert blocks == [] def test_get_blocks_from_dit_deprecated_single_attr(self): model = _DeprecatedSingleAttrModel(num_blocks=2) attr_names, blocks = LayerWiseOffloadBackend.get_blocks_from_dit(model) assert attr_names == ["blocks"] assert len(blocks) == 2 class TestGetBlocksAttrNames: def test_get_blocks_attr_names_new_format(self): model = _MultiBlockModel() attrs = LayerWiseOffloadBackend.get_blocks_attr_names(model) assert attrs == ["transformer_blocks", "single_transformer_blocks"] def test_get_blocks_attr_names_no_attrs(self): model = _NoAttrsModel() attrs = LayerWiseOffloadBackend.get_blocks_attr_names(model) assert attrs == [] def test_set_blocks_attr_names(self): model = _NoAttrsModel() LayerWiseOffloadBackend.set_blocks_attr_names(model, ["new_blocks"]) assert hasattr(model.__class__, "_layerwise_offload_blocks_attrs") assert model.__class__._layerwise_offload_blocks_attrs == ["new_blocks"]