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

1393 lines
56 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.
#
from __future__ import annotations
import argparse
import inspect
import os
import time
import warnings
from typing import Any, List, Optional
import numpy as np
import tensorrt as trt
import torch
from cuda.bindings import runtime as cudart
from diffusers.image_processor import VaeImageProcessor
from flux.content_filters import PixtralContentFilter
from huggingface_hub import snapshot_download
from demo_diffusion import path as path_module
from demo_diffusion.model import (
CLIPModel,
FLUXLoraLoader,
FluxTransformerModel,
T5Model,
VAEEncoderModel,
VAEModel,
get_clip_embedding_dim,
load,
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)
PREFERRED_KONTEXT_RESOLUTIONS = [
(672, 1568),
(688, 1504),
(720, 1456),
(752, 1392),
(800, 1328),
(832, 1248),
(880, 1184),
(944, 1104),
(1024, 1024),
(1104, 944),
(1184, 880),
(1248, 832),
(1328, 800),
(1392, 752),
(1456, 720),
(1504, 688),
(1568, 672),
]
class FluxKontextUtil:
"""
Utility class for Flux Kontext-related operations including context dimension calculations
and resolution handling.
"""
@staticmethod
def _get_context_dim(
image_height: int,
image_width: int,
compression_factor: int,
) -> int:
"""
Calculate the context dimension based on image dimensions and compression factor.
Args:
image_height: Height of the image
image_width: Width of the image
compression_factor: Compression factor applied to the image
Returns:
The calculated sequence length for the context
"""
seq_len = (image_height // (2 * compression_factor)) * (image_width // (2 * compression_factor))
return seq_len
@staticmethod
def get_context_latent_dim(
version: str,
kontext_resolution: tuple = None,
compression_factor: int = 8,
static_shape: bool = False,
):
"""
Get the context latent dimensions for Flux Kontext models.
Args:
version: Model version string
kontext_resolution: Tuple of (width, height) for kontext resolution
compression_factor: Compression factor for the model
static_shape: Whether to use static shapes
Returns:
Tuple of (min_context_latent_dim, context_latent_dim, max_context_latent_dim)
"""
min_context_latent_dim, context_latent_dim, max_context_latent_dim = 0, 0, 0
if version == "flux.1-kontext-dev":
assert kontext_resolution is not None, "kontext_resolution must be provided for flux.1-kontext-dev"
# get opt context size
context_latent_dim = FluxKontextUtil._get_context_dim(
image_height=kontext_resolution[1],
image_width=kontext_resolution[0],
compression_factor=compression_factor,
)
# get min context size
_, min_context_width, min_context_height = min((w * h, w, h) for w, h in PREFERRED_KONTEXT_RESOLUTIONS)
min_context_latent_dim = (
context_latent_dim
if static_shape
else FluxKontextUtil._get_context_dim(
image_height=min_context_height,
image_width=min_context_width,
compression_factor=compression_factor,
)
)
# get max context size
_, max_context_width, max_context_height = max((w * h, w, h) for w, h in PREFERRED_KONTEXT_RESOLUTIONS)
max_context_latent_dim = (
context_latent_dim
if static_shape
else FluxKontextUtil._get_context_dim(
image_height=max_context_height,
image_width=max_context_width,
compression_factor=compression_factor,
)
)
return min_context_latent_dim, context_latent_dim, max_context_latent_dim
@staticmethod
def get_preferred_resolutions():
"""
Get the list of preferred Kontext resolutions.
Returns:
List of (width, height) tuples representing preferred resolutions
"""
return PREFERRED_KONTEXT_RESOLUTIONS.copy()
@staticmethod
def get_min_max_kontext_dimensions():
"""
Get the resolution tuples with minimum and maximum aspect ratios from preferred Kontext resolutions.
Returns:
Tuple of ((min_aspect_width, min_aspect_height), (max_aspect_width, max_aspect_height))
"""
widths = [w for w, h in PREFERRED_KONTEXT_RESOLUTIONS]
heights = [h for w, h in PREFERRED_KONTEXT_RESOLUTIONS]
min_width = min(widths)
max_width = max(widths)
min_height = min(heights)
max_height = max(heights)
return (min_width, min_height), (max_width, max_height)
def calculate_shift(
image_seq_len,
base_seq_len: int = 256,
max_seq_len: int = 4096,
base_shift: float = 0.5,
max_shift: float = 1.16,
):
m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
b = base_shift - m * base_seq_len
mu = image_seq_len * m + b
return mu
class FluxPipeline(DiffusionPipeline):
"""
Application showcasing the acceleration of Flux pipelines using Nvidia TensorRT.
"""
def __init__(
self,
version="flux.1-dev",
pipeline_type=PIPELINE_TYPE.TXT2IMG,
guidance_scale=3.5,
max_sequence_length=512,
calibration_dataset=None,
t5_weight_streaming_budget_percentage=None,
transformer_weight_streaming_budget_percentage=None,
lora_scale: float = 1.0,
lora_weight: Optional[List[float]] = None,
lora_path: Optional[List[str]] = None,
**kwargs,
):
"""
Initializes the Flux pipeline.
Args:
version (`str`, defaults to `flux.1-dev`)
Version of the underlying Flux model.
guidance_scale (`float`, defaults to 3.5):
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.
max_sequence_length (`int`, defaults to 512):
Maximum sequence length to use with the `prompt`.
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 model.
"""
super().__init__(
version=version,
pipeline_type=pipeline_type,
text_encoder_weight_streaming_budget_percentage=t5_weight_streaming_budget_percentage,
denoiser_weight_streaming_budget_percentage=transformer_weight_streaming_budget_percentage,
**kwargs,
)
self.guidance_scale = guidance_scale
self.max_sequence_length = max_sequence_length
self.calibration_dataset = calibration_dataset # Currently supported for Flux ControlNet pipelines only
# Initialize LoRA
self.lora_loader = None
if lora_path:
self.lora_weights = dict()
self.lora_loader = FLUXLoraLoader(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]
@classmethod
def FromArgs(cls, args: argparse.Namespace, pipeline_type: PIPELINE_TYPE) -> FluxPipeline:
"""Factory method to construct a `FluxPipeline` object from parsed arguments.
Overrides:
DiffusionPipeline.FromArgs
"""
MAX_BATCH_SIZE = 4
DEVICE = "cuda"
DO_RETURN_LATENTS = False
# Resolve all paths.
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,
guidance_scale=args.guidance_scale,
max_sequence_length=args.max_sequence_length,
bf16=args.bf16,
calibration_dataset=args.calibration_dataset if hasattr(args, "calibration_dataset") else None,
low_vram=args.low_vram,
torch_fallback=args.torch_fallback,
weight_streaming=args.ws,
t5_weight_streaming_budget_percentage=args.t5_ws_percentage,
transformer_weight_streaming_budget_percentage=args.transformer_ws_percentage,
max_batch_size=MAX_BATCH_SIZE,
denoising_steps=args.denoising_steps,
scheduler=args.scheduler,
lora_scale=args.lora_scale,
lora_weight=args.lora_weight,
lora_path=args.lora_path,
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,
return_latents=DO_RETURN_LATENTS,
torch_inference=args.torch_inference,
)
@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
"""
if pipeline_type.is_img2img():
return ["clip", "t5", "transformer", "vae", "vae_encoder"]
else:
return ["clip", "t5", "transformer", "vae"]
def download_onnx_models(self, model_name: str, model_config: dict[str, Any]) -> None:
if self.fp16:
raise ValueError(
"ONNX models can be downloaded only for the following precisions: BF16, FP8, FP4. This pipeline is running in FP16."
)
hf_download_path = "-".join([load.get_path(self.version, self.pipeline_type.name), "onnx"])
model_path = model_config["onnx_opt_path"]
base_dir = os.path.dirname(os.path.dirname(model_config["onnx_opt_path"]))
if not os.path.exists(model_path):
if model_name == "clip":
dirname = "clip.opt"
elif model_name == "t5":
dirname = "t5.opt"
elif model_name == "transformer":
if model_config["use_fp4"]:
dirname = "transformer.opt/fp4"
if self.version == "flux.1-kontext-dev":
dirname = "_".join([dirname, "svd32"])
elif model_config["use_fp8"]:
dirname = "transformer.opt/fp8"
elif self.bf16:
dirname = "transformer.opt/bf16"
elif model_name == "vae":
dirname = "vae.opt"
elif model_name == "vae_encoder":
dirname = "vae_encoder.opt"
else:
raise ValueError(f"{model_name} not found in {self.stages}")
snapshot_download(
repo_id=hf_download_path,
allow_patterns=os.path.join(dirname, "*"),
local_dir=base_dir,
token=self.hf_token,
)
# Rename directory from <model_name>.opt to <model_name>
saved_dir = os.path.join(base_dir, dirname)
model_dir = os.path.dirname(model_path)
os.rename(saved_dir, model_dir)
# Rename model from model.onnx to model_optimized.onnx
os.rename(os.path.join(model_dir, "model.onnx"), model_path)
def is_native_export_supported(self, model_config: dict[str, Any]) -> bool:
if self.version.startswith("flux.1") and model_config["use_fp4"]:
# Native export not supported for FP4.
raise ValueError(
f"Native FP4 quantization is not supported. No ONNX model found in {model_config['onnx_opt_path']}. Please pass --download-onnx-models."
)
if (
self.version in ["flux.1-dev-canny", "flux.1-dev-depth"]
and model_config["use_fp8"]
and not self.calibration_dataset
):
# Native export of FP8 model requires calibration data.
raise ValueError(
f"No ONNX model found in {model_config['onnx_opt_path']}. Please pass --download-onnx-models. If you would like to quantize and export natively, please provide calibration data using --calibration-."
)
return True
def _initialize_models(self, framework_model_dir, int8, fp8, fp4):
# Load text tokenizer(s)
self.tokenizer = make_tokenizer(
self.version, self.pipeline_type, self.hf_token, framework_model_dir,
)
self.tokenizer2 = make_tokenizer(
self.version,
self.pipeline_type,
self.hf_token,
framework_model_dir,
subfolder="tokenizer_2",
tokenizer_type="t5",
)
# 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.bf16 = True if int8 or fp8 or fp4 else self.bf16
self.fp16 = True if not self.bf16 else False
self.tf32 = True
if "clip" in self.stages:
# BF16 CLIP ONNX export fails with ComplexDouble error in newer PyTorch; use FP16.
self.models["clip"] = CLIPModel(
**models_args,
fp16=True,
tf32=self.tf32,
bf16=False,
embedding_dim=get_clip_embedding_dim(self.version, self.pipeline_type),
keep_pooled_output=True,
subfolder="text_encoder",
)
if "t5" in self.stages:
# Known accuracy issues with FP16
self.models["t5"] = T5Model(
**models_args,
fp16=self.fp16,
tf32=self.tf32,
bf16=self.bf16,
subfolder="text_encoder_2",
text_maxlen=self.max_sequence_length,
weight_streaming=self.weight_streaming,
weight_streaming_budget_percentage=self.text_encoder_weight_streaming_budget_percentage,
)
if "vae" in self.stages:
# Accuracy issues with FP16
self.models["vae"] = VAEModel(**models_args, fp16=False, tf32=self.tf32, bf16=self.bf16)
self.vae_scale_factor = (
2 ** (len(self.models["vae"].config["block_out_channels"]))
if "vae" in self.stages and self.models["vae"] is not None
else 16
)
self.vae_latent_channels = (
self.models["vae"].config["latent_channels"]
if "vae" in self.stages and self.models["vae"] is not None
else 16
)
if "vae_encoder" in self.stages:
self.image_processor = VaeImageProcessor(
vae_scale_factor=self.vae_scale_factor * 2, vae_latent_channels=self.vae_latent_channels
)
# Add kontext_resolution if available (for FluxKontextPipeline)
vae_encoder_kwargs = {}
if hasattr(self, "kontext_image") and self.kontext_image is not None:
self.resize_height, self.resize_width = self._get_resize_dimensions(self.kontext_image)
vae_encoder_kwargs["kontext_resolution"] = (self.resize_width, self.resize_height)
self.models["vae_encoder"] = VAEEncoderModel(**models_args, fp16=False, tf32=self.tf32, bf16=self.bf16, **vae_encoder_kwargs)
if "transformer" in self.stages:
transformer_kwargs = {
**models_args,
"bf16": self.bf16,
"fp16": self.fp16,
"int8": int8,
"fp8": fp8,
"tf32": self.tf32,
"text_maxlen": self.max_sequence_length,
"weight_streaming": self.weight_streaming,
"weight_streaming_budget_percentage": self.denoiser_weight_streaming_budget_percentage,
}
if hasattr(self, "kontext_image") and self.kontext_image is not None:
transformer_kwargs["kontext_resolution"] = (self.resize_width, self.resize_height)
self.models["transformer"] = FluxTransformerModel(**transformer_kwargs)
def encode_image(self, input_image, encoder="vae_encoder"):
self.profile_start(encoder, color='red')
cast_to = torch.float16 if self.models[encoder].fp16 else torch.bfloat16 if self.models[encoder].bf16 else torch.float32
input_image = input_image.to(dtype=cast_to)
if self.torch_inference or self.torch_fallback[encoder]:
image_latents = self.torch_models[encoder](input_image)
else:
image_latents = self.run_engine(encoder, {'images': input_image})['latent']
image_latents = self.models[encoder].config["scaling_factor"] * (image_latents - self.models[encoder].config["shift_factor"])
self.profile_stop(encoder)
return image_latents
# Copied from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/flux/pipeline_flux_controlnet.py#L546
def prepare_image(
self,
image,
width,
height,
batch_size,
num_images_per_prompt,
device,
dtype,
do_classifier_free_guidance=False,
guess_mode=False,
):
if isinstance(image, torch.Tensor):
pass
else:
image = self.image_processor.preprocess(image, height=height, width=width)
image_batch_size = image.shape[0]
if image_batch_size == 1:
repeat_by = batch_size
else:
# image batch size is the same as prompt batch size
repeat_by = num_images_per_prompt
image = image.repeat_interleave(repeat_by, dim=0)
image = image.to(device=device, dtype=dtype)
if do_classifier_free_guidance and not guess_mode:
image = torch.cat([image] * 2)
return image
# Copied from https://github.com/huggingface/diffusers/blob/v0.30.1/src/diffusers/pipelines/flux/pipeline_flux.py#L436
@staticmethod
def _pack_latents(latents, batch_size, num_channels_latents, height, width):
"""
Reshapes latents from (B, C, H, W) to (B, H/2, W/2, C*4) as expected by the denoiser
"""
latents = latents.view(
batch_size, num_channels_latents, height // 2, 2, width // 2, 2
)
latents = latents.permute(0, 2, 4, 1, 3, 5)
latents = latents.reshape(
batch_size, (height // 2) * (width // 2), num_channels_latents * 4
)
return latents
# Copied from https://github.com/huggingface/diffusers/blob/v0.30.1/src/diffusers/pipelines/flux/pipeline_flux.py#L444
@staticmethod
def _unpack_latents(latents, height, width, vae_scale_factor):
"""
Reshapes denoised latents to the format (B, C, H, W)
"""
batch_size, num_patches, channels = latents.shape
height = height // vae_scale_factor
width = width // vae_scale_factor
latents = latents.view(batch_size, height, width, channels // 4, 2, 2)
latents = latents.permute(0, 3, 1, 4, 2, 5)
latents = latents.reshape(
batch_size, channels // (2 * 2), height * 2, width * 2
)
return latents
# Copied from https://github.com/huggingface/diffusers/blob/v0.30.1/src/diffusers/pipelines/flux/pipeline_flux.py#L421
@staticmethod
def _prepare_latent_image_ids(height, width, dtype, device):
"""
Prepares latent image indices
"""
latent_image_ids = torch.zeros(height // 2, width // 2, 3)
latent_image_ids[..., 1] = (
latent_image_ids[..., 1] + torch.arange(height // 2)[:, None]
)
latent_image_ids[..., 2] = (
latent_image_ids[..., 2] + torch.arange(width // 2)[None, :]
)
latent_image_id_height, latent_image_id_width, latent_image_id_channels = (
latent_image_ids.shape
)
latent_image_ids = latent_image_ids.reshape(
latent_image_id_height * latent_image_id_width, latent_image_id_channels
)
return latent_image_ids.to(device=device, dtype=dtype)
def initialize_latents(
self,
batch_size,
num_channels_latents,
latent_height,
latent_width,
latent_timestep=None,
image_latents=None,
latents_dtype=torch.float32,
):
latents_dtype = latents_dtype # text_embeddings.dtype
latents_shape = (batch_size, num_channels_latents, latent_height, latent_width)
latents = torch.randn(
latents_shape,
device=self.device,
dtype=latents_dtype,
generator=self.generator,
)
if image_latents is not None:
image_latents = torch.cat([image_latents], dim=0).to(latents_dtype)
latents = self.scheduler.scale_noise(image_latents, latent_timestep, latents)
latents = self._pack_latents(
latents, batch_size, num_channels_latents, latent_height, latent_width
)
latent_image_ids = self._prepare_latent_image_ids(latent_height, latent_width, latents_dtype, self.device)
return latents, latent_image_ids
# Copied from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/flux/pipeline_flux_img2img.py#L416C1
def get_timesteps(self, num_inference_steps, strength):
# get the original timestep using init_timestep
init_timestep = min(num_inference_steps * strength, num_inference_steps)
t_start = int(max(num_inference_steps - init_timestep, 0))
timesteps = self.scheduler.timesteps[t_start * self.scheduler.order :]
if hasattr(self.scheduler, "set_begin_index"):
self.scheduler.set_begin_index(t_start * self.scheduler.order)
return timesteps, num_inference_steps - t_start
def encode_prompt(
self, prompt, encoder="clip", max_sequence_length=None, pooled_output=False
):
self.profile_start(encoder, color="green")
tokenizer = self.tokenizer2 if encoder == "t5" else self.tokenizer
max_sequence_length = (
tokenizer.model_max_length
if max_sequence_length is None
else max_sequence_length
)
def tokenize(prompt, max_sequence_length):
text_input_ids = (
tokenizer(
prompt,
padding="max_length",
max_length=max_sequence_length,
truncation=True,
return_overflowing_tokens=False,
return_length=False,
return_tensors="pt",
)
.input_ids.type(torch.int32)
.to(self.device)
)
untruncated_ids = tokenizer(
prompt, padding="longest", return_tensors="pt"
).input_ids.type(torch.int32).to(self.device)
if untruncated_ids.shape[-1] >= text_input_ids.shape[
-1
] and not torch.equal(text_input_ids, untruncated_ids):
removed_text = tokenizer.batch_decode(
untruncated_ids[:, max_sequence_length - 1 : -1]
)
warnings.warn(
"The following part of your input was truncated because `max_sequence_length` is set to "
f"{max_sequence_length} tokens: {removed_text}"
)
if self.torch_inference or self.torch_fallback[encoder]:
outputs = self.torch_models[encoder](
text_input_ids, output_hidden_states=False
)
text_encoder_output = (
outputs[0].clone()
if pooled_output == False
else outputs.pooler_output.clone()
)
else:
# NOTE: output tensor for the encoder must be cloned because it will be overwritten when called again for prompt2
outputs = self.run_engine(encoder, {"input_ids": text_input_ids})
output_name = (
"text_embeddings" if not pooled_output else "pooled_embeddings"
)
text_encoder_output = outputs[output_name].clone()
return text_encoder_output
# Tokenize prompt
text_encoder_output = tokenize(prompt, max_sequence_length)
self.profile_stop(encoder)
return text_encoder_output.to(torch.float16) if self.fp16 else text_encoder_output.to(torch.bfloat16) if self.bf16 else text_encoder_output
def denoise_latent(
self,
latents,
timesteps,
text_embeddings,
pooled_embeddings,
text_ids,
latent_image_ids,
denoiser="transformer",
guidance=None,
control_latent=None,
):
do_autocast = self.torch_inference != "" and self.models[denoiser].fp16
with torch.autocast("cuda", enabled=do_autocast):
self.profile_start(denoiser, color="blue")
# handle guidance
if self.models[denoiser].config["guidance_embeds"] and guidance is None:
guidance = torch.full(
[1], self.guidance_scale, device=self.device, dtype=torch.float32
)
guidance = guidance.expand(latents.shape[0])
for step_index, timestep in enumerate(timesteps):
# Prepare latents
latents_input = latents if control_latent is None else torch.cat((latents, control_latent), dim=-1)
# prepare inputs
timestep_inp = timestep.expand(latents.shape[0]).to(latents_input.dtype)
params = {
"hidden_states": latents_input,
"timestep": timestep_inp / 1000,
"pooled_projections": pooled_embeddings,
"encoder_hidden_states": text_embeddings,
"txt_ids": text_ids.float(),
"img_ids": latent_image_ids.float(),
}
if guidance is not None:
params.update({"guidance": guidance})
# Predict the noise residual
if self.torch_inference or self.torch_fallback[denoiser]:
noise_pred = self.torch_models[denoiser](**params)["sample"]
else:
noise_pred = self.run_engine(denoiser, params)["latent"]
latents = self.scheduler.step(
noise_pred, timestep, latents, return_dict=False
)[0]
self.profile_stop(denoiser)
return latents.to(dtype=torch.bfloat16) if self.bf16 else latents.to(dtype=torch.float32)
def decode_latent(self, latents, decoder="vae"):
self.profile_start(decoder, color="red")
cast_to = torch.float16 if self.models[decoder].fp16 else torch.bfloat16 if self.models[decoder].bf16 else torch.float32
latents = latents.to(dtype=cast_to)
if self.torch_inference or self.torch_fallback[decoder]:
images = self.torch_models[decoder](latents, return_dict=False)[0]
else:
images = self.run_engine(decoder, {"latent": latents})["images"]
self.profile_stop(decoder)
return images
def print_summary(self, denoising_steps, walltime_ms, batch_size):
print("|-----------------|--------------|")
print("| {:^15} | {:^12} |".format("Module", "Latency"))
print("|-----------------|--------------|")
print(
"| {:^15} | {:>9.2f} ms |".format(
"CLIP",
cudart.cudaEventElapsedTime(
self.events["clip"][0], self.events["clip"][1]
)[1],
)
)
print(
"| {:^15} | {:>9.2f} ms |".format(
"T5",
cudart.cudaEventElapsedTime(self.events["t5"][0], self.events["t5"][1])[
1
],
)
)
if "vae_encoder" in self.stages:
print(
"| {:^15} | {:>9.2f} ms |".format(
"VAE-Enc",
cudart.cudaEventElapsedTime(
self.events["vae_encoder"][0], self.events["vae_encoder"][1]
)[1],
)
)
print(
"| {:^15} | {:>9.2f} ms |".format(
"Transformer x " + str(denoising_steps),
cudart.cudaEventElapsedTime(
self.events["transformer"][0], self.events["transformer"][1]
)[1],
)
)
print(
"| {:^15} | {:>9.2f} ms |".format(
"VAE-Dec",
cudart.cudaEventElapsedTime(
self.events["vae"][0], self.events["vae"][1]
)[1],
)
)
print("|-----------------|--------------|")
print("| {:^15} | {:>9.2f} ms |".format("Pipeline", walltime_ms))
print("|-----------------|--------------|")
print("Throughput: {:.5f} image/s".format(batch_size * 1000.0 / walltime_ms))
def _check_integrity(self, images):
integrity_checker = PixtralContentFilter(self.device)
for image in images:
image_ = np.array(image) / 255.0
image_ = 2 * image_ - 1
image_ = torch.from_numpy(image_).to(self.device, dtype=torch.float32).permute(0, 3, 1, 2)
if integrity_checker.test_image(image_):
raise ValueError("Your image has been flagged. Choose another prompt/image or try again.")
def save_images(self, prompt, images, check_integrity=False):
if check_integrity:
self._check_integrity(images)
for image in images:
self.save_image(image, self.pipeline_type.name.lower(), prompt, self.seed)
def infer(
self,
prompt,
prompt2,
image_height,
image_width,
input_image=None,
image_strength=1.0,
control_image=None,
warmup=False,
save_image=True,
):
"""
Run the diffusion pipeline.
Args:
prompt (str):
The text prompt to guide image generation.
prompt2 (str):
The prompt to be sent to the T5 tokenizer and text encoder
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 (PIL.Image.Image):
`Image` representing an image batch to be used as the starting point.
image_strength (`float`, *optional*, defaults to 1.0):
Indicates extent to transform the reference `image`. Must be between 0 and 1. `image` is used as a
starting point and more noise is added the higher the `strength`. The number of denoising steps depends
on the amount of noise initially added. When `strength` is 1, added noise is maximum and the denoising
process runs for the full number of iterations specified in `num_inference_steps`. A value of 1
essentially ignores `image`.
control_image (PIL.Image.Image):
The ControlNet input condition to provide guidance to the `transformer` for generation.
warmup (bool):
Indicate if this is a warmup run.
save_image (bool):
Save the generated image (if applicable)
"""
assert len(prompt) == len(prompt2)
batch_size = len(prompt)
# Spatial dimensions of latent tensor
latent_height = 2 * (int(image_height) // self.vae_scale_factor)
latent_width = 2 * (int(image_width) // self.vae_scale_factor)
num_inference_steps = self.denoising_steps
latent_kwargs = {}
with torch.inference_mode(), trt.Runtime(TRT_LOGGER):
torch.cuda.synchronize()
e2e_tic = time.perf_counter()
num_channels_latents = self.models["transformer"].config["in_channels"] // 4
if control_image:
num_channels_latents = self.models["transformer"].config["in_channels"] // 8
# Prepare control latents
control_image = self.prepare_image(
image=control_image,
width=image_width,
height=image_height,
batch_size=batch_size,
num_images_per_prompt=1,
device=self.device,
dtype=torch.float16 if self.models["vae"].fp16 else torch.bfloat16 if self.models["vae"].bf16 else torch.float32,
)
if control_image.ndim == 4:
with self.model_memory_manager(["vae_encoder"], low_vram=self.low_vram):
control_image = self.encode_image(control_image)
height_control_image, width_control_image = control_image.shape[2:]
control_image = self._pack_latents(
control_image,
batch_size,
num_channels_latents,
height_control_image,
width_control_image,
)
# CLIP and T5 text encoder(s)
with self.model_memory_manager(["clip", "t5"], low_vram=self.low_vram):
pooled_embeddings = self.encode_prompt(prompt, pooled_output=True)
text_embeddings = self.encode_prompt(
prompt2, encoder="t5", max_sequence_length=self.max_sequence_length
)
text_ids = torch.zeros(text_embeddings.shape[1], 3).to(
device=self.device, dtype=text_embeddings.dtype
)
# Prepare timesteps
sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps)
image_seq_len = (latent_height // 2) * (latent_width // 2)
mu = calculate_shift(
image_seq_len,
self.scheduler.config.base_image_seq_len,
self.scheduler.config.max_image_seq_len,
self.scheduler.config.base_shift,
self.scheduler.config.max_shift,
)
timesteps = None
# TODO: support custom timesteps
if timesteps is not None:
if (
"timesteps"
not in inspect.signature(self.scheduler.set_timesteps).parameters
):
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(sigmas=sigmas, mu=mu, device=self.device)
timesteps = self.scheduler.timesteps.to(self.device)
num_inference_steps = len(timesteps)
# Pre-process input image and timestep for the img2img pipeline
if input_image:
input_image = self.image_processor.preprocess(input_image, height=image_height, width=image_width).to(
self.device
)
with self.model_memory_manager(["vae_encoder"], low_vram=self.low_vram):
image_latents = self.encode_image(input_image)
timesteps, num_inference_steps = self.get_timesteps(self.denoising_steps, image_strength)
if num_inference_steps < 1:
raise ValueError(
f"After adjusting the num_inference_steps by strength parameter: {image_strength}, the number of pipeline"
f"steps is {num_inference_steps} which is < 1 and not appropriate for this pipeline."
)
latent_timestep = timesteps[:1].repeat(batch_size)
latent_kwargs.update({"image_latents": image_latents, "latent_timestep": latent_timestep})
# Initialize latents
latents, latent_image_ids = self.initialize_latents(
batch_size=batch_size,
num_channels_latents=num_channels_latents,
latent_height=latent_height,
latent_width=latent_width,
latents_dtype=torch.float16 if self.fp16 else torch.bfloat16 if self.bf16 else torch.float32,
**latent_kwargs,
)
# DiT denoiser
with self.model_memory_manager(["transformer"], low_vram=self.low_vram):
latents = self.denoise_latent(
latents,
timesteps,
text_embeddings,
pooled_embeddings,
text_ids,
latent_image_ids,
control_latent=control_image,
)
# VAE decode latent
with self.model_memory_manager(["vae"], low_vram=self.low_vram):
latents = self._unpack_latents(
latents, image_height, image_width, self.vae_scale_factor
)
latents = (
latents / self.models["vae"].config["scaling_factor"]
) + self.models["vae"].config["shift_factor"]
images = self.decode_latent(latents)
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)
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()
)
return (latents, walltime_ms) if self.return_latents else (images, walltime_ms)
def run(
self,
prompt,
prompt2,
height,
width,
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, prompt2, height, width, warmup=True, **kwargs)
images = []
for _ in range(batch_count):
print("[I] Running Flux pipeline")
if self.nvtx_profile:
cudart.cudaProfilerStart()
image, _ = self.infer(prompt, prompt2, height, width, warmup=False, **kwargs)
images.append(image)
if self.nvtx_profile:
cudart.cudaProfilerStop()
return images
class FluxKontextPipeline(FluxPipeline):
"""
Application showcasing the acceleration of Flux Kontext pipelines using Nvidia TensorRT.
This pipeline handles the specific logic for the Kontext model variant.
"""
def __init__(
self,
kontext_image,
version="flux.1-kontext-dev",
pipeline_type=PIPELINE_TYPE.IMG2IMG,
guidance_scale=3.5,
max_sequence_length=512,
**kwargs,
):
"""
Initializes the Flux Kontext pipeline.
Args:
kontext_image (`PIL.Image.Image`):
Kontext Image to be edited.
version (`str`, defaults to `flux.1-kontext-dev`)
Version of the underlying Flux Kontext model.
guidance_scale (`float`, defaults to 3.5):
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.
max_sequence_length (`int`, defaults to 512):
Maximum sequence length to use with the `prompt`.
"""
super().__init__(
version=version,
pipeline_type=pipeline_type,
guidance_scale=guidance_scale,
max_sequence_length=max_sequence_length,
**kwargs,
)
self.kontext_image = kontext_image
# WAR to avoid RuntimeError: ScalarType ComplexDouble is an unexpected tensor scalar type during CLIP export
self.config["clip_torch_fallback"] = True
@classmethod
def FromArgs(cls, args: argparse.Namespace, pipeline_type: PIPELINE_TYPE) -> FluxKontextPipeline:
"""Factory method to construct a `FluxKontextPipeline` object from parsed arguments.
Overrides:
FluxPipeline.FromArgs
"""
MAX_BATCH_SIZE = 4
DEVICE = "cuda"
DO_RETURN_LATENTS = False
# Resolve all paths.
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,
guidance_scale=args.guidance_scale,
max_sequence_length=args.max_sequence_length,
bf16=args.bf16,
low_vram=args.low_vram,
torch_fallback=args.torch_fallback,
weight_streaming=args.ws,
t5_weight_streaming_budget_percentage=args.t5_ws_percentage,
transformer_weight_streaming_budget_percentage=args.transformer_ws_percentage,
max_batch_size=MAX_BATCH_SIZE,
denoising_steps=args.denoising_steps,
scheduler=args.scheduler,
lora_scale=args.lora_scale,
lora_weight=args.lora_weight,
lora_path=args.lora_path,
kontext_image=args.kontext_image if hasattr(args, "kontext_image") else None,
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,
return_latents=DO_RETURN_LATENTS,
torch_inference=args.torch_inference,
)
def initialize_latents(
self,
batch_size,
num_channels_latents,
latent_height,
latent_width,
latent_timestep=None,
image_latents=None,
latents_dtype=torch.float32,
):
"""
Initialize latents for Kontext pipeline.
Overrides FluxPipeline.initialize_latents to handle Kontext-specific logic.
"""
latents_dtype = latents_dtype # text_embeddings.dtype
latents_shape = (batch_size, num_channels_latents, latent_height, latent_width)
latents = torch.randn(
latents_shape,
device=self.device,
dtype=latents_dtype,
generator=self.generator,
)
image_ids = None
if image_latents is not None:
image_latents = torch.cat([image_latents], dim=0).to(latents_dtype)
image_latent_height, image_latent_width = image_latents.shape[2:]
image_latents = self._pack_latents(
image_latents, batch_size, num_channels_latents, image_latent_height, image_latent_width
)
image_ids = self._prepare_latent_image_ids(
image_latent_height, image_latent_width, latents_dtype, self.device
)
# image ids are the same as latent ids with the first dimension set to 1 instead of 0
image_ids[..., 0] = 1
latents = self._pack_latents(latents, batch_size, num_channels_latents, latent_height, latent_width)
latent_ids = self._prepare_latent_image_ids(latent_height, latent_width, latents_dtype, self.device)
latent_image_ids = torch.cat([latent_ids, image_ids], dim=0) if image_ids is not None else latent_ids
return latents, image_latents, latent_image_ids
def denoise_latent(
self,
latents,
timesteps,
text_embeddings,
pooled_embeddings,
text_ids,
latent_image_ids,
image_latents,
denoiser="transformer",
guidance=None,
):
"""
Denoise latents for Kontext pipeline.
Overrides FluxPipeline.denoise_latent to handle Kontext-specific logic.
"""
do_autocast = self.torch_inference != "" and self.models[denoiser].fp16
with torch.autocast("cuda", enabled=do_autocast):
self.profile_start(denoiser, color="blue")
# handle guidance
if self.models[denoiser].config["guidance_embeds"] and guidance is None:
guidance = torch.full([1], self.guidance_scale, device=self.device, dtype=torch.float32)
guidance = guidance.expand(latents.shape[0])
for step_index, timestep in enumerate(timesteps):
# Kontext-specific: concatenate image_latents along dim=1
latents_input = torch.cat([latents, image_latents], dim=1)
# prepare inputs
timestep_inp = timestep.expand(latents.shape[0]).to(latents_input.dtype)
params = {
"hidden_states": latents_input,
"timestep": timestep_inp / 1000,
"pooled_projections": pooled_embeddings,
"encoder_hidden_states": text_embeddings,
"txt_ids": text_ids.float(),
"img_ids": latent_image_ids.float(),
}
if guidance is not None:
params.update({"guidance": guidance})
# Predict the noise residual
if self.torch_inference or self.torch_fallback[denoiser]:
noise_pred = self.torch_models[denoiser](**params)["sample"]
else:
noise_pred = self.run_engine(denoiser, params)["latent"]
# Kontext-specific: extract only the latent part of the prediction
noise_pred = noise_pred[:, : latents.size(1)]
latents = self.scheduler.step(noise_pred, timestep, latents, return_dict=False)[0]
self.profile_stop(denoiser)
return latents.to(dtype=torch.bfloat16) if self.bf16 else latents.to(dtype=torch.float32)
def _get_resize_dimensions(self, input_image):
"""
Preprocess input image for Kontext pipeline using preferred resolutions.
Uses FluxKontextUtil to get the standardized list of preferred resolutions.
"""
multiple_of = self.vae_scale_factor * 2
resize_height, resize_width = self.image_processor.get_default_height_width(input_image)
aspect_ratio = resize_width / resize_height
# Kontext is trained on specific resolutions, using one of them is recommended
preferred_resolutions = FluxKontextUtil.get_preferred_resolutions()
_, resize_width, resize_height = min(
(abs(aspect_ratio - w / h), w, h) for w, h in preferred_resolutions
)
resize_width = resize_width // multiple_of * multiple_of
resize_height = resize_height // multiple_of * multiple_of
return resize_height, resize_width
def infer(
self,
prompt,
prompt2,
image_height,
image_width,
image_strength=1.0,
warmup=False,
save_image=True,
):
"""
Run the Kontext diffusion pipeline.
Args:
prompt (str):
The text prompt to guide image generation.
prompt2 (str):
The prompt to be sent to the T5 tokenizer and text encoder
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_strength (`float`, *optional*, defaults to 1.0):
Indicates extent to transform the reference `image`. Must be between 0 and 1. `image` is used as a
starting point and more noise is added the higher the `strength`. The number of denoising steps depends
on the amount of noise initially added. When `strength` is 1, added noise is maximum and the denoising
process runs for the full number of iterations specified in `num_inference_steps`. A value of 1
essentially ignores `image`.
warmup (bool):
Indicate if this is a warmup run.
save_image (bool):
Save the generated image (if applicable)
"""
assert len(prompt) == len(prompt2)
batch_size = len(prompt)
# Spatial dimensions of latent tensor
latent_height = 2 * (int(image_height) // self.vae_scale_factor)
latent_width = 2 * (int(image_width) // self.vae_scale_factor)
num_inference_steps = self.denoising_steps
latent_kwargs = {}
with torch.inference_mode(), trt.Runtime(TRT_LOGGER):
torch.cuda.synchronize()
e2e_tic = time.perf_counter()
num_channels_latents = self.models["transformer"].config["in_channels"] // 4
# CLIP and T5 text encoder(s)
with self.model_memory_manager(["clip", "t5"], low_vram=self.low_vram):
pooled_embeddings = self.encode_prompt(prompt, pooled_output=True)
text_embeddings = self.encode_prompt(
prompt2, encoder="t5", max_sequence_length=self.max_sequence_length
)
text_ids = torch.zeros(text_embeddings.shape[1], 3).to(device=self.device, dtype=text_embeddings.dtype)
# Prepare timesteps
sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps)
image_seq_len = (latent_height // 2) * (latent_width // 2)
mu = calculate_shift(
image_seq_len,
self.scheduler.config.base_image_seq_len,
self.scheduler.config.max_image_seq_len,
self.scheduler.config.base_shift,
self.scheduler.config.max_shift,
)
timesteps = None
# TODO: support custom timesteps
if timesteps is not None:
if "timesteps" not in inspect.signature(self.scheduler.set_timesteps).parameters:
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(sigmas=sigmas, mu=mu, device=self.device)
timesteps = self.scheduler.timesteps.to(self.device)
num_inference_steps = len(timesteps)
# Pre-process kontext image and timestep for the img2img pipeline
if self.kontext_image:
# Kontext-specific image preprocessing
kontext_image = self.image_processor.resize(self.kontext_image, self.resize_height, self.resize_width)
kontext_image = self.image_processor.preprocess(
kontext_image, height=self.resize_height, width=self.resize_width
).to(self.device)
with self.model_memory_manager(["vae_encoder"], low_vram=self.low_vram):
image_latents = self.encode_image(kontext_image)
timesteps, num_inference_steps = self.get_timesteps(self.denoising_steps, image_strength)
if num_inference_steps < 1:
raise ValueError(
f"After adjusting the num_inference_steps by strength parameter: {image_strength}, the number of pipeline"
f"steps is {num_inference_steps} which is < 1 and not appropriate for this pipeline."
)
latent_timestep = timesteps[:1].repeat(batch_size)
latent_kwargs.update({"image_latents": image_latents, "latent_timestep": latent_timestep})
# Initialize latents
latents, image_latents, latent_ids = self.initialize_latents(
batch_size=batch_size,
num_channels_latents=num_channels_latents,
latent_height=latent_height,
latent_width=latent_width,
latents_dtype=torch.float16 if self.fp16 else torch.bfloat16 if self.bf16 else torch.float32,
**latent_kwargs,
)
# DiT denoiser
with self.model_memory_manager(["transformer"], low_vram=self.low_vram):
latents = self.denoise_latent(
latents,
timesteps,
text_embeddings,
pooled_embeddings,
text_ids,
latent_ids,
image_latents,
)
# VAE decode latent
with self.model_memory_manager(["vae"], low_vram=self.low_vram):
latents = self._unpack_latents(latents, image_height, image_width, self.vae_scale_factor)
latents = (latents / self.models["vae"].config["scaling_factor"]) + self.models["vae"].config[
"shift_factor"
]
images = self.decode_latent(latents)
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)
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()
)
return (latents, walltime_ms) if self.return_latents else (images, walltime_ms)