# Copyright 2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import torch from transformers import ( CLIPTextModelWithProjection, CLIPTokenizer, T5EncoderModel, T5TokenizerFast, ) from ...configuration_utils import FrozenDict from ...image_processor import VaeImageProcessor from ...loaders import SD3LoraLoaderMixin from ...models import AutoencoderKL from ...utils import USE_PEFT_BACKEND, logging, scale_lora_layers, unscale_lora_layers from ..modular_pipeline import ModularPipelineBlocks, PipelineState from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam from .modular_pipeline import StableDiffusion3ModularPipeline logger = logging.get_logger(__name__) # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.retrieve_latents def retrieve_latents( encoder_output: torch.Tensor, generator: torch.Generator | None = None, sample_mode: str = "sample" ): if hasattr(encoder_output, "latent_dist") and sample_mode == "sample": return encoder_output.latent_dist.sample(generator) elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax": return encoder_output.latent_dist.mode() elif hasattr(encoder_output, "latents"): return encoder_output.latents else: raise AttributeError("Could not access latents of provided encoder_output") def encode_vae_image( vae: AutoencoderKL, image: torch.Tensor, generator: torch.Generator, sample_mode="sample", ): if isinstance(generator, list): image_latents = [ retrieve_latents( vae.encode(image[i : i + 1]), generator=generator[i], sample_mode=sample_mode, ) for i in range(image.shape[0]) ] image_latents = torch.cat(image_latents, dim=0) else: image_latents = retrieve_latents(vae.encode(image), generator=generator, sample_mode=sample_mode) image_latents = (image_latents - vae.config.shift_factor) * vae.config.scaling_factor return image_latents def _get_t5_prompt_embeds( text_encoder: T5EncoderModel | None, tokenizer: T5TokenizerFast | None, prompt: str | list[str] = None, max_sequence_length: int = 256, device: torch.device | None = None, joint_attention_dim: int = 4096, dtype: torch.dtype | None = None, ): device = device or (text_encoder.device if text_encoder is not None else torch.device("cpu")) dtype = dtype or (text_encoder.dtype if text_encoder is not None else torch.float32) prompt = [prompt] if isinstance(prompt, str) else prompt batch_size = len(prompt) if text_encoder is None or tokenizer is None: return torch.zeros( (batch_size, max_sequence_length, joint_attention_dim), device=device, dtype=dtype, ) text_inputs = tokenizer( prompt, padding="max_length", max_length=max_sequence_length, truncation=True, add_special_tokens=True, return_tensors="pt", ) text_input_ids = text_inputs.input_ids untruncated_ids = 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 = tokenizer.batch_decode(untruncated_ids[:, tokenizer.model_max_length - 1 : -1]) logger.warning( f"The following part of your input was truncated because `max_sequence_length` is set to " f" {max_sequence_length} tokens: {removed_text}" ) prompt_embeds = text_encoder(text_input_ids.to(device))[0] prompt_embeds = prompt_embeds.to(dtype=dtype, device=device) return prompt_embeds def _get_clip_prompt_embeds( text_encoder: CLIPTextModelWithProjection | None, tokenizer: CLIPTokenizer | None, prompt: str | list[str], device: torch.device | None = None, clip_skip: int | None = None, hidden_size: int = 768, dtype: torch.dtype | None = None, ): device = device or (text_encoder.device if text_encoder is not None else torch.device("cpu")) dtype = dtype or (text_encoder.dtype if text_encoder is not None else torch.float32) prompt = [prompt] if isinstance(prompt, str) else prompt batch_size = len(prompt) if text_encoder is None or tokenizer is None: prompt_embeds = torch.zeros((batch_size, 77, hidden_size), device=device, dtype=dtype) pooled_prompt_embeds = torch.zeros((batch_size, hidden_size), device=device, dtype=dtype) return prompt_embeds, pooled_prompt_embeds text_inputs = tokenizer( prompt, padding="max_length", max_length=tokenizer.model_max_length, truncation=True, return_tensors="pt", ) text_input_ids = text_inputs.input_ids untruncated_ids = 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 = tokenizer.batch_decode(untruncated_ids[:, tokenizer.model_max_length - 1 : -1]) logger.warning( f"The following part of your input was truncated because CLIP can only handle sequences up to" f" {tokenizer.model_max_length} tokens: {removed_text}" ) prompt_embeds = text_encoder(text_input_ids.to(device), output_hidden_states=True) pooled_prompt_embeds = prompt_embeds[0] if clip_skip is None: prompt_embeds = prompt_embeds.hidden_states[-2] else: prompt_embeds = prompt_embeds.hidden_states[-(clip_skip + 2)] prompt_embeds = prompt_embeds.to(dtype=dtype, device=device) return prompt_embeds, pooled_prompt_embeds def encode_prompt( components, prompt: str | list[str], prompt_2: str | list[str] | None = None, prompt_3: str | list[str] | None = None, device: torch.device | None = None, negative_prompt: str | list[str] | None = None, negative_prompt_2: str | list[str] | None = None, negative_prompt_3: str | list[str] | None = None, clip_skip: int | None = None, max_sequence_length: int = 256, lora_scale: float | None = None, ): device = device or components._execution_device expected_dtype = None if components.text_encoder is not None: expected_dtype = components.text_encoder.dtype elif components.text_encoder_2 is not None: expected_dtype = components.text_encoder_2.dtype elif getattr(components, "transformer", None) is not None: expected_dtype = components.transformer.dtype else: expected_dtype = torch.float32 if lora_scale is not None and isinstance(components, SD3LoraLoaderMixin): components._lora_scale = lora_scale if components.text_encoder is not None and USE_PEFT_BACKEND: scale_lora_layers(components.text_encoder, lora_scale) if components.text_encoder_2 is not None and USE_PEFT_BACKEND: scale_lora_layers(components.text_encoder_2, lora_scale) prompt = [prompt] if isinstance(prompt, str) else prompt batch_size = len(prompt) prompt_2 = prompt_2 or prompt prompt_2 = [prompt_2] if isinstance(prompt_2, str) else prompt_2 prompt_3 = prompt_3 or prompt prompt_3 = [prompt_3] if isinstance(prompt_3, str) else prompt_3 prompt_embed, pooled_prompt_embed = _get_clip_prompt_embeds( components.text_encoder, components.tokenizer, prompt=prompt, device=device, clip_skip=clip_skip, hidden_size=768, dtype=expected_dtype, ) prompt_2_embed, pooled_prompt_2_embed = _get_clip_prompt_embeds( components.text_encoder_2, components.tokenizer_2, prompt=prompt_2, device=device, clip_skip=clip_skip, hidden_size=1280, dtype=expected_dtype, ) clip_prompt_embeds = torch.cat([prompt_embed, prompt_2_embed], dim=-1) t5_prompt_embed = _get_t5_prompt_embeds( components.text_encoder_3, components.tokenizer_3, prompt=prompt_3, max_sequence_length=max_sequence_length, device=device, joint_attention_dim=( components.transformer.config.joint_attention_dim if getattr(components, "transformer", None) is not None else 4096 ), dtype=expected_dtype, ) clip_prompt_embeds = torch.nn.functional.pad( clip_prompt_embeds, (0, t5_prompt_embed.shape[-1] - clip_prompt_embeds.shape[-1]), ) prompt_embeds = torch.cat([clip_prompt_embeds, t5_prompt_embed], dim=-2) pooled_prompt_embeds = torch.cat([pooled_prompt_embed, pooled_prompt_2_embed], dim=-1) negative_prompt_embeds = None negative_pooled_prompt_embeds = None if negative_prompt is not None or negative_prompt_2 is not None or negative_prompt_3 is not None: negative_prompt = negative_prompt or "" negative_prompt_2 = negative_prompt_2 or negative_prompt negative_prompt_3 = negative_prompt_3 or negative_prompt negative_prompt = batch_size * [negative_prompt] if isinstance(negative_prompt, str) else negative_prompt negative_prompt_2 = ( batch_size * [negative_prompt_2] if isinstance(negative_prompt_2, str) else negative_prompt_2 ) negative_prompt_3 = ( batch_size * [negative_prompt_3] if isinstance(negative_prompt_3, str) else negative_prompt_3 ) negative_prompt_embed, negative_pooled_prompt_embed = _get_clip_prompt_embeds( components.text_encoder, components.tokenizer, prompt=negative_prompt, device=device, clip_skip=None, hidden_size=768, dtype=expected_dtype, ) negative_prompt_2_embed, negative_pooled_prompt_2_embed = _get_clip_prompt_embeds( components.text_encoder_2, components.tokenizer_2, prompt=negative_prompt_2, device=device, clip_skip=None, hidden_size=1280, dtype=expected_dtype, ) negative_clip_prompt_embeds = torch.cat([negative_prompt_embed, negative_prompt_2_embed], dim=-1) t5_negative_prompt_embed = _get_t5_prompt_embeds( components.text_encoder_3, components.tokenizer_3, prompt=negative_prompt_3, max_sequence_length=max_sequence_length, device=device, joint_attention_dim=( components.transformer.config.joint_attention_dim if getattr(components, "transformer", None) is not None else 4096 ), dtype=expected_dtype, ) negative_clip_prompt_embeds = torch.nn.functional.pad( negative_clip_prompt_embeds, ( 0, t5_negative_prompt_embed.shape[-1] - negative_clip_prompt_embeds.shape[-1], ), ) negative_prompt_embeds = torch.cat([negative_clip_prompt_embeds, t5_negative_prompt_embed], dim=-2) negative_pooled_prompt_embeds = torch.cat( [negative_pooled_prompt_embed, negative_pooled_prompt_2_embed], dim=-1 ) if components.text_encoder is not None and isinstance(components, SD3LoraLoaderMixin) and USE_PEFT_BACKEND: unscale_lora_layers(components.text_encoder, lora_scale) if components.text_encoder_2 is not None and isinstance(components, SD3LoraLoaderMixin) and USE_PEFT_BACKEND: unscale_lora_layers(components.text_encoder_2, lora_scale) return ( prompt_embeds, negative_prompt_embeds, pooled_prompt_embeds, negative_pooled_prompt_embeds, ) class StableDiffusion3ProcessImagesInputStep(ModularPipelineBlocks): model_name = "stable-diffusion-3" @property def description(self) -> str: return "Image Preprocess step for SD3." @property def expected_components(self) -> list[ComponentSpec]: return [ ComponentSpec( "image_processor", VaeImageProcessor, config=FrozenDict({"vae_scale_factor": 8, "vae_latent_channels": 16}), default_creation_method="from_config", ), ] @property def inputs(self) -> list[InputParam]: return [ InputParam( "image", description="The input image to be used as the starting point for the image-to-image process.", ), InputParam("height", description="The height in pixels of the generated image."), InputParam("width", description="The width in pixels of the generated image."), ] @property def intermediate_outputs(self) -> list[OutputParam]: return [OutputParam(name="processed_image", description="The pre-processed image tensor.")] @staticmethod def check_inputs(height, width, vae_scale_factor, patch_size): if height is not None and height % (vae_scale_factor * patch_size) != 0: raise ValueError(f"Height must be divisible by {vae_scale_factor * patch_size} but is {height}") if width is not None and width % (vae_scale_factor * patch_size) != 0: raise ValueError(f"Width must be divisible by {vae_scale_factor * patch_size} but is {width}") @torch.no_grad() def __call__(self, components: StableDiffusion3ModularPipeline, state: PipelineState): block_state = self.get_block_state(state) if block_state.image is None: raise ValueError("`image` cannot be None") image = block_state.image self.check_inputs( height=block_state.height, width=block_state.width, vae_scale_factor=components.vae_scale_factor, patch_size=components.patch_size, ) height = block_state.height or components.default_height width = block_state.width or components.default_width block_state.processed_image = components.image_processor.preprocess(image=image, height=height, width=width) self.set_block_state(state, block_state) return components, state class StableDiffusion3VaeEncoderStep(ModularPipelineBlocks): model_name = "stable-diffusion-3" def __init__( self, input_name: str = "processed_image", output_name: str = "image_latents", sample_mode: str = "sample", ): self._image_input_name = input_name self._image_latents_output_name = output_name self.sample_mode = sample_mode super().__init__() @property def description(self) -> str: return f"Dynamic VAE Encoder step that converts {self._image_input_name} into latent representations {self._image_latents_output_name}." @property def expected_components(self) -> list[ComponentSpec]: return [ComponentSpec("vae", AutoencoderKL)] @property def inputs(self) -> list[InputParam]: return [ InputParam( self._image_input_name, description="The processed image input to be encoded.", ), InputParam( "generator", description="One or a list of torch generator(s) to make generation deterministic.", ), ] @property def intermediate_outputs(self) -> list[OutputParam]: return [ OutputParam( self._image_latents_output_name, type_hint=torch.Tensor, description="The latents representing the reference image", ) ] @torch.no_grad() def __call__(self, components: StableDiffusion3ModularPipeline, state: PipelineState) -> PipelineState: block_state = self.get_block_state(state) image = getattr(block_state, self._image_input_name) if image is None: setattr(block_state, self._image_latents_output_name, None) else: device = components._execution_device dtype = components.vae.dtype image = image.to(device=device, dtype=dtype) image_latents = encode_vae_image( image=image, vae=components.vae, generator=block_state.generator, sample_mode=self.sample_mode, ) setattr(block_state, self._image_latents_output_name, image_latents) self.set_block_state(state, block_state) return components, state class StableDiffusion3TextEncoderStep(ModularPipelineBlocks): model_name = "stable-diffusion-3" @property def description(self) -> str: return "Text Encoder step that generates text embeddings to guide the image generation for SD3." @property def expected_components(self) -> list[ComponentSpec]: return [ ComponentSpec("text_encoder", CLIPTextModelWithProjection), ComponentSpec("tokenizer", CLIPTokenizer), ComponentSpec("text_encoder_2", CLIPTextModelWithProjection), ComponentSpec("tokenizer_2", CLIPTokenizer), ComponentSpec("text_encoder_3", T5EncoderModel), ComponentSpec("tokenizer_3", T5TokenizerFast), ] @property def inputs(self) -> list[InputParam]: return [ InputParam( "prompt", description="The prompt or prompts to guide the image generation.", ), InputParam( "prompt_2", description="The prompt or prompts to be sent to tokenizer_2 and text_encoder_2.", ), InputParam( "prompt_3", description="The prompt or prompts to be sent to tokenizer_3 and text_encoder_3.", ), InputParam( "negative_prompt", description="The prompt or prompts not to guide the image generation.", ), InputParam( "negative_prompt_2", description="The prompt or prompts not to guide the image generation for tokenizer_2.", ), InputParam( "negative_prompt_3", description="The prompt or prompts not to guide the image generation for tokenizer_3.", ), InputParam( "clip_skip", type_hint=int, description="Number of layers to be skipped from CLIP while computing the prompt embeddings.", ), InputParam( "max_sequence_length", type_hint=int, default=256, description="Maximum sequence length to use with the prompt.", ), InputParam( "joint_attention_kwargs", description="A kwargs dictionary passed along to the AttentionProcessor.", ), ] @property def intermediate_outputs(self) -> list[OutputParam]: return [ OutputParam("prompt_embeds", type_hint=torch.Tensor), OutputParam("negative_prompt_embeds", type_hint=torch.Tensor), OutputParam("pooled_prompt_embeds", type_hint=torch.Tensor), OutputParam("negative_pooled_prompt_embeds", type_hint=torch.Tensor), ] @torch.no_grad() def __call__(self, components: StableDiffusion3ModularPipeline, state: PipelineState) -> PipelineState: block_state = self.get_block_state(state) block_state.device = components._execution_device lora_scale = ( block_state.joint_attention_kwargs.get("scale", None) if block_state.joint_attention_kwargs else None ) ( prompt_embeds, negative_prompt_embeds, pooled_prompt_embeds, negative_pooled_prompt_embeds, ) = encode_prompt( components=components, prompt=block_state.prompt, prompt_2=block_state.prompt_2, prompt_3=block_state.prompt_3, device=block_state.device, negative_prompt=block_state.negative_prompt, negative_prompt_2=block_state.negative_prompt_2, negative_prompt_3=block_state.negative_prompt_3, clip_skip=block_state.clip_skip, max_sequence_length=block_state.max_sequence_length, lora_scale=lora_scale, ) block_state.prompt_embeds = prompt_embeds block_state.negative_prompt_embeds = negative_prompt_embeds block_state.pooled_prompt_embeds = pooled_prompt_embeds block_state.negative_pooled_prompt_embeds = negative_pooled_prompt_embeds self.set_block_state(state, block_state) return components, state