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

344 lines
16 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 inspect
import time
import tensorrt as trt
import torch
from cuda.bindings import runtime as cudart
from diffusers import DDPMWuerstchenScheduler
from demo_diffusion.model import (
CLIPWithProjModel,
UNetCascadeModel,
VQGANModel,
make_tokenizer,
)
from demo_diffusion.pipeline.stable_diffusion_pipeline import StableDiffusionPipeline
from demo_diffusion.pipeline.type import PIPELINE_TYPE
TRT_LOGGER = trt.Logger(trt.Logger.ERROR)
class StableCascadePipeline(StableDiffusionPipeline):
"""
Application showcasing the acceleration of Stable Cascade pipelines using NVidia TensorRT.
"""
def __init__(
self,
version='cascade',
pipeline_type=PIPELINE_TYPE.CASCADE_PRIOR,
latent_dim_scale=10.67,
lite=False,
**kwargs
):
"""
Initializes the Stable Cascade pipeline.
Args:
version (str):
The version of the pipeline. Should be one of [cascade]
pipeline_type (PIPELINE_TYPE):
Type of current pipeline.
latent_dim_scale (float):
Multiplier to determine the VQ latent space size from the image embeddings. If the image embeddings are
height=24 and width=24, the VQ latent shape needs to be height=int(24*10.67)=256 and
width=int(24*10.67)=256 in order to match the training conditions.
lite (bool):
Boolean indicating if the Lite Version of the Stage B and Stage C models is to be used
"""
super().__init__(
version=version,
pipeline_type=pipeline_type,
**kwargs
)
self.config['clip_hidden_states'] = True
# from Diffusers: https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_cascade/pipeline_stable_cascade.py#L91C9-L91C41
self.latent_dim_scale = latent_dim_scale
self.lite = lite
def initializeModels(self, framework_model_dir, int8, fp8):
# Load text tokenizer(s)
self.tokenizer = make_tokenizer(self.version, self.pipeline_type, self.hf_token, framework_model_dir)
# Load pipeline models
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': self.max_batch_size}
self.fp16 = False # TODO: enable FP16 mode for decoder model (requires strongly typed engine)
self.bf16 = True
if 'clip' in self.stages:
self.models['clip'] = CLIPWithProjModel(**models_args, fp16=self.fp16, bf16=self.bf16, output_hidden_states=self.config.get('clip_hidden_states', False), subfolder='text_encoder')
if 'unet' in self.stages:
self.models['unet'] = UNetCascadeModel(**models_args, fp16=self.fp16, bf16=self.bf16, lite=self.lite, do_classifier_free_guidance=self.do_classifier_free_guidance)
if 'vqgan' in self.stages:
self.models['vqgan'] = VQGANModel(**models_args, fp16=self.fp16, bf16=self.bf16, latent_dim_scale = self.latent_dim_scale)
def encode_prompt(self, prompt, negative_prompt, encoder='clip', pooled_outputs=False, output_hidden_states=False):
self.profile_start(encoder, color='green')
tokenizer = self.tokenizer
def tokenize(prompt, output_hidden_states):
text_inputs = tokenizer(
prompt,
padding="max_length",
max_length=tokenizer.model_max_length,
truncation=True,
return_tensors="pt",
)
text_input_ids = text_inputs.input_ids.type(torch.int32).to(self.device)
attention_mask = text_inputs.attention_mask.type(torch.int32).to(self.device)
text_hidden_states = None
if self.torch_inference:
outputs = self.torch_models[encoder](text_input_ids, attention_mask=attention_mask, output_hidden_states=output_hidden_states)
text_embeddings = outputs[0].clone()
if output_hidden_states:
hidden_state_layer = -1
text_hidden_states = outputs['hidden_states'][hidden_state_layer].clone()
else:
# NOTE: output tensor for CLIP must be cloned because it will be overwritten when called again for negative prompt
outputs = self.runEngine(encoder, {'input_ids': text_input_ids, 'attention_mask': attention_mask})
text_embeddings = outputs['text_embeddings'].clone()
if output_hidden_states:
text_hidden_states = outputs['hidden_states'].clone()
return text_embeddings, text_hidden_states
# Tokenize prompt
text_embeddings, text_hidden_states = tokenize(prompt, output_hidden_states)
if self.do_classifier_free_guidance:
# Tokenize negative prompt
uncond_embeddings, uncond_hidden_states = tokenize(negative_prompt, output_hidden_states)
# Concatenate the unconditional and text embeddings into a single batch to avoid doing two forward passes for classifier free guidance
text_embeddings = torch.cat([text_embeddings, uncond_embeddings])
if pooled_outputs:
pooled_output = text_embeddings
if output_hidden_states:
text_embeddings = torch.cat([text_hidden_states, uncond_hidden_states]) if self.do_classifier_free_guidance else text_hidden_states
self.profile_stop(encoder)
if pooled_outputs:
return text_embeddings, pooled_output
return text_embeddings
def denoise_latent(self,
latents,
pooled_embeddings,
text_embeddings=None,
image_embeds=None,
effnet=None,
denoiser='unet',
timesteps=None,
):
do_autocast = False
with torch.autocast('cuda', enabled=do_autocast):
self.profile_start(denoiser, color='blue')
for step_index, timestep in enumerate(timesteps):
# ratio input required for stable cascade prior
timestep_ratio = timestep.expand(latents.size(0)).to(latents.dtype)
# Expand the latents and timestep_ratio if we are doing classifier free guidance
latent_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents
timestep_ratio_input = torch.cat([timestep_ratio] * 2) if self.do_classifier_free_guidance else timestep_ratio
params = {"sample": latent_model_input, "timestep_ratio": timestep_ratio_input, "clip_text_pooled": pooled_embeddings}
if text_embeddings is not None:
params.update({'clip_text': text_embeddings})
if image_embeds is not None:
params.update({'clip_img': image_embeds})
if effnet is not None:
params.update({'effnet': effnet})
# Predict the noise residual
if self.torch_inference:
noise_pred = self.torch_models[denoiser](**params)['sample']
else:
noise_pred = self.runEngine(denoiser, params)['latent']
# Perform guidance
if self.do_classifier_free_guidance:
noise_pred_text, noise_pred_uncond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_text - noise_pred_uncond)
# from diffusers (prepare_extra_step_kwargs)
extra_step_kwargs = {}
if "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()):
# TODO: configurable eta
eta = 0.0
extra_step_kwargs["eta"] = eta
if "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()):
extra_step_kwargs["generator"] = self.generator
latents = self.scheduler.step(noise_pred, timestep_ratio, latents, **extra_step_kwargs, return_dict=False)[0]
latents = latents.to(dtype=torch.bfloat16 if self.bf16 else torch.float32)
self.profile_stop(denoiser)
return latents
def decode_latent(self, latents, model_name='vqgan'):
self.profile_start(model_name, color='red')
latents = self.models[model_name].scale_factor * latents
if self.torch_inference:
images = self.torch_models[model_name](latents)['sample']
else:
images = self.runEngine(model_name, {'latent': latents})['images']
self.profile_stop(model_name)
return images
def print_summary(self, denoising_steps, walltime_ms, batch_size):
print('|-----------------|--------------|')
print('| {:^15} | {:^12} |'.format('Module', 'Latency'))
print('|-----------------|--------------|')
for stage in self.stages:
stage_name = stage + ' x ' + str(denoising_steps) if stage == 'unet' else stage
print(
"| {:^15} | {:>9.2f} ms |".format(
stage_name, cudart.cudaEventElapsedTime(self.events[stage][0], self.events[stage][1])[1],
)
)
print('|-----------------|--------------|')
print('| {:^15} | {:>9.2f} ms |'.format('Pipeline', walltime_ms))
print('|-----------------|--------------|')
print('Throughput: {:.5f} image/s'.format(batch_size*1000./walltime_ms))
def infer(
self,
prompt,
negative_prompt,
image_height,
image_width,
image_embeddings=None,
warmup=False,
verbose=False,
save_image=True,
):
"""
Run the diffusion pipeline.
Args:
prompt (str):
The text prompt to guide image generation.
negative_prompt (str):
The prompt not to guide the image generation.
image_height (int):
Height (in pixels) of the image to be generated. Must be a multiple of 8.
image_width (int):
Width (in pixels) of the image to be generated. Must be a multiple of 8.
image_embeddings (`torch.FloatTensor` or `List[torch.FloatTensor]`):
Image Embeddings either extracted from an image or generated by a Prior Model.
warmup (bool):
Indicate if this is a warmup run.
verbose (bool):
Verbose in logging
save_image (bool):
Save the generated image (if applicable)
"""
if self.pipeline_type.is_cascade_decoder():
assert image_embeddings is not None, "Image Embeddings are required to run the decoder. Provided None"
assert len(prompt) == len(negative_prompt)
batch_size = len(prompt)
# Spatial dimensions of latent tensor
latent_height = image_height // 42
latent_width = image_width // 42
if image_embeddings is not None:
assert latent_height == image_embeddings.shape[-2]
assert latent_width == image_embeddings.shape[-1]
if self.generator and self.seed:
self.generator.manual_seed(self.seed)
num_inference_steps = self.denoising_steps
with torch.inference_mode(), trt.Runtime(TRT_LOGGER):
torch.cuda.synchronize()
e2e_tic = time.perf_counter()
denoise_kwargs = {}
# TODO: support custom timesteps
timesteps = None
if timesteps is not None:
if "timesteps" not in set(inspect.signature(self.scheduler.set_timesteps).parameters.keys()):
raise ValueError(
f"The current scheduler class {self.scheduler.__class__}'s `set_timesteps` does not support custom"
f" timestep schedules. Please check whether you are using the correct scheduler."
)
self.scheduler.set_timesteps(timesteps=timesteps, device=self.device)
assert self.denoising_steps == len(self.scheduler.timesteps)
else:
self.scheduler.set_timesteps(self.denoising_steps, device=self.device)
timesteps = self.scheduler.timesteps.to(self.device)
if isinstance(self.scheduler, DDPMWuerstchenScheduler):
timesteps = timesteps[:-1]
denoise_kwargs.update({'timesteps': timesteps})
# Initialize latents
latents_dtpye = torch.float16 if self.fp16 else torch.bfloat16 if self.bf16 else torch.float32
latents = self.initialize_latents(
batch_size=batch_size,
unet_channels=16 if self.pipeline_type.is_cascade_prior() else 4, # TODO: can we query "in_channels" from config
latent_height=latent_height if self.pipeline_type.is_cascade_prior() else int(latent_height * self.latent_dim_scale),
latent_width=latent_width if self.pipeline_type.is_cascade_prior() else int(latent_width * self.latent_dim_scale),
latents_dtype=latents_dtpye
)
# CLIP text encoder(s)
text_embeddings, pooled_embeddings = self.encode_prompt(prompt, negative_prompt,
encoder='clip', pooled_outputs=True, output_hidden_states=True)
if self.pipeline_type.is_cascade_prior():
denoise_kwargs.update({'text_embeddings': text_embeddings})
# image embeds
image_embeds_pooled = torch.zeros(batch_size, 1, 768, device=self.device, dtype=latents_dtpye)
image_embeds = (torch.cat([image_embeds_pooled, torch.zeros_like(image_embeds_pooled)]) if self.do_classifier_free_guidance else image_embeddings)
denoise_kwargs.update({'image_embeds': image_embeds})
else:
effnet = (torch.cat([image_embeddings, torch.zeros_like(image_embeddings)]) if self.do_classifier_free_guidance else image_embeddings)
denoise_kwargs.update({'effnet': effnet})
# UNet denoiser
latents = self.denoise_latent(latents, pooled_embeddings.unsqueeze(1), denoiser='unet', **denoise_kwargs)
if not self.return_latents:
images = self.decode_latent(latents)
torch.cuda.synchronize()
e2e_toc = time.perf_counter()
walltime_ms = (e2e_toc - e2e_tic) * 1000.
if not warmup:
self.print_summary(num_inference_steps, walltime_ms, batch_size)
if not self.return_latents and save_image:
# post-process images
images = ((images) * 255).clamp(0, 255).detach().permute(0, 2, 3, 1).round().type(torch.uint8).cpu().numpy()
self.save_image(images, self.pipeline_type.name.lower(), prompt, self.seed)
return (latents, walltime_ms) if self.return_latents else (images, walltime_ms)