madtune's picture
add pipeline patch and scripts
c85ad6e
Raw
History Blame Contribute Delete
20.4 kB
# 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)