Spaces:
Sleeping
Sleeping
File size: 20,378 Bytes
1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 c85ad6e 1bf3c29 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 | # 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()
@classmethod
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}")
@property
def guidance_scale(self):
return self._guidance_scale
@property
def do_classifier_free_guidance(self):
return self._guidance_scale > 1.0
@property
def num_timesteps(self):
return self._num_timesteps
@torch.no_grad()
@replace_example_docstring(EXAMPLE_DOC_STRING)
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)
|