# # 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 .opt to 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)