Extend Cosmos3 CPU offloading to component and layerwise paths (#4695)
This commit is contained in:
1 parent
98229b08ba
commit
66b9ad287a
11 files changed
+653
-160
No files matched your search
@@ -20,6 +20,16 @@ from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
||||
|
||||
|
||||
def test_pipeline_declares_layerwise_offload_components() -> None:
|
||||
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import Cosmos3OmniDiffusersPipeline
|
||||
|
||||
assert Cosmos3OmniDiffusersPipeline._dit_modules == ["transformer.language_model", "transformer"]
|
||||
assert Cosmos3OmniDiffusersPipeline._encoder_modules == []
|
||||
assert Cosmos3OmniDiffusersPipeline._vae_modules == ["vae"]
|
||||
assert Cosmos3OmniDiffusersPipeline._resident_modules == []
|
||||
assert hasattr(Cosmos3OmniDiffusersPipeline, "enable_omni_model_cpu_offload")
|
||||
|
||||
|
||||
class StubScheduler:
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -10,9 +10,19 @@ import pytest
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accelerator_device() -> torch.device:
|
||||
"""Provide an accelerator device, skipping when none is available."""
|
||||
if current_omni_platform.get_device_count() == 0:
|
||||
pytest.skip("Accelerator required for this test")
|
||||
return current_omni_platform.get_torch_device(0)
|
||||
|
||||
|
||||
def _tiny_cosmos3_config(**overrides):
|
||||
config = {
|
||||
"hidden_size": 8,
|
||||
@@ -88,7 +98,7 @@ def test_validate_supported_config_rejects_unsupported_flags(key: str, value) ->
|
||||
|
||||
|
||||
def test_transformer_sharding_offload_and_patch_round_trip_contracts() -> None:
|
||||
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3 import Cosmos3VFMTransformer
|
||||
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3 import Cosmos3LanguageModel, Cosmos3VFMTransformer
|
||||
|
||||
model = object.__new__(Cosmos3VFMTransformer)
|
||||
nn.Module.__init__(model)
|
||||
@@ -104,6 +114,7 @@ def test_transformer_sharding_offload_and_patch_round_trip_contracts() -> None:
|
||||
]
|
||||
assert matched == ["language_model.layers.0", "language_model.layers.1", "gen_layers.0"]
|
||||
assert Cosmos3VFMTransformer._layerwise_offload_blocks_attrs == ["gen_layers"]
|
||||
assert Cosmos3LanguageModel._layerwise_offload_blocks_attrs == ["layers"]
|
||||
assert Cosmos3VFMTransformer._repeated_blocks == ["Cosmos3GenDecoderLayer"]
|
||||
|
||||
model.latent_patch_size = 2
|
||||
@@ -131,6 +142,97 @@ def test_forward_returns_video_prediction(monkeypatch: pytest.MonkeyPatch) -> No
|
||||
assert tuple(output.shape) == (1, 2, 1, 2, 2)
|
||||
|
||||
|
||||
def test_model_cpu_offload_swaps_back_to_generator(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from vllm_omni.diffusion.models.cosmos3 import transformer_cosmos3
|
||||
|
||||
monkeypatch.setattr(transformer_cosmos3, "_get_ulysses_state", lambda: (1, 0, None))
|
||||
|
||||
class ToyReasonerLayer(nn.Module):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.weight = nn.Parameter(torch.ones(1))
|
||||
|
||||
def forward(self, hidden: torch.Tensor, freqs):
|
||||
del freqs
|
||||
kv = hidden.new_zeros(hidden.shape[0], hidden.shape[1], 1, 1)
|
||||
return hidden + self.weight.to(hidden.dtype), kv, kv
|
||||
|
||||
class ToyGeneratorLayer(nn.Module):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.weight = nn.Parameter(torch.ones(1))
|
||||
|
||||
def forward(self, hidden: torch.Tensor, **kwargs):
|
||||
del kwargs
|
||||
return hidden + self.weight.to(hidden.dtype)
|
||||
|
||||
model = transformer_cosmos3.Cosmos3VFMTransformer(
|
||||
SimpleNamespace(tf_model_config=_tiny_cosmos3_config(), dtype=torch.float32)
|
||||
)
|
||||
model.language_model.layers = nn.ModuleList([ToyReasonerLayer()])
|
||||
model.gen_layers = nn.ModuleList([ToyGeneratorLayer()])
|
||||
model.enable_model_cpu_offload(device=torch.device("cpu"), pin_memory=False)
|
||||
|
||||
assert model._model_cpu_offload_enabled
|
||||
assert set(model._model_cpu_offload_components()) == {"reasoner", "generator"}
|
||||
assert model.device == torch.device("cpu")
|
||||
|
||||
output = model(
|
||||
hidden_states=torch.zeros(1, 2, 1, 2, 2),
|
||||
timestep=torch.tensor([1.0]),
|
||||
text_ids=torch.tensor([[1, 2]], dtype=torch.long),
|
||||
text_mask=torch.ones(1, 2, dtype=torch.long),
|
||||
video_shape=(1, 2, 2),
|
||||
fps=24.0,
|
||||
)
|
||||
|
||||
assert tuple(output.shape) == (1, 2, 1, 2, 2)
|
||||
# The reasoner runs once then the generator component stays resident for GEN.
|
||||
assert model._active_model_cpu_offload_component == "generator"
|
||||
|
||||
model.disable_model_cpu_offload()
|
||||
assert not model._model_cpu_offload_enabled
|
||||
|
||||
|
||||
def test_model_cpu_offload_moves_reasoner_and_generator_between_cpu_and_device(
|
||||
monkeypatch: pytest.MonkeyPatch, accelerator_device: torch.device
|
||||
) -> None:
|
||||
from vllm_omni.diffusion.models.cosmos3 import transformer_cosmos3
|
||||
|
||||
monkeypatch.setattr(transformer_cosmos3, "_get_ulysses_state", lambda: (1, 0, None))
|
||||
|
||||
model = transformer_cosmos3.Cosmos3VFMTransformer(
|
||||
SimpleNamespace(tf_model_config=_tiny_cosmos3_config(), dtype=torch.float32)
|
||||
)
|
||||
model.language_model.layers = nn.ModuleList([nn.Linear(2, 2)])
|
||||
model.gen_layers = nn.ModuleList([nn.Linear(2, 2)])
|
||||
model.to(accelerator_device)
|
||||
|
||||
reasoner_param = model.language_model.layers[0].weight
|
||||
generator_param = model.gen_layers[0].weight
|
||||
|
||||
model.enable_model_cpu_offload(device=accelerator_device, pin_memory=False)
|
||||
|
||||
assert model._model_cpu_offload_enabled
|
||||
# On enable, every group is parked on CPU until a phase activates it.
|
||||
assert reasoner_param.device.type == "cpu"
|
||||
assert generator_param.device.type == "cpu"
|
||||
|
||||
model._activate_model_cpu_offload_component("reasoner")
|
||||
assert reasoner_param.device == accelerator_device
|
||||
assert generator_param.device.type == "cpu"
|
||||
|
||||
model._activate_model_cpu_offload_component("generator")
|
||||
assert reasoner_param.device.type == "cpu"
|
||||
assert generator_param.device == accelerator_device
|
||||
|
||||
model.disable_model_cpu_offload()
|
||||
assert not model._model_cpu_offload_enabled
|
||||
# Disable restores both components onto the device.
|
||||
assert reasoner_param.device == accelerator_device
|
||||
assert generator_param.device == accelerator_device
|
||||
|
||||
|
||||
def test_forward_accepts_transfer_control_latents(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from vllm_omni.diffusion.models.cosmos3 import transformer_cosmos3
|
||||
|
||||
|
||||
@@ -149,6 +149,40 @@ class MultiVaePipeline(nn.Module, SupportsComponentDiscovery):
|
||||
self.audio_vae = nn.Linear(10, 10)
|
||||
|
||||
|
||||
class SiblingDiTsPipeline(nn.Module, SupportsComponentDiscovery):
|
||||
"""Two independent DiTs (e.g. a MoE transformer pair)."""
|
||||
|
||||
_dit_modules: ClassVar[list[str]] = ["transformer", "transformer_2"]
|
||||
_encoder_modules: ClassVar[list[str]] = []
|
||||
_vae_modules: ClassVar[list[str]] = ["vae"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.transformer = nn.Linear(10, 10)
|
||||
self.transformer_2 = nn.Linear(10, 10)
|
||||
self.vae = nn.Linear(10, 10)
|
||||
|
||||
|
||||
class NestedDiTPipeline(nn.Module, SupportsComponentDiscovery):
|
||||
"""Cosmos3-like: a DiT plus one of its own submodules listed as a DiT.
|
||||
|
||||
The nested entry exists so offloading can treat the inner stack as an
|
||||
independent ring; the outer DiT's _hsdp_shard_conditions still own all
|
||||
nested blocks for sharding. Order is nested-first, matching Cosmos3.
|
||||
"""
|
||||
|
||||
_dit_modules: ClassVar[list[str]] = ["transformer.language_model", "transformer"]
|
||||
_encoder_modules: ClassVar[list[str]] = []
|
||||
_vae_modules: ClassVar[list[str]] = ["vae"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.transformer = nn.Module()
|
||||
self.transformer.language_model = nn.Linear(10, 10)
|
||||
self.transformer.gen_layers = nn.Linear(10, 10)
|
||||
self.vae = nn.Linear(10, 10)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -238,3 +272,35 @@ class TestProtocolDiscovery:
|
||||
assert len(result.vaes) == 2
|
||||
assert result.vaes[0] is pipeline.vae
|
||||
assert result.vaes[1] is pipeline.audio_vae
|
||||
|
||||
|
||||
class TestOutermostDits:
|
||||
"""PipelineModules.outermost_dits() drops DiTs nested inside another DiT.
|
||||
|
||||
HSDP shards each returned DiT via its own _hsdp_shard_conditions, so a DiT
|
||||
nested inside another discovered DiT must be excluded: it is already covered
|
||||
by its ancestor's conditions and may not declare conditions of its own.
|
||||
"""
|
||||
|
||||
def test_keeps_independent_sibling_dits(self):
|
||||
result = ModuleDiscovery.discover(SiblingDiTsPipeline())
|
||||
|
||||
names, modules = result.outermost_dits()
|
||||
|
||||
assert names == ["transformer", "transformer_2"]
|
||||
assert [id(m) for m in modules] == [id(m) for m in result.dits]
|
||||
|
||||
def test_drops_nested_dit_keeping_only_ancestor(self):
|
||||
pipeline = NestedDiTPipeline()
|
||||
result = ModuleDiscovery.discover(pipeline)
|
||||
|
||||
# Discovery sees both, nested-first (matches Cosmos3's _dit_modules order).
|
||||
assert result.dit_names == ["transformer.language_model", "transformer"]
|
||||
|
||||
names, modules = result.outermost_dits()
|
||||
|
||||
# Only the ancestor survives; sharding the nested condition-less stack
|
||||
# again would double-wrap its blocks and raise in apply_hsdp_to_model.
|
||||
assert names == ["transformer"]
|
||||
assert len(modules) == 1
|
||||
assert modules[0] is pipeline.transformer
|
||||
@@ -7,7 +7,8 @@ import pytest
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from vllm_omni.diffusion.offloader.sequential_backend import SequentialOffloadHook
|
||||
from vllm_omni.diffusion.offloader.base import OffloadConfig, OffloadStrategy
|
||||
from vllm_omni.diffusion.offloader.sequential_backend import ModelLevelOffloadBackend, SequentialOffloadHook
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
pytestmark = [pytest.mark.diffusion, pytest.mark.cpu, pytest.mark.core_model]
|
||||
@@ -41,6 +42,40 @@ def _track_pin_memory_calls():
|
||||
return tracker, mock
|
||||
|
||||
|
||||
def test_model_level_backend_delegates_to_custom_pipeline_offload() -> None:
|
||||
class CustomPipeline(nn.Module):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.enable_args = None
|
||||
self.disable_called = False
|
||||
|
||||
def enable_omni_model_cpu_offload(self, **kwargs) -> None:
|
||||
self.enable_args = kwargs
|
||||
|
||||
def disable_omni_model_cpu_offload(self) -> None:
|
||||
self.disable_called = True
|
||||
|
||||
pipeline = CustomPipeline()
|
||||
backend = ModelLevelOffloadBackend(
|
||||
OffloadConfig(strategy=OffloadStrategy.MODEL_LEVEL, pin_cpu_memory=False),
|
||||
torch.device("cpu"),
|
||||
)
|
||||
|
||||
backend.enable(pipeline)
|
||||
|
||||
assert backend.enabled is True
|
||||
assert pipeline.enable_args == {
|
||||
"device": torch.device("cpu"),
|
||||
"pin_memory": False,
|
||||
"use_hsdp": False,
|
||||
}
|
||||
|
||||
backend.disable()
|
||||
|
||||
assert backend.enabled is False
|
||||
assert pipeline.disable_called is True
|
||||
|
||||
|
||||
class TestMoveParamsPinMemory:
|
||||
def test_dtensor_skips_pin_memory(self, accelerator_device, monkeypatch: pytest.MonkeyPatch):
|
||||
"""DTensor should skip pin_memory to avoid RuntimeError."""
|
||||
|
||||
Reference in new issue
Block a user