[Feature] Support HSDP for diffusion models (#1339)

Signed-off-by: gcanlin <canlinguosdu@gmail.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
Canlin GuoandHongsheng Liu authored and GitHub committed 2026-02-26 21:31:10 +08:00
1 parent 66c3e04263
commit f53152d340
17 files changed
+1023 -46

No files matched your search

+146
View File
@@ -0,0 +1,146 @@
# HSDP
This section describes how to add HSDP (Hybrid Sharded Data Parallel) support to a diffusion transformer model. We use the Wan2.2 transformer as the reference implementation.
---
## Table of Contents
- [Overview](#overview)
- [Step-by-Step Implementation](#step-by-step-implementation)
- [Testing](#testing)
- [Reference Implementations](#reference-implementations)
---
## Overview
### What is HSDP?
HSDP (Hybrid Sharded Data Parallel) is a memory optimization technique that **shards model weights** across multiple GPUs using PyTorch's FSDP2. Unlike Tensor Parallelism which splits computation, HSDP:
- Shards weights across GPUs to reduce per-GPU memory usage
- Gathers weights on-demand during forward passes
- Can work standalone or combined with other parallelism (e.g., Sequence Parallel)
This enables inference of large models (e.g., Wan2.2 14B) on GPUs with limited memory.
**Important constraints:**
- HSDP cannot be used with Tensor Parallelism
- For standalone HSDP (no other parallelism), `hsdp_shard_size` must be specified explicitly
### Architecture
HSDP implementation relies on:
1. **`_hsdp_shard_conditions`**: Model attribute specifying which modules to shard
2. **`apply_hsdp_to_model`**: Function that applies FSDP2 sharding
3. **`HSDPInferenceConfig`**: Runtime configuration for HSDP
---
## Step-by-Step Implementation
### Step 1: Identify Modules to Shard
Determine which modules in your transformer should be sharded. Typically, these are:
- Transformer blocks (e.g., `blocks.0`, `blocks.1`, ...)
- Large submodules with significant weight memory
**Key questions:**
- Which modules have the largest weights?
- Which modules are repeated (like transformer blocks)?
### Step 2: Define Shard Conditions
Add `_hsdp_shard_conditions` to your model class. This is a list of functions that return `True` for modules that should be sharded.
**Example (Transformer Blocks):**
```python
class MyTransformerModel(nn.Module):
@staticmethod
def _is_transformer_block(name: str, module) -> bool:
"""Match transformer blocks for HSDP sharding.
Args:
name: Module name from named_modules() (e.g., "blocks.0", "blocks.0.attn")
module: The module instance
Returns:
True if this module should be sharded
"""
return "blocks" in name and name.split(".")[-1].isdigit()
_hsdp_shard_conditions = [_is_transformer_block]
```
**Multiple Conditions Example:**
```python
class MyModel(nn.Module):
@staticmethod
def _is_transformer_block(name: str, module) -> bool:
return "blocks" in name and name.split(".")[-1].isdigit()
@staticmethod
def _is_moe_expert(name: str, module) -> bool:
# Also shard MoE expert layers
return "experts" in name and name.split(".")[-1].isdigit()
# Module is sharded if ANY condition returns True
_hsdp_shard_conditions = [_is_transformer_block, _is_moe_expert]
```
---
## Testing
After adding HSDP support, test with:
```python
from vllm_omni import Omni
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
parallel_config = DiffusionParallelConfig(
use_hsdp=True,
hsdp_shard_size=8, # Shard across 8 GPUs
)
omni = Omni(model="your-model-name", parallel_config=parallel_config)
output = omni.generate(
"a cup of coffee on the table",
OmniDiffusionSamplingParams(num_inference_steps=50),
)
```
**Or via command line:**
```bash
vllm serve Your-org/your-model --omni --port 8091 --use-hsdp
```
**Verify:**
1. Check logs for "HSDP Inference: replicate_size=..., shard_size=..."
2. Check logs for "Sharded N modules + root"
3. Verify memory usage is reduced proportionally
4. Compare generated output quality with HSDP disabled
---
## Reference Implementations
Complete examples in the codebase:
| Model | Path | Notes |
|-------|------|-------|
| **Wan2.2** | `vllm_omni/diffusion/models/wan2_2/wan2_2_transformer.py` | Reference implementation |
| **HSDP Core** | `vllm_omni/diffusion/distributed/hsdp.py` | `apply_hsdp_to_model`, `shard_model` |
| **HSDP Tests** | `tests/diffusion/distributed/test_hsdp.py` | Unit tests |
---
@@ -16,6 +16,8 @@ The following parallelism methods are currently supported in vLLM-Omni:
5. [VAE Patch Parallelism](#vae-patch-parallelism): VAE patch parallelism shards VAE decode spatially across ranks. This can reduce the peak memory of VAE decode and (depending on resolution and communication overhead) speed up VAE decode.
6. [HSDP](#hsdp): Hybrid Sharded Data Parallel shards model weights across GPUs using PyTorch FSDP2. This reduces per-GPU memory usage, enabling inference of large models on GPUs with limited memory.
The following table shows which models are currently supported by parallelism method:
### ImageGen
@@ -49,9 +51,9 @@ The following table shows which models are currently supported by parallelism me
### VideoGen
| Model | Model Identifier | Ulysses-SP | Ring-Attention | Tensor-Parallel |
|-------|------------------|------------|---------|--------------------------|
| **Wan2.2** | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | ✅ | ✅ | ✅ |
| Model | Model Identifier | Ulysses-SP | Ring-Attention | Tensor-Parallel | HSDP |
|-------|------------------|:----------:|:--------------:|:---------------:|:----:|
| **Wan2.2** | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | ✅ | ✅ | ✅ | ✅ |
### Tensor Parallelism
@@ -330,3 +332,84 @@ You can enable CFG-Parallel in online serving for diffusion models via `--cfg-pa
```bash
vllm serve Qwen/Qwen-Image-Edit --omni --port 8091 --cfg-parallel-size 2
```
### HSDP
HSDP (Hybrid Sharded Data Parallel) shards model weights across GPUs to reduce per-GPU memory usage. This enables inference of large models (e.g., Wan2.2 14B) on GPUs with limited memory.
Unlike Tensor Parallelism which splits computation, HSDP uses PyTorch's FSDP2 to shard and redistribute weights at runtime. Each GPU only holds a fraction of the model weights, and weights are gathered on-demand during forward passes.
#### Configuration
HSDP is configured via `DiffusionParallelConfig`:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `use_hsdp` | bool | False | Enable HSDP |
| `hsdp_shard_size` | int | -1 | Number of GPUs to shard weights across. -1 = auto (requires other parallelism > 1) |
| `hsdp_replicate_size` | int | 1 | Number of replica groups. Each group holds a full sharded copy |
**Constraints:**
- `hsdp_replicate_size × hsdp_shard_size == world_size`
- HSDP cannot be used with Tensor Parallelism (`tensor_parallel_size` must be 1)
#### Operating Modes
HSDP can work in two modes:
- **Standalone Mode**: HSDP alone without other parallelism. Must specify `hsdp_shard_size` explicitly.
- **Combined Mode**: HSDP overlays on top of other parallelism (Ulysses Sequence Parallel, CFG Parallel). HSDP dimensions must match world_size.
#### Offline Inference
**Standalone HSDP** (shard across 4 GPUs, no other parallelism):
```python
from vllm_omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.diffusion.data import DiffusionParallelConfig
omni = Omni(
model="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
parallel_config=DiffusionParallelConfig(
use_hsdp=True,
hsdp_shard_size=4, # Shard across 4 GPUs
),
)
outputs = omni.generate(
"A cat playing piano",
OmniDiffusionSamplingParams(num_inference_steps=50),
)
```
**Combined HSDP + Sequence Parallel**:
```python
omni = Omni(
model="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
parallel_config=DiffusionParallelConfig(
ulysses_degree=4, # Sequence parallel
use_hsdp=True, # HSDP overlays on SP
),
)
```
#### Online Serving
**Standalone HSDP** (shard model across 4 GPUs):
```bash
vllm serve Wan-AI/Wan2.2-T2V-A14B-Diffusers --omni --port 8091 --use-hsdp --hsdp-shard-size 4
```
**Combined with Sequence Parallel**:
```bash
vllm serve Wan-AI/Wan2.2-T2V-A14B-Diffusers --omni --port 8091 --use-hsdp --usp 4
```
#### Adding HSDP Support to New Models
For detailed instructions on adding HSDP support to new models, see the [HSDP Contributing Guide](../../design/feature/hsdp.md).
+30 -3
View File
@@ -30,6 +30,8 @@ vLLM-Omni also supports parallelism methods for diffusion models, including:
5. [VAE Patch Parallelism](diffusion/parallelism_acceleration.md#vae-patch-parallelism) - shards VAE decode/encode spatially across ranks to reduce VAE peak memory (and can speed up VAE decode).
6. [HSDP](diffusion/parallelism_acceleration.md#hsdp) - Hybrid Sharded Data Parallel shards model weights across GPUs to reduce per-GPU memory usage, enabling inference of large models on limited GPU memory.
## Quick Comparison
### Cache Methods
@@ -69,9 +71,9 @@ The following table shows which models are currently supported by each accelerat
### VideoGen
| Model | Model Identifier | TeaCache | Cache-DiT | Ulysses-SP | Ring-Attention | CFG-Parallel |
|-------|------------------|:--------:|:---------:|:----------:|:--------------:|:----------------:|
| **Wan2.2** | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | ❌ | ✅ | ✅ | ✅ | ✅ |
| Model | Model Identifier | TeaCache | Cache-DiT | Ulysses-SP | Ring-Attention | CFG-Parallel | HSDP |
|-------|------------------|:--------:|:---------:|:----------:|:--------------:|:------------:|:----:|
| **Wan2.2** | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
### Quantization
@@ -265,6 +267,30 @@ outputs = omni.generate(
)
```
### Using HSDP
HSDP (Hybrid Sharded Data Parallel) shards model weights across GPUs to reduce per-GPU memory usage. This enables inference of large models (e.g., Wan2.2 14B) on GPUs with limited memory.
```python
from vllm_omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.diffusion.data import DiffusionParallelConfig
omni = Omni(
model="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
parallel_config=DiffusionParallelConfig(
use_hsdp=True,
hsdp_replicate_size=1, # Number of replica groups
hsdp_shard_size=8, # Number of GPUs to shard across (or -1 for auto)
),
)
outputs = omni.generate(
"A cat playing piano",
OmniDiffusionSamplingParams(num_inference_steps=50),
)
```
### Using CFG-Parallel
Run image-to-image:
@@ -319,3 +345,4 @@ For detailed information on each acceleration method:
- **[Sequence Parallelism](diffusion/parallelism_acceleration.md#sequence-parallelism)** - Guidance on how to set sequence parallelism with configuration.
- **[CFG-Parallel](diffusion/parallelism_acceleration.md#cfg-parallel)** - Guidance on how to set CFG-Parallel to run positive/negative branches across ranks.
- **[VAE Patch Parallelism](diffusion/parallelism_acceleration.md#vae-patch-parallelism)** - Guidance on how to reduce VAE memory via patch/tile parallelism.
- **[HSDP](diffusion/parallelism_acceleration.md#hsdp)** - Hybrid Sharded Data Parallel for memory-efficient inference of large models.
@@ -66,5 +66,10 @@ Key arguments:
- `--vae-use-tiling`: Enable VAE tiling for memory optimization.
- `--cfg-parallel-size`: set it to 2 to enable CFG Parallel. See more examples in [`user_guide`](../../../docs/user_guide/diffusion/parallelism_acceleration.md#cfg-parallel).
- `--enable-cpu-offload`: enable CPU offloading for diffusion models.
- `--use-hsdp`: Enable Hybrid Sharded Data Parallel to shard model weights across GPUs.
- `--hsdp-shard-size`: Number of GPUs to shard model weights across within each replica group. -1 (default) auto-calculates as world_size / replicate_size.
- `--hsdp-replicate-size`: Number of replica groups for HSDP. Each replica holds a full sharded copy. Default 1 means pure sharding (no replication).
> ℹ️ If you encounter OOM errors, try using `--vae-use-slicing` and `--vae-use-tiling` to reduce memory usage.
@@ -80,6 +80,24 @@ def parse_args() -> argparse.Namespace:
action="store_true",
help="Enable layerwise (blockwise) offloading on DiT modules.",
)
parser.add_argument(
"--ulysses-degree",
type=int,
default=1,
help="Number of GPUs used for ulysses sequence parallelism.",
)
parser.add_argument(
"--ring-degree",
type=int,
default=1,
help="Number of GPUs used for ring sequence parallelism.",
)
parser.add_argument(
"--tensor-parallel-size",
type=int,
default=1,
help="Number of GPUs used for tensor parallelism (TP) inside the DiT.",
)
parser.add_argument(
"--cfg-parallel-size",
type=int,
@@ -92,6 +110,29 @@ def parse_args() -> argparse.Namespace:
action="store_true",
help="Disable torch.compile and force eager execution.",
)
parser.add_argument(
"--use-hsdp",
action="store_true",
help=("Enable Hybrid Sharded Data Parallel to shard model weights across GPUs. "),
)
parser.add_argument(
"--hsdp-shard-size",
type=int,
default=-1,
help=(
"Number of GPUs to shard model weights across within each replica group. "
"-1 (default) auto-calculates as world_size / replicate_size. "
),
)
parser.add_argument(
"--hsdp-replicate-size",
type=int,
default=1,
help=(
"Number of replica groups for HSDP. Each replica holds a full sharded copy. "
"Default 1 means pure sharding (no replication). "
),
)
return parser.parse_args()
@@ -128,7 +169,13 @@ def main():
# Check if profiling is requested via environment variable
profiler_enabled = bool(os.getenv("VLLM_TORCH_PROFILER_DIR"))
parallel_config = DiffusionParallelConfig(
ulysses_degree=args.ulysses_degree,
ring_degree=args.ring_degree,
cfg_parallel_size=args.cfg_parallel_size,
tensor_parallel_size=args.tensor_parallel_size,
use_hsdp=args.use_hsdp,
hsdp_shard_size=args.hsdp_shard_size,
hsdp_replicate_size=args.hsdp_replicate_size,
)
omni = Omni(
model=args.model,
+225
View File
@@ -0,0 +1,225 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for HSDP (Hybrid Sharded Data Parallel) configuration and utilities.
These tests verify HSDP configuration logic without requiring a distributed environment.
"""
import pytest
import torch.nn as nn
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.diffusion.distributed.hsdp import HSDPInferenceConfig
pytestmark = [pytest.mark.diffusion, pytest.mark.parallel, pytest.mark.cpu]
class TestHSDPInferenceConfig:
"""Tests for HSDPInferenceConfig dataclass."""
def test_default_values(self):
"""Test default configuration values."""
config = HSDPInferenceConfig()
assert config.enabled is False
assert config.hsdp_replicate_size == 1
assert config.hsdp_shard_size == -1
def test_custom_values(self):
"""Test custom configuration values."""
config = HSDPInferenceConfig(
enabled=True,
hsdp_replicate_size=2,
hsdp_shard_size=4,
)
assert config.enabled is True
assert config.hsdp_replicate_size == 2
assert config.hsdp_shard_size == 4
class TestDiffusionParallelConfigHSDP:
"""Tests for HSDP settings in DiffusionParallelConfig."""
def test_hsdp_disabled_by_default(self):
"""HSDP should be disabled by default."""
config = DiffusionParallelConfig()
assert config.use_hsdp is False
assert config.hsdp_shard_size == -1
assert config.hsdp_replicate_size == 1
def test_hsdp_auto_shard_size(self):
"""Test auto-calculation of hsdp_shard_size when use_hsdp=True."""
# ulysses_degree=4 -> world_size=4
# hsdp_shard_size should be auto-calculated as 4 // 1 = 4
config = DiffusionParallelConfig(
ulysses_degree=4,
use_hsdp=True,
)
assert config.world_size == 4
assert config.hsdp_shard_size == 4
assert config.hsdp_replicate_size == 1
def test_hsdp_auto_shard_size_fails_standalone(self):
"""Test that auto-calculate fails when other parallelism is all 1."""
# When all other parallelism is 1, cannot auto-calculate
# User must specify hsdp_shard_size explicitly
with pytest.raises(ValueError, match="Cannot auto-calculate hsdp_shard_size"):
DiffusionParallelConfig(
use_hsdp=True,
# All other parallelism defaults to 1
)
def test_hsdp_standalone_mode(self):
"""Test standalone HSDP (HSDP without other parallelism)."""
# Standalone HSDP: all other parallelism=1, explicit shard_size
config = DiffusionParallelConfig(
use_hsdp=True,
hsdp_shard_size=4, # Explicit shard size
hsdp_replicate_size=1,
)
# world_size should be determined by HSDP
assert config.world_size == 4
assert config.hsdp_shard_size == 4
assert config.hsdp_replicate_size == 1
def test_hsdp_standalone_with_replicate(self):
"""Test standalone HSDP with replication."""
config = DiffusionParallelConfig(
use_hsdp=True,
hsdp_shard_size=4,
hsdp_replicate_size=2,
)
# world_size = shard_size * replicate_size
assert config.world_size == 8
assert config.hsdp_shard_size == 4
assert config.hsdp_replicate_size == 2
def test_hsdp_with_replicate(self):
"""Test HSDP with replication (hybrid mode) combined with other parallelism."""
# world_size=8, replicate=2 -> shard_size should be 4
config = DiffusionParallelConfig(
ulysses_degree=8,
use_hsdp=True,
hsdp_replicate_size=2,
)
assert config.world_size == 8
assert config.hsdp_shard_size == 4
assert config.hsdp_replicate_size == 2
def test_hsdp_explicit_shard_size_valid(self):
"""Test explicit hsdp_shard_size that matches world_size."""
config = DiffusionParallelConfig(
ulysses_degree=4,
use_hsdp=True,
hsdp_shard_size=4,
hsdp_replicate_size=1,
)
assert config.hsdp_shard_size == 4
def test_hsdp_explicit_shard_size_invalid(self):
"""Test that invalid HSDP dimensions raise an error when combined with other parallelism."""
with pytest.raises(ValueError, match="HSDP dimensions"):
DiffusionParallelConfig(
ulysses_degree=4, # world_size=4
use_hsdp=True,
hsdp_shard_size=3, # 1 * 3 != 4
hsdp_replicate_size=1,
)
def test_hsdp_replicate_size_exceeds_world_size(self):
"""Test that replicate_size > world_size raises an error."""
with pytest.raises(ValueError, match="replicate_size.*must evenly divide world_size"):
DiffusionParallelConfig(
ulysses_degree=4, # world_size=4
use_hsdp=True,
hsdp_replicate_size=8, # 8 > 4, invalid
)
def test_hsdp_combined_world_size(self):
"""Test that combined HSDP matches other parallelism world_size."""
config_no_hsdp = DiffusionParallelConfig(ulysses_degree=4)
config_with_hsdp = DiffusionParallelConfig(
ulysses_degree=4,
use_hsdp=True,
hsdp_shard_size=4,
)
# When combined with other parallelism, world_size should match
assert config_no_hsdp.world_size == config_with_hsdp.world_size == 4
def test_hsdp_standalone_world_size(self):
"""Test that standalone HSDP determines world_size."""
config_hsdp = DiffusionParallelConfig(
use_hsdp=True,
hsdp_shard_size=8,
)
# Standalone HSDP: world_size is determined by HSDP
assert config_hsdp.world_size == 8
def test_hsdp_cannot_use_with_tp(self):
"""Test that HSDP and Tensor Parallelism cannot be used together."""
with pytest.raises(ValueError, match="cannot be used with Tensor Parallelism"):
DiffusionParallelConfig(
tensor_parallel_size=2,
use_hsdp=True,
hsdp_shard_size=4,
)
def test_from_dict_with_hsdp(self):
"""Test creating config from dict with HSDP settings."""
config = DiffusionParallelConfig.from_dict(
{
"ulysses_degree": 4,
"use_hsdp": True,
"hsdp_replicate_size": 2,
}
)
assert config.use_hsdp is True
assert config.hsdp_replicate_size == 2
assert config.hsdp_shard_size == 2 # auto: 4 // 2
class TestHSDPShardConditions:
"""Tests for _hsdp_shard_conditions matching logic."""
@staticmethod
def _is_transformer_block(name: str, module: nn.Module) -> bool:
"""Example shard condition matching transformer blocks."""
return "blocks" in name and name.split(".")[-1].isdigit()
def test_condition_matches_blocks(self):
"""Test that condition matches transformer block patterns."""
cond = self._is_transformer_block
# Should match
assert cond("blocks.0", nn.Linear(10, 10)) is True
assert cond("blocks.15", nn.Linear(10, 10)) is True
assert cond("transformer.blocks.0", nn.Linear(10, 10)) is True
# Should not match
assert cond("blocks", nn.Linear(10, 10)) is False
assert cond("blocks.norm", nn.Linear(10, 10)) is False
assert cond("embeddings", nn.Linear(10, 10)) is False
def test_model_with_shard_conditions(self):
"""Test model with _hsdp_shard_conditions attribute."""
class MockModel(nn.Module):
@staticmethod
def _is_block(name: str, module: nn.Module) -> bool:
return name.startswith("blocks.") and name.split(".")[-1].isdigit()
_hsdp_shard_conditions = [_is_block]
def __init__(self):
super().__init__()
self.blocks = nn.ModuleList([nn.Linear(10, 10) for _ in range(2)])
model = MockModel()
conditions = getattr(model, "_hsdp_shard_conditions", None)
assert conditions is not None
assert len(conditions) == 1
# Verify conditions work on actual model modules
matched = []
for name, module in model.named_modules():
if any(cond(name, module) for cond in conditions):
matched.append(name)
assert "blocks.0" in matched
assert "blocks.1" in matched
@@ -54,6 +54,7 @@ def _make_runner(cache_backend, cache_backend_name: str, enable_cache_dit_summar
runner.od_config = SimpleNamespace(
cache_backend=cache_backend_name,
enable_cache_dit_summary=enable_cache_dit_summary,
parallel_config=SimpleNamespace(use_hsdp=False),
)
runner.kv_transfer_manager = SimpleNamespace(receive_kv_cache=lambda req, target_device: None)
return runner
+61 -4
View File
@@ -52,6 +52,15 @@ class DiffusionParallelConfig:
vae_patch_parallel_size: int = 1
"""Number of ranks used for VAE patch/tile parallelism (decode/encode)."""
use_hsdp: bool = False
"""Enable Hybrid Sharded Data Parallel (HSDP) for model weight sharding."""
hsdp_shard_size: int = -1
"""Number of GPUs to shard weights across within each replica group. -1 means auto-calculate."""
hsdp_replicate_size: int = 1
"""Number of replica groups for HSDP. Each replica holds a full sharded copy."""
@model_validator(mode="after")
def _validate_parallel_config(self) -> Self:
"""Validates the config relationships among the parallel strategies."""
@@ -68,13 +77,19 @@ class DiffusionParallelConfig:
"Sequence parallel size must be equal to the product of ulysses degree and ring degree,"
f" but got {self.sequence_parallel_size} != {self.ulysses_degree} * {self.ring_degree}"
)
# Validate HSDP configuration
if self.use_hsdp:
assert self.hsdp_replicate_size > 0, "HSDP replicate size must be > 0"
assert self.hsdp_shard_size > 0, "HSDP shard size must be > 0 (should be set in __post_init__)"
return self
def __post_init__(self) -> None:
if self.sequence_parallel_size is None:
self.sequence_parallel_size = self.ulysses_degree * self.ring_degree
self.world_size = (
# Calculate world_size from other parallelism dimensions
other_parallel_world_size = (
self.pipeline_parallel_size
* self.data_parallel_size
* self.tensor_parallel_size
@@ -83,6 +98,51 @@ class DiffusionParallelConfig:
* self.cfg_parallel_size
)
# Handle HSDP configuration
# HSDP can work in two modes:
# 1. Standalone: when other parallelism is all 1, HSDP determines world_size
# 2. Combined: HSDP overlays on top of other parallelism
if self.use_hsdp:
if self.tensor_parallel_size > 1:
raise ValueError(
"HSDP (use_hsdp=True) cannot be used with Tensor Parallelism "
f"(tensor_parallel_size={self.tensor_parallel_size}). "
"Set tensor_parallel_size=1 when using HSDP."
)
if self.hsdp_shard_size == -1:
# Auto-calculate: use other_parallel_world_size as shard_size
if self.hsdp_replicate_size <= 0:
raise ValueError("hsdp_replicate_size must be > 0")
if other_parallel_world_size == 1:
raise ValueError(
"Cannot auto-calculate hsdp_shard_size when other parallelism is all 1. "
"Please specify hsdp_shard_size explicitly for standalone HSDP."
)
if other_parallel_world_size % self.hsdp_replicate_size != 0:
raise ValueError(
f"Invalid HSDP configuration: replicate_size ({self.hsdp_replicate_size}) "
f"must evenly divide world_size ({other_parallel_world_size}) when shard_size is -1."
)
self.hsdp_shard_size = other_parallel_world_size // self.hsdp_replicate_size
self.world_size = other_parallel_world_size
else:
# Explicit shard_size: HSDP can work standalone or combined
hsdp_world_size = self.hsdp_replicate_size * self.hsdp_shard_size
if other_parallel_world_size == 1:
# Standalone HSDP: world_size is determined by HSDP
self.world_size = hsdp_world_size
else:
# Combined: HSDP must match other parallelism world_size
if hsdp_world_size != other_parallel_world_size:
raise ValueError(
f"HSDP dimensions "
f"({self.hsdp_replicate_size} × {self.hsdp_shard_size} = {hsdp_world_size}) "
f"must equal world_size from other parallelism ({other_parallel_world_size})"
)
self.world_size = other_parallel_world_size
else:
self.world_size = other_parallel_world_size
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "DiffusionParallelConfig":
"""
@@ -280,8 +340,6 @@ class OmniDiffusionConfig:
num_gpus: int | None = None
hsdp_replicate_dim: int = 1
hsdp_shard_dim: int = -1
dist_timeout: int | None = None # timeout for torch.distributed
# pipeline_config: PipelineConfig = field(default_factory=PipelineConfig, repr=False)
@@ -301,7 +359,6 @@ class OmniDiffusionConfig:
# Layer-wise offloading (block-level offloading) parameters
enable_layerwise_offload: bool = False
use_fsdp_inference: bool = False
pin_cpu_memory: bool = True # Use pinned memory for faster transfers when offloading
# VAE memory optimization parameters
@@ -2,6 +2,15 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Distributed utilities for vLLM-Omni diffusion models."""
from vllm_omni.diffusion.distributed.hsdp import (
HSDPInferenceConfig,
apply_hsdp_to_model,
)
from vllm_omni.diffusion.distributed.parallel_state import (
get_fs_group,
get_fully_shard_rank,
get_fully_shard_world_size,
)
from vllm_omni.diffusion.distributed.sp_plan import (
SequenceParallelConfig,
SequenceParallelInput,
@@ -35,4 +44,10 @@ __all__ = [
"sp_shard_with_padding",
"ShardingValidator",
"get_sharding_validator",
"HSDPInferenceConfig",
"apply_hsdp_to_model",
# FS (Fully Shard) utilities
"get_fs_group",
"get_fully_shard_rank",
"get_fully_shard_world_size",
]
+202
View File
@@ -0,0 +1,202 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
import torch
import torch.distributed
from torch import nn
from torch.distributed import DeviceMesh, init_device_mesh
from torch.distributed.fsdp import (
MixedPrecisionPolicy,
fully_shard,
)
from vllm.logger import init_logger
from vllm_omni.diffusion.distributed.parallel_state import (
get_fs_group,
get_fully_shard_rank,
get_fully_shard_world_size,
get_world_group,
)
from vllm_omni.platforms import current_omni_platform
logger = init_logger(__name__)
@dataclass
class HSDPInferenceConfig:
"""Configuration for HSDP inference.
This is a runtime config created from DiffusionParallelConfig's HSDP settings.
"""
enabled: bool = False
hsdp_replicate_size: int = 1
hsdp_shard_size: int = -1 # -1 = auto (shard across entire world)
param_dtype: torch.dtype = torch.bfloat16
reduce_dtype: torch.dtype = torch.float32
output_dtype: torch.dtype | None = None
reshard_after_forward: bool = True
def _create_hsdp_mesh(
device_type: str,
replicate_size: int,
shard_pg: torch.distributed.ProcessGroup,
) -> DeviceMesh:
"""Create a 2D DeviceMesh for HSDP using an existing ProcessGroup for the shard dimension.
Args:
device_type: The device type (e.g., "cuda", "npu")
replicate_size: Number of replica groups
shard_pg: The ProcessGroup for the shard dimension (from FS GroupCoordinator)
Returns:
A 2D DeviceMesh with dimensions ("replicate", "shard")
"""
shard_size = torch.distributed.get_world_size(shard_pg)
world_size = replicate_size * shard_size
# Build 2D mesh tensor: shape (replicate_size, shard_size)
# Ranks are arranged so that each row is a shard group
mesh_tensor = torch.arange(world_size).reshape(replicate_size, shard_size)
# Create DeviceMesh with the shard ProcessGroup
# For the shard dimension, we reuse the existing FS ProcessGroup
device_mesh = init_device_mesh(
device_type,
mesh_shape=(replicate_size, shard_size),
mesh_dim_names=("replicate", "shard"),
)
# Note: init_device_mesh creates new ProcessGroups internally.
# For consistency, we verify the mesh structure matches our FS group.
# In a future optimization, we could pass the existing ProcessGroups directly.
logger.debug(
"Created HSDP mesh: replicate_size=%d, shard_size=%d, mesh=%s",
replicate_size,
shard_size,
mesh_tensor.tolist(),
)
return device_mesh
def apply_hsdp_to_model(
model: nn.Module,
hsdp_config: HSDPInferenceConfig,
) -> nn.Module:
"""
Apply HSDP sharding to a model that already has weights loaded.
This function redistributes the model's parameters across GPUs using HSDP.
The model should already have its weights loaded via the standard load_weights method.
Args:
model: Model instance with weights already loaded
hsdp_config: HSDP configuration with HSDP mesh dimensions
Returns:
HSDP-wrapped model ready for inference
"""
if not hsdp_config.enabled:
raise ValueError("HSDP is not enabled in config")
# Use GroupCoordinator for distributed info
world_group = get_world_group()
fs_group = get_fs_group()
world_size = world_group.world_size
rank = world_group.rank_in_group
fs_world_size = get_fully_shard_world_size()
fs_rank = get_fully_shard_rank()
hsdp_replicate_size = hsdp_config.hsdp_replicate_size
hsdp_shard_size = hsdp_config.hsdp_shard_size
# Validate that the FS group matches the HSDP shard size
if fs_world_size != hsdp_shard_size:
raise ValueError(
f"FS group world_size ({fs_world_size}) does not match "
f"HSDP shard_size ({hsdp_shard_size}). "
"Ensure fully_shard_degree is set correctly in initialize_model_parallel."
)
logger.info(
"HSDP Inference: replicate_size=%d, shard_size=%d, world_size=%d, rank=%d, fs_world_size=%d, fs_rank=%d",
hsdp_replicate_size,
hsdp_shard_size,
world_size,
rank,
fs_world_size,
fs_rank,
)
mp_policy = MixedPrecisionPolicy(
param_dtype=hsdp_config.param_dtype,
reduce_dtype=hsdp_config.reduce_dtype,
output_dtype=hsdp_config.output_dtype,
cast_forward_inputs=False,
)
device_type = current_omni_platform.device_type
# Create 2D DeviceMesh for HSDP using the FS group's ProcessGroup for shard dimension
# The mesh shape is (replicate, shard) where:
# - replicate: groups of ranks that hold the same shard (for gradient all-reduce in training)
# - shard: groups of ranks that each hold different shards (for parameter all-gather)
device_mesh = _create_hsdp_mesh(
device_type=device_type,
replicate_size=hsdp_replicate_size,
shard_pg=fs_group.device_group,
)
hsdp_shard_conditions = getattr(model, "_hsdp_shard_conditions", None)
if not hsdp_shard_conditions or len(hsdp_shard_conditions) == 0:
raise ValueError(f"Model {type(model).__name__} has no _hsdp_shard_conditions defined")
# Apply HSDP sharding, this will automatically handle weight distribution
shard_model(
model,
reshard_after_forward=hsdp_config.reshard_after_forward,
mp_policy=mp_policy,
mesh=device_mesh,
hsdp_shard_conditions=hsdp_shard_conditions,
)
for param in model.parameters():
param.requires_grad = False
logger.info("HSDP applied to model: %s", type(model).__name__)
return model
def shard_model(
model: nn.Module,
*,
reshard_after_forward: bool = True,
mp_policy: MixedPrecisionPolicy | None = None,
mesh: DeviceMesh | None = None,
hsdp_shard_conditions: list[Callable[[str, nn.Module], bool]],
) -> None:
"""Apply HSDP sharding to model modules based on shard conditions."""
hsdp_kwargs: dict[str, Any] = {
"reshard_after_forward": reshard_after_forward,
"mesh": mesh,
"mp_policy": mp_policy,
}
num_sharded = 0
for name, module in reversed(list(model.named_modules())):
if any(cond(name, module) for cond in hsdp_shard_conditions):
fully_shard(module, **hsdp_kwargs)
num_sharded += 1
if num_sharded == 0:
raise ValueError("No modules were sharded")
fully_shard(model, **hsdp_kwargs)
logger.info("Sharded %d modules + root", num_sharded)
@@ -56,6 +56,7 @@ _SP: SequenceParallelGroupCoordinator | None = None
_PP: PipelineGroupCoordinator | None = None
_CFG: GroupCoordinator | None = None
_DP: GroupCoordinator | None = None
_FS: GroupCoordinator | None = None # Fully Sharded (HSDP shard dimension)
_DIT: GroupCoordinator | None = None
@@ -175,7 +176,8 @@ class RankGenerator:
pp: int,
cfg: int,
dp: int,
order: str,
fs: int = 1,
order: str = "tp-sp-pp-cfg-dp",
rank_offset: int = 0,
) -> None:
self.tp = tp
@@ -183,6 +185,7 @@ class RankGenerator:
self.pp = pp
self.cfg = cfg
self.dp = dp
self.fs = fs
self.rank_offset = rank_offset
self.world_size = tp * sp * pp * cfg * dp
@@ -192,14 +195,19 @@ class RankGenerator:
"pp": self.pp,
"cfg": self.cfg,
"dp": self.dp,
"fs": self.fs,
}
order = order.lower()
for name in self.name_to_size.keys():
# Skip 'fs' validation - it's handled separately with independent_ranks=True
# and doesn't participate in the main orthogonal rank generation
if name == "fs":
continue
if name not in order and self.name_to_size[name] != 1:
raise RuntimeError(
f"The size of ({name}) is ({self.name_to_size[name]}), "
f"but you haven't specified the order ({self.order})."
f"but you haven't specified the order ({order})."
)
elif name not in order:
order = order + "-" + name
@@ -218,7 +226,7 @@ class RankGenerator:
mask[ordered_token.index(t)] = True
return mask
def get_ranks(self, token):
def get_ranks(self, token, independent_ranks: bool = False):
"""Get rank group by input token.
Arguments:
@@ -227,7 +235,21 @@ class RankGenerator:
to obtain multiple parallel types, we can use a hyphen
'-' to separate them. For example, if we want to obtain
the TP_DP group, the token should be 'tp-dp'.
independent_ranks (bool):
If True, generate independent rank groups that divide the world
into groups of the specified size. Used for FS (fully shard) groups
which operate independently from the main parallelism hierarchy.
"""
if independent_ranks and token == "fs":
# FS groups divide world into groups of size fs
# e.g., world_size=8, fs=4 -> [[0,1,2,3], [4,5,6,7]]
ranks = []
num_groups = self.world_size // self.fs
for i in range(num_groups):
group = list(range(i * self.fs + self.rank_offset, (i + 1) * self.fs + self.rank_offset))
ranks.append(group)
return ranks
mask = self.get_mask(self.order, token)
ranks = generate_masked_orthogonal_rank_groups(self.world_size, self.ordered_size, mask)
if self.rank_offset > 0:
@@ -333,6 +355,22 @@ def get_data_parallel_rank():
return get_dp_group().rank_in_group
# FS (Fully Shard / HSDP shard dimension)
def get_fs_group() -> GroupCoordinator:
assert _FS is not None, "fully shard group is not initialized"
return _FS
def get_fully_shard_world_size():
"""Return world size for the fully shard group."""
return get_fs_group().world_size
def get_fully_shard_rank():
"""Return my rank for the fully shard group."""
return get_fs_group().rank_in_group
def is_dp_last_group():
"""Return True if in the last data parallel group, False otherwise."""
return (
@@ -438,6 +476,7 @@ def init_model_parallel_group(
"tensor",
"sequence",
"classifier_free_guidance",
"fully_shard",
], f"parallel_mode {parallel_mode} is not supported"
if parallel_mode == "pipeline":
return PipelineGroupCoordinator(
@@ -629,6 +668,7 @@ def initialize_model_parallel(
ring_degree: int = 1,
tensor_parallel_size: int = 1,
pipeline_parallel_size: int = 1,
fully_shard_degree: int = 1,
backend: str | None = None,
) -> None:
if backend is None:
@@ -645,6 +685,7 @@ def initialize_model_parallel(
ring_degree: number of GPUs used for ring sequence parallelism.
tensor_parallel_size: number of GPUs used for tensor parallelism.
pipeline_parallel_size: number of GPUs used for pipeline parallelism.
fully_shard_degree: number of GPUs used for fully sharded data parallelism (HSDP shard dimension).
backend: distributed backend of pytorch collective comm.
Let's say we have a total of 16 GPUs denoted by g0 ... g15 and we
@@ -716,7 +757,8 @@ def initialize_model_parallel(
pipeline_parallel_size,
cfg_parallel_size,
data_parallel_size,
"tp-sp-pp-cfg-dp",
fs=fully_shard_degree,
order="tp-sp-pp-cfg-dp",
)
sp_group_ranks = rank_generator.get_ranks("sp")
global _DP
@@ -772,6 +814,16 @@ def initialize_model_parallel(
backend=backend,
parallel_mode="tensor",
)
global _FS
assert _FS is None, "fully shard group is already initialized"
_FS = init_model_parallel_group(
group_ranks=rank_generator.get_ranks("fs", independent_ranks=True),
local_rank=get_world_group().local_rank,
backend=backend,
parallel_mode="fully_shard",
)
init_dit_group(dit_parallel_size, backend)
@@ -801,6 +853,11 @@ def destroy_model_parallel():
_PP.destroy()
_PP = None
global _FS
if _FS:
_FS.destroy()
_FS = None
def destroy_distributed_environment():
global _WORLD
@@ -26,6 +26,7 @@ from vllm.utils.import_utils import resolve_obj_by_qualname
from vllm.utils.torch_utils import set_default_torch_dtype
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.diffusion.distributed.hsdp import HSDPInferenceConfig
from vllm_omni.diffusion.registry import initialize_model
logger = init_logger(__name__)
@@ -219,15 +220,20 @@ class DiffusersPipelineLoader:
"""Load a model with the given configurations."""
target_device = torch.device(load_device)
with set_default_torch_dtype(od_config.dtype):
with target_device:
if load_format == "default":
model = initialize_model(od_config)
elif load_format == "custom_pipeline":
model_cls = resolve_obj_by_qualname(custom_pipeline_name)
model = model_cls(od_config=od_config)
logger.debug("Loading weights on %s ...", load_device)
# Quantization does not happen in `load_weights` but after it
self.load_weights(model)
if od_config.parallel_config.use_hsdp:
model = self._load_model_with_hsdp(
od_config, load_format=load_format, custom_pipeline_name=custom_pipeline_name
)
else:
with target_device:
if load_format == "default":
model = initialize_model(od_config)
elif load_format == "custom_pipeline":
model_cls = resolve_obj_by_qualname(custom_pipeline_name)
model = model_cls(od_config=od_config)
logger.debug("Loading weights on %s ...", load_device)
# Quantization does not happen in `load_weights` but after it
self.load_weights(model)
# Process weights after loading for quantization (e.g., FP8 online quantization)
# This is needed for vLLM's quantization methods that need to transform weights
@@ -278,3 +284,57 @@ class DiffusersPipelineLoader:
# "Following weights were not initialized from "
# f"checkpoint: {weights_not_loaded}"
# )
def _load_model_with_hsdp(
self,
od_config: OmniDiffusionConfig,
load_format: str = "default",
custom_pipeline_name: str | None = None,
) -> nn.Module:
"""Load model with HSDP sharding for inference.
The pipeline contains multiple components (text_encoder, VAE, transformer).
Only the transformer is sharded with HSDP. Other components are loaded normally.
Approach: Load weights first using model's load_weights (handles QKV fusion etc.),
then apply HSDP sharding to redistribute weights across GPUs.
"""
from vllm_omni.diffusion.distributed.hsdp import apply_hsdp_to_model
parallel_config = od_config.parallel_config
hsdp_config = HSDPInferenceConfig(
enabled=True,
hsdp_replicate_size=parallel_config.hsdp_replicate_size,
hsdp_shard_size=parallel_config.hsdp_shard_size,
param_dtype=od_config.dtype,
)
# Initialize model WITHOUT device context (weights start on CPU).
# Unlike the non-HSDP path which uses `with target_device:` to create weights
# directly on GPU, HSDP needs weights on CPU first so they can be redistributed
# across GPUs by apply_hsdp_to_model. The model's load_weights handles weight
# mapping (QKV fusion, etc.).
if load_format == "default":
model = initialize_model(od_config)
elif load_format == "custom_pipeline":
model_cls = resolve_obj_by_qualname(custom_pipeline_name)
model = model_cls(od_config=od_config)
self.load_weights(model)
# Collect all transformers to shard (some models have transformer_2 for MoE)
transformers_to_shard = []
transformer = getattr(model, "transformer", None)
if transformer is None:
raise ValueError("Model has no transformer attribute for HSDP")
transformers_to_shard.append(("transformer", transformer))
# Check for transformer_2 (MoE two-stage models like Wan2.2-I2V)
transformer_2 = getattr(model, "transformer_2", None)
if transformer_2 is not None:
transformers_to_shard.append(("transformer_2", transformer_2))
# Apply HSDP sharding to all transformers
for name, trans in transformers_to_shard:
logger.debug("Applying HSDP to %s", name)
apply_hsdp_to_model(trans, hsdp_config)
return model
@@ -725,6 +725,13 @@ class WanTransformer3DModel(nn.Module):
"to_qkv": ["to_q", "to_k", "to_v"],
}
@staticmethod
def _is_transformer_block(name: str, module) -> bool:
"""Match transformer blocks for HSDP sharding (e.g., blocks.0, blocks.1)."""
return "blocks" in name and name.split(".")[-1].isdigit()
_hsdp_shard_conditions = [_is_transformer_block]
# Sequence Parallelism for Wan (following diffusers' _cp_plan pattern)
#
# The _sp_plan specifies sharding/gathering at module boundaries:
@@ -166,7 +166,6 @@ class DiffusionModelRunner:
"""Load weights into the pipeline."""
return self.pipeline.load_weights(weights)
@torch.inference_mode()
def execute_model(self, req: OmniDiffusionRequest) -> DiffusionOutput:
"""
Execute a forward pass for the given requests.
@@ -176,34 +175,44 @@ class DiffusionModelRunner:
Returns:
DiffusionOutput with generated results.
Note:
We use torch.no_grad() for HSDP because HSDP2's fully_shard requires access
to tensor version counters in pre_forward hooks, which inference tensors do
not track. For non-HSDP inference, we use torch.inference_mode() for better
performance.
"""
assert self.pipeline is not None, "Model not loaded. Call load_model() first."
if len(req.prompts) == 0:
raise ValueError("Cannot execute model with empty request list")
# The manager handles the check for need_recv_cache internally
self.kv_transfer_manager.receive_kv_cache(req, target_device=getattr(self.pipeline, "device", None))
# Use no_grad() for HSDP compatibility, inference_mode() otherwise for better perf
use_hsdp = self.od_config.parallel_config.use_hsdp
grad_context = torch.no_grad() if use_hsdp else torch.inference_mode()
with grad_context:
# The manager handles the check for need_recv_cache internally
self.kv_transfer_manager.receive_kv_cache(req, target_device=getattr(self.pipeline, "device", None))
if req.sampling_params.generator is None and req.sampling_params.seed is not None:
if req.sampling_params.generator_device is not None:
gen_device = req.sampling_params.generator_device
elif self.device.type == "cpu":
gen_device = "cpu"
else:
gen_device = self.device
req.sampling_params.generator = torch.Generator(device=gen_device).manual_seed(req.sampling_params.seed)
if req.sampling_params.generator is None and req.sampling_params.seed is not None:
if req.sampling_params.generator_device is not None:
gen_device = req.sampling_params.generator_device
elif self.device.type == "cpu":
gen_device = "cpu"
else:
gen_device = self.device
req.sampling_params.generator = torch.Generator(device=gen_device).manual_seed(req.sampling_params.seed)
# Refresh cache context if needed
if (
not getattr(req, "skip_cache_refresh", False)
and self.cache_backend is not None
and self.cache_backend.is_enabled()
):
self.cache_backend.refresh(self.pipeline, req.sampling_params.num_inference_steps)
# Refresh cache context if needed
if (
not getattr(req, "skip_cache_refresh", False)
and self.cache_backend is not None
and self.cache_backend.is_enabled()
):
self.cache_backend.refresh(self.pipeline, req.sampling_params.num_inference_steps)
with set_forward_context(vllm_config=self.vllm_config, omni_diffusion_config=self.od_config):
with record_function("pipeline_forward"):
output = self.pipeline.forward(req)
with set_forward_context(vllm_config=self.vllm_config, omni_diffusion_config=self.od_config):
with record_function("pipeline_forward"):
output = self.pipeline.forward(req)
# NOTE:
if (
@@ -214,4 +223,4 @@ class DiffusionModelRunner:
):
cache_summary(self.pipeline, details=True)
return output
return output
@@ -122,6 +122,7 @@ class DiffusionWorker:
ring_degree=parallel_config.ring_degree,
tensor_parallel_size=parallel_config.tensor_parallel_size,
pipeline_parallel_size=parallel_config.pipeline_parallel_size,
fully_shard_degree=parallel_config.hsdp_shard_size if parallel_config.use_hsdp else 1,
)
init_workspace_manager(self.device)
+15 -1
View File
@@ -141,9 +141,20 @@ class AsyncOmni(OmniBase):
sequence_parallel_size = kwargs.get("sequence_parallel_size")
tensor_parallel_size = kwargs.get("tensor_parallel_size") or 1
cfg_parallel_size = kwargs.get("cfg_parallel_size") or 1
use_hsdp = kwargs.get("use_hsdp", False)
hsdp_shard_size = kwargs.get("hsdp_shard_size", -1)
hsdp_replicate_size = kwargs.get("hsdp_replicate_size", 1)
if sequence_parallel_size is None:
sequence_parallel_size = ulysses_degree * ring_degree
num_devices = sequence_parallel_size * tensor_parallel_size * cfg_parallel_size
# Calculate num_devices: consider standalone HSDP
other_parallel_size = sequence_parallel_size * tensor_parallel_size * cfg_parallel_size
if use_hsdp and other_parallel_size == 1 and hsdp_shard_size > 0:
# Standalone HSDP: num_devices is determined by HSDP dimensions
num_devices = hsdp_shard_size * hsdp_replicate_size
else:
num_devices = other_parallel_size
for i in range(1, num_devices):
devices += f",{i}"
parallel_config = DiffusionParallelConfig(
@@ -154,6 +165,9 @@ class AsyncOmni(OmniBase):
ulysses_degree=ulysses_degree,
ring_degree=ring_degree,
cfg_parallel_size=cfg_parallel_size,
use_hsdp=use_hsdp,
hsdp_shard_size=hsdp_shard_size,
hsdp_replicate_size=hsdp_replicate_size,
)
default_stage_cfg = [
{
+21
View File
@@ -210,6 +210,27 @@ class OmniServeCommand(CLISubcommand):
"Equivalent to setting DiffusionParallelConfig.ring_degree.",
)
# HSDP (Hybrid Sharded Data Parallel) parameters
omni_config_group.add_argument(
"--use-hsdp",
dest="use_hsdp",
action="store_true",
help="Enable HSDP (Hybrid Sharded Data Parallel) for diffusion models. "
"Shards model weights across GPUs to reduce per-GPU memory usage.",
)
omni_config_group.add_argument(
"--hsdp-shard-size",
type=int,
default=-1,
help="Number of GPUs to shard weights across. -1 = auto (world_size / replicate_size).",
)
omni_config_group.add_argument(
"--hsdp-replicate-size",
type=int,
default=1,
help="Number of replica groups for HSDP. Each group holds a full sharded copy.",
)
# Cache optimization parameters
omni_config_group.add_argument(
"--cache-backend",