Files
wehub-resource-sync c8a779b1bb
Docker Image CI / build-ubuntu2004 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:55 +08:00

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()