Files
vllm-omni/vllm_omni/diffusion/models/helios/pipeline_helios.py
T

1109 lines
47 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# Adapted from Helios (https://github.com/BestWishYsh/Helios)
from __future__ import annotations
import json
import logging
import math
import os
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any
import numpy as np
import torch
import torch.nn.functional as F
from diffusers import AutoencoderKLWan
from diffusers.utils.torch_utils import randn_tensor
from torch import nn
from transformers import AutoConfig, 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.helios.helios_transformer import HeliosTransformer3DModel
from vllm_omni.diffusion.models.helios.scheduling_helios import HeliosScheduler
from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.platforms import current_omni_platform
if TYPE_CHECKING:
from vllm.model_executor.layers.quantization.base_config import (
QuantizationConfig,
)
logger = logging.getLogger(__name__)
def calculate_shift(
image_seq_len,
base_seq_len: int = 256,
max_seq_len: int = 4096,
base_shift: float = 0.5,
max_shift: float = 1.15,
):
m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
b = base_shift - m * base_seq_len
mu = image_seq_len * m + b
return mu
def optimized_scale(positive_flat, negative_flat):
positive_flat = positive_flat.float()
negative_flat = negative_flat.float()
dot_product = torch.sum(positive_flat * negative_flat, dim=1, keepdim=True)
squared_norm = torch.sum(negative_flat**2, dim=1, keepdim=True) + 1e-8
st_star = dot_product / squared_norm
return st_star
def load_json_config(model_path: str, subfolder: str, filename: str, local_files_only: bool = True) -> dict:
"""Load a JSON config file from a local path or HuggingFace Hub repo."""
if local_files_only:
config_path = os.path.join(model_path, subfolder, filename)
if os.path.exists(config_path):
with open(config_path) as f:
return json.load(f)
else:
try:
from huggingface_hub import hf_hub_download
config_path = hf_hub_download(
repo_id=model_path,
filename=f"{subfolder}/{filename}",
)
with open(config_path) as f:
return json.load(f)
except Exception:
pass
return {}
def load_transformer_config(model_path: str, subfolder: str = "transformer", local_files_only: bool = True) -> dict:
return load_json_config(model_path, subfolder, "config.json", local_files_only)
def create_transformer_from_config(
config: dict, quant_config: QuantizationConfig | None = None
) -> HeliosTransformer3DModel:
kwargs = {}
key_map = [
"patch_size",
"num_attention_heads",
"attention_head_dim",
"in_channels",
"out_channels",
"text_dim",
"freq_dim",
"ffn_dim",
"num_layers",
"cross_attn_norm",
"qk_norm",
"eps",
"added_kv_proj_dim",
"rope_dim",
"rope_theta",
"guidance_cross_attn",
"zero_history_timestep",
"has_multi_term_memory_patch",
"is_amplify_history",
"history_scale_mode",
]
for key in key_map:
if key in config:
val = config[key]
if key in ("patch_size", "rope_dim") and isinstance(val, list):
val = tuple(val)
kwargs[key] = val
return HeliosTransformer3DModel(quant_config=quant_config, **kwargs)
def get_helios_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",
):
if output_type == "latent":
return video
return video_processor.postprocess_video(video, output_type=output_type)
return post_process_func
def get_helios_pre_process_func(
od_config: OmniDiffusionConfig,
):
def pre_process_func(request: OmniDiffusionRequest) -> OmniDiffusionRequest:
return request
return pre_process_func
class HeliosPipeline(nn.Module, CFGParallelMixin, ProgressBarMixin, DiffusionPipelineProfilerMixin):
"""Helios text-to-video / image-to-video / video-to-video pipeline for vllm-omni.
Supports T2V, I2V (with image input), and V2V (with video input).
Implements chunked video generation with multi-term memory history context.
"""
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)
self.weights_sources = [
DiffusersPipelineLoader.ComponentSource(
model_or_path=od_config.model,
subfolder="transformer",
revision=None,
prefix="transformer.",
fall_back_to_pt=True,
)
]
self.tokenizer = AutoTokenizer.from_pretrained(model, subfolder="tokenizer", local_files_only=local_files_only)
# Helios checkpoints store embed_tokens under ``shared.weight`` only,
# but the published config sets ``tie_word_embeddings=False``. When
# transformers sees ``tie=False`` it creates a separate
# ``encoder.embed_tokens.weight`` that is never loaded from the
# checkpoint, leaving it as all-zeros. This silently destroys prompt
# encoding and produces grey/meaningless video output. Force tying so
# that ``embed_tokens`` shares ``shared.weight`` as intended.
text_enc_cfg = AutoConfig.from_pretrained(model, subfolder="text_encoder", local_files_only=local_files_only)
text_enc_cfg.tie_word_embeddings = True
self.text_encoder = UMT5EncoderModel.from_pretrained(
model, subfolder="text_encoder", config=text_enc_cfg, torch_dtype=dtype, local_files_only=local_files_only
).to(self.device)
self.vae = AutoencoderKLWan.from_pretrained(
model, subfolder="vae", torch_dtype=torch.float32, local_files_only=local_files_only
).to(self.device)
transformer_config = load_transformer_config(model, "transformer", local_files_only)
self.transformer = create_transformer_from_config(
transformer_config, quant_config=od_config.quantization_config
)
# Read scheduler config to determine scheduler type
sched_cfg = load_json_config(model, "scheduler", "scheduler_config.json", local_files_only)
scheduler_kwargs = {}
passthrough_keys = [
"num_train_timesteps",
"shift",
"stages",
"stage_range",
"gamma",
"thresholding",
"prediction_type",
"solver_order",
"predict_x0",
"solver_type",
"lower_order_final",
"disable_corrector",
"use_flow_sigmas",
"scheduler_type",
"use_dynamic_shifting",
"time_shift_type",
]
for key in passthrough_keys:
if key in sched_cfg:
scheduler_kwargs[key] = sched_cfg[key]
self.scheduler = HeliosScheduler(**scheduler_kwargs)
self.is_distilled = scheduler_kwargs.get("scheduler_type") == "dmd"
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._num_timesteps = None
self._current_timestep = None
self.setup_diffusion_pipeline_profiler(
enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler
)
@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 forward(
self,
req: OmniDiffusionRequest,
prompt: str | None = None,
negative_prompt: str | None = None,
height: int = 384,
width: int = 640,
num_inference_steps: int = 50,
guidance_scale: float = 5.0,
frame_num: int = 132,
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,
# Helios-specific
history_sizes: list | None = None,
num_latent_frames_per_chunk: int = 9,
keep_first_frame: bool = True,
# I2V
image: torch.Tensor | None = None,
add_noise_to_image_latents: bool = True,
image_noise_sigma_min: float = 0.111,
image_noise_sigma_max: float = 0.135,
# V2V
video: torch.Tensor | None = None,
add_noise_to_video_latents: bool = True,
video_noise_sigma_min: float = 0.111,
video_noise_sigma_max: float = 0.135,
# Stage 2 (pyramid multi-stage denoising)
is_enable_stage2: bool = False,
pyramid_num_stages: int = 3,
pyramid_num_inference_steps_list: list | None = None,
is_skip_first_chunk: bool = False,
# DMD
is_amplify_first_chunk: bool = False,
# CFG Zero Star
use_cfg_zero_star: bool = False,
use_zero_init: bool = True,
zero_steps: int = 1,
**kwargs,
) -> DiffusionOutput:
if pyramid_num_inference_steps_list is None:
pyramid_num_inference_steps_list = [10, 10, 10]
if history_sizes is None:
history_sizes = [16, 2, 1]
# Read Helios-specific params from extra_args
extra = getattr(req.sampling_params, "extra_args", {}) or {}
is_enable_stage2 = extra.get("is_enable_stage2", is_enable_stage2)
pyramid_num_stages = extra.get("pyramid_num_stages", pyramid_num_stages)
pyramid_num_inference_steps_list = extra.get(
"pyramid_num_inference_steps_list", pyramid_num_inference_steps_list
)
is_amplify_first_chunk = extra.get("is_amplify_first_chunk", is_amplify_first_chunk)
use_cfg_zero_star = extra.get("use_cfg_zero_star", use_cfg_zero_star)
use_zero_init = extra.get("use_zero_init", use_zero_init)
zero_steps = extra.get("zero_steps", zero_steps)
is_skip_first_chunk = extra.get("is_skip_first_chunk", is_skip_first_chunk)
image = extra.get("image", image)
video = extra.get("video", video)
add_noise_to_image_latents = extra.get("add_noise_to_image_latents", add_noise_to_image_latents)
image_noise_sigma_min = extra.get("image_noise_sigma_min", image_noise_sigma_min)
image_noise_sigma_max = extra.get("image_noise_sigma_max", image_noise_sigma_max)
add_noise_to_video_latents = extra.get("add_noise_to_video_latents", add_noise_to_video_latents)
video_noise_sigma_min = extra.get("video_noise_sigma_min", video_noise_sigma_min)
video_noise_sigma_max = extra.get("video_noise_sigma_max", video_noise_sigma_max)
if image is not None and video is not None:
raise ValueError("image and video cannot be provided simultaneously")
if len(req.prompts) > 1:
raise ValueError("This model only supports a single prompt, not a batched request.")
if len(req.prompts) == 1:
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 Helios 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
num_steps = req.sampling_params.num_inference_steps or num_inference_steps
if req.sampling_params.guidance_scale_provided:
guidance_scale = req.sampling_params.guidance_scale
self._guidance_scale = guidance_scale
height = (height // 16) * 16
width = (width // 16) * 16
num_frames = max(num_frames, 1)
device = self.device
dtype = self.transformer.dtype
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)
# Encode prompts
if prompt_embeds is None:
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
prompt=prompt,
negative_prompt=negative_prompt,
do_classifier_free_guidance=self.do_classifier_free_guidance,
num_videos_per_prompt=req.sampling_params.num_outputs_per_prompt or 1,
max_sequence_length=req.sampling_params.max_sequence_length or 226,
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)
batch_size = prompt_embeds.shape[0]
history_sizes = sorted(history_sizes, reverse=True)
latents_mean = (
torch.tensor(self.vae.config.latents_mean)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(self.vae.device, self.vae.dtype)
)
latents_std = 1.0 / torch.tensor(self.vae.config.latents_std).view(1, self.vae.config.z_dim, 1, 1, 1).to(
self.vae.device, self.vae.dtype
)
# Prepare I2V image latents
fake_image_latents = None
image_latents = None
if image is not None:
image_latents, fake_image_latents = self.prepare_image_latents(
image,
latents_mean=latents_mean,
latents_std=latents_std,
num_latent_frames_per_chunk=num_latent_frames_per_chunk,
dtype=torch.float32,
device=device,
generator=generator,
)
if image_latents is not None and add_noise_to_image_latents:
image_noise_sigma = (
torch.rand(1, device=device, generator=generator) * (image_noise_sigma_max - image_noise_sigma_min)
+ image_noise_sigma_min
)
image_latents = (
image_noise_sigma * randn_tensor(image_latents.shape, generator=generator, device=device)
+ (1 - image_noise_sigma) * image_latents
)
fake_image_noise_sigma = (
torch.rand(1, device=device, generator=generator) * (video_noise_sigma_max - video_noise_sigma_min)
+ video_noise_sigma_min
)
fake_image_latents = (
fake_image_noise_sigma * randn_tensor(fake_image_latents.shape, generator=generator, device=device)
+ (1 - fake_image_noise_sigma) * fake_image_latents
)
# Prepare V2V video latents
video_latents = None
if video is not None:
image_latents, video_latents = self.prepare_video_latents(
video,
latents_mean=latents_mean,
latents_std=latents_std,
num_latent_frames_per_chunk=num_latent_frames_per_chunk,
dtype=torch.float32,
device=device,
generator=generator,
)
if video_latents is not None and add_noise_to_video_latents:
image_noise_sigma = (
torch.rand(1, device=device, generator=generator) * (image_noise_sigma_max - image_noise_sigma_min)
+ image_noise_sigma_min
)
image_latents = (
image_noise_sigma * randn_tensor(image_latents.shape, generator=generator, device=device)
+ (1 - image_noise_sigma) * image_latents
)
noisy_latents_chunks = []
num_latent_chunks = video_latents.shape[2] // num_latent_frames_per_chunk
for i in range(num_latent_chunks):
chunk_start = i * num_latent_frames_per_chunk
chunk_end = chunk_start + num_latent_frames_per_chunk
latent_chunk = video_latents[:, :, chunk_start:chunk_end, :, :]
chunk_frames = latent_chunk.shape[2]
frame_sigmas = (
torch.rand(chunk_frames, device=device, generator=generator)
* (video_noise_sigma_max - video_noise_sigma_min)
+ video_noise_sigma_min
)
frame_sigmas = frame_sigmas.view(1, 1, chunk_frames, 1, 1)
noisy_chunk = (
frame_sigmas * randn_tensor(latent_chunk.shape, generator=generator, device=device)
+ (1 - frame_sigmas) * latent_chunk
)
noisy_latents_chunks.append(noisy_chunk)
video_latents = torch.cat(noisy_latents_chunks, dim=2)
# Prepare latent variables
num_channels_latents = self.transformer.config.in_channels
window_num_frames = (num_latent_frames_per_chunk - 1) * self.vae_scale_factor_temporal + 1
num_latent_chunk = max(1, (num_frames + window_num_frames - 1) // window_num_frames)
num_history_latent_frames = sum(history_sizes)
history_video = None
total_generated_latent_frames = 0
if not keep_first_frame:
history_sizes[-1] = history_sizes[-1] + 1
history_latents = torch.zeros(
batch_size,
num_channels_latents,
num_history_latent_frames,
height // self.vae_scale_factor_spatial,
width // self.vae_scale_factor_spatial,
device=device,
dtype=torch.float32,
)
if fake_image_latents is not None:
history_latents = torch.cat([history_latents[:, :, :-1, :, :], fake_image_latents], dim=2)
total_generated_latent_frames += 1
if video_latents is not None:
history_frames = history_latents.shape[2]
video_frames = video_latents.shape[2]
if video_frames < history_frames:
keep_frames = history_frames - video_frames
history_latents = torch.cat([history_latents[:, :, :keep_frames, :, :], video_latents], dim=2)
else:
history_latents = video_latents
total_generated_latent_frames += video_latents.shape[2]
# Prepare frame indices
if keep_first_frame:
indices = torch.arange(0, sum([1, *history_sizes, num_latent_frames_per_chunk]))
(
indices_prefix,
indices_latents_history_long,
indices_latents_history_mid,
indices_latents_history_1x,
indices_hidden_states,
) = indices.split([1, *history_sizes, num_latent_frames_per_chunk], dim=0)
indices_latents_history_short = torch.cat([indices_prefix, indices_latents_history_1x], dim=0)
else:
indices = torch.arange(0, sum([*history_sizes, num_latent_frames_per_chunk]))
(
indices_latents_history_long,
indices_latents_history_mid,
indices_latents_history_short,
indices_hidden_states,
) = indices.split([*history_sizes, num_latent_frames_per_chunk], dim=0)
indices_hidden_states = indices_hidden_states.unsqueeze(0)
indices_latents_history_short = indices_latents_history_short.unsqueeze(0)
indices_latents_history_mid = indices_latents_history_mid.unsqueeze(0)
indices_latents_history_long = indices_latents_history_long.unsqueeze(0)
if attention_kwargs is None:
attention_kwargs = {}
vae_dtype = self.vae.dtype
# Chunked denoising loop
for k in range(num_latent_chunk):
is_first_chunk = k == 0
is_second_chunk = k == 1
if keep_first_frame:
latents_history_long, latents_history_mid, latents_history_1x = history_latents[
:, :, -num_history_latent_frames:
].split(history_sizes, dim=2)
if image_latents is None and is_first_chunk:
latents_prefix = torch.zeros(
(
batch_size,
num_channels_latents,
1,
latents_history_1x.shape[-2],
latents_history_1x.shape[-1],
),
device=latents_history_1x.device,
dtype=latents_history_1x.dtype,
)
else:
latents_prefix = image_latents
latents_history_short = torch.cat([latents_prefix, latents_history_1x], dim=2)
else:
latents_history_long, latents_history_mid, latents_history_short = history_latents[
:, :, -num_history_latent_frames:
].split(history_sizes, dim=2)
# Prepare noise for this chunk
latents = self.prepare_latents(
batch_size,
num_channels_latents,
height,
width,
window_num_frames,
dtype=torch.float32,
device=device,
generator=generator,
latents=None,
)
if not is_enable_stage2:
# Stage 1 only: single-stage denoising
patch_size = self.transformer.config.patch_size
image_seq_len = (latents.shape[-1] * latents.shape[-2] * latents.shape[-3]) // (
patch_size[0] * patch_size[1] * patch_size[2]
)
sigmas = np.linspace(0.999, 0.0, num_steps + 1)[:-1]
mu = calculate_shift(image_seq_len)
self.scheduler.set_timesteps(num_steps, device=device, sigmas=sigmas, mu=mu)
timesteps = self.scheduler.timesteps
self._num_timesteps = len(timesteps)
latents = self._stage1_sample(
latents=latents,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
timesteps=timesteps,
guidance_scale=guidance_scale,
indices_hidden_states=indices_hidden_states,
indices_latents_history_short=indices_latents_history_short,
indices_latents_history_mid=indices_latents_history_mid,
indices_latents_history_long=indices_latents_history_long,
latents_history_short=latents_history_short,
latents_history_mid=latents_history_mid,
latents_history_long=latents_history_long,
attention_kwargs=attention_kwargs,
transformer_dtype=dtype,
use_cfg_zero_star=use_cfg_zero_star,
use_zero_init=use_zero_init,
zero_steps=zero_steps,
)
else:
# Stage 2: pyramid multi-stage denoising
latents = self._stage2_sample(
latents=latents,
pyramid_num_stages=pyramid_num_stages,
pyramid_num_inference_steps_list=pyramid_num_inference_steps_list,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
guidance_scale=guidance_scale,
indices_hidden_states=indices_hidden_states,
indices_latents_history_short=indices_latents_history_short,
indices_latents_history_mid=indices_latents_history_mid,
indices_latents_history_long=indices_latents_history_long,
latents_history_short=latents_history_short,
latents_history_mid=latents_history_mid,
latents_history_long=latents_history_long,
attention_kwargs=attention_kwargs,
transformer_dtype=dtype,
is_amplify_first_chunk=is_amplify_first_chunk and is_first_chunk,
use_cfg_zero_star=use_cfg_zero_star,
use_zero_init=use_zero_init,
zero_steps=zero_steps,
device=device,
generator=generator,
)
if keep_first_frame and (
(is_first_chunk and image_latents is None) or (is_skip_first_chunk and is_second_chunk)
):
image_latents = latents[:, :, 0:1, :, :]
total_generated_latent_frames += latents.shape[2]
history_latents = torch.cat([history_latents, latents], dim=2)
real_history_latents = history_latents[:, :, -total_generated_latent_frames:]
index_slice = (
slice(None),
slice(None),
slice(-num_latent_frames_per_chunk, None),
)
current_latents = real_history_latents[index_slice].to(vae_dtype) / latents_std + latents_mean
current_video = self.vae.decode(current_latents, return_dict=False)[0]
if history_video is None:
history_video = current_video
else:
history_video = torch.cat([history_video, current_video], dim=2)
if current_omni_platform.is_available():
current_omni_platform.empty_cache()
self._current_timestep = None
if output_type == "latent":
output = real_history_latents
else:
output = history_video
return DiffusionOutput(
output=output, stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None
)
def _stage1_sample(
self,
latents: torch.Tensor,
prompt_embeds: torch.Tensor,
negative_prompt_embeds: torch.Tensor | None,
timesteps: torch.Tensor,
guidance_scale: float,
indices_hidden_states: torch.Tensor,
indices_latents_history_short: torch.Tensor,
indices_latents_history_mid: torch.Tensor,
indices_latents_history_long: torch.Tensor,
latents_history_short: torch.Tensor,
latents_history_mid: torch.Tensor,
latents_history_long: torch.Tensor,
attention_kwargs: dict,
transformer_dtype: torch.dtype,
use_cfg_zero_star: bool = False,
use_zero_init: bool = True,
zero_steps: int = 1,
) -> torch.Tensor:
"""Single-stage denoising loop for one chunk."""
batch_size = latents.shape[0]
do_true_cfg = guidance_scale > 1.0 and negative_prompt_embeds is not None
with self.progress_bar(total=len(timesteps)) as pbar:
for i, t in enumerate(timesteps):
self._current_timestep = t
timestep = t.expand(batch_size)
transformer_kwargs = {
"hidden_states": latents.to(transformer_dtype),
"timestep": timestep,
"indices_hidden_states": indices_hidden_states,
"indices_latents_history_short": indices_latents_history_short,
"indices_latents_history_mid": indices_latents_history_mid,
"indices_latents_history_long": indices_latents_history_long,
"latents_history_short": latents_history_short.to(transformer_dtype),
"latents_history_mid": latents_history_mid.to(transformer_dtype),
"latents_history_long": latents_history_long.to(transformer_dtype),
"attention_kwargs": attention_kwargs,
"return_dict": False,
}
if use_cfg_zero_star and do_true_cfg:
noise_pred = self.transformer(
encoder_hidden_states=prompt_embeds,
**transformer_kwargs,
)[0]
noise_uncond = self.transformer(
encoder_hidden_states=negative_prompt_embeds,
**transformer_kwargs,
)[0]
positive_flat = noise_pred.view(batch_size, -1)
negative_flat = noise_uncond.view(batch_size, -1)
alpha_cfg = optimized_scale(positive_flat, negative_flat)
alpha_cfg = alpha_cfg.view(batch_size, *([1] * (len(noise_pred.shape) - 1)))
alpha_cfg = alpha_cfg.to(noise_pred.dtype)
if (i <= zero_steps) and use_zero_init:
noise_pred = noise_pred * 0.0
else:
noise_pred = noise_uncond * alpha_cfg + guidance_scale * (noise_pred - noise_uncond * alpha_cfg)
else:
positive_kwargs = {
"encoder_hidden_states": prompt_embeds,
**transformer_kwargs,
}
if do_true_cfg:
negative_kwargs = {
"encoder_hidden_states": negative_prompt_embeds,
**transformer_kwargs,
}
else:
negative_kwargs = None
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,
)
latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, do_true_cfg)
pbar.update()
return latents
def _stage2_sample(
self,
latents: torch.Tensor,
pyramid_num_stages: int,
pyramid_num_inference_steps_list: list[int],
prompt_embeds: torch.Tensor,
negative_prompt_embeds: torch.Tensor | None,
guidance_scale: float,
indices_hidden_states: torch.Tensor,
indices_latents_history_short: torch.Tensor,
indices_latents_history_mid: torch.Tensor,
indices_latents_history_long: torch.Tensor,
latents_history_short: torch.Tensor,
latents_history_mid: torch.Tensor,
latents_history_long: torch.Tensor,
attention_kwargs: dict,
transformer_dtype: torch.dtype,
is_amplify_first_chunk: bool = False,
use_cfg_zero_star: bool = False,
use_zero_init: bool = True,
zero_steps: int = 1,
device: torch.device | None = None,
generator: torch.Generator | list[torch.Generator] | None = None,
) -> torch.Tensor:
"""Pyramid multi-stage denoising for one chunk."""
batch_size, num_channel, num_frames_lat, height, width = latents.shape
# Downsample latents to the smallest pyramid level
latents_flat = latents.permute(0, 2, 1, 3, 4).reshape(batch_size * num_frames_lat, num_channel, height, width)
for _ in range(pyramid_num_stages - 1):
height //= 2
width //= 2
latents_flat = F.interpolate(latents_flat, size=(height, width), mode="bilinear") * 2
latents = latents_flat.reshape(batch_size, num_frames_lat, num_channel, height, width).permute(0, 2, 1, 3, 4)
start_point_list = None
if self.is_distilled:
start_point_list = [latents]
do_true_cfg = guidance_scale > 1.0 and negative_prompt_embeds is not None
for i_s in range(pyramid_num_stages):
patch_size = self.transformer.config.patch_size
image_seq_len = (latents.shape[-1] * latents.shape[-2] * latents.shape[-3]) // (
patch_size[0] * patch_size[1] * patch_size[2]
)
mu = calculate_shift(image_seq_len)
self.scheduler.set_timesteps(
pyramid_num_inference_steps_list[i_s],
stage_index=i_s,
device=device,
mu=mu,
is_amplify_first_chunk=is_amplify_first_chunk,
)
timesteps = self.scheduler.timesteps
if i_s > 0:
# Upsample latents to next pyramid level
height *= 2
width *= 2
num_frames_cur = latents.shape[2]
latents_flat = latents.permute(0, 2, 1, 3, 4).reshape(
batch_size * num_frames_cur, num_channel, height // 2, width // 2
)
latents_flat = F.interpolate(latents_flat, size=(height, width), mode="nearest")
latents = latents_flat.reshape(batch_size, num_frames_cur, num_channel, height, width).permute(
0, 2, 1, 3, 4
)
# Add block noise to fix artifacts between stages
ori_sigma = 1 - self.scheduler.ori_start_sigmas[i_s]
gamma = self.scheduler.config.gamma
alpha = 1 / (math.sqrt(1 + (1 / gamma)) * (1 - ori_sigma) + ori_sigma)
beta = alpha * (1 - ori_sigma) / math.sqrt(gamma)
noise = self.sample_block_noise(
batch_size, num_channel, latents.shape[2], height, width, patch_size, generator=generator
)
noise = noise.to(device=device, dtype=transformer_dtype)
latents = alpha * latents + beta * noise
if self.is_distilled and start_point_list is not None:
start_point_list.append(latents)
with self.progress_bar(total=len(timesteps)) as pbar:
for idx, t in enumerate(timesteps):
self._current_timestep = t
timestep = t.expand(latents.shape[0]).to(torch.int64)
transformer_kwargs = {
"hidden_states": latents.to(transformer_dtype),
"timestep": timestep,
"indices_hidden_states": indices_hidden_states,
"indices_latents_history_short": indices_latents_history_short,
"indices_latents_history_mid": indices_latents_history_mid,
"indices_latents_history_long": indices_latents_history_long,
"latents_history_short": latents_history_short.to(transformer_dtype),
"latents_history_mid": latents_history_mid.to(transformer_dtype),
"latents_history_long": latents_history_long.to(transformer_dtype),
"attention_kwargs": attention_kwargs,
"return_dict": False,
}
noise_pred = self.transformer(
encoder_hidden_states=prompt_embeds,
**transformer_kwargs,
)[0]
if do_true_cfg:
noise_uncond = self.transformer(
encoder_hidden_states=negative_prompt_embeds,
**transformer_kwargs,
)[0]
if use_cfg_zero_star:
positive_flat = noise_pred.view(batch_size, -1)
negative_flat = noise_uncond.view(batch_size, -1)
alpha_cfg = optimized_scale(positive_flat, negative_flat)
alpha_cfg = alpha_cfg.view(batch_size, *([1] * (len(noise_pred.shape) - 1)))
alpha_cfg = alpha_cfg.to(noise_pred.dtype)
if (i_s == 0 and idx <= zero_steps) and use_zero_init:
noise_pred = noise_pred * 0.0
else:
noise_pred = noise_uncond * alpha_cfg + guidance_scale * (
noise_pred - noise_uncond * alpha_cfg
)
else:
noise_pred = noise_uncond + guidance_scale * (noise_pred - noise_uncond)
latents = self.scheduler.step(
noise_pred,
t,
latents,
return_dict=False,
cur_sampling_step=idx,
dmd_noisy_tensor=start_point_list[i_s] if start_point_list is not None else None,
dmd_sigmas=self.scheduler.sigmas,
dmd_timesteps=self.scheduler.timesteps,
all_timesteps=timesteps,
)[0]
pbar.update()
return latents
def sample_block_noise(self, batch_size, channel, num_frames, height, width, patch_size=(1, 2, 2), generator=None):
gamma = self.scheduler.config.gamma
_, ph, pw = patch_size
block_size = ph * pw
device = generator.device if generator is not None else self.device
cov = torch.eye(block_size) * (1 + gamma) - torch.ones(block_size, block_size) * gamma
cov += torch.eye(block_size) * 1e-8
cov = cov.float() # Upcast to fp32 for numerical stability — cholesky is unreliable in fp16/bf16.
L = torch.linalg.cholesky(cov).to(device)
block_number = batch_size * channel * num_frames * (height // ph) * (width // pw)
z = torch.randn(block_number, block_size, generator=generator, device=device)
noise = z @ L.T
noise = noise.view(batch_size, channel, num_frames, height // ph, width // pw, ph, pw)
noise = noise.permute(0, 1, 2, 3, 5, 4, 6).reshape(batch_size, channel, num_frames, height, width)
return noise
def predict_noise(self, **kwargs: Any) -> torch.Tensor:
return self.transformer(**kwargs)[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 = 226,
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 prepare_image_latents(
self,
image: torch.Tensor,
latents_mean: torch.Tensor,
latents_std: torch.Tensor,
num_latent_frames_per_chunk: int,
dtype: torch.dtype | None = None,
device: torch.device | None = None,
generator: torch.Generator | list[torch.Generator] | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Encode a single image into VAE latent space for I2V generation.
Returns (image_latents, fake_image_latents) where fake_image_latents
is the last-frame latent of a repeated-frame video, used to seed the
history buffer for the first denoising chunk.
"""
device = device or self.device
image = image.unsqueeze(2).to(device=device, dtype=self.vae.dtype)
latents = self.vae.encode(image).latent_dist.sample(generator=generator)
latents = (latents - latents_mean) * latents_std
min_frames = (num_latent_frames_per_chunk - 1) * self.vae_scale_factor_temporal + 1
fake_video = image.repeat(1, 1, min_frames, 1, 1)
fake_latents_full = self.vae.encode(fake_video).latent_dist.sample(generator=generator)
fake_latents_full = (fake_latents_full - latents_mean) * latents_std
fake_latents = fake_latents_full[:, :, -1:, :, :]
return latents.to(device=device, dtype=dtype), fake_latents.to(device=device, dtype=dtype)
def prepare_video_latents(
self,
video: torch.Tensor,
latents_mean: torch.Tensor,
latents_std: torch.Tensor,
num_latent_frames_per_chunk: int,
dtype: torch.dtype | None = None,
device: torch.device | None = None,
generator: torch.Generator | list[torch.Generator] | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Encode a video into VAE latent space for V2V generation.
Returns (first_frame_latent, video_latents) where first_frame_latent
is used as the image prefix, and video_latents fills the history buffer.
"""
device = device or self.device
video = video.to(device=device, dtype=self.vae.dtype)
num_frames = video.shape[2]
min_frames = (num_latent_frames_per_chunk - 1) * self.vae_scale_factor_temporal + 1
num_chunks = num_frames // min_frames
if num_chunks == 0:
raise ValueError(
f"Video must have at least {min_frames} frames (got {num_frames}). "
f"Required: (num_latent_frames_per_chunk - 1) * {self.vae_scale_factor_temporal} + 1"
)
total_valid_frames = num_chunks * min_frames
start_frame = num_frames - total_valid_frames
first_frame = video[:, :, 0:1, :, :]
first_frame_latent = self.vae.encode(first_frame).latent_dist.sample(generator=generator)
first_frame_latent = (first_frame_latent - latents_mean) * latents_std
latents_chunks = []
for i in range(num_chunks):
chunk_start = start_frame + i * min_frames
chunk_end = chunk_start + min_frames
video_chunk = video[:, :, chunk_start:chunk_end, :, :]
chunk_latents = self.vae.encode(video_chunk).latent_dist.sample(generator=generator)
chunk_latents = (chunk_latents - latents_mean) * latents_std
latents_chunks.append(chunk_latents)
video_latents = torch.cat(latents_chunks, dim=2)
return first_frame_latent.to(device=device, dtype=dtype), video_latents.to(device=device, dtype=dtype)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
loader = AutoWeightsLoader(self)
return loader.load_weights(weights)