2026-04-14 20:26:27 +08:00
# Parallelism Patterns Reference
## Overview
vLLM-Omni supports multiple parallelism strategies for diffusion models. Each targets a different bottleneck:
| Strategy | Splits | Best For | Constraint |
|----------|--------|----------|------------|
| Tensor Parallel (TP) | Model layers across GPUs | Latency reduction, large models | Requires fast GPU interconnect, `num_heads % tp == 0` |
| Sequence Parallel (SP/USP) | Sequence tokens across GPUs | Long sequences (video, high-res) | Near-linear scaling |
| CFG Parallel | Positive/negative CFG branches | Models using classifier-free guidance | Exactly 2 GPUs |
| HSDP | Weight shards via FSDP2 | VRAM reduction | Cannot combine with TP |
| VAE Patch Parallel | VAE decode spatial tiles | Large VAE outputs | Auto-enables tiling |
**Recommended integration order** : TP → SP → CFG Parallel → HSDP
**Official design docs** :
- TP: https://docs.vllm.ai/projects/vllm-omni/en/latest/design/feature/tensor_parallel
- SP: https://docs.vllm.ai/projects/vllm-omni/en/latest/design/feature/sequence_parallel
- CFG: https://docs.vllm.ai/projects/vllm-omni/en/latest/design/feature/cfg_parallel
- HSDP: https://docs.vllm.ai/projects/vllm-omni/en/latest/design/feature/hsdp
---
## Tensor Parallelism (TP)
Replace standard `nn.Linear` with vLLM's parallel linear layers. This is the most invasive change but provides direct VRAM savings and compute speedup.
### Layer replacement rules
| Pattern | vLLM Layer | When to Use |
|---------|-----------|-------------|
| Fan-out (first in FFN) | `ColumnParallelLinear` | Projection that splits output across ranks |
| Fan-in (second in FFN) | `RowParallelLinear` | Projection that gathers across ranks |
| QKV projection | `QKVParallelLinear` | Fused Q/K/V for self-attention |
| Single Q or K or V | `ColumnParallelLinear` | Separate projections (cross-attention) |
| Attention output | `RowParallelLinear` | Output projection after attention |
| Must not shard | `ReplicatedLinear` | Layers that must stay replicated |
### MLP Block (Up-Down Pattern)
```python
from vllm.model_executor.layers.linear import (
ColumnParallelLinear , RowParallelLinear ,
)
class TPFeedForward ( nn . Module ):
def __init__ ( self , dim , ffn_dim ):
super () . __init__ ()
self . fc1 = ColumnParallelLinear ( dim , ffn_dim , bias = False , return_bias = False )
self . fc2 = RowParallelLinear (
ffn_dim , dim , bias = False ,
input_is_parallel = True , # Input already sharded from fc1
return_bias = False ,
)
def forward ( self , x ):
x , _ = self . fc1 ( x )
x = torch . nn . functional . gelu ( x )
x , _ = self . fc2 ( x )
return x
```
### Attention Block (QKV-Out Pattern)
```python
from vllm.model_executor.layers.linear import QKVParallelLinear , RowParallelLinear
from vllm_omni.diffusion.attention.layer import Attention
class TPSelfAttention ( nn . Module ):
def __init__ ( self , dim , num_heads , num_kv_heads = None ):
super () . __init__ ()
num_kv_heads = num_kv_heads or num_heads
self . head_dim = dim // num_heads
self . to_qkv = QKVParallelLinear (
hidden_size = dim ,
head_size = self . head_dim ,
total_num_heads = num_heads ,
total_num_kv_heads = num_kv_heads ,
bias = False ,
return_bias = False ,
)
self . to_out = RowParallelLinear (
dim , dim , bias = False ,
input_is_parallel = True ,
return_bias = False ,
)
self . attn = Attention (
num_heads = self . to_qkv . num_heads , # Local heads per GPU
head_size = self . head_dim ,
softmax_scale = 1.0 / ( self . head_dim ** 0.5 ),
causal = False ,
num_kv_heads = self . to_qkv . num_kv_heads , # Local KV heads per GPU
2026-05-11 09:13:07 +08:00
role = "self" ,
2026-04-14 20:26:27 +08:00
)
def forward ( self , x ):
qkv , _ = self . to_qkv ( x )
q , k , v = qkv . split (
[ self . to_qkv . num_heads * self . head_dim ,
self . to_qkv . num_kv_heads * self . head_dim ,
self . to_qkv . num_kv_heads * self . head_dim ],
dim =- 1 ,
)
B , S , _ = x . shape
q = q . view ( B , S , self . to_qkv . num_heads , self . head_dim )
k = k . view ( B , S , self . to_qkv . num_kv_heads , self . head_dim )
v = v . view ( B , S , self . to_qkv . num_kv_heads , self . head_dim )
out = self . attn ( q , k , v )
out = out . reshape ( B , S , - 1 )
out , _ = self . to_out ( out )
return out
```
### QKV Fusion in load_weights
When you fuse separate Q/K/V into `QKVParallelLinear` , map diffusers' separate weight names:
```python
stacked_params_mapping = [
( "to_qkv" , "to_q" , "q" ),
( "to_qkv" , "to_k" , "k" ),
( "to_qkv" , "to_v" , "v" ),
]
def load_weights ( self , weights ):
params = dict ( self . named_parameters ())
loaded = set ()
for name , tensor in weights :
for fused_name , orig_name , shard_id in stacked_params_mapping :
if orig_name in name :
name = name . replace ( orig_name , fused_name )
param = params [ name ]
param . weight_loader ( param , tensor , shard_id )
loaded . add ( name )
break
else :
if name in params :
param = params [ name ]
if hasattr ( param , "weight_loader" ):
param . weight_loader ( param , tensor )
else :
default_weight_loader ( param , tensor )
loaded . add ( name )
return loaded
```
### RMSNorm with TP
When RMSNorm sits between TP-sharded dimensions, use `DistributedRMSNorm` — it computes global RMS via all-reduce across TP ranks. See the Wan2.2 implementation for the pattern.
### TP Constraints
- `num_heads % tp_size == 0`
- `num_kv_heads % tp_size == 0`
- Use `self.to_qkv.num_heads` (local per-GPU count), not total heads, for split sizes
### Testing TP
```bash
python text_to_image.py --model Your-org/your-model \
--tensor-parallel-size 2 --output "tp_test.png"
```
**Verify** : speedup, memory reduction proportional to TP size, quality matches single-GPU.
### Reference implementations
| Model | Path |
|-------|------|
| Z-Image | `vllm_omni/diffusion/models/z_image/z_image_transformer.py` |
| FLUX | `vllm_omni/diffusion/models/flux/flux_transformer.py` |
| Qwen-Image | `vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py` |
---
## Sequence Parallelism (SP / USP)
SP splits sequence tokens across GPUs using Ulysses (all-to-all) or Ring (P2P) communication. It is applied non-intrusively via the `_sp_plan` dict — no changes to `forward()` logic.
### Approach 1: Non-Intrusive `_sp_plan` (Recommended)
The framework automatically registers hooks to shard inputs and gather outputs at `nn.Module` boundaries.
#### Step 1: Identify module boundaries
Find where tensors need sharding/gathering:
```python
class MyTransformer ( nn . Module ):
def __init__ ( self ):
self . patch_embed = PatchEmbed () # Before blocks
self . pos_embed = RoPE () # RoPE may need splitting
self . blocks = nn . ModuleList ([ ... ]) # Blocks process sharded x
self . norm_out = LayerNorm ()
self . proj_out = Linear () # Gather after this
def forward ( self , x ):
x = self . patch_embed ( x )
pos = self . pos_embed ( x )
for block in self . blocks :
x = block ( x , pos )
x = self . norm_out ( x )
return self . proj_out ( x )
```
#### Step 2: Handle inline operations
`_sp_plan` hooks only work at `nn.Module` boundaries. Inline ops like `torch.cat()` must be extracted into submodules:
```python
# BAD: Inline — hooks can't intercept
unified = torch . cat ([ x , cap_feats ], dim = 1 )
# GOOD: Extract into submodule
class UnifiedPrepare ( nn . Module ):
def forward ( self , x , cap_feats ):
return torch . cat ([ x , cap_feats ], dim = 1 )
self . unified_prepare = UnifiedPrepare ()
unified = self . unified_prepare ( x , cap_feats )
```
Common cases: `torch.cat()` , `pad_sequence()` , `tensor.reshape()` , complex preprocessing.
#### Step 3: Write `_sp_plan`
**Pattern 1: Shard at first block, gather at output** (most common)
```python
from vllm_omni.diffusion.distributed.sp_plan import (
SequenceParallelInput , SequenceParallelOutput ,
)
class StandardTransformer ( nn . Module ):
_sp_plan = {
"blocks.0" : {
"hidden_states" : SequenceParallelInput ( split_dim = 1 , expected_dims = 3 ),
},
"proj_out" : SequenceParallelOutput ( gather_dim = 1 , expected_dims = 3 ),
}
```
**Pattern 2: Shard RoPE outputs separately**
```python
class TransformerWithRoPE ( nn . Module ):
_sp_plan = {
"rope" : {
0 : SequenceParallelInput ( split_dim = 1 , expected_dims = 4 , split_output = True ),
1 : SequenceParallelInput ( split_dim = 1 , expected_dims = 4 , split_output = True ),
},
"blocks.0" : {
"hidden_states" : SequenceParallelInput ( split_dim = 1 , expected_dims = 3 ),
},
"proj_out" : SequenceParallelOutput ( gather_dim = 1 , expected_dims = 3 ),
}
```
**Pattern 3: Dual-stream (shard image, replicate text)**
```python
class DualStreamTransformer ( nn . Module ):
_sp_plan = {
"rope_preparer" : {
2 : SequenceParallelInput ( split_dim = 0 , expected_dims = 2 , split_output = True ),
3 : SequenceParallelInput ( split_dim = 0 , expected_dims = 2 , split_output = True ),
},
"transformer_blocks.0" : {
"hidden_states" : SequenceParallelInput ( split_dim = 1 , expected_dims = 3 ),
},
"proj_out" : SequenceParallelOutput ( gather_dim = 1 , expected_dims = 3 ),
}
```
### API Reference
**SequenceParallelInput** :
| Parameter | Type | Description |
|-----------|------|-------------|
| `split_dim` | int | Dimension to split (usually 1 for sequence) |
| `expected_dims` | int/None | Expected tensor rank for validation |
| `split_output` | bool | `False` : shard input params; `True` : shard output tensors |
| `auto_pad` | bool | Auto-pad if sequence not divisible by world_size |
**SequenceParallelOutput** :
| Parameter | Type | Description |
|-----------|------|-------------|
| `gather_dim` | int | Dimension to gather (usually 1 for sequence) |
| `expected_dims` | int/None | Expected tensor rank for validation |
**Module naming** :
| Key | Meaning |
|-----|---------|
| `"blocks.0"` | First element of ModuleList |
| `"blocks.*"` | All elements of ModuleList |
| `"rope"` | Named submodule |
**Dictionary value types** :
| Key type | split_output | Description |
|----------|-------------|-------------|
| `"param_name"` (str) | False | Shard input parameter by name |
| `0, 1, ...` (int) | True | Shard output tuple by index |
### Approach 2: Intrusive Modification (Complex Cases)
For dynamic sharding logic that can't be expressed via `_sp_plan` :
```python
from vllm_omni.diffusion.distributed.sp_sharding import sp_shard , sp_gather
def forward ( self , hidden_states , ... ):
if self . parallel_config . sequence_parallel_size > 1 :
hidden_states = sp_shard ( hidden_states , dim = 1 )
for block in self . blocks :
hidden_states = block ( hidden_states )
if self . parallel_config . sequence_parallel_size > 1 :
hidden_states = sp_gather ( hidden_states , dim = 1 )
return hidden_states
```
Use intrusive modification as a last resort — `_sp_plan` is preferred for maintainability.
### UAA Mode (Experimental)
`ulysses_mode="advanced_uaa"` handles arbitrary sequence lengths and head counts that aren't divisible by `ulysses_degree` . Uses variable all-to-all split sizes and temporary head padding.
### Combining SP methods
Ulysses and Ring can be combined: `ulysses_degree × ring_degree = total SP GPUs` .
```python
DiffusionParallelConfig ( ulysses_degree = 2 , ring_degree = 2 ) # 4 GPUs total
```
### Testing SP
```bash
# Offline
python text_to_image.py --model Your-model --ulysses-degree 2
# Online serving
vllm serve Your-model --omni --usp 2
```
### Reference implementations
| Model | Path |
|-------|------|
| Qwen-Image | `vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py` |
| Wan2.2 | `vllm_omni/diffusion/models/wan2_2/wan2_2_transformer.py` |
| Z-Image | `vllm_omni/diffusion/models/z_image/z_image_transformer.py` |
---
## CFG Parallelism
Distributes positive/negative Classifier-Free Guidance branches across 2 GPUs.
### Implementation
Inherit `CFGParallelMixin` and implement `diffuse()` :
```python
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
class YourPipeline ( nn . Module , CFGParallelMixin ):
def diffuse ( self , latents , timesteps , prompt_embeds , negative_embeds ,
do_true_cfg , true_cfg_scale , ** kwargs ):
for i , t in enumerate ( timesteps ):
positive_kwargs = {
"hidden_states" : latents ,
"encoder_hidden_states" : prompt_embeds ,
"timestep" : t ,
}
negative_kwargs = {
"hidden_states" : latents ,
"encoder_hidden_states" : negative_embeds ,
"timestep" : t ,
} if do_true_cfg else None
noise_pred = self . predict_noise_maybe_with_cfg (
do_true_cfg = do_true_cfg ,
true_cfg_scale = true_cfg_scale ,
positive_kwargs = positive_kwargs ,
negative_kwargs = negative_kwargs ,
)
latents = self . scheduler_step_maybe_with_cfg (
noise_pred , t , latents , do_true_cfg
)
return latents
```
### Customization hooks
| Method | Override when |
|--------|-------------|
| `predict_noise()` | Non-standard transformer call (e.g., dual-transformer like Wan2.2) |
| `cfg_normalize_function()` | Custom normalization (e.g., LongCat with clamping) |
| `combine_cfg_noise()` | Multi-output models (e.g., video + audio: CFG on video, positive-only on audio) |
**Custom predict_noise** (Wan2.2 — selects active transformer):
```python
def predict_noise ( self , current_model = None , ** kwargs ):
if current_model is None :
current_model = self . transformer
return current_model ( ** kwargs )[ 0 ]
```
**Custom combine_cfg_noise** (multi-output):
```python
def combine_cfg_noise ( self , positive_pred , negative_pred , scale , normalize ):
video_pos , audio_pos = positive_pred
video_neg , audio_neg = negative_pred
video_combined = super () . combine_cfg_noise ( video_pos , video_neg , scale , normalize )
return ( video_combined , audio_pos )
```
### Composite scheduler for multi-output
When each output has its own schedule:
```python
class VideoAudioScheduler :
def __init__ ( self , video_scheduler , audio_scheduler ):
self . video_scheduler = video_scheduler
self . audio_scheduler = audio_scheduler
def step ( self , noise_pred , t , latents , return_dict = False , generator = None ):
video_out = self . video_scheduler . step (
noise_pred [ 0 ], t [ 0 ], latents [ 0 ], return_dict = False , generator = generator
)[ 0 ]
audio_out = self . audio_scheduler . step (
noise_pred [ 1 ], t [ 1 ], latents [ 1 ], return_dict = False , generator = generator
)[ 0 ]
return (( video_out , audio_out ),)
```
### Testing CFG Parallel
```bash
python text_to_image.py --model Your-model \
--cfg-parallel-size 2 --cfg-scale 4.0 \
--negative-prompt "ugly, unclear"
```
**Constraint** : `guidance_scale > 1.0` and negative prompt must be provided.
### Reference implementations
| Model | Path |
|-------|------|
| Qwen-Image | `vllm_omni/diffusion/models/qwen_image/cfg_parallel.py` |
| Wan2.2 | `vllm_omni/diffusion/models/wan2_2/pipeline_wan2_2.py` |
| Mixin base | `vllm_omni/diffusion/distributed/cfg_parallel.py` |
---
## HSDP (Hybrid Sharded Data Parallel)
Shards model weights across GPUs using PyTorch FSDP2. Reduces per-GPU VRAM without changing computation.
### Implementation
Add `_hsdp_shard_conditions` to the transformer class:
```python
class YourTransformer ( nn . Module ):
@staticmethod
def _is_transformer_block ( name : str , module ) -> bool :
return "blocks" in name and name . split ( "." )[ - 1 ] . isdigit ()
_hsdp_shard_conditions = [ _is_transformer_block ]
```
For MoE models, add additional conditions:
```python
class MoETransformer ( nn . Module ):
@staticmethod
def _is_transformer_block ( name , module ):
return "blocks" in name and name . split ( "." )[ - 1 ] . isdigit ()
@staticmethod
def _is_moe_expert ( name , module ):
return "experts" in name and name . split ( "." )[ - 1 ] . isdigit ()
_hsdp_shard_conditions = [ _is_transformer_block , _is_moe_expert ]
```
A module is sharded if **any** condition returns `True` .
### Constraints
- Cannot combine with Tensor Parallelism
- For standalone HSDP (no other parallelism), `hsdp_shard_size` must be specified explicitly
- Can combine with SP: HSDP reduces memory while SP distributes sequence
### Testing HSDP
```python
from vllm_omni.diffusion.data import DiffusionParallelConfig
parallel_config = DiffusionParallelConfig ( use_hsdp = True , hsdp_shard_size = 8 )
omni = Omni ( model = "your-model" , parallel_config = parallel_config )
```
Or CLI:
```bash
vllm serve Your-model --omni --use-hsdp
```
**Verify** : logs show "HSDP Inference: replicate_size=..., shard_size=..." and "Sharded N modules + root". Check VRAM reduction.
### Reference implementations
| Model | Path |
|-------|------|
| Wan2.2 | `vllm_omni/diffusion/models/wan2_2/wan2_2_transformer.py` |
| HSDP Core | `vllm_omni/diffusion/distributed/hsdp.py` |
---
## VAE Patch Parallelism
Shards VAE decode spatially across ranks using tiling:
```bash
python text_to_image.py --model Your-model --vae-patch-parallel-size 4
```
Auto-enables `--vae-use-tiling` . Uses `DistributedAutoencoderKLWan` or similar distributed VAE. Set `vae_patch_parallel_size` in `DiffusionParallelConfig` .
---
## Combining Parallelism Methods
Common multi-GPU recipes:
```bash
# 4 GPUs: CFG (2) × Ulysses (2)
python text_to_image.py --model Qwen/Qwen-Image \
--cfg-parallel-size 2 --ulysses-degree 2
# 8 GPUs: Ulysses (4) × Ring (2) + VAE patch (8)
python text_to_video.py --model Wan-AI/Wan2.2-T2V-A14B-Diffusers \
--ulysses-degree 4 --ring-degree 2 --vae-patch-parallel-size 8
# 2 GPUs: HSDP + Ulysses (cannot combine HSDP with TP)
vllm serve Your-model --omni --use-hsdp --usp 2
```
## Discovering Parallelism Support
Check which parallelism methods a model supports:
| Check | How |
|-------|-----|
| **Ulysses / Ring SP** | Transformer defines `_sp_plan` . Search: `grep -r '_sp_plan' vllm_omni/diffusion/models/` |
| **CFG Parallel** | Pipeline inherits `CFGParallelMixin` . Search: `grep -r 'CFGParallelMixin' vllm_omni/diffusion/models/` |
| **TP** | Uses `ColumnParallelLinear` / `QKVParallelLinear` . Search: `grep -r 'ParallelLinear\|QKVParallel' vllm_omni/diffusion/models/<model>/` |
| **HSDP** | Transformer defines `_hsdp_shard_conditions` . Search: `grep -r '_hsdp_shard_conditions' vllm_omni/diffusion/models/` |
2026-06-22 10:17:27 +08:00
The canonical per-model support overview/table is in `docs/user_guide/diffusion/parallelism/overview.md` .