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
2026-08-06 23:21:43 +08:00
multi-device deployments. In the default mode, each 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. The
`--dlo-no-use-allgather` mode instead streams standard-loader rank-local
2026-08-14 14:38:06 +08:00
weights independently and does not shard weights across ranks. For explicitly
mmap-compatible TP1 models, the same mode can retain node-shared file-backed
DiT weights and use bounded per-worker host staging instead of a private full
DiT copy.
2026-08-06 23:21:43 +08:00
For the implementation design and compatibility matrix, see
[Distributed Layerwise Offload ](../../design/feature/distributed_layerwise_offload.md ).
2026-07-30 21:35:38 +08:00
**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)
2026-08-14 14:38:06 +08:00
- **mmap weight storage**: compatible checkpoints use views into the shared OS
page cache. AllGather copies persistent rank shards; rank-local mode keeps
the views and packs complete blocks through two bounded host staging slots
2026-07-30 21:35:38 +08:00
- **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
2026-08-14 14:38:06 +08:00
# Without DLO AllGather (compatible TP1 models use node-shared mmap storage)
2026-07-30 21:35:38 +08:00
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` |
2026-08-06 23:21:43 +08:00
| `--data-parallel-size N` | Number of DP ranks; DLO weight-sharding group when AllGather is enabled | `1` |
2026-07-30 21:35:38 +08:00
| `--dlo-use-allgather` | Shard + AllGather (saves CPU, requires concurrent requests) | `true` |
2026-08-06 23:21:43 +08:00
| `--dlo-no-use-allgather` | Use standard-loader rank-local weights; no DLO sharding or AllGather | `false` |
2026-07-30 21:35:38 +08:00
### How model weights are loaded (mmap path)
2026-08-14 14:38:06 +08:00
When DLO and an mmap-compatible model are active, the offloader:
2026-07-30 21:35:38 +08:00
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()` ,
2026-08-14 14:38:06 +08:00
which return views into the OS page cache without eagerly copying the full
tensor into anonymous process memory
4. **Applies declared per-parameter layout adapters while packing** , when the
checkpoint and TP1 runtime layouts differ (for example MiniMax-H3 grouped
QKV)
2026-07-30 21:35:38 +08:00
5. **Restores non-persistent buffers** from saved copies
2026-08-14 14:38:06 +08:00
With AllGather, each rank copies only its persistent shard and then releases
the source mappings. Without AllGather, mappings remain open and every worker
owns two pinned staging slots sized to the largest streamed block. The latter
shares file-backed pages, not process-local Python objects, and does not alter
request routing or scheduling. Packing into the host slot is synchronous; the
pinned H2D copy can overlap with computation.
2026-07-30 21:35:38 +08:00
### 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
2026-08-06 23:21:43 +08:00
regular loading only when the model is outside the DLO+AllGather mmap path;
online quantization with DLO+AllGather is rejected. Use
`--dlo-no-use-allgather` or disable online quantization.
- Tensor Parallel > 1 is rejected in the DLO+AllGather mmap path because TP-aware
weight-loader callbacks are bypassed. The no-AllGather path retains the
2026-08-14 14:38:06 +08:00
standard TP loader instead of rank-local mmap and is experimental.
2026-08-06 23:21:43 +08:00
- HSDP + AllGather is rejected because it would double-shard parameters. HSDP
with no-AllGather is accepted at configuration level, but has limited
end-to-end validation.
2026-07-30 21:35:38 +08:00
- `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
2026-08-06 23:21:43 +08:00
(multi-GPU, sharded AllGather or rank-local mode)
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"` |
2026-08-14 14:38:06 +08:00
| MiniMaxH3Pipeline | `MiniMaxAI/MiniMax-H3` (`FL2VA` ) | `MiniMaxH3DiTModel` | - | ✓ | ✓ | `"blocks"` |
2026-07-30 21:35:38 +08:00
| 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-08-06 23:21:43 +08:00
- Distributed Layerwise Offloading reuses the layerwise block topology, but the
model and parallelism combination still needs validation. AllGather mode
2026-08-14 14:38:06 +08:00
additionally requires a compatible checkpoint/mmap layout. For explicitly
compatible TP1 models, no-AllGather uses node-shared mmap sources; other
layouts retain the standard loader's rank-local tensors. See the
2026-08-06 23:21:43 +08:00
[DLO design ](../../design/feature/distributed_layerwise_offload.md ) and
[Cosmos3 DistOffload recipe ](https://github.com/vllm-project/vllm-omni/blob/main/recipes/cosmos3/Cosmos3-DistOffload.md )
for usage examples.