2026-02-12 17:29:55 +08:00
# CPU Offloading for Diffusion Models
2026-01-12 16:11:18 +05:30
## Overview
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
vLLM-Omni provides two offloading strategies to reduce GPU memory usage for diffusion models:
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
1. **Model-level (Sequential) Offloading** : Mutual exclusion between DiT model and encoder - only one is on GPU at a time.
2. **Layerwise (Blockwise) Offloading** : Keeps only one transformer block on GPU at a time with compute-memory overlap.
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
Both strategies use pinned memory for faster CPU-GPU transfers. The strategies are **mutually exclusive** for now - if both are enabled, layerwise takes priority.
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
## Model-level (Sequential) Offloading
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
### How It Works
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
Model-level offloading implements mutual exclusion between DiT transformer and encoder modules using pre forward hooks:
2026-01-12 16:11:18 +05:30
2026-02-12 17:29:55 +08:00
- **When encoders run**: DiT transformer is offloaded to CPU
2026-05-05 18:48:46 -05:00
- **When DiT runs**: Encoders are offloaded to CPU, if more than one dit models, only one loaded on GPU, others get offloaded to CPU.
2026-02-12 17:29:55 +08:00
- **VAE**: Stays resident on GPU
2026-01-12 16:11:18 +05:30
2026-02-12 17:29:55 +08:00
Before each module's forward pass, the hook automatically moves it to GPU while offloading the other module group to CPU. Transfers use pinned memory for speed.
2026-01-12 16:11:18 +05:30
2026-02-12 17:29:55 +08:00
### Usage
2026-01-12 16:11:18 +05:30
2026-02-12 17:29:55 +08:00
**Python API:**
2026-01-12 16:11:18 +05:30
```python
from vllm_omni import Omni
2026-02-12 17:29:55 +08:00
m = Omni ( model = "Wan-AI/Wan2.2-T2V-A14B-Diffusers" , enable_cpu_offload = True )
2026-01-12 16:11:18 +05:30
```
2026-02-12 17:29:55 +08:00
**CLI:**
```bash
2026-07-13 23:59:42 +08:00
vllm serve Wan-AI/Wan2.2-T2V-A14B-Diffusers --omni --enable-cpu-offload
2026-02-12 17:29:55 +08:00
```
2026-01-12 16:11:18 +05:30
2026-04-22 16:23:00 -04:00
### To Support a Model
2026-05-08 10:16:49 -04:00
Implement the `SupportsComponentDiscovery` protocol to declare which
submodules serve as pipeline components (used by offloading, HSDP
sharding, and other framework features):
2026-04-22 16:23:00 -04:00
```python
from typing import ClassVar
2026-05-08 10:16:49 -04:00
from vllm_omni.diffusion.models.interface import SupportsComponentDiscovery
2026-04-22 16:23:00 -04:00
2026-05-08 10:16:49 -04:00
class MyPipeline ( nn . Module , SupportsComponentDiscovery ):
2026-04-22 16:23:00 -04:00
_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.
2026-01-30 17:35:09 +08:00
### Limitations
2026-02-12 17:29:55 +08:00
- Cold start latency increases
- Adds overhead from CPU-GPU transfers between encoder and denoising phases
- Support single GPU only for now
2026-01-30 17:35:09 +08:00
2026-07-13 11:48:53 +02:00
### Component offloading for split models (e.g. Cosmos3)
Some models split their transformer into mutually-exclusive *components* that run
in different phases of a single forward pass rather than as separate pipeline
components -- e.g. Cosmos3's understanding (reasoner) component runs once per
generation while the generation (generator) component runs every denoising step.
Such models have no separate text encoder to swap against, so the transformer
owns a small model-local offload path and wraps each phase with
`with self._offload_context(name):`
```python
class Cosmos3VFMTransformer ( nn . Module ):
def forward ( self , ... ):
with self . _offload_context ( "reasoner" ):
... # understanding pass, runs once
with self . _offload_context ( "generator" ):
... # denoising pass, runs every step
```
Model-level offloading then keeps exactly one component GPU-resident at a time
(the other on CPU), reusing the same `SequentialOffloadHook` `.to()` movers. The
pipeline opts in by exposing `enable_omni_model_cpu_offload` (which drives the
transformer's `enable_model_cpu_offload` and pins the VAE). Layerwise offloading
works for these models too -- each component declares its own block container via
`_layerwise_offload_blocks_attrs` .
2026-01-30 17:35:09 +08:00
## Layerwise (Blockwise) Offloading
2026-02-12 17:29:55 +08:00
### How It Works
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
Layerwise offloading keeps only one transformer block on GPU at a time.
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
As each block completes, the next block is prefetched to GPU while the current block is freed. The pre and forward hooks utilized by layerwise offloading apply a separate CUDA stream (`copy_stream` ) to overlap weight transfer with computation, and retain flattened tensors in pinned CPU memory for block parameters re-materialization. Encoders, VAE, and non-block DiT modules (embeddings, norms) always stay on GPU.
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
**Execution Flow:**
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
| Block | Pre-forward Hook | Forward | Post-forward Hook |
|-------|------------------|---------|-------------------|
| block-0 | Prefetch block-1 (async) | Compute block-0 | Free block-0 |
| block-1 | Prefetch block-2 (async) | Compute block-1 | Free block-1 |
| ... | ... | ... | ... |
| block-(n-1) | **Prefetch block-0** (async) | Compute block-(n-1) | Free block-(n-1) |
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
Each transformer block has a `LayerwiseOffloadHook` that prefetches the next block before forward and frees the current block after forward.
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
Layerwise offloading is primarily recommended for large **video generation models** where the compute cost per block is high enough to effectively overlap with memory prefetch operations. For example, Wan2.2 T2V and I2V pipelines.
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
### Usage
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
**Python API:**
2026-01-30 17:35:09 +08:00
```python
from vllm_omni import Omni
2026-02-12 17:29:55 +08:00
# Text-to-video
m = Omni ( model = "Wan-AI/Wan2.2-T2V-A14B-Diffusers" , enable_layerwise_offload = True )
# Or image-to-video
m = Omni ( model = "Wan-AI/Wan2.2-I2V-A14B-Diffusers" , enable_layerwise_offload = True )
2026-01-30 17:35:09 +08:00
```
2026-02-12 17:29:55 +08:00
**CLI:**
```bash
# Text-to-video
2026-07-13 23:59:42 +08:00
vllm serve Wan-AI/Wan2.2-T2V-A14B-Diffusers --omni --enable-layerwise-offload
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
# Or image-to-video
2026-07-13 23:59:42 +08:00
vllm serve Wan-AI/Wan2.2-I2V-A14B-Diffusers --omni --enable-layerwise-offload
2026-02-12 17:29:55 +08:00
```
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
### To Support a Model
2026-01-30 17:35:09 +08:00
2026-02-12 17:29:55 +08:00
Models must define the blocks attribute name for layerwise offloading:
```python
class WanTransformer3DModel ( nn . Module ):
2026-04-06 11:49:35 +08:00
_layerwise_offload_blocks_attrs = [ "blocks" ] # Attribute names containing transformer blocks
2026-02-12 17:29:55 +08:00
def __init__ ( self ):
self . blocks = nn . ModuleList ([ ... ]) # Transformer blocks
```
2026-01-30 17:35:09 +08:00
2026-04-06 11:49:35 +08:00
For models with multiple block types:
```python
class Flux2Transformer2DModel ( nn . Module ):
_layerwise_offload_blocks_attrs = [ "transformer_blocks" , "single_transformer_blocks" ]
```
2026-01-30 17:35:09 +08:00
### Limitations
2026-07-13 11:48:53 +02:00
- Cold start latency increases because offloaded components must be moved to CPU
during setup; layerwise offload may add extra weight consolidation and pinning
work.
2026-02-12 17:29:55 +08:00
- Performance depends on compute cost and H2D bandwidth as well
2026-01-30 17:35:09 +08:00
- Support single GPU only for now
2026-02-12 17:29:55 +08:00
2026-07-30 21:35:38 +08:00
## Distributed Layerwise Offloading
### How It Works
Distributed layerwise offloading extends single-GPU layerwise offloading to
multi-device deployments. Each DP rank stores only **1/dp_size** of the model
weights in host memory; full layer weights are reconstructed at runtime via
**AllGather** on a dedicated communication stream, overlapped with computation
via a fixed double-buffer scheme.
**Key features:**
- **Weight sharding + AllGather**: each rank stores 1/dp_size of weights,
reconstructed per-layer via `all_gather_into_tensor`
- **Fixed double-buffer**: exactly 2 transformer blocks on each device at any
time, regardless of model size
- **DP multi-concurrency**: N concurrent requests processed in parallel
(AllGather only gathers weight shards, which are request-independent)
- **mmap weight loading**: weights loaded as mmap views pointing to shared OS
page cache, eliminating O(dp_size × model_size) RSS during model creation
- **Platform-agnostic**: works on NVIDIA GPU (CUDA/NCCL) and Ascend NPU
(CANN/HCCL) via vLLM-Omni's platform abstraction
**Execution Flow (per device):**
```
Compute Stream: [Layer N] [Layer N+1] [Layer N+2]
H2D Stream: [H2D Shard N+1] [H2D Shard N+2]
AllGather: [AG N+1] [AG N+2]
Slot Usage: Slot0: Layer N Slot1: Layer N+1
```
### Usage
**CLI:**
```bash
# 4× GPU/NPU with AllGather (recommended)
vllm serve /path/to/model --omni \
--enable-distributed-layerwise-offload \
--data-parallel-size 4
# Without AllGather (each rank loads full weights, no sharding)
vllm serve /path/to/model --omni \
--enable-distributed-layerwise-offload \
--data-parallel-size 4 \
--dlo-no-use-allgather
# With SP instead of DP (long sequences)
vllm serve /path/to/model --omni \
--enable-distributed-layerwise-offload \
--usp 4
```
**Python API:**
```python
from vllm_omni import Omni
m = Omni (
model = "/path/to/model" ,
enable_distributed_layerwise_offload = True ,
dlo_use_allgather = True , # default
)
```
### CLI Flags
| Flag | Description | Default |
|------|-------------|---------|
| `--enable-distributed-layerwise-offload` | Enable DLO | `false` |
| `--data-parallel-size N` | Number of DP ranks for weight sharding | `1` |
| `--dlo-use-allgather` | Shard + AllGather (saves CPU, requires concurrent requests) | `true` |
| `--dlo-no-use-allgather` | Full weights per rank (no sharding, no AllGather) | `false` |
### How model weights are loaded (mmap path)
When DLO + AllGather is active, the offloader:
1. **Saves non-persistent buffers** (e.g. RoPE `inv_freq` , timestep `freqs` )
from the normally-created transformer
2. **Converts to meta device** via `to_empty(device="meta")` , releasing random
initialization weights
3. **Loads checkpoint weights as mmap views** via `safe_open().get_tensor()` ,
which return views into the OS page cache (shared across all ranks, 0 RSS)
4. **Calls `post_load_weights()`** to apply model-specific dtype conversions
(e.g. Cosmos3's `time_embedder` → FP32)
5. **Restores non-persistent buffers** from saved copies
This approach requires **zero model-specific code changes** — no pipeline or
transformer modifications are needed.
### OffloadPlan (declarative topology metadata)
Models can optionally declare an `OffloadPlan` class variable to provide
topology metadata (block attribute names, submodules to offload) without
any offload-specific logic:
```python
from vllm_omni.diffusion.offloader import OffloadPlan
class MyPipeline ( nn . Module ):
_dit_modules = [ "transformer" ]
_offload_plan = OffloadPlan (
block_attrs = { "transformer" : ( "blocks" ,)},
offload_submodules = { "context_encoder" : "layers" },
)
```
When not declared, the offloader falls back to `_layerwise_offload_blocks_attrs`
and heuristic attribute search. This is backward-compatible — existing models
work without any changes.
### DP Multi-concurrency
When `--data-parallel-size > 1` and AllGather is enabled, the scheduler batches
up to `dp_size` requests per denoise step. Each DP rank processes a different
request while AllGather synchronizes only weight shards (request-independent).
**Requirements for concurrent requests:**
- `num_inference_steps` must be specified explicitly (None is not allowed)
- All concurrent requests must have the same `num_inference_steps` value
- These constraints exist because AllGather is a collective that requires
every rank to participate at each denoise step
### Limitations
- Online quantization (FP8) is incompatible with mmap loading — falls back to
regular `load_weights()` automatically
- Tensor Parallel is not supported (DLO uses DP-based sharding)
- HSDP + AllGather is rejected (would double-shard weights)
- `num_inference_steps=None` is not allowed in DP multi-concurrency mode
2026-02-12 17:29:55 +08:00
**Module Discovery**
2026-04-22 16:23:00 -04:00
The offloader discovers pipeline components in two ways:
2026-02-12 17:29:55 +08:00
2026-04-22 16:23:00 -04:00
1. **Protocol-based** (preferred): If the pipeline implements
2026-05-08 10:16:49 -04:00
`SupportsComponentDiscovery` , its `_dit_modules` , `_encoder_modules` ,
2026-04-22 16:23:00 -04:00
`_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`
2026-02-12 17:29:55 +08:00
**Hook System**
Both strategies use vLLM-Omni's hook registry system (`HookRegistry` and `ModelHook` ) to register pre/post forward callbacks on modules, enabling automatic swapping without modifying model code.
**Backend Architecture**
```
OffloadBackend (base class)
2026-07-13 11:48:53 +02:00
├── ModelLevelOffloadBackend → uses SequentialOffloadHook (.to() swap)
│ (delegates to a pipeline's enable_omni_model_cpu_offload
│ for split models like Cosmos3)
2026-07-30 21:35:38 +08:00
├── LayerWiseOffloadBackend → uses LayerwiseOffloadHook
│ (single-GPU, full weights on host)
└── DistributedLayerwiseOffloadBackend → uses DistributedLayerwiseOffloadHook
(multi-GPU, 1/dp_size sharded weights + AllGather)
2026-02-12 17:29:55 +08:00
```
2026-07-13 11:48:53 +02:00
Factory function `get_offload_backend()` selects the appropriate backend based on
configuration.
For split models, `ModelLevelOffloadBackend.enable()` detects a pipeline's
`enable_omni_model_cpu_offload` hook and delegates to it; Cosmos3 then swaps its
reasoner/generator components inside the model forward pass.
2026-02-12 17:29:55 +08:00
## Supported Models
2026-07-30 21:35:38 +08:00
| Architecture | Example Models | DiT Class | Model-Level Offload | Layerwise Offload | Distributed Layerwise Offload | Blocks Attrs (Layerwise specific) |
|--------------|----------------|-----------|---------------------|-------------------|-------------------------------|-----------------------------------|
| Flux2Pipeline | `black-forest-labs/FLUX.2-dev` | `Flux2Transformer2DModel` | ✓ | ✓ | - | `"transformer_blocks"` , `"single_transformer_blocks"` |
| LongCatImagePipeline | `meituan-longcat/LongCat-Image` | `LongCatImageTransformer2DModel` | - | ✓ | - | `"transformer_blocks"` , `"single_transformer_blocks"` |
| NextStep11Pipeline | `stepfun-ai/NextStep-1.1` | `NextStepModel` | - | ✓ | - | `"layers"` |
| OvisImagePipeline | `AIDC-AI/Ovis-Image-7B` | `OvisImageTransformer2DModel` | - | ✓ | - | `"transformer"` |
| QwenImagePipeline | `Qwen/Qwen-Image` | `QwenImageTransformer2DModel` | ✓ | ✓ | - | `"transformer_blocks"` |
| StableDiffusionXLPipeline | `stabilityai/stable-diffusion-xl-base-1.0` | `SDXLUNet2DConditionModel` | ✓ | ✓ | - | `"down_blocks"` , `"up_blocks"` |
| StableDiffusion3Pipeline | `stabilityai/stable-diffusion-3.5-medium` | `SD3Transformer2DModel` | - | ✓ | - | `"transformer_blocks"` |
| Wan22I2VPipeline | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | - | `"blocks"` |
| Wan22Pipeline | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | - | `"blocks"` |
| SoulXSingerPipeline / SoulXSingerSVCPipeline | `Soul-AILab/SoulX-Singer` | `DiffLlama` (`cfm_decoder.model.diff_estimator` ) | ✓ | ✓ | - | `"layers"` |
| BagelPipeline | `ByteDance-Seed/BAGEL-7B-MoT` | `Qwen2MoTModel` | - | ✓ | - | `"layers"` , `"customized modules"` |
| Cosmos3OmniDiffusersPipeline | `nvidia/Cosmos3-Nano` , `nvidia/Cosmos3-Super` | `Cosmos3VFMTransformer` , `Cosmos3LanguageModel` | ✓ | ✓ | ✓ | `"layers"` , `"gen_layers"` |
2026-02-12 17:29:55 +08:00
**Notes:**
- Model-Level Offloading is expected to be supported by all common diffusion models (DiT and encoders) naturally
2026-04-06 11:49:35 +08:00
- Layerwise Offloading requires DiT class to define `_layerwise_offload_blocks_attrs` pointing to transformer blocks
2026-07-30 21:35:38 +08:00
- Distributed Layerwise Offloading works with any model that supports Layerwise Offloading — no additional model changes required. See [Cosmos3 DistOffload recipe ](https://github.com/vllm-project/vllm-omni/blob/main/recipes/cosmos3/Cosmos3-DistOffload.md ) for usage examples.