Add support for Boogu Image and Boogu Image Edit

This commit is contained in:
Jaret Burkett
2026-06-18 15:05:49 -06:00
parent e886745051
commit 60c1ac6a50
14 changed files with 3323 additions and 3 deletions

View File

@@ -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,
] ]

View File

@@ -0,0 +1,4 @@
from .boogu_image import BooguImageModel
from .boogu_image_edit import BooguImageEditModel
__all__ = ["BooguImageModel", "BooguImageEditModel"]

View 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()
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View 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]

View 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,
)

File diff suppressed because it is too large Load Diff

View File

@@ -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.")

View File

@@ -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' });

View File

@@ -1 +1 @@
VERSION = "0.10.14" VERSION = "0.10.15"