Spaces:
Running on Zero
Running on Zero
| 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'.", | |
| ) | |
| 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 | |