Spaces:
Sleeping
Sleeping
| # Copyright 2024 NVIDIA and 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 | |
| import inspect | |
| import os | |
| from typing import Callable, Dict, List, Optional, Tuple, Union | |
| import torch | |
| from transformers import AutoTokenizer, AutoModelForCausalLM | |
| from diffusers.models import ModelMixin | |
| from diffusers.schedulers import FlowMatchEulerDiscreteScheduler | |
| from diffusers.utils import ( | |
| USE_PEFT_BACKEND, | |
| logging, | |
| replace_example_docstring, | |
| scale_lora_layers, | |
| unscale_lora_layers, | |
| ) | |
| from diffusers.utils.torch_utils import randn_tensor | |
| from diffusers.pipelines.pipeline_utils import DiffusionPipeline | |
| try: | |
| from .pipeline_output import PixelDiTPipelineOutput | |
| except ImportError: | |
| from pipeline_output import PixelDiTPipelineOutput | |
| logger = logging.get_logger(__name__) | |
| # chi_prompt: the instruction prefix prepended to every user prompt during training. | |
| # Gemma was trained to "enhance" prompts through this prefix — omitting it degrades output. | |
| _CHI_PROMPT = "\n".join([ | |
| 'Given a user prompt, generate an "Enhanced prompt" that provides detailed visual descriptions suitable for image generation. Evaluate the level of detail in the user prompt:', | |
| '- If the prompt is simple, focus on adding specifics about colors, shapes, sizes, textures, and spatial relationships to create vivid and concrete scenes.', | |
| '- If the prompt is already detailed, refine and enhance the existing details slightly without overcomplicating.', | |
| 'Here are examples of how to transform or refine prompts:', | |
| '- User Prompt: A cat sleeping -> Enhanced: A small, fluffy white cat curled up in a round shape, sleeping peacefully on a warm sunny windowsill, surrounded by pots of blooming red flowers.', | |
| '- User Prompt: A busy city street -> Enhanced: A bustling city street scene at dusk, featuring glowing street lamps, a diverse crowd of people in colorful clothing, and a double-decker bus passing by towering glass skyscrapers.', | |
| 'Please generate only the enhanced description for the prompt below and avoid including any additional commentary or evaluations:', | |
| 'User Prompt: ', | |
| ]) | |
| _TXT_MAX_LENGTH = 300 | |
| _SELECT_IDX = [0] + list(range(-(_TXT_MAX_LENGTH - 1), 0)) # BOS + last 299 tokens | |
| EXAMPLE_DOC_STRING = """ | |
| Examples: | |
| ```py | |
| >>> import torch | |
| >>> from diffusers import PixelDiTPipeline | |
| >>> pipe = PixelDiTPipeline.from_pretrained( | |
| ... "madtune/pixeldit-diffusers", torch_dtype=torch.bfloat16 | |
| ... ) | |
| >>> pipe.to("cuda") | |
| >>> prompt = "a white horse galloping through a meadow at sunset, cinematic lighting" | |
| >>> image = pipe(prompt).images[0] | |
| >>> image.save("pixeldit_out.png") | |
| ``` | |
| """ | |
| class PixelDiTPipeline(DiffusionPipeline): | |
| r""" | |
| Pipeline for text-to-image generation using PixelDiT. | |
| PixelDiT is a pixel-space diffusion transformer — it generates images directly without a VAE, | |
| using Gemma-2-2B as the text encoder with a chi_prompt instruction prefix. | |
| This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods | |
| implemented for all pipelines (downloading, saving, running on a device, etc.). | |
| Args: | |
| transformer ([`PixelDiTModel`]): | |
| Conditional transformer to denoise the image latents. | |
| scheduler ([`FlowMatchEulerDiscreteScheduler`]): | |
| Scheduler to denoise the image in combination with `transformer`. | |
| text_encoder ([`~transformers.AutoModelForCausalLM`]): | |
| Frozen Gemma-2-2B language model (decoder only). The chi_prompt prefix is applied internally. | |
| tokenizer ([`~transformers.AutoTokenizer`]): | |
| Tokenizer for the Gemma text encoder. | |
| """ | |
| model_cpu_offload_seq = "text_encoder->transformer" | |
| _optional_components = [] | |
| _callback_tensor_inputs = ["latents", "prompt_embeds", "negative_prompt_embeds"] | |
| def __init__( | |
| self, | |
| transformer, | |
| scheduler: FlowMatchEulerDiscreteScheduler, | |
| text_encoder, | |
| tokenizer, | |
| ): | |
| super().__init__() | |
| self.register_modules( | |
| transformer=transformer, | |
| scheduler=scheduler, | |
| text_encoder=text_encoder, | |
| tokenizer=tokenizer, | |
| ) | |
| self._num_chi_tokens = len(self.tokenizer.encode(_CHI_PROMPT)) | |
| # ------------------------------------------------------------------ | |
| # LoRA API | |
| # ------------------------------------------------------------------ | |
| def load_lora_weights( | |
| self, | |
| pretrained_model_name_or_path_or_dict, | |
| adapter_name: str = "default", | |
| **kwargs, | |
| ): | |
| """ | |
| Load LoRA weights into the transformer. | |
| Accepts: | |
| - A PEFT adapter directory (must contain adapter_config.json). | |
| - A path to a single .safetensors / .pt / .bin file. | |
| - A pre-loaded state dict. | |
| Keys may optionally carry a ``transformer.`` prefix — it will be stripped. | |
| Kohya-style ``.alpha`` keys are extracted as ``network_alphas``. | |
| """ | |
| print(f"[LoRA] Loading adapter '{adapter_name}'...") | |
| # --- PEFT adapter directory (saved by train_lora.py via model.save_pretrained) --- | |
| # These use adapter_model.safetensors + adapter_config.json (PEFT format). | |
| # diffusers' load_lora_adapter expects pytorch_lora_weights.safetensors, so | |
| # we use PEFT's native API here instead. | |
| if ( | |
| isinstance(pretrained_model_name_or_path_or_dict, str) | |
| and os.path.isdir(pretrained_model_name_or_path_or_dict) | |
| and os.path.exists( | |
| os.path.join(pretrained_model_name_or_path_or_dict, "adapter_config.json") | |
| ) | |
| ): | |
| from peft import PeftModel | |
| lora_dir = pretrained_model_name_or_path_or_dict | |
| if isinstance(self.transformer, PeftModel): | |
| # already wrapped — add another adapter | |
| self.transformer.load_adapter(lora_dir, adapter_name=adapter_name) | |
| else: | |
| # first LoRA — wrap the transformer in a PeftModel | |
| self.transformer = PeftModel.from_pretrained( | |
| self.transformer, lora_dir, adapter_name=adapter_name, is_trainable=False | |
| ) | |
| print(f"[LoRA] Loaded PEFT adapter '{adapter_name}'.") | |
| return | |
| # --- state dict path or in-memory dict --- | |
| if isinstance(pretrained_model_name_or_path_or_dict, dict): | |
| state_dict = dict(pretrained_model_name_or_path_or_dict) | |
| else: | |
| path = str(pretrained_model_name_or_path_or_dict) | |
| if os.path.isfile(path): | |
| weights_file = path | |
| else: | |
| import glob | |
| candidates = ( | |
| glob.glob(os.path.join(path, "*.safetensors")) | |
| + glob.glob(os.path.join(path, "*.bin")) | |
| + glob.glob(os.path.join(path, "*.pt")) | |
| ) | |
| if not candidates: | |
| raise FileNotFoundError(f"[LoRA] No weights file found in {path}") | |
| weights_file = candidates[0] | |
| if weights_file.endswith(".safetensors"): | |
| from safetensors.torch import load_file | |
| state_dict = load_file(weights_file) | |
| else: | |
| state_dict = torch.load(weights_file, map_location="cpu", weights_only=True) | |
| # strip component prefix | |
| if any(k.startswith("transformer.") for k in state_dict): | |
| state_dict = { | |
| k[len("transformer."):]: v | |
| for k, v in state_dict.items() | |
| if k.startswith("transformer.") | |
| } | |
| # extract Kohya-style network_alphas (.alpha keys) | |
| network_alphas: dict = {} | |
| clean: dict = {} | |
| for k, v in state_dict.items(): | |
| if k.endswith(".alpha"): | |
| network_alphas[k[: -len(".alpha")]] = float(v) | |
| else: | |
| clean[k] = v | |
| self.transformer.load_lora_adapter( | |
| clean, | |
| adapter_name=adapter_name, | |
| network_alphas=network_alphas if network_alphas else None, | |
| **kwargs, | |
| ) | |
| print( | |
| f"[LoRA] Loaded adapter '{adapter_name}' " | |
| f"({len(clean)} keys, {len(network_alphas)} alphas)." | |
| ) | |
| def save_lora_weights( | |
| self, | |
| save_directory: str, | |
| adapter_name: str = "default", | |
| safe_serialization: bool = True, | |
| upcast_before_saving: bool = False, | |
| ): | |
| """Save LoRA adapter weights to disk (PEFT format).""" | |
| self.transformer.save_lora_adapter( | |
| save_directory, | |
| adapter_name=adapter_name, | |
| safe_serialization=safe_serialization, | |
| upcast_before_saving=upcast_before_saving, | |
| ) | |
| print(f"[LoRA] Saved adapter '{adapter_name}' to {save_directory}") | |
| def unload_lora_weights(self): | |
| """Remove all LoRA adapters and restore the base transformer weights.""" | |
| from peft import PeftModel | |
| if isinstance(self.transformer, PeftModel): | |
| self.transformer = self.transformer.merge_and_unload() | |
| print("[LoRA] LoRA merged and unloaded.") | |
| elif hasattr(self.transformer, "unload_lora"): | |
| self.transformer.unload_lora() | |
| print("[LoRA] LoRA unloaded.") | |
| def set_adapters(self, adapter_names, adapter_weights=None): | |
| """Activate one or more named adapters with optional per-adapter scales.""" | |
| self.transformer.set_adapters( | |
| adapter_names, | |
| weights=adapter_weights, | |
| ) | |
| def disable_lora(self): | |
| self.transformer.disable_lora() | |
| def enable_lora(self): | |
| self.transformer.enable_lora() | |
| def fuse_lora(self, lora_scale: float = 1.0, safe_fusing: bool = False, adapter_names=None, **kwargs): | |
| """Bake LoRA weights permanently into the base transformer weights.""" | |
| self.transformer.fuse_lora( | |
| lora_scale=lora_scale, | |
| safe_fusing=safe_fusing, | |
| adapter_names=adapter_names, | |
| ) | |
| def unfuse_lora(self, **kwargs): | |
| """Revert a previous fuse_lora() call.""" | |
| self.transformer.unfuse_lora() | |
| def from_pretrained(cls, pretrained_model_name_or_path, **kwargs): | |
| """ | |
| Load pipeline. The transformer is loaded from a PixelDiTModel checkpoint. | |
| Text encoder and tokenizer are loaded from Gemma-2-2B. | |
| """ | |
| import diffusers | |
| from .modeling_pixeldit_hf import PixelDiTModel | |
| # model_index.json references ["diffusers", "PixelDiTModel"] — inject at runtime | |
| if not hasattr(diffusers, "PixelDiTModel"): | |
| diffusers.PixelDiTModel = PixelDiTModel | |
| return super().from_pretrained(pretrained_model_name_or_path, **kwargs) | |
| def encode_prompt( | |
| self, | |
| prompt: Union[str, List[str]], | |
| device: torch.device, | |
| dtype: torch.dtype, | |
| do_classifier_free_guidance: bool = True, | |
| negative_prompt: Optional[Union[str, List[str]]] = None, | |
| lora_scale: Optional[float] = None, | |
| ) -> Tuple[torch.Tensor, torch.Tensor]: | |
| """ | |
| Encode prompt(s) using Gemma with chi_prompt prefix. | |
| Returns (prompt_embeds, negative_prompt_embeds), each [B, 300, 2304]. | |
| lora_scale: if set and a LoRA is loaded on the text encoder, scales its | |
| contribution during encoding then restores the original scale. | |
| """ | |
| if isinstance(prompt, str): | |
| prompt = [prompt] | |
| batch_size = len(prompt) | |
| # scale text-encoder LoRA if requested | |
| if lora_scale is not None and USE_PEFT_BACKEND: | |
| scale_lora_layers(self.text_encoder, lora_scale) | |
| try: | |
| if hasattr(self.text_encoder, "encode"): | |
| prompt_embeds = self.text_encoder.encode(prompt).to(device=device, dtype=dtype) | |
| if do_classifier_free_guidance: | |
| if negative_prompt is None: | |
| negative_prompt_embeds = self.text_encoder.encode_null(batch_size) | |
| else: | |
| if isinstance(negative_prompt, str): | |
| negative_prompt = [negative_prompt] * batch_size | |
| negative_prompt_embeds = self.text_encoder.encode(negative_prompt) | |
| negative_prompt_embeds = negative_prompt_embeds.to(device=device, dtype=dtype) | |
| else: | |
| negative_prompt_embeds = None | |
| return prompt_embeds, negative_prompt_embeds | |
| # --- positive embeds --- | |
| texts_full = [_CHI_PROMPT + p for p in prompt] | |
| max_len = self._num_chi_tokens + _TXT_MAX_LENGTH - 2 | |
| tok = self.tokenizer( | |
| texts_full, | |
| max_length=max_len, | |
| padding="max_length", | |
| truncation=True, | |
| return_tensors="pt", | |
| ).to(device) | |
| with torch.no_grad(): | |
| emb = self.text_encoder( | |
| input_ids=tok.input_ids, | |
| attention_mask=tok.attention_mask, | |
| ).last_hidden_state | |
| prompt_embeds = emb[:, _SELECT_IDX, :].to(dtype) | |
| # --- negative embeds --- | |
| if do_classifier_free_guidance: | |
| if negative_prompt is None: | |
| negative_prompt = [""] * batch_size | |
| elif isinstance(negative_prompt, str): | |
| negative_prompt = [negative_prompt] * batch_size | |
| neg_tok = self.tokenizer( | |
| negative_prompt, | |
| max_length=_TXT_MAX_LENGTH, | |
| padding="max_length", | |
| truncation=True, | |
| return_tensors="pt", | |
| ).to(device) | |
| with torch.no_grad(): | |
| neg_emb = self.text_encoder( | |
| input_ids=neg_tok.input_ids, | |
| attention_mask=neg_tok.attention_mask, | |
| ).last_hidden_state | |
| negative_prompt_embeds = neg_emb.to(dtype) | |
| else: | |
| negative_prompt_embeds = None | |
| return prompt_embeds, negative_prompt_embeds | |
| finally: | |
| if lora_scale is not None and USE_PEFT_BACKEND: | |
| unscale_lora_layers(self.text_encoder, lora_scale) | |
| def check_inputs(self, prompt, height, width, negative_prompt=None): | |
| if not isinstance(prompt, (str, list)): | |
| raise ValueError(f"`prompt` must be str or list, got {type(prompt)}") | |
| if height % 16 != 0 or width % 16 != 0: | |
| raise ValueError(f"`height` and `width` must be divisible by 16, got {height}×{width}") | |
| def guidance_scale(self): | |
| return self._guidance_scale | |
| def do_classifier_free_guidance(self): | |
| return self._guidance_scale > 1.0 | |
| def num_timesteps(self): | |
| return self._num_timesteps | |
| def __call__( | |
| self, | |
| prompt: Union[str, List[str]], | |
| negative_prompt: Optional[Union[str, List[str]]] = None, | |
| height: int = 512, | |
| width: int = 512, | |
| num_inference_steps: int = 20, | |
| guidance_scale: float = 3.5, | |
| generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, | |
| output_type: str = "pil", | |
| return_dict: bool = True, | |
| cross_attention_kwargs: Optional[Dict[str, any]] = None, | |
| callback_on_step_end: Optional[Callable[[int, int, Dict], None]] = None, | |
| callback_on_step_end_tensor_inputs: List[str] = ["latents"], | |
| **kwargs, | |
| ) -> Union[PixelDiTPipelineOutput, Tuple]: | |
| """ | |
| Generate images from text prompts. | |
| Args: | |
| prompt (`str` or `List[str]`): Prompt(s) to guide image generation. | |
| negative_prompt (`str` or `List[str]`, *optional*): Negative prompt(s). | |
| height (`int`, *optional*, defaults to 512): Output image height. Must be divisible by 16. | |
| width (`int`, *optional*, defaults to 512): Output image width. Must be divisible by 16. | |
| num_inference_steps (`int`, *optional*, defaults to 20): Number of denoising steps. | |
| guidance_scale (`float`, *optional*, defaults to 3.5): CFG guidance scale. | |
| generator (`torch.Generator`, *optional*): RNG for reproducibility. | |
| output_type (`str`, *optional*, defaults to `"pil"`): `"pil"` or `"np"`. | |
| return_dict (`bool`, *optional*, defaults to `True`): Return `PixelDiTPipelineOutput` or plain tuple. | |
| callback_on_step_end (`Callable`, *optional*): Called at end of each denoising step. | |
| callback_on_step_end_tensor_inputs (`List[str]`, *optional*): Tensor names passed to callback. | |
| Examples: | |
| %s | |
| Returns: | |
| [`PixelDiTPipelineOutput`] or `tuple`. | |
| """ | |
| # 0. setup | |
| device = self._execution_device | |
| dtype = self.transformer.dtype | |
| self._guidance_scale = guidance_scale | |
| lora_scale = (cross_attention_kwargs or {}).get("scale", None) | |
| if isinstance(prompt, str): | |
| prompt = [prompt] | |
| batch_size = len(prompt) | |
| # 1. validate | |
| self.check_inputs(prompt, height, width, negative_prompt) | |
| # 2. encode text | |
| prompt_embeds, negative_prompt_embeds = self.encode_prompt( | |
| prompt, | |
| device=device, | |
| dtype=dtype, | |
| do_classifier_free_guidance=self.do_classifier_free_guidance, | |
| negative_prompt=negative_prompt, | |
| lora_scale=lora_scale, | |
| ) | |
| # 3. prepare timesteps | |
| self.scheduler.set_timesteps(num_inference_steps, device=device) | |
| timesteps = self.scheduler.timesteps | |
| self._num_timesteps = len(timesteps) | |
| # 4. prepare noise (pixel-space — no VAE encoding needed) | |
| latents = randn_tensor( | |
| (batch_size, 3, height, width), | |
| generator=generator, | |
| device=device, | |
| dtype=dtype, | |
| ) | |
| # 5. denoising loop | |
| for i, t in enumerate(self.progress_bar(timesteps)): | |
| # expand for CFG | |
| if self.do_classifier_free_guidance: | |
| latent_model_input = torch.cat([latents] * 2) | |
| embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) | |
| else: | |
| latent_model_input = latents | |
| embeds = prompt_embeds | |
| # FlowMatchEulerDiscreteScheduler already returns t in [0, 1000] | |
| t_input = t.expand(latent_model_input.shape[0]) | |
| noise_pred = self.transformer(latent_model_input, t_input, embeds) | |
| # CFG | |
| if self.do_classifier_free_guidance: | |
| noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) | |
| noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) | |
| # scheduler step | |
| if hasattr(self.scheduler, "scale_model_input"): | |
| latents = self.scheduler.step(noise_pred, t, self.scheduler.scale_model_input(latents, t), return_dict=False)[0] | |
| else: | |
| latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0] | |
| if callback_on_step_end is not None: | |
| cb_kwargs = {} | |
| for k in callback_on_step_end_tensor_inputs: | |
| cb_kwargs[k] = locals()[k] | |
| callback_on_step_end(i, t, cb_kwargs) | |
| # 6. decode (pixel-space — just clamp and normalize) | |
| image = (latents.clamp(-1, 1) + 1) / 2 | |
| image = (image * 255).byte().permute(0, 2, 3, 1).cpu().numpy() | |
| if output_type == "pil": | |
| from PIL import Image | |
| image = [Image.fromarray(img) for img in image] | |
| self.maybe_free_model_hooks() | |
| if not return_dict: | |
| return (image,) | |
| return PixelDiTPipelineOutput(images=image) | |