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

1102 lines
52 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 gc
import inspect
import json
import os
import pathlib
import sys
import time
from hashlib import md5
from typing import List, Optional
import modelopt.torch.opt as mto
import modelopt.torch.quantization as mtq
import numpy as np
import nvtx
import tensorrt as trt
import torch
from cuda.bindings import runtime as cudart
from diffusers import (
DDIMScheduler,
DDPMScheduler,
DDPMWuerstchenScheduler,
EulerAncestralDiscreteScheduler,
EulerDiscreteScheduler,
LCMScheduler,
LMSDiscreteScheduler,
PNDMScheduler,
UniPCMultistepScheduler,
)
import demo_diffusion.engine as engine_module
import demo_diffusion.image as image_module
from demo_diffusion.model import (
CLIPModel,
CLIPWithProjModel,
SDLoraLoader,
UNet2DConditionControlNetModel,
UNetModel,
UNetXLModel,
UNetXLModelControlNet,
VAEEncoderModel,
VAEModel,
get_clip_embedding_dim,
make_scheduler,
make_tokenizer,
merge_loras,
unload_torch_model,
)
from demo_diffusion.pipeline.calibrate import load_calib_prompts
from demo_diffusion.pipeline.type import PIPELINE_TYPE
from demo_diffusion.utils_modelopt import (
SD_FP8_FP16_DEFAULT_CONFIG,
SD_FP8_FP32_DEFAULT_CONFIG,
check_lora,
filter_func,
generate_fp8_scales,
get_int8_config,
quantize_lvl,
set_fmha,
)
TRT_LOGGER = trt.Logger(trt.Logger.ERROR)
class StableDiffusionPipeline:
SCHEDULER_DEFAULTS = {
"1.4": "PNDM",
"dreamshaper-7": "PNDM",
"xl-1.0" : "Euler",
"xl-turbo": "EulerA",
"svd-xt-1.1": "Euler",
"cascade": "DDPMWuerstchen"
}
"""
Application showcasing the acceleration of Stable Diffusion pipelines using NVidia TensorRT.
"""
def __init__(
self,
version='1.4',
pipeline_type=PIPELINE_TYPE.TXT2IMG,
max_batch_size=16,
denoising_steps=30,
scheduler=None,
guidance_scale=7.5,
device='cuda',
output_dir='.',
hf_token=None,
verbose=False,
nvtx_profile=False,
use_cuda_graph=False,
vae_scaling_factor=0.18215,
framework_model_dir='pytorch_model',
controlnets=None,
lora_scale: float = 1.0,
lora_weight: Optional[List[float]] = None,
lora_path: Optional[List[str]] = None,
return_latents=False,
torch_inference='',
):
"""
Initializes the Diffusion pipeline.
Args:
version (str):
The version of the pipeline. Should be one of [1.4, SDXL]
pipeline_type (PIPELINE_TYPE):
Type of current pipeline.
max_batch_size (int):
Maximum batch size for dynamic batch engine.
denoising_steps (int):
The number of denoising steps.
More denoising steps usually lead to a higher quality image at the expense of slower inference.
scheduler (str):
The scheduler to guide the denoising process. Must be one of [DDIM, DPM, EulerA, Euler, LCM, LMSD, PNDM].
guidance_scale (float):
Guidance scale is enabled by setting as > 1.
Higher guidance scale encourages to generate images that are closely linked to the text prompt, usually at the expense of lower image quality.
device (str):
PyTorch device to run inference. Default: 'cuda'
output_dir (str):
Output directory for log files and image artifacts
hf_token (str):
HuggingFace User Access Token to use for downloading Stable Diffusion model checkpoints.
verbose (bool):
Enable verbose logging.
nvtx_profile (bool):
Insert NVTX profiling markers.
use_cuda_graph (bool):
Use CUDA graph to capture engine execution and then launch inference
vae_scaling_factor (float):
VAE scaling factor
framework_model_dir (str):
cache directory for framework checkpoints
controlnets (str):
Which ControlNet/ControlNets to use.
return_latents (bool):
Skip decoding the image and return latents instead.
torch_inference (str):
Run inference with PyTorch (using specified compilation mode) instead of TensorRT.
"""
self.denoising_steps = denoising_steps
self.guidance_scale = guidance_scale
self.do_classifier_free_guidance = (guidance_scale > 1.0)
self.vae_scaling_factor = vae_scaling_factor
self.max_batch_size = max_batch_size
self.framework_model_dir = framework_model_dir
self.output_dir = output_dir
for directory in [self.framework_model_dir, self.output_dir]:
if not os.path.exists(directory):
print(f"[I] Create directory: {directory}")
pathlib.Path(directory).mkdir(parents=True)
self.hf_token = hf_token
self.device = device
self.verbose = verbose
self.nvtx_profile = nvtx_profile
self.version = version
self.controlnets = controlnets
# Pipeline type
self.pipeline_type = pipeline_type
if self.pipeline_type.is_txt2img():
self.stages = ['clip','unet','vae']
elif self.pipeline_type.is_img2img():
self.stages = ['vae_encoder', 'clip','unet','vae']
elif self.pipeline_type.is_sd_xl_base():
self.stages = ['clip', 'clip2', 'unetxl']
if not return_latents:
self.stages.append('vae')
elif self.pipeline_type.is_sd_xl_refiner():
self.stages = ['clip2', 'unetxl', 'vae']
elif self.pipeline_type.is_img2vid():
self.stages = ['clip-vis', 'clip-imgfe', 'unet-temp', 'vae-temp']
elif self.pipeline_type.is_cascade_prior():
self.stages = ['clip', 'unet']
elif self.pipeline_type.is_cascade_decoder():
self.stages = ['clip', 'unet', 'vqgan']
else:
raise ValueError(f"Unsupported pipeline {self.pipeline_type.name}.")
self.return_latents = return_latents
if not scheduler:
scheduler = 'UniPC' if self.pipeline_type.is_controlnet() else self.SCHEDULER_DEFAULTS.get(version, 'DDIM')
print(f"[I] Autoselected scheduler: {scheduler}")
scheduler_class_map = {
"DDIM" : DDIMScheduler,
"DDPM" : DDPMScheduler,
"EulerA" : EulerAncestralDiscreteScheduler,
"Euler" : EulerDiscreteScheduler,
"LCM" : LCMScheduler,
"LMSD" : LMSDiscreteScheduler,
"PNDM" : PNDMScheduler,
"UniPC" : UniPCMultistepScheduler,
"DDPMWuerstchen" : DDPMWuerstchenScheduler,
}
try:
scheduler_class = scheduler_class_map[scheduler]
except KeyError:
raise ValueError(f"Unsupported scheduler {scheduler}. Should be one of {list(scheduler_class.keys())}.")
self.scheduler = make_scheduler(scheduler_class, version, pipeline_type, hf_token, framework_model_dir)
self.config = {}
if self.pipeline_type.is_sd_xl():
self.config['clip_hidden_states'] = True
self.torch_inference = torch_inference
if self.torch_inference:
torch._inductor.config.conv_1x1_as_mm = True
torch._inductor.config.coordinate_descent_tuning = True
torch._inductor.config.epilogue_fusion = False
torch._inductor.config.coordinate_descent_check_all_directions = True
self.use_cuda_graph = use_cuda_graph
# initialized in loadEngines()
self.models = {}
self.torch_models = {}
self.engine = {}
self.shared_device_memory = None
# initialize lora loader and scales
self.lora_loader = None
self.lora_weights = dict()
if lora_path:
self.lora_loader = SDLoraLoader(lora_path, lora_weight, lora_scale)
assert len(lora_path) == len(lora_weight)
for i, path in enumerate(lora_path):
self.lora_weights[path] = lora_weight[i]
# initialized in loadResources()
self.events = {}
self.generator = None
self.markers = {}
self.seed = None
self.stream = None
self.tokenizer = None
def loadResources(self, image_height, image_width, batch_size, seed):
# Initialize noise generator
if seed:
self.seed = seed
self.generator = torch.Generator(device="cuda").manual_seed(seed)
# Create CUDA events and stream
for stage in self.stages:
self.events[stage] = [cudart.cudaEventCreate()[1], cudart.cudaEventCreate()[1]]
self.stream = cudart.cudaStreamCreate()[1]
# Allocate TensorRT I/O buffers
if not self.torch_inference:
for model_name, obj in self.models.items():
self.engine[model_name].allocate_buffers(shape_dict=obj.get_shape_dict(batch_size, image_height, image_width), device=self.device)
def teardown(self):
for e in self.events.values():
cudart.cudaEventDestroy(e[0])
cudart.cudaEventDestroy(e[1])
for engine in self.engine.values():
del engine
if self.shared_device_memory:
cudart.cudaFree(self.shared_device_memory)
for torch_model in self.torch_models.values():
del torch_model
cudart.cudaStreamDestroy(self.stream)
del self.stream
def cachedModelName(self, model_name):
return model_name
def getOnnxPath(self, model_name, onnx_dir, opt=True, suffix=''):
onnx_model_dir = os.path.join(onnx_dir, self.cachedModelName(model_name)+suffix+('.opt' if opt else ''))
os.makedirs(onnx_model_dir, exist_ok=True)
return os.path.join(onnx_model_dir, 'model.onnx')
def getEnginePath(self, model_name, engine_dir, enable_refit=False, suffix=''):
return os.path.join(engine_dir, self.cachedModelName(model_name)+suffix+('.refit' if enable_refit else '')+'.trt'+trt.__version__+'.plan')
def getWeightsMapPath(self, model_name, onnx_dir):
onnx_model_dir = os.path.join(onnx_dir, self.cachedModelName(model_name)+'.opt')
os.makedirs(onnx_model_dir, exist_ok=True)
return os.path.join(onnx_model_dir, 'weights_map.json')
def getRefitNodesPath(self, model_name, onnx_dir, suffix=''):
onnx_model_dir = os.path.join(onnx_dir, self.cachedModelName(model_name)+'.opt')
os.makedirs(onnx_model_dir, exist_ok=True)
return os.path.join(onnx_model_dir, 'refit'+suffix+'.json')
def getStateDictPath(self, model_name, onnx_dir, suffix=''):
onnx_model_dir = os.path.join(onnx_dir, self.cachedModelName(model_name)+suffix)
os.makedirs(onnx_model_dir, exist_ok=True)
return os.path.join(onnx_model_dir, 'state_dict.pt')
def initializeModels(self, framework_model_dir, int8, fp8):
# Load text tokenizer(s)
if not self.pipeline_type.is_sd_xl_refiner():
self.tokenizer = make_tokenizer(self.version, self.pipeline_type, self.hf_token, framework_model_dir)
if self.pipeline_type.is_sd_xl():
self.tokenizer2 = make_tokenizer(self.version, self.pipeline_type, self.hf_token, framework_model_dir, subfolder='tokenizer_2')
# 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}
if 'clip' in self.stages:
subfolder = 'text_encoder'
self.models['clip'] = CLIPModel(**models_args, fp16=True, embedding_dim=get_clip_embedding_dim(self.version, self.pipeline_type), output_hidden_states=self.config.get('clip_hidden_states', False), subfolder=subfolder)
if 'clip2' in self.stages:
subfolder = 'text_encoder_2'
self.models['clip2'] = CLIPWithProjModel(**models_args, fp16=True, output_hidden_states=self.config.get('clip_hidden_states', False), subfolder=subfolder)
if 'unet' in self.stages:
self.models['unet'] = UNetModel(**models_args, fp16=True, int8=int8, fp8=fp8, controlnets=self.controlnets, do_classifier_free_guidance=self.do_classifier_free_guidance)
if 'unetxl' in self.stages:
if not self.controlnets:
self.models["unetxl"] = UNetXLModel(
**models_args,
fp16=True,
int8=int8,
fp8=fp8,
do_classifier_free_guidance=self.do_classifier_free_guidance,
)
else:
self.models["unetxl"] = UNetXLModelControlNet(
**models_args,
fp16=True,
int8=int8,
fp8=fp8,
controlnets=self.controlnets,
do_classifier_free_guidance=self.do_classifier_free_guidance,
)
vae_fp16 = not self.pipeline_type.is_sd_xl()
if 'vae' in self.stages:
self.models['vae'] = VAEModel(**models_args, fp16=vae_fp16, tf32=True)
if 'vae_encoder' in self.stages:
self.models['vae_encoder'] = VAEEncoderModel(**models_args, fp16=vae_fp16)
def loadEngines(
self,
engine_dir,
framework_model_dir,
onnx_dir,
onnx_opset,
opt_batch_size,
opt_image_height,
opt_image_width,
optimization_level=3,
static_batch=False,
static_shape=True,
enable_refit=False,
enable_all_tactics=False,
timing_cache=None,
int8=False,
fp8=False,
quantization_level=2.5,
quantization_percentile=1.0,
quantization_alpha=0.8,
calibration_size=32,
calib_batch_size=2,
**_kwargs,
):
"""
Build and load engines for TensorRT accelerated inference.
Export ONNX models first, if applicable.
Args:
engine_dir (str):
Directory to store the TensorRT engines.
framework_model_dir (str):
Directory to store the framework model ckpt.
onnx_dir (str):
Directory to store the ONNX models.
onnx_opset (int):
ONNX opset version to export the models.
opt_batch_size (int):
Batch size to optimize for during engine building.
opt_image_height (int):
Image height to optimize for during engine building. Must be a multiple of 8.
opt_image_width (int):
Image width to optimize for during engine building. Must be a multiple of 8.
optimization_level (int):
Optimization level to build the TensorRT engine with.
static_batch (bool):
Build engine only for specified opt_batch_size.
static_shape (bool):
Build engine only for specified opt_image_height & opt_image_width. Default = True.
enable_refit (bool):
Build engines with refit option enabled.
enable_all_tactics (bool):
Enable all tactic sources during TensorRT engine builds.
timing_cache (str):
Path to the timing cache to speed up TensorRT build.
int8 (bool):
Whether to quantize to int8 format or not (SDXL, SD15 and SD21 only).
fp8 (bool):
Whether to quantize to fp8 format or not (SDXL, SD15 and SD21 only).
quantization_level (float):
Controls which layers to quantize. 1: CNN, 2: CNN+FFN, 2.5: CNN+FFN+QKV, 3: CNN+FC
quantization_percentile (float):
Control quantization scaling factors (amax) collecting range, where the minimum amax in
range(n_steps * percentile) will be collected. Recommendation: 1.0
quantization_alpha (float):
The alpha parameter for SmoothQuant quantization used for linear layers.
Recommendation: 0.8 for SDXL
calibration_size (int):
The number of steps to use for calibrating the model for quantization.
Recommendation: 32, 64, 128 for SDXL
calib_batch_size (int):
The batch size to use for calibration. Defaults to 2.
"""
# Create directories if missing
for directory in [engine_dir, onnx_dir]:
if not os.path.exists(directory):
print(f"[I] Create directory: {directory}")
pathlib.Path(directory).mkdir(parents=True)
# Initialize models
self.initializeModels(framework_model_dir, int8, fp8)
# Configure pipeline models to load
model_names = self.models.keys()
lora_suffix = '-'+'-'.join([str(md5(path.encode('utf-8')).hexdigest())+'-'+('%.2f' % self.lora_weights[path])+'-'+('%.2f' % self.lora_loader.scale) for path in sorted(self.lora_loader.paths)]) if self.lora_loader else ''
# Enable refit and LoRA merging only for UNet & UNetXL for now
do_engine_refit = dict(zip(model_names, [not self.pipeline_type.is_sd_xl_refiner() and enable_refit and model_name.startswith('unet') for model_name in model_names]))
do_lora_merge = dict(zip(model_names, [not enable_refit and self.lora_loader and model_name.startswith('unet') for model_name in model_names]))
# Torch fallback for VAE if specified
torch_fallback = dict(zip(model_names, [self.torch_inference for model_name in model_names]))
model_suffix = dict(zip(model_names, [lora_suffix if do_lora_merge[model_name] else '' for model_name in model_names]))
use_int8 = dict.fromkeys(model_names, False)
use_fp8 = dict.fromkeys(model_names, False)
if int8:
assert self.pipeline_type.is_sd_xl_base() or self.version == "1.4", "int8 quantization only supported for SDXL and SD1.4 pipeline"
model_name = 'unetxl' if self.pipeline_type.is_sd_xl() else 'unet'
use_int8[model_name] = True
model_suffix[model_name] += f"-int8.l{quantization_level}.bs2.s{self.denoising_steps}.c{calibration_size}.p{quantization_percentile}.a{quantization_alpha}"
elif fp8:
assert self.pipeline_type.is_sd_xl() or self.version == "1.4", "fp8 quantization only supported for SDXL and SD1.4 pipeline"
model_name = 'unetxl' if self.pipeline_type.is_sd_xl() else 'unet'
use_fp8[model_name] = True
model_suffix[model_name] += f"-fp8.l{quantization_level}.bs2.s{self.denoising_steps}.c{calibration_size}.p{quantization_percentile}.a{quantization_alpha}"
onnx_path = dict(zip(model_names, [self.getOnnxPath(model_name, onnx_dir, opt=False, suffix=model_suffix[model_name]) for model_name in model_names]))
onnx_opt_path = dict(zip(model_names, [self.getOnnxPath(model_name, onnx_dir, suffix=model_suffix[model_name]) for model_name in model_names]))
engine_path = dict(zip(model_names, [self.getEnginePath(model_name, engine_dir, do_engine_refit[model_name], suffix=model_suffix[model_name]) for model_name in model_names]))
weights_map_path = dict(zip(model_names, [(self.getWeightsMapPath(model_name, onnx_dir) if do_engine_refit[model_name] else None) for model_name in model_names]))
for model_name, obj in self.models.items():
if torch_fallback[model_name]:
continue
# Export models to ONNX and save weights name mapping
do_export_onnx = not os.path.exists(engine_path[model_name]) and not os.path.exists(onnx_opt_path[model_name])
do_export_weights_map = weights_map_path[model_name] and not os.path.exists(weights_map_path[model_name])
if do_export_onnx or do_export_weights_map:
# Non-quantized ONNX export
if not use_int8[model_name] and not use_fp8[model_name]:
obj.export_onnx(onnx_path[model_name], onnx_opt_path[model_name], onnx_opset, opt_image_height, opt_image_width, enable_lora_merge=do_lora_merge[model_name], static_shape=static_shape, lora_loader=self.lora_loader)
else:
pipeline = obj.get_pipeline()
if self.pipeline_type.is_controlnet():
model = UNet2DConditionControlNetModel(pipeline.unet, pipeline.controlnet.nets)
else:
model = pipeline.unet
if use_fp8[model_name] and quantization_level == 4.0:
set_fmha(model)
state_dict_path = self.getStateDictPath(model_name, onnx_dir, suffix=model_suffix[model_name])
if not os.path.exists(state_dict_path):
print(f"[I] Calibrated weights not found, generating {state_dict_path}")
root_dir = os.path.dirname(os.path.abspath(sys.modules["__main__"].__file__))
calibration_file = os.path.join(root_dir, "calibration_data", "calibration-prompts.txt")
calibration_prompts = load_calib_prompts(calib_batch_size, calibration_file)
if self.pipeline_type.is_controlnet():
calibration_image_canny = image_module.download_image(
"https://huggingface.co/diffusers/controlnet-canny-sdxl-1.0/resolve/main/out_bird.png"
)
# "out_bird.png" has 5 images combined in a row. We pick the first image which is the input image.
calibration_image_canny = calibration_image_canny.crop(
(0, 0, calibration_image_canny.width / 5, calibration_image_canny.height)
)
calibration_images = [calibration_image_canny]
# TODO check size > calibration_size
def do_calibrate(pipeline, calibration_prompts, **kwargs):
for i_th, prompts in enumerate(calibration_prompts):
if i_th >= kwargs["calib_size"]:
return
pipeline_call_kwargs = {
"prompt": prompts,
"num_inference_steps": kwargs["n_steps"],
"negative_prompt": [
"normal quality, low quality, worst quality, low res, blurry, nsfw, nude"
]
* len(prompts),
}
if self.pipeline_type.is_controlnet():
pipeline_call_kwargs["image"] = calibration_images
pipeline(**pipeline_call_kwargs).images
def forward_loop(model):
if self.pipeline_type.is_controlnet():
pipeline.unet = model.unet
pipeline.controlnet.nets = model.controlnets
else:
pipeline.unet = model
do_calibrate(
pipeline=pipeline,
calibration_prompts=calibration_prompts,
calib_size=calibration_size // calib_batch_size,
n_steps=self.denoising_steps,
)
print(f"[I] Performing calibration for {calibration_size} steps.")
if use_int8[model_name]:
quant_config = get_int8_config(
model,
quantization_level,
quantization_alpha,
quantization_percentile,
self.denoising_steps
)
elif use_fp8[model_name]:
quant_config = SD_FP8_FP16_DEFAULT_CONFIG
# Handle LoRA
if do_lora_merge[model_name]:
assert self.lora_loader is not None
model = merge_loras(model, self.lora_loader)
check_lora(model)
mtq.quantize(model, quant_config, forward_loop)
mto.save(model, state_dict_path)
else:
mto.restore(model, state_dict_path)
print(f"[I] Generating quantized ONNX model: {onnx_opt_path[model_name]}")
if not os.path.exists(onnx_path[model_name]):
quantize_lvl(self.version, model, quantization_level)
mtq.disable_quantizer(model, filter_func)
if use_fp8[model_name]:
generate_fp8_scales(model)
else:
model = None
obj.export_onnx(onnx_path[model_name], onnx_opt_path[model_name], onnx_opset, opt_image_height, opt_image_width, custom_model=model, static_shape=static_shape)
# FIXME do_export_weights_map needs ONNX graph
if do_export_weights_map:
print(f"[I] Saving weights map: {weights_map_path[model_name]}")
obj.export_weights_map(onnx_opt_path[model_name], weights_map_path[model_name])
# Release temp GPU memory during onnx export to avoid OOM.
gc.collect()
torch.cuda.empty_cache()
# Build TensorRT engines
for model_name, obj in self.models.items():
if torch_fallback[model_name]:
continue
engine = engine_module.Engine(engine_path[model_name])
if not os.path.exists(engine_path[model_name]):
update_output_names = obj.get_output_names() + obj.extra_output_names if obj.extra_output_names else None
# TF32 can be enabled for all precisions (including INT8/FP8)
tf32amp = obj.tf32
precision_constraints = 'none'
engine.build(onnx_opt_path[model_name],
tf32=tf32amp,
input_profile=obj.get_input_profile(
opt_batch_size, opt_image_height, opt_image_width,
static_batch=static_batch, static_shape=static_shape
),
enable_refit=do_engine_refit[model_name],
enable_all_tactics=enable_all_tactics,
timing_cache=timing_cache,
update_output_names=update_output_names,
verbose=self.verbose,
builder_optimization_level=optimization_level,
precision_constraints=precision_constraints,
)
self.engine[model_name] = engine
# Load TensorRT engines
for model_name, obj in self.models.items():
if torch_fallback[model_name]:
continue
self.engine[model_name].load()
if do_engine_refit[model_name] and self.lora_loader:
assert weights_map_path[model_name]
with open(weights_map_path[model_name], 'r') as fp_wts:
print(f"[I] Loading weights map: {weights_map_path[model_name]} ")
[weights_name_mapping, weights_shape_mapping] = json.load(fp_wts)
refit_weights_path = self.getRefitNodesPath(model_name, engine_dir, suffix=lora_suffix)
if not os.path.exists(refit_weights_path):
print(f"[I] Saving refit weights: {refit_weights_path}")
model = merge_loras(obj.get_model(), self.lora_loader)
refit_weights, updated_weight_names = engine_module.get_refit_weights(
model.state_dict(), onnx_opt_path[model_name], weights_name_mapping, weights_shape_mapping
)
torch.save((refit_weights, updated_weight_names), refit_weights_path)
unload_torch_model(model)
else:
print(f"[I] Loading refit weights: {refit_weights_path}")
refit_weights, updated_weight_names = torch.load(refit_weights_path)
self.engine[model_name].refit(refit_weights, updated_weight_names)
# Load torch models
for model_name, obj in self.models.items():
if torch_fallback[model_name]:
self.torch_models[model_name] = obj.get_model(torch_inference=self.torch_inference)
# Release temp GPU memory during onnx export to avoid OOM.
gc.collect()
torch.cuda.empty_cache()
def calculateMaxDeviceMemory(self):
max_device_memory = 0
for model_name, engine in self.engine.items():
max_device_memory = max(max_device_memory, engine.engine.device_memory_size_v2)
return max_device_memory
def activateEngines(self, shared_device_memory=None):
if shared_device_memory is None:
max_device_memory = self.calculateMaxDeviceMemory()
_, shared_device_memory = cudart.cudaMalloc(max_device_memory)
self.shared_device_memory = shared_device_memory
# Load and activate TensorRT engines
for engine in self.engine.values():
engine.activate(device_memory=self.shared_device_memory)
def runEngine(self, model_name, feed_dict):
engine = self.engine[model_name]
return engine.infer(feed_dict, self.stream, use_cuda_graph=self.use_cuda_graph)
def initialize_latents(self, batch_size, unet_channels, latent_height, latent_width, latents_dtype=torch.float32):
latents_dtype = latents_dtype # text_embeddings.dtype
latents_shape = (batch_size, unet_channels, latent_height, latent_width)
latents = torch.randn(latents_shape, device=self.device, dtype=latents_dtype, generator=self.generator)
# Scale the initial noise by the standard deviation required by the scheduler
latents = latents * self.scheduler.init_noise_sigma
return latents
def profile_start(self, name, color='blue'):
if self.nvtx_profile:
self.markers[name] = nvtx.start_range(message=name, color=color)
if name in self.events:
cudart.cudaEventRecord(self.events[name][0], 0)
def profile_stop(self, name):
if name in self.events:
cudart.cudaEventRecord(self.events[name][1], 0)
if self.nvtx_profile:
nvtx.end_range(self.markers[name])
def preprocess_images(self, batch_size, images=()):
if not images:
return ()
self.profile_start('preprocess', color='pink')
input_images=[]
for image in images:
image = image.to(self.device).float()
if image.shape[0] != batch_size:
image = image.repeat(batch_size, 1, 1, 1)
input_images.append(image)
self.profile_stop('preprocess')
return tuple(input_images)
def preprocess_controlnet_images(self, batch_size, images=None):
'''
images: List of PIL.Image.Image
'''
if images is None:
return None
self.profile_start('preprocess', color='pink')
images = [(np.array(i.convert("RGB")).astype(np.float32) / 255.0)[..., None].transpose(3, 2, 0, 1).repeat(batch_size, axis=0) for i in images]
# do_classifier_free_guidance
images = [torch.cat([torch.from_numpy(i).to(self.device).float()] * 2) for i in images]
images = torch.cat([image[None, ...] for image in images], dim=0)
self.profile_stop('preprocess')
return images
def encode_prompt(self, prompt, negative_prompt, encoder='clip', pooled_outputs=False, output_hidden_states=False):
self.profile_start(encoder, color='green')
tokenizer = self.tokenizer2 if encoder == 'clip2' else self.tokenizer
def tokenize(prompt, output_hidden_states):
text_input_ids = tokenizer(
prompt,
padding="max_length",
max_length=tokenizer.model_max_length,
truncation=True,
return_tensors="pt",
).input_ids.type(torch.int32).to(self.device)
text_hidden_states = None
if self.torch_inference:
outputs = self.torch_models[encoder](text_input_ids, output_hidden_states=output_hidden_states)
text_embeddings = outputs[0].clone()
if output_hidden_states:
text_hidden_states = outputs['hidden_states'][-2].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})
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([uncond_embeddings, text_embeddings]).to(dtype=torch.float16)
if pooled_outputs:
pooled_output = text_embeddings
if output_hidden_states:
text_embeddings = torch.cat([uncond_hidden_states, text_hidden_states]).to(dtype=torch.float16) 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
# from diffusers (get_timesteps)
def get_timesteps(self, num_inference_steps, strength, denoising_start=None):
# get the original timestep using init_timestep
if denoising_start is None:
init_timestep = min(int(num_inference_steps * strength), num_inference_steps)
t_start = max(num_inference_steps - init_timestep, 0)
else:
t_start = 0
timesteps = self.scheduler.timesteps[t_start * self.scheduler.order :]
# Strength is irrelevant if we directly request a timestep to start at;
# that is, strength is determined by the denoising_start instead.
if denoising_start is not None:
discrete_timestep_cutoff = int(
round(
self.scheduler.config.num_train_timesteps
- (denoising_start * self.scheduler.config.num_train_timesteps)
)
)
num_inference_steps = (timesteps < discrete_timestep_cutoff).sum().item()
if self.scheduler.order == 2 and num_inference_steps % 2 == 0:
# if the scheduler is a 2nd order scheduler we might have to do +1
# because `num_inference_steps` might be even given that every timestep
# (except the highest one) is duplicated. If `num_inference_steps` is even it would
# mean that we cut the timesteps in the middle of the denoising step
# (between 1st and 2nd devirative) which leads to incorrect results. By adding 1
# we ensure that the denoising process always ends after the 2nd derivate step of the scheduler
num_inference_steps = num_inference_steps + 1
# because t_n+1 >= t_n, we slice the timesteps starting from the end
timesteps = timesteps[-num_inference_steps:]
return timesteps, num_inference_steps
return timesteps, num_inference_steps - t_start
def denoise_latent(self,
latents,
text_embeddings,
denoiser='unet',
timesteps=None,
step_offset=0,
mask=None,
masked_image_latents=None,
image_guidance=1.5,
controlnet_imgs=None,
controlnet_scales=None,
text_embeds=None,
time_ids=None):
assert image_guidance > 1.0, "Image guidance has to be > 1.0"
controlnet_imgs = self.preprocess_controlnet_images(latents.shape[0], controlnet_imgs)
do_autocast = self.torch_inference != '' and self.models[denoiser].fp16
with torch.autocast('cuda', enabled=do_autocast):
self.profile_start(denoiser, color='blue')
for step_index, timestep in enumerate(timesteps):
# Expand the latents if we are doing classifier free guidance
latent_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents
latent_model_input = self.scheduler.scale_model_input(latent_model_input, timestep)
if isinstance(mask, torch.Tensor):
latent_model_input = torch.cat([latent_model_input, mask, masked_image_latents], dim=1)
# Predict the noise residual
if self.torch_inference:
params = {"sample": latent_model_input, "timestep": timestep, "encoder_hidden_states": text_embeddings}
if controlnet_imgs is not None:
params.update({"images": controlnet_imgs, "controlnet_scales": controlnet_scales})
added_cond_kwargs = {}
if text_embeds != None:
added_cond_kwargs.update({'text_embeds': text_embeds})
if time_ids != None:
added_cond_kwargs.update({'time_ids': time_ids})
if text_embeds != None or time_ids != None:
params.update({'added_cond_kwargs': added_cond_kwargs})
noise_pred = self.torch_models[denoiser](**params)["sample"]
else:
timestep_float = timestep.float() if timestep.dtype != torch.float32 else timestep
params = {"sample": latent_model_input, "timestep": timestep_float, "encoder_hidden_states": text_embeddings}
if controlnet_imgs is not None:
params.update({"images": controlnet_imgs, "controlnet_scales": controlnet_scales})
if text_embeds != None:
params.update({'text_embeds': text_embeds})
if time_ids != None:
params.update({'time_ids': time_ids})
noise_pred = self.runEngine(denoiser, params)['latent']
# Perform guidance
if self.do_classifier_free_guidance:
noise_pred_uncond, noise_pred_text = 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, latents, **extra_step_kwargs, return_dict=False)[0]
latents = 1. / self.vae_scaling_factor * latents
latents = latents.to(dtype=torch.float32)
self.profile_stop(denoiser)
return latents
def encode_image(self, input_image):
self.profile_start('vae_encoder', color='red')
cast_to = torch.float16 if self.models['vae_encoder'].fp16 else torch.bfloat16 if self.models['vae_encoder'].bf16 else torch.float32
input_image = input_image.to(dtype=cast_to)
if self.torch_inference:
image_latents = self.torch_models['vae_encoder'](input_image)
else:
image_latents = self.runEngine('vae_encoder', {'images': input_image})['latent']
image_latents = self.vae_scaling_factor * image_latents
self.profile_stop('vae_encoder')
return image_latents
def decode_latent(self, latents):
self.profile_start('vae', color='red')
cast_to = torch.float16 if self.models['vae'].fp16 else torch.bfloat16 if self.models['vae'].bf16 else torch.float32
latents = latents.to(dtype=cast_to)
if self.torch_inference:
images = self.torch_models['vae'](latents, return_dict=False)[0]
else:
images = self.runEngine('vae', {'latent': latents})['images']
self.profile_stop('vae')
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
if "unet" in stage:
if self.pipeline_type.is_controlnet():
stage_name += '+cnet'
stage_name += ' x ' + str(denoising_steps)
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 save_image(self, images, pipeline, prompt, seed):
# Save image
image_name_prefix = pipeline+''.join(set(['-'+prompt[i].replace(' ','_')[:10] for i in range(len(prompt))]))+'-'+str(seed)+'-'
image_name_suffix = 'torch' if self.torch_inference else 'trt'
image_module.save_image(images, self.output_dir, image_name_prefix, image_name_suffix)
def infer(
self,
prompt,
negative_prompt,
image_height,
image_width,
input_image=None,
image_strength=0.75,
controlnet_scales=None,
aesthetic_score=6.0,
negative_aesthetic_score=2.5,
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.
input_image (image):
Input image used to initialize the latents.
image_strength (float):
Strength of transformation applied to input_image. Must be between 0 and 1.
controlnet_scales (torch.Tensor)
A tensor which containes ControlNet scales, essential for multi ControlNet.
Must be equal to number of Controlnets.
warmup (bool):
Indicate if this is a warmup run.
verbose (bool):
Verbose in logging
save_image (bool):
Save the generated image (if applicable)
"""
assert len(prompt) == len(negative_prompt)
batch_size = len(prompt)
# Spatial dimensions of latent tensor
latent_height = image_height // 8
latent_width = image_width // 8
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()
# 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)
denoise_kwargs = {}
if not (self.pipeline_type.is_img2img() or self.pipeline_type.is_sd_xl_refiner()):
# Initialize latents
latents = self.initialize_latents(batch_size=batch_size,
unet_channels=4,
latent_height=latent_height,
latent_width=latent_width)
if self.pipeline_type.is_controlnet():
denoise_kwargs.update({'controlnet_imgs': input_image, 'controlnet_scales': controlnet_scales})
# Pre-process and VAE encode input image
if self.pipeline_type.is_img2img() or self.pipeline_type.is_sd_xl_refiner():
assert input_image != None
# Initialize timesteps and pre-process input image
timesteps, num_inference_steps = self.get_timesteps(self.denoising_steps, image_strength)
denoise_kwargs.update({'timesteps': timesteps})
if self.pipeline_type.is_img2img() or self.pipeline_type.is_sd_xl_refiner():
latent_timestep = timesteps[:1].repeat(batch_size)
input_image = self.preprocess_images(batch_size, (input_image,))[0]
# Encode if not a latent
image_latents = input_image if input_image.shape[1] == 4 else self.encode_image(input_image)
# Add noise to latents using timesteps
noise = torch.randn(image_latents.shape, generator=self.generator, device=self.device, dtype=torch.float32)
latents = self.scheduler.add_noise(image_latents, noise, latent_timestep)
# CLIP text encoder(s)
if self.pipeline_type.is_sd_xl():
text_embeddings2, pooled_embeddings2 = self.encode_prompt(prompt, negative_prompt,
encoder='clip2', pooled_outputs=True, output_hidden_states=True)
# Merge text embeddings
if self.pipeline_type.is_sd_xl_base():
text_embeddings = self.encode_prompt(prompt, negative_prompt, output_hidden_states=True)
text_embeddings = torch.cat([text_embeddings, text_embeddings2], dim=-1)
else:
text_embeddings = text_embeddings2
# Time embeddings
def _get_add_time_ids(original_size, crops_coords_top_left, target_size, dtype, aesthetic_score=None, negative_aesthetic_score=None):
if self.pipeline_type.is_sd_xl_refiner(): #self.requires_aesthetics_score:
add_time_ids = list(original_size + crops_coords_top_left + (aesthetic_score,))
if self.do_classifier_free_guidance:
add_neg_time_ids = list(original_size + crops_coords_top_left + (negative_aesthetic_score,))
else:
add_time_ids = list(original_size + crops_coords_top_left + target_size)
if self.do_classifier_free_guidance:
add_neg_time_ids = list(original_size + crops_coords_top_left + target_size)
add_time_ids = torch.tensor([add_time_ids], dtype=dtype, device=self.device)
if self.do_classifier_free_guidance:
add_neg_time_ids = torch.tensor([add_neg_time_ids], dtype=dtype, device=self.device)
add_time_ids = torch.cat([add_neg_time_ids, add_time_ids], dim=0)
return add_time_ids
original_size = (image_height, image_width)
crops_coords_top_left = (0, 0)
target_size = (image_height, image_width)
if self.pipeline_type.is_sd_xl_refiner():
add_time_ids = _get_add_time_ids(
original_size, crops_coords_top_left, target_size, dtype=text_embeddings.dtype, aesthetic_score=aesthetic_score, negative_aesthetic_score=negative_aesthetic_score
)
else:
add_time_ids = _get_add_time_ids(
original_size, crops_coords_top_left, target_size, dtype=text_embeddings.dtype
)
add_time_ids = add_time_ids.repeat(batch_size, 1)
denoise_kwargs.update({'text_embeds': pooled_embeddings2, 'time_ids': add_time_ids})
else:
text_embeddings = self.encode_prompt(prompt, negative_prompt)
# UNet denoiser + (optional) ControlNet(s)
denoiser = 'unetxl' if self.pipeline_type.is_sd_xl() else 'unet'
latents = self.denoise_latent(latents, text_embeddings, denoiser=denoiser, **denoise_kwargs)
# VAE decode latent (if applicable)
if self.return_latents:
latents = latents * self.vae_scaling_factor
else:
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 + 1) * 255 / 2).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)
def run(self, prompt, negative_prompt, height, width, batch_size, batch_count, num_warmup_runs, use_cuda_graph, **kwargs):
# Process prompt
if not isinstance(prompt, list):
raise ValueError(f"`prompt` must be of type `str` list, but is {type(prompt)}")
prompt = prompt * batch_size
if not isinstance(negative_prompt, list):
raise ValueError(f"`--negative-prompt` must be of type `str` list, but is {type(negative_prompt)}")
if len(negative_prompt) == 1:
negative_prompt = negative_prompt * batch_size
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, negative_prompt, height, width, warmup=True, **kwargs)
for _ in range(batch_count):
print("[I] Running StableDiffusion pipeline")
if self.nvtx_profile:
cudart.cudaProfilerStart()
self.infer(prompt, negative_prompt, height, width, warmup=False, **kwargs)
if self.nvtx_profile:
cudart.cudaProfilerStop()