[Refactor] Let diffusion pipelines declare offloadable modules via SupportsModuleOffload (#2427)

Signed-off-by: Nick Cao <ncao@redhat.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Nick CaoandClaude authored and GitHub committed 2026-04-23 04:23:00 +08:00
1 parent ead87aa521
commit e3b0afbb97
11 files changed
+491 -62

No files matched your search

+36 -4
View File
@@ -282,10 +282,11 @@ For Omni or custom models, create:
Required updates:
1. `docs/user_guide/diffusion/parallelism_acceleration.md` — parallelism support table
2. `docs/user_guide/diffusion/teacache.md` — if TeaCache supported
3. `docs/user_guide/diffusion/cache_dit_acceleration.md` — if Cache-DiT supported
4. `examples/offline_inference/xxx/README.md` — offline example docs
5. `examples/online_serve/xxx/README.md` — online serve docs
2. `docs/user_guide/diffusion/cpu_offload_diffusion.md` — if CPU offload supported (add to supported models table)
3. `docs/user_guide/diffusion/teacache.md` — if TeaCache supported
4. `docs/user_guide/diffusion/cache_dit_acceleration.md` — if Cache-DiT supported
5. `examples/offline_inference/xxx/README.md` — offline example docs
6. `examples/online_serve/xxx/README.md` — online serve docs
### Step 8: Add E2E Tests (Recommended)
@@ -512,6 +513,37 @@ After adding parallelism support, update:
1. `docs/user_guide/diffusion/parallelism_acceleration.md` — add your model to the support table
2. Record which parallelism methods are supported (USP, Ring, CFG, TP, HSDP, VAE-Patch)
### Step 11: Add CPU Offload Support
Implement `SupportsModuleOffload` on your pipeline class to enable
`--enable-cpu-offload` and `--enable-layerwise-offload`. The protocol
declares which submodules the offloader should manage:
```python
from typing import ClassVar
from vllm_omni.diffusion.models.interface import SupportsModuleOffload
class YourPipeline(nn.Module, SupportsModuleOffload):
_dit_modules: ClassVar[list[str]] = ["transformer"]
_encoder_modules: ClassVar[list[str]] = ["text_encoder"]
_vae_modules: ClassVar[list[str]] = ["vae"]
_resident_modules: ClassVar[list[str]] = [] # optional
```
- `_dit_modules`: denoising submodules (kept on GPU during diffusion loop)
- `_encoder_modules`: encoder/vision submodules (offloaded to CPU during diffusion loop)
- `_vae_modules`: VAE(s) (handled by both sequential and layerwise backends)
- `_resident_modules`: additional modules to pin on GPU during layerwise
offloading (e.g. embedders, connectors). Only used by the layerwise
backend. Optional — defaults to `[]`.
All attribute names support dotted paths for nested submodules
(e.g. `"pipe.transformer"`, `"bagel.time_embedder"`).
Pipelines without `SupportsModuleOffload` fall back to scanning
well-known attribute names (`transformer`, `text_encoder`, `vae`,
etc.), which fails for non-standard names.
---
## Iterative Development Tips
@@ -36,6 +36,45 @@ m = Omni(model="Wan-AI/Wan2.2-T2V-A14B-Diffusers", enable_cpu_offload=True)
vllm-omni serve diffusion Wan-AI/Wan2.2-T2V-A14B-Diffusers --enable-cpu-offload
```
### To Support a Model
Implement the `SupportsModuleOffload` protocol to declare which
submodules participate in offloading:
```python
from typing import ClassVar
from vllm_omni.diffusion.models.interface import SupportsModuleOffload
class MyPipeline(nn.Module, SupportsModuleOffload):
_dit_modules: ClassVar[list[str]] = ["transformer"]
_encoder_modules: ClassVar[list[str]] = ["text_encoder", "vision_model"]
_vae_modules: ClassVar[list[str]] = ["vae"]
_resident_modules: ClassVar[list[str]] = [] # optional
def __init__(self):
super().__init__()
self.transformer = ... # DiT — stays on GPU during denoising
self.text_encoder = ... # Encoder — offloaded to CPU during denoising
self.vision_model = ... # Encoder — offloaded to CPU during denoising
self.vae = ... # VAE — always on GPU
```
- `_dit_modules`: attribute names of denoising submodules (kept on GPU
during the diffusion loop).
- `_encoder_modules`: attribute names of encoder/vision submodules
(offloaded to CPU during the diffusion loop).
- `_vae_modules`: attribute names of VAE(s) (always kept on GPU, not
part of the mutual exclusion hooks).
- `_resident_modules`: attribute names of small submodules that must
stay on GPU during layerwise offloading (e.g. embedders, connectors).
Optional — defaults to `[]`.
All attribute names support dotted paths for nested submodules
(e.g. `"pipe.transformer"`, `"bagel.time_embedder"`).
Both DiT and encoder lists are needed because the offload hooks use
mutual exclusion: when one group runs, the other moves to CPU.
### Limitations
- Cold start latency increases
- Adds overhead from CPU-GPU transfers between encoder and denoising phases
@@ -116,11 +155,19 @@ class Flux2Transformer2DModel(nn.Module):
**Module Discovery**
The offloader automatically discovers pipeline components:
The offloader discovers pipeline components in two ways:
- **DiT modules**: `transformer`, `transformer_2`, `dit`
- **Encoders**: `text_encoder`, `text_encoder_2`, `text_encoder_3`, `image_encoder`
- **VAE**: `vae`
1. **Protocol-based** (preferred): If the pipeline implements
`SupportsModuleOffload`, its `_dit_modules`, `_encoder_modules`,
`_vae_modules`, and `_resident_modules` class variables are used
directly. All attribute names support dotted paths (e.g.
`"pipe.transformer"`, `"bagel.time_embedder"`) for nested submodules.
2. **Fallback attribute scan**: Otherwise, the offloader scans for
well-known attribute names:
- **DiT modules**: `transformer`, `transformer_2`, `dit`, `sr_dit`, `language_model`, `transformer_blocks`, `model`
- **Encoders**: `text_encoder`, `text_encoder_2`, `text_encoder_3`, `image_encoder`
- **VAE**: `vae`, `audio_vae`
**Hook System**
@@ -0,0 +1,240 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for ModuleDiscovery and SupportsModuleOffload."""
from typing import ClassVar
import pytest
from torch import nn
from vllm_omni.diffusion.models.interface import SupportsModuleOffload
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, SupportsModuleOffload):
"""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, SupportsModuleOffload):
"""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, SupportsModuleOffload):
"""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, SupportsModuleOffload):
"""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, SupportsModuleOffload):
"""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, SupportsModuleOffload):
"""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 SupportsModuleOffload)."""
def test_discovers_standard_attrs(self):
pipeline = FallbackPipeline()
result = ModuleDiscovery.discover(pipeline)
assert not isinstance(pipeline, SupportsModuleOffload)
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 SupportsModuleOffload protocol."""
def test_discovers_declared_attrs_and_ignores_undeclared(self):
pipeline = ProtocolPipeline()
result = ModuleDiscovery.discover(pipeline)
assert isinstance(pipeline, SupportsModuleOffload)
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
@@ -12,6 +12,7 @@ from collections.abc import Iterable
from copy import deepcopy
from dataclasses import dataclass
from math import isqrt
from typing import ClassVar
import numpy as np
import torch
@@ -26,6 +27,7 @@ from vllm.transformers_utils.configs.bagel import BagelConfig
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.interface import SupportsModuleOffload
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific
@@ -148,12 +150,25 @@ class SiglipNaViTWrapper(nn.Module):
return outputs.last_hidden_state.squeeze(0)
class BagelPipeline(nn.Module, DiffusionPipelineProfilerMixin):
class BagelPipeline(nn.Module, SupportsModuleOffload, DiffusionPipelineProfilerMixin):
"""Bagel generation pipeline (MoT) packaged for vllm-omni diffusion engine.
This pipeline is self-contained and uses the ported Bagel core files.
"""
_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",
"bagel.llm2vae",
"bagel.latent_pos_embed",
"bagel.vit_model",
"bagel.connector",
"bagel.vit_pos_embed",
]
def __init__(self, *, od_config: OmniDiffusionConfig, prefix: str = ""):
super().__init__()
self.od_config = od_config
+21
View File
@@ -58,6 +58,27 @@ class SupportsStepExecution(Protocol):
"""Decode output after denoise loop."""
@runtime_checkable
class SupportsModuleOffload(Protocol):
"""Declares which submodules participate in CPU offload.
All attribute names support dotted paths for nested submodules
(e.g. ``"pipe.transformer"``).
Attributes:
_dit_modules: Denoising submodules (on GPU during diffusion).
_encoder_modules: Encoder submodules (offloaded during diffusion).
_vae_modules: VAE(s) (always on GPU).
_resident_modules: Extra modules pinned on GPU during layerwise
offloading. Optional, defaults to ``[]``.
"""
_dit_modules: ClassVar[list[str]]
_encoder_modules: ClassVar[list[str]]
_vae_modules: ClassVar[list[str]]
_resident_modules: ClassVar[list[str]] = []
def supports_step_execution(pipeline: object) -> bool:
"""Return whether `pipeline` implements :class:`SupportsStepExecution`."""
@@ -9,7 +9,7 @@ import json
import os
from collections.abc import Iterable
from contextlib import nullcontext
from typing import Any
from typing import Any, ClassVar
import numpy as np
import torch
@@ -34,6 +34,7 @@ from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.lora.manager import DiffusionLoRAManager
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.dmd2 import DMD2PipelineMixin
from vllm_omni.diffusion.models.interface import SupportsModuleOffload
from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.lora.request import LoRARequest
@@ -1152,9 +1153,13 @@ class LTX2Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin):
return loader.load_weights(weights)
class LTX2TwoStagesPipeline(nn.Module):
class LTX2TwoStagesPipeline(nn.Module, SupportsModuleOffload):
"""LTX2TwoStagesPipeline is for two stages image to video generation"""
_dit_modules: ClassVar[list[str]] = ["pipe.transformer"]
_encoder_modules: ClassVar[list[str]] = ["pipe.text_encoder"]
_vae_modules: ClassVar[list[str]] = ["pipe.vae", "pipe.audio_vae"]
def __init__(
self,
*,
@@ -6,7 +6,7 @@ from __future__ import annotations
import copy
import os
from collections.abc import Iterable
from typing import Any
from typing import Any, ClassVar
import numpy as np
import PIL.Image
@@ -26,6 +26,7 @@ from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.lora.manager import DiffusionLoRAManager
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.dmd2 import DMD2PipelineMixin
from vllm_omni.diffusion.models.interface import SupportsModuleOffload
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.lora.request import LoRARequest
@@ -733,11 +734,15 @@ class LTX2ImageToVideoPipeline(LTX2Pipeline):
return DiffusionOutput(output=(video, audio))
class LTX2ImageToVideoTwoStagesPipeline(nn.Module):
class LTX2ImageToVideoTwoStagesPipeline(nn.Module, SupportsModuleOffload):
"""LTXImageToVideoTwoStagesPipeline is for two stages image to video generation"""
support_image_input = True
_dit_modules: ClassVar[list[str]] = ["pipe.transformer"]
_encoder_modules: ClassVar[list[str]] = ["pipe.text_encoder"]
_vae_modules: ClassVar[list[str]] = ["pipe.vae", "pipe.audio_vae"]
def __init__(
self,
*,
@@ -8,7 +8,7 @@ import os
import warnings
from collections.abc import Iterable
from dataclasses import dataclass
from typing import Any
from typing import Any, ClassVar
import numpy as np
import PIL.Image
@@ -32,6 +32,7 @@ from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.interface import SupportsModuleOffload
from vllm_omni.diffusion.models.omnigen2.omnigen2_transformer import (
OmniGen2RotaryPosEmbed,
OmniGen2Transformer2DModel,
@@ -620,7 +621,7 @@ def retrieve_timesteps(
return timesteps, num_inference_steps
class OmniGen2Pipeline(CFGParallelMixin, nn.Module):
class OmniGen2Pipeline(CFGParallelMixin, nn.Module, SupportsModuleOffload):
"""
Pipeline for text-to-image generation using OmniGen2.
@@ -634,6 +635,10 @@ class OmniGen2Pipeline(CFGParallelMixin, nn.Module):
od_config (OmniDiffusionConfig): The OmniDiffusion configuration.
"""
_dit_modules: ClassVar[list[str]] = ["transformer"]
_encoder_modules: ClassVar[list[str]] = ["mllm"]
_vae_modules: ClassVar[list[str]] = ["vae"]
def __init__(
self,
*,
@@ -298,13 +298,20 @@ class LayerWiseOffloadBackend(OffloadBackend):
for enc in modules.encoders:
enc.to(self.device)
# Move VAE to GPU if available
if modules.vae is not None:
# Move VAE(s) to GPU if available
for vae in modules.vaes:
try:
modules.vae.to(self.device, non_blocking=True)
vae.to(self.device, non_blocking=True)
except Exception as exc:
logger.debug("Failed to move VAE to GPU: %s", exc)
# Move resident modules to GPU (small modules needed every forward)
for name, module in zip(modules.resident_names, modules.resident_modules):
try:
module.to(self.device)
except Exception as exc:
logger.debug("Failed to move resident module %s to GPU: %s", name, exc)
logger.info("Applying layer-wise offloading on %s", modules.dit_names)
# Apply block-wise offloading hook for each of the blocks in DiT model(s)
@@ -1,11 +1,14 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from dataclasses import dataclass
from dataclasses import dataclass, field
from operator import attrgetter
from torch import nn
from vllm.logger import init_logger
from vllm_omni.diffusion.models.interface import SupportsModuleOffload
logger = init_logger(__name__)
@@ -15,15 +18,81 @@ class PipelineModules:
dit_names: list[str]
encoders: list[nn.Module]
encoder_names: list[str]
vae: nn.Module | None = None
vaes: list[nn.Module]
resident_modules: list[nn.Module] = field(default_factory=list)
resident_names: list[str] = field(default_factory=list)
class ModuleDiscovery:
"""Discovers pipeline components for offloading"""
"""Discovers pipeline components for offloading.
DIT_ATTRS = ["transformer", "transformer_2", "dit", "sr_dit", "language_model", "transformer_blocks", "model"]
ENCODER_ATTRS = ["text_encoder", "text_encoder_2", "text_encoder_3", "image_encoder"]
VAE_ATTRS = ["vae", "audio_vae"]
If the pipeline implements :class:`SupportsModuleOffload`,
its ``_dit_modules``, ``_encoder_modules``, and ``_vae_modules``
class variables are used directly. Otherwise, falls back to
scanning well-known attribute names.
"""
# Fallback attribute names for pipelines that do not implement
# SupportsModuleOffload.
_FALLBACK_DIT_ATTRS = [
"transformer",
"transformer_2",
"dit",
"sr_dit",
"language_model",
"transformer_blocks",
"model",
]
_FALLBACK_ENCODER_ATTRS = [
"text_encoder",
"text_encoder_2",
"text_encoder_3",
"image_encoder",
]
_FALLBACK_VAE_ATTRS = [
"vae",
"audio_vae",
]
@staticmethod
def _collect_modules(
pipeline: nn.Module,
attr_names: list[str],
*,
warn_missing: bool = False,
) -> tuple[list[nn.Module], list[str]]:
"""Resolve attribute names to (module, name) pairs, skipping missing.
Supports dotted paths via :func:`operator.attrgetter`.
Warns on missing attributes when *warn_missing* is True.
"""
modules: list[nn.Module] = []
names: list[str] = []
seen: set[int] = set()
for attr in attr_names:
try:
module = attrgetter(attr)(pipeline)
except AttributeError:
module = None
if module is None:
if warn_missing:
logger.warning(
"Pipeline declares '%s' as offloadable but the attribute does not exist or is None",
attr,
)
continue
if not isinstance(module, nn.Module):
logger.warning(
"Expected '%s' to be nn.Module, got %r",
attr,
type(module),
)
continue
if id(module) not in seen:
seen.add(id(module))
modules.append(module)
names.append(attr)
return modules, names
@staticmethod
def discover(pipeline: nn.Module) -> PipelineModules:
@@ -35,46 +104,29 @@ class ModuleDiscovery:
Returns:
PipelineModules with lists of discovered modules and names
"""
# Collect DiT/transformer modules
dit_modules: list[nn.Module] = []
dit_names: list[str] = []
for attr in ModuleDiscovery.DIT_ATTRS:
if not hasattr(pipeline, attr):
continue
module_obj = getattr(pipeline, attr)
if module_obj is None:
continue
declared = isinstance(pipeline, SupportsModuleOffload)
if declared:
dit_attrs = pipeline._dit_modules
enc_attrs = pipeline._encoder_modules
vae_attrs = pipeline._vae_modules
res_attrs = pipeline._resident_modules
else:
dit_attrs = ModuleDiscovery._FALLBACK_DIT_ATTRS
enc_attrs = ModuleDiscovery._FALLBACK_ENCODER_ATTRS
vae_attrs = ModuleDiscovery._FALLBACK_VAE_ATTRS
res_attrs = []
if not isinstance(module_obj, nn.Module):
logger.warning(f"Expected {attr} to be nn.Module, got {type(module_obj)!r}")
continue
if module_obj in dit_modules:
continue
dit_modules.append(module_obj)
dit_names.append(attr)
# Collect all encoders
encoders: list[nn.Module] = []
encoder_names: list[str] = []
for attr in ModuleDiscovery.ENCODER_ATTRS:
if hasattr(pipeline, attr) and getattr(pipeline, attr) is not None:
encoders.append(getattr(pipeline, attr))
encoder_names.append(attr)
# Collect VAE
vae = None
for attr in ModuleDiscovery.VAE_ATTRS:
module = getattr(pipeline, attr, None)
if module is not None:
vae = module
break
dit_modules, dit_names = ModuleDiscovery._collect_modules(pipeline, dit_attrs, warn_missing=declared)
encoders, encoder_names = ModuleDiscovery._collect_modules(pipeline, enc_attrs, warn_missing=declared)
vaes, _ = ModuleDiscovery._collect_modules(pipeline, vae_attrs, warn_missing=declared)
residents, resident_names = ModuleDiscovery._collect_modules(pipeline, res_attrs, warn_missing=declared)
return PipelineModules(
dits=dit_modules,
dit_names=dit_names,
encoders=encoders,
encoder_names=encoder_names,
vae=vae,
vaes=vaes,
resident_modules=residents,
resident_names=resident_names,
)
@@ -210,10 +210,10 @@ class ModelLevelOffloadBackend(OffloadBackend):
for enc in modules.encoders:
enc.to(self.device)
# Move VAE to GPU if available
if modules.vae is not None:
# Move VAE(s) to GPU if available
for vae in modules.vaes:
try:
modules.vae.to(self.device, non_blocking=True)
vae.to(self.device, non_blocking=True)
except Exception as exc:
logger.debug("Failed to move VAE to GPU: %s", exc)