mage-flow / mage_flow /models /mage_flow.py
multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
f8d22a5 verified
Raw
History Blame Contribute Delete
16 kB
from dataclasses import dataclass
from typing import Any
import torch
import torch.nn as nn
from einops import rearrange, repeat
from loguru import logger
from pydantic import BaseModel, Field
from torch import Tensor
from .modules._attn_backend import set_attn_backend
from .modules.mage_layers import (
AdaLayerNormContinuous,
MageFlowEmbedRope,
MageFlowTimestepProjEmbeddings,
MageFlowTransformerBlock,
RMSNorm,
)
from .modules.text_encoder import TextEncoder, qwen3_patch_forward
class ModelConfig(BaseModel):
static_shift: float = Field(
default=6.0,
description="Static shift value for the z-image time-shift schedule (the only "
"supported schedule). Default: 6.0.",
)
vae_path: str = Field(...)
model_structure: dict = Field(default_factory=dict)
txt_enc_path: str = Field(...)
txt_max_length: int = Field(default=4096)
pretrained_model_name_or_path: str | None = Field(default=None)
pretrained_full_model_path: str | None = Field(default=None) # Load full model weights (DiT + txt_enc + vae)
packing: bool = Field(default=False)
vae_sample_posterior: bool = Field(default=True) # Sample (vs mode) from VAE posterior at encode time (Flux2 + CoD)
vae_encoder_only: bool = Field(default=False) # Skip loading VAE decoder to save GPU memory (training only, MageVAE)
compile_vae_encoder: bool = Field(default=False) # torch.compile VAE encoder to reduce CUDA kernel launch overhead
attn_type: str = Field(
default="flash2",
description="Flash-attn backend used by both the DiT (mage_layers) "
"and the HF text encoder (text_encoder). One of: 'flash2' (default) or 'flash4'.",
)
@dataclass
class MageFlowParams:
in_channels: int
out_channels: int
context_in_dim: int
hidden_size: int
num_heads: int
depth: int
axes_dim: list[int]
checkpoint: bool
patch_size: int = 1
class MageFlow(nn.Module):
def __init__(self, params: MageFlowParams):
super().__init__()
self.params = params
self.checkpoint = 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 forward(
self,
img: Tensor,
txt: Tensor,
timesteps: Tensor,
img_shapes=None,
img_cu_seqlens: Tensor | None = None,
txt_cu_seqlens: Tensor | None = None,
attention_kwargs: dict[str, Any] | None = 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)
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 _index_block, block in enumerate(self.transformer_blocks):
if self.training and self.checkpoint:
txt, img = torch.utils.checkpoint.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
class MageFlowModel(nn.Module):
def __init__(self, config: ModelConfig):
super().__init__()
self.config = config
set_attn_backend(getattr(config, "attn_type", "flash2"))
self.patch_text_encoder_forward()
self.vae = self.load_vae()
self.transformer = self.load_transformer()
self.txt_enc = self.load_text_enc()
# Optionally override all components from a full model checkpoint (e.g. ema.pt)
full_path = getattr(self.config, "pretrained_full_model_path", None)
if full_path is not None:
import os
if os.path.exists(full_path):
logger.info(f"Loading full model weights from {full_path}")
sd = torch.load(full_path, map_location="cpu")
# Handle wrapped EMA format: {'ema_state_dict': ..., ...}
if isinstance(sd, dict) and "ema_state_dict" in sd:
sd = sd["ema_state_dict"]
missing, unexpected = self.load_state_dict(sd, strict=False)
if missing:
logger.warning(f"Full model load missing keys ({len(missing)}): {missing[:5]}...")
if unexpected:
logger.warning(f"Full model load unexpected keys ({len(unexpected)}): {unexpected[:5]}...")
logger.info("Full model weights loaded successfully.")
else:
logger.warning(f"pretrained_full_model_path not found: {full_path}")
# Freeze VAE and Text Encoder
self.vae.requires_grad_(False)
# Drop VAE decoder to save GPU memory (training only, decoder unused during training)
if self.config.vae_encoder_only:
from .modules.mage_vae import MageVAE
if isinstance(self.vae, MageVAE):
decoder_params = sum(p.numel() for p in self.vae.decoder_model.parameters()) / 1e6
self.vae.decoder_model = None
elif hasattr(self.vae, "decoder"):
decoder_params = sum(p.numel() for p in self.vae.decoder.parameters()) / 1e6
self.vae.decoder = None
else:
decoder_params = 0
logger.info(f"vae_encoder_only=True: dropped VAE decoder ({decoder_params:.1f}M params) to save memory")
# NOTE: VAE encoder torch.compile() is deferred to
# maybe_compile_vae_encoder(), called after checkpoint load. Reason:
# avoid wasted compile work before load_checkpoint overwrites weights.
# The save-side _unwrap_compiled_submodules guard in DeepSpeedTrainer
# is a belt-and-suspenders defense against any future code that
# re-introduces the function-style ``module = torch.compile(module)``
# pattern (which does pollute state_dict with ``_orig_mod.``).
# Text encoder is always frozen (inference only).
self.txt_enc.requires_grad_(False)
logger.info(
f"{sum([p.numel() for p in self.transformer.parameters() if p.requires_grad]) / 1000000} M parameters"
)
def patch_text_encoder_forward(self):
qwen3_patch_forward()
logger.info("Patched Qwen3-VL text encoder forward methods")
def maybe_compile_vae_encoder(self) -> None:
"""Compile the VAE encoder with torch.compile() to fuse small ops and
reduce CUDA kernel launch overhead.
Uses ``nn.Module.compile()`` (in-place) for the encoder so the module
hierarchy and parameter names are unchanged — ``state_dict()`` keeps
clean keys (no ``_orig_mod.`` prefix), and checkpoints stay
interchangeable with the non-compiled path.
For the MageVAE branch we still assign ``torch.compile(...)`` to a
method (``_encode_moments``); methods aren't ``nn.Module``s so this
does not pollute ``state_dict()``.
Idempotent: safe to call multiple times; already-compiled modules are
detected and skipped.
"""
if not getattr(self.config, "compile_vae_encoder", False):
return
torch.set_float32_matmul_precision("high")
from .modules.mage_vae import MageVAE
if isinstance(self.vae, MageVAE):
fn = self.vae._encode_moments
if hasattr(fn, "_torchdynamo_orig_callable") or hasattr(fn, "_orig_mod"):
return # already compiled
self.vae._encode_moments = torch.compile(fn, dynamic=True)
logger.info("compile_vae_encoder=True: compiled MageVAE._encode_moments")
elif hasattr(self.vae, "encoder"):
if getattr(self.vae.encoder, "_compiled_call_impl", None) is not None:
return # already compiled
self.vae.encoder.compile()
logger.info("compile_vae_encoder=True: compiled VAE encoder (in-place)")
def load_text_enc(self):
return TextEncoder(
model_name=self.config.txt_enc_path,
version=self.config.txt_enc_path,
tokenizer_max_length=self.config.txt_max_length,
torch_dtype=torch.bfloat16,
prompt_template=None,
dit_structure=self.config.model_structure,
use_packed_text_infer=self.config.packing,
attn_type=getattr(self.config, "attn_type", "flash2"),
)
def load_vae(self):
from .modules.mage_vae import MageVAE
return MageVAE(
ckpt_path=self.config.vae_path,
sample_posterior=self.config.vae_sample_posterior,
)
def load_transformer(self):
# Imported lazily to avoid a circular import: ``utils`` imports MageFlow /
# MageFlowParams from this module.
from .utils import load_model
return load_model(
dit_structure=self.config.model_structure,
pretrain_path=self.config.pretrained_model_name_or_path,
)
def compile(self):
self.transformer.compile()
def compute_vae_encodings(
self,
pixel_values: torch.Tensor | list[torch.Tensor],
with_ids: bool = True,
):
if isinstance(pixel_values, list):
# All same resolution → batch encode via the tensor path
if len(pixel_values) > 1 and len({img.shape for img in pixel_values}) == 1:
stacked = torch.stack(pixel_values, dim=0)
result = self.compute_vae_encodings(stacked, with_ids=with_ids)
# Repack from [N, L, C] batch format to [1, N*L, C] packed format
if with_ids:
model_input, img_shapes, img_ids = result
model_input = model_input.reshape(1, -1, model_input.shape[-1])
img_ids = img_ids.reshape(1, -1, img_ids.shape[-1])
return model_input, img_shapes, img_ids
model_input, img_shapes = result
model_input = model_input.reshape(1, -1, model_input.shape[-1])
return model_input, img_shapes
# Packed / variable-size images
model_inputs = []
img_shapes = []
img_ids_list = []
def _append(latents):
_, _, h, w = latents.shape
img_shapes.append([(1, h, w)])
model_inputs.append(rearrange(latents, "b c h w -> b (h w) c").squeeze(0))
if with_ids:
ids = torch.zeros(h, w, 3, device=latents.device)
ids[..., 1] = ids[..., 1] + torch.arange(h, device=latents.device)[:, None]
ids[..., 2] = ids[..., 2] + torch.arange(w, device=latents.device)[None, :]
img_ids_list.append(rearrange(ids, "h w c -> (h w) c"))
# MageVAE encoder is launch-bound on B=1; group same-shape images
# in the pack into one batched encode call.
if len(pixel_values) > 1:
groups: dict[tuple[int, int], list[int]] = {}
for i, img in enumerate(pixel_values):
key = (int(img.shape[-2]), int(img.shape[-1]))
groups.setdefault(key, []).append(i)
latents_per_idx = [None] * len(pixel_values)
for (h, w), idxs in groups.items():
batch = torch.stack([pixel_values[i] for i in idxs], dim=0)
batch = batch.to(memory_format=torch.contiguous_format).float()
batch = batch.to(self.vae.device, dtype=self.vae.dtype)
with torch.no_grad():
lat = self.vae.encode(batch) # [B, 128, H/16, W/16]
for j, i in enumerate(idxs):
latents_per_idx[i] = lat[j:j + 1]
for latents in latents_per_idx:
_append(latents)
else:
for img in pixel_values:
img = img.unsqueeze(0).to(memory_format=torch.contiguous_format).float()
img = img.to(self.vae.device, dtype=self.vae.dtype)
with torch.no_grad():
latents = self.vae.encode(img) # [1, 128, H/16, W/16]
_append(latents)
model_input = torch.cat(model_inputs, dim=0).unsqueeze(0)
if with_ids:
img_ids = torch.cat(img_ids_list, dim=0).unsqueeze(0)
return model_input, img_shapes, img_ids
return model_input, img_shapes
# Tensor (padded batch)
pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float()
pixel_values = pixel_values.to(self.vae.device, dtype=self.vae.dtype)
with torch.no_grad():
model_input = self.vae.encode(pixel_values) # [B, 128, H/16, W/16]
bs, c, h, w = model_input.shape
img_shapes = [[(1, h, w)]] * bs
model_input = rearrange(model_input, "b c h w -> b (h w) c")
if with_ids:
img_ids = torch.zeros(h, w, 3, device=model_input.device)
img_ids[..., 1] = img_ids[..., 1] + torch.arange(h, device=model_input.device)[:, None]
img_ids[..., 2] = img_ids[..., 2] + torch.arange(w, device=model_input.device)[None, :]
img_ids = repeat(img_ids, "h w c -> b (h w) c", b=bs)
return model_input, img_shapes, img_ids
return model_input, img_shapes