Add support for Mage-Flow and Mage-Flow Edit

This commit is contained in:
Jaret Burkett
2026-07-25 11:34:20 -06:00
parent 0c3a5e6970
commit 92bdb6e473
12 changed files with 2690 additions and 1 deletions

View File

@@ -0,0 +1,73 @@
"""Varlen attention shim for Mage-Flow.
The reference implementation (github.com/microsoft/Mage) runs every attention
call through flash-attn's ``flash_attn_varlen_func`` so that several
variable-length samples can be packed into one sequence and kept isolated via
``cu_seqlens``. This shim exposes the same function with the FA2 calling
convention, falling back to a per-sequence ``torch.scaled_dot_product_attention``
loop when flash-attn is not installed (functionally equivalent for the dense /
non-causal path the DiT uses, just slower).
"""
from __future__ import annotations
from typing import Any, Callable
import torch
import torch.nn.functional as F
_RESOLVED_FN: Callable[..., Any] | None = None
def _sdpa_varlen(
q,
k,
v,
cu_seqlens_q=None,
cu_seqlens_k=None,
max_seqlen_q=None,
max_seqlen_k=None,
dropout_p: float = 0.0,
softmax_scale=None,
causal: bool = False,
**_unused: Any,
):
"""FA2-varlen-compatible SDPA fallback: one SDPA dispatch per sequence."""
cu_q = cu_seqlens_q.tolist()
cu_k = cu_seqlens_k.tolist()
outs = []
for qs, qe, ks, ke in zip(cu_q[:-1], cu_q[1:], cu_k[:-1], cu_k[1:]):
# (s, h, d) -> (1, h, s, d)
q_i = q[qs:qe].transpose(0, 1).unsqueeze(0)
k_i = k[ks:ke].transpose(0, 1).unsqueeze(0)
v_i = v[ks:ke].transpose(0, 1).unsqueeze(0)
out_i = F.scaled_dot_product_attention(
q_i,
k_i,
v_i,
attn_mask=None,
dropout_p=dropout_p,
is_causal=causal,
scale=softmax_scale,
)
outs.append(out_i.squeeze(0).transpose(0, 1))
return torch.cat(outs, dim=0).contiguous()
def _resolve() -> Callable[..., Any]:
global _RESOLVED_FN
if _RESOLVED_FN is None:
try:
from flash_attn import flash_attn_varlen_func as _fn
_RESOLVED_FN = _fn
except ImportError:
_RESOLVED_FN = _sdpa_varlen
return _RESOLVED_FN
def flash_attn_varlen_func(*args, **kwargs):
return _resolve()(*args, **kwargs)
__all__ = ["flash_attn_varlen_func"]

View File

@@ -0,0 +1,229 @@
"""Packing / sampling helpers for Mage-Flow.
Turns latents + Qwen3-VL text features into the packed variable-length
sequences the ``MageFlow`` DiT consumes (batch dim 1, per-sample
``cu_seqlens``, exactly mirroring the reference training/inference packing),
and provides a minimal flow-matching sampler for ai-toolkit preview images.
Time convention: rectified flow with ``x_t = (1 - sigma) * clean + sigma *
noise``, the model predicts the velocity ``noise - clean``, and sampling
integrates sigma from 1 (pure noise) down to 0 (clean). This is identical to
ai-toolkit's convention, so the toolkit ``timestep / 1000`` flows straight
through as sigma. The inference sigma schedule is the reference's static
shift: ``shifted = shift * s / (1 + (shift - 1) * s)`` over ``s =
linspace(1, 1/steps, steps)`` with a terminal 0 (shift 6.0).
Edit conditioning: each sample's sequence is ``[target, ref_1, …, ref_N]`` —
clean reference latents ride along after the noisy target tokens, sharing the
sample's timestep modulation. RoPE gives every image segment in the pack its
own "frame" index (segment order), so references land on later frame
coordinates than their target, as in the reference ``generate_edits``.
"""
from __future__ import annotations
from typing import List, Optional
import torch
from einops import rearrange
from PIL import Image
from diffusers.utils.torch_utils import randn_tensor
from .transformer import MageFlow
def lens_to_cu(lens: List[int], device) -> torch.Tensor:
"""Sequence lengths -> cumulative cu_seqlens [0, l0, l0+l1, ...] (int32)."""
t = torch.tensor(lens, device=device, dtype=torch.int32)
return torch.cat(
[
torch.zeros(1, dtype=torch.int32, device=device),
torch.cumsum(t, dim=0, dtype=torch.int32),
]
)
def pack_text_features(
features_list: List[torch.Tensor], device, dtype
) -> tuple[torch.Tensor, torch.Tensor]:
"""Concatenate per-sample ``(L_i, D)`` text features into the packed
``[1, sum L, D]`` text stream + its ``cu_seqlens``. No padding — varlen
attention isolates the samples."""
feats = [f.to(device, dtype) for f in features_list]
txt = torch.cat(feats, dim=0).unsqueeze(0)
txt_cu = lens_to_cu([f.shape[0] for f in feats], device)
return txt, txt_cu
def predict_velocity(
model: MageFlow,
latents: torch.Tensor, # (B, C, h, w) noisy target latents
t: torch.Tensor, # (B,) flow sigma in [0, 1] (1 = pure noise)
text_embeds: List[torch.Tensor], # per-sample (L_i, D) Qwen3-VL features
ref_latents: Optional[
List[List[torch.Tensor]]
] = None, # per-sample clean (C, hr, wr) refs
) -> torch.Tensor:
"""Run the DiT on the packed [text | target(+refs)] sequences.
Packs the batch into one varlen forward (mirroring reference packing:
per-sample cu_seqlens isolate the samples inside the attention kernel).
Reference latents, when given, are appended clean after each sample's
target tokens; the returned velocity covers only the target tokens,
reshaped back to ``(B, C, h, w)``. No time flip / negation: Mage-Flow's
convention matches ai-toolkit's.
"""
device = latents.device
dtype = latents.dtype
b, c, h, w = latents.shape
if ref_latents is not None and not any(len(r) > 0 for r in ref_latents):
ref_latents = None
img_parts, samp_lens, shape_seq, target_idx_parts = [], [], [], []
off = 0
for i in range(b):
tgt = rearrange(latents[i], "c h w -> (h w) c")
img_parts.append(tgt)
shape_seq.append((1, h, w))
target_idx_parts.append(torch.arange(off, off + h * w, device=device))
samp_len = h * w
off += h * w
if ref_latents is not None:
for ref in ref_latents[i]:
ref = ref.to(device, dtype)
_, rh, rw = ref.shape
img_parts.append(rearrange(ref, "c h w -> (h w) c"))
shape_seq.append((1, rh, rw))
samp_len += rh * rw
off += rh * rw
samp_lens.append(samp_len)
img = torch.cat(img_parts, dim=0).unsqueeze(0) # [1, sum, C]
img_cu = lens_to_cu(samp_lens, device)
img_shapes = [shape_seq]
target_idx = torch.cat(target_idx_parts)
txt, txt_cu = pack_text_features(text_embeds, device, dtype)
out = model(
img=img,
txt=txt,
timesteps=t.to(device),
img_shapes=img_shapes,
img_cu_seqlens=img_cu,
txt_cu_seqlens=txt_cu,
) # [1, sum, C_out]
pred = out[:, target_idx, :] # [1, B*h*w, C]
return rearrange(pred, "1 (b h w) c -> b c h w", b=b, h=h, w=w)
def build_shifted_sigmas(
num_steps: int, shift: float = 6.0, device=None
) -> torch.Tensor:
"""The reference inference sigma schedule: ``linspace(1, 1/steps, steps)``
run through the static shift ``shift*s/(1+(shift-1)*s)`` with a terminal 0."""
s = torch.linspace(1.0, 1.0 / num_steps, num_steps, dtype=torch.float64)
s = shift * s / (1 + (shift - 1) * s)
sigmas = torch.cat([s, torch.zeros(1, dtype=torch.float64)])
return sigmas.to(device=device, dtype=torch.float32)
class MageFlowPipeline:
"""Lightweight flow-matching sampler used by ai-toolkit's preview generation."""
def __init__(self, model):
# ``model`` is the MageFlowModel (BaseModel) so we can reuse its
# encode/decode and config.
self.model = model
@property
def device(self):
return self.model.device_torch
def to(self, *args, **kwargs):
return self
def set_progress_bar_config(self, **kwargs):
pass
@torch.no_grad()
def __call__(
self,
conditional_embeds,
unconditional_embeds,
height: int = 1024,
width: int = 1024,
num_inference_steps: int = 30,
guidance_scale: float = 5.0,
latents: Optional[torch.Tensor] = None,
generator: Optional[torch.Generator] = None,
ref_latents: Optional[List[List[torch.Tensor]]] = None,
**kwargs,
) -> List[Image.Image]:
model = self.model
device = model.device_torch
dtype = model.torch_dtype
transformer: MageFlow = model.transformer
shift = float(model.model_config.model_kwargs.get("static_shift", 6.0))
renorm = bool(model.model_config.model_kwargs.get("cfg_renormalization", False))
do_cfg = guidance_scale > 1.0 and unconditional_embeds is not None
gh = height // model.vae_scale_factor
gw = width // model.vae_scale_factor
latent_channels = transformer.in_channels
if latents is None:
latents = randn_tensor(
(1, latent_channels, gh, gw),
generator=generator,
device=device,
dtype=torch.float32,
)
latents = latents.to(device, dtype=torch.float32)
cond_feats = [f.to(device, dtype) for f in conditional_embeds.text_embeds]
if do_cfg:
uncond_feats = [
f.to(device, dtype) for f in unconditional_embeds.text_embeds
]
sigmas = build_shifted_sigmas(num_inference_steps, shift=shift, device=device)
# Euler integration of the flow ODE, sigma 1 -> 0 (with optional CFG:
# v = uncond + cfg * (cond - uncond), reference convention).
for i in range(num_inference_steps):
s_cur = sigmas[i].item()
s_next = sigmas[i + 1].item()
t = torch.full((latents.shape[0],), s_cur, dtype=dtype, device=device)
v_cond = predict_velocity(
transformer, latents.to(dtype), t, cond_feats, ref_latents=ref_latents
)
if do_cfg:
v_uncond = predict_velocity(
transformer,
latents.to(dtype),
t,
uncond_feats,
ref_latents=ref_latents,
)
v = v_uncond + guidance_scale * (v_cond - v_uncond)
if renorm:
# Rescale the guided velocity per token back to the
# conditional velocity's norm (reduces oversaturation).
v = v * (
torch.norm(v_cond, dim=1, keepdim=True)
/ (torch.norm(v, dim=1, keepdim=True) + 1e-6)
)
else:
v = v_cond
latents = latents + (s_next - s_cur) * v.to(torch.float32)
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,169 @@
"""Qwen3-VL prompt encoding for Mage-Flow.
The reference implementation (mage_flow/models/modules/text_encoder.py) packs
many prompts into one varlen forward; per-sequence the math reduces to a plain
causal forward with positions ``arange(L)`` replicated on every mrope axis and
the leading system-prompt tokens dropped from the output. ai-toolkit encodes
prompts one at a time (embeds are cached per caption), so that per-sequence
form is implemented here directly on the stock HF Qwen3-VL module.
The DiT consumes the final hidden states ``[L - drop_idx, 2560]``; the pooled
"vec" of the reference is unused by the released checkpoints (vec_type null —
the transformer adds a zero vector), so it is not computed.
"""
from __future__ import annotations
import math
from typing import List, Optional
import torch
import torch.nn.functional as F
# Prompt templates from the reference (mage_flow/models/utils.py). ``start_idx``
# is the number of leading (system prompt) tokens dropped from the encoder
# output before it conditions the DiT.
PROMPT_TEMPLATE = {
"mage-flow": {
"template": (
"<|im_start|>system\nDescribe the image by detailing the color, shape, size, texture, quantity, "
"text, spatial relationships of the objects and background:"
"<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
),
"start_idx": 34,
},
"mage-flow-edit": {
"template": (
"<|im_start|>system\nDescribe 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.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
),
"start_idx": 64,
},
}
# Fixed image placeholder used at edit training time (one per reference image).
EDIT_IMAGE_PLACEHOLDER = "<|vision_start|><|image_pad|><|vision_end|>"
def edit_prompt_body(instruction: str, num_refs: int) -> str:
"""Training-time multi-reference prompt body:
``Image 1: <ph>Image 2: <ph>…{instruction}``."""
prefix = "".join(
f"Image {j}: {EDIT_IMAGE_PLACEHOLDER}" for j in range(1, num_refs + 1)
)
return prefix + instruction
def patch_qwen_vl_patch_embed(model):
"""Qwen-VL's vision patch_embed is a Conv3d whose kernel == stride, i.e. a plain
linear projection of each flattened patch. bf16 Conv3d has no fast cuDNN kernel and
falls back to a slow, GPU-underutilizing path. Swap it for the equivalent F.linear
(a GEMM). The weight is read lazily so this survives later .to(device)/dtype moves.
Returns the number of patch_embed modules patched. (Same patch as the krea2
extension / Qwen3VLCaptioner.)"""
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
def resize_vl_images(
images: List[torch.Tensor], max_long_edge: int = 384
) -> List["PIL.Image.Image"]:
"""Prepare reference images for the Qwen3-VL conditioning pass.
Matches the reference's ``_resize_long_edge`` (cap the long edge at
``max_long_edge``, preserving aspect ratio, BICUBIC, never upscale) — the
MLLM only needs a coarse view of the reference; full-resolution detail
flows through the VAE reference latents. Input tensors are ``(C, H, W)``
or ``(1, C, H, W)`` in [0, 1]; output is PIL for the Qwen processor.
"""
from torchvision.transforms.functional import to_pil_image
out = []
for img in images:
if img.dim() == 4:
img = img[0]
img = img.float().clamp(0, 1).cpu()
pil = to_pil_image(img)
if max_long_edge and max_long_edge > 0:
w, h = pil.size
long_edge = max(w, h)
if long_edge > max_long_edge:
scale = max_long_edge / long_edge
new_w = max(1, int(round(w * scale)))
new_h = max(1, int(round(h * scale)))
pil = pil.resize((new_w, new_h), resample=3) # PIL.Image.BICUBIC
out.append(pil)
return out
@torch.no_grad()
def encode_mageflow_prompt(
text_encoder, # Qwen3VLForConditionalGeneration
tokenizer,
prompt: str,
template_name: str = "mage-flow",
max_length: int = 2048,
images: Optional[list] = None, # list of PIL images (already VL-resized)
processor=None, # AutoProcessor, required when images are given
dtype: torch.dtype = torch.bfloat16,
) -> torch.Tensor:
"""Encode one prompt (optionally with reference images) to DiT conditioning.
Returns ``(L - start_idx, 2560)`` final hidden states with the system
prompt dropped, matching the reference ``TextEncoder.forward`` txt output.
"""
info = PROMPT_TEMPLATE[template_name]
template = info["template"]
drop_idx = int(info["start_idx"])
device = next(text_encoder.parameters()).device
if images:
if processor is None:
raise ValueError(
"encoding reference images requires the Qwen3-VL AutoProcessor"
)
formatted = template.format(edit_prompt_body(prompt, len(images)))
vl = processor(text=[formatted], images=list(images), return_tensors="pt")
input_ids = vl["input_ids"].to(device)
extra = {}
for key in ("pixel_values", "image_grid_thw"):
if vl.get(key) is not None:
extra[key] = vl[key].to(device)
else:
formatted = template.format(prompt)
input_ids = tokenizer(
formatted,
max_length=max_length + drop_idx,
truncation=True,
return_tensors="pt",
).input_ids.to(device)
extra = {}
# The reference packed encoder gives every sequence positions arange(L)
# replicated on all mrope axes (even for image tokens); a 2D position_ids
# is expanded to exactly that by the HF text model.
position_ids = torch.arange(input_ids.shape[1], device=device).unsqueeze(0)
out = text_encoder.model(
input_ids=input_ids,
position_ids=position_ids,
**extra,
)
hidden = out.last_hidden_state[0] # (L, D)
return hidden[drop_idx:].to(dtype)

View File

@@ -0,0 +1,809 @@
"""Mage-Flow NR-MMDiT (dual-stream, native-resolution packed) for ai-toolkit.
Vendored from the reference implementation
(github.com/microsoft/Mage, mage_flow/models/mage_flow.py + modules/mage_layers.py)
with these deviations:
- loguru / pydantic / torch._dynamo decorators dropped,
- gradient checkpointing is gated on ``self.gradient_checkpointing and
torch.is_grad_enabled()`` (instead of the reference's ``self.training and
self.checkpoint``) and exposed via ``enable_gradient_checkpointing`` /
``disable_gradient_checkpointing`` so ai-toolkit's trainer can toggle it,
- flash-attn is imported through the local shim (``.attn``) which falls back
to a per-sequence SDPA loop when flash-attn is unavailable.
State-dict keys are identical to the reference checkpoints
(``transformer/diffusion_pytorch_model.safetensors`` in
microsoft/Mage-Flow-Base and microsoft/Mage-Flow-Edit-Base).
Sequence layout: images and text are packed as variable-length sequences of
batch dim 1 (``[1, sum_len, C]``) with per-sample ``cu_seqlens``; joint text+image
attention runs in a single varlen kernel. 2D multi-scale RoPE is applied to
image tokens only (text tokens are not rotated). ``timesteps`` is the flow
sigma in [0, 1] (1 = pure noise), one entry per packed sample.
"""
import math
from dataclasses import dataclass
from typing import Any
import torch
import torch.nn as nn
from torch import Tensor
from torch.utils.checkpoint import checkpoint
from diffusers.models.attention import FeedForward
from diffusers.models.embeddings import TimestepEmbedding
from diffusers.models.normalization import RMSNorm
from .attn import flash_attn_varlen_func
def apply_rotary_emb_mageflow(x: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor:
"""Apply complex rotary embeddings to `x` ([S, H, D]) using `freqs_cis`
(the MageFlowEmbedRope 2D multi-scale RoPE, adjacent-pair complex convention)."""
x_rotated = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
freqs_cis = freqs_cis.unsqueeze(1)
x_out = torch.view_as_real(x_rotated * freqs_cis).flatten(-2)
return x_out.type_as(x)
def get_timestep_embedding(
timesteps: torch.Tensor,
embedding_dim: int,
flip_sin_to_cos: bool = False,
downscale_freq_shift: float = 1,
scale: float = 1,
max_period: int = 10000,
) -> torch.Tensor:
"""Sinusoidal timestep embeddings (DDPM convention).
NOTE: kept vendored (not diffusers') because the frequency table is
downcast to ``timesteps.dtype`` (bf16) here — the model was trained with
this exact bf16 rounding, so diffusers' fp32 variant produces a slightly
different embedding and degrades outputs.
"""
assert len(timesteps.shape) == 1, "Timesteps should be a 1d-array"
half_dim = embedding_dim // 2
exponent = -math.log(max_period) * torch.arange(
start=0, end=half_dim, dtype=torch.float32, device=timesteps.device
)
exponent = exponent / (half_dim - downscale_freq_shift)
emb = torch.exp(exponent).to(timesteps.dtype)
emb = timesteps[:, None].float() * emb[None, :]
# scale embeddings
emb = scale * emb
# concat sine and cosine embeddings
emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1)
# flip sine and cosine embeddings
if flip_sin_to_cos:
emb = torch.cat([emb[:, half_dim:], emb[:, :half_dim]], dim=-1)
# zero pad
if embedding_dim % 2 == 1:
emb = torch.nn.functional.pad(emb, (0, 1, 0, 0))
return emb
class Timesteps(nn.Module):
def __init__(
self,
num_channels: int,
flip_sin_to_cos: bool,
downscale_freq_shift: float,
scale: int = 1,
):
super().__init__()
self.num_channels = num_channels
self.flip_sin_to_cos = flip_sin_to_cos
self.downscale_freq_shift = downscale_freq_shift
self.scale = scale
def forward(self, timesteps: torch.Tensor) -> torch.Tensor:
return get_timestep_embedding(
timesteps,
self.num_channels,
flip_sin_to_cos=self.flip_sin_to_cos,
downscale_freq_shift=self.downscale_freq_shift,
scale=self.scale,
)
class MageFlowTimestepProjEmbeddings(nn.Module):
def __init__(self, embedding_dim):
super().__init__()
self.time_proj = Timesteps(
num_channels=256, flip_sin_to_cos=True, downscale_freq_shift=0, scale=1000
)
self.timestep_embedder = TimestepEmbedding(
in_channels=256, time_embed_dim=embedding_dim
)
def forward(self, timestep, hidden_states):
timesteps_proj = self.time_proj(timestep)
timesteps_emb = self.timestep_embedder(
timesteps_proj.to(dtype=hidden_states.dtype)
) # (N, D)
return timesteps_emb
class MageFlowEmbedRope(nn.Module):
def __init__(self, theta: int, axes_dim: list, scale_rope=False):
super().__init__()
self.theta = theta
self.axes_dim = axes_dim
pos_index = torch.arange(4096)
neg_index = torch.arange(4096).flip(0) * -1 - 1
self.pos_freqs = torch.cat(
[
self.rope_params(pos_index, self.axes_dim[0], self.theta),
self.rope_params(pos_index, self.axes_dim[1], self.theta),
self.rope_params(pos_index, self.axes_dim[2], self.theta),
],
dim=1,
)
self.neg_freqs = torch.cat(
[
self.rope_params(neg_index, self.axes_dim[0], self.theta),
self.rope_params(neg_index, self.axes_dim[1], self.theta),
self.rope_params(neg_index, self.axes_dim[2], self.theta),
],
dim=1,
)
# DO NOT USING REGISTER BUFFER HERE, IT WILL CAUSE COMPLEX NUMBERS LOSE ITS IMAGINARY PART
self.scale_rope = scale_rope
self.video_freq_cache = {}
def rope_params(self, index, dim, theta=10000):
assert dim % 2 == 0
freqs = torch.outer(
index,
1.0 / torch.pow(theta, torch.arange(0, dim, 2).to(torch.float32).div(dim)),
)
freqs = torch.polar(torch.ones_like(freqs), freqs)
return freqs
def forward(
self, video_fhw, device: torch.device, max_img_len: int = None
) -> torch.Tensor:
"""Compute the vision RoPE frequencies for the packed image tokens.
Text tokens are NOT rotated, so no text RoPE is computed.
``video_fhw`` is a list of (frame, height, width) tuples, one per image
segment in the packed sequence; segment index doubles as the "frame"
RoPE coordinate (edit reference images land on later frame indices).
"""
if self.pos_freqs.device != device:
self.pos_freqs = self.pos_freqs.to(device)
self.neg_freqs = self.neg_freqs.to(device)
if isinstance(video_fhw, list):
video_fhw = video_fhw[0]
if not isinstance(video_fhw, list):
video_fhw = [video_fhw]
vid_freqs = []
for idx, fhw in enumerate(video_fhw):
frame, height, width = fhw
key = (frame, height, width, idx)
if key not in self.video_freq_cache:
self.video_freq_cache[key] = self._compute_video_freqs(
frame, height, width, idx
)
vid_freqs.append(self.video_freq_cache[key].to(device))
vid_freqs = torch.cat(vid_freqs, dim=0)
if max_img_len is not None and vid_freqs.shape[0] < max_img_len:
pad_len = max_img_len - vid_freqs.shape[0]
vid_freqs = torch.nn.functional.pad(vid_freqs, (0, 0, 0, pad_len))
return vid_freqs
def _compute_video_freqs(
self, frame: int, height: int, width: int, idx: int = 0
) -> torch.Tensor:
seq_lens = frame * height * width
freqs_pos = self.pos_freqs.split([x // 2 for x in self.axes_dim], dim=1)
freqs_neg = self.neg_freqs.split([x // 2 for x in self.axes_dim], dim=1)
freqs_frame = (
freqs_pos[0][idx : idx + frame]
.view(frame, 1, 1, -1)
.expand(frame, height, width, -1)
)
if self.scale_rope:
freqs_height = torch.cat(
[freqs_neg[1][-(height - height // 2) :], freqs_pos[1][: height // 2]],
dim=0,
)
freqs_height = freqs_height.view(1, height, 1, -1).expand(
frame, height, width, -1
)
freqs_width = torch.cat(
[freqs_neg[2][-(width - width // 2) :], freqs_pos[2][: width // 2]],
dim=0,
)
freqs_width = freqs_width.view(1, 1, width, -1).expand(
frame, height, width, -1
)
else:
freqs_height = (
freqs_pos[1][:height]
.view(1, height, 1, -1)
.expand(frame, height, width, -1)
)
freqs_width = (
freqs_pos[2][:width]
.view(1, 1, width, -1)
.expand(frame, height, width, -1)
)
freqs = torch.cat([freqs_frame, freqs_height, freqs_width], dim=-1).reshape(
seq_lens, -1
)
return freqs.clone().contiguous()
class Attention(nn.Module):
"""Joint text+image attention (reference subset: the double-stream config
MageFlow actually instantiates — qk rms_norm, added kv projections)."""
def __init__(
self,
query_dim: int,
heads: int = 8,
dim_head: int = 64,
dropout: float = 0.0,
bias: bool = False,
added_kv_proj_dim: int = None,
added_proj_bias: bool = True,
out_bias: bool = True,
eps: float = 1e-5,
processor=None,
out_dim: int = None,
out_context_dim: int = None,
elementwise_affine: bool = True,
):
super().__init__()
self.inner_dim = out_dim if out_dim is not None else dim_head * heads
self.inner_kv_dim = self.inner_dim
self.query_dim = query_dim
self.use_bias = bias
self.cross_attention_dim = query_dim
self.out_dim = out_dim if out_dim is not None else query_dim
self.out_context_dim = (
out_context_dim if out_context_dim is not None else query_dim
)
self.heads = out_dim // dim_head if out_dim is not None else heads
self.added_kv_proj_dim = added_kv_proj_dim
self.added_proj_bias = added_proj_bias
# qk_norm is always "rms_norm" for MageFlow.
self.norm_q = RMSNorm(dim_head, eps=eps, elementwise_affine=elementwise_affine)
self.norm_k = RMSNorm(dim_head, eps=eps, elementwise_affine=elementwise_affine)
self.to_q = nn.Linear(query_dim, self.inner_dim, bias=bias)
self.to_k = nn.Linear(self.cross_attention_dim, self.inner_kv_dim, bias=bias)
self.to_v = nn.Linear(self.cross_attention_dim, self.inner_kv_dim, bias=bias)
self.add_k_proj = nn.Linear(
added_kv_proj_dim, self.inner_kv_dim, bias=added_proj_bias
)
self.add_v_proj = nn.Linear(
added_kv_proj_dim, self.inner_kv_dim, bias=added_proj_bias
)
self.add_q_proj = nn.Linear(
added_kv_proj_dim, self.inner_dim, bias=added_proj_bias
)
self.norm_added_q = RMSNorm(dim_head, eps=eps)
self.norm_added_k = RMSNorm(dim_head, eps=eps)
self.to_out = nn.ModuleList([])
self.to_out.append(nn.Linear(self.inner_dim, self.out_dim, bias=out_bias))
self.to_out.append(nn.Dropout(dropout))
self.to_add_out = nn.Linear(self.inner_dim, self.out_context_dim, bias=out_bias)
self.processor = processor
def forward(
self,
hidden_states: torch.Tensor,
txt_cu_lens: torch.Tensor = None,
img_cu_lens: torch.Tensor = None,
image_rotary_emb: torch.Tensor = None,
**attention_kwargs,
) -> torch.Tensor:
return self.processor(
self,
hidden_states,
txt_cu_lens=txt_cu_lens,
img_cu_lens=img_cu_lens,
image_rotary_emb=image_rotary_emb,
**attention_kwargs,
)
class MageDoubleStreamAttnProcessor:
"""Attention processor for the Mage double-stream architecture. Implements
joint attention where the packed text and image streams are concatenated
per sample (order: [text, image]) and run through one varlen kernel."""
def __call__(
self,
attn: Attention,
hidden_states: torch.FloatTensor, # Image stream [1, sum_img, D]
img_cu_lens: torch.LongTensor,
encoder_hidden_states: torch.FloatTensor = None, # Text stream [1, sum_txt, D]
txt_cu_lens: torch.LongTensor = None,
image_rotary_emb: torch.Tensor = None,
**kwargs,
) -> torch.FloatTensor:
if encoder_hidden_states is None:
raise ValueError(
"MageDoubleStreamAttnProcessor requires encoder_hidden_states (text stream)"
)
# Compute QKV for image stream (sample projections)
img_query = attn.to_q(hidden_states)
img_key = attn.to_k(hidden_states)
img_value = attn.to_v(hidden_states)
# Compute QKV for text stream (context projections)
txt_query = attn.add_q_proj(encoder_hidden_states)
txt_key = attn.add_k_proj(encoder_hidden_states)
txt_value = attn.add_v_proj(encoder_hidden_states)
# Reshape for multi-head attention
img_query = img_query.unflatten(-1, (attn.heads, -1))
img_key = img_key.unflatten(-1, (attn.heads, -1))
img_value = img_value.unflatten(-1, (attn.heads, -1))
txt_query = txt_query.unflatten(-1, (attn.heads, -1))
txt_key = txt_key.unflatten(-1, (attn.heads, -1))
txt_value = txt_value.unflatten(-1, (attn.heads, -1))
if img_query.ndim == 4:
img_query = img_query.flatten(0, 1)
img_key = img_key.flatten(0, 1)
img_value = img_value.flatten(0, 1)
if txt_query.ndim == 4:
txt_query = txt_query.flatten(0, 1)
txt_key = txt_key.flatten(0, 1)
txt_value = txt_value.flatten(0, 1)
# Apply QK normalization
img_query = attn.norm_q(img_query)
img_key = attn.norm_k(img_key)
txt_query = attn.norm_added_q(txt_query)
txt_key = attn.norm_added_k(txt_key)
# Apply 2D multi-scale RoPE (MageFlowEmbedRope) to image tokens
img_freqs = image_rotary_emb
img_query = apply_rotary_emb_mageflow(img_query, img_freqs)
img_key = apply_rotary_emb_mageflow(img_key, img_freqs)
# Calculate lengths
img_lens = img_cu_lens[1:] - img_cu_lens[:-1]
txt_lens = txt_cu_lens[1:] - txt_cu_lens[:-1]
# Calculate joint cu_seqlens
joint_lens = txt_lens + img_lens
joint_cu_lens = torch.cat(
[
torch.zeros(1, dtype=torch.int32, device=joint_lens.device),
torch.cumsum(joint_lens, dim=0, dtype=torch.int32),
],
dim=0,
)
device = joint_lens.device
batch_size = len(txt_lens)
sample_indices = torch.arange(batch_size, device=device)
txt_sample_ids = torch.repeat_interleave(sample_indices, txt_lens)
img_sample_ids = torch.repeat_interleave(sample_indices, img_lens)
txt_intra_pos = (
torch.arange(txt_query.shape[0], device=device)
- txt_cu_lens[txt_sample_ids]
)
img_intra_pos = (
torch.arange(img_query.shape[0], device=device)
- img_cu_lens[img_sample_ids]
)
txt_dest_indices = joint_cu_lens[txt_sample_ids] + txt_intra_pos
img_dest_indices = (
joint_cu_lens[img_sample_ids] + txt_lens[img_sample_ids] + img_intra_pos
)
total_tokens = joint_cu_lens[-1]
joint_query = torch.empty(
(total_tokens, *txt_query.shape[1:]), dtype=txt_query.dtype, device=device
)
joint_key = torch.empty(
(total_tokens, *txt_key.shape[1:]), dtype=txt_key.dtype, device=device
)
joint_value = torch.empty(
(total_tokens, *txt_value.shape[1:]), dtype=txt_value.dtype, device=device
)
joint_query[txt_dest_indices] = txt_query
joint_query[img_dest_indices] = img_query
joint_key[txt_dest_indices] = txt_key
joint_key[img_dest_indices] = img_key
joint_value[txt_dest_indices] = txt_value
joint_value[img_dest_indices] = img_value
max_seqlen = joint_lens.max().item()
joint_attn_output = flash_attn_varlen_func(
joint_query,
joint_key,
joint_value,
cu_seqlens_q=joint_cu_lens,
cu_seqlens_k=joint_cu_lens,
max_seqlen_q=max_seqlen,
max_seqlen_k=max_seqlen,
dropout_p=0.0,
softmax_scale=None,
causal=False,
)
txt_attn_output = joint_attn_output[txt_dest_indices]
img_attn_output = joint_attn_output[img_dest_indices]
img_attn_output = img_attn_output.flatten(1, 2) # (N, H, D) -> (N, H*D)
img_attn_output = img_attn_output.to(joint_query.dtype)
txt_attn_output = txt_attn_output.flatten(1, 2) # (N, H, D) -> (N, H*D)
txt_attn_output = txt_attn_output.to(joint_query.dtype)
img_attn_output = attn.to_out[0](img_attn_output)
img_attn_output = attn.to_out[1](img_attn_output) # dropout
txt_attn_output = attn.to_add_out(txt_attn_output)
txt_attn_output = txt_attn_output.view(
encoder_hidden_states.shape[0],
encoder_hidden_states.shape[1],
txt_attn_output.shape[-1],
)
return img_attn_output, txt_attn_output
class MageFlowTransformerBlock(nn.Module):
def __init__(
self,
dim: int,
num_attention_heads: int,
attention_head_dim: int,
eps: float = 1e-6,
):
super().__init__()
self.dim = dim
self.num_attention_heads = num_attention_heads
self.attention_head_dim = attention_head_dim
# Image processing modules
self.img_mod = nn.Sequential(
nn.SiLU(),
nn.Linear(
dim, 6 * dim, bias=True
), # For scale, shift, gate for norm1 and norm2
)
self.img_norm1 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
self.attn = Attention(
query_dim=dim,
added_kv_proj_dim=dim, # Enable added KV projections for text stream
dim_head=attention_head_dim,
heads=num_attention_heads,
out_dim=dim,
bias=True,
processor=MageDoubleStreamAttnProcessor(),
eps=eps,
)
self.img_norm2 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
self.img_mlp = FeedForward(
dim=dim, dim_out=dim, activation_fn="gelu-approximate"
)
# Text processing modules
self.txt_mod = nn.Sequential(
nn.SiLU(),
nn.Linear(
dim, 6 * dim, bias=True
), # For scale, shift, gate for norm1 and norm2
)
self.txt_norm1 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
# Text doesn't need separate attention - it's handled by img attn joint computation
self.txt_norm2 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
self.txt_mlp = FeedForward(
dim=dim, dim_out=dim, activation_fn="gelu-approximate"
)
def _modulate(self, x, mod_params, cu_lens=None):
"""Apply modulation to input tensor"""
shift, scale, gate = mod_params.chunk(3, dim=-1)
if cu_lens is not None:
assert x.shape[0] == 1, "x must be of shape (1, *) when cu_lens is not None"
x_flattened = x.view(-1, x.shape[-1])
lengths = cu_lens[1:] - cu_lens[:-1]
shift_t = shift.repeat_interleave(lengths, dim=0)
scale_t = scale.repeat_interleave(lengths, dim=0)
gate_t = gate.repeat_interleave(lengths, dim=0)
x_flattened = x_flattened * (1 + scale_t) + shift_t
x = x_flattened.view(x.shape)
return x, gate_t
else:
return x * (1 + scale) + shift, gate
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor,
temb: torch.Tensor,
image_rotary_emb: torch.Tensor,
txt_cu_lens: torch.Tensor,
img_cu_lens: torch.Tensor,
joint_attention_kwargs: dict = None,
):
# Get modulation parameters for both streams
img_mod_params = self.img_mod(temb) # [B, 6*dim]
txt_mod_params = self.txt_mod(temb) # [B, 6*dim]
# Split modulation parameters for norm1 and norm2
img_mod1, img_mod2 = img_mod_params.chunk(2, dim=-1) # Each [B, 3*dim]
txt_mod1, txt_mod2 = txt_mod_params.chunk(2, dim=-1) # Each [B, 3*dim]
# Process image stream - norm1 + modulation
img_normed = self.img_norm1(hidden_states)
img_modulated, img_gate1 = self._modulate(
img_normed, img_mod1, cu_lens=img_cu_lens
)
# Process text stream - norm1 + modulation
txt_normed = self.txt_norm1(encoder_hidden_states)
txt_modulated, txt_gate1 = self._modulate(
txt_normed, txt_mod1, cu_lens=txt_cu_lens
)
# Joint attention: computes QKV for both streams, applies QK norm and
# RoPE, concatenates per sample and splits the results back.
joint_attention_kwargs = joint_attention_kwargs or {}
attn_output = self.attn(
hidden_states=img_modulated,
encoder_hidden_states=txt_modulated,
image_rotary_emb=image_rotary_emb,
txt_cu_lens=txt_cu_lens,
img_cu_lens=img_cu_lens,
**joint_attention_kwargs,
)
img_attn_output, txt_attn_output = attn_output
# Apply attention gates and add residual
hidden_states = hidden_states + img_gate1 * img_attn_output
encoder_hidden_states = encoder_hidden_states + txt_gate1 * txt_attn_output
# Process image stream - norm2 + MLP
img_normed2 = self.img_norm2(hidden_states)
img_modulated2, img_gate2 = self._modulate(
img_normed2, img_mod2, cu_lens=img_cu_lens
)
img_mlp_output = self.img_mlp(img_modulated2)
hidden_states = hidden_states + img_gate2 * img_mlp_output
# Process text stream - norm2 + MLP
txt_normed2 = self.txt_norm2(encoder_hidden_states)
txt_modulated2, txt_gate2 = self._modulate(
txt_normed2, txt_mod2, cu_lens=txt_cu_lens
)
txt_mlp_output = self.txt_mlp(txt_modulated2)
encoder_hidden_states = encoder_hidden_states + txt_gate2 * txt_mlp_output
# Clip to prevent overflow for fp16
if encoder_hidden_states.dtype == torch.float16:
encoder_hidden_states = encoder_hidden_states.clip(-65504, 65504)
if hidden_states.dtype == torch.float16:
hidden_states = hidden_states.clip(-65504, 65504)
return encoder_hidden_states, hidden_states
class AdaLayerNormContinuous(nn.Module):
"""Adaptive output norm; supports packed sequences via ``cu_seqlens``."""
def __init__(
self,
embedding_dim: int,
conditioning_embedding_dim: int,
elementwise_affine=True,
eps=1e-5,
bias=True,
norm_type="layer_norm",
):
super().__init__()
self.silu = nn.SiLU()
self.linear = nn.Linear(
conditioning_embedding_dim, embedding_dim * 2, 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, elementwise_affine)
else:
raise ValueError(f"unknown norm_type {norm_type}")
def forward(
self,
x: torch.Tensor,
conditioning_embedding: torch.Tensor,
cu_seqlens: torch.Tensor = None,
) -> torch.Tensor:
emb = self.linear(self.silu(conditioning_embedding).to(x.dtype))
if cu_seqlens is None:
scale, shift = torch.chunk(emb, 2, dim=-1)
x = self.norm(x) * (1 + scale) + shift
else:
sample_lens = cu_seqlens[1:] - cu_seqlens[:-1]
flattened_x = x.view(-1, x.shape[-1])
scale, shift = torch.chunk(emb, 2, dim=-1)
scale_t = torch.repeat_interleave(scale, sample_lens, dim=0)
shift_t = torch.repeat_interleave(shift, sample_lens, dim=0)
flattened_x = self.norm(flattened_x) * (1 + scale_t) + shift_t
x = flattened_x.view(x.shape)
return x
@dataclass
class MageFlowParams:
in_channels: int
out_channels: int
context_in_dim: int
hidden_size: int
num_heads: int
depth: int
axes_dim: list
checkpoint: bool
patch_size: int = 1
class MageFlow(nn.Module):
def __init__(self, params: MageFlowParams):
super().__init__()
self.params = params
self.gradient_checkpointing = bool(params.checkpoint)
self.in_channels = params.in_channels
self.out_channels = params.out_channels
self.inner_dim = params.hidden_size # num_attention_heads * attention_head_dim
self.axes_dim = params.axes_dim
self.num_attention_heads = params.num_heads
self.attention_head_dim = self.inner_dim // self.num_attention_heads
self.patch_size = params.patch_size
assert sum(self.axes_dim) == self.attention_head_dim
self.pos_embed = MageFlowEmbedRope(
theta=10000, axes_dim=self.axes_dim, scale_rope=True
)
self.img_in = nn.Linear(self.in_channels, self.inner_dim)
self.txt_norm = RMSNorm(params.context_in_dim, eps=1e-6)
self.txt_in = nn.Linear(params.context_in_dim, self.inner_dim)
self.time_text_embed = MageFlowTimestepProjEmbeddings(
embedding_dim=self.inner_dim
)
self.transformer_blocks = nn.ModuleList(
[
MageFlowTransformerBlock(
dim=self.inner_dim,
num_attention_heads=self.num_attention_heads,
attention_head_dim=self.attention_head_dim,
)
for _ in range(params.depth)
]
)
self.norm_out = AdaLayerNormContinuous(
self.inner_dim, self.inner_dim, elementwise_affine=False, eps=1e-6
)
self.proj_out = nn.Linear(
self.inner_dim,
self.patch_size * self.patch_size * self.out_channels,
bias=True,
)
def enable_gradient_checkpointing(self, enable: bool = True):
self.gradient_checkpointing = enable
def disable_gradient_checkpointing(self):
self.gradient_checkpointing = False
def reset_rope(self):
"""Rebuild the RoPE frequency tables. Needed after a meta-device init
(the tables are plain tensors, not buffers, so ``load_state_dict`` /
``assign=True`` never materializes them)."""
self.pos_embed = MageFlowEmbedRope(
theta=10000, axes_dim=self.axes_dim, scale_rope=True
)
@property
def device(self):
return next(self.parameters()).device
@property
def dtype(self):
return next(self.parameters()).dtype
def forward(
self,
img: Tensor,
txt: Tensor,
timesteps: Tensor,
img_shapes=None,
img_cu_seqlens: Tensor = None,
txt_cu_seqlens: Tensor = None,
attention_kwargs: dict = None,
) -> Tensor:
if img.ndim != 3 or txt.ndim != 3:
raise ValueError("Input img and txt tensors must have 3 dimensions.")
# Prepare vision RoPE (msrope); text tokens are not rotated.
ms_pe = self.pos_embed(img_shapes, device=img.device)
img = self.img_in(img)
txt = self.txt_norm(txt)
timesteps = timesteps.to(img.dtype)
temb = self.time_text_embed(timesteps, img)
txt = self.txt_in(txt)
# The reference adds a zero "text vector" to temb (vec_type is null in
# the released config); kept for parity with the reference forward.
txt_vec = torch.zeros(
txt.shape[0], self.inner_dim, dtype=txt.dtype, device=txt.device
)
temb = temb + txt_vec
attention_kwargs = attention_kwargs or {}
for block in self.transformer_blocks:
if self.gradient_checkpointing and torch.is_grad_enabled():
txt, img = checkpoint(
block,
img, # hidden_states
txt, # encoder_hidden_states
temb, # temb
ms_pe, # image_rotary_emb
txt_cu_seqlens, # txt_cu_lens
img_cu_seqlens, # img_cu_lens
use_reentrant=False,
)
else:
txt, img = block(
hidden_states=img,
encoder_hidden_states=txt,
txt_cu_lens=txt_cu_seqlens,
img_cu_lens=img_cu_seqlens,
temb=temb,
image_rotary_emb=ms_pe,
joint_attention_kwargs=attention_kwargs,
)
# Use only the image part (hidden_states) from the dual-stream blocks
img = self.norm_out(img, temb, cu_seqlens=img_cu_seqlens)
img = self.proj_out(img)
return img

View File

@@ -0,0 +1,703 @@
"""
MageVAE: DConvEncoder + DConvDenoiser (with CoD Decoder) wrapper.
Vendored from the reference implementation
(github.com/microsoft/Mage, mage_flow/models/modules/mage_vae.py); loguru
swapped for prints, otherwise unchanged. Loads the kl0.1 CoD ckpt layout used
by ``vae/diffusion_pytorch_model.safetensors`` in the Mage-Flow HF repos:
encoder weights: 'student.dconv_encoder.*' (packed mean+logvar, out_ch_mult=2)
decoder weights: 'pipeline.*' (denoiser + y_embedder.decoder)
Latent shape: [B, 128, H/16, W/16] — no patch packing, no normalization.
Encode input / decode output are [-1, 1] pixel images with H, W multiples of 16.
"""
import math
import os
from functools import lru_cache
import torch
import torch.nn as nn
import torch.nn.functional as F
# ---------------------------------------------------------------------------
# Primitive layers (vendored from GenCodec, inference subset)
# ---------------------------------------------------------------------------
def nonlinearity(x):
return x * torch.sigmoid(x)
def Normalize(in_channels):
return torch.nn.GroupNorm(
num_groups=32, num_channels=in_channels, eps=1e-6, affine=True
)
def modulate(x, shift, scale):
if x.dim() == 4:
b, c = x.shape[:2]
return x * (1 + scale.view(b, c, 1, 1)) + shift.view(b, c, 1, 1)
return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
class LayerNorm2d(nn.LayerNorm):
def __init__(self, num_channels, eps=1e-6, affine=True):
super().__init__(num_channels, eps=eps, elementwise_affine=affine)
def forward(self, x):
# .contiguous() prevents a channels_last-strided NCHW view from
# propagating into downstream depthwise convs, which would otherwise
# hit a slow cuDNN path with a per-shape heuristic search.
x = x.permute(0, 2, 3, 1).contiguous()
x = F.layer_norm(x, self.normalized_shape, self.weight, self.bias, self.eps)
return x.permute(0, 3, 1, 2).contiguous()
class _EncoderLayerNorm2d(LayerNorm2d):
pass
class RMSNorm(nn.Module):
def __init__(self, hidden_size, eps=1e-6):
super().__init__()
self.weight = nn.Parameter(torch.ones(hidden_size))
self.variance_epsilon = eps
def forward(self, x):
in_dtype = x.dtype
x = x.to(torch.float32)
var = x.pow(2).mean(-1, keepdim=True)
x = x * torch.rsqrt(var + self.variance_epsilon)
return self.weight * x.to(in_dtype)
class TimestepEmbedder(nn.Module):
"""DConv-style timestep MLP (max_period=10000, freq_size=256, hidden=384)."""
def __init__(self, hidden_size, frequency_embedding_size=256):
super().__init__()
self.mlp = nn.Sequential(
nn.Linear(frequency_embedding_size, hidden_size, bias=True),
nn.SiLU(),
nn.Linear(hidden_size, hidden_size, bias=True),
)
self.frequency_embedding_size = frequency_embedding_size
@staticmethod
def timestep_embedding(t, dim, max_period=10000):
half = dim // 2
freqs = torch.exp(
-math.log(max_period) * torch.arange(0, half, dtype=torch.float32) / half
).to(t.device)
args = t[:, None].float() * freqs[None]
emb = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
if dim % 2:
emb = torch.cat([emb, torch.zeros_like(emb[:, :1])], dim=-1)
return emb
def forward(self, t):
emb = self.timestep_embedding(t, self.frequency_embedding_size)
return self.mlp(emb.to(self.mlp[0].weight.dtype))
class BottleneckPatchEmbed(nn.Module):
"""Image patch embed concatenated with a per-patch conditioning vector."""
def __init__(
self, patch_size=16, in_chans=3, pca_dim=128, embed_dim=384, bias=True
):
super().__init__()
self.proj1 = nn.Conv2d(
in_chans, pca_dim, kernel_size=patch_size, stride=patch_size, bias=False
)
self.proj2 = nn.Conv2d(pca_dim + embed_dim, embed_dim, kernel_size=1, bias=bias)
def forward(self, x, cond):
return self.proj2(torch.cat([self.proj1(x), cond], dim=1))
class DiCoBlock(nn.Module):
"""DConv block with adaLN modulation."""
def __init__(self, hidden_size, mlp_ratio=4.0):
super().__init__()
self.conv1 = nn.Conv2d(hidden_size, hidden_size, 1, bias=True)
self.conv2 = nn.Conv2d(
hidden_size, hidden_size, 3, padding=1, groups=hidden_size, bias=True
)
self.conv3 = nn.Conv2d(hidden_size, hidden_size, 1, bias=True)
self.ca = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Conv2d(hidden_size, hidden_size, 1, bias=True),
nn.Sigmoid(),
)
ffn = int(mlp_ratio * hidden_size)
self.conv4 = nn.Conv2d(hidden_size, ffn, 1, bias=True)
self.conv5 = nn.Conv2d(ffn, hidden_size, 1, bias=True)
self.norm1 = LayerNorm2d(hidden_size, affine=False)
self.norm2 = LayerNorm2d(hidden_size, affine=False)
self.adaLN_modulation = nn.Sequential(
nn.SiLU(),
nn.Linear(hidden_size, 6 * hidden_size, bias=True),
)
def forward(self, inp, c):
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
self.adaLN_modulation(c).chunk(6, dim=1)
)
x = modulate(self.norm1(inp), shift_msa, scale_msa)
x = F.gelu(self.conv2(self.conv1(x)))
x = x * self.ca(x)
x = self.conv3(x)
x = inp + gate_msa[..., None, None] * x
x = x + gate_mlp[..., None, None] * self.conv5(
F.gelu(self.conv4(modulate(self.norm2(x), shift_mlp, scale_mlp)))
)
return x
class _EncoderDiCoBlock(nn.Module):
"""DiCoBlock without adaLN, for the encoder pathway."""
def __init__(self, hidden_size, mlp_ratio=4.0):
super().__init__()
self.conv1 = nn.Conv2d(hidden_size, hidden_size, 1, bias=True)
self.conv2 = nn.Conv2d(
hidden_size, hidden_size, 3, padding=1, groups=hidden_size, bias=True
)
self.conv3 = nn.Conv2d(hidden_size, hidden_size, 1, bias=True)
self.ca = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Conv2d(hidden_size, hidden_size, 1, bias=True),
nn.Sigmoid(),
)
ffn = int(mlp_ratio * hidden_size)
self.conv4 = nn.Conv2d(hidden_size, ffn, 1, bias=True)
self.conv5 = nn.Conv2d(ffn, hidden_size, 1, bias=True)
self.norm1 = _EncoderLayerNorm2d(hidden_size)
self.norm2 = _EncoderLayerNorm2d(hidden_size)
def forward(self, inp):
x = self.norm1(inp)
x = F.gelu(self.conv2(self.conv1(x)))
x = x * self.ca(x)
x = self.conv3(x)
x = inp + x
return x + self.conv5(F.gelu(self.conv4(self.norm2(x))))
class NerfEmbedder(nn.Module):
"""Patch-position embedder used by the DConv decoder x-pathway."""
def __init__(self, in_channels, hidden_size_input, max_freqs=8):
super().__init__()
self.max_freqs = max_freqs
self.embedder = nn.Sequential(
nn.Linear(in_channels + max_freqs**2, hidden_size_input, bias=True),
)
@lru_cache
def fetch_pos(self, patch_size, device, dtype):
pos = torch.linspace(0, 1, patch_size, device=device, dtype=dtype)
pos_y, pos_x = torch.meshgrid(pos, pos, indexing="ij")
pos_x = pos_x.reshape(-1, 1, 1)
pos_y = pos_y.reshape(-1, 1, 1)
freqs = torch.linspace(
0, self.max_freqs, self.max_freqs, dtype=dtype, device=device
)
fx = freqs[None, :, None]
fy = freqs[None, None, :]
coeffs = (1 + fx * fy) ** -1
dct_x = torch.cos(pos_x * fx * torch.pi)
dct_y = torch.cos(pos_y * fy * torch.pi)
return (dct_x * dct_y * coeffs).view(1, -1, self.max_freqs**2)
def forward(self, x):
B, P2, _ = x.shape
ps = int(P2**0.5)
dct = self.fetch_pos(ps, x.device, x.dtype).expand(B, -1, -1)
return self.embedder(torch.cat([x, dct], dim=-1))
class NerfFinalLayer(nn.Module):
def __init__(self, hidden_size, out_channels):
super().__init__()
self.norm = RMSNorm(hidden_size)
self.linear = nn.Linear(hidden_size, out_channels, bias=True)
def forward(self, x):
return self.linear(self.norm(x))
class SimpleMLPAdaLN(nn.Module):
"""Final small MLP that maps NerfEmbedder features to per-patch RGB."""
def __init__(
self,
in_channels,
model_channels,
out_channels,
z_channels,
num_res_blocks,
patch_size,
):
super().__init__()
self.in_channels = in_channels
self.model_channels = model_channels
self.out_channels = out_channels
self.num_res_blocks = num_res_blocks
self.patch_size = patch_size
self.cond_embed = nn.Linear(z_channels, patch_size**2 * model_channels)
self.input_proj = nn.Linear(in_channels, model_channels)
self.res_blocks = nn.ModuleList(
_MLPResBlock(model_channels) for _ in range(num_res_blocks)
)
def forward(self, x, c):
x = self.input_proj(x)
c = self.cond_embed(c).reshape(c.shape[0], self.patch_size**2, -1)
for block in self.res_blocks:
x = block(x, c)
return x
class _MLPResBlock(nn.Module):
def __init__(self, channels):
super().__init__()
self.in_ln = nn.LayerNorm(channels, eps=1e-6)
self.mlp = nn.Sequential(
nn.Linear(channels, channels, bias=True),
nn.SiLU(),
nn.Linear(channels, channels, bias=True),
)
self.adaLN_modulation = nn.Sequential(
nn.SiLU(),
nn.Linear(channels, 3 * channels, bias=True),
)
def forward(self, x, y):
shift, scale, gate = self.adaLN_modulation(y).chunk(3, dim=-1)
h = self.in_ln(x) * (1 + scale) + shift
return x + gate * self.mlp(h)
class ResnetBlock(nn.Module):
"""GroupNorm + Conv ResBlock used by the CoD Decoder."""
def __init__(self, *, in_channels, out_channels=None, dropout=0.0):
super().__init__()
out_channels = out_channels or in_channels
self.in_channels = in_channels
self.out_channels = out_channels
self.norm1 = Normalize(in_channels)
self.conv1 = nn.Conv2d(in_channels, out_channels, 3, padding=1)
self.norm2 = Normalize(out_channels)
self.dropout = nn.Dropout(dropout)
self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1)
if in_channels != out_channels:
self.nin_shortcut = nn.Conv2d(in_channels, out_channels, 1)
def forward(self, x):
h = self.conv1(nonlinearity(self.norm1(x)))
h = self.conv2(self.dropout(nonlinearity(self.norm2(h))))
if self.in_channels != self.out_channels:
x = self.nin_shortcut(x)
return x + h
class AttnBlock(nn.Module):
"""Patched self-attention used at inference (eval mode of the original)."""
def __init__(self, in_channels, patch_size=32):
super().__init__()
self.in_channels = in_channels
self.patch_size = patch_size
self.norm = Normalize(in_channels)
self.q = nn.Conv2d(in_channels, in_channels, 1)
self.k = nn.Conv2d(in_channels, in_channels, 1)
self.v = nn.Conv2d(in_channels, in_channels, 1)
self.proj_out = nn.Conv2d(in_channels, in_channels, 1)
def forward(self, x):
h_ = self.norm(x)
Q = self.q(h_)
K = self.k(h_)
V = self.v(h_)
d = self.patch_size
b, c, H, W = Q.shape
pad_h = (d - H % d) % d
pad_w = (d - W % d) % d
if pad_h or pad_w:
Q = F.pad(Q, (0, pad_w, 0, pad_h), mode="replicate")
K = F.pad(K, (0, pad_w, 0, pad_h), mode="replicate")
V = F.pad(V, (0, pad_w, 0, pad_h), mode="replicate")
_, _, H_pad, W_pad = Q.shape
nph, npw = H_pad // d, W_pad // d
np_ = nph * npw
def to_patches(t):
return (
t.reshape(b, c, nph, d, npw, d)
.permute(0, 2, 4, 1, 3, 5)
.reshape(b * np_, c, d * d)
)
Q = to_patches(Q)
K = to_patches(K)
V = to_patches(V)
w_ = torch.bmm(Q.permute(0, 2, 1), K) * (c**-0.5)
w_ = F.softmax(w_, dim=2).permute(0, 2, 1)
h_ = (
torch.bmm(V, w_)
.reshape(b, nph, npw, c, d, d)
.permute(0, 3, 1, 4, 2, 5)
.reshape(b, c, H_pad, W_pad)
)
if pad_h or pad_w:
h_ = h_[:, :, :H, :W]
return x + self.proj_out(h_)
# ---------------------------------------------------------------------------
# adaLN constant-folding: at fixed t=0, adaLN_modulation(c) is constant.
# Replace the MLP with a buffer so DiCoBlock.forward stays unchanged.
# ---------------------------------------------------------------------------
class _ConstAdaLN(nn.Module):
def __init__(self, modulation: torch.Tensor):
super().__init__()
self.register_buffer("modulation", modulation.detach().clone())
def forward(self, c):
b = c.shape[0]
if self.modulation.shape[0] != b:
return self.modulation.expand(b, *self.modulation.shape[1:])
return self.modulation
def _replace_adaln_with_const(module: nn.Module, c: torch.Tensor) -> int:
# Only DiCoBlock is targeted: its adaLN is conditioned solely on t.
# Other adaLN_modulation submodules (e.g. _MLPResBlock in the decoder MLP)
# take a per-position latent and must not be folded.
n = 0
for child in module.modules():
if not isinstance(child, DiCoBlock):
continue
adaln = child.adaLN_modulation
if isinstance(adaln, _ConstAdaLN):
continue
with torch.no_grad():
mod = adaln(c)
child.adaLN_modulation = _ConstAdaLN(mod)
n += 1
return n
# ---------------------------------------------------------------------------
# CoD Decoder: latent → conditioning features for the denoiser
# ---------------------------------------------------------------------------
class _Decoder(nn.Module):
"""ds=16, up2x=True, light=True only."""
def __init__(self, out_ch=384, z_ch=128):
super().__init__()
self.conv_in = nn.Conv2d(z_ch, out_ch, kernel_size=3, stride=1, padding=1)
self.block = nn.Sequential(
ResnetBlock(in_channels=out_ch, out_channels=out_ch),
AttnBlock(out_ch, patch_size=32),
ResnetBlock(in_channels=out_ch, out_channels=out_ch),
AttnBlock(out_ch, patch_size=32),
ResnetBlock(in_channels=out_ch, out_channels=out_ch),
)
self.norm_out = Normalize(out_ch)
self.conv_out = nn.Conv2d(out_ch, out_ch, kernel_size=3, stride=1, padding=1)
self.ada = nn.Identity()
def forward(self, z):
h = self.block(self.conv_in(z))
h = self.conv_out(nonlinearity(self.norm_out(h)))
return self.ada(h)
# ---------------------------------------------------------------------------
# DConvEncoder: image → packed (mean, logvar) latent
# ---------------------------------------------------------------------------
class _DConvEncoder(nn.Module):
def __init__(
self,
z_ch=128,
hidden_size=384,
num_blocks=21,
patch_size=16,
mlp_ratio=4.0,
head_size=768,
num_head_blocks=2,
out_ch_mult=2,
):
super().__init__()
self.z_ch = z_ch
self.patch_size = patch_size
self.patch_cond_embed = nn.Conv2d(
3, head_size, kernel_size=patch_size, stride=patch_size, bias=True
)
self.head_blocks = nn.ModuleList(
[
_EncoderDiCoBlock(head_size, mlp_ratio=mlp_ratio)
for _ in range(num_head_blocks)
]
)
self.proj_down = nn.Conv2d(head_size, hidden_size, kernel_size=1, bias=True)
self.z_proj = nn.Conv2d(z_ch, hidden_size, kernel_size=1, bias=True)
self.fuse_proj = nn.Conv2d(
hidden_size * 2, hidden_size, kernel_size=1, bias=True
)
self.t_embedder = TimestepEmbedder(hidden_size)
self.blocks = nn.ModuleList(
[DiCoBlock(hidden_size, mlp_ratio=mlp_ratio) for _ in range(num_blocks)]
)
self.norm_out = LayerNorm2d(hidden_size)
self.proj_out = nn.Conv2d(
hidden_size, z_ch * out_ch_mult, kernel_size=1, bias=True
)
def forward_pred(self, z_t, t, y):
cond = self.patch_cond_embed(y)
for block in self.head_blocks:
cond = block(cond)
cond = self.proj_down(cond)
s = self.fuse_proj(torch.cat([cond, self.z_proj(z_t)], dim=1))
c = self.t_embedder(t.view(-1))
for block in self.blocks:
s = block(s, c)
return self.proj_out(self.norm_out(s))
# ---------------------------------------------------------------------------
# DConv denoiser: latent (via cond) + zero noise → reconstructed image
# ---------------------------------------------------------------------------
class _YEmbedder(nn.Module):
"""Holds only the CoD decoder; the original Flux2 VAE encoder side is omitted."""
def __init__(self, ch=384, z_ch=128):
super().__init__()
self.decoder = _Decoder(out_ch=ch, z_ch=z_ch)
class _DConvDenoiser(nn.Module):
def __init__(
self,
patch_size=16,
in_channels=3,
hidden_size=384,
hidden_size_x=32,
mlp_ratio=4.0,
num_blocks=24,
num_cond_blocks=21,
bottleneck_dim=128,
):
super().__init__()
self.in_channels = in_channels
self.patch_size = patch_size
self.hidden_size = hidden_size
self.num_cond_blocks = num_cond_blocks
self.t_embedder = TimestepEmbedder(hidden_size)
self.y_embedder_x = nn.Conv2d(
hidden_size, hidden_size_x * patch_size**2, 1, 1, 0
)
self.x_embedder = NerfEmbedder(
in_channels + hidden_size_x, hidden_size_x, max_freqs=8
)
self.s_embedder = BottleneckPatchEmbed(
patch_size, in_channels, bottleneck_dim, hidden_size, bias=True
)
self.blocks = nn.ModuleList(
[
DiCoBlock(hidden_size, mlp_ratio=mlp_ratio)
for _ in range(num_cond_blocks)
]
)
self.dec_net = SimpleMLPAdaLN(
in_channels=hidden_size_x,
model_channels=hidden_size_x,
out_channels=in_channels,
z_channels=hidden_size,
num_res_blocks=num_blocks - num_cond_blocks,
patch_size=patch_size,
)
self.final_layer = NerfFinalLayer(hidden_size_x, in_channels)
self.y_embedder = _YEmbedder(ch=hidden_size, z_ch=bottleneck_dim)
def forward(self, x, t, cond):
b, _, h, w = x.shape
c = self.t_embedder(t.view(-1))
s = self.s_embedder(x, cond)
for block in self.blocks:
s = block(s, c)
length = s.shape[-2] * s.shape[-1]
s = s.permute(0, 2, 3, 1).reshape(-1, self.hidden_size)
x = torch.nn.functional.unfold(
x, kernel_size=self.patch_size, stride=self.patch_size
)
x = torch.cat([x, self.y_embedder_x(cond).flatten(2)], dim=1)
x = (
x.reshape(b, -1, self.patch_size**2, length)
.permute(0, 3, 2, 1)
.flatten(0, 1)
)
x = self.x_embedder(x)
x = self.dec_net(x, s)
x = self.final_layer(x)
x = x.transpose(1, 2).reshape(b, length, -1)
return torch.nn.functional.fold(
x.transpose(1, 2).contiguous(),
(h, w),
kernel_size=self.patch_size,
stride=self.patch_size,
)
# ---------------------------------------------------------------------------
# Wrapper
# ---------------------------------------------------------------------------
def _load_state_dict(ckpt_path: str):
if ckpt_path.endswith(".safetensors"):
from safetensors.torch import load_file
return load_file(ckpt_path, device="cpu")
if os.path.exists(os.path.join(ckpt_path, "checkpoint-state_dict.pt")):
ckpt_path = os.path.join(ckpt_path, "checkpoint-state_dict.pt")
elif os.path.isdir(ckpt_path):
ckpt_path = os.path.join(ckpt_path, "checkpoint", "mp_rank_00_model_states.pt")
state = torch.load(ckpt_path, map_location="cpu")
if "module" in state:
return state["module"]
if "state_dict" in state:
return state["state_dict"]
return state
class MageVAE(nn.Module):
"""
Encode: DConvEncoder (one-step diffusion) → latent [B, 128, H/16, W/16]
Decode: DConvDenoiser + CoD Decoder → image [B, 3, H, W] in [-1, 1]
"""
latent_channels = 128
downsample_factor = 16
def __init__(self, ckpt_path: str, sample_posterior: bool = True):
super().__init__()
self.sample_posterior = sample_posterior
self.dconv_encoder = _DConvEncoder()
self.decoder_model = _DConvDenoiser()
sd = _load_state_dict(ckpt_path)
self._load_encoder(sd, ckpt_path)
self._load_decoder(sd, ckpt_path)
# adaLN modulation depends only on t, and we always run at t=0.
# Precompute and drop the MLPs once at construction (~37M params saved).
self._freeze_adaln_cache()
def _load_encoder(self, sd, ckpt_path):
prefix = "student.dconv_encoder."
enc_sd = {k[len(prefix) :]: v for k, v in sd.items() if k.startswith(prefix)}
if not enc_sd:
raise RuntimeError(f"MageVAE encoder: no '{prefix}*' keys in {ckpt_path}")
proj = enc_sd.get("proj_out.weight")
if proj is None or proj.shape[0] != 2 * self.latent_channels:
raise RuntimeError(
f"MageVAE encoder: expected packed mean+logvar (proj_out out_channels="
f"{2 * self.latent_channels}), got {None if proj is None else tuple(proj.shape)}"
)
missing, unexpected = self.dconv_encoder.load_state_dict(enc_sd, strict=False)
if missing:
print(f"MageVAE encoder missing keys: {missing[:10]}")
def _load_decoder(self, sd, ckpt_path):
prefix = "pipeline."
if not any(k.startswith(prefix) for k in sd):
raise RuntimeError(f"MageVAE decoder: no '{prefix}*' keys in {ckpt_path}")
model_dict = self.decoder_model.state_dict()
matched = {}
for k, v in sd.items():
if not k.startswith(prefix):
continue
new_k = k[len(prefix) :]
if new_k.startswith("y_embedder.encoder.") or new_k.startswith(
"y_embedder.bottleneck."
):
continue
if new_k in model_dict and model_dict[new_k].shape == v.shape:
matched[new_k] = v
self.decoder_model.load_state_dict(matched, strict=False)
if not matched:
raise RuntimeError(f"MageVAE decoder: 0 params matched from {ckpt_path}")
@torch.no_grad()
def _moments(self, x: torch.Tensor):
B, _, H, W = x.shape
ps = self.dconv_encoder.patch_size
z_t = torch.zeros(
B, self.dconv_encoder.z_ch, H // ps, W // ps, device=x.device, dtype=x.dtype
)
t = torch.zeros(B, device=x.device, dtype=x.dtype)
out = self.dconv_encoder.forward_pred(z_t, t, x)
mean = out[:, : self.latent_channels]
logvar = out[:, self.latent_channels :].clamp(min=-20.0, max=10.0)
return mean, logvar
@torch.no_grad()
def encode(self, x: torch.Tensor) -> torch.Tensor:
ps = self.dconv_encoder.patch_size
H, W = x.shape[-2], x.shape[-1]
if H % ps or W % ps:
raise ValueError(f"H, W must be multiples of {ps}, got ({H}, {W})")
mean, logvar = self._moments(x)
if self.sample_posterior:
return mean + torch.exp(0.5 * logvar) * torch.randn_like(mean)
return mean
@torch.no_grad()
def decode(self, z: torch.Tensor) -> torch.Tensor:
cond = self.decoder_model.y_embedder.decoder(z)
B = z.shape[0]
H = z.shape[2] * self.downsample_factor
W = z.shape[3] * self.downsample_factor
noise = torch.zeros(B, 3, H, W, device=z.device, dtype=z.dtype)
t = torch.zeros(B, device=z.device, dtype=z.dtype)
return self.decoder_model.forward(noise, t, cond)
@property
def device(self):
return next(self.parameters()).device
@property
def dtype(self):
return next(self.parameters()).dtype
def _freeze_adaln_cache(self):
"""Constant-fold adaLN_modulation MLPs at t=0 (encoder + decoder)."""
device = next(self.parameters()).device
dtype = next(self.parameters()).dtype
t = torch.zeros(1, device=device, dtype=dtype)
c_enc = self.dconv_encoder.t_embedder(t)
_replace_adaln_with_const(self.dconv_encoder, c_enc)
c_dec = self.decoder_model.t_embedder(t)
_replace_adaln_with_const(self.decoder_model, c_dec)