Add support for Boogu Image and Boogu Image Edit
This commit is contained in:
@@ -15,6 +15,7 @@ from .hidream.hidream_o1_model import HidreamO1Model
|
|||||||
from .z_image.z_image_l2p_model import ZImageL2PModel
|
from .z_image.z_image_l2p_model import ZImageL2PModel
|
||||||
from .ideogram4 import Ideogram4Model
|
from .ideogram4 import Ideogram4Model
|
||||||
from .prx_pixel_t2i import PRXPixelT2IModel
|
from .prx_pixel_t2i import PRXPixelT2IModel
|
||||||
|
from .boogu_image import BooguImageModel, BooguImageEditModel
|
||||||
|
|
||||||
AI_TOOLKIT_MODELS = [
|
AI_TOOLKIT_MODELS = [
|
||||||
# put a list of models here
|
# put a list of models here
|
||||||
@@ -44,4 +45,6 @@ AI_TOOLKIT_MODELS = [
|
|||||||
ZImageL2PModel,
|
ZImageL2PModel,
|
||||||
Ideogram4Model,
|
Ideogram4Model,
|
||||||
PRXPixelT2IModel,
|
PRXPixelT2IModel,
|
||||||
|
BooguImageModel,
|
||||||
|
BooguImageEditModel,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from .boogu_image import BooguImageModel
|
||||||
|
from .boogu_image_edit import BooguImageEditModel
|
||||||
|
|
||||||
|
__all__ = ["BooguImageModel", "BooguImageEditModel"]
|
||||||
459
extensions_built_in/diffusion_models/boogu_image/boogu_image.py
Normal file
459
extensions_built_in/diffusion_models/boogu_image/boogu_image.py
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
"""Boogu-Image base (text-to-image) integration for ai-toolkit.
|
||||||
|
|
||||||
|
Boogu-Image is a Lumina2-style mixed double-/single-stream flow-matching DiT
|
||||||
|
conditioned on Qwen3-VL instruction features. This wires up the base T2I model
|
||||||
|
for LoRA / fine-tune training and preview sampling.
|
||||||
|
|
||||||
|
Only the base text-to-image path is implemented here (no reference-image / edit
|
||||||
|
conditioning). The architecture lives under ``./src`` (vendored & trimmed from the
|
||||||
|
upstream Boogu repo); nothing is imported from the original repo.
|
||||||
|
|
||||||
|
Weights are pulled from the bf16 release ``Boogu/Boogu-Image-0.1-Base`` (clean
|
||||||
|
safetensors). The ``-fp8`` sibling ships torchao float8 ``.bin`` weights that
|
||||||
|
need a matching torchao/cache_dit to deserialize and is not supported here --
|
||||||
|
use the bf16 repo and set ``quantize: true`` to run the transformer in fp8 via
|
||||||
|
ai-toolkit's own quantization.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.nn.functional as F
|
||||||
|
import yaml
|
||||||
|
from safetensors.torch import save_file
|
||||||
|
|
||||||
|
from transformers import AutoModel, AutoProcessor
|
||||||
|
|
||||||
|
from toolkit.accelerator import unwrap_model
|
||||||
|
from toolkit.advanced_prompt_embeds import AdvancedPromptEmbeds
|
||||||
|
from toolkit.basic import flush
|
||||||
|
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||||
|
from toolkit.models.base_model import BaseModel
|
||||||
|
from toolkit.samplers.custom_flowmatch_sampler import (
|
||||||
|
CustomFlowMatchEulerDiscreteScheduler,
|
||||||
|
)
|
||||||
|
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||||
|
|
||||||
|
from optimum.quanto import freeze, QTensor
|
||||||
|
from diffusers import AutoencoderKL
|
||||||
|
|
||||||
|
from .src.transformer import BooguImageTransformer2DModel
|
||||||
|
from .src.rope import get_freqs_cis
|
||||||
|
from .src.pipeline import (
|
||||||
|
BooguImagePipeline,
|
||||||
|
pad_instruction_features,
|
||||||
|
run_boogu_transformer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ai-toolkit uses CustomFlowMatchEulerDiscreteScheduler for training and (via our
|
||||||
|
# pipeline) sampling. ``shift`` warps timesteps toward the high-noise end; 3.0 is a
|
||||||
|
# reasonable high-resolution default and Boogu's own time-shift is applied in the
|
||||||
|
# preview sampler (see src/pipeline.boogu_time_schedule).
|
||||||
|
scheduler_config = {
|
||||||
|
"num_train_timesteps": 1000,
|
||||||
|
"use_dynamic_shifting": False,
|
||||||
|
"shift": 3.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Released weights. The "-fp8" sibling ships torchao float8 weights that need
|
||||||
|
# cache_dit/torchao to deserialize; the plain repo ships clean bf16 safetensors,
|
||||||
|
# which load directly and let ai-toolkit do its own (optional) quantization.
|
||||||
|
BOOGU_BASE_PATH = "Boogu/Boogu-Image-0.1-Base"
|
||||||
|
|
||||||
|
# System prompt the base T2I model was trained with (SYSTEM_PROMPT_4_T2I upstream).
|
||||||
|
SYSTEM_PROMPT_T2I = (
|
||||||
|
"You are a helpful assistant that generates high-quality images based on user "
|
||||||
|
"instructions. The instructions are as follows."
|
||||||
|
)
|
||||||
|
|
||||||
|
HF_TOKEN = os.getenv("HF_TOKEN", None)
|
||||||
|
|
||||||
|
|
||||||
|
def patch_qwen_vl_patch_embed(model) -> int:
|
||||||
|
"""Swap Qwen-VL's vision ``patch_embed`` Conv3d for the equivalent ``F.linear``.
|
||||||
|
|
||||||
|
Qwen-VL's patch_embed is a Conv3d whose kernel == stride, i.e. just a linear
|
||||||
|
projection of each flattened patch. bf16 Conv3d has no fast cuDNN kernel and
|
||||||
|
falls back to a slow path that effectively locks up image caching for the edit
|
||||||
|
(TI2I) model. The weight is read lazily so this survives later ``.to()`` moves.
|
||||||
|
Returns the number of patch_embed modules patched. (Vendored from
|
||||||
|
extensions_built_in/captioner/Qwen3VLCaptioner.py.)
|
||||||
|
"""
|
||||||
|
patched = 0
|
||||||
|
for module in model.modules():
|
||||||
|
proj = getattr(module, "proj", None)
|
||||||
|
if isinstance(proj, torch.nn.Conv3d) and tuple(proj.kernel_size) == tuple(
|
||||||
|
proj.stride
|
||||||
|
):
|
||||||
|
|
||||||
|
def fast_forward(hidden_states, _proj=proj):
|
||||||
|
w = _proj.weight.reshape(_proj.weight.shape[0], -1)
|
||||||
|
x = hidden_states.view(-1, w.shape[1]).to(w.dtype)
|
||||||
|
return F.linear(x, w, _proj.bias)
|
||||||
|
|
||||||
|
module.forward = fast_forward
|
||||||
|
patched += 1
|
||||||
|
return patched
|
||||||
|
|
||||||
|
|
||||||
|
class BooguImageModel(BaseModel):
|
||||||
|
arch = "boogu_image"
|
||||||
|
# Default HF repo when model.name_or_path is unset (overridden by the edit model).
|
||||||
|
default_repo = BOOGU_BASE_PATH
|
||||||
|
use_old_lokr_format = False
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
device,
|
||||||
|
model_config: ModelConfig,
|
||||||
|
dtype="bf16",
|
||||||
|
custom_pipeline=None,
|
||||||
|
noise_scheduler=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
device, model_config, dtype, custom_pipeline, noise_scheduler, **kwargs
|
||||||
|
)
|
||||||
|
self.is_flow_matching = True
|
||||||
|
self.is_transformer = True
|
||||||
|
self.target_lora_modules = ["BooguImageTransformer2DModel"]
|
||||||
|
|
||||||
|
self.patch_size = 2
|
||||||
|
self.vae_scale_factor = 8
|
||||||
|
# Safety cap on instruction token length (truncation only). Each caption is
|
||||||
|
# encoded at its natural length and padded to the batch max at the model
|
||||||
|
# call, so this is just an upper bound.
|
||||||
|
self.max_text_length = int(
|
||||||
|
self.model_config.model_kwargs.get("max_text_length", 1024)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Lazily-built, resolution-independent rotary frequency tables.
|
||||||
|
self._freqs_cis = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text_embedding_space_version(self):
|
||||||
|
return self.arch + "_v1"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_train_scheduler():
|
||||||
|
return CustomFlowMatchEulerDiscreteScheduler(**scheduler_config)
|
||||||
|
|
||||||
|
def get_bucket_divisibility(self):
|
||||||
|
# 8 for the VAE downsample, 2 for the patch size.
|
||||||
|
return self.vae_scale_factor * self.patch_size
|
||||||
|
|
||||||
|
def get_freqs_cis(self):
|
||||||
|
"""Precompute (once) the per-axis rotary frequency tables for the model."""
|
||||||
|
if self._freqs_cis is None:
|
||||||
|
cfg = unwrap_model(self.model).config
|
||||||
|
self._freqs_cis = get_freqs_cis(
|
||||||
|
cfg.axes_dim_rope, cfg.axes_lens, theta=10000
|
||||||
|
)
|
||||||
|
return self._freqs_cis
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Loading
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def load_model(self):
|
||||||
|
dtype = self.torch_dtype
|
||||||
|
self.print_and_status_update("Loading Boogu-Image model")
|
||||||
|
base = self.model_config.name_or_path or self.default_repo
|
||||||
|
|
||||||
|
# --- transformer ---
|
||||||
|
# Loads the bf16 release (clean safetensors). The "-fp8" sibling ships
|
||||||
|
# torchao float8 .bin weights that need a matching torchao/cache_dit to
|
||||||
|
# deserialize -- use the bf16 repo and let ai-toolkit quantize if wanted.
|
||||||
|
self.print_and_status_update("Loading transformer")
|
||||||
|
try:
|
||||||
|
transformer = BooguImageTransformer2DModel.from_pretrained(
|
||||||
|
base, subfolder="transformer", torch_dtype=dtype, token=HF_TOKEN
|
||||||
|
)
|
||||||
|
except OSError as e:
|
||||||
|
raise OSError(
|
||||||
|
f"Could not load Boogu transformer safetensors from '{base}'. The "
|
||||||
|
f"'-fp8' release ships torchao float8 .bin weights, which are not "
|
||||||
|
f"supported here -- point model.name_or_path at the bf16 repo "
|
||||||
|
f"'{BOOGU_BASE_PATH}' instead."
|
||||||
|
) from e
|
||||||
|
transformer.eval()
|
||||||
|
flush()
|
||||||
|
|
||||||
|
# Attention defaults to torch SDPA ("native"); opt into Flash Attention 2
|
||||||
|
# with model_kwargs.attention_backend: "flash" (needs the flash_attn pkg).
|
||||||
|
attention_backend = self.model_config.model_kwargs.get(
|
||||||
|
"attention_backend", "native"
|
||||||
|
)
|
||||||
|
if attention_backend != "native":
|
||||||
|
transformer.set_attention_backend(attention_backend)
|
||||||
|
|
||||||
|
if self.model_config.quantize:
|
||||||
|
self.print_and_status_update("Quantizing transformer")
|
||||||
|
quantize_model(self, transformer)
|
||||||
|
flush()
|
||||||
|
|
||||||
|
if self.model_config.low_vram:
|
||||||
|
self.print_and_status_update("Moving transformer to CPU")
|
||||||
|
transformer.to("cpu")
|
||||||
|
else:
|
||||||
|
transformer.to(self.device_torch, dtype=dtype)
|
||||||
|
flush()
|
||||||
|
|
||||||
|
# --- instruction encoder (Qwen3-VL) + processor ---
|
||||||
|
te_path = self.model_config.model_kwargs.get("text_encoder_path", base)
|
||||||
|
te_subfolder = self.model_config.model_kwargs.get(
|
||||||
|
"text_encoder_subfolder", "mllm"
|
||||||
|
)
|
||||||
|
self.print_and_status_update("Loading Qwen3-VL instruction encoder")
|
||||||
|
processor = AutoProcessor.from_pretrained(
|
||||||
|
te_path, subfolder="processor", token=HF_TOKEN
|
||||||
|
)
|
||||||
|
# AutoModel yields the inner Qwen3VLModel (the ``.model`` of the
|
||||||
|
# *ForConditionalGeneration), whose last_hidden_state is exactly the
|
||||||
|
# instruction feature the Boogu pipeline consumes.
|
||||||
|
text_encoder = AutoModel.from_pretrained(
|
||||||
|
te_path, subfolder=te_subfolder, torch_dtype=dtype, token=HF_TOKEN
|
||||||
|
)
|
||||||
|
text_encoder.eval()
|
||||||
|
text_encoder.requires_grad_(False)
|
||||||
|
# The vision tower's bf16 Conv3d patch_embed has no fast kernel and stalls
|
||||||
|
# image caching for the edit model -- swap it for an equivalent F.linear.
|
||||||
|
# No-op for the base T2I model (it never runs the vision tower).
|
||||||
|
n_patched = patch_qwen_vl_patch_embed(text_encoder)
|
||||||
|
if n_patched:
|
||||||
|
self.print_and_status_update(
|
||||||
|
f" - patched {n_patched} Qwen-VL Conv3d patch_embed -> linear"
|
||||||
|
)
|
||||||
|
flush()
|
||||||
|
|
||||||
|
if self.model_config.quantize_te:
|
||||||
|
self.print_and_status_update("Quantizing instruction encoder")
|
||||||
|
text_encoder.to(self.device_torch)
|
||||||
|
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
|
||||||
|
freeze(text_encoder)
|
||||||
|
flush()
|
||||||
|
|
||||||
|
if self.model_config.low_vram:
|
||||||
|
self.print_and_status_update("Moving instruction encoder to CPU")
|
||||||
|
text_encoder.to("cpu")
|
||||||
|
else:
|
||||||
|
text_encoder.to(self.device_torch)
|
||||||
|
flush()
|
||||||
|
|
||||||
|
# --- VAE (FLUX AutoencoderKL) ---
|
||||||
|
self.print_and_status_update("Loading VAE")
|
||||||
|
vae = AutoencoderKL.from_pretrained(
|
||||||
|
base, subfolder="vae", torch_dtype=self.vae_torch_dtype, token=HF_TOKEN
|
||||||
|
)
|
||||||
|
vae.to(self.vae_device_torch, dtype=self.vae_torch_dtype)
|
||||||
|
vae.eval()
|
||||||
|
vae.requires_grad_(False)
|
||||||
|
flush()
|
||||||
|
|
||||||
|
self.noise_scheduler = BooguImageModel.get_train_scheduler()
|
||||||
|
self.vae = vae
|
||||||
|
self.text_encoder = text_encoder
|
||||||
|
self.tokenizer = processor
|
||||||
|
self.model = transformer
|
||||||
|
self.pipeline = BooguImagePipeline(self)
|
||||||
|
self.print_and_status_update("Model Loaded")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Generation
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def get_generation_pipeline(self):
|
||||||
|
return BooguImagePipeline(self)
|
||||||
|
|
||||||
|
def generate_single_image(
|
||||||
|
self,
|
||||||
|
pipeline: BooguImagePipeline,
|
||||||
|
gen_config: GenerateImageConfig,
|
||||||
|
conditional_embeds: AdvancedPromptEmbeds,
|
||||||
|
unconditional_embeds: AdvancedPromptEmbeds,
|
||||||
|
generator: torch.Generator,
|
||||||
|
extra: dict,
|
||||||
|
):
|
||||||
|
if self.model.device == torch.device("cpu"):
|
||||||
|
self.model.to(self.device_torch)
|
||||||
|
|
||||||
|
sc = self.get_bucket_divisibility()
|
||||||
|
gen_config.width = int(gen_config.width // sc * sc)
|
||||||
|
gen_config.height = int(gen_config.height // sc * sc)
|
||||||
|
|
||||||
|
img = pipeline(
|
||||||
|
conditional_embeds=conditional_embeds,
|
||||||
|
unconditional_embeds=unconditional_embeds,
|
||||||
|
height=gen_config.height,
|
||||||
|
width=gen_config.width,
|
||||||
|
num_inference_steps=gen_config.num_inference_steps,
|
||||||
|
guidance_scale=gen_config.guidance_scale,
|
||||||
|
latents=gen_config.latents,
|
||||||
|
generator=generator,
|
||||||
|
)[0]
|
||||||
|
return img
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Training hooks
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def get_noise_prediction(
|
||||||
|
self,
|
||||||
|
latent_model_input: torch.Tensor, # (B, 16, h, w)
|
||||||
|
timestep: torch.Tensor, # 0..1000 scale (1000 = pure noise)
|
||||||
|
text_embeddings: AdvancedPromptEmbeds,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
if self.model.device == torch.device("cpu"):
|
||||||
|
self.model.to(self.device_torch)
|
||||||
|
|
||||||
|
# toolkit timestep (0..1000, 1000=noise) -> Boogu native time (0=noise, 1=clean)
|
||||||
|
t01 = timestep.to(self.device_torch, dtype=torch.float32) / 1000.0
|
||||||
|
if t01.dim() == 0:
|
||||||
|
t01 = t01.unsqueeze(0)
|
||||||
|
if t01.shape[0] != latent_model_input.shape[0]:
|
||||||
|
t01 = t01.expand(latent_model_input.shape[0])
|
||||||
|
boogu_t = 1.0 - t01
|
||||||
|
|
||||||
|
instr_feats, instr_mask = pad_instruction_features(
|
||||||
|
text_embeddings.text_embeds, self.device_torch, self.torch_dtype
|
||||||
|
)
|
||||||
|
|
||||||
|
# Model predicts clean - noise; negate to return the toolkit velocity
|
||||||
|
# (noise - clean), matching get_loss_target / the scheduler.
|
||||||
|
raw_velocity = run_boogu_transformer(
|
||||||
|
self.transformer,
|
||||||
|
latent_model_input.to(self.device_torch, self.torch_dtype),
|
||||||
|
boogu_t,
|
||||||
|
instr_feats,
|
||||||
|
instr_mask,
|
||||||
|
self.get_freqs_cis(),
|
||||||
|
)
|
||||||
|
return -raw_velocity
|
||||||
|
|
||||||
|
def get_prompt_embeds(self, prompt) -> AdvancedPromptEmbeds:
|
||||||
|
if isinstance(prompt, str):
|
||||||
|
prompt = [prompt]
|
||||||
|
|
||||||
|
if self.text_encoder.device == torch.device("cpu"):
|
||||||
|
self.text_encoder.to(self.device_torch)
|
||||||
|
device = self.text_encoder.device
|
||||||
|
|
||||||
|
# Encode each instruction at its natural length (no cross-sample padding);
|
||||||
|
# padding to a common length is deferred to the model call. The system
|
||||||
|
# prompt + chat template match the base T2I training setup.
|
||||||
|
features_list = []
|
||||||
|
for p in prompt:
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": [{"type": "text", "text": SYSTEM_PROMPT_T2I}],
|
||||||
|
},
|
||||||
|
{"role": "user", "content": [{"type": "text", "text": p}]},
|
||||||
|
]
|
||||||
|
inputs = self.tokenizer.apply_chat_template(
|
||||||
|
[messages],
|
||||||
|
tokenize=True,
|
||||||
|
return_dict=True,
|
||||||
|
return_tensors="pt",
|
||||||
|
add_generation_prompt=False,
|
||||||
|
truncation=True,
|
||||||
|
max_length=self.max_text_length,
|
||||||
|
)
|
||||||
|
input_ids = inputs["input_ids"].to(device)
|
||||||
|
attention_mask = inputs["attention_mask"].to(device)
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
output = self.text_encoder(
|
||||||
|
input_ids=input_ids, attention_mask=attention_mask
|
||||||
|
)
|
||||||
|
# (L, D) -- drop the batch dim, one tensor per prompt
|
||||||
|
features_list.append(output.last_hidden_state[0].to(self.torch_dtype))
|
||||||
|
|
||||||
|
return AdvancedPromptEmbeds(text_embeds=features_list)
|
||||||
|
|
||||||
|
def get_loss_target(self, *args, **kwargs):
|
||||||
|
noise = kwargs.get("noise")
|
||||||
|
batch = kwargs.get("batch")
|
||||||
|
return (noise - batch.latents).detach()
|
||||||
|
|
||||||
|
def get_model_has_grad(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_te_has_grad(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# VAE
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def encode_images(self, image_list: List[torch.Tensor], device=None, dtype=None):
|
||||||
|
if device is None:
|
||||||
|
device = self.vae_device_torch
|
||||||
|
if dtype is None:
|
||||||
|
dtype = self.vae_torch_dtype
|
||||||
|
if self.vae.device == torch.device("cpu"):
|
||||||
|
self.vae.to(self.vae_device_torch)
|
||||||
|
|
||||||
|
if isinstance(image_list, list):
|
||||||
|
images = torch.stack(image_list, dim=0)
|
||||||
|
else:
|
||||||
|
images = image_list
|
||||||
|
images = images.to(device, dtype=dtype)
|
||||||
|
|
||||||
|
latents = self.vae.encode(images).latent_dist.sample()
|
||||||
|
shift = self.vae.config["shift_factor"] or 0
|
||||||
|
latents = (latents - shift) * self.vae.config["scaling_factor"]
|
||||||
|
return latents.to(device, dtype=dtype)
|
||||||
|
|
||||||
|
def decode_latents(self, latents: torch.Tensor, device=None, dtype=None):
|
||||||
|
if device is None:
|
||||||
|
device = self.vae_device_torch
|
||||||
|
if dtype is None:
|
||||||
|
dtype = self.vae_torch_dtype
|
||||||
|
if self.vae.device == torch.device("cpu"):
|
||||||
|
self.vae.to(self.vae_device_torch)
|
||||||
|
|
||||||
|
latents = latents.to(device, dtype=dtype)
|
||||||
|
shift = self.vae.config["shift_factor"] or 0
|
||||||
|
latents = latents / self.vae.config["scaling_factor"] + shift
|
||||||
|
return self.vae.decode(latents).sample
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Saving / misc
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def save_model(self, output_path, meta, save_dtype):
|
||||||
|
transformer: BooguImageTransformer2DModel = unwrap_model(self.model)
|
||||||
|
transformer_dir = os.path.join(output_path, "transformer")
|
||||||
|
os.makedirs(transformer_dir, exist_ok=True)
|
||||||
|
|
||||||
|
state_dict = transformer.state_dict()
|
||||||
|
save_dict = {}
|
||||||
|
for k, v in state_dict.items():
|
||||||
|
if isinstance(v, QTensor):
|
||||||
|
v = v.dequantize()
|
||||||
|
save_dict[k] = v.clone().to("cpu", dtype=save_dtype)
|
||||||
|
save_file(
|
||||||
|
save_dict,
|
||||||
|
os.path.join(transformer_dir, "diffusion_pytorch_model.safetensors"),
|
||||||
|
)
|
||||||
|
# config.json so the saved transformer can be reloaded with from_pretrained.
|
||||||
|
transformer.save_config(transformer_dir)
|
||||||
|
with open(os.path.join(output_path, "aitk_meta.yaml"), "w") as f:
|
||||||
|
yaml.dump(meta, f)
|
||||||
|
|
||||||
|
def get_base_model_version(self):
|
||||||
|
return "boogu_image.0.1"
|
||||||
|
|
||||||
|
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||||
|
return ["double_stream_layers", "single_stream_layers"]
|
||||||
|
|
||||||
|
def convert_lora_weights_before_save(self, state_dict):
|
||||||
|
return {
|
||||||
|
k.replace("transformer.", "diffusion_model."): v
|
||||||
|
for k, v in state_dict.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def convert_lora_weights_before_load(self, state_dict):
|
||||||
|
return {
|
||||||
|
k.replace("diffusion_model.", "transformer."): v
|
||||||
|
for k, v in state_dict.items()
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
"""Boogu-Image edit (TI2I) integration for ai-toolkit.
|
||||||
|
|
||||||
|
The edit model is the same Lumina2-style transformer + Qwen3-VL encoder as the
|
||||||
|
base T2I model, with reference-image conditioning. A reference image feeds the
|
||||||
|
model in TWO places:
|
||||||
|
|
||||||
|
1. Into the Qwen3-VL instruction encoder as image content alongside the edit
|
||||||
|
instruction (so the *text embeddings* already encode the reference image).
|
||||||
|
This is why ``encode_control_in_text_embeddings = True``.
|
||||||
|
2. Into the transformer as reference-image VAE latents
|
||||||
|
(``ref_image_hidden_states``), which the ref-image refiner + double-stream
|
||||||
|
blocks attend to.
|
||||||
|
|
||||||
|
Everything else (transformer, VAE, scheduler, time/velocity convention, saving)
|
||||||
|
is inherited from ``BooguImageModel`` -- this file only overrides the pieces
|
||||||
|
that change for TI2I.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.nn.functional as F
|
||||||
|
from PIL import Image
|
||||||
|
from torchvision.transforms.functional import to_tensor
|
||||||
|
|
||||||
|
from toolkit.advanced_prompt_embeds import AdvancedPromptEmbeds
|
||||||
|
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||||
|
|
||||||
|
from .boogu_image import BooguImageModel
|
||||||
|
from .src.pipeline import (
|
||||||
|
BooguImagePipeline,
|
||||||
|
pad_instruction_features,
|
||||||
|
run_boogu_transformer,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from toolkit.data_transfer_object.data_loader import DataLoaderBatchDTO
|
||||||
|
|
||||||
|
|
||||||
|
# Edit release (clean bf16 safetensors); same layout as the base repo.
|
||||||
|
BOOGU_EDIT_PATH = "Boogu/Boogu-Image-0.1-Edit"
|
||||||
|
|
||||||
|
# System prompt the edit model was trained with (SYSTEM_PROMPT_4_TI2I upstream).
|
||||||
|
SYSTEM_PROMPT_TI2I = (
|
||||||
|
"Describe the key features of the input image (color, shape, size, texture, "
|
||||||
|
"objects, background), then explain how the user's text instruction should "
|
||||||
|
"alter or modify the image. Generate a new image that meets the user's "
|
||||||
|
"requirements while maintaining consistency with the original input where "
|
||||||
|
"appropriate."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BooguImageEditModel(BooguImageModel):
|
||||||
|
arch = "boogu_image_edit"
|
||||||
|
default_repo = BOOGU_EDIT_PATH
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
device,
|
||||||
|
model_config: ModelConfig,
|
||||||
|
dtype="bf16",
|
||||||
|
custom_pipeline=None,
|
||||||
|
noise_scheduler=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
device, model_config, dtype, custom_pipeline, noise_scheduler, **kwargs
|
||||||
|
)
|
||||||
|
# The reference image is encoded into the Qwen3-VL instruction features,
|
||||||
|
# so get_prompt_embeds receives the control image(s).
|
||||||
|
self.encode_control_in_text_embeddings = True
|
||||||
|
# Boogu supports up to 5 reference images -> they arrive as a list.
|
||||||
|
self.has_multiple_control_images = True
|
||||||
|
# Reference images keep their own aspect/size (not resized to the target).
|
||||||
|
self.use_raw_control_images = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text_embedding_space_version(self):
|
||||||
|
# Distinct from the base T2I cache: the edit features fold in the ref image.
|
||||||
|
return self.arch + "_v1"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Reference-image helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _vlm_resize_hw(self, h, w, max_pixels, max_side, factor=16):
|
||||||
|
"""Boogu's VLM image downscale (BooguImageProcessor.get_new_height_width).
|
||||||
|
|
||||||
|
Scale down (never up) to fit BOTH ``max_pixels`` (area) and
|
||||||
|
``max_side_length``, then round each dim down to a multiple of ``factor``
|
||||||
|
(the image processor's ``vae_scale_factor`` = 16 for this model). The Qwen
|
||||||
|
processor's own smart_resize runs afterwards, exactly as upstream.
|
||||||
|
"""
|
||||||
|
longest = h if h > w else w
|
||||||
|
ratio_side = max_side / longest
|
||||||
|
ratio_pixels = (max_pixels / (h * w)) ** 0.5
|
||||||
|
ratio = min(ratio_pixels, ratio_side, 1.0)
|
||||||
|
nh = max(factor, int(h * ratio) // factor * factor)
|
||||||
|
nw = max(factor, int(w * ratio) // factor * factor)
|
||||||
|
return nh, nw
|
||||||
|
|
||||||
|
def _ref_target_pixels(self, target_pixels: Optional[int]) -> int:
|
||||||
|
"""Decide the pixel budget each reference image is resized to fit within.
|
||||||
|
|
||||||
|
- default: ``control_image_max_pixels`` model_kwarg (1 MP) -- a hard cap so
|
||||||
|
raw, full-size control images don't blow up the token count / VRAM.
|
||||||
|
- ``match_target_res`` model_kwarg: use the target generation area instead,
|
||||||
|
matching Boogu's recommendation of ``max_input_image_pixels ~= H*W``.
|
||||||
|
"""
|
||||||
|
max_pixels = int(
|
||||||
|
self.model_config.model_kwargs.get("control_image_max_pixels", 1024 * 1024)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
self.model_config.model_kwargs.get("match_target_res", False)
|
||||||
|
and target_pixels
|
||||||
|
):
|
||||||
|
return int(target_pixels)
|
||||||
|
return max_pixels
|
||||||
|
|
||||||
|
def _encode_ref_latents(
|
||||||
|
self, control_tensors, target_pixels: Optional[int] = None
|
||||||
|
) -> List[torch.Tensor]:
|
||||||
|
"""Encode ``[0, 1]`` reference image tensors to VAE latents.
|
||||||
|
|
||||||
|
Returns a list of ``(16, h, w)`` latents (one per reference image). Each
|
||||||
|
control image is resized so its area fits within the pixel budget (see
|
||||||
|
``_ref_target_pixels``) -- preserving aspect ratio -- then snapped so the
|
||||||
|
latent grid is divisible by the patch size. ``control_tensors`` is a list
|
||||||
|
of ``(C, H, W)`` or ``(1, C, H, W)`` tensors in ``[0, 1]``.
|
||||||
|
"""
|
||||||
|
sc = self.get_bucket_divisibility() # 16: VAE(8) * patch(2)
|
||||||
|
budget = self._ref_target_pixels(target_pixels)
|
||||||
|
match = self.model_config.model_kwargs.get("match_target_res", False)
|
||||||
|
|
||||||
|
latents = []
|
||||||
|
for img in control_tensors:
|
||||||
|
if img.dim() == 3:
|
||||||
|
img = img.unsqueeze(0)
|
||||||
|
img = img.to(self.device_torch, dtype=self.torch_dtype)
|
||||||
|
|
||||||
|
h, w = img.shape[2], img.shape[3]
|
||||||
|
# match_target_res: scale area *to* the budget; otherwise only scale
|
||||||
|
# *down* when the image is larger than the budget.
|
||||||
|
area = h * w
|
||||||
|
if match or area > budget:
|
||||||
|
ratio = h / w
|
||||||
|
new_h = math.sqrt(budget * ratio)
|
||||||
|
new_w = new_h / ratio
|
||||||
|
else:
|
||||||
|
new_h, new_w = float(h), float(w)
|
||||||
|
|
||||||
|
# snap to a multiple of the bucket divisibility so the VAE latent grid
|
||||||
|
# is patchifiable (the transformer rearranges 2x2 latent patches).
|
||||||
|
new_h = max(sc, int(round(new_h / sc)) * sc)
|
||||||
|
new_w = max(sc, int(round(new_w / sc)) * sc)
|
||||||
|
if (new_h, new_w) != (h, w):
|
||||||
|
img = F.interpolate(img, size=(new_h, new_w), mode="bilinear")
|
||||||
|
|
||||||
|
# encode_images expects [-1, 1]; control tensors arrive in [0, 1].
|
||||||
|
latent = self.encode_images(
|
||||||
|
img * 2 - 1, device=self.device_torch, dtype=self.torch_dtype
|
||||||
|
)
|
||||||
|
latents.append(latent[0]) # drop batch dim -> (16, h, w)
|
||||||
|
return latents
|
||||||
|
|
||||||
|
def _batch_ref_latents_from_batch(
|
||||||
|
self,
|
||||||
|
batch: "DataLoaderBatchDTO",
|
||||||
|
batch_size: int,
|
||||||
|
target_pixels: Optional[int] = None,
|
||||||
|
) -> Optional[List[List[torch.Tensor]]]:
|
||||||
|
"""Build the transformer's ``ref_image_hidden_states`` from a train batch."""
|
||||||
|
control_list = batch.control_tensor_list
|
||||||
|
if control_list is None and batch.control_tensor is not None:
|
||||||
|
control_list = [batch.control_tensor[b : b + 1] for b in range(batch_size)]
|
||||||
|
if control_list is None:
|
||||||
|
return None
|
||||||
|
if len(control_list) != batch_size:
|
||||||
|
raise ValueError("Control tensor list length does not match batch size")
|
||||||
|
return [
|
||||||
|
self._encode_ref_latents(controls, target_pixels=target_pixels)
|
||||||
|
for controls in control_list
|
||||||
|
]
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Conditioning
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def get_prompt_embeds(self, prompt, control_images=None) -> AdvancedPromptEmbeds:
|
||||||
|
if isinstance(prompt, str):
|
||||||
|
prompt = [prompt]
|
||||||
|
|
||||||
|
if control_images is None:
|
||||||
|
raise ValueError("BooguImageEditModel requires control (reference) images")
|
||||||
|
|
||||||
|
# Normalize to List[List[Tensor]] (per-prompt list of reference images), the
|
||||||
|
# same convention qwen_image_edit_plus uses.
|
||||||
|
if not isinstance(control_images, list):
|
||||||
|
control_images = [control_images]
|
||||||
|
if not isinstance(control_images[0], list):
|
||||||
|
control_images = [control_images]
|
||||||
|
if len(prompt) != len(control_images):
|
||||||
|
raise ValueError(
|
||||||
|
"Number of prompts must match number of control image sets"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.text_encoder.device == torch.device("cpu"):
|
||||||
|
self.text_encoder.to(self.device_torch)
|
||||||
|
device = self.text_encoder.device
|
||||||
|
|
||||||
|
features_list = []
|
||||||
|
for p, ctrl in zip(prompt, control_images):
|
||||||
|
# Keep reference images as tensors the whole way (no GPU->CPU->PIL
|
||||||
|
# round-trip). Match Boogu's VLM preprocessing: downscale each control
|
||||||
|
# image to fit max_pixels (384^2) AND max_side_length (768) -- the MLLM
|
||||||
|
# only needs a coarse understanding of the reference (high-res detail
|
||||||
|
# flows through the VAE ref latents), and this keeps the instruction
|
||||||
|
# sequence well under the transformer rope axes_lens (~144 tokens/ref).
|
||||||
|
max_pixels = int(
|
||||||
|
self.model_config.model_kwargs.get("vlm_max_pixels", 384 * 384)
|
||||||
|
)
|
||||||
|
max_side = int(
|
||||||
|
self.model_config.model_kwargs.get("vlm_max_side_length", 768)
|
||||||
|
)
|
||||||
|
images = []
|
||||||
|
for img in ctrl:
|
||||||
|
if img.dim() == 4:
|
||||||
|
img = img[0]
|
||||||
|
img = img.to(device)
|
||||||
|
nh, nw = self._vlm_resize_hw(
|
||||||
|
img.shape[1], img.shape[2], max_pixels, max_side
|
||||||
|
)
|
||||||
|
if (nh, nw) != (img.shape[1], img.shape[2]):
|
||||||
|
img = (
|
||||||
|
F.interpolate(
|
||||||
|
img.unsqueeze(0),
|
||||||
|
size=(nh, nw),
|
||||||
|
mode="bicubic",
|
||||||
|
antialias=True,
|
||||||
|
)
|
||||||
|
.squeeze(0)
|
||||||
|
.clamp(0, 1)
|
||||||
|
)
|
||||||
|
images.append(img)
|
||||||
|
|
||||||
|
# Build just the text template with image placeholders (tokenize=False),
|
||||||
|
# then let the processor expand the image tokens from the real grid size.
|
||||||
|
user_content = [{"type": "image"} for _ in images]
|
||||||
|
user_content.append({"type": "text", "text": p})
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": [{"type": "text", "text": SYSTEM_PROMPT_TI2I}],
|
||||||
|
},
|
||||||
|
{"role": "user", "content": user_content},
|
||||||
|
]
|
||||||
|
text = self.tokenizer.apply_chat_template(
|
||||||
|
messages, tokenize=False, add_generation_prompt=False
|
||||||
|
)
|
||||||
|
# do_rescale=False: control tensors are already [0, 1] (the image
|
||||||
|
# normalizer maps them to [-1, 1]). No size override -- the images are
|
||||||
|
# already at Boogu's target size, the processor just snaps to its grid.
|
||||||
|
inputs = self.tokenizer(
|
||||||
|
text=[text],
|
||||||
|
images=images,
|
||||||
|
return_tensors="pt",
|
||||||
|
do_rescale=False,
|
||||||
|
)
|
||||||
|
model_inputs = {}
|
||||||
|
for k, v in inputs.items():
|
||||||
|
if isinstance(v, torch.Tensor):
|
||||||
|
v = v.to(device)
|
||||||
|
# cast image pixels to the encoder dtype; leave ids/masks as ints
|
||||||
|
if v.is_floating_point():
|
||||||
|
v = v.to(self.torch_dtype)
|
||||||
|
model_inputs[k] = v
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
output = self.text_encoder(**model_inputs)
|
||||||
|
features_list.append(output.last_hidden_state[0].to(self.torch_dtype))
|
||||||
|
|
||||||
|
return AdvancedPromptEmbeds(text_embeds=features_list)
|
||||||
|
|
||||||
|
def get_noise_prediction(
|
||||||
|
self,
|
||||||
|
latent_model_input: torch.Tensor, # (B, 16, h, w)
|
||||||
|
timestep: torch.Tensor, # 0..1000 scale (1000 = pure noise)
|
||||||
|
text_embeddings: AdvancedPromptEmbeds,
|
||||||
|
batch: "DataLoaderBatchDTO" = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
if self.model.device == torch.device("cpu"):
|
||||||
|
self.model.to(self.device_torch)
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
# target pixel area from the noise latents (h, w are VAE-downsampled)
|
||||||
|
_, _, lh, lw = latent_model_input.shape
|
||||||
|
target_pixels = (lh * self.vae_scale_factor) * (lw * self.vae_scale_factor)
|
||||||
|
ref_latents = (
|
||||||
|
self._batch_ref_latents_from_batch(
|
||||||
|
batch, latent_model_input.shape[0], target_pixels=target_pixels
|
||||||
|
)
|
||||||
|
if batch is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# toolkit timestep (0..1000, 1000=noise) -> Boogu native time (0=noise, 1=clean)
|
||||||
|
t01 = timestep.to(self.device_torch, dtype=torch.float32) / 1000.0
|
||||||
|
if t01.dim() == 0:
|
||||||
|
t01 = t01.unsqueeze(0)
|
||||||
|
if t01.shape[0] != latent_model_input.shape[0]:
|
||||||
|
t01 = t01.expand(latent_model_input.shape[0])
|
||||||
|
boogu_t = 1.0 - t01
|
||||||
|
|
||||||
|
instr_feats, instr_mask = pad_instruction_features(
|
||||||
|
text_embeddings.text_embeds, self.device_torch, self.torch_dtype
|
||||||
|
)
|
||||||
|
|
||||||
|
# Model predicts clean - noise; negate to return the toolkit velocity.
|
||||||
|
raw_velocity = run_boogu_transformer(
|
||||||
|
self.transformer,
|
||||||
|
latent_model_input.to(self.device_torch, self.torch_dtype),
|
||||||
|
boogu_t,
|
||||||
|
instr_feats,
|
||||||
|
instr_mask,
|
||||||
|
self.get_freqs_cis(),
|
||||||
|
ref_image_hidden_states=ref_latents,
|
||||||
|
)
|
||||||
|
return -raw_velocity
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Sampling previews
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def generate_single_image(
|
||||||
|
self,
|
||||||
|
pipeline: BooguImagePipeline,
|
||||||
|
gen_config: GenerateImageConfig,
|
||||||
|
conditional_embeds: AdvancedPromptEmbeds,
|
||||||
|
unconditional_embeds: AdvancedPromptEmbeds,
|
||||||
|
generator: torch.Generator,
|
||||||
|
extra: dict,
|
||||||
|
):
|
||||||
|
if self.model.device == torch.device("cpu"):
|
||||||
|
self.model.to(self.device_torch)
|
||||||
|
|
||||||
|
sc = self.get_bucket_divisibility()
|
||||||
|
gen_config.width = int(gen_config.width // sc * sc)
|
||||||
|
gen_config.height = int(gen_config.height // sc * sc)
|
||||||
|
|
||||||
|
# Load the reference image(s) for the transformer ref latents. The MLLM
|
||||||
|
# side already saw them (baked into conditional/unconditional embeds).
|
||||||
|
ctrl_paths = [
|
||||||
|
p
|
||||||
|
for p in (
|
||||||
|
gen_config.ctrl_img,
|
||||||
|
gen_config.ctrl_img_1,
|
||||||
|
gen_config.ctrl_img_2,
|
||||||
|
gen_config.ctrl_img_3,
|
||||||
|
)
|
||||||
|
if p is not None
|
||||||
|
]
|
||||||
|
ref_latents = None
|
||||||
|
if ctrl_paths:
|
||||||
|
ctrl_tensors = [
|
||||||
|
to_tensor(Image.open(path).convert("RGB")) for path in ctrl_paths
|
||||||
|
]
|
||||||
|
target_pixels = gen_config.width * gen_config.height
|
||||||
|
# one batch item (preview batch size is 1) -> List[List[(16, h, w)]]
|
||||||
|
ref_latents = [
|
||||||
|
self._encode_ref_latents(ctrl_tensors, target_pixels=target_pixels)
|
||||||
|
]
|
||||||
|
|
||||||
|
img = pipeline(
|
||||||
|
conditional_embeds=conditional_embeds,
|
||||||
|
unconditional_embeds=unconditional_embeds,
|
||||||
|
height=gen_config.height,
|
||||||
|
width=gen_config.width,
|
||||||
|
num_inference_steps=gen_config.num_inference_steps,
|
||||||
|
guidance_scale=gen_config.guidance_scale,
|
||||||
|
latents=gen_config.latents,
|
||||||
|
generator=generator,
|
||||||
|
ref_latents=ref_latents,
|
||||||
|
)[0]
|
||||||
|
return img
|
||||||
|
|
||||||
|
def get_base_model_version(self):
|
||||||
|
return "boogu_image_edit.0.1"
|
||||||
@@ -0,0 +1,491 @@
|
|||||||
|
# Vendored from the Boogu-Image repository (boogu/models/attention_processor.py).
|
||||||
|
# Original work: Copyright 2025 BAAI / OmniGen2 / HuggingFace. Apache-2.0.
|
||||||
|
#
|
||||||
|
# Attention here defaults to torch's ``scaled_dot_product_attention`` (the
|
||||||
|
# "native" backend) so the model has NO hard dependency on flash-attn. Flash
|
||||||
|
# Attention 2 is an OPTIONAL backend: each processor carries an
|
||||||
|
# ``attention_backend`` flag (set in bulk via
|
||||||
|
# ``BooguImageTransformer2DModel.set_attention_backend``) and only the "flash"
|
||||||
|
# branch touches the ``flash_attn`` package, so importing it stays lazy/guarded.
|
||||||
|
import math
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
import torch.nn.functional as F
|
||||||
|
from diffusers.models.attention_processor import Attention
|
||||||
|
from einops import repeat
|
||||||
|
|
||||||
|
from .embeddings import apply_rotary_emb
|
||||||
|
|
||||||
|
try:
|
||||||
|
from flash_attn import flash_attn_varlen_func
|
||||||
|
from flash_attn.bert_padding import index_first_axis, pad_input, unpad_input
|
||||||
|
|
||||||
|
_FLASH_ATTN_AVAILABLE = True
|
||||||
|
except ImportError: # flash-attn is optional; "native" SDPA needs none of this.
|
||||||
|
flash_attn_varlen_func = None
|
||||||
|
index_first_axis = pad_input = unpad_input = None
|
||||||
|
_FLASH_ATTN_AVAILABLE = False
|
||||||
|
|
||||||
|
# Supported attention backends. "native" -> SDPA, "flash" -> Flash Attention 2.
|
||||||
|
ATTENTION_BACKENDS = ("native", "flash")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_unpad_data(mask_2d: torch.Tensor):
|
||||||
|
"""Indices / cu_seqlens / max_seqlen from a 2D padding mask [B, L]."""
|
||||||
|
seqlens_in_batch = mask_2d.sum(dim=-1, dtype=torch.int32)
|
||||||
|
indices = torch.nonzero(mask_2d.flatten(), as_tuple=False).flatten()
|
||||||
|
max_seqlen_in_batch = seqlens_in_batch.max().item()
|
||||||
|
cu_seqlens = F.pad(torch.cumsum(seqlens_in_batch, dim=0, dtype=torch.int32), (1, 0))
|
||||||
|
return indices, cu_seqlens, max_seqlen_in_batch
|
||||||
|
|
||||||
|
|
||||||
|
def _upad_input(query, key, value, attention_mask, query_length, num_heads):
|
||||||
|
"""Unpad q/k/v for ``flash_attn_varlen_func`` given a [B, L] padding mask."""
|
||||||
|
indices_k, cu_seqlens_k, max_seqlen_in_batch_k = _get_unpad_data(attention_mask)
|
||||||
|
batch_size, kv_seq_len, num_key_value_heads, head_dim = key.shape
|
||||||
|
|
||||||
|
key = index_first_axis(
|
||||||
|
key.reshape(batch_size * kv_seq_len, num_key_value_heads, head_dim), indices_k
|
||||||
|
)
|
||||||
|
value = index_first_axis(
|
||||||
|
value.reshape(batch_size * kv_seq_len, num_key_value_heads, head_dim), indices_k
|
||||||
|
)
|
||||||
|
|
||||||
|
if query_length == kv_seq_len:
|
||||||
|
query = index_first_axis(
|
||||||
|
query.reshape(batch_size * kv_seq_len, num_heads, head_dim), indices_k
|
||||||
|
)
|
||||||
|
cu_seqlens_q = cu_seqlens_k
|
||||||
|
max_seqlen_in_batch_q = max_seqlen_in_batch_k
|
||||||
|
indices_q = indices_k
|
||||||
|
elif query_length == 1:
|
||||||
|
max_seqlen_in_batch_q = 1
|
||||||
|
cu_seqlens_q = torch.arange(
|
||||||
|
batch_size + 1, dtype=torch.int32, device=query.device
|
||||||
|
)
|
||||||
|
indices_q = cu_seqlens_q[:-1]
|
||||||
|
query = query.squeeze(1)
|
||||||
|
else:
|
||||||
|
q_mask = attention_mask[:, -query_length:]
|
||||||
|
query, indices_q, cu_seqlens_q, max_seqlen_in_batch_q = unpad_input(
|
||||||
|
query, q_mask
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
indices_q,
|
||||||
|
(cu_seqlens_q, cu_seqlens_k),
|
||||||
|
(max_seqlen_in_batch_q, max_seqlen_in_batch_k),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _flash_varlen_attention(query, key, value, attention_mask, attn, softmax_scale):
|
||||||
|
"""Run flash-attn varlen over a [B, L, heads, head_dim] q/k/v with a 2D mask.
|
||||||
|
|
||||||
|
Returns the attention output flattened back to [B, L, heads * head_dim].
|
||||||
|
"""
|
||||||
|
batch_size, sequence_length = query.shape[0], query.shape[1]
|
||||||
|
kv_heads = key.shape[2]
|
||||||
|
|
||||||
|
mask_2d = attention_mask.bool() if attention_mask is not None else None
|
||||||
|
(
|
||||||
|
query_states,
|
||||||
|
key_states,
|
||||||
|
value_states,
|
||||||
|
indices_q,
|
||||||
|
(cu_seqlens_q, cu_seqlens_k),
|
||||||
|
(max_seqlen_q, max_seqlen_k),
|
||||||
|
) = _upad_input(query, key, value, mask_2d, sequence_length, attn.heads)
|
||||||
|
|
||||||
|
if kv_heads < attn.heads:
|
||||||
|
key_states = repeat(key_states, "l h c -> l (h k) c", k=attn.heads // kv_heads)
|
||||||
|
value_states = repeat(
|
||||||
|
value_states, "l h c -> l (h k) c", k=attn.heads // kv_heads
|
||||||
|
)
|
||||||
|
|
||||||
|
attn_output_unpad = flash_attn_varlen_func(
|
||||||
|
query_states,
|
||||||
|
key_states,
|
||||||
|
value_states,
|
||||||
|
cu_seqlens_q=cu_seqlens_q,
|
||||||
|
cu_seqlens_k=cu_seqlens_k,
|
||||||
|
max_seqlen_q=max_seqlen_q,
|
||||||
|
max_seqlen_k=max_seqlen_k,
|
||||||
|
dropout_p=0.0,
|
||||||
|
causal=False,
|
||||||
|
softmax_scale=softmax_scale,
|
||||||
|
)
|
||||||
|
hidden_states = pad_input(attn_output_unpad, indices_q, batch_size, sequence_length)
|
||||||
|
return hidden_states.flatten(-2)
|
||||||
|
|
||||||
|
|
||||||
|
class BooguImageDoubleStreamSelfAttnProcessor(nn.Module):
|
||||||
|
"""
|
||||||
|
Double-stream self-attention processor.
|
||||||
|
|
||||||
|
Instruction and image features each get their own q/k/v projections; the two
|
||||||
|
streams are concatenated (instruction first), attended jointly, then split
|
||||||
|
back and projected with separate output heads. Uses torch SDPA by default;
|
||||||
|
set ``attention_backend = "flash"`` for Flash Attention 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
head_dim: int,
|
||||||
|
num_attention_heads: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
qkv_bias: bool = False,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
if not hasattr(F, "scaled_dot_product_attention"):
|
||||||
|
raise ImportError(
|
||||||
|
"BooguImageDoubleStreamSelfAttnProcessor requires PyTorch 2.0+."
|
||||||
|
)
|
||||||
|
|
||||||
|
self.head_dim = head_dim
|
||||||
|
self.num_attention_heads = num_attention_heads
|
||||||
|
self.num_kv_heads = num_kv_heads
|
||||||
|
self.attention_backend = "native"
|
||||||
|
|
||||||
|
query_dim = head_dim * num_attention_heads
|
||||||
|
kv_dim = head_dim * num_kv_heads
|
||||||
|
|
||||||
|
self.img_to_q = nn.Linear(query_dim, query_dim, bias=qkv_bias)
|
||||||
|
self.img_to_k = nn.Linear(query_dim, kv_dim, bias=qkv_bias)
|
||||||
|
self.img_to_v = nn.Linear(query_dim, kv_dim, bias=qkv_bias)
|
||||||
|
|
||||||
|
self.instruct_to_q = nn.Linear(query_dim, query_dim, bias=qkv_bias)
|
||||||
|
self.instruct_to_k = nn.Linear(query_dim, kv_dim, bias=qkv_bias)
|
||||||
|
self.instruct_to_v = nn.Linear(query_dim, kv_dim, bias=qkv_bias)
|
||||||
|
|
||||||
|
self.instruct_out = nn.Linear(query_dim, query_dim, bias=qkv_bias)
|
||||||
|
self.img_out = nn.Linear(query_dim, query_dim, bias=qkv_bias)
|
||||||
|
|
||||||
|
self.initialize_weights()
|
||||||
|
|
||||||
|
def initialize_weights(self) -> None:
|
||||||
|
nn.init.xavier_uniform_(self.img_to_q.weight)
|
||||||
|
nn.init.xavier_uniform_(self.img_to_k.weight)
|
||||||
|
nn.init.xavier_uniform_(self.img_to_v.weight)
|
||||||
|
nn.init.xavier_uniform_(self.instruct_to_q.weight)
|
||||||
|
nn.init.xavier_uniform_(self.instruct_to_k.weight)
|
||||||
|
nn.init.xavier_uniform_(self.instruct_to_v.weight)
|
||||||
|
nn.init.xavier_uniform_(self.instruct_out.weight)
|
||||||
|
nn.init.xavier_uniform_(self.img_out.weight)
|
||||||
|
|
||||||
|
if self.img_to_q.bias is not None:
|
||||||
|
nn.init.zeros_(self.img_to_q.bias)
|
||||||
|
nn.init.zeros_(self.img_to_k.bias)
|
||||||
|
nn.init.zeros_(self.img_to_v.bias)
|
||||||
|
nn.init.zeros_(self.instruct_to_q.bias)
|
||||||
|
nn.init.zeros_(self.instruct_to_k.bias)
|
||||||
|
nn.init.zeros_(self.instruct_to_v.bias)
|
||||||
|
nn.init.zeros_(self.instruct_out.bias)
|
||||||
|
nn.init.zeros_(self.img_out.bias)
|
||||||
|
|
||||||
|
def _concat_instruction_image_features(
|
||||||
|
self,
|
||||||
|
img_hidden_states_list: List[torch.Tensor],
|
||||||
|
instruct_hidden_states_list: List[torch.Tensor],
|
||||||
|
encoder_seq_lengths: List[int],
|
||||||
|
seq_lengths: List[int],
|
||||||
|
) -> List[torch.Tensor]:
|
||||||
|
"""Concatenate instruction then image features into one joint sequence."""
|
||||||
|
batch_size = img_hidden_states_list[0].shape[0]
|
||||||
|
max_seq_len = max(seq_lengths)
|
||||||
|
|
||||||
|
concatenated_list = []
|
||||||
|
for img_tensor, instruct_tensor in zip(
|
||||||
|
img_hidden_states_list, instruct_hidden_states_list
|
||||||
|
):
|
||||||
|
device = img_tensor.device
|
||||||
|
if instruct_tensor.device != device:
|
||||||
|
instruct_tensor = instruct_tensor.to(device)
|
||||||
|
|
||||||
|
feature_dim = img_tensor.shape[-1]
|
||||||
|
concatenated = img_tensor.new_zeros(batch_size, max_seq_len, feature_dim)
|
||||||
|
|
||||||
|
for i, (encoder_seq_len, seq_len) in enumerate(
|
||||||
|
zip(encoder_seq_lengths, seq_lengths)
|
||||||
|
):
|
||||||
|
concatenated[i, :encoder_seq_len] = instruct_tensor[i, :encoder_seq_len]
|
||||||
|
concatenated[i, encoder_seq_len:seq_len] = img_tensor[
|
||||||
|
i, : seq_len - encoder_seq_len
|
||||||
|
]
|
||||||
|
|
||||||
|
concatenated_list.append(concatenated)
|
||||||
|
|
||||||
|
return concatenated_list
|
||||||
|
|
||||||
|
def _split_instruction_image_features(
|
||||||
|
self,
|
||||||
|
hidden_states_list: List[torch.Tensor],
|
||||||
|
encoder_seq_lengths: List[int],
|
||||||
|
seq_lengths: List[int],
|
||||||
|
) -> List[Tuple[torch.Tensor, torch.Tensor]]:
|
||||||
|
"""Inverse of ``_concat_instruction_image_features``."""
|
||||||
|
result_list = []
|
||||||
|
for hidden_states in hidden_states_list:
|
||||||
|
batch_size = hidden_states.shape[0]
|
||||||
|
feature_dim = hidden_states.shape[-1]
|
||||||
|
|
||||||
|
max_instruct_len = max(encoder_seq_lengths)
|
||||||
|
max_img_len = max(
|
||||||
|
seq_len - encoder_seq_len
|
||||||
|
for seq_len, encoder_seq_len in zip(seq_lengths, encoder_seq_lengths)
|
||||||
|
)
|
||||||
|
|
||||||
|
instruct_hidden_states = hidden_states.new_zeros(
|
||||||
|
batch_size, max_instruct_len, feature_dim
|
||||||
|
)
|
||||||
|
img_hidden_states = hidden_states.new_zeros(
|
||||||
|
batch_size, max_img_len, feature_dim
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, (encoder_seq_len, seq_len) in enumerate(
|
||||||
|
zip(encoder_seq_lengths, seq_lengths)
|
||||||
|
):
|
||||||
|
img_len = seq_len - encoder_seq_len
|
||||||
|
instruct_hidden_states[i, :encoder_seq_len] = hidden_states[
|
||||||
|
i, :encoder_seq_len
|
||||||
|
]
|
||||||
|
img_hidden_states[i, :img_len] = hidden_states[
|
||||||
|
i, encoder_seq_len:seq_len
|
||||||
|
]
|
||||||
|
|
||||||
|
result_list.append((instruct_hidden_states, img_hidden_states))
|
||||||
|
|
||||||
|
return result_list
|
||||||
|
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
attn: Attention,
|
||||||
|
img_hidden_states: torch.Tensor,
|
||||||
|
instruct_hidden_states: torch.Tensor,
|
||||||
|
joint_attention_mask: Optional[torch.Tensor] = None,
|
||||||
|
rotary_emb: Optional[torch.Tensor] = None,
|
||||||
|
encoder_seq_lengths: List[int] = None,
|
||||||
|
seq_lengths: List[int] = None,
|
||||||
|
base_sequence_length: Optional[int] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
batch_size = img_hidden_states.shape[0]
|
||||||
|
|
||||||
|
img_query = self.img_to_q(img_hidden_states)
|
||||||
|
img_key = self.img_to_k(img_hidden_states)
|
||||||
|
img_value = self.img_to_v(img_hidden_states)
|
||||||
|
|
||||||
|
instruct_query = self.instruct_to_q(instruct_hidden_states)
|
||||||
|
instruct_key = self.instruct_to_k(instruct_hidden_states)
|
||||||
|
instruct_value = self.instruct_to_v(instruct_hidden_states)
|
||||||
|
|
||||||
|
img_list = [img_query, img_key, img_value]
|
||||||
|
instruct_list = [instruct_query, instruct_key, instruct_value]
|
||||||
|
concatenated_list = self._concat_instruction_image_features(
|
||||||
|
img_list, instruct_list, encoder_seq_lengths, seq_lengths
|
||||||
|
)
|
||||||
|
query, key, value = concatenated_list
|
||||||
|
|
||||||
|
sequence_length = max(seq_lengths)
|
||||||
|
|
||||||
|
query_dim = query.shape[-1]
|
||||||
|
inner_dim = key.shape[-1]
|
||||||
|
head_dim = query_dim // attn.heads
|
||||||
|
dtype = query.dtype
|
||||||
|
|
||||||
|
kv_heads = inner_dim // head_dim
|
||||||
|
|
||||||
|
query = query.view(batch_size, -1, attn.heads, head_dim)
|
||||||
|
key = key.view(batch_size, -1, kv_heads, head_dim)
|
||||||
|
value = value.view(batch_size, -1, kv_heads, head_dim)
|
||||||
|
|
||||||
|
if attn.norm_q is not None:
|
||||||
|
query = attn.norm_q(query)
|
||||||
|
if attn.norm_k is not None:
|
||||||
|
key = attn.norm_k(key)
|
||||||
|
|
||||||
|
if rotary_emb is not None:
|
||||||
|
query = apply_rotary_emb(query, rotary_emb, use_real=False)
|
||||||
|
key = apply_rotary_emb(key, rotary_emb, use_real=False)
|
||||||
|
|
||||||
|
query, key = query.to(dtype), key.to(dtype)
|
||||||
|
|
||||||
|
if base_sequence_length is not None:
|
||||||
|
softmax_scale = (
|
||||||
|
math.sqrt(math.log(sequence_length, base_sequence_length)) * attn.scale
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
softmax_scale = attn.scale
|
||||||
|
|
||||||
|
if self.attention_backend == "flash":
|
||||||
|
# q/k/v are [B, L, heads, head_dim]; the joint padding mask is 2D.
|
||||||
|
hidden_states = _flash_varlen_attention(
|
||||||
|
query, key, value, joint_attention_mask, attn, softmax_scale
|
||||||
|
)
|
||||||
|
hidden_states = hidden_states.type_as(query)
|
||||||
|
else:
|
||||||
|
if joint_attention_mask is not None:
|
||||||
|
joint_attention_mask = joint_attention_mask.bool()
|
||||||
|
if joint_attention_mask.dim() == 2:
|
||||||
|
joint_attention_mask = joint_attention_mask.view(
|
||||||
|
batch_size, 1, 1, -1
|
||||||
|
)
|
||||||
|
elif joint_attention_mask.dim() == 3:
|
||||||
|
joint_attention_mask = joint_attention_mask.unsqueeze(1)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported joint_attention_mask shape: {joint_attention_mask.shape}"
|
||||||
|
)
|
||||||
|
|
||||||
|
q = query.transpose(1, 2)
|
||||||
|
k = key.transpose(1, 2)
|
||||||
|
v = value.transpose(1, 2)
|
||||||
|
|
||||||
|
# explicitly repeat key/value to avoid the slow MATH SDPA backend that
|
||||||
|
# enable_gqa triggers on some torch builds
|
||||||
|
k = k.repeat_interleave(q.size(-3) // k.size(-3), -3)
|
||||||
|
v = v.repeat_interleave(q.size(-3) // v.size(-3), -3)
|
||||||
|
|
||||||
|
hidden_states = F.scaled_dot_product_attention(
|
||||||
|
q, k, v, attn_mask=joint_attention_mask, scale=softmax_scale
|
||||||
|
)
|
||||||
|
hidden_states = hidden_states.transpose(1, 2).reshape(
|
||||||
|
batch_size, -1, attn.heads * head_dim
|
||||||
|
)
|
||||||
|
hidden_states = hidden_states.type_as(query)
|
||||||
|
|
||||||
|
split_results = self._split_instruction_image_features(
|
||||||
|
[hidden_states], encoder_seq_lengths, seq_lengths
|
||||||
|
)
|
||||||
|
instruct_hidden_states, img_hidden_states = split_results[0]
|
||||||
|
|
||||||
|
instruct_projected = self.instruct_out(instruct_hidden_states)
|
||||||
|
img_projected = self.img_out(img_hidden_states)
|
||||||
|
|
||||||
|
merged_list = self._concat_instruction_image_features(
|
||||||
|
[img_projected], [instruct_projected], encoder_seq_lengths, seq_lengths
|
||||||
|
)
|
||||||
|
hidden_states = merged_list[0]
|
||||||
|
|
||||||
|
hidden_states = attn.to_out[0](hidden_states)
|
||||||
|
hidden_states = attn.to_out[1](hidden_states)
|
||||||
|
|
||||||
|
return hidden_states
|
||||||
|
|
||||||
|
|
||||||
|
class BooguImageAttnProcessor:
|
||||||
|
"""
|
||||||
|
Single-stream self-attention processor with RoPE + QK norm.
|
||||||
|
|
||||||
|
Uses torch SDPA by default; set ``attention_backend = "flash"`` for Flash
|
||||||
|
Attention 2 (requires the ``flash_attn`` package).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
if not hasattr(F, "scaled_dot_product_attention"):
|
||||||
|
raise ImportError("BooguImageAttnProcessor requires PyTorch 2.0+.")
|
||||||
|
self.attention_backend = "native"
|
||||||
|
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
attn: Attention,
|
||||||
|
hidden_states: torch.Tensor,
|
||||||
|
encoder_hidden_states: torch.Tensor,
|
||||||
|
attention_mask: Optional[torch.Tensor] = None,
|
||||||
|
image_rotary_emb: Optional[torch.Tensor] = None,
|
||||||
|
base_sequence_length: Optional[int] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
batch_size, sequence_length, _ = hidden_states.shape
|
||||||
|
|
||||||
|
query = attn.to_q(hidden_states)
|
||||||
|
key = attn.to_k(encoder_hidden_states)
|
||||||
|
value = attn.to_v(encoder_hidden_states)
|
||||||
|
|
||||||
|
query_dim = query.shape[-1]
|
||||||
|
inner_dim = key.shape[-1]
|
||||||
|
head_dim = query_dim // attn.heads
|
||||||
|
dtype = query.dtype
|
||||||
|
|
||||||
|
kv_heads = inner_dim // head_dim
|
||||||
|
|
||||||
|
query = query.view(batch_size, -1, attn.heads, head_dim)
|
||||||
|
key = key.view(batch_size, -1, kv_heads, head_dim)
|
||||||
|
value = value.view(batch_size, -1, kv_heads, head_dim)
|
||||||
|
|
||||||
|
if attn.norm_q is not None:
|
||||||
|
query = attn.norm_q(query)
|
||||||
|
if attn.norm_k is not None:
|
||||||
|
key = attn.norm_k(key)
|
||||||
|
|
||||||
|
if image_rotary_emb is not None:
|
||||||
|
query = apply_rotary_emb(query, image_rotary_emb, use_real=False)
|
||||||
|
key = apply_rotary_emb(key, image_rotary_emb, use_real=False)
|
||||||
|
|
||||||
|
query, key = query.to(dtype), key.to(dtype)
|
||||||
|
|
||||||
|
if base_sequence_length is not None:
|
||||||
|
softmax_scale = (
|
||||||
|
math.sqrt(math.log(sequence_length, base_sequence_length)) * attn.scale
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
softmax_scale = attn.scale
|
||||||
|
|
||||||
|
if self.attention_backend == "flash" and (
|
||||||
|
attention_mask is None or attention_mask.dim() == 2
|
||||||
|
):
|
||||||
|
mask = (
|
||||||
|
attention_mask
|
||||||
|
if attention_mask is not None
|
||||||
|
else query.new_ones(batch_size, sequence_length, dtype=torch.bool)
|
||||||
|
)
|
||||||
|
hidden_states = _flash_varlen_attention(
|
||||||
|
query, key, value, mask, attn, softmax_scale
|
||||||
|
)
|
||||||
|
hidden_states = hidden_states.type_as(query)
|
||||||
|
hidden_states = attn.to_out[0](hidden_states)
|
||||||
|
hidden_states = attn.to_out[1](hidden_states)
|
||||||
|
return hidden_states
|
||||||
|
|
||||||
|
if attention_mask is not None:
|
||||||
|
attention_mask = attention_mask.bool()
|
||||||
|
if attention_mask.dim() == 2:
|
||||||
|
attention_mask = attention_mask.view(batch_size, 1, 1, -1)
|
||||||
|
elif attention_mask.dim() == 3:
|
||||||
|
B, L, _ = attention_mask.shape
|
||||||
|
diag_valid = torch.diagonal(attention_mask, dim1=-2, dim2=-1)
|
||||||
|
lengths = diag_valid.sum(dim=-1)
|
||||||
|
arange_L = torch.arange(L, device=attention_mask.device)
|
||||||
|
q_valid = arange_L.unsqueeze(0) < lengths.unsqueeze(1)
|
||||||
|
k_valid = q_valid
|
||||||
|
causal = torch.tril(
|
||||||
|
torch.ones(L, L, dtype=torch.bool, device=attention_mask.device)
|
||||||
|
)
|
||||||
|
combined = causal & q_valid.unsqueeze(-1) & k_valid.unsqueeze(-2)
|
||||||
|
attention_mask = combined.unsqueeze(1)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported attention_mask shape: {attention_mask.shape}"
|
||||||
|
)
|
||||||
|
|
||||||
|
query = query.transpose(1, 2)
|
||||||
|
key = key.transpose(1, 2)
|
||||||
|
value = value.transpose(1, 2)
|
||||||
|
|
||||||
|
key = key.repeat_interleave(query.size(-3) // key.size(-3), -3)
|
||||||
|
value = value.repeat_interleave(query.size(-3) // value.size(-3), -3)
|
||||||
|
|
||||||
|
hidden_states = F.scaled_dot_product_attention(
|
||||||
|
query, key, value, attn_mask=attention_mask, scale=softmax_scale
|
||||||
|
)
|
||||||
|
hidden_states = hidden_states.transpose(1, 2).reshape(
|
||||||
|
batch_size, -1, attn.heads * head_dim
|
||||||
|
)
|
||||||
|
hidden_states = hidden_states.type_as(query)
|
||||||
|
|
||||||
|
hidden_states = attn.to_out[0](hidden_states)
|
||||||
|
hidden_states = attn.to_out[1](hidden_states)
|
||||||
|
|
||||||
|
return hidden_states
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# Vendored from the Boogu-Image repository (boogu/models/transformers/block_lumina2.py).
|
||||||
|
# Original work: Copyright 2025 BAAI / OmniGen2 / HuggingFace. Apache-2.0.
|
||||||
|
#
|
||||||
|
# The optional triton RMSNorm and flash-attn SwiGLU fast paths are dropped here;
|
||||||
|
# we always use torch.nn.RMSNorm and a plain SwiGLU so the model runs anywhere.
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
import torch.nn.functional as F
|
||||||
|
from diffusers.models.embeddings import Timesteps
|
||||||
|
from torch.nn import RMSNorm
|
||||||
|
|
||||||
|
from .embeddings import TimestepEmbedding
|
||||||
|
|
||||||
|
|
||||||
|
def swiglu(x, y):
|
||||||
|
return F.silu(x.float(), inplace=False).to(x.dtype) * y
|
||||||
|
|
||||||
|
|
||||||
|
class LuminaRMSNormZero(nn.Module):
|
||||||
|
"""Adaptive RMS normalization with a zero-initialized modulation projection."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
embedding_dim: int,
|
||||||
|
norm_eps: float,
|
||||||
|
norm_elementwise_affine: bool,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.silu = nn.SiLU()
|
||||||
|
self.linear = nn.Linear(
|
||||||
|
min(embedding_dim, 1024),
|
||||||
|
4 * embedding_dim,
|
||||||
|
bias=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.norm = RMSNorm(embedding_dim, eps=norm_eps)
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
x: torch.Tensor,
|
||||||
|
emb: Optional[torch.Tensor] = None,
|
||||||
|
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||||
|
emb = self.linear(self.silu(emb))
|
||||||
|
scale_msa, gate_msa, scale_mlp, gate_mlp = emb.chunk(4, dim=1)
|
||||||
|
x = self.norm(x) * (1 + scale_msa[:, None])
|
||||||
|
return x, gate_msa, scale_mlp, gate_mlp
|
||||||
|
|
||||||
|
|
||||||
|
class LuminaLayerNormContinuous(nn.Module):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
embedding_dim: int,
|
||||||
|
conditioning_embedding_dim: int,
|
||||||
|
elementwise_affine=True,
|
||||||
|
eps=1e-5,
|
||||||
|
bias=True,
|
||||||
|
norm_type="layer_norm",
|
||||||
|
out_dim: Optional[int] = None,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
# AdaLN
|
||||||
|
self.silu = nn.SiLU()
|
||||||
|
self.linear_1 = nn.Linear(conditioning_embedding_dim, embedding_dim, bias=bias)
|
||||||
|
|
||||||
|
if norm_type == "layer_norm":
|
||||||
|
self.norm = nn.LayerNorm(embedding_dim, eps, elementwise_affine, bias)
|
||||||
|
elif norm_type == "rms_norm":
|
||||||
|
self.norm = RMSNorm(
|
||||||
|
embedding_dim, eps=eps, elementwise_affine=elementwise_affine
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"unknown norm_type {norm_type}")
|
||||||
|
|
||||||
|
self.linear_2 = None
|
||||||
|
if out_dim is not None:
|
||||||
|
self.linear_2 = nn.Linear(embedding_dim, out_dim, bias=bias)
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
x: torch.Tensor,
|
||||||
|
conditioning_embedding: torch.Tensor,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
emb = self.linear_1(self.silu(conditioning_embedding).to(x.dtype))
|
||||||
|
scale = emb
|
||||||
|
x = self.norm(x) * (1 + scale)[:, None, :]
|
||||||
|
|
||||||
|
if self.linear_2 is not None:
|
||||||
|
x = self.linear_2(x)
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
class LuminaFeedForward(nn.Module):
|
||||||
|
"""A SwiGLU feed-forward layer with a multiple-of-256 inner dim."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dim: int,
|
||||||
|
inner_dim: int,
|
||||||
|
multiple_of: Optional[int] = 256,
|
||||||
|
ffn_dim_multiplier: Optional[float] = None,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.swiglu = swiglu
|
||||||
|
|
||||||
|
if ffn_dim_multiplier is not None:
|
||||||
|
inner_dim = int(ffn_dim_multiplier * inner_dim)
|
||||||
|
inner_dim = multiple_of * ((inner_dim + multiple_of - 1) // multiple_of)
|
||||||
|
|
||||||
|
self.linear_1 = nn.Linear(dim, inner_dim, bias=False)
|
||||||
|
self.linear_2 = nn.Linear(inner_dim, dim, bias=False)
|
||||||
|
self.linear_3 = nn.Linear(dim, inner_dim, bias=False)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
h1, h2 = self.linear_1(x), self.linear_3(x)
|
||||||
|
return self.linear_2(self.swiglu(h1, h2))
|
||||||
|
|
||||||
|
|
||||||
|
class Lumina2CombinedTimestepCaptionEmbedding(nn.Module):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hidden_size: int = 4096,
|
||||||
|
instruction_feat_dim: int = 2048,
|
||||||
|
frequency_embedding_size: int = 256,
|
||||||
|
norm_eps: float = 1e-5,
|
||||||
|
timestep_scale: float = 1.0,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.time_proj = Timesteps(
|
||||||
|
num_channels=frequency_embedding_size,
|
||||||
|
flip_sin_to_cos=True,
|
||||||
|
downscale_freq_shift=0.0,
|
||||||
|
scale=timestep_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.timestep_embedder = TimestepEmbedding(
|
||||||
|
in_channels=frequency_embedding_size, time_embed_dim=min(hidden_size, 1024)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.caption_embedder = nn.Sequential(
|
||||||
|
RMSNorm(instruction_feat_dim, eps=norm_eps),
|
||||||
|
nn.Linear(instruction_feat_dim, hidden_size, bias=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
self._initialize_weights()
|
||||||
|
|
||||||
|
def _initialize_weights(self):
|
||||||
|
nn.init.trunc_normal_(self.caption_embedder[1].weight, std=0.02)
|
||||||
|
nn.init.zeros_(self.caption_embedder[1].bias)
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
timestep: torch.Tensor,
|
||||||
|
instruction_hidden_states: torch.Tensor,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
timestep_proj = self.time_proj(timestep).to(dtype=dtype)
|
||||||
|
time_embed = self.timestep_embedder(timestep_proj)
|
||||||
|
caption_embed = self.caption_embedder(instruction_hidden_states)
|
||||||
|
return time_embed, caption_embed
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Vendored from the Boogu-Image repository (boogu/models/embeddings.py).
|
||||||
|
# Original work: Copyright 2024 The HuggingFace Team. Apache-2.0.
|
||||||
|
#
|
||||||
|
# Only the pieces the Boogu transformer actually needs are kept here:
|
||||||
|
# ``TimestepEmbedding`` and ``apply_rotary_emb``.
|
||||||
|
from typing import Optional, Tuple, Union
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from diffusers.models.activations import get_activation
|
||||||
|
from torch import nn
|
||||||
|
|
||||||
|
|
||||||
|
class TimestepEmbedding(nn.Module):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
in_channels: int,
|
||||||
|
time_embed_dim: int,
|
||||||
|
act_fn: str = "silu",
|
||||||
|
out_dim: int = None,
|
||||||
|
post_act_fn: Optional[str] = None,
|
||||||
|
cond_proj_dim=None,
|
||||||
|
sample_proj_bias=True,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.linear_1 = nn.Linear(in_channels, time_embed_dim, sample_proj_bias)
|
||||||
|
|
||||||
|
if cond_proj_dim is not None:
|
||||||
|
self.cond_proj = nn.Linear(cond_proj_dim, in_channels, bias=False)
|
||||||
|
else:
|
||||||
|
self.cond_proj = None
|
||||||
|
|
||||||
|
self.act = get_activation(act_fn)
|
||||||
|
|
||||||
|
if out_dim is not None:
|
||||||
|
time_embed_dim_out = out_dim
|
||||||
|
else:
|
||||||
|
time_embed_dim_out = time_embed_dim
|
||||||
|
self.linear_2 = nn.Linear(time_embed_dim, time_embed_dim_out, sample_proj_bias)
|
||||||
|
|
||||||
|
if post_act_fn is None:
|
||||||
|
self.post_act = None
|
||||||
|
else:
|
||||||
|
self.post_act = get_activation(post_act_fn)
|
||||||
|
|
||||||
|
self.initialize_weights()
|
||||||
|
|
||||||
|
def initialize_weights(self):
|
||||||
|
nn.init.normal_(self.linear_1.weight, std=0.02)
|
||||||
|
nn.init.zeros_(self.linear_1.bias)
|
||||||
|
nn.init.normal_(self.linear_2.weight, std=0.02)
|
||||||
|
nn.init.zeros_(self.linear_2.bias)
|
||||||
|
|
||||||
|
def forward(self, sample, condition=None):
|
||||||
|
if condition is not None:
|
||||||
|
sample = sample + self.cond_proj(condition)
|
||||||
|
sample = self.linear_1(sample)
|
||||||
|
|
||||||
|
if self.act is not None:
|
||||||
|
sample = self.act(sample)
|
||||||
|
|
||||||
|
sample = self.linear_2(sample)
|
||||||
|
|
||||||
|
if self.post_act is not None:
|
||||||
|
sample = self.post_act(sample)
|
||||||
|
return sample
|
||||||
|
|
||||||
|
|
||||||
|
def apply_rotary_emb(
|
||||||
|
x: torch.Tensor,
|
||||||
|
freqs_cis: Union[torch.Tensor, Tuple[torch.Tensor]],
|
||||||
|
use_real: bool = True,
|
||||||
|
use_real_unbind_dim: int = -1,
|
||||||
|
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
"""
|
||||||
|
Apply rotary embeddings to input tensors using the given frequency tensor.
|
||||||
|
|
||||||
|
Boogu always calls this with ``use_real=False`` (the Lumina-style complex
|
||||||
|
path): ``freqs_cis`` is a complex tensor and ``x`` is reinterpreted as
|
||||||
|
complex, multiplied, and returned as real.
|
||||||
|
"""
|
||||||
|
if use_real:
|
||||||
|
cos, sin = freqs_cis # [S, D]
|
||||||
|
cos = cos[None, None]
|
||||||
|
sin = sin[None, None]
|
||||||
|
cos, sin = cos.to(x.device), sin.to(x.device)
|
||||||
|
|
||||||
|
if use_real_unbind_dim == -1:
|
||||||
|
# Used for flux, cogvideox, hunyuan-dit
|
||||||
|
x_real, x_imag = x.reshape(*x.shape[:-1], -1, 2).unbind(-1)
|
||||||
|
x_rotated = torch.stack([-x_imag, x_real], dim=-1).flatten(3)
|
||||||
|
elif use_real_unbind_dim == -2:
|
||||||
|
# Used for Stable Audio, Boogu and CogView4
|
||||||
|
x_real, x_imag = x.reshape(*x.shape[:-1], 2, -1).unbind(-2)
|
||||||
|
x_rotated = torch.cat([-x_imag, x_real], dim=-1)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"`use_real_unbind_dim={use_real_unbind_dim}` but should be -1 or -2."
|
||||||
|
)
|
||||||
|
|
||||||
|
out = (x.float() * cos + x_rotated.float() * sin).to(x.dtype)
|
||||||
|
|
||||||
|
return out
|
||||||
|
else:
|
||||||
|
# used for lumina / boogu
|
||||||
|
x_rotated = torch.view_as_complex(
|
||||||
|
x.float().reshape(*x.shape[:-1], x.shape[-1] // 2, 2)
|
||||||
|
)
|
||||||
|
freqs_cis = freqs_cis.unsqueeze(2)
|
||||||
|
x_out = torch.view_as_real(x_rotated * freqs_cis).flatten(3)
|
||||||
|
|
||||||
|
return x_out.type_as(x)
|
||||||
231
extensions_built_in/diffusion_models/boogu_image/src/pipeline.py
Normal file
231
extensions_built_in/diffusion_models/boogu_image/src/pipeline.py
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
"""Packing / sampling helpers for Boogu-Image (base T2I).
|
||||||
|
|
||||||
|
This module glues the Qwen3-VL instruction features and the image latents into
|
||||||
|
the call the Boogu transformer expects, and provides a minimal flow-matching
|
||||||
|
sampler used to render preview images during training.
|
||||||
|
|
||||||
|
Time convention
|
||||||
|
---------------
|
||||||
|
Boogu's native flow time is ``t in [0, 1]`` with ``t=0`` pure noise and ``t=1``
|
||||||
|
clean; the transformer predicts ``clean - noise``. ai-toolkit's scheduler uses
|
||||||
|
the opposite convention (``t=1`` noise, velocity ``noise - clean``). The
|
||||||
|
conversion lives in ``BooguImageModel.get_noise_prediction``; this sampler runs
|
||||||
|
entirely in Boogu's native domain via :func:`run_boogu_transformer`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
from PIL import Image
|
||||||
|
from diffusers.utils.torch_utils import randn_tensor
|
||||||
|
|
||||||
|
from .transformer import BooguImageTransformer2DModel
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Instruction feature padding.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def pad_instruction_features(
|
||||||
|
features_list: List[torch.Tensor],
|
||||||
|
device: torch.device,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
"""Right-pad per-sample ``(L_i, D)`` instruction features into a batch.
|
||||||
|
|
||||||
|
Captions are stored per-sample at their natural length and only padded to the
|
||||||
|
batch max here, right before the model call. Returns ``(features (B, L, D),
|
||||||
|
attention_mask (B, L))`` with the mask 1 for real tokens, 0 for padding.
|
||||||
|
"""
|
||||||
|
lengths = [f.shape[0] for f in features_list]
|
||||||
|
max_len = max(lengths)
|
||||||
|
dim = features_list[0].shape[-1]
|
||||||
|
batch_size = len(features_list)
|
||||||
|
|
||||||
|
features = torch.zeros(batch_size, max_len, dim, device=device, dtype=dtype)
|
||||||
|
mask = torch.zeros(batch_size, max_len, dtype=torch.long, device=device)
|
||||||
|
for i, f in enumerate(features_list):
|
||||||
|
n = f.shape[0]
|
||||||
|
features[i, :n] = f.to(device, dtype)
|
||||||
|
mask[i, :n] = 1
|
||||||
|
return features, mask
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Time-shift schedule (mirrors the released Boogu base scheduler: v1 shift).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _lin_shift(
|
||||||
|
num_tokens: float,
|
||||||
|
x1: float = 256.0,
|
||||||
|
y1: float = 0.5,
|
||||||
|
x2: float = 4096.0,
|
||||||
|
y2: float = 1.15,
|
||||||
|
) -> float:
|
||||||
|
"""Linear token-count -> mu mapping (Boogu base_shift/max_shift defaults)."""
|
||||||
|
m = (y2 - y1) / (x2 - x1)
|
||||||
|
b = y1 - m * x1
|
||||||
|
return m * num_tokens + b
|
||||||
|
|
||||||
|
|
||||||
|
def boogu_time_schedule(
|
||||||
|
num_steps: int,
|
||||||
|
num_patch_tokens: int,
|
||||||
|
device: Optional[torch.device] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Boogu native-domain timesteps (0=noise .. 1=clean) with v1 time shift.
|
||||||
|
|
||||||
|
Returns a length ``num_steps + 1`` tensor; the trailing ``1.0`` is the clean
|
||||||
|
endpoint, matching the ``_timesteps`` tail in the reference scheduler.
|
||||||
|
"""
|
||||||
|
t_arr = np.linspace(0.0, 1.0, num_steps + 1, dtype=np.float32)[:-1]
|
||||||
|
|
||||||
|
mu = _lin_shift(max(1, int(num_patch_tokens)))
|
||||||
|
eps = 1e-8
|
||||||
|
t1 = np.clip(1.0 - t_arr, eps, 1.0 - eps)
|
||||||
|
num = math.exp(mu)
|
||||||
|
denom = num + (1.0 / t1 - 1.0)
|
||||||
|
t_arr = (1.0 - num / denom).astype(np.float32)
|
||||||
|
|
||||||
|
times = np.concatenate([t_arr, np.ones(1, dtype=np.float32)])
|
||||||
|
return torch.from_numpy(times).to(device=device, dtype=torch.float32)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Transformer call (Boogu native time domain).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def run_boogu_transformer(
|
||||||
|
transformer: BooguImageTransformer2DModel,
|
||||||
|
latents: torch.Tensor, # (B, 16, H, W)
|
||||||
|
boogu_t: torch.Tensor, # (B,) in [0, 1], 0=noise, 1=clean
|
||||||
|
instruction_features: torch.Tensor, # (B, L, instruction_feat_dim)
|
||||||
|
instruction_mask: torch.Tensor, # (B, L) 1 for real tokens
|
||||||
|
freqs_cis, # precomputed per-axis rotary tables
|
||||||
|
ref_image_hidden_states=None, # edit/TI2I: List[List[(16, H, W)]] per batch item
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Run the transformer and return the raw model velocity (``clean - noise``).
|
||||||
|
|
||||||
|
Shapes pass straight through: the prediction comes back as ``(B, 16, H, W)``
|
||||||
|
in the same latent layout as ``latents``. ``ref_image_hidden_states`` stays
|
||||||
|
``None`` for the base T2I model and carries reference-image VAE latents for
|
||||||
|
the edit (TI2I) model.
|
||||||
|
"""
|
||||||
|
out = transformer(
|
||||||
|
hidden_states=latents,
|
||||||
|
timestep=boogu_t,
|
||||||
|
instruction_hidden_states=instruction_features,
|
||||||
|
freqs_cis=freqs_cis,
|
||||||
|
instruction_attention_mask=instruction_mask,
|
||||||
|
ref_image_hidden_states=ref_image_hidden_states,
|
||||||
|
return_dict=False,
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Minimal sampling pipeline (for training previews).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class BooguImagePipeline:
|
||||||
|
"""Lightweight flow-matching sampler used by ai-toolkit's preview generation."""
|
||||||
|
|
||||||
|
def __init__(self, model):
|
||||||
|
# ``model`` is the BooguImageModel so we can reuse its encode/decode and
|
||||||
|
# latent helpers without duplicating state.
|
||||||
|
self.model = model
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self):
|
||||||
|
return self.model.device_torch
|
||||||
|
|
||||||
|
def to(self, *args, **kwargs):
|
||||||
|
return self
|
||||||
|
|
||||||
|
@torch.no_grad()
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
conditional_embeds,
|
||||||
|
unconditional_embeds,
|
||||||
|
height: int = 1024,
|
||||||
|
width: int = 1024,
|
||||||
|
num_inference_steps: int = 50,
|
||||||
|
guidance_scale: float = 4.0,
|
||||||
|
latents: Optional[torch.Tensor] = None,
|
||||||
|
generator: Optional[torch.Generator] = None,
|
||||||
|
ref_latents=None, # edit/TI2I: List[List[(16, H, W)]] reference VAE latents
|
||||||
|
**kwargs,
|
||||||
|
) -> List[Image.Image]:
|
||||||
|
model = self.model
|
||||||
|
device = model.device_torch
|
||||||
|
dtype = model.torch_dtype
|
||||||
|
transformer = model.transformer
|
||||||
|
patch = model.patch_size
|
||||||
|
ae_scale = model.vae_scale_factor # 8
|
||||||
|
|
||||||
|
latent_channels = transformer.config.in_channels
|
||||||
|
h_lat = height // ae_scale
|
||||||
|
w_lat = width // ae_scale
|
||||||
|
num_patch_tokens = (h_lat // patch) * (w_lat // patch)
|
||||||
|
|
||||||
|
freqs_cis = model.get_freqs_cis()
|
||||||
|
|
||||||
|
do_cfg = guidance_scale > 1.0
|
||||||
|
|
||||||
|
if latents is None:
|
||||||
|
shape = (1, latent_channels, h_lat, w_lat)
|
||||||
|
latents = randn_tensor(
|
||||||
|
shape, generator=generator, device=device, dtype=torch.float32
|
||||||
|
)
|
||||||
|
# In Boogu's domain t=0 is pure noise, so the initial latent IS the noise.
|
||||||
|
latents = latents.to(device, dtype=torch.float32)
|
||||||
|
|
||||||
|
cond_feats, cond_mask = pad_instruction_features(
|
||||||
|
conditional_embeds.text_embeds, device, dtype
|
||||||
|
)
|
||||||
|
if do_cfg:
|
||||||
|
uncond_feats, uncond_mask = pad_instruction_features(
|
||||||
|
unconditional_embeds.text_embeds, device, dtype
|
||||||
|
)
|
||||||
|
|
||||||
|
times = boogu_time_schedule(num_inference_steps, num_patch_tokens, device)
|
||||||
|
|
||||||
|
for t, t_next in zip(times[:-1], times[1:]):
|
||||||
|
boogu_t = t.expand(latents.shape[0])
|
||||||
|
v_cond = run_boogu_transformer(
|
||||||
|
transformer,
|
||||||
|
latents.to(dtype),
|
||||||
|
boogu_t,
|
||||||
|
cond_feats,
|
||||||
|
cond_mask,
|
||||||
|
freqs_cis,
|
||||||
|
ref_image_hidden_states=ref_latents,
|
||||||
|
)
|
||||||
|
if do_cfg:
|
||||||
|
v_uncond = run_boogu_transformer(
|
||||||
|
transformer,
|
||||||
|
latents.to(dtype),
|
||||||
|
boogu_t,
|
||||||
|
uncond_feats,
|
||||||
|
uncond_mask,
|
||||||
|
freqs_cis,
|
||||||
|
ref_image_hidden_states=ref_latents,
|
||||||
|
)
|
||||||
|
v = v_uncond + guidance_scale * (v_cond - v_uncond)
|
||||||
|
else:
|
||||||
|
v = v_cond
|
||||||
|
latents = latents + v.to(torch.float32) * (t_next - t)
|
||||||
|
|
||||||
|
images = model.decode_latents(latents, device=device, dtype=dtype)
|
||||||
|
images = images.float().clamp(-1.0, 1.0)
|
||||||
|
images = ((images + 1.0) * 127.5).round().to(torch.uint8)
|
||||||
|
images = images.permute(0, 2, 3, 1).cpu().numpy()
|
||||||
|
return [Image.fromarray(arr) for arr in images]
|
||||||
244
extensions_built_in/diffusion_models/boogu_image/src/rope.py
Normal file
244
extensions_built_in/diffusion_models/boogu_image/src/rope.py
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
# Vendored from the Boogu-Image repository (boogu/models/transformers/rope.py).
|
||||||
|
# Original work: Copyright 2025 BAAI / OmniGen2 / HuggingFace. Apache-2.0.
|
||||||
|
#
|
||||||
|
# Only the double-stream rotary embedder (the one the transformer uses) and the
|
||||||
|
# ``get_freqs_cis`` precompute helper are kept. The MPS-specific branch is
|
||||||
|
# preserved verbatim.
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
from diffusers.models.embeddings import get_1d_rotary_pos_embed
|
||||||
|
from einops import repeat
|
||||||
|
|
||||||
|
|
||||||
|
def get_freqs_cis(
|
||||||
|
axes_dim: Tuple[int, int, int], axes_lens: Tuple[int, int, int], theta: int
|
||||||
|
) -> List[torch.Tensor]:
|
||||||
|
"""Precompute the per-axis rotary frequency tables (done once per resolution)."""
|
||||||
|
freqs_cis = []
|
||||||
|
freqs_dtype = torch.float32 if torch.backends.mps.is_available() else torch.float64
|
||||||
|
for d, e in zip(axes_dim, axes_lens):
|
||||||
|
emb = get_1d_rotary_pos_embed(d, e, theta=theta, freqs_dtype=freqs_dtype)
|
||||||
|
freqs_cis.append(emb)
|
||||||
|
return freqs_cis
|
||||||
|
|
||||||
|
|
||||||
|
class BooguImageDoubleStreamRotaryPosEmbed(nn.Module):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
theta: int,
|
||||||
|
axes_dim: Tuple[int, int, int],
|
||||||
|
axes_lens: Tuple[int, int, int] = (300, 512, 512),
|
||||||
|
patch_size: int = 2,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.theta = theta
|
||||||
|
self.axes_dim = axes_dim
|
||||||
|
self.axes_lens = axes_lens
|
||||||
|
self.patch_size = patch_size
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_freqs_cis(
|
||||||
|
axes_dim: Tuple[int, int, int], axes_lens: Tuple[int, int, int], theta: int
|
||||||
|
) -> List[torch.Tensor]:
|
||||||
|
return get_freqs_cis(axes_dim, axes_lens, theta)
|
||||||
|
|
||||||
|
def _get_freqs_cis(self, freqs_cis, ids: torch.Tensor) -> torch.Tensor:
|
||||||
|
device = ids.device
|
||||||
|
if ids.device.type == "mps":
|
||||||
|
ids = ids.to("cpu")
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for i in range(len(self.axes_dim)):
|
||||||
|
freqs = freqs_cis[i].to(ids.device)
|
||||||
|
index = ids[:, :, i : i + 1].repeat(1, 1, freqs.shape[-1]).to(torch.int64)
|
||||||
|
result.append(
|
||||||
|
torch.gather(
|
||||||
|
freqs.unsqueeze(0).repeat(index.shape[0], 1, 1), dim=1, index=index
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return torch.cat(result, dim=-1).to(device)
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
freqs_cis,
|
||||||
|
attention_mask,
|
||||||
|
l_effective_ref_img_len,
|
||||||
|
l_effective_img_len,
|
||||||
|
ref_img_sizes,
|
||||||
|
img_sizes,
|
||||||
|
device,
|
||||||
|
):
|
||||||
|
batch_size = len(attention_mask)
|
||||||
|
p = self.patch_size
|
||||||
|
|
||||||
|
encoder_seq_len = attention_mask.shape[1]
|
||||||
|
l_effective_cap_len = attention_mask.sum(dim=1).tolist()
|
||||||
|
|
||||||
|
seq_lengths = [
|
||||||
|
cap_len + sum(ref_img_len) + img_len
|
||||||
|
for cap_len, ref_img_len, img_len in zip(
|
||||||
|
l_effective_cap_len, l_effective_ref_img_len, l_effective_img_len
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
max_seq_len = max(seq_lengths)
|
||||||
|
max_ref_img_len = max(
|
||||||
|
[sum(ref_img_len) for ref_img_len in l_effective_ref_img_len]
|
||||||
|
)
|
||||||
|
max_img_len = max(l_effective_img_len)
|
||||||
|
|
||||||
|
# Create position IDs
|
||||||
|
position_ids = torch.zeros(
|
||||||
|
batch_size, max_seq_len, 3, dtype=torch.int32, device=device
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, (cap_seq_len, seq_len) in enumerate(
|
||||||
|
zip(l_effective_cap_len, seq_lengths)
|
||||||
|
):
|
||||||
|
# add text position ids
|
||||||
|
position_ids[i, :cap_seq_len] = repeat(
|
||||||
|
torch.arange(cap_seq_len, dtype=torch.int32, device=device), "l -> l 3"
|
||||||
|
)
|
||||||
|
|
||||||
|
pe_shift = cap_seq_len
|
||||||
|
pe_shift_len = cap_seq_len
|
||||||
|
|
||||||
|
if ref_img_sizes[i] is not None:
|
||||||
|
for ref_img_size, ref_img_len in zip(
|
||||||
|
ref_img_sizes[i], l_effective_ref_img_len[i]
|
||||||
|
):
|
||||||
|
H, W = ref_img_size
|
||||||
|
ref_H_tokens, ref_W_tokens = H // p, W // p
|
||||||
|
assert ref_H_tokens * ref_W_tokens == ref_img_len
|
||||||
|
|
||||||
|
row_ids = repeat(
|
||||||
|
torch.arange(ref_H_tokens, dtype=torch.int32, device=device),
|
||||||
|
"h -> h w",
|
||||||
|
w=ref_W_tokens,
|
||||||
|
).flatten()
|
||||||
|
col_ids = repeat(
|
||||||
|
torch.arange(ref_W_tokens, dtype=torch.int32, device=device),
|
||||||
|
"w -> h w",
|
||||||
|
h=ref_H_tokens,
|
||||||
|
).flatten()
|
||||||
|
position_ids[i, pe_shift_len : pe_shift_len + ref_img_len, 0] = (
|
||||||
|
pe_shift
|
||||||
|
)
|
||||||
|
position_ids[i, pe_shift_len : pe_shift_len + ref_img_len, 1] = (
|
||||||
|
row_ids
|
||||||
|
)
|
||||||
|
position_ids[i, pe_shift_len : pe_shift_len + ref_img_len, 2] = (
|
||||||
|
col_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
pe_shift += max(ref_H_tokens, ref_W_tokens)
|
||||||
|
pe_shift_len += ref_img_len
|
||||||
|
|
||||||
|
H, W = img_sizes[i]
|
||||||
|
H_tokens, W_tokens = H // p, W // p
|
||||||
|
assert H_tokens * W_tokens == l_effective_img_len[i]
|
||||||
|
|
||||||
|
row_ids = repeat(
|
||||||
|
torch.arange(H_tokens, dtype=torch.int32, device=device),
|
||||||
|
"h -> h w",
|
||||||
|
w=W_tokens,
|
||||||
|
).flatten()
|
||||||
|
col_ids = repeat(
|
||||||
|
torch.arange(W_tokens, dtype=torch.int32, device=device),
|
||||||
|
"w -> h w",
|
||||||
|
h=H_tokens,
|
||||||
|
).flatten()
|
||||||
|
|
||||||
|
assert pe_shift_len + l_effective_img_len[i] == seq_len
|
||||||
|
position_ids[i, pe_shift_len:seq_len, 0] = pe_shift
|
||||||
|
position_ids[i, pe_shift_len:seq_len, 1] = row_ids
|
||||||
|
position_ids[i, pe_shift_len:seq_len, 2] = col_ids
|
||||||
|
|
||||||
|
# Get combined rotary embeddings
|
||||||
|
freqs_cis = self._get_freqs_cis(freqs_cis, position_ids)
|
||||||
|
|
||||||
|
# create separate rotary embeddings for captions and images
|
||||||
|
cap_freqs_cis = torch.zeros(
|
||||||
|
batch_size,
|
||||||
|
encoder_seq_len,
|
||||||
|
freqs_cis.shape[-1],
|
||||||
|
device=device,
|
||||||
|
dtype=freqs_cis.dtype,
|
||||||
|
)
|
||||||
|
ref_img_freqs_cis = torch.zeros(
|
||||||
|
batch_size,
|
||||||
|
max_ref_img_len,
|
||||||
|
freqs_cis.shape[-1],
|
||||||
|
device=device,
|
||||||
|
dtype=freqs_cis.dtype,
|
||||||
|
)
|
||||||
|
img_freqs_cis = torch.zeros(
|
||||||
|
batch_size,
|
||||||
|
max_img_len,
|
||||||
|
freqs_cis.shape[-1],
|
||||||
|
device=device,
|
||||||
|
dtype=freqs_cis.dtype,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate combined image sequence lengths (ref_img + img) for each sample
|
||||||
|
combined_img_seq_lengths = [
|
||||||
|
sum(ref_img_len) + img_len
|
||||||
|
for ref_img_len, img_len in zip(
|
||||||
|
l_effective_ref_img_len, l_effective_img_len
|
||||||
|
)
|
||||||
|
]
|
||||||
|
max_combined_img_len = max(combined_img_seq_lengths)
|
||||||
|
|
||||||
|
# Create combined image rotary embeddings
|
||||||
|
combined_img_freqs_cis = torch.zeros(
|
||||||
|
batch_size,
|
||||||
|
max_combined_img_len,
|
||||||
|
freqs_cis.shape[-1],
|
||||||
|
device=device,
|
||||||
|
dtype=freqs_cis.dtype,
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, (cap_seq_len, ref_img_len, img_len, seq_len) in enumerate(
|
||||||
|
zip(
|
||||||
|
l_effective_cap_len,
|
||||||
|
l_effective_ref_img_len,
|
||||||
|
l_effective_img_len,
|
||||||
|
seq_lengths,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
cap_freqs_cis[i, :cap_seq_len] = freqs_cis[i, :cap_seq_len]
|
||||||
|
ref_img_freqs_cis[i, : sum(ref_img_len)] = freqs_cis[
|
||||||
|
i, cap_seq_len : cap_seq_len + sum(ref_img_len)
|
||||||
|
]
|
||||||
|
img_freqs_cis[i, :img_len] = freqs_cis[
|
||||||
|
i,
|
||||||
|
cap_seq_len + sum(ref_img_len) : cap_seq_len
|
||||||
|
+ sum(ref_img_len)
|
||||||
|
+ img_len,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Combined image rotary embeddings: ref_img + img (same order as img_patch_embed_and_refine)
|
||||||
|
combined_img_freqs_cis[i, : sum(ref_img_len)] = freqs_cis[
|
||||||
|
i, cap_seq_len : cap_seq_len + sum(ref_img_len)
|
||||||
|
]
|
||||||
|
combined_img_freqs_cis[i, sum(ref_img_len) : sum(ref_img_len) + img_len] = (
|
||||||
|
freqs_cis[
|
||||||
|
i,
|
||||||
|
cap_seq_len + sum(ref_img_len) : cap_seq_len
|
||||||
|
+ sum(ref_img_len)
|
||||||
|
+ img_len,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
cap_freqs_cis,
|
||||||
|
ref_img_freqs_cis,
|
||||||
|
img_freqs_cis,
|
||||||
|
freqs_cis,
|
||||||
|
l_effective_cap_len,
|
||||||
|
seq_lengths,
|
||||||
|
combined_img_freqs_cis,
|
||||||
|
combined_img_seq_lengths,
|
||||||
|
)
|
||||||
1174
extensions_built_in/diffusion_models/boogu_image/src/transformer.py
Normal file
1174
extensions_built_in/diffusion_models/boogu_image/src/transformer.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1402,9 +1402,9 @@ def validate_configs(
|
|||||||
raise ValueError("All datasets must have cache_text_embeddings set to True when caching text embeddings is enabled.")
|
raise ValueError("All datasets must have cache_text_embeddings set to True when caching text embeddings is enabled.")
|
||||||
|
|
||||||
# qwen image edit cannot cache text embeddings
|
# qwen image edit cannot cache text embeddings
|
||||||
if model_config.arch == 'qwen_image_edit':
|
if model_config.arch in ['qwen_image_edit', 'boogu_image_edit']:
|
||||||
if train_config.unload_text_encoder:
|
if train_config.unload_text_encoder:
|
||||||
raise ValueError("Cannot cache unload text encoder with qwen_image_edit model. Control images are encoded with text embeddings. You can cache the text embeddings though")
|
raise ValueError(f"Cannot cache unload text encoder with {model_config.arch} model. Control images are encoded with text embeddings. You can cache the text embeddings though")
|
||||||
|
|
||||||
if train_config.diff_output_preservation and train_config.blank_prompt_preservation:
|
if train_config.diff_output_preservation and train_config.blank_prompt_preservation:
|
||||||
raise ValueError("Cannot use both differential output preservation and blank prompt preservation at the same time. Please set one of them to False.")
|
raise ValueError("Cannot use both differential output preservation and blank prompt preservation at the same time. Please set one of them to False.")
|
||||||
|
|||||||
@@ -1031,6 +1031,58 @@ export const modelArchs: ModelArch[] = [
|
|||||||
'model.layer_offloading',
|
'model.layer_offloading',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'boogu_image',
|
||||||
|
label: 'Boogu Image',
|
||||||
|
group: 'image',
|
||||||
|
defaults: {
|
||||||
|
'config.process[0].model.name_or_path': ['Boogu/Boogu-Image-0.1-Base', defaultNameOrPath],
|
||||||
|
'config.process[0].model.quantize': [true, false],
|
||||||
|
'config.process[0].model.quantize_te': [true, false],
|
||||||
|
'config.process[0].train.timestep_type': ['linear', 'sigmoid'],
|
||||||
|
'config.process[0].network.conv': [undefined, 16],
|
||||||
|
'config.process[0].network.conv_alpha': [undefined, 16],
|
||||||
|
'config.process[0].model.low_vram': [true, false],
|
||||||
|
},
|
||||||
|
disableSections: [
|
||||||
|
'network.conv',
|
||||||
|
],
|
||||||
|
additionalSections: [
|
||||||
|
'model.low_vram',
|
||||||
|
'model.layer_offloading',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'boogu_image_edit',
|
||||||
|
label: 'Boogu Image Edit',
|
||||||
|
group: 'instruction',
|
||||||
|
defaults: {
|
||||||
|
'config.process[0].model.name_or_path': ['Boogu/Boogu-Image-0.1-Edit', defaultNameOrPath],
|
||||||
|
'config.process[0].model.quantize': [true, false],
|
||||||
|
'config.process[0].model.quantize_te': [true, false],
|
||||||
|
'config.process[0].train.timestep_type': ['linear', 'sigmoid'],
|
||||||
|
'config.process[0].network.conv': [undefined, 16],
|
||||||
|
'config.process[0].network.conv_alpha': [undefined, 16],
|
||||||
|
'config.process[0].model.low_vram': [true, false],
|
||||||
|
'config.process[0].train.unload_text_encoder': [false, false],
|
||||||
|
'config.process[0].model.model_kwargs': [
|
||||||
|
{
|
||||||
|
match_target_res: false,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
disableSections: [
|
||||||
|
'network.conv', 'train.unload_text_encoder',
|
||||||
|
],
|
||||||
|
additionalSections: [
|
||||||
|
'datasets.multi_control_paths',
|
||||||
|
'sample.multi_ctrl_imgs',
|
||||||
|
'model.low_vram',
|
||||||
|
'model.layer_offloading',
|
||||||
|
'model.qie.match_target_res',
|
||||||
|
],
|
||||||
|
},
|
||||||
].sort((a, b) => {
|
].sort((a, b) => {
|
||||||
// Sort by label, case-insensitive
|
// Sort by label, case-insensitive
|
||||||
return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' });
|
return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' });
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
VERSION = "0.10.14"
|
VERSION = "0.10.15"
|
||||||
|
|||||||
Reference in New Issue
Block a user