Files
vllm-omni/vllm_omni/diffusion/models/wan2_2/pipeline_wan2_2.py
T
tzhouam 617c6ca55f feat: enhance model loading with prefetching and retry logic
- Introduced `from_pretrained_with_prefetch` to handle racy cache scenarios by re-prefetching and retrying on failures.
- Updated various model pipelines to utilize the new prefetching mechanism, ensuring robust loading of model components.
- Prefetch logic added to multiple models, including HunyuanVideo, LongCatImage, and QwenImage, to mitigate issues with incomplete cache states.
2026-06-02 09:01:47 +00:00

1021 lines
44 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import json
import logging
import os
import time
from collections.abc import Iterable
from typing import Any, cast
import PIL.Image
import torch
from diffusers.utils.torch_utils import randn_tensor
from torch import nn
from transformers import AutoTokenizer, UMT5EncoderModel
from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm.sequence import IntermediateTensors
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_wan import DistributedAutoencoderKLWan
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.pipeline_parallel import AsyncLatents, PipelineParallelMixin
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.forward_context import set_forward_context_denoise_step_idx
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.model_loader.hub_prefetch import from_pretrained_with_prefetch, prefetch_subfolders
from vllm_omni.diffusion.models.dmd2 import DMD2PipelineMixin
from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin, _is_rank_zero
from vllm_omni.diffusion.models.schedulers import FlowUniPCMultistepScheduler
from vllm_omni.diffusion.models.wan2_2.scheduling_wan_euler import WanEulerScheduler
from vllm_omni.diffusion.models.wan2_2.wan2_2_transformer import WanTransformer3DModel
from vllm_omni.diffusion.postprocess import interpolate_video_tensor
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
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__)
DEBUG_PERF = False
WAN_SAMPLE_SOLVER_CHOICES = {"unipc", "euler"}
def build_wan_scheduler(sample_solver: str, flow_shift: float) -> Any:
if sample_solver == "unipc":
return FlowUniPCMultistepScheduler(
num_train_timesteps=1000,
shift=flow_shift,
prediction_type="flow_prediction",
)
if sample_solver == "euler":
return WanEulerScheduler(
num_train_timesteps=1000,
shift=flow_shift,
)
raise ValueError(
f"Unsupported Wan sample_solver: {sample_solver}. Expected one of: {sorted(WAN_SAMPLE_SOLVER_CHOICES)}"
)
def resolve_wan_sample_solver(req: OmniDiffusionRequest, default: str = "unipc") -> str:
extra_args = getattr(req.sampling_params, "extra_args", {}) or {}
raw = extra_args.get("sample_solver", default)
sample_solver = str(raw).strip().lower()
if sample_solver not in WAN_SAMPLE_SOLVER_CHOICES:
raise ValueError(f"Invalid sample_solver={raw!r}. Expected one of: {sorted(WAN_SAMPLE_SOLVER_CHOICES)}")
return sample_solver
def resolve_wan_flow_shift(req: OmniDiffusionRequest, od_config: OmniDiffusionConfig) -> float:
extra_args = getattr(req.sampling_params, "extra_args", {}) or {}
raw_flow_shift = extra_args.get("flow_shift")
if raw_flow_shift is None:
raw_flow_shift = od_config.flow_shift if od_config.flow_shift is not None else 5.0
try:
return float(raw_flow_shift)
except (TypeError, ValueError) as exc:
raise ValueError(f"Invalid flow_shift={raw_flow_shift!r}. flow_shift must be a float.") from exc
def retrieve_latents(
encoder_output: torch.Tensor,
generator: torch.Generator | None = None,
sample_mode: str = "sample",
):
"""Retrieve latents from VAE encoder output."""
if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
return encoder_output.latent_dist.sample(generator)
elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
return encoder_output.latent_dist.mode()
elif hasattr(encoder_output, "latents"):
return encoder_output.latents
else:
raise AttributeError("Could not access latents of provided encoder_output")
def load_transformer_config(model_path: str, subfolder: str = "transformer", local_files_only: bool = True) -> dict:
"""Load transformer config from model directory or HF Hub."""
if local_files_only:
config_path = os.path.join(model_path, subfolder, "config.json")
if os.path.exists(config_path):
with open(config_path) as f:
return json.load(f)
else:
# Try to download config from HF Hub
try:
from huggingface_hub import hf_hub_download
config_path = hf_hub_download(
repo_id=model_path,
filename=f"{subfolder}/config.json",
)
with open(config_path) as f:
return json.load(f)
except Exception:
pass
return {}
def create_transformer_from_config(
config: dict, quant_config: QuantizationConfig | None = None, prefix: str = ""
) -> WanTransformer3DModel:
"""Create WanTransformer3DModel from config dict."""
kwargs: dict = {}
if "patch_size" in config:
kwargs["patch_size"] = tuple(config["patch_size"])
if "num_attention_heads" in config:
kwargs["num_attention_heads"] = config["num_attention_heads"]
if "attention_head_dim" in config:
kwargs["attention_head_dim"] = config["attention_head_dim"]
if "in_channels" in config:
kwargs["in_channels"] = config["in_channels"]
if "out_channels" in config:
kwargs["out_channels"] = config["out_channels"]
if "text_dim" in config:
kwargs["text_dim"] = config["text_dim"]
if "freq_dim" in config:
kwargs["freq_dim"] = config["freq_dim"]
if "ffn_dim" in config:
kwargs["ffn_dim"] = config["ffn_dim"]
if "num_layers" in config:
kwargs["num_layers"] = config["num_layers"]
if "cross_attn_norm" in config:
kwargs["cross_attn_norm"] = config["cross_attn_norm"]
if "eps" in config:
kwargs["eps"] = config["eps"]
if "image_dim" in config:
kwargs["image_dim"] = config["image_dim"]
if "added_kv_proj_dim" in config:
kwargs["added_kv_proj_dim"] = config["added_kv_proj_dim"]
if "rope_max_seq_len" in config:
kwargs["rope_max_seq_len"] = config["rope_max_seq_len"]
if "pos_embed_seq_len" in config:
kwargs["pos_embed_seq_len"] = config["pos_embed_seq_len"]
if "quantization_config" in config:
from vllm_omni.quantization.factory import resolve_quant_config_from_disk
quant_config = resolve_quant_config_from_disk(quant_config, config["quantization_config"])
if quant_config is not None:
kwargs["quant_config"] = quant_config
if prefix:
kwargs["prefix"] = prefix
return WanTransformer3DModel(**kwargs)
def get_wan22_post_process_func(
od_config: OmniDiffusionConfig,
):
from diffusers.video_processor import VideoProcessor
video_processor = VideoProcessor(vae_scale_factor=8)
def post_process_func(
video: torch.Tensor,
output_type: str = "np",
sampling_params=None,
):
if output_type == "latent":
return video
custom_output = {}
if sampling_params is not None and getattr(sampling_params, "enable_frame_interpolation", False):
video, multiplier = interpolate_video_tensor(
video,
exp=sampling_params.frame_interpolation_exp,
scale=sampling_params.frame_interpolation_scale,
model_path=sampling_params.frame_interpolation_model_path,
)
custom_output["video_fps_multiplier"] = multiplier
return {
"video": video_processor.postprocess_video(video, output_type=output_type),
"custom_output": custom_output,
}
return post_process_func
def get_wan22_pre_process_func(
od_config: OmniDiffusionConfig,
):
"""Pre-process function for Wan2.2: optionally load and resize input image for I2V mode."""
import numpy as np
def pre_process_func(request: OmniDiffusionRequest) -> OmniDiffusionRequest:
for i, prompt in enumerate(request.prompts):
multi_modal_data = prompt.get("multi_modal_data", {}) if not isinstance(prompt, str) else None
raw_image = multi_modal_data.get("image", None) if multi_modal_data is not None else None
if isinstance(prompt, str):
prompt = OmniTextPrompt(prompt=prompt)
if "additional_information" not in prompt:
prompt["additional_information"] = {}
if raw_image is None:
continue
if not isinstance(raw_image, (str, PIL.Image.Image)):
raise TypeError(
f"""Unsupported image format {raw_image.__class__}.""",
"""Please correctly set `"multi_modal_data": {"image": <an image object or file path>, …}`""",
)
image = PIL.Image.open(raw_image).convert("RGB") if isinstance(raw_image, str) else raw_image
# Calculate dimensions based on aspect ratio if not provided
if request.sampling_params.height is None or request.sampling_params.width is None:
# Default max area for 720P
max_area = 720 * 1280
aspect_ratio = image.height / image.width
# Calculate dimensions maintaining aspect ratio
mod_value = 16 # Must be divisible by 16
height = round(np.sqrt(max_area * aspect_ratio)) // mod_value * mod_value
width = round(np.sqrt(max_area / aspect_ratio)) // mod_value * mod_value
if request.sampling_params.height is None:
request.sampling_params.height = height
if request.sampling_params.width is None:
request.sampling_params.width = width
# Resize image to target dimensions
image = image.resize(
(request.sampling_params.width, request.sampling_params.height), # type: ignore # Above has ensured that width & height are not None
PIL.Image.Resampling.LANCZOS,
)
prompt["multi_modal_data"]["image"] = image # type: ignore # key existence already checked above
request.prompts[i] = prompt
return request
return pre_process_func
class Wan22Pipeline(
nn.Module, PipelineParallelMixin, CFGParallelMixin, ProgressBarMixin, DiffusionPipelineProfilerMixin
):
def __init__(
self,
*,
od_config: OmniDiffusionConfig,
prefix: str = "",
):
super().__init__()
self.od_config = od_config
self.device = get_local_device()
dtype = getattr(od_config, "dtype", torch.bfloat16)
model = od_config.model
local_files_only = os.path.exists(model)
# Read model_index.json to detect expand_timesteps mode (for TI2V-5B)
self.expand_timesteps = False
self.has_transformer_2 = False
if local_files_only:
model_index_path = os.path.join(model, "model_index.json")
if os.path.exists(model_index_path):
with open(model_index_path) as f:
model_index = json.load(f)
self.expand_timesteps = model_index.get("expand_timesteps", False)
# Check if this is a two-stage model (MoE with transformer_2)
transformer_2_path = os.path.join(model, "transformer_2")
self.has_transformer_2 = os.path.exists(transformer_2_path)
else:
# For remote models, download and read model_index.json
try:
from huggingface_hub import hf_hub_download
model_index_path = hf_hub_download(repo_id=model, filename="model_index.json")
with open(model_index_path) as f:
model_index = json.load(f)
self.expand_timesteps = model_index.get("expand_timesteps", False)
# Check transformer_2 from model_index
transformer_2_info = model_index.get("transformer_2", [None, None])
self.has_transformer_2 = transformer_2_info[0] is not None
except Exception:
pass
self.boundary_ratio = od_config.boundary_ratio
# Determine which transformers to load based on boundary_ratio
# boundary_ratio=1.0: only load transformer_2 (low-noise stage only)
# boundary_ratio=0.0: only load transformer (high-noise stage only)
# otherwise: load both transformers
load_transformer = self.boundary_ratio != 1.0 if self.boundary_ratio is not None else True
load_transformer_2 = self.has_transformer_2 and (
self.boundary_ratio != 0.0 if self.boundary_ratio is not None else True
)
# Set up weights sources for transformer(s)
self.weights_sources = []
if load_transformer:
self.weights_sources.append(
DiffusersPipelineLoader.ComponentSource(
model_or_path=od_config.model,
subfolder="transformer",
revision=None,
prefix="transformer.",
fall_back_to_pt=True,
)
)
if load_transformer_2:
self.weights_sources.append(
DiffusersPipelineLoader.ComponentSource(
model_or_path=od_config.model,
subfolder="transformer_2",
revision=None,
prefix="transformer_2.",
fall_back_to_pt=True,
)
)
# See ``hub_prefetch.py`` for the transformers v5 subfolder race.
component_subfolders = ["tokenizer", "text_encoder", "vae"]
prefetch_subfolders(
model,
component_subfolders,
local_files_only=local_files_only,
)
# ``from_pretrained_with_prefetch`` re-prefetches and retries if the
# cache is still half-written (the missing-shard ``OSError`` and the
# default-``UMT5Config`` size-mismatch ``RuntimeError`` seen on multi
# -worker HSDP / ring launches), instead of crashing the worker.
self.tokenizer = from_pretrained_with_prefetch(
AutoTokenizer.from_pretrained,
model,
subfolder="tokenizer",
prefetch_list=component_subfolders,
local_files_only=local_files_only,
)
self.text_encoder = from_pretrained_with_prefetch(
UMT5EncoderModel.from_pretrained,
model,
subfolder="text_encoder",
prefetch_list=component_subfolders,
local_files_only=local_files_only,
torch_dtype=dtype,
).to(self.device)
self.vae = from_pretrained_with_prefetch(
DistributedAutoencoderKLWan.from_pretrained,
model,
subfolder="vae",
prefetch_list=component_subfolders,
local_files_only=local_files_only,
torch_dtype=dtype,
).to(self.device)
# Initialize transformers with correct config (weights loaded via load_weights)
if load_transformer:
transformer_config = load_transformer_config(model, "transformer", local_files_only)
self.transformer = self._create_transformer(transformer_config)
else:
self.transformer = None
if load_transformer_2:
transformer_2_config = load_transformer_config(model, "transformer_2", local_files_only)
self.transformer_2 = self._create_transformer(transformer_2_config)
else:
self.transformer_2 = None
# Store the active transformer config
if load_transformer:
self.transformer_config = self.transformer.config
elif load_transformer_2:
self.transformer_config = self.transformer_2.config
else:
raise RuntimeError("No transformer loaded")
self._sample_solver = "unipc"
self._flow_shift = od_config.flow_shift if od_config.flow_shift is not None else 5.0
self.scheduler = build_wan_scheduler(self._sample_solver, self._flow_shift)
self.vae_scale_factor_temporal = self.vae.config.scale_factor_temporal if getattr(self, "vae", None) else 4
self.vae_scale_factor_spatial = self.vae.config.scale_factor_spatial if getattr(self, "vae", None) else 8
self._guidance_scale = None
self._guidance_scale_2 = None
self._num_timesteps = None
self._current_timestep = None
self.setup_diffusion_pipeline_profiler(
enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler
)
def _create_transformer(self, config: dict) -> WanTransformer3DModel:
"""Create a transformer from a config dict. Respects od_config.quantization_config."""
quant_config = getattr(self.od_config, "quantization_config", None)
return create_transformer_from_config(config, quant_config=quant_config)
@property
def guidance_scale(self):
return self._guidance_scale
@property
def do_classifier_free_guidance(self):
return self._guidance_scale is not None and self._guidance_scale > 1.0
@property
def num_timesteps(self):
return self._num_timesteps
@property
def current_timestep(self):
return self._current_timestep
def diffuse(
self,
latents: torch.Tensor,
timesteps: torch.Tensor,
prompt_embeds: torch.Tensor,
negative_prompt_embeds: torch.Tensor | None,
guidance_low: float,
guidance_high: float,
boundary_timestep: float | None,
dtype: torch.dtype,
attention_kwargs: dict[str, Any],
latent_condition: torch.Tensor | None = None,
first_frame_mask: torch.Tensor | None = None,
) -> torch.Tensor | AsyncLatents:
if attention_kwargs is None:
attention_kwargs = {}
with self.progress_bar(total=len(timesteps)) as pbar:
for step_idx, t in enumerate(timesteps):
self._current_timestep = t
set_forward_context_denoise_step_idx(step_idx)
# Select model based on timestep and boundary_ratio
# High noise stage (t >= boundary_timestep): use transformer
# Low noise stage (t < boundary_timestep): use transformer_2
if boundary_timestep is not None and t < boundary_timestep:
# Low noise stage - always use guidance_high for this stage
current_guidance_scale = guidance_high
if self.transformer_2 is not None:
current_model = self.transformer_2
elif self.transformer is not None:
# Fallback to transformer if transformer_2 not loaded
current_model = self.transformer
else:
raise RuntimeError("No transformer available for low-noise stage")
else:
# High noise stage - always use guidance_low for this stage
current_guidance_scale = guidance_low
if self.transformer is not None:
current_model = self.transformer
elif self.transformer_2 is not None:
# Fallback to transformer_2 if transformer not loaded
current_model = self.transformer_2
else:
raise RuntimeError("No transformer available for high-noise stage")
if self.expand_timesteps and latent_condition is not None:
# I2V mode: blend condition with latents using mask
latent_model_input = (1 - first_frame_mask) * latent_condition + first_frame_mask * latents
latent_model_input = latent_model_input.to(dtype)
# Expand timesteps per patch - use floor division to match patch embedding
patch_size = self.transformer_config.patch_size
patch_height = latents.shape[3] // patch_size[1]
patch_width = latents.shape[4] // patch_size[2]
# Create mask at patch resolution (same as hidden states sequence length)
patch_mask = first_frame_mask[:, :, :, :: patch_size[1], :: patch_size[2]]
patch_mask = patch_mask[:, :, :, :patch_height, :patch_width] # Ensure correct dimensions
temp_ts = (patch_mask[0][0] * t).flatten()
timestep = temp_ts.unsqueeze(0).expand(latents.shape[0], -1)
else:
# T2V mode: standard forward
latent_model_input = latents.to(dtype)
timestep = t.expand(latents.shape[0])
do_true_cfg = current_guidance_scale > 1.0 and negative_prompt_embeds is not None
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
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_maybe_with_cfg(noise_pred, t, latents, do_true_cfg)
pbar.update()
return latents
def forward(
self,
req: OmniDiffusionRequest,
prompt: str | None = None,
negative_prompt: str | None = None,
height: int = 480,
width: int = 832,
num_inference_steps: int = 40,
guidance_scale: float | tuple[float, float] = 4.0,
frame_num: int = 81,
output_type: str | None = "np",
generator: torch.Generator | list[torch.Generator] | None = None,
prompt_embeds: torch.Tensor | None = None,
negative_prompt_embeds: torch.Tensor | None = None,
attention_kwargs: dict | None = None,
**kwargs,
) -> DiffusionOutput:
# Get parameters from request or arguments
if len(req.prompts) > 1:
raise ValueError(
"""This model only supports a single prompt, not a batched request.""",
"""Please pass in a single prompt object or string, or a single-item list.""",
)
if len(req.prompts) == 1: # If req.prompt is empty, default to prompt & neg_prompt in param list
prompt = req.prompts[0] if isinstance(req.prompts[0], str) else req.prompts[0].get("prompt")
negative_prompt = None if isinstance(req.prompts[0], str) else req.prompts[0].get("negative_prompt")
if prompt is None and prompt_embeds is None:
raise ValueError("Prompt or prompt_embeds is required for Wan2.2 generation.")
height = req.sampling_params.height or height
width = req.sampling_params.width or width
num_frames = req.sampling_params.num_frames if req.sampling_params.num_frames else frame_num
# Ensure dimensions are compatible with VAE and patch size
# For expand_timesteps mode, we need latent dims to be even (divisible by patch_size)
patch_size = self.transformer_config.patch_size
mod_value = self.vae_scale_factor_spatial * patch_size[1] # 16*2=32 for TI2V, 8*2=16 for I2V
height = (height // mod_value) * mod_value
width = (width // mod_value) * mod_value
num_steps = req.sampling_params.num_inference_steps or num_inference_steps
# Respect per-request guidance_scale when explicitly provided.
if req.sampling_params.guidance_scale_provided:
guidance_scale = req.sampling_params.guidance_scale
guidance_low = guidance_scale if isinstance(guidance_scale, (int, float)) else guidance_scale[0]
guidance_high = (
req.sampling_params.guidance_scale_2
if req.sampling_params.guidance_scale_2 is not None
else (
guidance_scale[1]
if isinstance(guidance_scale, (list, tuple)) and len(guidance_scale) > 1
else guidance_low
)
)
# record guidance for properties
self._guidance_scale = guidance_low
self._guidance_scale_2 = guidance_high
# Prefer engine-configured boundary_ratio, but allow per-request fallback.
boundary_ratio = self.boundary_ratio if self.boundary_ratio is not None else req.sampling_params.boundary_ratio
if boundary_ratio is None:
boundary_ratio = 0.875
logger.warning("boundary_ratio is required for T2V generation. using default value 0.875")
# validate shapes
self.check_inputs(
prompt=prompt,
negative_prompt=negative_prompt,
height=height,
width=width,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
guidance_scale_2=guidance_high if boundary_ratio is not None else None,
boundary_ratio=boundary_ratio,
)
if num_frames % self.vae_scale_factor_temporal != 1:
num_frames = num_frames // self.vae_scale_factor_temporal * self.vae_scale_factor_temporal + 1
num_frames = max(num_frames, 1)
device = self.device
# Get dtype from whichever transformer is loaded
if self.transformer is not None:
dtype = self.transformer.dtype
elif self.transformer_2 is not None:
dtype = self.transformer_2.dtype
else:
# Fallback to text_encoder dtype if no transformer loaded
dtype = self.text_encoder.dtype
# Seed / generator
if generator is None:
generator = req.sampling_params.generator
if generator is None and req.sampling_params.seed is not None:
generator = torch.Generator(device=device).manual_seed(req.sampling_params.seed)
if DEBUG_PERF:
# Sync GPU before timing to ensure accurate measurements
current_omni_platform.synchronize()
_t_pipeline_start = time.perf_counter()
_t_text_enc_start = _t_pipeline_start
if prompt_embeds is None:
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
prompt=prompt,
negative_prompt=negative_prompt,
do_classifier_free_guidance=guidance_low > 1.0 or guidance_high > 1.0,
num_videos_per_prompt=req.sampling_params.num_outputs_per_prompt or 1,
max_sequence_length=req.sampling_params.max_sequence_length or 512,
device=device,
dtype=dtype,
)
else:
prompt_embeds = prompt_embeds.to(device=device, dtype=dtype)
if negative_prompt_embeds is not None:
negative_prompt_embeds = negative_prompt_embeds.to(device=device, dtype=dtype)
elif guidance_low > 1.0 or guidance_high > 1.0:
raise ValueError(
"negative_prompt_embeds must be provided when prompt_embeds are given and guidance > 1."
)
if DEBUG_PERF:
current_omni_platform.synchronize()
_t_text_enc_ms = (time.perf_counter() - _t_text_enc_start) * 1000
sample_solver = resolve_wan_sample_solver(req, default=self._sample_solver)
flow_shift = resolve_wan_flow_shift(req, self.od_config)
if sample_solver != self._sample_solver or abs(flow_shift - self._flow_shift) > 1e-6:
self.scheduler = build_wan_scheduler(sample_solver, flow_shift)
self._sample_solver = sample_solver
self._flow_shift = flow_shift
# Timesteps
self.scheduler.set_timesteps(num_steps, device=device)
timesteps = self.scheduler.timesteps
self._num_timesteps = len(timesteps)
boundary_timestep = None
if boundary_ratio is not None:
boundary_timestep = boundary_ratio * self.scheduler.config.num_train_timesteps
if DEBUG_PERF:
_t_latent_prep_start = time.perf_counter()
multi_modal_data = req.prompts[0].get("multi_modal_data", {}) if not isinstance(req.prompts[0], str) else None
raw_image = multi_modal_data.get("image", None) if multi_modal_data is not None else None
if isinstance(raw_image, list):
if len(raw_image) > 1:
logger.warning(
"""Received a list of image. Only a single image is supported by this model."""
"""Taking only the first image for now."""
)
raw_image = raw_image[0]
if raw_image is None:
image = None
elif isinstance(raw_image, str):
image = PIL.Image.open(raw_image)
else:
image = cast(PIL.Image.Image | torch.Tensor, raw_image)
latent_condition = None
first_frame_mask = None
if self.expand_timesteps and image is not None:
# I2V mode: encode image and prepare condition
from diffusers.video_processor import VideoProcessor
video_processor = VideoProcessor(vae_scale_factor=self.vae_scale_factor_spatial)
# Preprocess image
if isinstance(image, PIL.Image.Image):
image = image.resize((width, height), PIL.Image.Resampling.LANCZOS)
image_tensor = video_processor.preprocess(image, height=height, width=width)
else:
image_tensor = image
# Use out_channels for noise latents (not in_channels which includes condition)
num_channels_latents = self.transformer_config.out_channels
batch_size = prompt_embeds.shape[0]
# Prepare noise latents
latents = self.prepare_latents(
batch_size=batch_size,
num_channels_latents=num_channels_latents,
height=height,
width=width,
num_frames=num_frames,
dtype=torch.float32,
device=device,
generator=generator,
latents=req.sampling_params.latents,
)
# Encode image condition
num_latent_frames = latents.shape[2]
latent_height = latents.shape[3]
latent_width = latents.shape[4]
image_tensor = image_tensor.unsqueeze(2) # [B, C, 1, H, W]
image_tensor = image_tensor.to(device=device, dtype=self.vae.dtype)
latent_condition = retrieve_latents(self.vae.encode(image_tensor), sample_mode="argmax")
latent_condition = latent_condition.repeat(batch_size, 1, 1, 1, 1)
# Normalize condition latents
latents_mean = (
torch.tensor(self.vae.config.latents_mean)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(latent_condition.device, latent_condition.dtype)
)
latents_std = 1.0 / torch.tensor(self.vae.config.latents_std).view(1, self.vae.config.z_dim, 1, 1, 1).to(
latent_condition.device, latent_condition.dtype
)
latent_condition = (latent_condition - latents_mean) * latents_std
latent_condition = latent_condition.to(torch.float32)
# Create mask: 0 for first frame (condition), 1 for rest (to denoise)
first_frame_mask = torch.ones(
1, 1, num_latent_frames, latent_height, latent_width, dtype=torch.float32, device=device
)
first_frame_mask[:, :, 0] = 0
else:
# T2V mode: standard latent preparation
num_channels_latents = self.transformer_config.in_channels
latents = self.prepare_latents(
batch_size=prompt_embeds.shape[0],
num_channels_latents=num_channels_latents,
height=height,
width=width,
num_frames=num_frames,
dtype=torch.float32,
device=device,
generator=generator,
latents=req.sampling_params.latents,
)
if DEBUG_PERF:
current_omni_platform.synchronize()
_t_latent_prep_ms = (time.perf_counter() - _t_latent_prep_start) * 1000
if attention_kwargs is None:
attention_kwargs = {}
if DEBUG_PERF:
_t_denoise_start = time.perf_counter()
latents = self.diffuse(
latents=latents,
timesteps=timesteps,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
guidance_low=guidance_low,
guidance_high=guidance_high,
boundary_timestep=boundary_timestep,
dtype=dtype,
attention_kwargs=attention_kwargs,
latent_condition=latent_condition,
first_frame_mask=first_frame_mask,
)
# 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
if DEBUG_PERF:
current_omni_platform.synchronize()
_t_denoise_ms = (time.perf_counter() - _t_denoise_start) * 1000
# For I2V mode: blend final latents with condition
if self.expand_timesteps and latent_condition is not None:
latents = (1 - first_frame_mask) * latent_condition + first_frame_mask * latents
if DEBUG_PERF:
_t_decode_start = time.perf_counter()
if output_type == "latent":
output = latents
else:
latents = latents.to(self.vae.dtype)
latents_mean = (
torch.tensor(self.vae.config.latents_mean)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(latents.device, latents.dtype)
)
latents_std = 1.0 / torch.tensor(self.vae.config.latents_std).view(1, self.vae.config.z_dim, 1, 1, 1).to(
latents.device, latents.dtype
)
latents = latents / latents_std + latents_mean
output = self.vae.decode(latents, return_dict=False)[0]
if DEBUG_PERF:
current_omni_platform.synchronize()
_t_decode_ms = (time.perf_counter() - _t_decode_start) * 1000
_t_pipeline_wall_ms = (time.perf_counter() - _t_pipeline_start) * 1000
_t_stages_sum = _t_text_enc_ms + _t_latent_prep_ms + _t_denoise_ms + _t_decode_ms
if _is_rank_zero():
logger.info(
"Pipeline stage timing summary: "
"TextEncoding=%.2f ms, LatentPreparation=%.2f ms, "
"Denoising=%.2f ms (%d steps), Decoding=%.2f ms, "
"StagesSum=%.2f ms, PipelineWall=%.2f ms, Unaccounted=%.2f ms",
_t_text_enc_ms,
_t_latent_prep_ms,
_t_denoise_ms,
len(timesteps),
_t_decode_ms,
_t_stages_sum,
_t_pipeline_wall_ms,
_t_pipeline_wall_ms - _t_stages_sum,
)
return DiffusionOutput(
output=output, stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None
)
def predict_noise(
self,
current_model: nn.Module | None = None,
**kwargs: Any,
) -> torch.Tensor | IntermediateTensors:
"""
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 or IntermediateTensors on non-last PP stages.
"""
if current_model is None:
current_model = self.transformer
result = current_model(**kwargs)
return result if isinstance(result, IntermediateTensors) else result[0]
def encode_prompt(
self,
prompt: str | list[str],
negative_prompt: str | list[str] | None = None,
do_classifier_free_guidance: bool = True,
num_videos_per_prompt: int = 1,
max_sequence_length: int = 512,
device: torch.device | None = None,
dtype: torch.dtype | None = None,
):
device = device or self.device
dtype = dtype or self.text_encoder.dtype
prompt = [prompt] if isinstance(prompt, str) else prompt
prompt_clean = [self._prompt_clean(p) for p in prompt]
batch_size = len(prompt_clean)
text_inputs = self.tokenizer(
prompt_clean,
padding="max_length",
max_length=max_sequence_length,
truncation=True,
add_special_tokens=True,
return_attention_mask=True,
return_tensors="pt",
)
ids, mask = text_inputs.input_ids, text_inputs.attention_mask
seq_lens = mask.gt(0).sum(dim=1).long()
prompt_embeds = self.text_encoder(ids.to(device), mask.to(device)).last_hidden_state
prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
prompt_embeds = [u[:v] for u, v in zip(prompt_embeds, seq_lens)]
prompt_embeds = torch.stack(
[torch.cat([u, u.new_zeros(max_sequence_length - u.size(0), u.size(1))]) for u in prompt_embeds], dim=0
)
_, seq_len, _ = prompt_embeds.shape
prompt_embeds = prompt_embeds.repeat(1, num_videos_per_prompt, 1)
prompt_embeds = prompt_embeds.view(batch_size * num_videos_per_prompt, seq_len, -1)
negative_prompt_embeds = None
if do_classifier_free_guidance:
negative_prompt = negative_prompt or ""
negative_prompt = batch_size * [negative_prompt] if isinstance(negative_prompt, str) else negative_prompt
neg_text_inputs = self.tokenizer(
[self._prompt_clean(p) for p in negative_prompt],
padding="max_length",
max_length=max_sequence_length,
truncation=True,
add_special_tokens=True,
return_attention_mask=True,
return_tensors="pt",
)
ids_neg, mask_neg = neg_text_inputs.input_ids, neg_text_inputs.attention_mask
seq_lens_neg = mask_neg.gt(0).sum(dim=1).long()
negative_prompt_embeds = self.text_encoder(ids_neg.to(device), mask_neg.to(device)).last_hidden_state
negative_prompt_embeds = negative_prompt_embeds.to(dtype=dtype, device=device)
negative_prompt_embeds = [u[:v] for u, v in zip(negative_prompt_embeds, seq_lens_neg)]
negative_prompt_embeds = torch.stack(
[
torch.cat([u, u.new_zeros(max_sequence_length - u.size(0), u.size(1))])
for u in negative_prompt_embeds
],
dim=0,
)
negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_videos_per_prompt, 1)
negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_videos_per_prompt, seq_len, -1)
return prompt_embeds, negative_prompt_embeds
@staticmethod
def _prompt_clean(text: str) -> str:
return " ".join(text.strip().split())
def prepare_latents(
self,
batch_size: int,
num_channels_latents: int,
height: int,
width: int,
num_frames: int,
dtype: torch.dtype | None,
device: torch.device | None,
generator: torch.Generator | list[torch.Generator] | None,
latents: torch.Tensor | None = None,
) -> torch.Tensor:
if latents is not None:
return latents.to(device=device, dtype=dtype)
num_latent_frames = (num_frames - 1) // self.vae_scale_factor_temporal + 1
shape = (
batch_size,
num_channels_latents,
num_latent_frames,
int(height) // self.vae_scale_factor_spatial,
int(width) // self.vae_scale_factor_spatial,
)
if isinstance(generator, list) and len(generator) != batch_size:
raise ValueError(f"Generator list length {len(generator)} does not match batch size {batch_size}.")
latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
return latents
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
"""Load weights using AutoWeightsLoader for vLLM integration."""
loader = AutoWeightsLoader(self)
return loader.load_weights(weights)
def check_inputs(
self,
prompt,
negative_prompt,
height,
width,
prompt_embeds=None,
negative_prompt_embeds=None,
guidance_scale_2=None,
boundary_ratio=None,
):
if height % 16 != 0 or width % 16 != 0:
raise ValueError(f"`height` and `width` have to be divisible by 16 but are {height} and {width}.")
if prompt is not None and prompt_embeds is not None:
raise ValueError(
f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to"
" only forward one of the two."
)
elif negative_prompt is not None and negative_prompt_embeds is not None:
raise ValueError(
f"Cannot forward both `negative_prompt`: {negative_prompt} and "
f"`negative_prompt_embeds`: {negative_prompt_embeds}. "
"Please make sure to only forward one of the two."
)
elif prompt is None and prompt_embeds is None:
raise ValueError(
"Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined."
)
elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
elif negative_prompt is not None and (
not isinstance(negative_prompt, str) and not isinstance(negative_prompt, list)
):
raise ValueError(f"`negative_prompt` has to be of type `str` or `list` but is {type(negative_prompt)}")
if boundary_ratio is None and guidance_scale_2 is not None:
raise ValueError("`guidance_scale_2` is only supported when `boundary_ratio` is set.")
# ---------------------------------------------------------------------------
# DMD2-distilled variant
# ---------------------------------------------------------------------------
class WanT2VDMD2Pipeline(DMD2PipelineMixin, Wan22Pipeline):
"""Wan 2.x T2V pipeline for FastGen DMD2-distilled models."""
def __init__(self, *, od_config: OmniDiffusionConfig, prefix: str = ""):
super().__init__(od_config=od_config, prefix=prefix)
self.__init_dmd2__()