sekkit's picture orwelian84's picture
Duplicate from orwelian84/UltraTex-Qwen-Image-2.1
cf8a54e
Raw History Blame Contribute Delete
17.2 kB
"""Pieces shared by the Krea 2 and Qwen-Image 2.1 UltraTex backbones.
Both backbones are single-stream DiTs that consume one flat token sequence with
per-token 3-axis RoPE ids. UltraTex's Background Token Dropping (BTD) is a pure
sequence operation (gather the foreground tokens and their ids), so the only
backbone-specific parts are the block, the id convention and the modulation.
This module holds the attention dispatch (dense SDPA or the vendored SLA
block-sparse kernel), RoPE helpers, the flow-matching schedules and the
ComfyUI-backed VAE / latent normalisation used by both.
"""
from __future__ import annotations
import math
import os
import sys
from dataclasses import dataclass
import torch
from einops import rearrange
from torch import Tensor, nn
from torch.nn import functional as F
# ----------------------------------------------------------------------------
# Sparse attention backend (vendored SLA, same configuration as the FLUX ports)
# ----------------------------------------------------------------------------
_SPARSE_ATTENTION = None
_SPARSE_ATTENTION_CONFIG: dict = {"enabled": False}
def configure_sparse_attention(
enabled: bool,
head_dim: int = 128,
topk: float = 0.20,
blkq: int = 128,
blkk: int = 64,
) -> dict:
"""Select the attention backend used by every DiT block.
Mirrors ``ultratex/backbones/flux2/model.py``. The SLA Triton kernel needs
equal query/key lengths, which holds for the pruned [text, target, refs]
sequence that the trainers build.
"""
global _SPARSE_ATTENTION, _SPARSE_ATTENTION_CONFIG
if not enabled:
_SPARSE_ATTENTION = None
_SPARSE_ATTENTION_CONFIG = {"enabled": False}
return dict(_SPARSE_ATTENTION_CONFIG)
if not 0.0 < topk <= 1.0:
raise ValueError(f"sparse attention topk must be in (0, 1], got {topk}")
if head_dim != 128:
raise ValueError(f"The vendored SLA kernel expects head_dim=128, got {head_dim}")
from ultratex.sparse_attention import SparseLinearAttention
_SPARSE_ATTENTION = SparseLinearAttention(
head_dim=head_dim, topk=topk, feature_map="softmax", BLKQ=blkq, BLKK=blkk
)
_SPARSE_ATTENTION_CONFIG = {
"enabled": True,
"backend": "SparseLinearAttention",
"head_dim": head_dim,
"topk": topk,
"BLKQ": blkq,
"BLKK": blkk,
}
return dict(_SPARSE_ATTENTION_CONFIG)
def sparse_attention_config() -> dict:
return dict(_SPARSE_ATTENTION_CONFIG)
def sparse_attention_enabled() -> bool:
return _SPARSE_ATTENTION is not None
def dense_attention(q: Tensor, k: Tensor, v: Tensor, mask: Tensor | None = None) -> Tensor:
"""q, k, v: [B, H, N, D] -> [B, N, H*D]."""
out = F.scaled_dot_product_attention(q, k, v, attn_mask=mask)
return rearrange(out, "b h n d -> b n (h d)")
def full_attention(q: Tensor, k: Tensor, v: Tensor) -> Tensor:
"""Bidirectional attention over the whole sequence, sparse when configured."""
if _SPARSE_ATTENTION is not None and q.shape[2] == k.shape[2]:
return rearrange(_SPARSE_ATTENTION(q, k, v), "b h n d -> b n (h d)")
return dense_attention(q, k, v)
def block_causal_attention(q: Tensor, k: Tensor, v: Tensor, segments: list[tuple[int, int, bool]]) -> Tensor:
"""Qwen-Image 2.1 block-causal attention.
``segments`` lists ``(start, end, causal)`` spans covering the sequence in
order. A span attends to every key before its own end; a causal span (text)
additionally applies a causal triangle inside itself. The last span is the
target image, which therefore sees everything.
With the sparse backend, the target rows come from one SLA pass over the
full sequence (the only large block) and the prefix rows are recomputed
exactly with dense SDPA over their restricted keys.
"""
n = q.shape[2]
if not segments or segments[-1][1] != n:
raise ValueError(f"segments {segments} do not cover the sequence of length {n}")
if _SPARSE_ATTENTION is not None:
out = rearrange(_SPARSE_ATTENTION(q, k, v), "b h n d -> b n (h d)")
prefix_end = segments[-1][0]
if prefix_end == 0:
return out
parts = [
_segment_attention(q, k, v, start, end, causal) for start, end, causal in segments[:-1]
]
return torch.cat(parts + [out[:, prefix_end:]], dim=1)
parts = [_segment_attention(q, k, v, start, end, causal) for start, end, causal in segments]
return torch.cat(parts, dim=1) if len(parts) > 1 else parts[0]
def _segment_attention(q, k, v, start, end, causal):
mask = None
if causal:
mask = torch.ones((end - start, end), dtype=torch.bool, device=q.device).tril(start)
return dense_attention(q[:, :, start:end], k[:, :, :end], v[:, :, :end], mask)
def prefix_cached_attention(q: Tensor, k: Tensor, v: Tensor, prefix_k: Tensor, prefix_v: Tensor) -> Tensor:
"""Target-only queries against [cached prefix, target] keys (dense)."""
return dense_attention(q, torch.cat([prefix_k, k], dim=2), torch.cat([prefix_v, v], dim=2))
# ----------------------------------------------------------------------------
# RoPE
# ----------------------------------------------------------------------------
def rope(pos: Tensor, dim: int, theta: float) -> Tensor:
assert dim % 2 == 0
scale = torch.arange(0, dim, 2, dtype=torch.float64, device=pos.device) / dim
omega = 1.0 / (theta**scale)
out = torch.einsum("...n,d->...nd", pos.to(torch.float64), omega)
out = torch.stack([torch.cos(out), -torch.sin(out), torch.sin(out), torch.cos(out)], dim=-1)
out = rearrange(out, "b n d (i j) -> b n d i j", i=2, j=2)
return out.float()
class EmbedND(nn.Module):
def __init__(self, dim: int, theta: float, axes_dim: list[int]):
super().__init__()
self.dim = dim
self.theta = theta
self.axes_dim = axes_dim
def forward(self, ids: Tensor) -> Tensor:
emb = torch.cat(
[rope(ids[..., i], self.axes_dim[i], self.theta) for i in range(len(self.axes_dim))], dim=-3
)
return emb.unsqueeze(1)
def apply_rope(xq: Tensor, xk: Tensor, freqs_cis: Tensor) -> tuple[Tensor, Tensor]:
xq_ = xq.float().reshape(*xq.shape[:-1], -1, 1, 2)
xk_ = xk.float().reshape(*xk.shape[:-1], -1, 1, 2)
xq_out = freqs_cis[..., 0] * xq_[..., 0] + freqs_cis[..., 1] * xq_[..., 1]
xk_out = freqs_cis[..., 0] * xk_[..., 0] + freqs_cis[..., 1] * xk_[..., 1]
return xq_out.reshape(*xq.shape).type_as(xq), xk_out.reshape(*xk.shape).type_as(xk)
def timestep_embedding(t: Tensor, dim: int, max_period: int = 10000, time_factor: float = 1000.0) -> Tensor:
t = time_factor * t
half = dim // 2
freqs = torch.exp(-math.log(max_period) * torch.arange(0, half, device=t.device, dtype=torch.float32) / half)
args = t[:, None].float() * freqs[None]
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
if dim % 2:
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
if torch.is_floating_point(t):
embedding = embedding.to(t)
return embedding
# ----------------------------------------------------------------------------
# Token packing (BTD operates on these flat sequences)
# ----------------------------------------------------------------------------
def patchify_tokens(latent: Tensor, patch: int) -> tuple[Tensor, int, int]:
"""[B, C, H, W] -> [B, (H/p)(W/p), C*p*p] plus the token grid size."""
b, c, h, w = latent.shape
if h % patch or w % patch:
raise ValueError(f"latent {h}x{w} is not divisible by patch {patch}")
tokens = rearrange(latent, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=patch, pw=patch)
return tokens, h // patch, w // patch
def unpatchify_tokens(tokens: Tensor, h: int, w: int, channels: int, patch: int) -> Tensor:
return rearrange(tokens, "b (h w) (c ph pw) -> b c (h ph) (w pw)", h=h, w=w, c=channels, ph=patch, pw=patch)
def gather_sequence(sequence: Tensor, indices: Tensor) -> Tensor:
"""Gather along the token axis with one index list per batch item."""
return sequence.gather(1, indices[..., None].expand(-1, -1, sequence.shape[-1]))
def scatter_sequence(base: Tensor, indices: Tensor, values: Tensor) -> Tensor:
return base.scatter(1, indices[..., None].expand_as(values), values)
def foreground_indices(mask: Tensor, token_count: int, label: str) -> Tensor:
"""Flat foreground token indices [B, K] from a [B, h, w] (or [h, w]) mask.
Batch size 1 is the supported training configuration (exactly like the
FLUX ports); larger batches must have equal foreground counts.
"""
if mask.ndim == 2:
mask = mask[None]
flat = mask.reshape(mask.shape[0], -1).bool()
if flat.shape[1] != token_count:
raise ValueError(f"{label} mask has {flat.shape[1]} cells but the backbone produced {token_count} tokens")
counts = flat.sum(dim=1)
if torch.any(counts == 0):
raise ValueError(f"{label} contains a sample with no foreground tokens")
if not torch.all(counts == counts[0]):
raise ValueError(f"{label} foreground counts differ within the batch ({counts.tolist()}); use batch_size=1")
return torch.stack([row.nonzero(as_tuple=True)[0] for row in flat], dim=0)
# ----------------------------------------------------------------------------
# Flow-matching schedules
# ----------------------------------------------------------------------------
def time_shift(mu: float, sigma: float, t: Tensor) -> Tensor:
return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma)
def shifted_schedule(num_steps: int, mu: float) -> list[float]:
timesteps = torch.linspace(1, 0, num_steps + 1)
timesteps = time_shift(mu, 1.0, timesteps)
return timesteps.tolist()
def qwen21_mu(image_seq_len: int, base_shift: float = 0.5, max_shift: float = 0.9, base_len: int = 256, max_len: int = 8192) -> float:
"""Qwen-Image 2.1 scheduler mu: 0.5 at 256 tokens, 0.9 at 8192 (linear, extrapolated)."""
slope = (max_shift - base_shift) / (max_len - base_len)
return slope * image_seq_len + (base_shift - slope * base_len)
# ----------------------------------------------------------------------------
# Latent normalisation (what the DiTs were trained on; the VAE itself is raw)
# ----------------------------------------------------------------------------
@dataclass(frozen=True)
class LatentNorm:
mean: tuple[float, ...]
std: tuple[float, ...]
def process_in(self, latent: Tensor) -> Tensor:
mean = torch.tensor(self.mean, device=latent.device, dtype=latent.dtype).view(1, -1, 1, 1)
std = torch.tensor(self.std, device=latent.device, dtype=latent.dtype).view(1, -1, 1, 1)
return (latent - mean) / std
def process_out(self, latent: Tensor) -> Tensor:
mean = torch.tensor(self.mean, device=latent.device, dtype=latent.dtype).view(1, -1, 1, 1)
std = torch.tensor(self.std, device=latent.device, dtype=latent.dtype).view(1, -1, 1, 1)
return latent * std + mean
# Wan 2.1 / Qwen-Image VAE (Krea 2 latent space), 16 channels, 8x.
WAN21_NORM = LatentNorm(
mean=(-0.7571, -0.7089, -0.9113, 0.1075, -0.1745, 0.9653, -0.1517, 1.5508,
0.4134, -0.0715, 0.5517, -0.3632, -0.1922, -0.9497, 0.2503, -0.2921),
std=(2.8184, 1.4541, 2.3275, 2.6558, 1.2196, 1.7708, 2.6052, 2.0743,
3.2687, 2.1526, 2.8652, 1.5579, 1.6382, 1.1253, 2.8251, 1.9160),
)
# Qwen-Image 2.1 RGBA VAE, 64 channels, 16x.
QWEN21_NORM = LatentNorm(
mean=(0.5126, 0.7721, -0.0631, 1.3506, -0.7855, -2.1025, -0.3458, 1.3722,
1.8873, -1.7177, -0.6510, 0.2732, 0.7562, -0.6163, -1.0277, 3.8363,
2.0210, 0.0472, 0.9320, 2.0087, 2.4954, -0.1391, -1.4249, 1.8464,
-0.5236, 1.2826, 3.7046, -1.3035, 2.7286, -1.4518, -1.9036, -1.9955,
-0.0342, -1.0265, -0.7636, 3.0555, 0.0746, -3.0751, -0.1076, 1.7376,
-1.0914, -1.9435, -0.2784, -1.3680, 0.4809, -0.4433, 0.3764, 0.5729,
-2.0595, 1.0960, -1.3260, -2.0211, -5.0179, 0.5275, 4.0162, 1.8505,
0.3026, 1.9373, 1.4937, 0.2632, 0.5547, -1.7121, -0.1562, 0.0304),
std=(3.2001, 3.2936, 3.4321, 3.0091, 3.1061, 4.0379, 4.0705, 3.7910,
3.0785, 3.6500, 3.9308, 3.0904, 2.8778, 3.7675, 3.7320, 5.0756,
3.2864, 4.0397, 3.1317, 4.0443, 2.9249, 3.9454, 3.0988, 4.2489,
3.4896, 3.8513, 3.9323, 3.4719, 3.7498, 4.2830, 3.5694, 4.2467,
3.9037, 3.2947, 5.0770, 3.5075, 3.2700, 3.4767, 2.8063, 5.1125,
3.5327, 4.7833, 3.1286, 4.1819, 3.8527, 3.8312, 3.5605, 4.3875,
3.9624, 4.0168, 3.5643, 4.0550, 5.5614, 4.2963, 4.4080, 3.4959,
3.8747, 3.7608, 3.5735, 3.1490, 3.7662, 3.6746, 3.4563, 3.8161),
)
# ----------------------------------------------------------------------------
# ComfyUI-backed VAE
# ----------------------------------------------------------------------------
def import_comfy(comfy_root: str | None = None):
"""Make ComfyUI importable as a library (its CLI parser must not see our argv)."""
root = comfy_root or os.environ.get("COMFY_ROOT")
if not root or not os.path.isdir(os.path.join(root, "comfy")):
raise FileNotFoundError(
"COMFY_ROOT must point at a ComfyUI checkout with Qwen-Image 2.1 support "
f"(got {root!r})"
)
if root not in sys.path:
sys.path.insert(0, root)
saved_argv = sys.argv
sys.argv = [saved_argv[0]]
try:
import comfy.sd # noqa: F401
import comfy.utils # noqa: F401
finally:
sys.argv = saved_argv
import comfy
return comfy
class ComfyVAE(nn.Module):
"""Thin wrapper over ComfyUI's Wan-family VAEs.
``encode``/``decode`` work on single images ``[B, C, H, W]`` in ``[-1, 1]``
and return/consume *normalised* latents (the DiT's input space). The raw
autoencoder is ``self.model`` (``comfy.ldm.wan.vae*.WanVAE``); its
``decoder`` + ``conv2`` are what the foreground-aware fine-tune trains.
"""
def __init__(self, vae_path: str, norm: LatentNorm, image_channels: int, comfy_root: str | None = None, device="cpu", dtype=torch.float32):
super().__init__()
comfy = import_comfy(comfy_root)
sd = comfy.utils.load_torch_file(vae_path)
wrapper = comfy.sd.VAE(sd=sd, device=torch.device("cpu"), dtype=dtype)
if wrapper.latent_channels != len(norm.mean):
raise ValueError(
f"{vae_path} has {wrapper.latent_channels} latent channels, expected {len(norm.mean)}"
)
if wrapper.output_channels != image_channels:
raise ValueError(
f"{vae_path} reconstructs {wrapper.output_channels} image channels, expected {image_channels}"
)
self.model = wrapper.first_stage_model.to(device=device, dtype=dtype)
self.norm = norm
self.image_channels = image_channels
self.latent_channels = wrapper.latent_channels
self.downscale = wrapper.downscale_ratio if isinstance(wrapper.downscale_ratio, int) else wrapper.downscale_ratio[-1]
# The Wan 2.1 VAE (Krea 2 latent space) is a video VAE that only takes [B, C, T, H, W];
# the Qwen-Image 2.1 VAE accepts single images directly.
self.video_only = type(self.model).__module__.endswith("wan.vae")
@property
def device(self):
return next(self.model.parameters()).device
@property
def dtype(self):
return next(self.model.parameters()).dtype
def trainable_decoder_parameters(self):
return list(self.model.decoder.parameters()) + list(self.model.conv2.parameters())
def decoder_state_dict(self) -> dict[str, Tensor]:
out = {f"decoder.{k}": v for k, v in self.model.decoder.state_dict().items()}
out.update({f"conv2.{k}": v for k, v in self.model.conv2.state_dict().items()})
return out
def load_decoder_state_dict(self, state: dict[str, Tensor]) -> None:
dec = {k[len("decoder."):]: v for k, v in state.items() if k.startswith("decoder.")}
conv2 = {k[len("conv2."):]: v for k, v in state.items() if k.startswith("conv2.")}
self.model.decoder.load_state_dict(dec, strict=True)
if conv2:
self.model.conv2.load_state_dict(conv2, strict=True)
def encode(self, pixels: Tensor) -> Tensor:
"""pixels [B, C, H, W] in [-1, 1] (C == image_channels) -> normalised latent."""
if pixels.shape[1] != self.image_channels:
raise ValueError(f"expected {self.image_channels} image channels, got {pixels.shape[1]}")
pixels = pixels.to(self.device, self.dtype)
if self.video_only:
z = self.model.encode(pixels.unsqueeze(2)).squeeze(2)
else:
z = self.model.encode(pixels)
return self.norm.process_in(z.float())
def decode(self, latent: Tensor) -> Tensor:
"""normalised latent [B, C, h, w] -> pixels [B, image_channels, H, W] in [-1, 1]."""
z = self.norm.process_out(latent.float()).to(self.dtype)
if self.video_only:
return self.model.decode(z.unsqueeze(2)).squeeze(2).float()
return self.model.decode(z).float()