# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project # This implementation is heavily inspired by the diffusers project. # Original implementation: https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/flux/pipeline_flux_kontext.py from __future__ import annotations import json import os from collections.abc import Callable, Iterable from typing import Any, ClassVar, cast import numpy as np import PIL.Image import torch import torch.nn as nn from diffusers.image_processor import VaeImageProcessor from diffusers.loaders import TextualInversionLoaderMixin from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL from diffusers.schedulers import FlowMatchEulerDiscreteScheduler from diffusers.utils.torch_utils import randn_tensor from transformers import CLIPTextModel, CLIPTokenizer, T5EncoderModel, T5TokenizerFast from vllm.model_executor.models.utils import AutoWeightsLoader from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin from vllm_omni.diffusion.distributed.utils import get_local_device from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader from vllm_omni.diffusion.models.flux import ( FluxKontextTransformer2DModel, ) from vllm_omni.diffusion.models.flux.flux_pipeline_mixin import FluxPipelineMixin from vllm_omni.diffusion.models.interface import SupportImageInput, SupportsComponentDiscovery from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin from vllm_omni.diffusion.utils.tf_utils import get_transformer_config_kwargs from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch from vllm_omni.logger import init_logger logger = init_logger(__name__) def get_flux_kontext_post_process_func(od_config: OmniDiffusionConfig) -> Callable: """Get postprocessing function for FLUX.1-Kontext pipeline.""" model_name = od_config.model if os.path.exists(model_name): model_path = model_name else: from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific model_path = download_weights_from_hf_specific(model_name, None, ["*"]) vae_config_path = os.path.join(model_path, "vae/config.json") if not os.path.exists(vae_config_path): raise FileNotFoundError( f"VAE config not found at {vae_config_path}. " "Please ensure the model path contains a valid VAE configuration." ) with open(vae_config_path) as f: vae_config = json.load(f) vae_scale_factor = 2 ** (len(vae_config["block_out_channels"]) - 1) if "block_out_channels" in vae_config else 8 image_processor = VaeImageProcessor(vae_scale_factor=vae_scale_factor * 2) def post_process_func(images: torch.Tensor) -> list[PIL.Image.Image]: result = image_processor.postprocess(images) return result return post_process_func class FluxKontextPipeline( nn.Module, FluxPipelineMixin, CFGParallelMixin, SupportImageInput, ProgressBarMixin, DiffusionPipelineProfilerMixin, SupportsComponentDiscovery, ): """FLUX.1-Kontext pipeline for image editing with text guidance.""" _dit_modules: ClassVar[list[str]] = ["transformer"] _encoder_modules: ClassVar[list[str]] = ["text_encoder", "text_encoder_2"] _vae_modules: ClassVar[list[str]] = ["vae"] support_image_input = True def __init__( self, *, od_config: OmniDiffusionConfig, prefix: str = "", ): super().__init__() self.od_config = od_config self.weights_sources = [ DiffusersPipelineLoader.ComponentSource( model_or_path=od_config.model, subfolder="transformer", revision=None, prefix="transformer.", fall_back_to_pt=True, ) ] self._execution_device = get_local_device() model = od_config.model local_files_only = os.path.exists(model) self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained( model, subfolder="scheduler", local_files_only=local_files_only, ) self.text_encoder = CLIPTextModel.from_pretrained( model, subfolder="text_encoder", local_files_only=local_files_only, ).to(self._execution_device) self.text_encoder_2 = T5EncoderModel.from_pretrained( model, subfolder="text_encoder_2", local_files_only=local_files_only, ).to(self._execution_device) self.tokenizer = CLIPTokenizer.from_pretrained( model, subfolder="tokenizer", local_files_only=local_files_only, ) self.tokenizer_2 = T5TokenizerFast.from_pretrained( model, subfolder="tokenizer_2", local_files_only=local_files_only, ) self.vae = AutoencoderKL.from_pretrained( model, subfolder="vae", local_files_only=local_files_only, ).to(self._execution_device) transformer_kwargs = get_transformer_config_kwargs(od_config.tf_model_config, FluxKontextTransformer2DModel) transformer_kwargs["od_config"] = od_config transformer_kwargs["quant_config"] = od_config.quantization_config self.transformer = FluxKontextTransformer2DModel(**transformer_kwargs) self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) if getattr(self, "vae", None) else 8 self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor * 2) self.tokenizer_max_length = ( self.tokenizer.model_max_length if hasattr(self, "tokenizer") and self.tokenizer is not None else 77 ) self.default_sample_size = 128 self._guidance_scale = None self._attention_kwargs = None self._joint_attention_kwargs = None self._num_timesteps = None self._current_timestep = None self._interrupt = False self._callback_tensor_inputs = ["latents", "prompt_embeds"] self.latent_channels = self.vae.config.latent_channels if hasattr(self.vae, "config") else 16 self.setup_diffusion_pipeline_profiler( enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler ) def _get_t5_prompt_embeds( self, prompt: str | list[str] = None, num_images_per_prompt: int = 1, max_sequence_length: int = 512, device: torch.device | None = None, dtype: torch.dtype | None = None, ): device = device or self._execution_device dtype = dtype or self.text_encoder.dtype prompt = [prompt] if isinstance(prompt, str) else prompt batch_size = len(prompt) if isinstance(self, TextualInversionLoaderMixin): prompt = self.maybe_convert_prompt(prompt, self.tokenizer_2) text_inputs = self.tokenizer_2( prompt, padding="max_length", max_length=max_sequence_length, truncation=True, return_length=False, return_overflowing_tokens=False, return_tensors="pt", ) text_input_ids = text_inputs.input_ids untruncated_ids = self.tokenizer_2(prompt, padding="longest", return_tensors="pt").input_ids if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids): removed_text = self.tokenizer_2.batch_decode(untruncated_ids[:, self.tokenizer_max_length - 1 : -1]) logger.warning( "The following part of your input was truncated because `max_sequence_length` is set to " f" {max_sequence_length} tokens: {removed_text}" ) prompt_embeds = self.text_encoder_2(text_input_ids.to(device), output_hidden_states=False)[0] dtype = self.text_encoder_2.dtype prompt_embeds = prompt_embeds.to(dtype=dtype, device=device) _, seq_len, _ = prompt_embeds.shape prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) prompt_embeds = prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) return prompt_embeds def _get_clip_prompt_embeds( self, prompt: str | list[str], num_images_per_prompt: int = 1, device: torch.device | None = None, ): device = device or self._execution_device prompt = [prompt] if isinstance(prompt, str) else prompt batch_size = len(prompt) if isinstance(self, TextualInversionLoaderMixin): prompt = self.maybe_convert_prompt(prompt, self.tokenizer) text_inputs = self.tokenizer( prompt, padding="max_length", max_length=self.tokenizer_max_length, truncation=True, return_overflowing_tokens=False, return_length=False, return_tensors="pt", ) text_input_ids = text_inputs.input_ids untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids): removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer_max_length - 1 : -1]) logger.warning( "The following part of your input was truncated because CLIP can only handle sequences up to" f" {self.tokenizer_max_length} tokens: {removed_text}" ) prompt_embeds = self.text_encoder(text_input_ids.to(device), output_hidden_states=False) prompt_embeds = prompt_embeds.pooler_output prompt_embeds = prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt) prompt_embeds = prompt_embeds.view(batch_size * num_images_per_prompt, -1) return prompt_embeds def encode_prompt( self, prompt: str | list[str], prompt_2: str | list[str] | None = None, device: torch.device | None = None, num_images_per_prompt: int = 1, prompt_embeds: torch.FloatTensor | None = None, pooled_prompt_embeds: torch.FloatTensor | None = None, max_sequence_length: int = 512, ): device = device or self._execution_device prompt = [prompt] if isinstance(prompt, str) else prompt if prompt_embeds is None: prompt_2 = prompt_2 or prompt prompt_2 = [prompt_2] if isinstance(prompt_2, str) else prompt_2 # We only use the pooled prompt output from the CLIPTextModel pooled_prompt_embeds = self._get_clip_prompt_embeds( prompt=prompt, device=device, num_images_per_prompt=num_images_per_prompt, ) prompt_embeds = self._get_t5_prompt_embeds( prompt=prompt_2, num_images_per_prompt=num_images_per_prompt, max_sequence_length=max_sequence_length, device=device, ) dtype = self.text_encoder.dtype if self.text_encoder is not None else self.transformer.dtype text_ids = torch.zeros(prompt_embeds.shape[1], 3).to(device=device, dtype=dtype) return prompt_embeds, pooled_prompt_embeds, text_ids def check_inputs( self, prompt, prompt_2=None, height=None, width=None, negative_prompt=None, negative_prompt_2=None, prompt_embeds=None, pooled_prompt_embeds=None, negative_prompt_embeds=None, negative_pooled_prompt_embeds=None, callback_on_step_end_tensor_inputs=None, max_sequence_length=None, ): if height is not None and height % (self.vae_scale_factor * 2) != 0: logger.warning( f"`height` and `width` have to be divisible by " f"{self.vae_scale_factor * 2} but are {height} and {width}. " f"Dimensions will be resized accordingly" ) if callback_on_step_end_tensor_inputs is not None and not all( k in self._callback_tensor_inputs for k in callback_on_step_end_tensor_inputs ): invalid_keys = [k for k in callback_on_step_end_tensor_inputs if k not in self._callback_tensor_inputs] raise ValueError( f"`callback_on_step_end_tensor_inputs` has to be in " f"{self._callback_tensor_inputs}, but found {invalid_keys}" ) if prompt is not None and prompt_embeds is not None: raise ValueError( f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" " only forward one of the two." ) elif prompt_2 is not None and prompt_embeds is not None: raise ValueError( f"Cannot forward both `prompt_2`: {prompt_2} and `prompt_embeds`: {prompt_embeds}. Please make sure to" " only forward one of the two." ) elif prompt is None and prompt_embeds is None: raise ValueError( "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." ) elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") elif prompt_2 is not None and (not isinstance(prompt_2, str) and not isinstance(prompt_2, list)): raise ValueError(f"`prompt_2` has to be of type `str` or `list` but is {type(prompt_2)}") if negative_prompt is not None and negative_prompt_embeds is not None: raise ValueError( f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" f" {negative_prompt_embeds}. Please make sure to only forward one of the two." ) elif negative_prompt_2 is not None and negative_prompt_embeds is not None: raise ValueError( f"Cannot forward both `negative_prompt_2`: {negative_prompt_2} and `negative_prompt_embeds`:" f" {negative_prompt_embeds}. Please make sure to only forward one of the two." ) if prompt_embeds is not None and pooled_prompt_embeds is None: raise ValueError( "If `prompt_embeds` are provided, `pooled_prompt_embeds` also have to be passed. " "Make sure to generate `pooled_prompt_embeds` from the same text encoder " "that was used to generate `prompt_embeds`." ) if negative_prompt_embeds is not None and negative_pooled_prompt_embeds is None: raise ValueError( "If `negative_prompt_embeds` are provided, `negative_pooled_prompt_embeds` " "also have to be passed. Make sure to generate `negative_pooled_prompt_embeds` " "from the same text encoder that was used to generate `negative_prompt_embeds`." ) if max_sequence_length is not None and max_sequence_length > 512: raise ValueError(f"`max_sequence_length` cannot be greater than 512 but is {max_sequence_length}") def _encode_vae_image(self, image: torch.Tensor, generator: torch.Generator): if image.ndim != 4: raise ValueError(f"Expected image dims 4, got {image.ndim}.") if isinstance(generator, list): image_latents = [ self.retrieve_latents(self.vae.encode(image[i : i + 1]), generator=generator[i], sample_mode="argmax") for i in range(image.shape[0]) ] image_latents = torch.cat(image_latents, dim=0) else: image_latents = self.retrieve_latents(self.vae.encode(image), generator=generator, sample_mode="argmax") image_latents = (image_latents - self.vae.config.shift_factor) * self.vae.config.scaling_factor if image_latents.ndim == 5: logger.warning("VAE output has 5 dimensions, squeezing first dimension") image_latents = image_latents.squeeze(0) return image_latents def prepare_latents( self, image: torch.Tensor | None, batch_size: int, num_channels_latents: int, height: int, width: int, dtype: torch.dtype, device: torch.device, generator: torch.Generator | list[torch.Generator] | None = None, latents: torch.Tensor | None = None, ): if isinstance(generator, list) and len(generator) != batch_size: raise ValueError( f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" f" size of {batch_size}. Make sure the batch size matches the length of the generators." ) height = 2 * (int(height) // (self.vae_scale_factor * 2)) width = 2 * (int(width) // (self.vae_scale_factor * 2)) shape = (batch_size, num_channels_latents, height, width) image_latents = image_ids = None if image is not None: image = image.to(device=device, dtype=dtype) if image.shape[1] != self.latent_channels: image_latents = self._encode_vae_image(image=image, generator=generator) else: image_latents = image if batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] == 0: # expand init_latents for batch_size additional_image_per_prompt = batch_size // image_latents.shape[0] image_latents = torch.cat([image_latents] * additional_image_per_prompt, dim=0) elif batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] != 0: raise ValueError( f"Cannot duplicate `image` of batch size {image_latents.shape[0]} to {batch_size} text prompts." ) else: image_latents = torch.cat([image_latents], dim=0) 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 // 2, image_latent_width // 2, device, dtype) # image ids are the same as latent ids with the first dimension set to 1 instead of 0 image_ids[..., 0] = 1 latent_ids = self._prepare_latent_image_ids(height // 2, width // 2, device, dtype) if latents is None: latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) latents = self._pack_latents(latents, batch_size, num_channels_latents, height, width) else: latents = latents.to(device=device, dtype=dtype) if latents.dim() == 4: latents = self._pack_latents(latents, batch_size, num_channels_latents, height, width) return latents, image_latents, latent_ids, image_ids @property def interrupt(self): return self._interrupt @property def guidance_scale(self): return self._guidance_scale @property def joint_attention_kwargs(self): return self._joint_attention_kwargs @property def num_timesteps(self): return self._num_timesteps @property def current_timestep(self) -> torch.Tensor | None: return self._current_timestep def predict_noise( self, hidden_states: torch.Tensor, timestep: torch.Tensor, guidance: torch.Tensor | None, pooled_projections: torch.Tensor, encoder_hidden_states: torch.Tensor, txt_ids: torch.Tensor, img_ids: torch.Tensor, joint_attention_kwargs: dict[str, Any] | None, return_dict: bool = False, ) -> torch.Tensor: return self.transformer( hidden_states=hidden_states, timestep=timestep, guidance=guidance, pooled_projections=pooled_projections, encoder_hidden_states=encoder_hidden_states, txt_ids=txt_ids, img_ids=img_ids, joint_attention_kwargs=joint_attention_kwargs, return_dict=return_dict, )[0] def diffuse( self, prompt_embeds: torch.Tensor, pooled_prompt_embeds: torch.Tensor, negative_prompt_embeds: torch.Tensor | None, negative_pooled_prompt_embeds: torch.Tensor | None, latents: torch.Tensor, image_latents: torch.Tensor | None, latent_ids: torch.Tensor, text_ids: torch.Tensor, negative_text_ids: torch.Tensor | None, timesteps: torch.Tensor, do_true_cfg: bool, guidance: torch.Tensor | None, true_cfg_scale: float, callback_on_step_end: Callable[[int, int, dict], None] | None = None, callback_on_step_end_tensor_inputs: list[str] = ["latents"], ) -> torch.Tensor: self.scheduler.set_begin_index(0) self.transformer.do_true_cfg = do_true_cfg with self.progress_bar(total=len(timesteps)) as pbar: for i, t in enumerate(timesteps): if self.interrupt: continue self._current_timestep = t latent_model_input = latents if image_latents is not None: latent_model_input = torch.cat([latents, image_latents], dim=1) timestep = t.expand(latents.shape[0]).to(device=latents.device, dtype=latents.dtype) positive_kwargs = { "hidden_states": latent_model_input, "timestep": timestep / 1000, "guidance": guidance, "pooled_projections": pooled_prompt_embeds, "encoder_hidden_states": prompt_embeds, "txt_ids": text_ids, "img_ids": latent_ids, "joint_attention_kwargs": self.joint_attention_kwargs, "return_dict": False, } negative_kwargs = None if do_true_cfg: negative_kwargs = { "hidden_states": latent_model_input, "timestep": timestep / 1000, "guidance": guidance, "pooled_projections": negative_pooled_prompt_embeds, "encoder_hidden_states": negative_prompt_embeds, "txt_ids": negative_text_ids, "img_ids": latent_ids, "joint_attention_kwargs": self.joint_attention_kwargs, "return_dict": False, } noise_pred = self.predict_noise_maybe_with_cfg( do_true_cfg, true_cfg_scale, positive_kwargs, negative_kwargs, cfg_normalize=False, output_slice=latents.size(1), ) latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, do_true_cfg) if callback_on_step_end is not None: callback_kwargs = {} for k in callback_on_step_end_tensor_inputs: callback_kwargs[k] = locals()[k] callback_outputs = callback_on_step_end(self, i, t, callback_kwargs) latents = callback_outputs.pop("latents", latents) prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds) pbar.update() return latents @torch.no_grad() def forward( self, req: DiffusionRequestBatch, image: PIL.Image.Image | list[PIL.Image.Image] | None = None, prompt: str | list[str] | None = None, prompt_2: str | list[str] | None = None, negative_prompt: str | list[str] | None = None, negative_prompt_2: str | list[str] | None = None, height: int | None = None, width: int | None = None, num_inference_steps: int = 28, guidance_scale: float = 3.5, true_cfg_scale: float = 1.0, num_images_per_prompt: int = 1, generator: torch.Generator | list[torch.Generator] | None = None, latents: torch.Tensor | None = None, prompt_embeds: torch.Tensor | None = None, pooled_prompt_embeds: torch.Tensor | None = None, negative_prompt_embeds: torch.Tensor | None = None, negative_pooled_prompt_embeds: torch.Tensor | None = None, output_type: str | None = "pil", return_dict: bool = True, attention_kwargs: dict[str, Any] | None = None, callback_on_step_end: Callable[[int, int, dict], None] | None = None, callback_on_step_end_tensor_inputs: list[str] = ["latents"], max_sequence_length: int = 512, sigmas: list[float] | None = None, ) -> DiffusionOutput: # Handle multiple prompts - only take the first one, similar to Flux2KleinPipeline if len(req.prompts) > 1: logger.warning( "This model only supports a single prompt, not a batched request. Taking only the first prompt for now." ) first_prompt = req.prompts[0] if req.prompts else None if isinstance(first_prompt, str): prompt = first_prompt # Keep negative_prompt from function parameter (passed via API) elif first_prompt: prompt = first_prompt.get("prompt") or "" # Prefer negative_prompt from prompt dict, fallback to function parameter np_from_dict = first_prompt.get("negative_prompt") if np_from_dict is not None: negative_prompt = np_from_dict # else: keep original prompt unchanged # Handle image from prompt data if isinstance(first_prompt, str): raw_image = None elif first_prompt: raw_image = first_prompt.get("multi_modal_data", {}).get("image") else: raw_image = None if raw_image is None: pass # use image from param list elif isinstance(raw_image, list): image = [PIL.Image.open(im) if isinstance(im, str) else cast(PIL.Image.Image, im) for im in raw_image] else: image = PIL.Image.open(raw_image) if isinstance(raw_image, str) else cast(PIL.Image.Image, raw_image) height = req.sampling_params.height or height width = req.sampling_params.width or width num_inference_steps = req.sampling_params.num_inference_steps or num_inference_steps sigmas = req.sampling_params.sigmas or sigmas guidance_scale = ( req.sampling_params.guidance_scale if req.sampling_params.guidance_scale_provided else guidance_scale ) num_images_per_prompt = ( req.sampling_params.num_outputs_per_prompt if req.sampling_params.num_outputs_per_prompt > 0 else num_images_per_prompt ) true_cfg_scale = ( req.sampling_params.true_cfg_scale if req.sampling_params.true_cfg_scale is not None else true_cfg_scale ) generator = req.sampling_params.generator or generator num_images_per_prompt = ( req.sampling_params.num_outputs_per_prompt if req.sampling_params.num_outputs_per_prompt > 0 else num_images_per_prompt ) latents = ( req.sampling_params.extra_args.get("latents") if req.sampling_params.extra_args.get("latents") is not None else latents ) height = height or self.default_sample_size * self.vae_scale_factor width = width or self.default_sample_size * self.vae_scale_factor self.check_inputs( prompt, prompt_2=prompt_2, height=height, width=width, negative_prompt=negative_prompt, negative_prompt_2=negative_prompt_2, prompt_embeds=prompt_embeds, pooled_prompt_embeds=pooled_prompt_embeds, negative_prompt_embeds=negative_prompt_embeds, negative_pooled_prompt_embeds=negative_pooled_prompt_embeds, callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs, max_sequence_length=max_sequence_length, ) self._guidance_scale = guidance_scale self._attention_kwargs = attention_kwargs self._joint_attention_kwargs = attention_kwargs if attention_kwargs is not None else {} self._interrupt = False if prompt is not None and isinstance(prompt, str): batch_size = 1 elif prompt is not None and isinstance(prompt, list): batch_size = len(prompt) else: batch_size = prompt_embeds.shape[0] device = self._execution_device has_neg_prompt = negative_prompt is not None or ( negative_prompt_embeds is not None and negative_pooled_prompt_embeds is not None ) do_true_cfg = true_cfg_scale > 1.0 and has_neg_prompt do_true_cfg = do_true_cfg and self.check_cfg_parallel_validity(true_cfg_scale, has_neg_prompt) # 1. Prepare text embeddings prompt_embeds, pooled_prompt_embeds, text_ids = self.encode_prompt( prompt=prompt, prompt_2=prompt_2, device=device, num_images_per_prompt=num_images_per_prompt, prompt_embeds=prompt_embeds, pooled_prompt_embeds=pooled_prompt_embeds, max_sequence_length=max_sequence_length, ) if do_true_cfg: negative_prompt_embeds, negative_pooled_prompt_embeds, negative_text_ids = self.encode_prompt( prompt=negative_prompt, prompt_2=negative_prompt_2, prompt_embeds=negative_prompt_embeds, pooled_prompt_embeds=negative_pooled_prompt_embeds, device=device, num_images_per_prompt=num_images_per_prompt, max_sequence_length=max_sequence_length, ) # 2. Process images if image is not None and not (isinstance(image, torch.Tensor) and image.size(1) == self.latent_channels): img = image[0] if isinstance(image, list) else image image_height, image_width = self.image_processor.get_default_height_width(img) multiple_of = self.vae_scale_factor * 2 image_width = image_width // multiple_of * multiple_of image_height = image_height // multiple_of * multiple_of image = self.image_processor.resize(image, image_height, image_width) image = self.image_processor.preprocess(image, image_height, image_width) if height is None and width is None: height = image_height width = image_width # 3. Prepare latent variables num_channels_latents = self.transformer.in_channels // 4 latents, image_latents, latent_ids, image_ids = self.prepare_latents( image=image, batch_size=batch_size * num_images_per_prompt, num_channels_latents=num_channels_latents, height=height, width=width, dtype=prompt_embeds.dtype, device=device, generator=generator, latents=latents, ) if image_ids is not None: latent_ids = torch.cat([latent_ids, image_ids], dim=0) # 4. Prepare timesteps sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas if hasattr(self.scheduler.config, "use_flow_sigmas") and self.scheduler.config.use_flow_sigmas: sigmas = None image_seq_len = latents.shape[1] mu = self.calculate_shift( image_seq_len, self.scheduler.config.get("base_image_seq_len", 256), self.scheduler.config.get("max_image_seq_len", 4096), self.scheduler.config.get("base_shift", 0.5), self.scheduler.config.get("max_shift", 1.15), ) timesteps, num_inference_steps = self.retrieve_timesteps( self.scheduler, num_inference_steps, device, sigmas=sigmas, mu=mu, ) self._num_timesteps = len(timesteps) if self.transformer.guidance_embeds: guidance = torch.full([1], guidance_scale, device=device, dtype=torch.float32) guidance = guidance.expand(latents.shape[0]) else: guidance = None if self.joint_attention_kwargs is None: self._joint_attention_kwargs = {} # 5. Denoising loop latents = self.diffuse( prompt_embeds=prompt_embeds, pooled_prompt_embeds=pooled_prompt_embeds, negative_prompt_embeds=negative_prompt_embeds, negative_pooled_prompt_embeds=negative_pooled_prompt_embeds, latents=latents, image_latents=image_latents, latent_ids=latent_ids, text_ids=text_ids, negative_text_ids=negative_text_ids if do_true_cfg else None, timesteps=timesteps, do_true_cfg=do_true_cfg, guidance=guidance, true_cfg_scale=true_cfg_scale, callback_on_step_end=callback_on_step_end, callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs, ) if output_type == "latent": image = latents else: latents = self._unpack_latents(latents, height, width, self.vae_scale_factor) latents = (latents / self.vae.config.scaling_factor) + self.vae.config.shift_factor image = self.vae.decode(latents, return_dict=False)[0] return DiffusionOutput(output=image) def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: loader = AutoWeightsLoader(self) return loader.load_weights(weights)