842 lines
36 KiB
Python
842 lines
36 KiB
Python
#
|
|
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
|
|
import argparse
|
|
import gc
|
|
import html
|
|
import os
|
|
import pathlib
|
|
import random
|
|
import re
|
|
import time
|
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
|
|
import tensorrt as trt
|
|
import torch
|
|
from cuda.bindings import runtime as cudart
|
|
from diffusers.utils.torch_utils import randn_tensor
|
|
from PIL import Image
|
|
from tqdm.auto import tqdm
|
|
|
|
try:
|
|
import ftfy
|
|
FTFY_AVAILABLE = True
|
|
except ImportError:
|
|
FTFY_AVAILABLE = False
|
|
|
|
import demo_diffusion.engine as engine_module
|
|
import demo_diffusion.image as image_module
|
|
import demo_diffusion.path as path_module
|
|
from demo_diffusion.model import (
|
|
T5Model,
|
|
WanTransformerModel,
|
|
AutoencoderKLWanModel,
|
|
make_tokenizer,
|
|
)
|
|
from demo_diffusion.pipeline.diffusion_pipeline import DiffusionPipeline
|
|
from demo_diffusion.pipeline.type import PIPELINE_TYPE
|
|
|
|
TRT_LOGGER = trt.Logger(trt.Logger.ERROR)
|
|
|
|
# Copied from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/wan/pipeline_wan.py#L78
|
|
def basic_clean(text):
|
|
if FTFY_AVAILABLE:
|
|
text = ftfy.fix_text(text)
|
|
text = html.unescape(html.unescape(text))
|
|
return text.strip()
|
|
|
|
# Copied from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/wan/pipeline_wan.py#L84
|
|
def whitespace_clean(text):
|
|
text = re.sub(r"\s+", " ", text)
|
|
text = text.strip()
|
|
return text
|
|
|
|
# Copied from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/wan/pipeline_wan.py#L90
|
|
def prompt_clean(text):
|
|
text = whitespace_clean(basic_clean(text))
|
|
return text
|
|
|
|
|
|
class WanPipeline(DiffusionPipeline):
|
|
"""
|
|
Application showcasing the acceleration of Wan 2.2 T2V pipeline using Nvidia TensorRT.
|
|
"""
|
|
|
|
_callback_tensor_inputs = ["latents", "prompt_embeds", "negative_prompt_embeds"]
|
|
|
|
def __init__(
|
|
self,
|
|
dd_path,
|
|
version='wan2.2-t2v-a14b',
|
|
pipeline_type=PIPELINE_TYPE.TXT2VID,
|
|
boundary_ratio: float = 0.875,
|
|
guidance_scale: float = 4.0,
|
|
guidance_scale_2: float = 3.0,
|
|
t5_weight_streaming_budget_percentage=None,
|
|
transformer_weight_streaming_budget_percentage=None,
|
|
**kwargs
|
|
):
|
|
"""
|
|
Initializes the Wan T2V pipeline.
|
|
|
|
Args:
|
|
dd_path (load_module.DDPath):
|
|
DDPath object that contains all paths used in DemoDiffusion
|
|
version (str):
|
|
The version of the pipeline. Should be [wan2.2-t2v-a14b]
|
|
pipeline_type (PIPELINE_TYPE):
|
|
Type of current pipeline (TXT2VID)
|
|
boundary_ratio (float, defaults to 0.875):
|
|
Ratio of total timesteps to use as the boundary for switching between transformers
|
|
in two-stage denoising. Transformer handles high-noise stages (timesteps >= boundary)
|
|
and transformer_2 handles low-noise stages (timesteps < boundary).
|
|
Wan 2.2 T2V always uses two-stage denoising.
|
|
guidance_scale (float):
|
|
Guidance scale for high-noise stage (transformer). Wan default: 4.0
|
|
guidance_scale_2 (float):
|
|
Guidance scale for low-noise stage (transformer_2). Wan default: 3.0
|
|
t5_weight_streaming_budget_percentage (`int`, defaults to None):
|
|
Weight streaming budget as a percentage of the size of total streamable weights for the T5 model.
|
|
transformer_weight_streaming_budget_percentage (`int`, defaults to None):
|
|
Weight streaming budget as a percentage of the size of total streamable weights for the transformer models.
|
|
"""
|
|
super().__init__(
|
|
dd_path=dd_path,
|
|
version=version,
|
|
pipeline_type=pipeline_type,
|
|
scheduler="UniPC",
|
|
bf16=True,
|
|
text_encoder_weight_streaming_budget_percentage=t5_weight_streaming_budget_percentage,
|
|
denoiser_weight_streaming_budget_percentage=transformer_weight_streaming_budget_percentage,
|
|
**kwargs
|
|
)
|
|
|
|
# Validate boundary_ratio (required for Wan 2.2 two-stage denoising)
|
|
if boundary_ratio is None or not (0.0 < boundary_ratio < 1.0):
|
|
raise ValueError(
|
|
f"`boundary_ratio` must be between 0.0 and 1.0, got {boundary_ratio}"
|
|
)
|
|
|
|
self.boundary_ratio = boundary_ratio
|
|
self.guidance_scale = guidance_scale
|
|
self.guidance_scale_2 = guidance_scale_2
|
|
|
|
self.vae_scale_factor_temporal = 4
|
|
self.vae_scale_factor_spatial = 8
|
|
|
|
self.opt_image_height = 720
|
|
self.opt_image_width = 1280
|
|
self.opt_num_frames = 81
|
|
self.max_sequence_length = 512
|
|
|
|
@classmethod
|
|
def FromArgs(cls, args: argparse.Namespace, pipeline_type: PIPELINE_TYPE) -> 'WanPipeline':
|
|
"""Factory method to construct a WanPipeline object from parsed arguments."""
|
|
|
|
MAX_BATCH_SIZE = 1 # Wan always uses batch size 1
|
|
DEVICE = "cuda"
|
|
|
|
dd_path = path_module.resolve_path(
|
|
cls.get_model_names(pipeline_type), args, pipeline_type, cls._get_pipeline_uid(args.version)
|
|
)
|
|
|
|
return cls(
|
|
dd_path=dd_path,
|
|
version=args.version,
|
|
pipeline_type=pipeline_type,
|
|
boundary_ratio=args.boundary_ratio,
|
|
denoising_steps=args.denoising_steps,
|
|
guidance_scale=args.guidance_scale,
|
|
guidance_scale_2=args.guidance_scale_2,
|
|
t5_weight_streaming_budget_percentage=args.t5_ws_percentage if hasattr(args, 't5_ws_percentage') else None,
|
|
transformer_weight_streaming_budget_percentage=args.transformer_ws_percentage if hasattr(args, 'transformer_ws_percentage') else None,
|
|
max_batch_size=MAX_BATCH_SIZE,
|
|
device=DEVICE,
|
|
output_dir=args.output_dir,
|
|
hf_token=args.hf_token,
|
|
verbose=args.verbose,
|
|
nvtx_profile=args.nvtx_profile,
|
|
use_cuda_graph=args.use_cuda_graph,
|
|
framework_model_dir=args.framework_model_dir,
|
|
low_vram=args.low_vram,
|
|
torch_inference=args.torch_inference,
|
|
torch_fallback=args.torch_fallback if hasattr(args, 'torch_fallback') else None,
|
|
weight_streaming=args.ws if hasattr(args, 'ws') else False,
|
|
)
|
|
|
|
@classmethod
|
|
def get_model_names(cls, pipeline_type: PIPELINE_TYPE, controlnet_type: str = None) -> List[str]:
|
|
"""Return a list of model names used by this pipeline.
|
|
|
|
Overrides:
|
|
DiffusionPipeline.get_model_names
|
|
"""
|
|
return ["text_encoder", "transformer", "transformer_2", "vae_decoder"]
|
|
|
|
def download_onnx_models(self, model_name: str, model_config: dict[str, Any]) -> None:
|
|
raise NotImplementedError(
|
|
"Pre-exported Wan ONNX models are not available for download. "
|
|
"Export ONNX models locally using the provided export script."
|
|
)
|
|
|
|
def _initialize_models(self, framework_model_dir, int8=False, fp8=False, fp4=False):
|
|
self.tokenizer = make_tokenizer(
|
|
self.version,
|
|
self.pipeline_type,
|
|
self.hf_token,
|
|
framework_model_dir,
|
|
subfolder='tokenizer',
|
|
tokenizer_type='t5'
|
|
)
|
|
|
|
models_args = {
|
|
'version': self.version,
|
|
'pipeline': self.pipeline_type,
|
|
'device': self.device,
|
|
'hf_token': self.hf_token,
|
|
'verbose': self.verbose,
|
|
'framework_model_dir': framework_model_dir,
|
|
'max_batch_size': 1
|
|
}
|
|
|
|
if "text_encoder" in self.stages:
|
|
self.models['text_encoder'] = T5Model(
|
|
**models_args,
|
|
fp16=False,
|
|
bf16=True,
|
|
text_maxlen=self.max_sequence_length,
|
|
weight_streaming=self.weight_streaming,
|
|
weight_streaming_budget_percentage=self.text_encoder_weight_streaming_budget_percentage,
|
|
use_attention_mask=True,
|
|
)
|
|
|
|
if "transformer" in self.stages:
|
|
self.models['transformer'] = WanTransformerModel(
|
|
**models_args,
|
|
subfolder='transformer',
|
|
fp16=False,
|
|
bf16=True,
|
|
text_maxlen=self.max_sequence_length,
|
|
num_frames=self.opt_num_frames,
|
|
height=self.opt_image_height,
|
|
width=self.opt_image_width,
|
|
weight_streaming=self.weight_streaming,
|
|
weight_streaming_budget_percentage=self.denoiser_weight_streaming_budget_percentage,
|
|
)
|
|
|
|
if "transformer_2" in self.stages:
|
|
self.models['transformer_2'] = WanTransformerModel(
|
|
**models_args,
|
|
subfolder='transformer_2',
|
|
fp16=False,
|
|
bf16=True,
|
|
text_maxlen=self.max_sequence_length,
|
|
num_frames=self.opt_num_frames,
|
|
height=self.opt_image_height,
|
|
width=self.opt_image_width,
|
|
weight_streaming=self.weight_streaming,
|
|
weight_streaming_budget_percentage=self.denoiser_weight_streaming_budget_percentage,
|
|
)
|
|
|
|
if "vae_decoder" in self.stages:
|
|
self.models['vae_decoder'] = AutoencoderKLWanModel(
|
|
**models_args,
|
|
)
|
|
|
|
self.config['vae_decoder_torch_fallback'] = True
|
|
|
|
def load_resources(self, image_height, image_width, batch_size, seed):
|
|
"""Override to create additional 'denoise' event for combined transformer timing."""
|
|
super().load_resources(image_height, image_width, batch_size, seed)
|
|
# additional event for combined denoising timing (both transformers)
|
|
self.events['denoise'] = [cudart.cudaEventCreate()[1], cudart.cudaEventCreate()[1]]
|
|
|
|
def print_summary(self, denoising_steps, walltime_ms, batch_size, num_frames):
|
|
print("|----------------------|--------------|")
|
|
print("| {:^20} | {:^12} |".format("Module", "Latency"))
|
|
print("|----------------------|--------------|")
|
|
|
|
# calculate transformer timings from combined denoise event
|
|
total_denoise_time = cudart.cudaEventElapsedTime(self.events['denoise'][0], self.events['denoise'][1])[1]
|
|
transformer_steps_map = {
|
|
'transformer': self.transformer_steps if (self.transformer_steps > 0 and self.transformer_2_steps > 0) else denoising_steps,
|
|
'transformer_2': self.transformer_2_steps if self.transformer_2_steps > 0 else 0
|
|
}
|
|
|
|
for stage in self.stages:
|
|
if stage in transformer_steps_map and transformer_steps_map[stage] > 0:
|
|
steps = transformer_steps_map[stage]
|
|
time_ms = total_denoise_time * (steps / denoising_steps)
|
|
stage_label = f"{stage} x {steps}"
|
|
elif stage in transformer_steps_map:
|
|
continue # skip transformer_2 if unused
|
|
else:
|
|
time_ms = cudart.cudaEventElapsedTime(self.events[stage][0], self.events[stage][1])[1]
|
|
stage_label = stage
|
|
|
|
print("| {:^20} | {:>9.2f} ms |".format(stage_label, time_ms))
|
|
|
|
print("|----------------------|--------------|")
|
|
print("| {:^20} | {:>9.2f} ms |".format("Pipeline", walltime_ms))
|
|
print("|----------------------|--------------|")
|
|
print("Throughput: {:.2f} videos/min ({} frames)".format(batch_size * 60000.0 / walltime_ms, num_frames))
|
|
|
|
def save_video(self, frames, pipeline, prompt, seed):
|
|
if isinstance(prompt, list):
|
|
prompt_prefix = ''.join(set([p.replace(' ','_')[:10] for p in prompt]))
|
|
else:
|
|
prompt_prefix = prompt.replace(' ','_')[:10]
|
|
|
|
seed_str = str(seed) if seed is not None else 'random'
|
|
precision = 'bf16' if self.bf16 else 'fp16' if self.fp16 else 'fp32'
|
|
video_name_prefix = '-'.join([pipeline, prompt_prefix, precision, seed_str, str(random.randint(1000,9999))])
|
|
video_name_suffix = 'torch' if self.torch_inference else 'trt'
|
|
video_path = video_name_prefix+'-'+video_name_suffix+'.gif'
|
|
full_path = os.path.join(self.output_dir, video_path)
|
|
print(f"Saving video to: {full_path}")
|
|
frames[0].save(full_path, save_all=True, optimize=False, append_images=frames[1:], loop=0)
|
|
|
|
# Adapted from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/wan/pipeline_wan.py#L198
|
|
def encode_prompt(
|
|
self,
|
|
prompt: Union[str, List[str]],
|
|
negative_prompt: Optional[Union[str, List[str]]] = None,
|
|
do_classifier_free_guidance: bool = True,
|
|
num_videos_per_prompt: int = 1,
|
|
prompt_embeds: Optional[torch.Tensor] = None,
|
|
negative_prompt_embeds: Optional[torch.Tensor] = None,
|
|
max_sequence_length: int = 226,
|
|
device: Optional[torch.device] = None,
|
|
dtype: Optional[torch.dtype] = None,
|
|
):
|
|
r"""
|
|
Encodes the prompt into text encoder hidden states.
|
|
|
|
Implementation modeled from diffusers Wan pipeline, adapted for TensorRT.
|
|
|
|
Args:
|
|
prompt (`str` or `List[str]`, *optional*):
|
|
prompt to be encoded
|
|
negative_prompt (`str` or `List[str]`, *optional*):
|
|
The prompt or prompts not to guide the image generation. If not defined, one has to pass
|
|
`negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is
|
|
less than `1`).
|
|
do_classifier_free_guidance (`bool`, *optional*, defaults to `True`):
|
|
Whether to use classifier free guidance or not.
|
|
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
|
Number of videos that should be generated per prompt. torch device to place the resulting embeddings on
|
|
prompt_embeds (`torch.Tensor`, *optional*):
|
|
Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
|
|
provided, text embeddings will be generated from `prompt` input argument.
|
|
negative_prompt_embeds (`torch.Tensor`, *optional*):
|
|
Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt
|
|
weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input
|
|
argument.
|
|
max_sequence_length (`int`, defaults to `226`):
|
|
Maximum sequence length for text encoder.
|
|
device: (`torch.device`, *optional*):
|
|
torch device
|
|
dtype: (`torch.dtype`, *optional*):
|
|
torch dtype
|
|
"""
|
|
self.profile_start('text_encoder', color='green')
|
|
|
|
device = device or self._execution_device
|
|
|
|
prompt = [prompt] if isinstance(prompt, str) else prompt
|
|
if prompt is not None:
|
|
batch_size = len(prompt)
|
|
else:
|
|
batch_size = prompt_embeds.shape[0]
|
|
|
|
if prompt_embeds is None:
|
|
prompt_embeds = self._get_t5_prompt_embeds(
|
|
prompt=prompt,
|
|
num_videos_per_prompt=num_videos_per_prompt,
|
|
max_sequence_length=max_sequence_length,
|
|
device=device,
|
|
dtype=dtype,
|
|
)
|
|
|
|
if do_classifier_free_guidance and negative_prompt_embeds is None:
|
|
negative_prompt = negative_prompt or ""
|
|
negative_prompt = batch_size * [negative_prompt] if isinstance(negative_prompt, str) else negative_prompt
|
|
|
|
if prompt is not None and type(prompt) is not type(negative_prompt):
|
|
raise TypeError(
|
|
f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !="
|
|
f" {type(prompt)}."
|
|
)
|
|
elif batch_size != len(negative_prompt):
|
|
raise ValueError(
|
|
f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:"
|
|
f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches"
|
|
" the batch size of `prompt`."
|
|
)
|
|
|
|
negative_prompt_embeds = self._get_t5_prompt_embeds(
|
|
prompt=negative_prompt,
|
|
num_videos_per_prompt=num_videos_per_prompt,
|
|
max_sequence_length=max_sequence_length,
|
|
device=device,
|
|
dtype=dtype,
|
|
)
|
|
|
|
self.profile_stop('text_encoder')
|
|
return prompt_embeds, negative_prompt_embeds
|
|
|
|
def denoise_latents(
|
|
self,
|
|
latents: torch.Tensor,
|
|
prompt_embeds: torch.Tensor,
|
|
negative_prompt_embeds: Optional[torch.Tensor],
|
|
timesteps: torch.Tensor,
|
|
guidance_scale: float,
|
|
guidance_scale_2: float,
|
|
transformer_dtype: torch.dtype,
|
|
num_warmup_steps: int,
|
|
attention_kwargs: Optional[Dict[str, Any]] = None,
|
|
callback_on_step_end: Optional[Callable] = None,
|
|
callback_on_step_end_tensor_inputs: Optional[List[str]] = None,
|
|
warmup: bool = False,
|
|
) -> torch.Tensor:
|
|
boundary_timestep = self.boundary_ratio * self.scheduler.config.num_train_timesteps
|
|
self.profile_start('denoise', color='blue')
|
|
|
|
timestep_stages = []
|
|
for i, t in enumerate(timesteps):
|
|
if t >= boundary_timestep:
|
|
timestep_stages.append((i, t, 'transformer', guidance_scale))
|
|
else:
|
|
timestep_stages.append((i, t, 'transformer_2', guidance_scale_2))
|
|
|
|
stage_groups = []
|
|
if timestep_stages:
|
|
current_group = {
|
|
'transformer': timestep_stages[0][2],
|
|
'guidance_scale': timestep_stages[0][3],
|
|
'timesteps': [(timestep_stages[0][0], timestep_stages[0][1])]
|
|
}
|
|
|
|
for i, t, transformer_name, gs in timestep_stages[1:]:
|
|
if transformer_name == current_group['transformer']:
|
|
current_group['timesteps'].append((i, t))
|
|
else:
|
|
stage_groups.append(current_group)
|
|
current_group = {
|
|
'transformer': transformer_name,
|
|
'guidance_scale': gs,
|
|
'timesteps': [(i, t)]
|
|
}
|
|
stage_groups.append(current_group)
|
|
|
|
self.transformer_steps = sum(len(g['timesteps']) for g in stage_groups if g['transformer'] == 'transformer')
|
|
self.transformer_2_steps = sum(len(g['timesteps']) for g in stage_groups if g['transformer'] == 'transformer_2')
|
|
|
|
with tqdm(total=len(timesteps)) as progress_bar:
|
|
for stage_group in stage_groups:
|
|
transformer_name = stage_group['transformer']
|
|
current_guidance_scale = stage_group['guidance_scale']
|
|
|
|
with self.model_memory_manager([transformer_name], low_vram=self.low_vram):
|
|
for step_index, t in stage_group['timesteps']:
|
|
latent_model_input = latents.to(transformer_dtype)
|
|
timestep = t.expand(latent_model_input.shape[0])
|
|
|
|
if self.torch_inference or self.torch_fallback[transformer_name]:
|
|
current_model = self.torch_models[transformer_name]
|
|
|
|
noise_pred_cond = current_model(
|
|
hidden_states=latent_model_input,
|
|
timestep=timestep,
|
|
encoder_hidden_states=prompt_embeds,
|
|
attention_kwargs=attention_kwargs,
|
|
return_dict=False,
|
|
)[0]
|
|
|
|
if self.do_classifier_free_guidance:
|
|
noise_pred_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_pred_uncond + current_guidance_scale * (noise_pred_cond - noise_pred_uncond)
|
|
else:
|
|
noise_pred = noise_pred_cond
|
|
else:
|
|
if self.do_classifier_free_guidance:
|
|
params_cond = {
|
|
"hidden_states": latent_model_input,
|
|
"timestep": timestep,
|
|
"encoder_hidden_states": prompt_embeds,
|
|
}
|
|
|
|
# conditional engine call
|
|
output_cond = self.run_engine(transformer_name, params_cond)['denoised_latents']
|
|
|
|
noise_pred_cond = output_cond.clone()
|
|
|
|
params_uncond = {
|
|
"hidden_states": latent_model_input,
|
|
"timestep": timestep,
|
|
"encoder_hidden_states": negative_prompt_embeds,
|
|
}
|
|
|
|
# unconditional engine call
|
|
output_uncond = self.run_engine(transformer_name, params_uncond)['denoised_latents']
|
|
|
|
noise_pred_uncond = output_uncond.clone()
|
|
|
|
# Apply classifier-free guidance
|
|
noise_pred = noise_pred_uncond + current_guidance_scale * (noise_pred_cond - noise_pred_uncond)
|
|
else:
|
|
# No CFG
|
|
params = {
|
|
"hidden_states": latent_model_input,
|
|
"timestep": timestep,
|
|
"encoder_hidden_states": prompt_embeds,
|
|
}
|
|
noise_pred = self.run_engine(transformer_name, params)['denoised_latents']
|
|
|
|
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
|
|
|
|
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, step_index, t, callback_kwargs)
|
|
|
|
latents = callback_outputs.pop("latents", latents)
|
|
prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds)
|
|
negative_prompt_embeds = callback_outputs.pop("negative_prompt_embeds", negative_prompt_embeds)
|
|
|
|
if step_index == len(timesteps) - 1 or ((step_index + 1) > num_warmup_steps and (step_index + 1) % self.scheduler.order == 0):
|
|
progress_bar.update()
|
|
|
|
self.profile_stop('denoise')
|
|
return latents
|
|
|
|
# Adapted from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/wan/pipeline_wan.py#L324
|
|
def prepare_latents(
|
|
self,
|
|
batch_size: int,
|
|
num_channels_latents: int = 16,
|
|
height: int = 720,
|
|
width: int = 1280,
|
|
num_frames: int = 81,
|
|
dtype: Optional[torch.dtype] = None,
|
|
device: Optional[torch.device] = None,
|
|
generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
|
|
latents: Optional[torch.Tensor] = 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"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
|
|
f" size of {batch_size}. Make sure the batch size matches the length of the generators."
|
|
)
|
|
|
|
if generator is None and hasattr(self, 'generator'):
|
|
generator = self.generator
|
|
|
|
latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
|
|
return latents
|
|
|
|
# Copied from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/wan/pipeline_wan.py#L279
|
|
def check_inputs(
|
|
self,
|
|
prompt,
|
|
negative_prompt,
|
|
height,
|
|
width,
|
|
prompt_embeds=None,
|
|
negative_prompt_embeds=None,
|
|
callback_on_step_end_tensor_inputs=None,
|
|
guidance_scale_2=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 callback_on_step_end_tensor_inputs is not None:
|
|
pass
|
|
|
|
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 `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)}")
|
|
|
|
# Adapted from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/wan/pipeline_wan.py#L157
|
|
def _get_t5_prompt_embeds(
|
|
self,
|
|
prompt: Union[str, List[str]] = None,
|
|
num_videos_per_prompt: int = 1,
|
|
max_sequence_length: int = 226,
|
|
device: Optional[torch.device] = None,
|
|
dtype: Optional[torch.dtype] = None,
|
|
encoder: str = "text_encoder",
|
|
):
|
|
device = device or self._execution_device
|
|
dtype = dtype or (self.torch_models[encoder].dtype if self.torch_fallback.get(encoder) and encoder in self.torch_models else torch.float32)
|
|
|
|
prompt = [prompt] if isinstance(prompt, str) else prompt
|
|
prompt = [prompt_clean(u) for u in prompt]
|
|
batch_size = len(prompt)
|
|
|
|
text_inputs = self.tokenizer(
|
|
prompt,
|
|
padding="max_length",
|
|
max_length=max_sequence_length,
|
|
truncation=True,
|
|
add_special_tokens=True,
|
|
return_attention_mask=True,
|
|
return_tensors="pt",
|
|
)
|
|
text_input_ids, mask = text_inputs.input_ids, text_inputs.attention_mask
|
|
seq_lens = mask.gt(0).sum(dim=1).long()
|
|
|
|
if self.torch_inference or self.torch_fallback[encoder]:
|
|
outputs = self.torch_models[encoder](text_input_ids.to(device), mask.to(device))
|
|
prompt_embeds = outputs.last_hidden_state.clone()
|
|
else:
|
|
outputs = self.run_engine(encoder, {
|
|
"input_ids": text_input_ids.to(device),
|
|
"attention_mask": mask.to(device)
|
|
})
|
|
prompt_embeds = outputs['text_embeddings'].clone()
|
|
|
|
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)
|
|
|
|
return prompt_embeds
|
|
|
|
@property
|
|
def _execution_device(self):
|
|
return self.device
|
|
|
|
@property
|
|
def do_classifier_free_guidance(self):
|
|
return self.guidance_scale > 1.0
|
|
|
|
@property
|
|
def num_timesteps(self):
|
|
return self._num_timesteps
|
|
|
|
def decode_latents(self, latents, num_frames):
|
|
self.profile_start('vae_decoder', color='red')
|
|
|
|
vae_config = self.models['vae_decoder'].config
|
|
z_dim = vae_config.get("z_dim", 16)
|
|
|
|
vae_dtype = torch.float32
|
|
latents = latents.to(vae_dtype)
|
|
|
|
latents_mean = (
|
|
torch.tensor(vae_config.get("latents_mean"))
|
|
.view(1, z_dim, 1, 1, 1)
|
|
.to(latents.device, latents.dtype)
|
|
)
|
|
latents_std = 1.0 / torch.tensor(vae_config.get("latents_std")).view(1, z_dim, 1, 1, 1).to(
|
|
latents.device, latents.dtype
|
|
)
|
|
|
|
latents = latents / latents_std + latents_mean
|
|
|
|
frames = self.torch_models['vae_decoder'].decode(latents, return_dict=False)[0]
|
|
|
|
self.profile_stop('vae_decoder')
|
|
return frames
|
|
|
|
def postprocess(self, video: torch.Tensor, output_type: str = "pil"):
|
|
# Convert [F, C, H, W] -> [F, H, W, C]
|
|
video = video.permute(0, 2, 3, 1)
|
|
# Convert to list of PIL Images
|
|
video = (video + 1.0) / 2.0
|
|
video = torch.clamp(video, 0.0, 1.0)
|
|
video = (video * 255.0).to(torch.uint8).cpu().numpy()
|
|
pil_frames = [Image.fromarray(frame) for frame in video]
|
|
return pil_frames
|
|
|
|
def infer(
|
|
self,
|
|
prompt: Union[str, List[str]],
|
|
negative_prompt: Optional[Union[str, List[str]]] = None,
|
|
height: int = 720,
|
|
width: int = 1280,
|
|
num_frames: int = 81,
|
|
num_inference_steps: int = 40,
|
|
num_videos_per_prompt: int = 1,
|
|
generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
|
|
latents: Optional[torch.Tensor] = None,
|
|
prompt_embeds: Optional[torch.Tensor] = None,
|
|
negative_prompt_embeds: Optional[torch.Tensor] = None,
|
|
output_type: str = "pil",
|
|
attention_kwargs: Optional[Dict[str, Any]] = None,
|
|
callback_on_step_end: Optional[Union[Callable, Any]] = None,
|
|
callback_on_step_end_tensor_inputs: List[str] = ["latents"],
|
|
max_sequence_length: int = 512,
|
|
warmup: bool = False,
|
|
save_video: bool = True,
|
|
):
|
|
"""
|
|
Run the Wan text-to-video diffusion pipeline.
|
|
"""
|
|
|
|
self.check_inputs(
|
|
prompt,
|
|
negative_prompt,
|
|
height,
|
|
width,
|
|
prompt_embeds,
|
|
negative_prompt_embeds,
|
|
callback_on_step_end_tensor_inputs,
|
|
self.guidance_scale_2,
|
|
)
|
|
|
|
if num_frames % self.vae_scale_factor_temporal != 1:
|
|
print(f"[W] `num_frames - 1` has to be divisible by {self.vae_scale_factor_temporal}. Rounding to the nearest number.")
|
|
num_frames = num_frames // self.vae_scale_factor_temporal * self.vae_scale_factor_temporal + 1
|
|
num_frames = max(num_frames, 1)
|
|
|
|
device = self._execution_device
|
|
batch_size = 1
|
|
|
|
with torch.inference_mode(), trt.Runtime(TRT_LOGGER):
|
|
torch.cuda.synchronize()
|
|
e2e_tic = time.perf_counter()
|
|
|
|
with self.model_memory_manager(["text_encoder"], low_vram=self.low_vram):
|
|
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=num_videos_per_prompt,
|
|
prompt_embeds=prompt_embeds,
|
|
negative_prompt_embeds=negative_prompt_embeds,
|
|
max_sequence_length=max_sequence_length,
|
|
device=device,
|
|
)
|
|
|
|
transformer_dtype = torch.bfloat16
|
|
prompt_embeds = prompt_embeds.to(transformer_dtype)
|
|
if negative_prompt_embeds is not None:
|
|
negative_prompt_embeds = negative_prompt_embeds.to(transformer_dtype)
|
|
|
|
self.scheduler.set_timesteps(num_inference_steps, device=self.device)
|
|
timesteps = self.scheduler.timesteps
|
|
|
|
num_channels_latents = self.models["transformer"].config.get("in_channels", 16)
|
|
latents = self.prepare_latents(
|
|
batch_size * num_videos_per_prompt,
|
|
num_channels_latents,
|
|
height,
|
|
width,
|
|
num_frames,
|
|
torch.float32,
|
|
device,
|
|
generator,
|
|
latents,
|
|
)
|
|
|
|
num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order
|
|
self._num_timesteps = len(timesteps)
|
|
|
|
latents = self.denoise_latents(
|
|
latents=latents,
|
|
prompt_embeds=prompt_embeds,
|
|
negative_prompt_embeds=negative_prompt_embeds,
|
|
timesteps=timesteps,
|
|
guidance_scale=self.guidance_scale,
|
|
guidance_scale_2=self.guidance_scale_2,
|
|
transformer_dtype=transformer_dtype,
|
|
num_warmup_steps=num_warmup_steps,
|
|
attention_kwargs=attention_kwargs,
|
|
callback_on_step_end=callback_on_step_end,
|
|
callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs,
|
|
warmup=warmup,
|
|
)
|
|
|
|
with self.model_memory_manager(["vae_decoder"], low_vram=self.low_vram):
|
|
video_raw = self.decode_latents(latents, num_frames)
|
|
video = image_module.tensor2vid(video_raw, self, output_type="pil")
|
|
|
|
torch.cuda.synchronize()
|
|
e2e_toc = time.perf_counter()
|
|
|
|
walltime_ms = (e2e_toc - e2e_tic) * 1000.0
|
|
if not warmup:
|
|
self.print_summary(num_inference_steps, walltime_ms, batch_size, num_frames)
|
|
if save_video:
|
|
self.save_video(video[0], self.pipeline_type.name.lower(), prompt, self.seed)
|
|
|
|
return video, walltime_ms
|
|
|
|
def run(self, prompt, height, width, num_frames, batch_size, batch_count, num_warmup_runs, use_cuda_graph, **kwargs):
|
|
if self.low_vram and self.use_cuda_graph:
|
|
print("[W] Using low_vram, use_cuda_graph will be disabled")
|
|
self.use_cuda_graph = False
|
|
|
|
num_warmup_runs = max(1, num_warmup_runs) if use_cuda_graph else num_warmup_runs
|
|
if num_warmup_runs > 0:
|
|
print("[I] Warming up ..")
|
|
for _ in range(num_warmup_runs):
|
|
self.infer(prompt, height=height, width=width, num_frames=num_frames, warmup=True, **kwargs)
|
|
|
|
for _ in range(batch_count):
|
|
print("[I] Running Wan T2V pipeline")
|
|
if self.nvtx_profile:
|
|
cudart.cudaProfilerStart()
|
|
self.infer(prompt, height=height, width=width, num_frames=num_frames, warmup=False, **kwargs)
|
|
if self.nvtx_profile:
|
|
cudart.cudaProfilerStop()
|