[Perf]: CFG parallel abstraction (#851)

Signed-off-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
Didan DengandHongsheng Liu authored and GitHub committed 2026-01-30 16:23:44 +00:00
1 parent 8ad39570b8
commit 23cf48d7bb
31 files changed
+1506 -814

No files matched your search

@@ -373,9 +373,9 @@ def forward(self, hidden_states, ...):
### CFG-Parallel
##### Offline Inference
#### Offline Inference
CFG-Parallel is enabled through `DiffusionParallelConfig(cfg_parallel_size=...)`. The recommended configuration is `cfg_parallel_size=2` (one rank for the positive branch and one rank for the negative branch).
CFG-Parallel is enabled through `DiffusionParallelConfig(cfg_parallel_size=2)`, which runs one rank for the positive branch and one rank for the negative branch.
An example of offline inference using CFG-Parallel (image-to-image) is shown below:
@@ -383,19 +383,21 @@ An example of offline inference using CFG-Parallel (image-to-image) is shown bel
from vllm_omni import Omni
from vllm_omni.diffusion.data import DiffusionParallelConfig
image_path = "path_to_image.png"
omni = Omni(
model="Qwen/Qwen-Image-Edit",
parallel_config=DiffusionParallelConfig(cfg_parallel_size=2),
)
input_image = Image.open(image_path).convert("RGB")
outputs = omni.generate(
{
"prompt": "turn this cat to a dog",
"negative_prompt": "low quality, blurry",
"multi_modal_data": {"image": input_image},
},
OmniDiffusionSamplingParams(
true_cfg_scale=4.0,
pil_image=input_image,
num_inference_steps=50,
),
)
@@ -403,7 +405,28 @@ outputs = omni.generate(
Notes:
- CFG-Parallel is only effective when **true CFG** is enabled (i.e., `true_cfg_scale > 1` and a `negative_prompt` is provided).
- CFG-Parallel is only effective when a `negative_prompt` is provided AND a guidance scale (or `cfg_scale`) is greater than 1.
See `examples/offline_inference/image_to_image/image_edit.py` for a complete working example.
```bash
cd examples/offline_inference/image_to_image/
python image_edit.py \
--model "Qwen/Qwen-Image-Edit" \
--image "qwen_image_output.png" \
--prompt "turn this cat to a dog" \
--negative_prompt "low quality, blurry" \
--cfg_scale 4.0 \
--output "edited_image.png" \
--cfg_parallel_size 2
```
#### Online Serving
You can enable CFG-Parallel in online serving for diffusion models via `--cfg-parallel-size`:
```bash
vllm serve Qwen/Qwen-Image-Edit --omni --port 8091 --cfg-parallel-size 2
```
#### How to parallelize a pipeline
@@ -416,58 +439,130 @@ In `QwenImagePipeline`, each diffusion step runs two denoiser forward passes seq
CFG-Parallel assigns these two branches to different ranks in the **CFG group** and synchronizes the results.
Below is an example of CFG-Parallel implementation:
vLLM-omni provides `CFGParallelMixin` base class that encapsulates the CFG parallel logic. By inheriting from this mixin and calling its methods, pipelines can easily implement CFG parallel without writing repetitive code.
**Key Methods in CFGParallelMixin:**
- `predict_noise_maybe_with_cfg()`: Automatically handles CFG parallel noise prediction
- `scheduler_step_maybe_with_cfg()`: Scheduler step with automatic CFG rank synchronization
**Example Implementation:**
```python
def diffuse(
class QwenImageCFGParallelMixin(CFGParallelMixin):
"""
Base Mixin class for Qwen Image pipelines providing shared CFG methods.
"""
def diffuse(
self,
...
):
# Enable CFG-parallel: rank0 computes positive, rank1 computes negative.
cfg_parallel_ready = do_true_cfg and get_classifier_free_guidance_world_size() > 1
prompt_embeds: torch.Tensor,
prompt_embeds_mask: torch.Tensor,
negative_prompt_embeds: torch.Tensor,
negative_prompt_embeds_mask: torch.Tensor,
latents: torch.Tensor,
img_shapes: torch.Tensor,
txt_seq_lens: torch.Tensor,
negative_txt_seq_lens: torch.Tensor,
timesteps: torch.Tensor,
do_true_cfg: bool,
guidance: torch.Tensor,
true_cfg_scale: float,
image_latents: torch.Tensor | None = None,
cfg_normalize: bool = True,
additional_transformer_kwargs: dict[str, Any] | None = None,
) -> torch.Tensor:
self.transformer.do_true_cfg = do_true_cfg
self.transformer.do_true_cfg = do_true_cfg
for i, t in enumerate(timesteps):
timestep = t.expand(latents.shape[0]).to(device=latents.device, dtype=latents.dtype)
if cfg_parallel_ready:
cfg_group = get_cfg_group()
cfg_rank = get_classifier_free_guidance_rank()
# Prepare kwargs for positive (conditional) prediction
positive_kwargs = {
"hidden_states": latents,
"timestep": timestep / 1000,
"guidance": guidance,
"encoder_hidden_states_mask": prompt_embeds_mask,
"encoder_hidden_states": prompt_embeds,
"img_shapes": img_shapes,
"txt_seq_lens": txt_seq_lens,
}
if cfg_rank == 0:
local_pred = self.transformer(
hidden_states=latents,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=prompt_embeds_mask,
encoder_hidden_states=prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=txt_seq_lens,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
else:
local_pred = self.transformer(
hidden_states=latents,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=negative_prompt_embeds_mask,
encoder_hidden_states=negative_prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=negative_txt_seq_lens,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
# Prepare kwargs for negative (unconditional) prediction
if do_true_cfg:
negative_kwargs = {
"hidden_states": latents,
"timestep": timestep / 1000,
"guidance": guidance,
"encoder_hidden_states_mask": negative_prompt_embeds_mask,
"encoder_hidden_states": negative_prompt_embeds,
"img_shapes": img_shapes,
"txt_seq_lens": negative_txt_seq_lens,
}
else:
negative_kwargs = None
gathered = cfg_group.all_gather(local_pred, separate_tensors=True)
if cfg_rank == 0:
noise_pred = gathered[0]
neg_noise_pred = gathered[1]
comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
noise_pred = comb_pred * (cond_norm / noise_norm)
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
cfg_group.broadcast(latents, src=0)
else:
# fallback: run positive then negative sequentially on one rank
...
# Predict noise with automatic CFG parallel handling
# - In CFG parallel mode: rank0 computes positive, rank1 computes negative
# - Automatically gathers results and combines them on rank0
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,
cfg_normalize=cfg_normalize,
)
# Step scheduler with automatic CFG synchronization
# - Only rank0 computes the scheduler step
# - Automatically broadcasts updated latents to all ranks
latents = self.scheduler_step_maybe_with_cfg(
noise_pred, t, latents, do_true_cfg
)
return latents
```
**How it works:**
1. Prepare separate `positive_kwargs` and `negative_kwargs` for conditional and unconditional predictions
2. Call `predict_noise_maybe_with_cfg()` which:
- Detects if CFG parallel is enabled (`get_classifier_free_guidance_world_size() > 1`)
- Distributes computation: rank0 processes positive, rank1 processes negative
- Gathers predictions and combines them using `combine_cfg_noise()` on rank0
- Returns combined noise prediction (only valid on rank0)
3. Call `scheduler_step_maybe_with_cfg()` which:
- Only rank0 computes the scheduler step
- Broadcasts the updated latents to all ranks for synchronization
**How to customize**
Some pipelines may need to customize the following functions in `CFGParallelMixin`:
1. You may need to edit `predict_noise` function for custom behaviors.
```python
def predict_noise(self, *args, **kwargs):
"""
Forward pass through transformer to predict noise.
Subclasses should override this if they need custom behavior,
but the default implementation calls self.transformer.
"""
return self.transformer(*args, **kwargs)[0]
```
2. The default normalization function after combining the noise predictions from both branches is as follows. You may need to customize it.
```python
def cfg_normalize_function(self, noise_pred, comb_pred):
"""
Normalize the combined noise prediction.
Args:
noise_pred: positive noise prediction
comb_pred: combined noise prediction after CFG
Returns:
Normalized noise prediction tensor
"""
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
noise_pred = comb_pred * (cond_norm / noise_norm)
return noise_pred
```
+7 -7
View File
@@ -39,23 +39,23 @@ The following table shows which models are currently supported by each accelerat
| Model | Model Identifier | TeaCache | Cache-DiT | Ulysses-SP | Ring-Attention | CFG-Parallel |
|-------|------------------|:----------:|:-----------:|:-----------:|:----------------:|:----------------:|
| **LongCat-Image** | `meituan-longcat/LongCat-Image` | ❌ | ✅ | ❌ | ❌ | ❌ |
| **LongCat-Image-Edit** | `meituan-longcat/LongCat-Image-Edit` | ❌ | ✅ | ❌ | ❌ | ❌ |
| **Ovis-Image** | `OvisAI/Ovis-Image` | ❌ | ✅ | ❌ | ❌ | ❌ |
| **LongCat-Image** | `meituan-longcat/LongCat-Image` | ❌ | ✅ | ❌ | ❌ | ✅ |
| **LongCat-Image-Edit** | `meituan-longcat/LongCat-Image-Edit` | ❌ | ✅ | ❌ | ❌ | ✅ |
| **Ovis-Image** | `OvisAI/Ovis-Image` | ❌ | ✅ | ❌ | ❌ | ✅ |
| **Qwen-Image** | `Qwen/Qwen-Image` | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Qwen-Image-2512** | `Qwen/Qwen-Image-2512` | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Qwen-Image-Edit** | `Qwen/Qwen-Image-Edit` | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Qwen-Image-Edit-2509** | `Qwen/Qwen-Image-Edit-2509` | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Qwen-Image-Layered** | `Qwen/Qwen-Image-Layered` | ❌ | ✅ | ✅ | ✅ | ✅ |
| **Z-Image** | `Tongyi-MAI/Z-Image-Turbo` | ✅ | ✅ | ❌ | ❌ | ❌ |
| **Stable-Diffusion3.5** | `stabilityai/stable-diffusion-3.5` | ❌ | ✅ | ❌ | ❌ | ❌ |
| **Stable-Diffusion3.5** | `stabilityai/stable-diffusion-3.5` | ❌ | ✅ | ❌ | ❌ | ✅ |
| **Bagel** | `ByteDance-Seed/BAGEL-7B-MoT` | ✅ | ✅ | ❌ | ❌ | ❌ |
### VideoGen
| Model | Model Identifier | TeaCache | Cache-DiT | Ulysses-SP | Ring-Attention |
|-------|------------------|:--------:|:---------:|:----------:|:--------------:|
| **Wan2.2** | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | ❌ | ✅ | ❌ | ❌ |
| Model | Model Identifier | TeaCache | Cache-DiT | Ulysses-SP | Ring-Attention |CFG-Parallel |
|-------|------------------|:--------:|:---------:|:----------:|:--------------:|:----------------:|
| **Wan2.2** | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | ❌ | ✅ | ✅ | ✅ | ✅ |
## Performance Benchmarks
@@ -47,6 +47,7 @@ Key arguments:
- `--image`: path(s) to the source image(s) (PNG/JPG, converted to RGB). Can specify multiple images.
- `--prompt` / `--negative_prompt`: text description (string).
- `--cfg_scale`: true classifier-free guidance scale (default: 4.0). Classifier-free guidance is enabled by setting cfg_scale > 1 and providing a negative_prompt. Higher guidance scale encourages images closely linked to the text prompt, usually at the expense of lower image quality.
- `--cfg_parallel_size`: the number of devices to run CFG Parallel. CFG Parallel is valid only if classifier-free guidance is enabled and `cfg_parallel_size` is set to 2.
- `--guidance_scale`: guidance scale for guidance-distilled models (default: 1.0, disabled). Unlike classifier-free guidance (--cfg_scale), guidance-distilled models take the guidance scale directly as an input parameter. Enabled when guidance_scale > 1. Ignored when not using guidance-distilled models.
- `--num_inference_steps`: diffusion sampling steps (more steps = higher quality, slower).
- `--output`: path to save the generated PNG.
@@ -52,6 +52,7 @@ Key arguments:
- `--num_frames`: Number of frames (default 81).
- `--guidance_scale` and `--guidance_scale_high`: CFG scale (applied to low/high-noise stages for MoE).
- `--negative_prompt`: Optional list of artifacts to suppress.
- `--cfg_parallel_size`: the number of devices to run CFG Parallel. CFG Parallel is valid only if classifier-free guidance is enabled and `cfg_parallel_size` is set to 2.
- `--boundary_ratio`: Boundary split ratio for two-stage MoE models.
- `--flow_shift`: Scheduler flow shift (5.0 for 720p, 12.0 for 480p).
- `--num_inference_steps`: Number of denoising steps (default 50).
@@ -95,6 +95,7 @@ Key arguments:
- `--prompt`: text description (string).
- `--seed`: integer seed for deterministic sampling.
- `--cfg_scale`: true CFG scale (model-specific guidance strength).
- `--cfg_parallel_size`: the number of devices to run CFG Parallel. CFG Parallel is valid only if classifier-free guidance is enabled and `cfg_parallel_size` is set to 2.
- `--num_images_per_prompt`: number of images to generate per prompt (saves as `output`, `output_1`, ...).
- `--num_inference_steps`: diffusion sampling steps (more steps = higher quality, slower).
- `--height/--width`: output resolution (defaults 1024x1024).
@@ -28,6 +28,7 @@ Key arguments:
- `--num_frames`: Number of frames (Wan default is 81).
- `--guidance_scale` and `--guidance_scale_high`: CFG scale (applied to low/high)..
- `--negative_prompt`: optional list of artifacts to suppress (the PR demo used a long Chinese string).
- `--cfg_parallel_size`: the number of devices to run CFG Parallel. CFG Parallel is valid only if classifier-free guidance is enabled and `cfg_parallel_size` is set to 2.
- `--boundary_ratio`: Boundary split ratio for low/high DiT.
- `--fps`: frames per second for the saved MP4 (requires `diffusers` export_to_video).
- `--output`: path to save the generated video.
@@ -49,6 +49,7 @@ Key arguments:
- `--output`: path to save the generated PNG.
- `--vae_use_slicing`: enable VAE slicing for memory optimization.
- `--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.
> ℹ️ If you encounter OOM errors, try using `--vae_use_slicing` and `--vae_use_tiling` to reduce memory usage.
@@ -56,6 +56,7 @@ Key arguments:
- `--output`: Path to save the generated video.
- `--vae_use_slicing`: Enable VAE slicing for memory optimization.
- `--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.
> ℹ️ If you encounter OOM errors, try using `--vae_use_slicing` and `--vae_use_tiling` to reduce memory usage.
@@ -26,6 +26,7 @@ import numpy as np
import PIL.Image
import torch
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.outputs import OmniRequestOutput
@@ -85,6 +86,18 @@ def parse_args() -> argparse.Namespace:
default=1,
help="Number of ready layers (blocks) to keep on GPU during generation.",
)
parser.add_argument(
"--cfg_parallel_size",
type=int,
default=1,
choices=[1, 2],
help="Number of GPUs used for classifier free guidance parallel size.",
)
parser.add_argument(
"--enforce_eager",
action="store_true",
help="Disable torch.compile and force eager execution.",
)
return parser.parse_args()
@@ -120,7 +133,9 @@ def main():
# Check if profiling is requested via environment variable
profiler_enabled = bool(os.getenv("VLLM_TORCH_PROFILER_DIR"))
parallel_config = DiffusionParallelConfig(
cfg_parallel_size=args.cfg_parallel_size,
)
omni = Omni(
model=args.model,
enable_layerwise_offload=args.enable_layerwise_offload,
@@ -130,12 +145,24 @@ def main():
boundary_ratio=args.boundary_ratio,
flow_shift=args.flow_shift,
enable_cpu_offload=args.enable_cpu_offload,
parallel_config=parallel_config,
enforce_eager=args.enforce_eager,
)
if profiler_enabled:
print("[Profiler] Starting profiling...")
omni.start_profile()
# Print generation configuration
print(f"\n{'=' * 60}")
print("Generation Configuration:")
print(f" Model: {args.model}")
print(f" Inference steps: {args.num_inference_steps}")
print(f" Frames: {args.num_frames}")
print(f" Parallel configuration: cfg_parallel_size={args.cfg_parallel_size}")
print(f" Video size: {args.width}x{args.height}")
print(f"{'=' * 60}\n")
# omni.generate() returns Generator[OmniRequestOutput, None, None]
frames = omni.generate(
{
@@ -98,6 +98,7 @@ Key arguments:
- `--output`: path to save the generated PNG.
- `--vae_use_slicing`: enable VAE slicing for memory optimization.
- `--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.
> ℹ️ If you encounter OOM errors, try using `--vae_use_slicing` and `--vae_use_tiling` to reduce memory usage.
@@ -31,6 +31,7 @@ Key arguments:
- `--output`: path to save the generated video.
- `--vae_use_slicing`: enable VAE slicing for memory optimization.
- `--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.
> ℹ️ If you encounter OOM errors, try using `--vae_use_slicing` and `--vae_use_tiling` to reduce memory usage.
@@ -102,6 +102,14 @@ def parse_args() -> argparse.Namespace:
default=1,
help="Number of GPUs used for ring sequence parallelism.",
)
parser.add_argument(
"--cfg_parallel_size",
type=int,
default=1,
choices=[1, 2],
help="Number of GPUs used for classifier free guidance parallel size.",
)
return parser.parse_args()
@@ -132,6 +140,7 @@ def main():
parallel_config = DiffusionParallelConfig(
ulysses_degree=args.ulysses_degree,
ring_degree=args.ring_degree,
cfg_parallel_size=args.cfg_parallel_size,
)
# Check if profiling is requested via environment variable
@@ -163,7 +172,9 @@ def main():
print(f" Model: {args.model}")
print(f" Inference steps: {args.num_inference_steps}")
print(f" Frames: {args.num_frames}")
print(f" Parallel configuration: ulysses_degree={args.ulysses_degree}, ring_degree={args.ring_degree}")
print(
f" Parallel configuration: ulysses_degree={args.ulysses_degree}, ring_degree={args.ring_degree}, cfg_parallel_size={args.cfg_parallel_size}"
)
print(f" Video size: {args.width}x{args.height}")
print(f"{'=' * 60}\n")
@@ -0,0 +1,423 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for CFG (Classifier-Free Guidance) parallel functionality.
This test verifies that predict_noise_maybe_with_cfg produces numerically
equivalent results with and without CFG parallel using fixed random inputs.
"""
import os
import pytest
import torch
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.parallel_state import (
destroy_distributed_env,
get_classifier_free_guidance_rank,
get_classifier_free_guidance_world_size,
init_distributed_environment,
initialize_model_parallel,
)
from vllm_omni.platforms import current_omni_platform
def update_environment_variables(envs_dict: dict[str, str]):
"""Update multiple environment variables."""
for k, v in envs_dict.items():
os.environ[k] = v
class SimpleTransformer(torch.nn.Module):
"""Simple transformer model for testing with random initialization.
Contains:
- Input projection (conv to hidden_dim)
- QKV projection layers
- Self-attention layer
- Output projection
"""
def __init__(self, in_channels: int = 4, hidden_dim: int = 128, num_heads: int = 8):
super().__init__()
self.hidden_dim = hidden_dim
self.num_heads = num_heads
self.head_dim = hidden_dim // num_heads
assert hidden_dim % num_heads == 0, "hidden_dim must be divisible by num_heads"
# Input projection: (B, C, H, W) -> (B, hidden_dim, H, W)
self.input_proj = torch.nn.Conv2d(in_channels, hidden_dim, 1)
# QKV projection layers
self.q_proj = torch.nn.Linear(hidden_dim, hidden_dim)
self.k_proj = torch.nn.Linear(hidden_dim, hidden_dim)
self.v_proj = torch.nn.Linear(hidden_dim, hidden_dim)
# Output projection after attention
self.out_proj = torch.nn.Linear(hidden_dim, hidden_dim)
# Final output projection: (B, hidden_dim, H, W) -> (B, C, H, W)
self.final_proj = torch.nn.Conv2d(hidden_dim, in_channels, 1)
# Layer norm
self.norm1 = torch.nn.LayerNorm(hidden_dim)
self.norm2 = torch.nn.LayerNorm(hidden_dim)
def forward(self, x: torch.Tensor, **kwargs) -> tuple[torch.Tensor]:
"""Forward pass with self-attention.
Args:
x: Input tensor of shape (B, C, H, W)
Returns:
Output tensor of shape (B, C, H, W)
"""
B, C, H, W = x.shape
# Input projection
x = self.input_proj(x) # (B, hidden_dim, H, W)
# Reshape to sequence: (B, hidden_dim, H, W) -> (B, H*W, hidden_dim)
x = x.flatten(2).transpose(1, 2) # (B, H*W, hidden_dim)
# Self-attention with residual connection
residual = x
x = self.norm1(x)
# QKV projection
q = self.q_proj(x) # (B, H*W, hidden_dim)
k = self.k_proj(x) # (B, H*W, hidden_dim)
v = self.v_proj(x) # (B, H*W, hidden_dim)
# Reshape for multi-head attention: (B, H*W, hidden_dim) -> (B, num_heads, H*W, head_dim)
seq_len = H * W
q = q.view(B, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
k = k.view(B, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
v = v.view(B, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# Scaled dot-product attention
scale = self.head_dim**-0.5
attn_scores = torch.matmul(q, k.transpose(-2, -1)) * scale # (B, num_heads, H*W, H*W)
attn_weights = torch.nn.functional.softmax(attn_scores, dim=-1)
attn_output = torch.matmul(attn_weights, v)
attn_output = attn_output.transpose(1, 2).contiguous().view(B, seq_len, self.hidden_dim)
attn_output = self.out_proj(attn_output)
x = residual + attn_output
residual = x
x = self.norm2(x)
x = residual + x
x = x.transpose(1, 2).view(B, self.hidden_dim, H, W)
out = self.final_proj(x)
return (out,)
class TestCFGPipeline(CFGParallelMixin):
"""Test pipeline using CFGParallelMixin."""
def __init__(self, in_channels: int = 4, hidden_dim: int = 128, seed: int = 42):
# Set seed BEFORE creating transformer to ensure consistent layer initialization
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
self.transformer = SimpleTransformer(in_channels, hidden_dim)
# Re-initialize all parameters with fixed seed for full reproducibility
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
for param in self.transformer.parameters():
torch.nn.init.normal_(param, mean=0.0, std=0.02)
def _test_cfg_parallel_worker(
local_rank: int,
world_size: int,
cfg_parallel_size: int,
dtype: torch.dtype,
test_config: dict,
result_queue: torch.multiprocessing.Queue,
):
"""Worker function for CFG parallel test."""
device = torch.device(f"{current_omni_platform.device_type}:{local_rank}")
current_omni_platform.set_device(device)
update_environment_variables(
{
"RANK": str(local_rank),
"LOCAL_RANK": str(local_rank),
"WORLD_SIZE": str(world_size),
"MASTER_ADDR": "localhost",
"MASTER_PORT": "29502",
}
)
init_distributed_environment()
initialize_model_parallel(cfg_parallel_size=cfg_parallel_size)
cfg_rank = get_classifier_free_guidance_rank()
cfg_world_size = get_classifier_free_guidance_world_size()
assert cfg_world_size == cfg_parallel_size
# Create pipeline with same seed to ensure identical model weights across all ranks
# Note: model_seed is set inside TestCFGPipeline.__init__
pipeline = TestCFGPipeline(
in_channels=test_config["channels"],
hidden_dim=test_config["hidden_dim"],
seed=test_config["model_seed"],
)
pipeline.transformer = pipeline.transformer.to(device=device, dtype=dtype)
pipeline.transformer.eval() # Set to eval mode for deterministic behavior
# Create fixed inputs with explicit seed setting for reproducibility
# Set both CPU and CUDA seeds to ensure identical inputs across all ranks
torch.manual_seed(test_config["input_seed"])
if torch.cuda.is_available():
torch.cuda.manual_seed_all(test_config["input_seed"])
batch_size = test_config["batch_size"]
channels = test_config["channels"]
height = test_config["height"]
width = test_config["width"]
# Positive input
positive_input = torch.randn(batch_size, channels, height, width, dtype=dtype, device=device)
# Negative input with different seed
torch.manual_seed(test_config["input_seed"] + 1)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(test_config["input_seed"] + 1)
negative_input = torch.randn(batch_size, channels, height, width, dtype=dtype, device=device)
# Prepare kwargs for predict_noise_maybe_with_cfg
positive_kwargs = {"x": positive_input}
negative_kwargs = {"x": negative_input}
with torch.no_grad():
# Call predict_noise_maybe_with_cfg
noise_pred = pipeline.predict_noise_maybe_with_cfg(
do_true_cfg=True,
true_cfg_scale=test_config["cfg_scale"],
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
cfg_normalize=test_config["cfg_normalize"],
)
# Only rank 0 has valid output in CFG parallel mode
if cfg_rank == 0:
assert noise_pred is not None
result_queue.put(noise_pred.cpu())
else:
assert noise_pred is None
destroy_distributed_env()
def _test_cfg_sequential_worker(
local_rank: int,
world_size: int,
dtype: torch.dtype,
test_config: dict,
result_queue: torch.multiprocessing.Queue,
):
"""Worker function for sequential CFG test (baseline)."""
device = torch.device(f"{current_omni_platform.device_type}:{local_rank}")
current_omni_platform.set_device(device)
update_environment_variables(
{
"RANK": str(local_rank),
"LOCAL_RANK": str(local_rank),
"WORLD_SIZE": str(world_size),
"MASTER_ADDR": "localhost",
"MASTER_PORT": "29503",
}
)
init_distributed_environment()
initialize_model_parallel(cfg_parallel_size=1) # No CFG parallel
cfg_world_size = get_classifier_free_guidance_world_size()
assert cfg_world_size == 1
# Create pipeline with same seed to ensure identical model weights as CFG parallel
# Note: model_seed is set inside TestCFGPipeline.__init__
pipeline = TestCFGPipeline(
in_channels=test_config["channels"],
hidden_dim=test_config["hidden_dim"],
seed=test_config["model_seed"],
)
pipeline.transformer = pipeline.transformer.to(device=device, dtype=dtype)
pipeline.transformer.eval()
# Create fixed inputs (same seed as CFG parallel to ensure identical inputs)
# Set both CPU and CUDA seeds for full reproducibility
torch.manual_seed(test_config["input_seed"])
if torch.cuda.is_available():
torch.cuda.manual_seed_all(test_config["input_seed"])
batch_size = test_config["batch_size"]
channels = test_config["channels"]
height = test_config["height"]
width = test_config["width"]
# Positive input
positive_input = torch.randn(batch_size, channels, height, width, dtype=dtype, device=device)
# Negative input with different seed
torch.manual_seed(test_config["input_seed"] + 1)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(test_config["input_seed"] + 1)
negative_input = torch.randn(batch_size, channels, height, width, dtype=dtype, device=device)
positive_kwargs = {"x": positive_input}
negative_kwargs = {"x": negative_input}
with torch.no_grad():
noise_pred = pipeline.predict_noise_maybe_with_cfg(
do_true_cfg=True,
true_cfg_scale=test_config["cfg_scale"],
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
cfg_normalize=test_config["cfg_normalize"],
)
# Sequential CFG always returns output
assert noise_pred is not None
result_queue.put(noise_pred.cpu())
destroy_distributed_env()
@pytest.mark.parametrize("cfg_parallel_size", [2])
@pytest.mark.parametrize("dtype", [torch.bfloat16])
@pytest.mark.parametrize("batch_size", [2])
@pytest.mark.parametrize("cfg_normalize", [False, True])
def test_predict_noise_maybe_with_cfg(cfg_parallel_size: int, dtype: torch.dtype, batch_size: int, cfg_normalize: bool):
"""
Test that predict_noise_maybe_with_cfg produces identical results
with and without CFG parallel.
Args:
cfg_parallel_size: Number of GPUs for CFG parallel
dtype: Data type for computation
batch_size: Batch size for testing
cfg_normalize: Whether to normalize CFG output
"""
available_gpus = current_omni_platform.get_device_count()
if available_gpus < cfg_parallel_size:
pytest.skip(f"Test requires {cfg_parallel_size} GPUs but only {available_gpus} available")
test_config = {
"batch_size": batch_size,
"channels": 4,
"height": 16,
"width": 16,
"hidden_dim": 128,
"cfg_scale": 7.5,
"cfg_normalize": cfg_normalize,
"model_seed": 42, # Fixed seed for model initialization
"input_seed": 123, # Fixed seed for input generation
}
mp_context = torch.multiprocessing.get_context("spawn")
manager = mp_context.Manager()
baseline_queue = manager.Queue()
cfg_parallel_queue = manager.Queue()
# Run baseline (sequential CFG) on single GPU
torch.multiprocessing.spawn(
_test_cfg_sequential_worker,
args=(1, dtype, test_config, baseline_queue),
nprocs=1,
)
# Run CFG parallel on multiple GPUs
torch.multiprocessing.spawn(
_test_cfg_parallel_worker,
args=(cfg_parallel_size, cfg_parallel_size, dtype, test_config, cfg_parallel_queue),
nprocs=cfg_parallel_size,
)
# Get results from queues
baseline_output = baseline_queue.get()
cfg_parallel_output = cfg_parallel_queue.get()
# Verify shapes match
assert baseline_output.shape == cfg_parallel_output.shape, (
f"Shape mismatch: baseline {baseline_output.shape} vs CFG parallel {cfg_parallel_output.shape}"
)
# Verify numerical equivalence with appropriate tolerances
if dtype == torch.float32:
rtol, atol = 1e-5, 1e-5
elif dtype == torch.bfloat16:
rtol, atol = 1e-2, 1e-2
else:
rtol, atol = 1e-3, 1e-3
torch.testing.assert_close(
cfg_parallel_output,
baseline_output,
rtol=rtol,
atol=atol,
msg=(
f"CFG parallel output differs from sequential CFG\n"
f" dtype={dtype}, batch_size={batch_size}, cfg_normalize={cfg_normalize}\n"
f" Max diff: {(cfg_parallel_output - baseline_output).abs().max().item():.6e}"
),
)
print(
f"✓ Test passed: cfg_size={cfg_parallel_size}, dtype={dtype}, "
f"batch_size={batch_size}, cfg_normalize={cfg_normalize}"
)
@pytest.mark.parametrize("dtype", [torch.bfloat16])
def test_predict_noise_without_cfg(dtype: torch.dtype):
"""
Test predict_noise_maybe_with_cfg when do_true_cfg=False.
When CFG is disabled, only the positive branch should be computed.
This test runs on a single GPU without distributed environment.
"""
available_gpus = current_omni_platform.get_device_count()
if available_gpus < 1:
pytest.skip("Test requires at least 1 GPU")
device = torch.device(f"{current_omni_platform.device_type}:0")
current_omni_platform.set_device(device)
# Create pipeline without distributed environment
pipeline = TestCFGPipeline(in_channels=4, hidden_dim=128, seed=42)
pipeline.transformer = pipeline.transformer.to(device=device, dtype=dtype)
pipeline.transformer.eval()
# Set seed for input generation
torch.manual_seed(123)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(123)
positive_input = torch.randn(1, 4, 16, 16, dtype=dtype, device=device)
with torch.no_grad():
noise_pred = pipeline.predict_noise_maybe_with_cfg(
do_true_cfg=False, # No CFG
true_cfg_scale=7.5,
positive_kwargs={"x": positive_input},
negative_kwargs=None,
cfg_normalize=False,
)
# Should always return output when do_true_cfg=False
assert noise_pred is not None
assert noise_pred.shape == (1, 4, 16, 16)
print(f"✓ Test passed: predict_noise without CFG (dtype={dtype})")
+1
View File
@@ -55,6 +55,7 @@ class DiffusionParallelConfig:
assert self.ulysses_degree > 0, "Ulysses degree must be > 0"
assert self.ring_degree > 0, "Ring degree must be > 0"
assert self.cfg_parallel_size > 0, "CFG parallel size must be > 0"
assert self.cfg_parallel_size in [1, 2], f"CFG parallel size must be 1 or 2, but got {self.cfg_parallel_size}"
assert self.sequence_parallel_size == self.ulysses_degree * self.ring_degree, (
"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}"
@@ -0,0 +1,235 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Base pipeline class for Diffusion models with shared CFG functionality.
"""
from abc import ABCMeta
from typing import Any
import torch
from vllm_omni.diffusion.distributed.parallel_state import (
get_cfg_group,
get_classifier_free_guidance_rank,
get_classifier_free_guidance_world_size,
)
class CFGParallelMixin(metaclass=ABCMeta):
"""
Base Mixin class for Diffusion pipelines providing shared CFG methods.
All pipelines should inherit from this class to reuse
classifier-free guidance logic.
"""
def predict_noise_maybe_with_cfg(
self,
do_true_cfg: bool,
true_cfg_scale: float,
positive_kwargs: dict[str, Any],
negative_kwargs: dict[str, Any] | None,
cfg_normalize: bool = True,
output_slice: int | None = None,
) -> torch.Tensor | None:
"""
Predict noise with optional classifier-free guidance.
Args:
do_true_cfg: Whether to apply CFG
true_cfg_scale: CFG scale factor
positive_kwargs: Kwargs for positive/conditional prediction
negative_kwargs: Kwargs for negative/unconditional prediction
cfg_normalize: Whether to normalize CFG output (default: True)
output_slice: If set, slice output to [:, :output_slice] for image editing
Returns:
Predicted noise tensor (only valid on rank 0 in CFG parallel mode)
"""
if do_true_cfg:
# Automatically detect CFG parallel configuration
cfg_parallel_ready = get_classifier_free_guidance_world_size() > 1
if cfg_parallel_ready:
# Enable CFG-parallel: rank0 computes positive, rank1 computes negative.
cfg_group = get_cfg_group()
cfg_rank = get_classifier_free_guidance_rank()
if cfg_rank == 0:
local_pred = self.predict_noise(**positive_kwargs)
else:
local_pred = self.predict_noise(**negative_kwargs)
# Slice output for image editing pipelines (remove condition latents)
if output_slice is not None:
local_pred = local_pred[:, :output_slice]
gathered = cfg_group.all_gather(local_pred, separate_tensors=True)
if cfg_rank == 0:
noise_pred = gathered[0]
neg_noise_pred = gathered[1]
noise_pred = self.combine_cfg_noise(noise_pred, neg_noise_pred, true_cfg_scale, cfg_normalize)
return noise_pred
else:
return None
else:
# Sequential CFG: compute both positive and negative
positive_noise_pred = self.predict_noise(**positive_kwargs)
negative_noise_pred = self.predict_noise(**negative_kwargs)
# Slice output for image editing pipelines
if output_slice is not None:
positive_noise_pred = positive_noise_pred[:, :output_slice]
negative_noise_pred = negative_noise_pred[:, :output_slice]
noise_pred = self.combine_cfg_noise(
positive_noise_pred, negative_noise_pred, true_cfg_scale, cfg_normalize
)
return noise_pred
else:
# No CFG: only compute positive/conditional prediction
pred = self.predict_noise(**positive_kwargs)
if output_slice is not None:
pred = pred[:, :output_slice]
return pred
def cfg_normalize_function(self, noise_pred: torch.Tensor, comb_pred: torch.Tensor) -> torch.Tensor:
"""
Normalize the combined noise prediction.
Args:
noise_pred: positive noise prediction
comb_pred: combined noise prediction after CFG
Returns:
Normalized noise prediction tensor
"""
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
noise_pred = comb_pred * (cond_norm / noise_norm)
return noise_pred
def combine_cfg_noise(
self, noise_pred: torch.Tensor, neg_noise_pred: torch.Tensor, true_cfg_scale: float, cfg_normalize: bool = False
) -> torch.Tensor:
"""
Combine conditional and unconditional noise predictions with CFG.
Args:
noise_pred: Conditional noise prediction
neg_noise_pred: Unconditional noise prediction
true_cfg_scale: CFG scale factor
cfg_normalize: Whether to normalize the combined prediction (default: False)
Returns:
Combined noise prediction tensor
"""
comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
if cfg_normalize:
noise_pred = self.cfg_normalize_function(noise_pred, comb_pred)
else:
noise_pred = comb_pred
return noise_pred
def predict_noise(self, *args: Any, **kwargs: Any) -> torch.Tensor:
"""
Forward pass through transformer to predict noise.
Subclasses should override this if they need custom behavior,
but the default implementation calls self.transformer.
"""
return self.transformer(*args, **kwargs)[0]
def diffuse(
self,
*args: Any,
**kwargs: Any,
) -> Any:
"""
Diffusion loop with optional classifier-free guidance.
Subclasses MUST implement this method to define the complete
diffusion/denoising loop for their specific model.
Typical implementation pattern:
```python
def diffuse(self, latents, timesteps, prompt_embeds, negative_embeds, ...):
for t in timesteps:
# Prepare kwargs for positive and negative predictions
positive_kwargs = {...}
negative_kwargs = {...}
# Predict noise with automatic CFG handling
noise_pred = self.predict_noise_maybe_with_cfg(
do_true_cfg=True,
true_cfg_scale=self.guidance_scale,
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
)
# Step scheduler with automatic CFG sync
latents = self.scheduler_step_maybe_with_cfg(
noise_pred, t, latents, do_true_cfg=True
)
return latents
```
"""
raise NotImplementedError("Subclasses must implement diffuse")
def scheduler_step(self, noise_pred: torch.Tensor, t: torch.Tensor, latents: torch.Tensor) -> torch.Tensor:
"""
Step the scheduler.
Args:
noise_pred: Predicted noise
t: Current timestep
latents: Current latents
Returns:
Updated latents after scheduler step
"""
return self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
def scheduler_step_maybe_with_cfg(
self, noise_pred: torch.Tensor, t: torch.Tensor, latents: torch.Tensor, do_true_cfg: bool
) -> torch.Tensor:
"""
Step the scheduler with (maybe) automatic CFG parallel synchronization.
In CFG parallel mode, only rank 0 computes the scheduler step,
then broadcasts the result to other ranks.
Args:
noise_pred: Predicted noise (only valid on rank 0 in CFG parallel)
t: Current timestep
latents: Current latents
do_true_cfg: Whether CFG is enabled
Returns:
Updated latents (synchronized across all CFG ranks)
"""
# Automatically detect CFG parallel configuration
cfg_parallel_ready = do_true_cfg and get_classifier_free_guidance_world_size() > 1
if cfg_parallel_ready:
cfg_group = get_cfg_group()
cfg_rank = get_classifier_free_guidance_rank()
# Only rank 0 computes the scheduler step
if cfg_rank == 0:
latents = self.scheduler_step(noise_pred, t, latents)
# Broadcast the updated latents to all ranks
latents = latents.contiguous()
cfg_group.broadcast(latents, src=0)
else:
# No CFG parallel: directly compute scheduler step
latents = self.scheduler_step(noise_pred, t, latents)
return latents
@@ -22,6 +22,7 @@ from transformers import CLIPTextModel, CLIPTokenizer, T5EncoderModel, T5Tokeniz
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.parallel_state import get_classifier_free_guidance_world_size
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.flux import FluxTransformer2DModel
@@ -554,6 +555,21 @@ class FluxPipeline(
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
return latents
def check_cfg_parallel_validity(self, true_cfg_scale: float, has_neg_prompt: bool):
if get_classifier_free_guidance_world_size() == 1:
return True
if true_cfg_scale <= 1:
logger.warning("CFG parallel is NOT working correctly when true_cfg_scale <= 1.")
return False
if not has_neg_prompt:
logger.warning(
"CFG parallel is NOT working correctly when there is no negative prompt or negative prompt embeddings."
)
return False
return True
def forward(
self,
req: OmniDiffusionRequest,
@@ -637,6 +653,8 @@ class FluxPipeline(
)
do_true_cfg = true_cfg_scale > 1 and has_neg_prompt
self.check_cfg_parallel_validity(true_cfg_scale, has_neg_prompt)
(
prompt_embeds,
pooled_prompt_embeds,
@@ -36,6 +36,7 @@ from vllm.logger import init_logger
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.flux2_klein.flux2_klein_transformer import (
@@ -178,7 +179,7 @@ def compute_empirical_mu(image_seq_len: int, num_steps: int) -> float:
return float(mu)
class Flux2KleinPipeline(nn.Module, SupportImageInput):
class Flux2KleinPipeline(nn.Module, CFGParallelMixin, SupportImageInput):
"""Flux2 klein pipeline for text-to-image generation."""
support_image_input = True
@@ -923,38 +924,44 @@ class Flux2KleinPipeline(nn.Module, SupportImageInput):
latent_model_input = torch.cat([latents, image_latents], dim=1).to(self.transformer.dtype)
latent_image_ids = torch.cat([latent_ids, image_latent_ids], dim=1)
noise_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=None,
encoder_hidden_states=prompt_embeds,
txt_ids=text_ids,
img_ids=latent_image_ids,
joint_attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
noise_pred = noise_pred[:, : latents.size(1) :]
positive_kwargs = {
"hidden_states": latent_model_input,
"timestep": timestep / 1000,
"guidance": None,
"encoder_hidden_states": prompt_embeds,
"txt_ids": text_ids,
"img_ids": latent_image_ids,
"joint_attention_kwargs": self.attention_kwargs,
"return_dict": False,
}
if self.do_classifier_free_guidance:
neg_noise_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=None,
encoder_hidden_states=negative_prompt_embeds,
txt_ids=negative_text_ids,
img_ids=latent_image_ids,
joint_attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
neg_noise_pred = neg_noise_pred[:, : latents.size(1) :]
noise_pred = neg_noise_pred + guidance_scale * (noise_pred - neg_noise_pred)
negative_kwargs = {
"hidden_states": latent_model_input,
"timestep": timestep / 1000,
"guidance": None,
"encoder_hidden_states": negative_prompt_embeds,
"txt_ids": negative_text_ids,
"img_ids": latent_image_ids,
"joint_attention_kwargs": self.attention_kwargs,
"return_dict": False,
}
else:
negative_kwargs = None
latents_dtype = latents.dtype
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
# For editing pipelines, we need to slice the output to remove condition latents
output_slice = latents.size(1) if image_latents is not None else None
if latents.dtype != latents_dtype and torch.backends.mps.is_available():
latents = latents.to(latents_dtype)
noise_pred = self.predict_noise_maybe_with_cfg(
do_true_cfg=self.do_classifier_free_guidance,
true_cfg_scale=guidance_scale,
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
cfg_normalize=False,
output_slice=output_slice,
)
# Compute the previous noisy sample x_t -> x_t-1 with automatic CFG sync
latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, self.do_classifier_free_guidance)
if callback_on_step_end is not None:
callback_kwargs = {}
@@ -8,6 +8,7 @@ import json
import os
import re
from collections.abc import Iterable
from functools import partial
from typing import Any
import numpy as np
@@ -23,6 +24,7 @@ from vllm.logger import init_logger
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.longcat_image.longcat_image_transformer import LongCatImageTransformer2DModel
@@ -197,9 +199,7 @@ def get_prompt_language(prompt):
return "en"
class LongCatImagePipeline(
nn.Module,
):
class LongCatImagePipeline(nn.Module, CFGParallelMixin):
def __init__(
self,
*,
@@ -460,6 +460,16 @@ class LongCatImagePipeline(
f" {negative_prompt_embeds}. Please make sure to only forward one of the two."
)
def cfg_normalize_function(self, noise_pred, comb_pred, cfg_renorm_min=0.0):
"""
Normalize the combined noise prediction.
"""
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
scale = (cond_norm / (noise_norm + 1e-8)).clamp(min=cfg_renorm_min, max=1.0)
noise_pred = comb_pred * scale
return noise_pred
def forward(
self,
req: OmniDiffusionRequest,
@@ -604,6 +614,9 @@ class LongCatImagePipeline(
if self.do_classifier_free_guidance:
negative_prompt_embeds = negative_prompt_embeds.to(device)
# custom partial function with cfg_renorm_min
self.cfg_normalize_function = partial(self.cfg_normalize_function, cfg_renorm_min=cfg_renorm_min)
# 6. Denoising loop
for i, t in enumerate(timesteps):
if self._interrupt:
@@ -612,43 +625,37 @@ class LongCatImagePipeline(
self._current_timestep = t
timestep = t.expand(latents.shape[0]).to(latents.dtype)
noise_pred_text = self.transformer(
hidden_states=latents,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states=prompt_embeds,
txt_ids=text_ids,
img_ids=latent_image_ids,
return_dict=False,
)[0]
positive_kwargs = {
"hidden_states": latents,
"timestep": timestep / 1000,
"guidance": guidance,
"encoder_hidden_states": prompt_embeds,
"txt_ids": text_ids,
"img_ids": latent_image_ids,
"return_dict": False,
}
if self.do_classifier_free_guidance:
noise_pred_uncond = self.transformer(
hidden_states=latents,
timestep=timestep / 1000,
encoder_hidden_states=negative_prompt_embeds,
txt_ids=negative_text_ids,
img_ids=latent_image_ids,
return_dict=False,
)[0]
noise_pred = noise_pred_uncond + self._guidance_scale * (noise_pred_text - noise_pred_uncond)
if enable_cfg_renorm:
cond_norm = torch.norm(noise_pred_text, dim=-1, keepdim=True)
noise_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
scale = (cond_norm / (noise_norm + 1e-8)).clamp(min=cfg_renorm_min, max=1.0)
noise_pred = noise_pred * scale
negative_kwargs = {
"hidden_states": latents,
"timestep": timestep / 1000,
"encoder_hidden_states": negative_prompt_embeds,
"txt_ids": negative_text_ids,
"img_ids": latent_image_ids,
"return_dict": False,
}
else:
noise_pred = noise_pred_text
negative_kwargs = None
# compute the previous noisy sample x_t -> x_t-1
latents_dtype = latents.dtype
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
noise_pred = self.predict_noise_maybe_with_cfg(
do_true_cfg=self.do_classifier_free_guidance,
true_cfg_scale=guidance_scale,
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
cfg_normalize=enable_cfg_renorm,
)
if latents.dtype != latents_dtype:
if torch.backends.mps.is_available():
# some platforms (eg. apple mps) misbehave due to a pytorch bug: https://github.com/pytorch/pytorch/pull/99272
latents = latents.to(latents_dtype)
# Compute the previous noisy sample x_t -> x_t-1 with automatic CFG sync
latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, self.do_classifier_free_guidance)
self._current_timestep = None
@@ -26,6 +26,7 @@ from vllm.logger import init_logger
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.interface import SupportImageInput
@@ -215,7 +216,7 @@ def split_quotation(prompt, quote_pairs=None):
return result
class LongCatImageEditPipeline(nn.Module, SupportImageInput):
class LongCatImageEditPipeline(nn.Module, CFGParallelMixin, SupportImageInput):
def __init__(
self,
*,
@@ -652,38 +653,39 @@ class LongCatImageEditPipeline(nn.Module, SupportImageInput):
latent_model_input = torch.cat([latents, image_latents], dim=1)
timestep = t.expand(latent_model_input.shape[0]).to(latents.dtype)
do_true_cfg = guidance_scale > 1
positive_kwargs = {
"hidden_states": latent_model_input,
"timestep": timestep / 1000,
"guidance": guidance,
"encoder_hidden_states": prompt_embeds,
"txt_ids": text_ids,
"img_ids": latent_image_ids,
"return_dict": False,
}
noise_pred_text = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states=prompt_embeds,
txt_ids=text_ids,
img_ids=latent_image_ids,
return_dict=False,
)[0]
noise_pred_text = noise_pred_text[:, :image_seq_len]
if guidance_scale > 1:
noise_pred_uncond = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
encoder_hidden_states=negative_prompt_embeds,
txt_ids=negative_text_ids,
img_ids=latent_image_ids,
return_dict=False,
)[0]
noise_pred_uncond = noise_pred_uncond[:, :image_seq_len]
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
if do_true_cfg:
negative_kwargs = {
"hidden_states": latent_model_input,
"timestep": timestep / 1000,
"encoder_hidden_states": negative_prompt_embeds,
"txt_ids": negative_text_ids,
"img_ids": latent_image_ids,
"return_dict": False,
}
else:
noise_pred = noise_pred_text
# compute the previous noisy sample x_t -> x_t-1
latents_dtype = latents.dtype
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
negative_kwargs = None
if latents.dtype != latents_dtype:
if torch.backends.mps.is_available():
# some platforms (eg. apple mps) misbehave due to a pytorch bug: https://github.com/pytorch/pytorch/pull/99272
latents = latents.to(latents_dtype)
noise_pred = self.predict_noise_maybe_with_cfg(
do_true_cfg=do_true_cfg,
true_cfg_scale=guidance_scale,
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
cfg_normalize=False,
output_slice=image_seq_len,
)
# Compute the previous noisy sample x_t -> x_t-1 with automatic CFG sync
latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, do_true_cfg)
self._current_timestep = None
@@ -23,18 +23,19 @@ from typing import Any
import numpy as np
import torch
import torch.nn as nn
from diffusers.image_processor import VaeImageProcessor
from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
from diffusers.schedulers.scheduling_flow_match_euler_discrete import (
FlowMatchEulerDiscreteScheduler,
)
from diffusers.utils.torch_utils import randn_tensor
from torch import nn
from transformers import Qwen2TokenizerFast, Qwen3Model
from vllm.logger import init_logger
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.ovis_image.ovis_image_transformer import OvisImageTransformer2DModel
@@ -139,9 +140,7 @@ def retrieve_timesteps(
return timesteps, num_inference_steps
class OvisImagePipeline(
nn.Module,
):
class OvisImagePipeline(nn.Module, CFGParallelMixin):
def __init__(
self,
*,
@@ -431,68 +430,77 @@ class OvisImagePipeline(
)
return timesteps, num_inference_steps
def denoising(
def diffuse(
self,
latents,
timesteps,
prompt_embeds,
negative_prompt_embeds,
guidance_scale,
do_classifier_free_guidance,
callback_on_step_end,
callback_on_step_end_tensor_inputs,
text_ids,
negative_text_ids,
latent_image_ids,
):
latents: torch.Tensor,
timesteps: torch.Tensor,
prompt_embeds: torch.Tensor,
negative_prompt_embeds: torch.Tensor,
text_ids: torch.Tensor,
negative_text_ids: torch.Tensor,
latent_image_ids: torch.Tensor,
do_true_cfg: bool,
guidance_scale: float,
cfg_normalize: bool = False,
) -> torch.Tensor:
"""
Diffusion loop with optional classifier-free guidance.
Args:
latents: Noise latents to denoise
timesteps: Diffusion timesteps
prompt_embeds: Positive prompt embeddings
negative_prompt_embeds: Negative prompt embeddings
text_ids: Position IDs for positive text
negative_text_ids: Position IDs for negative text
latent_image_ids: Position IDs for image latents
do_true_cfg: Whether to apply CFG
guidance_scale: CFG scale factor
cfg_normalize: Whether to normalize CFG output (default: False)
Returns:
Denoised latents
"""
self.scheduler.set_begin_index(0)
for i, t in enumerate(timesteps):
if self._interrupt:
if self.interrupt:
break
self._current_timestep = t
# broadcast to batch dimension in a way that's compatible with ONNX/Core ML
timestep = t.expand(latents.shape[0]).to(latents.dtype)
noise_pred = self.transformer(
hidden_states=latents,
timestep=timestep / 1000,
encoder_hidden_states=prompt_embeds,
txt_ids=text_ids,
img_ids=latent_image_ids,
return_dict=False,
)[0]
positive_kwargs = {
"hidden_states": latents,
"timestep": timestep / 1000,
"encoder_hidden_states": prompt_embeds,
"txt_ids": text_ids,
"img_ids": latent_image_ids,
"return_dict": False,
}
if do_true_cfg:
negative_kwargs = {
"hidden_states": latents,
"timestep": timestep / 1000,
"encoder_hidden_states": negative_prompt_embeds,
"txt_ids": negative_text_ids,
"img_ids": latent_image_ids,
"return_dict": False,
}
else:
negative_kwargs = None
if do_classifier_free_guidance:
neg_noise_pred = self.transformer(
hidden_states=latents,
timestep=timestep / 1000,
encoder_hidden_states=negative_prompt_embeds,
txt_ids=negative_text_ids,
img_ids=latent_image_ids,
return_dict=False,
)[0]
# Predict noise with automatic CFG parallel handling
noise_pred = self.predict_noise_maybe_with_cfg(
do_true_cfg,
guidance_scale,
positive_kwargs,
negative_kwargs,
cfg_normalize,
)
noise_pred = neg_noise_pred + guidance_scale * (noise_pred - neg_noise_pred)
# compute the previous noisy sample x_t -> x_t-1
latents_dtype = latents.dtype
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
if latents.dtype != latents_dtype:
if torch.backends.mps.is_available():
# some platforms (eg. apple mps) misbehave due to a pytorch bug: https://github.com/pytorch/pytorch/pull/99272
latents = latents.to(latents_dtype)
# Not used in this pipeline
# if callback_on_step_end is not None:
# callback_kwargs = {}
# for k in callback_on_step_end_tensor_inputs:
# callback_kwargs[k] = locals()[k]
# callback_outputs = callback_on_step_end(self, i, t, callback_kwargs)
# latents = callback_outputs.pop("latents", latents)
# prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds)
# Compute the previous noisy sample x_t -> x_t-1 with automatic CFG sync
latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, do_true_cfg)
return latents
@@ -704,23 +712,18 @@ class OvisImagePipeline(
if self.joint_attention_kwargs is None:
self._joint_attention_kwargs = {}
# 6. Denoising loop
# We set the index here to remove DtoH sync, helpful especially during compilation
# Check out more details here: https://github.com/huggingface/diffusers/pull/11696
latents = self.denoising(
latents,
timesteps,
prompt_embeds,
negative_prompt_embeds,
guidance_scale,
do_classifier_free_guidance,
callback_on_step_end,
callback_on_step_end_tensor_inputs,
text_ids,
negative_text_ids,
latent_image_ids,
# 6. Denoising loop using diffuse method
latents = self.diffuse(
latents=latents,
timesteps=timesteps,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds if do_classifier_free_guidance else None,
text_ids=text_ids,
negative_text_ids=negative_text_ids if do_classifier_free_guidance else None,
latent_image_ids=latent_image_ids,
do_true_cfg=do_classifier_free_guidance,
guidance_scale=guidance_scale,
cfg_normalize=False,
)
self._current_timestep = None
@@ -2,6 +2,9 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Qwen Image diffusion model components."""
from vllm_omni.diffusion.models.qwen_image.cfg_parallel import (
QwenImageCFGParallelMixin,
)
from vllm_omni.diffusion.models.qwen_image.pipeline_qwen_image import (
QwenImagePipeline,
get_qwen_image_post_process_func,
@@ -11,6 +14,7 @@ from vllm_omni.diffusion.models.qwen_image.qwen_image_transformer import (
)
__all__ = [
"QwenImageCFGParallelMixin",
"QwenImagePipeline",
"QwenImageTransformer2DModel",
"get_qwen_image_post_process_func",
@@ -0,0 +1,160 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""CFG Parallel Mixin for Qwen Image series
Shared by
- QwenImagePipeline
- QwenImageEditPipeline
- QwenImageEditPlusPipeline
- QwenImageLayeredPipeline
"""
import logging
from typing import Any
import torch
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.parallel_state import get_classifier_free_guidance_world_size
logger = logging.getLogger(__name__)
class QwenImageCFGParallelMixin(CFGParallelMixin):
"""
Base Mixin class for Qwen Image pipelines providing shared CFG methods.
"""
def diffuse(
self,
prompt_embeds: torch.Tensor,
prompt_embeds_mask: torch.Tensor,
negative_prompt_embeds: torch.Tensor,
negative_prompt_embeds_mask: torch.Tensor,
latents: torch.Tensor,
img_shapes: torch.Tensor,
txt_seq_lens: torch.Tensor,
negative_txt_seq_lens: torch.Tensor,
timesteps: torch.Tensor,
do_true_cfg: bool,
guidance: torch.Tensor,
true_cfg_scale: float,
image_latents: torch.Tensor | None = None,
cfg_normalize: bool = True,
additional_transformer_kwargs: dict[str, Any] | None = None,
) -> torch.Tensor:
"""
Diffusion loop with optional classifier-free guidance.
Args:
prompt_embeds: Positive prompt embeddings
prompt_embeds_mask: Mask for positive prompt
negative_prompt_embeds: Negative prompt embeddings
negative_prompt_embeds_mask: Mask for negative prompt
latents: Noise latents to denoise
img_shapes: Image shape information
txt_seq_lens: Text sequence lengths for positive prompts
negative_txt_seq_lens: Text sequence lengths for negative prompts
timesteps: Diffusion timesteps
do_true_cfg: Whether to apply CFG
guidance: Guidance scale tensor
true_cfg_scale: CFG scale factor
image_latents: Conditional image latents for editing (default: None)
cfg_normalize: Whether to normalize CFG output (default: True)
additional_transformer_kwargs: Extra kwargs to pass to transformer (default: None)
Returns:
Denoised latents
"""
self.scheduler.set_begin_index(0)
self.transformer.do_true_cfg = do_true_cfg
additional_transformer_kwargs = additional_transformer_kwargs or {}
for i, t in enumerate(timesteps):
if self.interrupt:
continue
self._current_timestep = t
# Broadcast timestep to match batch size
timestep = t.expand(latents.shape[0]).to(device=latents.device, dtype=latents.dtype)
# Concatenate image latents with noise latents if available (for editing pipelines)
latent_model_input = latents
if image_latents is not None:
latent_model_input = torch.cat([latents, image_latents], dim=1)
positive_kwargs = {
"hidden_states": latent_model_input,
"timestep": timestep / 1000,
"guidance": guidance,
"encoder_hidden_states_mask": prompt_embeds_mask,
"encoder_hidden_states": prompt_embeds,
"img_shapes": img_shapes,
"txt_seq_lens": txt_seq_lens,
**additional_transformer_kwargs,
}
if do_true_cfg:
negative_kwargs = {
"hidden_states": latent_model_input,
"timestep": timestep / 1000,
"guidance": guidance,
"encoder_hidden_states_mask": negative_prompt_embeds_mask,
"encoder_hidden_states": negative_prompt_embeds,
"img_shapes": img_shapes,
"txt_seq_lens": negative_txt_seq_lens,
**additional_transformer_kwargs,
}
else:
negative_kwargs = None
# For editing pipelines, we need to slice the output to remove condition latents
output_slice = latents.size(1) if image_latents is not None else None
# Predict noise with automatic CFG parallel handling
noise_pred = self.predict_noise_maybe_with_cfg(
do_true_cfg,
true_cfg_scale,
positive_kwargs,
negative_kwargs,
cfg_normalize,
output_slice,
)
# Compute the previous noisy sample x_t -> x_t-1 with automatic CFG sync
latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, do_true_cfg)
return latents
def check_cfg_parallel_validity(self, true_cfg_scale: float, has_neg_prompt: bool):
"""
Validate whether CFG parallel is properly configured for the current generation request.
When CFG parallel is enabled (cfg_parallel_world_size > 1), this method verifies that the necessary
conditions are met for correct parallel execution. If validation fails, a warning is
logged to help identify configuration issues.
Args:
true_cfg_scale: The classifier-free guidance scale value. Must be > 1 for CFG to
have an effect.
has_neg_prompt: Whether negative prompts or negative prompt embeddings are provided.
Required for CFG to perform unconditional prediction.
Returns:
True if CFG parallel is disabled or all validation checks pass, False otherwise.
Note:
When CFG parallel is disabled (world_size == 1), this method always returns True
as no parallel-specific validation is needed.
"""
if get_classifier_free_guidance_world_size() == 1:
return True
if true_cfg_scale <= 1:
logger.warning("CFG parallel is NOT working correctly when true_cfg_scale <= 1.")
return False
if not has_neg_prompt:
logger.warning(
"CFG parallel is NOT working correctly when there is no negative prompt or negative prompt embeddings."
)
return False
return True
@@ -25,13 +25,11 @@ from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.parallel_state import (
get_cfg_group,
get_classifier_free_guidance_rank,
get_classifier_free_guidance_world_size,
)
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.qwen_image.cfg_parallel import (
QwenImageCFGParallelMixin,
)
from vllm_omni.diffusion.models.qwen_image.qwen_image_transformer import (
QwenImageTransformer2DModel,
)
@@ -239,9 +237,7 @@ def apply_rotary_emb_qwen(
return x_out.type_as(x)
class QwenImagePipeline(
nn.Module,
):
class QwenImagePipeline(nn.Module, QwenImageCFGParallelMixin):
def __init__(
self,
*,
@@ -536,109 +532,6 @@ class QwenImagePipeline(
def interrupt(self):
return self._interrupt
def diffuse(
self,
prompt_embeds,
prompt_embeds_mask,
negative_prompt_embeds,
negative_prompt_embeds_mask,
latents,
img_shapes,
txt_seq_lens,
negative_txt_seq_lens,
timesteps,
do_true_cfg,
guidance,
true_cfg_scale,
):
self.scheduler.set_begin_index(0)
for i, t in enumerate(timesteps):
if self.interrupt:
continue
self._current_timestep = t
# Broadcast timestep to match batch size
timestep = t.expand(latents.shape[0]).to(device=latents.device, dtype=latents.dtype)
# Enable CFG-parallel: rank0 computes positive, rank1 computes negative.
cfg_parallel_ready = do_true_cfg and get_classifier_free_guidance_world_size() > 1
self.transformer.do_true_cfg = do_true_cfg
if cfg_parallel_ready:
cfg_group = get_cfg_group()
cfg_rank = get_classifier_free_guidance_rank()
if cfg_rank == 0:
local_pred = self.transformer(
hidden_states=latents,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=prompt_embeds_mask,
encoder_hidden_states=prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=txt_seq_lens,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
else:
local_pred = self.transformer(
hidden_states=latents,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=negative_prompt_embeds_mask,
encoder_hidden_states=negative_prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=negative_txt_seq_lens,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
gathered = cfg_group.all_gather(local_pred, separate_tensors=True)
if cfg_rank == 0:
noise_pred = gathered[0]
neg_noise_pred = gathered[1]
comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
noise_pred = comb_pred * (cond_norm / noise_norm)
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
cfg_group.broadcast(latents, src=0)
else:
# Forward pass for positive prompt (or unconditional if no CFG)
noise_pred = self.transformer(
hidden_states=latents,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=prompt_embeds_mask,
encoder_hidden_states=prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=txt_seq_lens,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
# Forward pass for negative prompt (CFG)
if do_true_cfg:
neg_noise_pred = self.transformer(
hidden_states=latents,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=negative_prompt_embeds_mask,
encoder_hidden_states=negative_prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=negative_txt_seq_lens,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
noise_pred = comb_pred * (cond_norm / noise_norm)
# compute the previous noisy sample x_t -> x_t-1
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
return latents
def forward(
self,
req: OmniDiffusionRequest,
@@ -720,6 +613,8 @@ class QwenImagePipeline(
)
do_true_cfg = true_cfg_scale > 1 and has_neg_prompt
self.check_cfg_parallel_validity(true_cfg_scale, has_neg_prompt)
prompt_embeds, prompt_embeds_mask = self.encode_prompt(
prompt=prompt,
prompt_embeds=prompt_embeds,
@@ -790,6 +685,12 @@ class QwenImagePipeline(
do_true_cfg,
guidance,
true_cfg_scale,
image_latents=None,
cfg_normalize=True,
additional_transformer_kwargs={
"return_dict": False,
"attention_kwargs": self.attention_kwargs,
},
)
self._current_timestep = None
@@ -25,14 +25,12 @@ from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, Qwe
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.parallel_state import (
get_cfg_group,
get_classifier_free_guidance_rank,
get_classifier_free_guidance_world_size,
)
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.interface import SupportImageInput
from vllm_omni.diffusion.models.qwen_image.cfg_parallel import (
QwenImageCFGParallelMixin,
)
from vllm_omni.diffusion.models.qwen_image.pipeline_qwen_image import calculate_shift
from vllm_omni.diffusion.models.qwen_image.qwen_image_transformer import (
QwenImageTransformer2DModel,
@@ -213,10 +211,7 @@ def retrieve_latents(
raise AttributeError("Could not access latents of provided encoder_output")
class QwenImageEditPipeline(
nn.Module,
SupportImageInput,
):
class QwenImageEditPipeline(nn.Module, SupportImageInput, QwenImageCFGParallelMixin):
def __init__(
self,
*,
@@ -598,118 +593,6 @@ class QwenImageEditPipeline(
def interrupt(self):
return self._interrupt
def diffuse(
self,
prompt_embeds,
prompt_embeds_mask,
negative_prompt_embeds,
negative_prompt_embeds_mask,
latents,
image_latents,
img_shapes,
txt_seq_lens,
negative_txt_seq_lens,
timesteps,
do_true_cfg,
guidance,
true_cfg_scale,
):
"""Diffusion loop with optional image conditioning."""
self.scheduler.set_begin_index(0)
for i, t in enumerate(timesteps):
if self.interrupt:
continue
self._current_timestep = t
# broadcast to batch dimension and place on same device/dtype as latents
timestep = t.expand(latents.shape[0]).to(device=latents.device, dtype=latents.dtype)
# Concatenate image latents with noise latents if available
latent_model_input = latents
if image_latents is not None:
latent_model_input = torch.cat([latents, image_latents], dim=1)
self.transformer.do_true_cfg = do_true_cfg # used in teacache hook
cfg_parallel_ready = do_true_cfg and get_classifier_free_guidance_world_size() > 1
if cfg_parallel_ready:
cfg_group = get_cfg_group()
cfg_rank = get_classifier_free_guidance_rank()
if cfg_rank == 0:
local_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=prompt_embeds_mask,
encoder_hidden_states=prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=txt_seq_lens,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
else:
local_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=negative_prompt_embeds_mask,
encoder_hidden_states=negative_prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=negative_txt_seq_lens,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
local_pred = local_pred[:, : latents.size(1)]
gathered = cfg_group.all_gather(local_pred, separate_tensors=True)
if cfg_rank == 0:
noise_pred = gathered[0]
neg_noise_pred = gathered[1]
comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
noise_pred = comb_pred * (cond_norm / noise_norm)
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
cfg_group.broadcast(latents, src=0)
else:
# Forward pass for positive prompt (or unconditional if no CFG)
noise_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=prompt_embeds_mask,
encoder_hidden_states=prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=txt_seq_lens,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
noise_pred = noise_pred[:, : latents.size(1)]
# Forward pass for negative prompt (CFG)
if do_true_cfg:
neg_noise_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=negative_prompt_embeds_mask,
encoder_hidden_states=negative_prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=negative_txt_seq_lens,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
neg_noise_pred = neg_noise_pred[:, : latents.size(1)]
comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
noise_pred = comb_pred * (cond_norm / noise_norm)
# compute the previous noisy sample x_t -> x_t-1
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
return latents
def forward(
self,
req: OmniDiffusionRequest,
@@ -823,6 +706,8 @@ class QwenImageEditPipeline(
)
do_true_cfg = true_cfg_scale > 1 and has_neg_prompt
self.check_cfg_parallel_validity(true_cfg_scale, has_neg_prompt)
prompt_embeds, prompt_embeds_mask = self.encode_prompt(
prompt=prompt,
image=prompt_image, # Use resized image for prompt encoding
@@ -886,7 +771,6 @@ class QwenImageEditPipeline(
negative_prompt_embeds,
negative_prompt_embeds_mask,
latents,
image_latents,
img_shapes,
txt_seq_lens,
negative_txt_seq_lens,
@@ -894,6 +778,12 @@ class QwenImageEditPipeline(
do_true_cfg,
guidance,
true_cfg_scale,
image_latents=image_latents,
cfg_normalize=True,
additional_transformer_kwargs={
"return_dict": False,
"attention_kwargs": self.attention_kwargs,
},
)
self._current_timestep = None
@@ -23,14 +23,12 @@ from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, Qwe
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.parallel_state import (
get_cfg_group,
get_classifier_free_guidance_rank,
get_classifier_free_guidance_world_size,
)
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.interface import SupportImageInput
from vllm_omni.diffusion.models.qwen_image.cfg_parallel import (
QwenImageCFGParallelMixin,
)
from vllm_omni.diffusion.models.qwen_image.pipeline_qwen_image import calculate_shift
from vllm_omni.diffusion.models.qwen_image.pipeline_qwen_image_edit import (
calculate_dimensions,
@@ -168,7 +166,7 @@ def get_qwen_image_edit_plus_post_process_func(
return post_process_func
class QwenImageEditPlusPipeline(nn.Module, SupportImageInput):
class QwenImageEditPlusPipeline(nn.Module, SupportImageInput, QwenImageCFGParallelMixin):
def __init__(
self,
*,
@@ -530,116 +528,6 @@ class QwenImageEditPlusPipeline(nn.Module, SupportImageInput):
def interrupt(self):
return self._interrupt
def diffuse(
self,
prompt_embeds,
prompt_embeds_mask,
negative_prompt_embeds,
negative_prompt_embeds_mask,
latents,
image_latents,
img_shapes,
txt_seq_lens,
negative_txt_seq_lens,
timesteps,
do_true_cfg,
guidance,
true_cfg_scale,
):
"""Diffusion loop with optional image conditioning."""
self.scheduler.set_begin_index(0)
for i, t in enumerate(timesteps):
if self.interrupt:
continue
self._current_timestep = t
# broadcast to batch dimension and place on same device/dtype as latents
timestep = t.expand(latents.shape[0]).to(device=latents.device, dtype=latents.dtype)
# Concatenate image latents with noise latents if available
latent_model_input = latents
if image_latents is not None:
latent_model_input = torch.cat([latents, image_latents], dim=1)
self.transformer.do_true_cfg = do_true_cfg
cfg_parallel_ready = do_true_cfg and get_classifier_free_guidance_world_size() > 1
if cfg_parallel_ready:
cfg_group = get_cfg_group()
cfg_rank = get_classifier_free_guidance_rank()
if cfg_rank == 0:
local_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=prompt_embeds_mask,
encoder_hidden_states=prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=txt_seq_lens,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
else:
local_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=negative_prompt_embeds_mask,
encoder_hidden_states=negative_prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=negative_txt_seq_lens,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
local_pred = local_pred[:, : latents.size(1)]
gathered = cfg_group.all_gather(local_pred, separate_tensors=True)
if cfg_rank == 0:
noise_pred = gathered[0]
neg_noise_pred = gathered[1]
comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
noise_pred = comb_pred * (cond_norm / noise_norm)
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
cfg_group.broadcast(latents, src=0)
else:
noise_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=prompt_embeds_mask,
encoder_hidden_states=prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=txt_seq_lens,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
noise_pred = noise_pred[:, : latents.size(1)]
if do_true_cfg:
neg_noise_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=negative_prompt_embeds_mask,
encoder_hidden_states=negative_prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=negative_txt_seq_lens,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
neg_noise_pred = neg_noise_pred[:, : latents.size(1)]
comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
noise_pred = comb_pred * (cond_norm / noise_norm)
# compute the previous noisy sample x_t -> x_t-1
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
return latents
def forward(
self,
req: OmniDiffusionRequest,
@@ -773,6 +661,8 @@ class QwenImageEditPlusPipeline(nn.Module, SupportImageInput):
)
do_true_cfg = true_cfg_scale > 1 and has_neg_prompt
self.check_cfg_parallel_validity(true_cfg_scale, has_neg_prompt)
prompt_embeds, prompt_embeds_mask = self.encode_prompt(
prompt=prompt,
image=condition_images, # Use condition images for prompt encoding
@@ -840,7 +730,6 @@ class QwenImageEditPlusPipeline(nn.Module, SupportImageInput):
negative_prompt_embeds,
negative_prompt_embeds_mask,
latents,
image_latents,
img_shapes,
txt_seq_lens,
negative_txt_seq_lens,
@@ -848,6 +737,12 @@ class QwenImageEditPlusPipeline(nn.Module, SupportImageInput):
do_true_cfg,
guidance,
true_cfg_scale,
image_latents=image_latents,
cfg_normalize=True,
additional_transformer_kwargs={
"return_dict": False,
"attention_kwargs": self.attention_kwargs,
},
)
self._current_timestep = None
@@ -22,17 +22,15 @@ from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, Qwe
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.parallel_state import (
get_cfg_group,
get_classifier_free_guidance_rank,
get_classifier_free_guidance_world_size,
)
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.interface import SupportImageInput
from vllm_omni.diffusion.models.qwen_image.autoencoder_kl_qwenimage import (
AutoencoderKLQwenImage,
)
from vllm_omni.diffusion.models.qwen_image.cfg_parallel import (
QwenImageCFGParallelMixin,
)
from vllm_omni.diffusion.models.qwen_image.qwen_image_transformer import (
QwenImageTransformer2DModel,
)
@@ -191,7 +189,7 @@ def retrieve_latents(
raise AttributeError("Could not access latents of provided encoder_output")
class QwenImageLayeredPipeline(nn.Module, SupportImageInput):
class QwenImageLayeredPipeline(nn.Module, SupportImageInput, QwenImageCFGParallelMixin):
color_format = "RGBA"
def __init__(
@@ -553,138 +551,6 @@ the image\n<|vision_start|><|image_pad|><|vision_end|><|im_end|>\n<|im_start|>as
return latents
def diffuse(
self,
prompt_embeds,
prompt_embeds_mask,
negative_prompt_embeds,
negative_prompt_embeds_mask,
latents,
image_latents,
img_shapes,
txt_seq_lens,
negative_txt_seq_lens,
timesteps,
do_true_cfg,
guidance,
true_cfg_scale,
cfg_normalize,
is_rgb,
):
self.scheduler.set_begin_index(0)
for i, t in enumerate(timesteps):
if self.interrupt:
continue
self._current_timestep = t
latent_model_input = latents
if image_latents is not None:
latent_model_input = torch.cat([latents, image_latents], dim=1)
# broadcast to batch dimension in a way that's compatible with ONNX/Core ML
timestep = t.expand(latents.shape[0]).to(latents.dtype)
# Enable CFG-parallel: rank0 computes positive, rank1 computes negative.
cfg_parallel_ready = do_true_cfg and get_classifier_free_guidance_world_size() > 1
self.transformer.do_true_cfg = do_true_cfg # used in teacache hook
if cfg_parallel_ready:
cfg_group = get_cfg_group()
cfg_rank = get_classifier_free_guidance_rank()
if cfg_rank == 0:
local_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=prompt_embeds_mask,
encoder_hidden_states=prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=txt_seq_lens,
attention_kwargs=self.attention_kwargs,
additional_t_cond=is_rgb,
return_dict=False,
)[0]
local_pred = local_pred[:, : latents.size(1)]
else:
local_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=negative_prompt_embeds_mask,
encoder_hidden_states=negative_prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=negative_txt_seq_lens,
attention_kwargs=self.attention_kwargs,
additional_t_cond=is_rgb,
return_dict=False,
)[0]
local_pred = local_pred[:, : latents.size(1)]
gathered = cfg_group.all_gather(local_pred, separate_tensors=True)
if cfg_rank == 0:
noise_pred = gathered[0]
neg_noise_pred = gathered[1]
comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
if cfg_normalize:
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
noise_pred = comb_pred * (cond_norm / noise_norm)
else:
noise_pred = comb_pred
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
cfg_group.broadcast(latents, src=0)
else:
# Forward pass for positive prompt (or unconditional if no CFG)
noise_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=prompt_embeds_mask,
encoder_hidden_states=prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=txt_seq_lens,
attention_kwargs=self.attention_kwargs,
additional_t_cond=is_rgb,
return_dict=False,
)[0]
noise_pred = noise_pred[:, : latents.size(1)]
if do_true_cfg:
neg_noise_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=guidance,
encoder_hidden_states_mask=negative_prompt_embeds_mask,
encoder_hidden_states=negative_prompt_embeds,
img_shapes=img_shapes,
txt_seq_lens=negative_txt_seq_lens,
attention_kwargs=self.attention_kwargs,
additional_t_cond=is_rgb,
return_dict=False,
)[0]
neg_noise_pred = neg_noise_pred[:, : latents.size(1)]
comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
if cfg_normalize:
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
noise_pred = comb_pred * (cond_norm / noise_norm)
else:
noise_pred = comb_pred
# compute the previous noisy sample x_t -> x_t-1
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
# no need callback now
return latents
@property
def guidance_scale(self):
return self._guidance_scale
@@ -840,6 +706,8 @@ the image\n<|vision_start|><|image_pad|><|vision_end|><|im_end|>\n<|im_start|>as
)
do_true_cfg = true_cfg_scale > 1 and has_neg_prompt
self.check_cfg_parallel_validity(true_cfg_scale, has_neg_prompt)
prompt_embeds, prompt_embeds_mask = self.encode_prompt(
prompt=prompt,
prompt_embeds=prompt_embeds,
@@ -924,7 +792,6 @@ the image\n<|vision_start|><|image_pad|><|vision_end|><|im_end|>\n<|im_start|>as
negative_prompt_embeds,
negative_prompt_embeds_mask,
latents,
image_latents,
img_shapes,
txt_seq_lens,
negative_txt_seq_lens,
@@ -932,8 +799,13 @@ the image\n<|vision_start|><|image_pad|><|vision_end|><|im_end|>\n<|im_start|>as
do_true_cfg,
guidance,
true_cfg_scale,
cfg_normalize,
is_rgb,
image_latents=image_latents,
cfg_normalize=cfg_normalize,
additional_transformer_kwargs={
"return_dict": False,
"additional_t_cond": is_rgb,
"attention_kwargs": self.attention_kwargs,
},
)
self._current_timestep = None
+57 -28
View File
@@ -16,6 +16,7 @@ from transformers import CLIPTextModelWithProjection, CLIPTokenizer, T5EncoderMo
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.sd3.sd3_transformer import (
@@ -126,9 +127,7 @@ def retrieve_timesteps(
return timesteps, num_inference_steps
class StableDiffusion3Pipeline(
nn.Module,
):
class StableDiffusion3Pipeline(nn.Module, CFGParallelMixin):
def __init__(
self,
*,
@@ -498,15 +497,35 @@ class StableDiffusion3Pipeline(
def diffuse(
self,
prompt_embeds,
pooled_prompt_embeds,
negative_prompt_embeds,
negative_pooled_prompt_embeds,
latents,
timesteps,
do_cfg,
):
latents: torch.Tensor,
timesteps: torch.Tensor,
prompt_embeds: torch.Tensor,
pooled_prompt_embeds: torch.Tensor,
negative_prompt_embeds: torch.Tensor,
negative_pooled_prompt_embeds: torch.Tensor,
do_true_cfg: bool,
guidance_scale: float,
cfg_normalize: bool = False,
) -> torch.Tensor:
"""
Diffusion loop with optional classifier-free guidance.
Args:
latents: Noise latents to denoise
timesteps: Diffusion timesteps
prompt_embeds: Positive prompt embeddings
pooled_prompt_embeds: Pooled positive prompt embeddings
negative_prompt_embeds: Negative prompt embeddings
negative_pooled_prompt_embeds: Pooled negative prompt embeddings
do_true_cfg: Whether to apply CFG
guidance_scale: CFG scale factor
cfg_normalize: Whether to normalize CFG output (default: False)
Returns:
Denoised latents
"""
self.scheduler.set_begin_index(0)
for _, t in enumerate(timesteps):
if self.interrupt:
continue
@@ -515,29 +534,36 @@ class StableDiffusion3Pipeline(
# Broadcast timestep to match batch size
timestep = t.expand(latents.shape[0]).to(device=latents.device, dtype=latents.dtype)
transformer_kwargs = {
positive_kwargs = {
"hidden_states": latents,
"timestep": timestep,
"encoder_hidden_states": prompt_embeds,
"pooled_projections": pooled_prompt_embeds,
"return_dict": False,
}
noise_pred = self.transformer(**transformer_kwargs)[0]
if do_cfg:
neg_transformer_kwargs = {
if do_true_cfg:
negative_kwargs = {
"hidden_states": latents,
"timestep": timestep,
"encoder_hidden_states": negative_prompt_embeds,
"pooled_projections": negative_pooled_prompt_embeds,
"return_dict": False,
}
else:
negative_kwargs = None
# Predict noise with automatic CFG parallel handling
noise_pred = self.predict_noise_maybe_with_cfg(
do_true_cfg,
guidance_scale,
positive_kwargs,
negative_kwargs,
cfg_normalize,
)
# Compute the previous noisy sample x_t -> x_t-1 with automatic CFG sync
latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, do_true_cfg)
neg_noise_pred = self.transformer(**neg_transformer_kwargs)[0]
noise_pred = neg_noise_pred + self.guidance_scale * (noise_pred - neg_noise_pred)
# compute the previous noisy sample x_t -> x_t-1
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
return latents
def forward(
@@ -644,14 +670,17 @@ class StableDiffusion3Pipeline(
timesteps, num_inference_steps = self.prepare_timesteps(num_inference_steps, sigmas, latents.shape[1])
self._num_timesteps = len(timesteps)
# Denoising loop using diffuse method
latents = self.diffuse(
prompt_embeds,
pooled_prompt_embeds,
negative_prompt_embeds,
negative_pooled_prompt_embeds,
latents,
timesteps,
do_cfg,
latents=latents,
timesteps=timesteps,
prompt_embeds=prompt_embeds,
pooled_prompt_embeds=pooled_prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds if do_cfg else None,
negative_pooled_prompt_embeds=negative_pooled_prompt_embeds if do_cfg else None,
do_true_cfg=do_cfg,
guidance_scale=self.guidance_scale,
cfg_normalize=False,
)
self._current_timestep = None
@@ -7,7 +7,7 @@ import json
import logging
import os
from collections.abc import Iterable
from typing import cast
from typing import Any, cast
import PIL.Image
import torch
@@ -18,12 +18,14 @@ from transformers import AutoTokenizer, UMT5EncoderModel
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.schedulers import FlowUniPCMultistepScheduler
from vllm_omni.diffusion.models.wan2_2.wan2_2_transformer import WanTransformer3DModel
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.inputs.data import OmniTextPrompt
from vllm_omni.platforms import current_omni_platform
logger = logging.getLogger(__name__)
@@ -184,7 +186,7 @@ def get_wan22_pre_process_func(
return pre_process_func
class Wan22Pipeline(nn.Module):
class Wan22Pipeline(nn.Module, CFGParallelMixin):
def __init__(
self,
*,
@@ -585,26 +587,44 @@ class Wan22Pipeline(nn.Module):
latent_model_input = latents.to(dtype)
timestep = t.expand(latents.shape[0])
noise_pred = current_model(
hidden_states=latent_model_input,
timestep=timestep,
encoder_hidden_states=prompt_embeds,
attention_kwargs=attention_kwargs,
return_dict=False,
)[0]
do_true_cfg = current_guidance_scale > 1.0 and negative_prompt_embeds is not None
# Prepare kwargs for positive and negative predictions
positive_kwargs = {
"hidden_states": latent_model_input,
"timestep": timestep,
"encoder_hidden_states": prompt_embeds,
"attention_kwargs": attention_kwargs,
"return_dict": False,
"current_model": current_model,
}
if do_true_cfg:
negative_kwargs = {
"hidden_states": latent_model_input,
"timestep": timestep,
"encoder_hidden_states": negative_prompt_embeds,
"attention_kwargs": attention_kwargs,
"return_dict": False,
"current_model": current_model,
}
else:
negative_kwargs = None
if current_guidance_scale > 1.0 and negative_prompt_embeds is not None:
noise_uncond = current_model(
hidden_states=latent_model_input,
timestep=timestep,
encoder_hidden_states=negative_prompt_embeds,
attention_kwargs=attention_kwargs,
return_dict=False,
)[0]
noise_pred = noise_uncond + current_guidance_scale * (noise_pred - noise_uncond)
# Predict noise with automatic CFG parallel handling
noise_pred = self.predict_noise_maybe_with_cfg(
do_true_cfg=do_true_cfg,
true_cfg_scale=current_guidance_scale,
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
cfg_normalize=False,
)
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
# Compute the previous noisy sample x_t -> x_t-1 with automatic CFG sync
latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, do_true_cfg)
# Wan2.2 is prone to out of memory errors when predicting large videos
# so we empty the cache here to avoid OOM before vae decoding.
if current_omni_platform.is_available():
current_omni_platform.empty_cache()
self._current_timestep = None
# For I2V mode: blend final latents with condition
@@ -629,6 +649,21 @@ class Wan22Pipeline(nn.Module):
return DiffusionOutput(output=output)
def predict_noise(self, current_model: nn.Module | None = None, **kwargs: Any) -> torch.Tensor:
"""
Forward pass through transformer to predict noise.
Args:
current_model: The transformer model to use (transformer or transformer_2)
**kwargs: Arguments to pass to the transformer
Returns:
Predicted noise tensor
"""
if current_model is None:
current_model = self.transformer
return current_model(**kwargs)[0]
def encode_prompt(
self,
prompt: str | list[str],
@@ -6,7 +6,7 @@ from __future__ import annotations
import logging
import os
from collections.abc import Iterable
from typing import cast
from typing import Any, cast
import numpy as np
import PIL.Image
@@ -18,6 +18,7 @@ from transformers import AutoTokenizer, CLIPImageProcessor, CLIPVisionModel, UMT
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.interface import SupportImageInput
@@ -29,6 +30,7 @@ from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2 import (
)
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.inputs.data import OmniTextPrompt
from vllm_omni.platforms import current_omni_platform
logger = logging.getLogger(__name__)
@@ -136,7 +138,7 @@ def get_wan22_i2v_pre_process_func(
return pre_process_func
class Wan22I2VPipeline(nn.Module, SupportImageInput):
class Wan22I2VPipeline(nn.Module, SupportImageInput, CFGParallelMixin):
"""
Wan2.2 Image-to-Video Pipeline.
@@ -484,31 +486,46 @@ class Wan22I2VPipeline(nn.Module, SupportImageInput):
latent_model_input = torch.cat([latents, condition], dim=1).to(dtype)
timestep = t.expand(latents.shape[0])
# Forward pass
noise_pred = current_model(
hidden_states=latent_model_input,
timestep=timestep,
encoder_hidden_states=prompt_embeds,
encoder_hidden_states_image=image_embeds,
attention_kwargs=attention_kwargs,
return_dict=False,
)[0]
do_true_cfg = current_guidance_scale > 1.0 and negative_prompt_embeds is not None
# Prepare kwargs for positive and negative predictions
positive_kwargs = {
"hidden_states": latent_model_input,
"timestep": timestep,
"encoder_hidden_states": prompt_embeds,
"encoder_hidden_states_image": image_embeds,
"attention_kwargs": attention_kwargs,
"return_dict": False,
"current_model": current_model,
}
if do_true_cfg:
negative_kwargs = {
"hidden_states": latent_model_input,
"timestep": timestep,
"encoder_hidden_states": negative_prompt_embeds,
"encoder_hidden_states_image": image_embeds,
"attention_kwargs": attention_kwargs,
"return_dict": False,
"current_model": current_model,
}
else:
negative_kwargs = None
# Classifier-free guidance
if current_guidance_scale > 1.0 and negative_prompt_embeds is not None:
noise_uncond = current_model(
hidden_states=latent_model_input,
timestep=timestep,
encoder_hidden_states=negative_prompt_embeds,
encoder_hidden_states_image=image_embeds,
attention_kwargs=attention_kwargs,
return_dict=False,
)[0]
noise_pred = noise_uncond + current_guidance_scale * (noise_pred - noise_uncond)
# Predict noise with automatic CFG parallel handling
noise_pred = self.predict_noise_maybe_with_cfg(
do_true_cfg=do_true_cfg,
true_cfg_scale=current_guidance_scale,
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
cfg_normalize=False,
)
# Scheduler step
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
# Compute the previous noisy sample x_t -> x_t-1 with automatic CFG sync
latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, do_true_cfg)
# Wan2.2 is prone to out of memory errors when predicting large videos
# so we empty the cache here to avoid OOM before vae decoding.
if current_omni_platform.is_available():
current_omni_platform.empty_cache()
self._current_timestep = None
# For expand_timesteps mode, blend final latents with condition
@@ -533,6 +550,21 @@ class Wan22I2VPipeline(nn.Module, SupportImageInput):
return DiffusionOutput(output=output)
def predict_noise(self, current_model: nn.Module | None = None, **kwargs: Any) -> torch.Tensor:
"""
Forward pass through transformer to predict noise.
Args:
current_model: The transformer model to use (transformer or transformer_2)
**kwargs: Arguments to pass to the transformer
Returns:
Predicted noise tensor
"""
if current_model is None:
current_model = self.transformer
return current_model(**kwargs)[0]
def encode_prompt(
self,
prompt: str | list[str],
@@ -19,7 +19,7 @@ from __future__ import annotations
import logging
import os
from collections.abc import Iterable
from typing import cast
from typing import Any, cast
import numpy as np
import PIL.Image
@@ -31,6 +31,7 @@ from transformers import AutoTokenizer, UMT5EncoderModel
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.interface import SupportImageInput
@@ -42,6 +43,7 @@ from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2 import (
)
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.inputs.data import OmniTextPrompt
from vllm_omni.platforms import current_omni_platform
logger = logging.getLogger(__name__)
@@ -126,7 +128,7 @@ def get_wan22_ti2v_pre_process_func(
return pre_process_func
class Wan22TI2VPipeline(nn.Module, SupportImageInput):
class Wan22TI2VPipeline(nn.Module, SupportImageInput, CFGParallelMixin):
"""
Wan2.2 Text-Image-to-Video (TI2V) Pipeline.
@@ -399,29 +401,44 @@ class Wan22TI2VPipeline(nn.Module, SupportImageInput):
temp_ts = (mask[0][0][:, ::2, ::2] * t).flatten()
timestep = temp_ts.unsqueeze(0).expand(latents.shape[0], -1)
# Forward pass
noise_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep,
encoder_hidden_states=prompt_embeds,
attention_kwargs=attention_kwargs,
return_dict=False,
)[0]
do_true_cfg = guidance_scale > 1.0 and negative_prompt_embeds is not None
# Prepare kwargs for positive and negative predictions
positive_kwargs = {
"hidden_states": latent_model_input,
"timestep": timestep,
"encoder_hidden_states": prompt_embeds,
"attention_kwargs": attention_kwargs,
"return_dict": False,
"current_model": self.transformer,
}
if do_true_cfg:
negative_kwargs = {
"hidden_states": latent_model_input,
"timestep": timestep,
"encoder_hidden_states": negative_prompt_embeds,
"attention_kwargs": attention_kwargs,
"return_dict": False,
"current_model": self.transformer,
}
else:
negative_kwargs = None
# Classifier-free guidance
if guidance_scale > 1.0 and negative_prompt_embeds is not None:
noise_uncond = self.transformer(
hidden_states=latent_model_input,
timestep=timestep,
encoder_hidden_states=negative_prompt_embeds,
attention_kwargs=attention_kwargs,
return_dict=False,
)[0]
noise_pred = noise_uncond + guidance_scale * (noise_pred - noise_uncond)
# Predict noise with automatic CFG parallel handling
noise_pred = self.predict_noise_maybe_with_cfg(
do_true_cfg=do_true_cfg,
true_cfg_scale=guidance_scale,
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
cfg_normalize=False,
)
# Scheduler step
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
# Compute the previous noisy sample x_t -> x_t-1 with automatic CFG sync
latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, do_true_cfg)
# Wan2.2 is prone to out of memory errors when predicting large videos
# so we empty the cache here to avoid OOM before vae decoding.
if current_omni_platform.is_available():
current_omni_platform.empty_cache()
self._current_timestep = None
# For I2V mode, blend final latents with condition
@@ -446,6 +463,21 @@ class Wan22TI2VPipeline(nn.Module, SupportImageInput):
return DiffusionOutput(output=output)
def predict_noise(self, current_model: nn.Module | None = None, **kwargs: Any) -> torch.Tensor:
"""
Forward pass through transformer to predict noise.
Args:
current_model: The transformer model to use
**kwargs: Arguments to pass to the transformer
Returns:
Predicted noise tensor
"""
if current_model is None:
current_model = self.transformer
return current_model(**kwargs)[0]
def encode_prompt(
self,
prompt: str | list[str],
+6 -1
View File
@@ -225,7 +225,12 @@ class OmniServeCommand(CLISubcommand):
help="Scheduler flow_shift for video models (e.g., 5.0 for 720p, 12.0 for 480p).",
)
omni_config_group.add_argument(
"--cfg-parallel-size", type=int, default=1, help="Number of GPUs for CFG parallel computation"
"--cfg-parallel-size",
type=int,
default=1,
choices=[1, 2],
help="Number of devices for CFG parallel computation for diffusion models. "
"Equivalent to setting DiffusionParallelConfig.cfg_parallel_size.",
)
return serve_parser