ajh-code commited on
Commit
2c994a0
·
verified ·
1 Parent(s): c847ee2

Add vendor/mage_flow/models/mage_flow.py

Browse files
Files changed (1) hide show
  1. vendor/mage_flow/models/mage_flow.py +364 -0
vendor/mage_flow/models/mage_flow.py ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+
4
+ import torch
5
+ import torch.nn as nn
6
+ from einops import rearrange, repeat
7
+ from loguru import logger
8
+ from pydantic import BaseModel, Field
9
+ from torch import Tensor
10
+
11
+ from .modules._attn_backend import set_attn_backend
12
+ from .modules.mage_layers import (
13
+ AdaLayerNormContinuous,
14
+ MageFlowEmbedRope,
15
+ MageFlowTimestepProjEmbeddings,
16
+ MageFlowTransformerBlock,
17
+ RMSNorm,
18
+ )
19
+ from .modules.text_encoder import TextEncoder, qwen3_patch_forward
20
+
21
+
22
+ class ModelConfig(BaseModel):
23
+ static_shift: float = Field(
24
+ default=6.0,
25
+ description="Static shift value for the z-image time-shift schedule (the only "
26
+ "supported schedule). Default: 6.0.",
27
+ )
28
+ vae_path: str = Field(...)
29
+ model_structure: dict = Field(default_factory=dict)
30
+ txt_enc_path: str = Field(...)
31
+ txt_max_length: int = Field(default=4096)
32
+ pretrained_model_name_or_path: str | None = Field(default=None)
33
+ pretrained_full_model_path: str | None = Field(default=None) # Load full model weights (DiT + txt_enc + vae)
34
+ packing: bool = Field(default=False)
35
+ vae_sample_posterior: bool = Field(default=True) # Sample (vs mode) from VAE posterior at encode time (Flux2 + CoD)
36
+ vae_encoder_only: bool = Field(default=False) # Skip loading VAE decoder to save GPU memory (training only, MageVAE)
37
+ compile_vae_encoder: bool = Field(default=False) # torch.compile VAE encoder to reduce CUDA kernel launch overhead
38
+ attn_type: str = Field(
39
+ default="flash2",
40
+ description="Flash-attn backend used by both the DiT (mage_layers) "
41
+ "and the HF text encoder (text_encoder). One of: 'flash2' (default) or 'flash4'.",
42
+ )
43
+
44
+
45
+ @dataclass
46
+ class MageFlowParams:
47
+ in_channels: int
48
+ out_channels: int
49
+ context_in_dim: int
50
+ hidden_size: int
51
+ num_heads: int
52
+ depth: int
53
+ axes_dim: list[int]
54
+ checkpoint: bool
55
+ patch_size: int = 1
56
+
57
+
58
+ class MageFlow(nn.Module):
59
+ def __init__(self, params: MageFlowParams):
60
+ super().__init__()
61
+ self.params = params
62
+ self.checkpoint = params.checkpoint
63
+ self.in_channels = params.in_channels
64
+ self.out_channels = params.out_channels
65
+ self.inner_dim = params.hidden_size # num_attention_heads * attention_head_dim
66
+ self.axes_dim = params.axes_dim
67
+ self.num_attention_heads = params.num_heads
68
+ self.attention_head_dim = self.inner_dim // self.num_attention_heads
69
+ self.patch_size = params.patch_size
70
+ assert sum(self.axes_dim) == self.attention_head_dim
71
+
72
+ self.pos_embed = MageFlowEmbedRope(theta=10000, axes_dim=self.axes_dim, scale_rope=True)
73
+ self.img_in = nn.Linear(self.in_channels, self.inner_dim)
74
+ self.txt_norm = RMSNorm(params.context_in_dim, eps=1e-6)
75
+ self.txt_in = nn.Linear(params.context_in_dim, self.inner_dim)
76
+
77
+ self.time_text_embed = MageFlowTimestepProjEmbeddings(embedding_dim=self.inner_dim)
78
+
79
+ self.transformer_blocks = nn.ModuleList(
80
+ [
81
+ MageFlowTransformerBlock(
82
+ dim=self.inner_dim,
83
+ num_attention_heads=self.num_attention_heads,
84
+ attention_head_dim=self.attention_head_dim,
85
+ )
86
+ for _ in range(params.depth)
87
+ ]
88
+ )
89
+
90
+ self.norm_out = AdaLayerNormContinuous(self.inner_dim, self.inner_dim, elementwise_affine=False, eps=1e-6)
91
+ self.proj_out = nn.Linear(self.inner_dim, self.patch_size * self.patch_size * self.out_channels, bias=True)
92
+
93
+ def forward(
94
+ self,
95
+ img: Tensor,
96
+ txt: Tensor,
97
+ timesteps: Tensor,
98
+ img_shapes=None,
99
+ img_cu_seqlens: Tensor | None = None,
100
+ txt_cu_seqlens: Tensor | None = None,
101
+ attention_kwargs: dict[str, Any] | None = None,
102
+ ) -> Tensor:
103
+ if img.ndim != 3 or txt.ndim != 3:
104
+ raise ValueError("Input img and txt tensors must have 3 dimensions.")
105
+
106
+ # Prepare vision RoPE (msrope); text tokens are not rotated.
107
+ ms_pe = self.pos_embed(img_shapes, device=img.device)
108
+
109
+ img = self.img_in(img)
110
+ txt = self.txt_norm(txt)
111
+
112
+ timesteps = timesteps.to(img.dtype)
113
+ temb = self.time_text_embed(timesteps, img)
114
+
115
+ txt = self.txt_in(txt)
116
+ txt_vec = torch.zeros(txt.shape[0], self.inner_dim, dtype=txt.dtype, device=txt.device)
117
+
118
+ temb = temb + txt_vec
119
+
120
+ attention_kwargs = attention_kwargs or {}
121
+
122
+ for _index_block, block in enumerate(self.transformer_blocks):
123
+ if self.training and self.checkpoint:
124
+ txt, img = torch.utils.checkpoint.checkpoint(
125
+ block,
126
+ img, # hidden_states
127
+ txt, # encoder_hidden_states
128
+ temb, # temb
129
+ ms_pe, # image_rotary_emb
130
+ txt_cu_seqlens, # txt_cu_lens
131
+ img_cu_seqlens, # img_cu_lens
132
+ use_reentrant=False,
133
+ )
134
+
135
+ else:
136
+ txt, img = block(
137
+ hidden_states=img,
138
+ encoder_hidden_states=txt,
139
+ txt_cu_lens=txt_cu_seqlens,
140
+ img_cu_lens=img_cu_seqlens,
141
+ temb=temb,
142
+ image_rotary_emb=ms_pe,
143
+ joint_attention_kwargs=attention_kwargs,
144
+ )
145
+
146
+ # Use only the image part (hidden_states) from the dual-stream blocks
147
+ img = self.norm_out(
148
+ img,
149
+ temb,
150
+ cu_seqlens=img_cu_seqlens,
151
+ )
152
+ img = self.proj_out(img)
153
+ return img
154
+
155
+
156
+ class MageFlowModel(nn.Module):
157
+ def __init__(self, config: ModelConfig):
158
+ super().__init__()
159
+ self.config = config
160
+ set_attn_backend(getattr(config, "attn_type", "flash2"))
161
+ self.patch_text_encoder_forward()
162
+ self.vae = self.load_vae()
163
+ self.transformer = self.load_transformer()
164
+ self.txt_enc = self.load_text_enc()
165
+
166
+ # Optionally override all components from a full model checkpoint (e.g. ema.pt)
167
+ full_path = getattr(self.config, "pretrained_full_model_path", None)
168
+ if full_path is not None:
169
+ import os
170
+
171
+ if os.path.exists(full_path):
172
+ logger.info(f"Loading full model weights from {full_path}")
173
+ sd = torch.load(full_path, map_location="cpu")
174
+ # Handle wrapped EMA format: {'ema_state_dict': ..., ...}
175
+ if isinstance(sd, dict) and "ema_state_dict" in sd:
176
+ sd = sd["ema_state_dict"]
177
+ missing, unexpected = self.load_state_dict(sd, strict=False)
178
+ if missing:
179
+ logger.warning(f"Full model load missing keys ({len(missing)}): {missing[:5]}...")
180
+ if unexpected:
181
+ logger.warning(f"Full model load unexpected keys ({len(unexpected)}): {unexpected[:5]}...")
182
+ logger.info("Full model weights loaded successfully.")
183
+ else:
184
+ logger.warning(f"pretrained_full_model_path not found: {full_path}")
185
+
186
+ # Freeze VAE and Text Encoder
187
+ self.vae.requires_grad_(False)
188
+
189
+ # Drop VAE decoder to save GPU memory (training only, decoder unused during training)
190
+ if self.config.vae_encoder_only:
191
+ from .modules.mage_vae import MageVAE
192
+ if isinstance(self.vae, MageVAE):
193
+ decoder_params = sum(p.numel() for p in self.vae.decoder_model.parameters()) / 1e6
194
+ self.vae.decoder_model = None
195
+ elif hasattr(self.vae, "decoder"):
196
+ decoder_params = sum(p.numel() for p in self.vae.decoder.parameters()) / 1e6
197
+ self.vae.decoder = None
198
+ else:
199
+ decoder_params = 0
200
+ logger.info(f"vae_encoder_only=True: dropped VAE decoder ({decoder_params:.1f}M params) to save memory")
201
+
202
+ # NOTE: VAE encoder torch.compile() is deferred to
203
+ # maybe_compile_vae_encoder(), called after checkpoint load. Reason:
204
+ # avoid wasted compile work before load_checkpoint overwrites weights.
205
+ # The save-side _unwrap_compiled_submodules guard in DeepSpeedTrainer
206
+ # is a belt-and-suspenders defense against any future code that
207
+ # re-introduces the function-style ``module = torch.compile(module)``
208
+ # pattern (which does pollute state_dict with ``_orig_mod.``).
209
+
210
+ # Text encoder is always frozen (inference only).
211
+ self.txt_enc.requires_grad_(False)
212
+ logger.info(
213
+ f"{sum([p.numel() for p in self.transformer.parameters() if p.requires_grad]) / 1000000} M parameters"
214
+ )
215
+
216
+ def patch_text_encoder_forward(self):
217
+ qwen3_patch_forward()
218
+ logger.info("Patched Qwen3-VL text encoder forward methods")
219
+
220
+ def maybe_compile_vae_encoder(self) -> None:
221
+ """Compile the VAE encoder with torch.compile() to fuse small ops and
222
+ reduce CUDA kernel launch overhead.
223
+
224
+ Uses ``nn.Module.compile()`` (in-place) for the encoder so the module
225
+ hierarchy and parameter names are unchanged — ``state_dict()`` keeps
226
+ clean keys (no ``_orig_mod.`` prefix), and checkpoints stay
227
+ interchangeable with the non-compiled path.
228
+
229
+ For the MageVAE branch we still assign ``torch.compile(...)`` to a
230
+ method (``_encode_moments``); methods aren't ``nn.Module``s so this
231
+ does not pollute ``state_dict()``.
232
+
233
+ Idempotent: safe to call multiple times; already-compiled modules are
234
+ detected and skipped.
235
+ """
236
+ if not getattr(self.config, "compile_vae_encoder", False):
237
+ return
238
+ torch.set_float32_matmul_precision("high")
239
+ from .modules.mage_vae import MageVAE
240
+ if isinstance(self.vae, MageVAE):
241
+ fn = self.vae._encode_moments
242
+ if hasattr(fn, "_torchdynamo_orig_callable") or hasattr(fn, "_orig_mod"):
243
+ return # already compiled
244
+ self.vae._encode_moments = torch.compile(fn, dynamic=True)
245
+ logger.info("compile_vae_encoder=True: compiled MageVAE._encode_moments")
246
+ elif hasattr(self.vae, "encoder"):
247
+ if getattr(self.vae.encoder, "_compiled_call_impl", None) is not None:
248
+ return # already compiled
249
+ self.vae.encoder.compile()
250
+ logger.info("compile_vae_encoder=True: compiled VAE encoder (in-place)")
251
+
252
+ def load_text_enc(self):
253
+ return TextEncoder(
254
+ model_name=self.config.txt_enc_path,
255
+ version=self.config.txt_enc_path,
256
+ tokenizer_max_length=self.config.txt_max_length,
257
+ torch_dtype=torch.bfloat16,
258
+ prompt_template=None,
259
+ dit_structure=self.config.model_structure,
260
+ use_packed_text_infer=self.config.packing,
261
+ attn_type=getattr(self.config, "attn_type", "flash2"),
262
+ )
263
+
264
+ def load_vae(self):
265
+ from .modules.mage_vae import MageVAE
266
+ return MageVAE(
267
+ ckpt_path=self.config.vae_path,
268
+ sample_posterior=self.config.vae_sample_posterior,
269
+ )
270
+
271
+ def load_transformer(self):
272
+ # Imported lazily to avoid a circular import: ``utils`` imports MageFlow /
273
+ # MageFlowParams from this module.
274
+ from .utils import load_model
275
+ return load_model(
276
+ dit_structure=self.config.model_structure,
277
+ pretrain_path=self.config.pretrained_model_name_or_path,
278
+ )
279
+
280
+ def compile(self):
281
+ self.transformer.compile()
282
+
283
+ def compute_vae_encodings(
284
+ self,
285
+ pixel_values: torch.Tensor | list[torch.Tensor],
286
+ with_ids: bool = True,
287
+ ):
288
+ if isinstance(pixel_values, list):
289
+ # All same resolution → batch encode via the tensor path
290
+ if len(pixel_values) > 1 and len({img.shape for img in pixel_values}) == 1:
291
+ stacked = torch.stack(pixel_values, dim=0)
292
+ result = self.compute_vae_encodings(stacked, with_ids=with_ids)
293
+ # Repack from [N, L, C] batch format to [1, N*L, C] packed format
294
+ if with_ids:
295
+ model_input, img_shapes, img_ids = result
296
+ model_input = model_input.reshape(1, -1, model_input.shape[-1])
297
+ img_ids = img_ids.reshape(1, -1, img_ids.shape[-1])
298
+ return model_input, img_shapes, img_ids
299
+ model_input, img_shapes = result
300
+ model_input = model_input.reshape(1, -1, model_input.shape[-1])
301
+ return model_input, img_shapes
302
+
303
+ # Packed / variable-size images
304
+ model_inputs = []
305
+ img_shapes = []
306
+ img_ids_list = []
307
+
308
+ def _append(latents):
309
+ _, _, h, w = latents.shape
310
+ img_shapes.append([(1, h, w)])
311
+ model_inputs.append(rearrange(latents, "b c h w -> b (h w) c").squeeze(0))
312
+ if with_ids:
313
+ ids = torch.zeros(h, w, 3, device=latents.device)
314
+ ids[..., 1] = ids[..., 1] + torch.arange(h, device=latents.device)[:, None]
315
+ ids[..., 2] = ids[..., 2] + torch.arange(w, device=latents.device)[None, :]
316
+ img_ids_list.append(rearrange(ids, "h w c -> (h w) c"))
317
+
318
+ # MageVAE encoder is launch-bound on B=1; group same-shape images
319
+ # in the pack into one batched encode call.
320
+ if len(pixel_values) > 1:
321
+ groups: dict[tuple[int, int], list[int]] = {}
322
+ for i, img in enumerate(pixel_values):
323
+ key = (int(img.shape[-2]), int(img.shape[-1]))
324
+ groups.setdefault(key, []).append(i)
325
+ latents_per_idx = [None] * len(pixel_values)
326
+ for (h, w), idxs in groups.items():
327
+ batch = torch.stack([pixel_values[i] for i in idxs], dim=0)
328
+ batch = batch.to(memory_format=torch.contiguous_format).float()
329
+ batch = batch.to(self.vae.device, dtype=self.vae.dtype)
330
+ with torch.no_grad():
331
+ lat = self.vae.encode(batch) # [B, 128, H/16, W/16]
332
+ for j, i in enumerate(idxs):
333
+ latents_per_idx[i] = lat[j:j + 1]
334
+ for latents in latents_per_idx:
335
+ _append(latents)
336
+ else:
337
+ for img in pixel_values:
338
+ img = img.unsqueeze(0).to(memory_format=torch.contiguous_format).float()
339
+ img = img.to(self.vae.device, dtype=self.vae.dtype)
340
+ with torch.no_grad():
341
+ latents = self.vae.encode(img) # [1, 128, H/16, W/16]
342
+ _append(latents)
343
+
344
+ model_input = torch.cat(model_inputs, dim=0).unsqueeze(0)
345
+ if with_ids:
346
+ img_ids = torch.cat(img_ids_list, dim=0).unsqueeze(0)
347
+ return model_input, img_shapes, img_ids
348
+ return model_input, img_shapes
349
+
350
+ # Tensor (padded batch)
351
+ pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float()
352
+ pixel_values = pixel_values.to(self.vae.device, dtype=self.vae.dtype)
353
+ with torch.no_grad():
354
+ model_input = self.vae.encode(pixel_values) # [B, 128, H/16, W/16]
355
+ bs, c, h, w = model_input.shape
356
+ img_shapes = [[(1, h, w)]] * bs
357
+ model_input = rearrange(model_input, "b c h w -> b (h w) c")
358
+ if with_ids:
359
+ img_ids = torch.zeros(h, w, 3, device=model_input.device)
360
+ img_ids[..., 1] = img_ids[..., 1] + torch.arange(h, device=model_input.device)[:, None]
361
+ img_ids[..., 2] = img_ids[..., 2] + torch.arange(w, device=model_input.device)[None, :]
362
+ img_ids = repeat(img_ids, "h w c -> b (h w) c", b=bs)
363
+ return model_input, img_shapes, img_ids
364
+ return model_input, img_shapes