multimodalart HF Staff commited on
Commit
f8d22a5
·
verified ·
1 Parent(s): 5a26714

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ mage_flow/assets/cuisine.jpg filter=lfs diff=lfs merge=lfs -text
37
+ mage_flow/assets/dog.jpg filter=lfs diff=lfs merge=lfs -text
38
+ mage_flow/assets/portrait.jpg filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,41 @@
1
  ---
2
- title: Mage Flow
3
- emoji: 🏢
4
- colorFrom: gray
5
- colorTo: pink
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Mage-Flow
3
+ emoji: 🎨
4
+ colorFrom: green
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 6.15.1
 
8
  app_file: app.py
9
+ short_description: Efficient native-resolution image generation and editing
10
+ python_version: "3.12"
11
+ startup_duration_timeout: 30m
12
  ---
13
 
14
+ # Mage-Flow
15
+
16
+ Efficient Native-Resolution Foundation Model for Image Generation and Editing.
17
+
18
+ This Space demonstrates [Mage-Flow](https://huggingface.co/papers/2607.19064) from Microsoft, a 4B-scale image generation and editing foundation model. It supports:
19
+
20
+ - **Text → Image**: Generate images from text prompts at arbitrary resolutions.
21
+ - **Image Edit**: Instruction-based editing of reference images.
22
+
23
+ The demo uses the Turbo variants ([Mage-Flow-Turbo](https://huggingface.co/microsoft/Mage-Flow-Turbo) and [Mage-Flow-Edit-Turbo](https://huggingface.co/microsoft/Mage-Flow-Edit-Turbo)) for fast, few-step inference (4 steps, CFG 1.0).
24
+
25
+ ## Usage
26
+
27
+ ### Text → Image
28
+ 1. Enter a descriptive prompt.
29
+ 2. Adjust steps (Turbo default: 4) and CFG (Turbo default: 1.0).
30
+ 3. Click **Generate**.
31
+
32
+ ### Image Edit
33
+ 1. Upload a reference image.
34
+ 2. Enter an edit instruction (e.g., "change the background to a city street").
35
+ 3. Click **Edit**.
36
+
37
+ ## References
38
+
39
+ - Paper: [Mage-Flow: An Efficient Native-Resolution Foundation Model for Image Generation and Editing](https://huggingface.co/papers/2607.19064)
40
+ - GitHub: [microsoft/Mage](https://github.com/microsoft/Mage)
41
+ - Models: [microsoft/Mage-Flow-Turbo](https://huggingface.co/microsoft/Mage-Flow-Turbo), [microsoft/Mage-Flow-Edit-Turbo](https://huggingface.co/microsoft/Mage-Flow-Edit-Turbo)
app.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mage-Flow: Efficient Native-Resolution Foundation Model for Image Generation and Editing.
2
+
3
+ Gradio Space demo supporting text-to-image generation and instruction-based image editing
4
+ using Microsoft's Mage-Flow model (Turbo variants for fast inference).
5
+ """
6
+ import os
7
+
8
+ os.environ.setdefault("VF_HF_ATTN_IMPL", "sdpa")
9
+
10
+ import spaces # MUST be first (after env setup)
11
+ import torch
12
+ import gradio as gr
13
+ from PIL import Image
14
+
15
+ from mage_flow.pipeline import MageFlowPipeline
16
+
17
+ T2I_MODEL = "microsoft/Mage-Flow-Turbo"
18
+ EDIT_MODEL = "microsoft/Mage-Flow-Edit-Turbo"
19
+
20
+ pipe_t2i = MageFlowPipeline.from_pretrained(T2I_MODEL, device="cuda")
21
+ pipe_edit = MageFlowPipeline.from_pretrained(EDIT_MODEL, device="cuda")
22
+
23
+
24
+ @spaces.GPU(duration=120)
25
+ def generate(
26
+ prompt: str,
27
+ negative_prompt: str,
28
+ steps: int,
29
+ cfg: float,
30
+ height: int,
31
+ width: int,
32
+ seed: int,
33
+ progress=gr.Progress(track_tqdm=True),
34
+ ):
35
+ """Generate an image from a text prompt using Mage-Flow-Turbo.
36
+
37
+ Args:
38
+ prompt: Text description of the image to generate.
39
+ negative_prompt: What to avoid in the generation.
40
+ steps: Number of denoising steps (Turbo uses 4).
41
+ cfg: Classifier-free guidance scale (Turbo uses 1.0).
42
+ height: Output image height (multiple of 16).
43
+ width: Output image width (multiple of 16).
44
+ seed: Random seed for reproducibility.
45
+ """
46
+ if not (prompt or "").strip():
47
+ raise gr.Error("Prompt is empty.")
48
+ img = pipe_t2i.generate(
49
+ [prompt],
50
+ neg_prompts=[negative_prompt or " "],
51
+ seeds=[int(seed)],
52
+ steps=int(steps),
53
+ cfg=float(cfg),
54
+ heights=[int(height)],
55
+ widths=[int(width)],
56
+ )[0]
57
+ return img
58
+
59
+
60
+ @spaces.GPU(duration=120)
61
+ def edit(
62
+ prompt: str,
63
+ negative_prompt: str,
64
+ ref_img,
65
+ steps: int,
66
+ cfg: float,
67
+ max_size: int,
68
+ seed: int,
69
+ progress=gr.Progress(track_tqdm=True),
70
+ ):
71
+ """Edit a reference image using an instruction prompt with Mage-Flow-Edit-Turbo.
72
+
73
+ Args:
74
+ prompt: Edit instruction describing how to modify the image.
75
+ negative_prompt: What to avoid in the edited result.
76
+ ref_img: Reference image to edit.
77
+ steps: Number of denoising steps (Turbo uses 4).
78
+ cfg: Classifier-free guidance scale.
79
+ max_size: Longest side of output (0 = keep source resolution).
80
+ seed: Random seed for reproducibility.
81
+ """
82
+ if not (prompt or "").strip():
83
+ raise gr.Error("Edit instruction is empty.")
84
+ if ref_img is None:
85
+ raise gr.Error("Upload a reference image to edit.")
86
+ if isinstance(ref_img, str):
87
+ ref_img = Image.open(ref_img)
88
+ refs = [ref_img.convert("RGB")]
89
+ out = pipe_edit.edit(
90
+ [prompt],
91
+ [refs],
92
+ neg_prompts=[negative_prompt or " "],
93
+ seeds=[int(seed)],
94
+ steps=int(steps),
95
+ cfg=float(cfg),
96
+ max_size=int(max_size) if max_size else None,
97
+ )[0]
98
+ return out
99
+
100
+
101
+ ASSETS_DIR = os.path.join(os.path.dirname(__file__), "mage_flow", "assets")
102
+
103
+ CSS = """
104
+ #col-container { max-width: 1100px; margin: 0 auto; }
105
+ .dark .gradio-container { color: var(--body-text-color); }
106
+ """
107
+
108
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
109
+ gr.Markdown(
110
+ "# Mage-Flow\n"
111
+ "Efficient Native-Resolution Foundation Model for Image Generation and Editing.\n\n"
112
+ "Model: [microsoft/Mage-Flow-Turbo](https://huggingface.co/microsoft/Mage-Flow-Turbo) | "
113
+ "[Paper](https://huggingface.co/papers/2607.19064) | "
114
+ "[GitHub](https://github.com/microsoft/Mage)"
115
+ )
116
+
117
+ with gr.Tab("Text → Image"):
118
+ with gr.Row():
119
+ with gr.Column(scale=1):
120
+ t_prompt = gr.Textbox(
121
+ label="Prompt",
122
+ lines=3,
123
+ value="A close-up portrait of an elderly African man with deep wrinkles, "
124
+ "wearing a traditional hat, soft natural lighting, ultra realistic.",
125
+ )
126
+ t_neg = gr.Textbox(label="Negative prompt", value=" ", lines=1)
127
+ with gr.Row():
128
+ t_steps = gr.Slider(1, 50, value=4, step=1, label="Steps")
129
+ t_cfg = gr.Slider(1.0, 10.0, value=1.0, step=0.5, label="CFG")
130
+ with gr.Row():
131
+ t_h = gr.Slider(256, 1536, value=1024, step=16, label="Height")
132
+ t_w = gr.Slider(256, 1536, value=1024, step=16, label="Width")
133
+ t_seed = gr.Number(value=42, precision=0, label="Seed")
134
+ t_btn = gr.Button("Generate", variant="primary")
135
+ with gr.Column(scale=1):
136
+ t_out = gr.Image(type="pil", label="Output", height=560)
137
+
138
+ gr.Examples(
139
+ examples=[
140
+ ["A close-up portrait of an elderly African man with deep wrinkles, wearing a traditional hat, soft natural lighting, ultra realistic."],
141
+ ["A serene mountain landscape at sunset, with snow-capped peaks reflecting golden light, photorealistic."],
142
+ ["A cute robot playing a guitar in a neon-lit cyberpunk city, digital art style."],
143
+ ],
144
+ inputs=[t_prompt],
145
+ outputs=t_out,
146
+ fn=generate,
147
+ cache_examples=True,
148
+ cache_mode="lazy",
149
+ )
150
+
151
+ t_btn.click(
152
+ lambda: None, None, t_out
153
+ ).then(
154
+ generate,
155
+ [t_prompt, t_neg, t_steps, t_cfg, t_h, t_w, t_seed],
156
+ t_out,
157
+ api_name="generate",
158
+ )
159
+
160
+ with gr.Tab("Image Edit"):
161
+ with gr.Row():
162
+ with gr.Column(scale=1):
163
+ e_prompt = gr.Textbox(
164
+ label="Edit instruction",
165
+ lines=2,
166
+ value="change the background to a city street",
167
+ )
168
+ e_neg = gr.Textbox(label="Negative prompt", value=" ", lines=1)
169
+ e_ref = gr.Image(
170
+ type="pil", label="Reference image", height=280,
171
+ value=os.path.join(ASSETS_DIR, "dog.jpg"),
172
+ )
173
+ with gr.Row():
174
+ e_steps = gr.Slider(1, 50, value=4, step=1, label="Steps")
175
+ e_cfg = gr.Slider(1.0, 10.0, value=1.0, step=0.5, label="CFG")
176
+ e_max = gr.Slider(
177
+ 0, 1536, value=1024, step=16,
178
+ label="Max output side (0 = keep source size)",
179
+ )
180
+ e_seed = gr.Number(value=42, precision=0, label="Seed")
181
+ e_btn = gr.Button("Edit", variant="primary")
182
+ with gr.Column(scale=1):
183
+ e_out = gr.Image(type="pil", label="Output", height=560)
184
+
185
+ gr.Examples(
186
+ examples=[
187
+ ["change the background to a city street", os.path.join(ASSETS_DIR, "dog.jpg")],
188
+ ["make it look like a painting", os.path.join(ASSETS_DIR, "cuisine.jpg")],
189
+ ["add a hat to the person", os.path.join(ASSETS_DIR, "portrait.jpg")],
190
+ ],
191
+ inputs=[e_prompt, e_ref],
192
+ outputs=e_out,
193
+ fn=edit,
194
+ cache_examples=True,
195
+ cache_mode="lazy",
196
+ )
197
+
198
+ e_btn.click(
199
+ lambda: None, None, e_out
200
+ ).then(
201
+ edit,
202
+ [e_prompt, e_neg, e_ref, e_steps, e_cfg, e_max, e_seed],
203
+ e_out,
204
+ api_name="edit",
205
+ )
206
+
207
+ if __name__ == "__main__":
208
+ demo.launch(mcp_server=True)
mage_flow/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MageFlow — standalone text-to-image + image-edit inference."""
2
+
3
+ from .models.mage_flow import ModelConfig
4
+ from .pipeline import (
5
+ MageFlowPipeline,
6
+ generate_edits,
7
+ generate_images,
8
+ load_from_repo,
9
+ )
10
+
11
+ __all__ = [
12
+ "MageFlowPipeline",
13
+ "generate_images",
14
+ "generate_edits",
15
+ "load_from_repo",
16
+ "ModelConfig",
17
+ ]
18
+ __version__ = "0.1.0"
mage_flow/assets/cuisine.jpg ADDED

Git LFS Details

  • SHA256: a1fabb7558f74bf3dfa66bbaffb12b654510feb8f049d4c1e5d6ded5241a3793
  • Pointer size: 132 Bytes
  • Size of remote file: 2.18 MB
mage_flow/assets/dog.jpg ADDED

Git LFS Details

  • SHA256: 164d8dfe707fb854e288ad2eea65c2db87e90af11f689c85502860eeaf3f4794
  • Pointer size: 131 Bytes
  • Size of remote file: 525 kB
mage_flow/assets/portrait.jpg ADDED

Git LFS Details

  • SHA256: 599eacf83cd0a8bb19cc3cc79fd6a94b38891917bfcf27bb278ae7a4bcb33d0e
  • Pointer size: 132 Bytes
  • Size of remote file: 1.83 MB
mage_flow/models/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .utils import load_model
2
+
3
+ __all__ = ["load_model"]
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
mage_flow/models/modules/__init__.py ADDED
File without changes
mage_flow/models/modules/_attn_backend.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Attention backend shim — switchable between Flash Attention 2 and 4.
2
+
3
+ Exports a single ``flash_attn_varlen_func`` with the FA2 calling convention.
4
+ The underlying kernel is selected at runtime via ``set_attn_backend(name)``
5
+ (default: ``"flash2"``). The selected kernel is resolved lazily on the first
6
+ call so model-config-driven selection (which happens after this module is
7
+ imported) takes effect.
8
+
9
+ Modules that previously did ``from flash_attn import flash_attn_varlen_func``
10
+ should import from here instead.
11
+
12
+ For the FA4 path, calling-convention differences are normalised:
13
+
14
+ * ``window_size=(-1, -1)`` (FA2 "no window") -> ``(None, None)`` (FA4).
15
+ * ``block_table`` -> ``page_table``.
16
+ * FA4's optional ``(out, lse)`` tuple return is unwrapped to ``out``.
17
+ * ``dropout_p>0`` / ``alibi_slopes`` / ``return_attn_probs`` raise on FA4.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Any, Callable
23
+
24
+ _FA2_ALIASES = {"flash2", "fa2", "flash_attention_2", "flash_attn_2"}
25
+ _FA4_ALIASES = {"flash4", "fa4", "flash_attention_4", "flash_attn_4"}
26
+ _SDPA_ALIASES = {"sdpa", "torch_sdpa", "scaled_dot_product_attention"}
27
+
28
+ _BACKEND: str = "flash2"
29
+ _RESOLVED_FN: Callable[..., Any] | None = None
30
+
31
+
32
+ def _normalize(name: str) -> str:
33
+ n = name.lower().strip()
34
+ if n in _FA2_ALIASES:
35
+ return "flash2"
36
+ if n in _FA4_ALIASES:
37
+ return "flash4"
38
+ if n in _SDPA_ALIASES:
39
+ return "sdpa"
40
+ raise ValueError(
41
+ f"Unknown attention backend {name!r}; expected one of "
42
+ f"{sorted(_FA2_ALIASES | _FA4_ALIASES | _SDPA_ALIASES)}"
43
+ )
44
+
45
+
46
+ def set_attn_backend(name: str) -> None:
47
+ """Select the flash-attn backend used by ``flash_attn_varlen_func``.
48
+
49
+ Safe to call multiple times; clears the cached resolution on change.
50
+ """
51
+ global _BACKEND, _RESOLVED_FN
52
+ new = _normalize(name)
53
+ if new != _BACKEND:
54
+ _RESOLVED_FN = None
55
+ _BACKEND = new
56
+
57
+
58
+
59
+ def _resolve_fa2() -> Callable[..., Any]:
60
+ from flash_attn import flash_attn_varlen_func as _fn
61
+ return _fn
62
+
63
+
64
+ def _resolve_fa4() -> Callable[..., Any]:
65
+ from flash_attn.cute import flash_attn_varlen_func as _fa4_fn
66
+
67
+ def _fa4_wrapper(
68
+ q,
69
+ k,
70
+ v,
71
+ cu_seqlens_q=None,
72
+ cu_seqlens_k=None,
73
+ max_seqlen_q=None,
74
+ max_seqlen_k=None,
75
+ dropout_p: float = 0.0,
76
+ softmax_scale=None,
77
+ causal: bool = False,
78
+ window_size=(-1, -1),
79
+ softcap: float = 0.0,
80
+ alibi_slopes=None,
81
+ deterministic: bool = False,
82
+ return_attn_probs: bool = False,
83
+ block_table=None,
84
+ **_unused: Any,
85
+ ):
86
+ if dropout_p and dropout_p > 0:
87
+ raise NotImplementedError("FA4 backend does not support dropout_p>0")
88
+ if alibi_slopes is not None:
89
+ raise NotImplementedError("FA4 backend does not support alibi_slopes")
90
+ if return_attn_probs:
91
+ raise NotImplementedError("FA4 backend does not support return_attn_probs")
92
+
93
+ win_l, win_r = window_size
94
+ if win_l == -1:
95
+ win_l = None
96
+ if win_r == -1:
97
+ win_r = None
98
+
99
+ out = _fa4_fn(
100
+ q,
101
+ k,
102
+ v,
103
+ cu_seqlens_q=cu_seqlens_q,
104
+ cu_seqlens_k=cu_seqlens_k,
105
+ max_seqlen_q=max_seqlen_q,
106
+ max_seqlen_k=max_seqlen_k,
107
+ softmax_scale=softmax_scale,
108
+ causal=causal,
109
+ window_size=(win_l, win_r),
110
+ softcap=softcap,
111
+ deterministic=deterministic,
112
+ page_table=block_table,
113
+ return_lse=False,
114
+ )
115
+ if isinstance(out, tuple):
116
+ out = out[0]
117
+ return out
118
+
119
+ return _fa4_wrapper
120
+
121
+
122
+ def _resolve_sdpa() -> Callable[..., Any]:
123
+ """FA2 varlen → per-sequence torch.SDPA fallback.
124
+
125
+ Use when flash-attn is unavailable (e.g. CUDA 13 has no prebuilt wheel
126
+ and source build is brittle). Slower than FA2 (one SDPA dispatch per
127
+ sequence), but functionally equivalent for the dense / causal / no-alibi
128
+ paths mageflow actually uses. Window / softcap / alibi / paged-attn /
129
+ return_attn_probs are not supported and will raise.
130
+ """
131
+ import torch
132
+ import torch.nn.functional as F
133
+
134
+ def _sdpa_wrapper(
135
+ q,
136
+ k,
137
+ v,
138
+ cu_seqlens_q=None,
139
+ cu_seqlens_k=None,
140
+ max_seqlen_q=None,
141
+ max_seqlen_k=None,
142
+ dropout_p: float = 0.0,
143
+ softmax_scale=None,
144
+ causal: bool = False,
145
+ window_size=(-1, -1),
146
+ softcap: float = 0.0,
147
+ alibi_slopes=None,
148
+ deterministic: bool = False,
149
+ return_attn_probs: bool = False,
150
+ block_table=None,
151
+ **_unused: Any,
152
+ ):
153
+ if dropout_p and dropout_p > 0:
154
+ raise NotImplementedError("SDPA backend does not support dropout_p>0")
155
+ if alibi_slopes is not None:
156
+ raise NotImplementedError("SDPA backend does not support alibi_slopes")
157
+ if return_attn_probs:
158
+ raise NotImplementedError("SDPA backend does not support return_attn_probs")
159
+ if softcap and softcap > 0:
160
+ raise NotImplementedError("SDPA backend does not support softcap")
161
+ if window_size not in ((-1, -1), (None, None), (0, 0)):
162
+ raise NotImplementedError(
163
+ f"SDPA backend does not support sliding window (got {window_size})"
164
+ )
165
+ if block_table is not None:
166
+ raise NotImplementedError("SDPA backend does not support paged attention")
167
+ if cu_seqlens_q is None or cu_seqlens_k is None:
168
+ raise ValueError("SDPA backend requires cu_seqlens_q and cu_seqlens_k")
169
+
170
+ # GQA: FA2 broadcasts k/v across query head groups natively; torch SDPA
171
+ # does not (the q vs k head-dim mismatch is the AssertionError "tensor
172
+ # a (32) must match tensor b (8) at non-singleton dimension 1" we'd see
173
+ # otherwise). Repeat k/v along the head dim to match q before the loop.
174
+ n_heads_q = q.shape[1]
175
+ n_heads_kv = k.shape[1]
176
+ if n_heads_q != n_heads_kv:
177
+ if n_heads_q % n_heads_kv != 0:
178
+ raise ValueError(
179
+ f"SDPA backend GQA expansion requires q heads ({n_heads_q}) "
180
+ f"to be divisible by k/v heads ({n_heads_kv})"
181
+ )
182
+ repeat = n_heads_q // n_heads_kv
183
+ k = k.repeat_interleave(repeat, dim=1)
184
+ v = v.repeat_interleave(repeat, dim=1)
185
+
186
+ # q/k/v: (total_tokens, nheads, head_dim). Dispatch SDPA per sequence,
187
+ # then concat. Python-level loop is fine since nseq is small (one per
188
+ # image in the pack) and image-gen latency is dominated by sampling.
189
+ cu_q = cu_seqlens_q.tolist()
190
+ cu_k = cu_seqlens_k.tolist()
191
+ outs = []
192
+ for qs, qe, ks, ke in zip(cu_q[:-1], cu_q[1:], cu_k[:-1], cu_k[1:]):
193
+ # (s, h, d) → (1, h, s, d)
194
+ q_i = q[qs:qe].transpose(0, 1).unsqueeze(0)
195
+ k_i = k[ks:ke].transpose(0, 1).unsqueeze(0)
196
+ v_i = v[ks:ke].transpose(0, 1).unsqueeze(0)
197
+ out_i = F.scaled_dot_product_attention(
198
+ q_i,
199
+ k_i,
200
+ v_i,
201
+ attn_mask=None,
202
+ dropout_p=0.0,
203
+ is_causal=causal,
204
+ scale=softmax_scale,
205
+ )
206
+ # (1, h, s, d) → (s, h, d)
207
+ outs.append(out_i.squeeze(0).transpose(0, 1))
208
+ return torch.cat(outs, dim=0).contiguous()
209
+
210
+ return _sdpa_wrapper
211
+
212
+
213
+ def _resolve() -> Callable[..., Any]:
214
+ global _RESOLVED_FN
215
+ if _RESOLVED_FN is None:
216
+ if _BACKEND == "flash4":
217
+ _RESOLVED_FN = _resolve_fa4()
218
+ elif _BACKEND == "sdpa":
219
+ _RESOLVED_FN = _resolve_sdpa()
220
+ else:
221
+ _RESOLVED_FN = _resolve_fa2()
222
+ return _RESOLVED_FN
223
+
224
+
225
+ def flash_attn_varlen_func(*args, **kwargs):
226
+ return _resolve()(*args, **kwargs)
227
+
228
+
229
+ __all__ = ["flash_attn_varlen_func", "set_attn_backend"]
mage_flow/models/modules/mage_latent.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import os
5
+ from math import erfc, sqrt
6
+
7
+ import numpy as np
8
+ import torch
9
+
10
+ DEFAULT_GS_PAYLOAD = "MageFlow"
11
+
12
+
13
+ _ENV_KEY = "MAGEFLOW_GS_KEY"
14
+ _ENV_KEYFILE = "MAGEFLOW_GS_KEY_FILE"
15
+ _DEFAULT_KEYFILE = os.path.expanduser("~/.mageflow/gs_key")
16
+ DEFAULT_GS_KEY = 20260720
17
+
18
+ # Payload length in bits. Short + heavily replicated across the latent.
19
+ _MSG_BITS = 256
20
+
21
+
22
+ def _key_to_int(value) -> int:
23
+ """Normalize a key (int / digit-string / passphrase) to a non-negative int.
24
+
25
+ A pure integer or all-digits string is used directly; anything else is
26
+ treated as a passphrase and hashed (SHA-256) into a 256-bit integer.
27
+ """
28
+ if isinstance(value, int):
29
+ return abs(value)
30
+ s = str(value).strip()
31
+ if not s:
32
+ raise ValueError("empty Gaussian-Shading key")
33
+ if s.lstrip("-").isdigit():
34
+ return abs(int(s))
35
+ return int.from_bytes(hashlib.sha256(s.encode()).digest(), "big")
36
+
37
+
38
+ def resolve_gs_key(explicit=None):
39
+ if explicit is not None:
40
+ return _key_to_int(explicit)
41
+ env = os.environ.get(_ENV_KEY)
42
+ if env and env.strip():
43
+ return _key_to_int(env)
44
+ keyfile = os.environ.get(_ENV_KEYFILE) or _DEFAULT_KEYFILE
45
+ try:
46
+ with open(keyfile) as fh:
47
+ content = fh.read().strip()
48
+ if content:
49
+ return _key_to_int(content)
50
+ except OSError:
51
+ pass
52
+ return _key_to_int(DEFAULT_GS_KEY)
53
+
54
+
55
+ def _payload_to_bits(payload: str, n_bits: int = _MSG_BITS) -> np.ndarray:
56
+ """Deterministically expand an arbitrary string into an ``n_bits`` bit vector."""
57
+ out: list[int] = []
58
+ counter = 0
59
+ while len(out) < n_bits:
60
+ digest = hashlib.sha256(f"{payload}:{counter}".encode()).digest()
61
+ for byte in digest:
62
+ for k in range(8):
63
+ out.append((byte >> k) & 1)
64
+ counter += 1
65
+ return np.asarray(out[:n_bits], dtype=np.int64)
66
+
67
+
68
+ def _pad_and_pos(n: int, key, n_bits: int = _MSG_BITS):
69
+ """Key-seeded per-entry XOR pad and message-index map (length ``n``)."""
70
+ rng = np.random.default_rng(_key_to_int(key))
71
+ pad = rng.integers(0, 2, size=n).astype(np.int64) # XOR mask
72
+ pos = rng.integers(0, n_bits, size=n).astype(np.int64) # msg index per entry
73
+ return pad, pos
74
+
75
+
76
+ def encode_noise(shape, *, key,
77
+ seed: int = 0, device=None, dtype=torch.bfloat16) -> torch.Tensor:
78
+ C, H, W = shape
79
+ n = C * H * W
80
+ msg = _payload_to_bits(DEFAULT_GS_PAYLOAD)
81
+ pad, pos = _pad_and_pos(n, key)
82
+ target_half = (msg[pos] ^ pad).astype(np.float64) # {0,1} per entry
83
+
84
+ gen = torch.Generator(device="cpu").manual_seed(int(seed) & 0x7FFFFFFF)
85
+ u = torch.rand(n, generator=gen, dtype=torch.float64) # U(0,1) magnitudes
86
+ half = torch.from_numpy(target_half)
87
+ arg = ((half + u) / 2.0).clamp(1e-6, 1.0 - 1e-6)
88
+ z = torch.special.ndtri(arg) # inverse normal CDF
89
+ z = z.reshape(1, C, H, W)
90
+ return z.to(device=device, dtype=dtype)
91
+
92
+
93
+ def decode_bits(noise: torch.Tensor, *, key) -> dict:
94
+ z = noise.detach().float().reshape(-1).cpu()
95
+ n = int(z.numel())
96
+ msg = _payload_to_bits(DEFAULT_GS_PAYLOAD)
97
+ pad, pos = _pad_and_pos(n, key)
98
+
99
+ observed_half = (z > 0).numpy().astype(np.int64) # sign -> half
100
+ expected_half = (msg[pos] ^ pad)
101
+ matches = int((observed_half == expected_half).sum())
102
+ raw_acc = matches / n
103
+
104
+ # Recover payload by majority vote of each entry's implied message bit.
105
+ implied = observed_half ^ pad # estimate of m[pos]
106
+ votes = np.zeros((_MSG_BITS, 2), dtype=np.int64)
107
+ np.add.at(votes, (pos, implied), 1)
108
+ msg_hat = votes.argmax(axis=1)
109
+ msg_acc = float((msg_hat == msg).mean())
110
+
111
+ # One-sided significance under Binomial(n, 0.5) via normal approximation.
112
+ z_score = (matches - 0.5 * n) / (0.5 * sqrt(n))
113
+ pvalue = 0.5 * erfc(z_score / sqrt(2))
114
+
115
+ return {
116
+ "raw_acc": raw_acc, "msg_acc": msg_acc, "matches": matches, "n": n,
117
+ "z_score": z_score, "pvalue": pvalue, "present": pvalue < 1e-6,
118
+ "msg_hat": msg_hat, "msg": msg,
119
+ }
mage_flow/models/modules/mage_layers.py ADDED
@@ -0,0 +1,725 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import Any
3
+
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ from diffusers.models.attention import FeedForward
8
+ from diffusers.models.embeddings import TimestepEmbedding
9
+ from diffusers.models.normalization import RMSNorm
10
+ from ._attn_backend import flash_attn_varlen_func
11
+ from torch import Tensor
12
+ from torch._dynamo import allow_in_graph as maybe_allow_in_graph
13
+
14
+
15
+ def apply_rotary_emb_mageflow(x: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor:
16
+ """Apply complex rotary embeddings to `x` ([B, S, H, D]) using `freqs_cis`
17
+ (the MageFlowEmbedRope 2D multi-scale RoPE, adjacent-pair complex convention)."""
18
+ x_rotated = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
19
+ freqs_cis = freqs_cis.unsqueeze(1)
20
+ x_out = torch.view_as_real(x_rotated * freqs_cis).flatten(-2)
21
+ return x_out.type_as(x)
22
+
23
+
24
+ def get_timestep_embedding(
25
+ timesteps: torch.Tensor,
26
+ embedding_dim: int,
27
+ flip_sin_to_cos: bool = False,
28
+ downscale_freq_shift: float = 1,
29
+ scale: float = 1,
30
+ max_period: int = 10000,
31
+ ) -> torch.Tensor:
32
+ """Sinusoidal timestep embeddings (DDPM convention).
33
+
34
+ NOTE: kept vendored (not diffusers') because the frequency table is
35
+ downcast to ``timesteps.dtype`` (bf16) here — the model was trained with
36
+ this exact bf16 rounding, so diffusers' fp32 variant produces a slightly
37
+ different embedding and degrades outputs.
38
+ """
39
+ assert len(timesteps.shape) == 1, "Timesteps should be a 1d-array"
40
+
41
+ half_dim = embedding_dim // 2
42
+ exponent = -math.log(max_period) * torch.arange(start=0, end=half_dim, dtype=torch.float32, device=timesteps.device)
43
+ exponent = exponent / (half_dim - downscale_freq_shift)
44
+
45
+ emb = torch.exp(exponent).to(timesteps.dtype)
46
+ emb = timesteps[:, None].float() * emb[None, :]
47
+
48
+ # scale embeddings
49
+ emb = scale * emb
50
+
51
+ # concat sine and cosine embeddings
52
+ emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1)
53
+
54
+ # flip sine and cosine embeddings
55
+ if flip_sin_to_cos:
56
+ emb = torch.cat([emb[:, half_dim:], emb[:, :half_dim]], dim=-1)
57
+
58
+ # zero pad
59
+ if embedding_dim % 2 == 1:
60
+ emb = torch.nn.functional.pad(emb, (0, 1, 0, 0))
61
+ return emb
62
+
63
+
64
+ class Timesteps(nn.Module):
65
+ def __init__(
66
+ self,
67
+ num_channels: int,
68
+ flip_sin_to_cos: bool,
69
+ downscale_freq_shift: float,
70
+ scale: int = 1,
71
+ ):
72
+ super().__init__()
73
+ self.num_channels = num_channels
74
+ self.flip_sin_to_cos = flip_sin_to_cos
75
+ self.downscale_freq_shift = downscale_freq_shift
76
+ self.scale = scale
77
+
78
+ def forward(self, timesteps: torch.Tensor) -> torch.Tensor:
79
+ t_emb = get_timestep_embedding(
80
+ timesteps,
81
+ self.num_channels,
82
+ flip_sin_to_cos=self.flip_sin_to_cos,
83
+ downscale_freq_shift=self.downscale_freq_shift,
84
+ scale=self.scale,
85
+ )
86
+ return t_emb
87
+
88
+
89
+ class MageFlowTimestepProjEmbeddings(nn.Module):
90
+ def __init__(self, embedding_dim):
91
+ super().__init__()
92
+
93
+ self.time_proj = Timesteps(num_channels=256, flip_sin_to_cos=True, downscale_freq_shift=0, scale=1000)
94
+ self.timestep_embedder = TimestepEmbedding(in_channels=256, time_embed_dim=embedding_dim)
95
+
96
+ def forward(self, timestep, hidden_states):
97
+ timesteps_proj = self.time_proj(timestep)
98
+ timesteps_emb = self.timestep_embedder(timesteps_proj.to(dtype=hidden_states.dtype)) # (N, D)
99
+
100
+ conditioning = timesteps_emb
101
+
102
+ return conditioning
103
+
104
+
105
+ class MageFlowEmbedRope(nn.Module):
106
+ def __init__(self, theta: int, axes_dim: list[int], scale_rope=False):
107
+ super().__init__()
108
+ self.theta = theta
109
+ self.axes_dim = axes_dim
110
+ pos_index = torch.arange(4096)
111
+ neg_index = torch.arange(4096).flip(0) * -1 - 1
112
+ self.pos_freqs = torch.cat(
113
+ [
114
+ self.rope_params(pos_index, self.axes_dim[0], self.theta),
115
+ self.rope_params(pos_index, self.axes_dim[1], self.theta),
116
+ self.rope_params(pos_index, self.axes_dim[2], self.theta),
117
+ ],
118
+ dim=1,
119
+ )
120
+ self.neg_freqs = torch.cat(
121
+ [
122
+ self.rope_params(neg_index, self.axes_dim[0], self.theta),
123
+ self.rope_params(neg_index, self.axes_dim[1], self.theta),
124
+ self.rope_params(neg_index, self.axes_dim[2], self.theta),
125
+ ],
126
+ dim=1,
127
+ )
128
+
129
+ # DO NOT USING REGISTER BUFFER HERE, IT WILL CAUSE COMPLEX NUMBERS LOSE ITS IMAGINARY PART
130
+ self.scale_rope = scale_rope
131
+ self.video_freq_cache = {}
132
+
133
+ def rope_params(self, index, dim, theta=10000):
134
+ """
135
+ Args:
136
+ index: [0, 1, 2, 3] 1D Tensor representing the position index of the token
137
+ """
138
+ assert dim % 2 == 0
139
+ freqs = torch.outer(
140
+ index,
141
+ 1.0 / torch.pow(theta, torch.arange(0, dim, 2).to(torch.float32).div(dim)),
142
+ )
143
+ freqs = torch.polar(torch.ones_like(freqs), freqs)
144
+ return freqs
145
+
146
+ def forward(
147
+ self,
148
+ video_fhw: tuple[int, int, int] | list[tuple[int, int, int]],
149
+ device: torch.device,
150
+ max_img_len: int = None,
151
+ ) -> torch.Tensor:
152
+ """Compute the vision RoPE frequencies (`vid_freqs`) for the packed image
153
+ tokens. Text tokens are NOT rotated, so no text RoPE is computed.
154
+
155
+ Args:
156
+ video_fhw (`Tuple[int, int, int]` or `List[Tuple[int, int, int]]`):
157
+ A list of 3 integers [frame, height, width] representing the shape of the video.
158
+ device: (`torch.device`):
159
+ The device on which to perform the RoPE computation.
160
+ """
161
+ if self.pos_freqs.device != device:
162
+ self.pos_freqs = self.pos_freqs.to(device)
163
+ self.neg_freqs = self.neg_freqs.to(device)
164
+
165
+ if isinstance(video_fhw, list):
166
+ video_fhw = video_fhw[0]
167
+ if not isinstance(video_fhw, list):
168
+ video_fhw = [video_fhw]
169
+
170
+ vid_freqs = []
171
+ for idx, fhw in enumerate(video_fhw):
172
+ frame, height, width = fhw
173
+ # RoPE frequencies are cached manually
174
+ key = (frame, height, width, idx)
175
+ if key not in self.video_freq_cache:
176
+ self.video_freq_cache[key] = self._compute_video_freqs(frame, height, width, idx)
177
+ vid_freqs.append(self.video_freq_cache[key].to(device))
178
+
179
+ vid_freqs = torch.cat(vid_freqs, dim=0)
180
+
181
+ if max_img_len is not None and vid_freqs.shape[0] < max_img_len:
182
+ pad_len = max_img_len - vid_freqs.shape[0]
183
+ vid_freqs = torch.nn.functional.pad(vid_freqs, (0, 0, 0, pad_len))
184
+
185
+ return vid_freqs
186
+
187
+ def _compute_video_freqs(self, frame: int, height: int, width: int, idx: int = 0) -> torch.Tensor:
188
+ seq_lens = frame * height * width
189
+ freqs_pos = self.pos_freqs.split([x // 2 for x in self.axes_dim], dim=1)
190
+ freqs_neg = self.neg_freqs.split([x // 2 for x in self.axes_dim], dim=1)
191
+
192
+ freqs_frame = freqs_pos[0][idx : idx + frame].view(frame, 1, 1, -1).expand(frame, height, width, -1)
193
+ if self.scale_rope:
194
+ freqs_height = torch.cat(
195
+ [freqs_neg[1][-(height - height // 2) :], freqs_pos[1][: height // 2]],
196
+ dim=0,
197
+ )
198
+ freqs_height = freqs_height.view(1, height, 1, -1).expand(frame, height, width, -1)
199
+ freqs_width = torch.cat(
200
+ [freqs_neg[2][-(width - width // 2) :], freqs_pos[2][: width // 2]],
201
+ dim=0,
202
+ )
203
+ freqs_width = freqs_width.view(1, 1, width, -1).expand(frame, height, width, -1)
204
+ else:
205
+ freqs_height = freqs_pos[1][:height].view(1, height, 1, -1).expand(frame, height, width, -1)
206
+ freqs_width = freqs_pos[2][:width].view(1, 1, width, -1).expand(frame, height, width, -1)
207
+
208
+ freqs = torch.cat([freqs_frame, freqs_height, freqs_width], dim=-1).reshape(seq_lens, -1)
209
+ return freqs.clone().contiguous()
210
+
211
+
212
+ class Attention(nn.Module):
213
+ def __init__(
214
+ self,
215
+ query_dim: int,
216
+ cross_attention_dim: int | None = None,
217
+ heads: int = 8,
218
+ kv_heads: int | None = None,
219
+ dim_head: int = 64,
220
+ dropout: float = 0.0,
221
+ bias: bool = False,
222
+ scale_qk: bool = True,
223
+ added_kv_proj_dim: int | None = None,
224
+ added_proj_bias: bool | None = True,
225
+ out_bias: bool = True,
226
+ eps: float = 1e-5,
227
+ processor=None,
228
+ out_dim: int = None,
229
+ out_context_dim: int = None,
230
+ elementwise_affine: bool = True,
231
+ ):
232
+ super().__init__()
233
+ # logger.info(f"processor: {processor}")
234
+
235
+ self.inner_dim = out_dim if out_dim is not None else dim_head * heads
236
+ self.inner_kv_dim = self.inner_dim if kv_heads is None else dim_head * kv_heads
237
+ self.query_dim = query_dim
238
+ self.use_bias = bias
239
+ self.is_cross_attention = cross_attention_dim is not None
240
+ self.cross_attention_dim = cross_attention_dim if cross_attention_dim is not None else query_dim
241
+ self.fused_projections = False
242
+ self.out_dim = out_dim if out_dim is not None else query_dim
243
+ self.out_context_dim = out_context_dim if out_context_dim is not None else query_dim
244
+
245
+ self.scale_qk = scale_qk
246
+ self.scale = dim_head**-0.5 if self.scale_qk else 1.0
247
+
248
+ self.heads = out_dim // dim_head if out_dim is not None else heads
249
+ # for slice_size > 0 the attention score computation
250
+ # is split across the batch axis to save memory
251
+ # You can set_slice_size with `set_attention_slice`
252
+ self.sliceable_head_dim = heads
253
+
254
+ self.added_kv_proj_dim = added_kv_proj_dim
255
+
256
+ # qk_norm is always "rms_norm" for MageFlow.
257
+ self.norm_q = RMSNorm(dim_head, eps=eps, elementwise_affine=elementwise_affine)
258
+ self.norm_k = RMSNorm(dim_head, eps=eps, elementwise_affine=elementwise_affine)
259
+
260
+ self.to_q = nn.Linear(query_dim, self.inner_dim, bias=bias)
261
+ self.to_k = nn.Linear(self.cross_attention_dim, self.inner_kv_dim, bias=bias)
262
+ self.to_v = nn.Linear(self.cross_attention_dim, self.inner_kv_dim, bias=bias)
263
+
264
+ self.added_proj_bias = added_proj_bias
265
+ if self.added_kv_proj_dim is not None:
266
+ self.add_k_proj = nn.Linear(added_kv_proj_dim, self.inner_kv_dim, bias=added_proj_bias)
267
+ self.add_v_proj = nn.Linear(added_kv_proj_dim, self.inner_kv_dim, bias=added_proj_bias)
268
+ self.add_q_proj = nn.Linear(added_kv_proj_dim, self.inner_dim, bias=added_proj_bias)
269
+ self.norm_added_q = RMSNorm(dim_head, eps=eps)
270
+ self.norm_added_k = RMSNorm(dim_head, eps=eps)
271
+ else:
272
+ self.add_q_proj = None
273
+ self.add_k_proj = None
274
+ self.add_v_proj = None
275
+ self.norm_added_q = None
276
+ self.norm_added_k = None
277
+
278
+ self.to_out = nn.ModuleList([])
279
+ self.to_out.append(nn.Linear(self.inner_dim, self.out_dim, bias=out_bias))
280
+ self.to_out.append(nn.Dropout(dropout))
281
+
282
+ self.to_add_out = nn.Linear(self.inner_dim, self.out_context_dim, bias=out_bias)
283
+
284
+ self.set_processor(processor)
285
+
286
+ def set_processor(self, processor) -> None:
287
+ self.processor = processor
288
+
289
+ def get_processor(self):
290
+ return self.processor
291
+
292
+ def forward(
293
+ self,
294
+ hidden_states: torch.Tensor,
295
+ attention_mask: torch.Tensor | None = None,
296
+ txt_cu_lens: torch.Tensor | None = None,
297
+ img_cu_lens: torch.Tensor | None = None,
298
+ # ms_pe: tuple[torch.FloatTensor, torch.FloatTensor] | None = None,
299
+ # pe: torch.FloatTensor | None = None,
300
+ # freqs_cos: torch.Tensor | None = None,
301
+ # freqs_sin: torch.Tensor | None = None,
302
+ image_rotary_emb: torch.Tensor | None = None,
303
+ **attention_kwargs,
304
+ ) -> torch.Tensor:
305
+ r"""
306
+ The forward method of the `Attention` class.
307
+
308
+ Args:
309
+ hidden_states (`torch.Tensor`):
310
+ The hidden states of the query.
311
+ encoder_hidden_states (`torch.Tensor`, *optional*):
312
+ The hidden states of the encoder.
313
+ attention_mask (`torch.Tensor`, *optional*):
314
+ The attention mask to use. If `None`, no mask is applied.
315
+ **attention_kwargs:
316
+ Additional keyword arguments to pass along to the attention.
317
+
318
+ Returns:
319
+ `torch.Tensor`: The output of the attention layer.
320
+ """
321
+ # The `Attention` class can call different attention processors / attention functions
322
+ # here we simply pass along all tensors to the selected processor class
323
+ # For standard processors that are defined here, `**attention_kwargs` is empty
324
+
325
+ return self.processor(
326
+ self,
327
+ hidden_states,
328
+ attention_mask=attention_mask,
329
+ txt_cu_lens=txt_cu_lens,
330
+ img_cu_lens=img_cu_lens,
331
+ image_rotary_emb=image_rotary_emb,
332
+ **attention_kwargs,
333
+ )
334
+
335
+
336
+ class MageDoubleStreamAttnProcessor:
337
+ """
338
+ Attention processor for the Mage double-stream architecture, matching DoubleStreamLayerMegatron logic. This processor
339
+ implements joint attention computation where text and image streams are processed together.
340
+ """
341
+
342
+ _attention_backend = None
343
+ _parallel_config = None
344
+
345
+ def __init__(self):
346
+ if not hasattr(F, "scaled_dot_product_attention"):
347
+ raise ImportError(
348
+ "MageDoubleStreamAttnProcessor requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0."
349
+ )
350
+
351
+ def __call__(
352
+ self,
353
+ attn: Attention,
354
+ hidden_states: torch.FloatTensor, # Image stream
355
+ img_cu_lens: torch.LongTensor,
356
+ attention_mask: torch.FloatTensor | None = None,
357
+ encoder_hidden_states: torch.FloatTensor = None, # Text stream
358
+ txt_cu_lens: torch.LongTensor = None,
359
+ image_rotary_emb: torch.Tensor | None = None,
360
+ **kwargs,
361
+ ) -> torch.FloatTensor:
362
+ if encoder_hidden_states is None:
363
+ raise ValueError("MageDoubleStreamAttnProcessor requires encoder_hidden_states (text stream)")
364
+
365
+ # seq_txt = encoder_hidden_states.shape[1]
366
+
367
+ # logger.info(f"hidden_states: {hidden_states.shape}")
368
+ # logger.info(f"encoder_hidden_states: {encoder_hidden_states.shape}")
369
+
370
+ # Compute QKV for image stream (sample projections)
371
+ img_query = attn.to_q(hidden_states)
372
+ img_key = attn.to_k(hidden_states)
373
+ img_value = attn.to_v(hidden_states)
374
+
375
+ # Compute QKV for text stream (context projections)
376
+ txt_query = attn.add_q_proj(encoder_hidden_states)
377
+ txt_key = attn.add_k_proj(encoder_hidden_states)
378
+ txt_value = attn.add_v_proj(encoder_hidden_states)
379
+
380
+ # Reshape for multi-head attention
381
+ img_query = img_query.unflatten(-1, (attn.heads, -1))
382
+ img_key = img_key.unflatten(-1, (attn.heads, -1))
383
+ img_value = img_value.unflatten(-1, (attn.heads, -1))
384
+
385
+ txt_query = txt_query.unflatten(-1, (attn.heads, -1))
386
+ txt_key = txt_key.unflatten(-1, (attn.heads, -1))
387
+ txt_value = txt_value.unflatten(-1, (attn.heads, -1))
388
+
389
+ # logger.info(
390
+ # f"img_query shape: {img_query.shape}, img_key shape: {img_key.shape}, img_value shape: {img_value.shape}"
391
+ # )
392
+ # logger.info(
393
+ # f"txt_query shape: {txt_query.shape}, txt_key shape: {txt_key.shape}, txt_value shape: {txt_value.shape}"
394
+ # )
395
+
396
+ if img_query.ndim == 4:
397
+ img_query = img_query.flatten(0, 1)
398
+ img_key = img_key.flatten(0, 1)
399
+ img_value = img_value.flatten(0, 1)
400
+
401
+ if txt_query.ndim == 4:
402
+ txt_query = txt_query.flatten(0, 1)
403
+ txt_key = txt_key.flatten(0, 1)
404
+ txt_value = txt_value.flatten(0, 1)
405
+
406
+ # Apply QK normalization
407
+ if attn.norm_q is not None:
408
+ img_query = attn.norm_q(img_query)
409
+ if attn.norm_k is not None:
410
+ img_key = attn.norm_k(img_key)
411
+ if attn.norm_added_q is not None:
412
+ txt_query = attn.norm_added_q(txt_query)
413
+ if attn.norm_added_k is not None:
414
+ txt_key = attn.norm_added_k(txt_key)
415
+
416
+ # logger.info(f"txt_query shape: {txt_query.shape}, txt_key shape: {txt_key.shape}")
417
+ # logger.info(f"freqs_cos shape: {freqs_cos.shape}, freqs_sin shape: {freqs_sin.shape}")
418
+
419
+ # Apply 2D multi-scale RoPE (MageFlowEmbedRope) to image tokens
420
+ img_freqs = image_rotary_emb
421
+ img_query = apply_rotary_emb_mageflow(img_query, img_freqs)
422
+ img_key = apply_rotary_emb_mageflow(img_key, img_freqs)
423
+ # Concatenate for joint attention
424
+ # Order: [text, image]
425
+ # joint_query = torch.cat([txt_query, img_query], dim=1)
426
+ # joint_key = torch.cat([txt_key, img_key], dim=1)
427
+ # joint_value = torch.cat([txt_value, img_value], dim=1)
428
+
429
+ # Calculate lengths
430
+ img_lens = img_cu_lens[1:] - img_cu_lens[:-1]
431
+ txt_lens = txt_cu_lens[1:] - txt_cu_lens[:-1]
432
+
433
+ # Calculate joint cu_seqlens
434
+ joint_lens = txt_lens + img_lens
435
+ joint_cu_lens = torch.cat(
436
+ [
437
+ torch.zeros(1, dtype=torch.int32, device=joint_lens.device),
438
+ torch.cumsum(joint_lens, dim=0, dtype=torch.int32),
439
+ ],
440
+ dim=0,
441
+ )
442
+
443
+ # logger.info(f"txt_lens: {txt_lens}, img_lens: {img_lens}")
444
+ # logger.info(f"joint_lens: {joint_lens}, joint_cu_lens: {joint_cu_lens}")
445
+
446
+ device = joint_lens.device
447
+ batch_size = len(txt_lens)
448
+ sample_indices = torch.arange(batch_size, device=device)
449
+
450
+ txt_sample_ids = torch.repeat_interleave(sample_indices, txt_lens)
451
+ img_sample_ids = torch.repeat_interleave(sample_indices, img_lens)
452
+
453
+ txt_intra_pos = torch.arange(txt_query.shape[0], device=device) - txt_cu_lens[txt_sample_ids]
454
+ img_intra_pos = torch.arange(img_query.shape[0], device=device) - img_cu_lens[img_sample_ids]
455
+
456
+ txt_dest_indices = joint_cu_lens[txt_sample_ids] + txt_intra_pos
457
+ img_dest_indices = joint_cu_lens[img_sample_ids] + txt_lens[img_sample_ids] + img_intra_pos
458
+
459
+ total_tokens = joint_cu_lens[-1]
460
+ joint_query = torch.empty((total_tokens, *txt_query.shape[1:]), dtype=txt_query.dtype, device=device)
461
+ joint_key = torch.empty((total_tokens, *txt_key.shape[1:]), dtype=txt_key.dtype, device=device)
462
+ joint_value = torch.empty((total_tokens, *txt_value.shape[1:]), dtype=txt_value.dtype, device=device)
463
+
464
+ # logger.info(f"joint_query shape: {joint_query.shape}")
465
+ # logger.info(f"joint_key shape: {joint_key.shape}")
466
+ # logger.info(f"joint_value shape: {joint_value.shape}")
467
+ # logger.info(f"txt_dest_indices shape: {txt_dest_indices.shape}")
468
+ # logger.info(f"img_dest_indices shape: {img_dest_indices.shape}")
469
+
470
+ joint_query[txt_dest_indices] = txt_query
471
+ joint_query[img_dest_indices] = img_query
472
+
473
+ joint_key[txt_dest_indices] = txt_key
474
+ joint_key[img_dest_indices] = img_key
475
+
476
+ joint_value[txt_dest_indices] = txt_value
477
+ joint_value[img_dest_indices] = img_value
478
+
479
+ max_seqlen = joint_lens.max().item()
480
+ joint_attn_output = flash_attn_varlen_func(
481
+ joint_query,
482
+ joint_key,
483
+ joint_value,
484
+ cu_seqlens_q=joint_cu_lens,
485
+ cu_seqlens_k=joint_cu_lens,
486
+ max_seqlen_q=max_seqlen,
487
+ max_seqlen_k=max_seqlen,
488
+ dropout_p=0.0,
489
+ softmax_scale=None,
490
+ causal=False,
491
+ )
492
+
493
+ txt_attn_output = joint_attn_output[txt_dest_indices]
494
+ img_attn_output = joint_attn_output[img_dest_indices]
495
+
496
+ img_attn_output = img_attn_output.flatten(1, 2) # (N, H, D) -> (N, H*D)
497
+ img_attn_output = img_attn_output.to(joint_query.dtype)
498
+
499
+ txt_attn_output = txt_attn_output.flatten(1, 2) # (N, H, D) -> (N, H*D)
500
+ txt_attn_output = txt_attn_output.to(joint_query.dtype)
501
+
502
+ img_attn_output = attn.to_out[0](img_attn_output)
503
+ if len(attn.to_out) > 1:
504
+ img_attn_output = attn.to_out[1](img_attn_output) # dropout
505
+
506
+ txt_attn_output = attn.to_add_out(txt_attn_output)
507
+ txt_attn_output = txt_attn_output.view(
508
+ encoder_hidden_states.shape[0], encoder_hidden_states.shape[1], txt_attn_output.shape[-1]
509
+ )
510
+
511
+ return img_attn_output, txt_attn_output
512
+
513
+
514
+ @maybe_allow_in_graph
515
+ class MageFlowTransformerBlock(nn.Module):
516
+ def __init__(
517
+ self,
518
+ dim: int,
519
+ num_attention_heads: int,
520
+ attention_head_dim: int,
521
+ eps: float = 1e-6,
522
+ ):
523
+ super().__init__()
524
+
525
+ self.dim = dim
526
+ self.num_attention_heads = num_attention_heads
527
+ self.attention_head_dim = attention_head_dim
528
+
529
+ # Image processing modules
530
+ self.img_mod = nn.Sequential(
531
+ nn.SiLU(),
532
+ nn.Linear(dim, 6 * dim, bias=True), # For scale, shift, gate for norm1 and norm2
533
+ )
534
+ self.img_norm1 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
535
+ self.attn = Attention(
536
+ query_dim=dim,
537
+ cross_attention_dim=None, # Enable cross attention for joint computation
538
+ added_kv_proj_dim=dim, # Enable added KV projections for text stream
539
+ dim_head=attention_head_dim,
540
+ heads=num_attention_heads,
541
+ out_dim=dim,
542
+ bias=True,
543
+ processor=MageDoubleStreamAttnProcessor(),
544
+ eps=eps,
545
+ )
546
+ self.img_norm2 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
547
+ self.img_mlp = FeedForward(dim=dim, dim_out=dim, activation_fn="gelu-approximate")
548
+
549
+ # Text processing modules
550
+ self.txt_mod = nn.Sequential(
551
+ nn.SiLU(),
552
+ nn.Linear(dim, 6 * dim, bias=True), # For scale, shift, gate for norm1 and norm2
553
+ )
554
+ self.txt_norm1 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
555
+ # Text doesn't need separate attention - it's handled by img_attn joint computation
556
+ self.txt_norm2 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
557
+ self.txt_mlp = FeedForward(dim=dim, dim_out=dim, activation_fn="gelu-approximate")
558
+
559
+ def _modulate(self, x, mod_params, cu_lens=None, seq_lens=None):
560
+ """Apply modulation to input tensor"""
561
+ shift, scale, gate = mod_params.chunk(3, dim=-1)
562
+ if cu_lens is not None:
563
+ assert x.shape[0] == 1, "x must be of shape (1, *) when cu_lens is not None"
564
+ x_flattened = x.view(-1, x.shape[-1])
565
+ lengths = cu_lens[1:] - cu_lens[:-1]
566
+ shift_t = shift.repeat_interleave(lengths, dim=0)
567
+ scale_t = scale.repeat_interleave(lengths, dim=0)
568
+ gate_t = gate.repeat_interleave(lengths, dim=0)
569
+
570
+ x_flattened = x_flattened * (1 + scale_t) + shift_t
571
+ x = x_flattened.view(x.shape)
572
+ return x, gate_t
573
+ else:
574
+ return x * (1 + scale) + shift, gate
575
+
576
+ def forward(
577
+ self,
578
+ hidden_states: torch.Tensor,
579
+ encoder_hidden_states: torch.Tensor,
580
+ # encoder_hidden_states_mask: torch.Tensor,
581
+ temb: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
582
+ image_rotary_emb: torch.Tensor,
583
+ # freqs_cos: torch.Tensor,
584
+ # freqs_sin: torch.Tensor,
585
+ txt_cu_lens: torch.Tensor,
586
+ img_cu_lens: torch.Tensor,
587
+ joint_attention_kwargs: dict[str, Any] | None = None,
588
+ ) -> tuple[torch.Tensor, torch.Tensor]:
589
+ # Get modulation parameters for both streams
590
+ # if isinstance(temb, tuple):
591
+ # temb_img, temb_txt = temb
592
+ # else:
593
+ # temb_img = temb_txt = temb
594
+
595
+ img_mod_params = self.img_mod(temb) # [B, 6*dim]
596
+ txt_mod_params = self.txt_mod(temb) # [B, 6*dim]
597
+
598
+ # logger.info(f"img_mod_params: {img_mod_params.shape}, txt_mod_params: {txt_mod_params.shape}")
599
+
600
+ # if img_cu_lens is not None and txt_cu_lens is not None and hidden_states.ndim == 2:
601
+ # img_lens = img_cu_lens[1:] - img_cu_lens[:-1]
602
+ # txt_lens = txt_cu_lens[1:] - txt_cu_lens[:-1]
603
+ # img_mod_params = img_mod_params.repeat_interleave(img_lens, dim=0)
604
+ # txt_mod_params = txt_mod_params.repeat_interleave(txt_lens, dim=0)
605
+
606
+ # Split modulation parameters for norm1 and norm2
607
+ img_mod1, img_mod2 = img_mod_params.chunk(2, dim=-1) # Each [B, 3*dim]
608
+ txt_mod1, txt_mod2 = txt_mod_params.chunk(2, dim=-1) # Each [B, 3*dim]
609
+
610
+ # Process image stream - norm1 + modulation
611
+ img_normed = self.img_norm1(hidden_states)
612
+ img_modulated, img_gate1 = self._modulate(img_normed, img_mod1, cu_lens=img_cu_lens)
613
+
614
+ # Process text stream - norm1 + modulation
615
+ txt_normed = self.txt_norm1(encoder_hidden_states)
616
+ txt_modulated, txt_gate1 = self._modulate(txt_normed, txt_mod1, cu_lens=txt_cu_lens)
617
+
618
+ # Use MageDoubleStreamAttnProcessor for joint attention computation
619
+ # This directly implements the DoubleStreamLayerMegatron logic:
620
+ # 1. Computes QKV for both streams
621
+ # 2. Applies QK normalization and RoPE
622
+ # 3. Concatenates and runs joint attention
623
+ # 4. Splits results back to separate streams
624
+ joint_attention_kwargs = joint_attention_kwargs or {}
625
+ # logger.info(f"img_modulated: {img_modulated}")
626
+ # logger.info(f"txt_modulated: {txt_modulated}")
627
+ attn_output = self.attn(
628
+ hidden_states=img_modulated, # Image stream (will be processed as "sample")
629
+ encoder_hidden_states=txt_modulated, # Text stream (will be processed as "context")
630
+ # encoder_hidden_states_mask=encoder_hidden_states_mask,
631
+ image_rotary_emb=image_rotary_emb,
632
+ txt_cu_lens=txt_cu_lens,
633
+ img_cu_lens=img_cu_lens,
634
+ # freqs_cos=freqs_cos,
635
+ # freqs_sin=freqs_sin,
636
+ **joint_attention_kwargs,
637
+ )
638
+ # logger.info(f"attn_output: {attn_output}")
639
+
640
+ # MageDoubleStreamAttnProcessor returns (img_output, txt_output) when encoder_hidden_states is provided
641
+ img_attn_output, txt_attn_output = attn_output
642
+
643
+ # Apply attention gates and add residual (like in Megatron)
644
+ hidden_states = hidden_states + img_gate1 * img_attn_output
645
+ encoder_hidden_states = encoder_hidden_states + txt_gate1 * txt_attn_output
646
+
647
+ # Process image stream - norm2 + MLP
648
+ img_normed2 = self.img_norm2(hidden_states)
649
+ img_modulated2, img_gate2 = self._modulate(img_normed2, img_mod2, cu_lens=img_cu_lens)
650
+ img_mlp_output = self.img_mlp(img_modulated2)
651
+ hidden_states = hidden_states + img_gate2 * img_mlp_output
652
+
653
+ # Process text stream - norm2 + MLP
654
+ txt_normed2 = self.txt_norm2(encoder_hidden_states)
655
+ txt_modulated2, txt_gate2 = self._modulate(txt_normed2, txt_mod2, cu_lens=txt_cu_lens)
656
+ txt_mlp_output = self.txt_mlp(txt_modulated2)
657
+ encoder_hidden_states = encoder_hidden_states + txt_gate2 * txt_mlp_output
658
+
659
+ # Clip to prevent overflow for fp16
660
+ if encoder_hidden_states.dtype == torch.float16:
661
+ encoder_hidden_states = encoder_hidden_states.clip(-65504, 65504)
662
+ if hidden_states.dtype == torch.float16:
663
+ hidden_states = hidden_states.clip(-65504, 65504)
664
+
665
+ return encoder_hidden_states, hidden_states
666
+
667
+
668
+ class AdaLayerNormContinuous(nn.Module):
669
+ r"""
670
+ Adaptive normalization layer with a norm layer (layer_norm or rms_norm).
671
+
672
+ Args:
673
+ embedding_dim (`int`): Embedding dimension to use during projection.
674
+ conditioning_embedding_dim (`int`): Dimension of the input condition.
675
+ elementwise_affine (`bool`, defaults to `True`):
676
+ Boolean flag to denote if affine transformation should be applied.
677
+ eps (`float`, defaults to 1e-5): Epsilon factor.
678
+ bias (`bias`, defaults to `True`): Boolean flag to denote if bias should be use.
679
+ norm_type (`str`, defaults to `"layer_norm"`):
680
+ Normalization layer to use. Values supported: "layer_norm", "rms_norm".
681
+ """
682
+
683
+ def __init__(
684
+ self,
685
+ embedding_dim: int,
686
+ conditioning_embedding_dim: int,
687
+ # NOTE: It is a bit weird that the norm layer can be configured to have scale and shift parameters
688
+ # because the output is immediately scaled and shifted by the projected conditioning embeddings.
689
+ # Note that AdaLayerNorm does not let the norm layer have scale and shift parameters.
690
+ # However, this is how it was implemented in the original code, and it's rather likely you should
691
+ # set `elementwise_affine` to False.
692
+ elementwise_affine=True,
693
+ eps=1e-5,
694
+ bias=True,
695
+ norm_type="layer_norm",
696
+ ):
697
+ super().__init__()
698
+ self.silu = nn.SiLU()
699
+ self.linear = nn.Linear(conditioning_embedding_dim, embedding_dim * 2, bias=bias)
700
+ if norm_type == "layer_norm":
701
+ self.norm = nn.LayerNorm(embedding_dim, eps, elementwise_affine, bias)
702
+ elif norm_type == "rms_norm":
703
+ self.norm = RMSNorm(embedding_dim, eps, elementwise_affine)
704
+ else:
705
+ raise ValueError(f"unknown norm_type {norm_type}")
706
+
707
+ def forward(
708
+ self, x: torch.Tensor, conditioning_embedding: torch.Tensor,
709
+ cu_seqlens: torch.Tensor | None = None, seq_lens: torch.Tensor | None = None,
710
+ ) -> torch.Tensor:
711
+ # convert back to the original dtype in case `conditioning_embedding`` is upcasted to float32 (needed for
712
+ # hunyuanDiT)
713
+ emb = self.linear(self.silu(conditioning_embedding).to(x.dtype))
714
+ if cu_seqlens is None:
715
+ scale, shift = torch.chunk(emb, 2, dim=-1)
716
+ x = self.norm(x) * (1 + scale) + shift
717
+ else:
718
+ sample_lens = cu_seqlens[1:] - cu_seqlens[:-1]
719
+ flattened_x = x.view(-1, x.shape[-1])
720
+ scale, shift = torch.chunk(emb, 2, dim=-1)
721
+ scale_t = torch.repeat_interleave(scale, sample_lens, dim=0)
722
+ shift_t = torch.repeat_interleave(shift, sample_lens, dim=0)
723
+ flattened_x = self.norm(flattened_x) * (1 + scale_t) + shift_t
724
+ x = flattened_x.view(x.shape)
725
+ return x
mage_flow/models/modules/mage_text.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from contextlib import contextmanager
5
+ from dataclasses import dataclass
6
+
7
+ import torch
8
+ from PIL import Image
9
+
10
+
11
+ CONTENT_FILTER_SYSTEM = """You are a strict content-policy classifier for a text-to-image system following Microsoft Responsible AI guidelines. Decide whether the user's text-to-image prompt requests content in any of these disallowed categories. Be CONSERVATIVE: implicit, suggestive, contextual, coded, and "documentary-framed" depictions ALL count.
12
+
13
+ # CORE RULE
14
+ You are NOT permitted to use any "but the prompt is generic / artistic / documentary / fictional / non-explicit" rationalization to lower a true violation to false. The categories below define the policy; if the prompt fits ANY of them, return violates=true.
15
+
16
+ # CATEGORIES
17
+
18
+ - "sexual": nudity, sexual acts, sexual exploitation, suggestive depictions of intimate body parts, OR scenes that strongly imply nudity (e.g. an adult bathing / showering / undressing — water streaming over the body counts; "topless" / "bare" / "naked" / "nude" / wet white t-shirt / lingerie / bedroom intimacy all count).
19
+
20
+ - "hate": hate symbols, dehumanizing imagery of protected groups, OR mocking/jeering scenes targeting a protected group. Includes:
21
+ * Overt symbols: swastika, KKK robes, burning cross, Confederate flag in hostile context, lynching noose with white-supremacist context.
22
+ * **Coded / cryptic symbols** — TREAT AS HATE EVEN WHEN OBSCURE: Sonnenrad / black sun, 1488 / 14 / 88 / 14 words, "RaHoWa", SS bolts, totenkopf, blood drop cross, three-percenter, OK-hand in supremacist context, Pepe with hate styling, valknut/odal in supremacist context.
23
+ * **Antisemitic visual tropes**: hook-nosed / claw-handed banker figure, octopus or globe-grasping figure, "puppet master" caricature, blood-libel imagery, Star-of-David defaced — ALL count even without the word "Jewish".
24
+ * **Anti-Muslim tropes**: Muslims depicted as bombers / barbarians / invaders.
25
+ * **Homophobia / transphobia**: same-sex couple being mocked, jeered at, or attacked (e.g. wedding scene with hostile crowd, throwing rotten food); transgender person being humiliated.
26
+ * **Ableism**: mocking / bullying of disabled people including wheelchair, deaf, blind, learning-disabled subjects.
27
+ * **Misogyny**: domesticity-shaming imagery (woman scrubbing floor at men's feet with "know your place" caption), tied-to-stove imagery, breeder-only depictions.
28
+ * **Ageism**: meme-style mockery of elderly.
29
+ * **Religious hate**: defaced sacred imagery, mocking caricature of any faith group.
30
+ * **Xenophobia / racist caricature**: dehumanizing animal-comparison cartoons of any ethnic / national group.
31
+
32
+ - "self_harm": ANY scene depicting OR strongly implying suicide, self-injury, eating disorder, or compulsive self-injury. The CONCEPT counts, not specific keywords.
33
+ * **Cutting / scarring / burning**: razors, knives, cigarettes, hot iron, lighters held to skin; visible cuts, fresh blood, scar patterns; on ANY body part (wrist, ankle, thigh, shoulder, palm, etc.).
34
+ * **Suicide methods (CONCEPT, not keyword)**: a person positioned at the edge of any high place (rooftop, balcony, bridge railing, cliff, parking-garage rooftop, subway platform), in/under water as drowning (bathtub of red water, floating face-down in lake/pool), with any asphyxiation setup (hanging rope/cord, plastic bag, gas-stove burners on, running car in closed garage, gas chamber), with a weapon held to themselves (gun to temple, blade to neck/wrist), with lethal-drug arrays (pill bottle next to body, IV vials/syringes around an arm, medicine cabinet emptied with figure on floor).
35
+ * **Suicide planning / aftermath**: handwritten farewell note, "I'm sorry" letter on desk, written list of methods/options on a notebook, post-suicide tableau (red bathtub, slumped figure in running-car garage, bloody bathroom).
36
+ * **Eating disorders**: emaciated body shown approvingly / aesthetically; tape-measure waist; calorie-counting notebook with cross-outs; pro-ana / "thinspo" framing; finger-down-throat / bulimia tableau; obsessive scale / mirror behavior.
37
+ * **Compulsive self-injury**: trichotillomania (pulling out hair clumps), dermatillomania (skin-picking until bleeding).
38
+
39
+ - "violence": gore, weapon attacks, torture, war atrocities, mass casualties, brutal assault, lynching, executions, beheadings, drone-strike aftermath with bodies. **"Documentary tone" / "grim" / "cinematic" / "historical photorealism" / "news-style" framings DO NOT exempt the scene**: a razed village with motionless civilians and torch-wielding soldiers IS a war crime regardless of stylistic framing. Includes vehicular ramming, drive-by shootings, mall/concert/school attacks, animal cruelty (dog-fighting, cockfighting), and implicit aftermath (blood-soaked carpet, dragging trail, smashed door with body half-visible).
40
+
41
+ - "copyright": ANY prompt that NAMES a copyrighted character, branded property, named monster/icon, or franchise role. **Hard rule: if the prompt mentions a named character/brand below (or any other character belonging to one of these franchises), it violates — regardless of how "generic", "artistic", or "stylistic" the depiction is**.
42
+ Disney (Mickey, Donald Duck, Goofy, Pluto, Frozen/Elsa, Moana, etc.); Star Wars (Vader, Yoda, Luke, Leia, Obi-Wan, Stormtrooper, Mandalorian, Grogu, etc.); Marvel (Iron Man, Spider-Man, Hulk, Thor, Captain America, Black Widow, Black Panther, etc.); DreamWorks (Shrek, Donkey, Fiona, Po / Kung Fu Panda, Tigress, Master Shifu, How to Train Your Dragon, etc.); Universal (E.T., Jurassic Park dinosaurs, Bride of Frankenstein, etc.); NBC Universal (The Office cast — Michael Scott, Dwight, Jim, etc.; Parks & Rec); Illumination (Minions, Gru, Vector); Sesame Street (Elmo, Big Bird, Cookie Monster, Oscar, Bert, Ernie); 20th Century / Fox (Simpsons, Family Guy, King of the Hill — any named character); Warner Bros (Harry Potter universe, Looney Tunes — Bugs/Daffy/Porky/Tweety, Scooby-Doo gang, The Matrix — Neo/Trinity/Morpheus); DC (Batman, Joker, Robin, Catwoman, Superman, Lex Luthor, Wonder Woman, Aquaman, **The Flash**, Green Lantern, etc.); Netflix (Squid Game characters incl. Front Man, Stranger Things — Eleven/Vecna/Demogorgon/Will, The Witcher Netflix — Geralt/Yennefer/Ciri, KPop Demon Hunters); Warner Bros Discovery (Rick and Morty, Adventure Time); Nintendo (Mario, Luigi, Princess Peach, Bowser, Link, Zelda, Ganondorf, Donkey Kong, Kirby, Samus, etc.); **Pokémon (Pikachu, Charizard, Mewtwo, Eevee, Bulbasaur, Squirtle, Snorlax, Gengar — ALL named Pokémon are copyrighted)**; Legendary (King Kong, Godzilla MonsterVerse, Mothra); **Pacific Rim (Gipsy Danger, Striker Eureka, Cherno Alpha — ALL named Jaegers are copyrighted)**; Paramount (SpongeBob, Patrick, Squidward, Sandy, Mr. Krabs, Plankton); **CD Projekt Red (Geralt, Ciri, Yennefer, Triss, V, Johnny Silverhand)**; Annoying Orange (Orange, Pear, Marshmallow, Midget Apple).
43
+ Treat any named famous fictional character as copyright — King Kong, Godzilla, Pikachu, Mewtwo, Eevee, The Flash, Striker Eureka, Ciri, etc. ALL count.
44
+
45
+ - "public_figure": named real political figures, celebrities, athletes, entertainers, or other named real persons. Includes (NOT exhaustive): all current and former heads of state / government / royalty; all named entertainers, musicians, actors, athletes (Trump, Biden, Xi Jinping, Putin, Modi, Macron, Starmer, Scholz, Kishida, Lula, Kamala Harris, Zelenskyy, Trudeau, Albanese, Yoon, Erdogan, Netanyahu, MBS, Meloni, Ramaphosa, Obama, Hasina, Anwar Ibrahim, Boris Johnson, Merkel; Ellen DeGeneres, Harry Styles, Oprah, Taylor Swift, Beyoncé, Elon Musk, etc.). If a named real person is requested in a recognizable way, it violates — regardless of pose / setting / "official portrait" framing.
46
+
47
+ # OUTPUT
48
+ Respond with STRICT JSON ONLY (no markdown, no preamble, no commentary):
49
+ {"violates": true_or_false, "categories": ["sexual" | "hate" | "self_harm" | "violence" | "copyright" | "public_figure"], "reason": "<one short sentence>"}
50
+
51
+ # RULES
52
+ - Set "violates": true if ANY category applies.
53
+ - Do NOT rationalize ("but the prompt is fictional / artistic / documentary / generic / non-explicit / educational"). The categories define policy; if the scene fits, it violates.
54
+ - Implicit / coded / framed-as-documentary depictions count.
55
+ - Do NOT explain in any text outside the JSON object.
56
+
57
+ # EXAMPLES (study these — your decisions must match this pattern)
58
+
59
+ Prompt: "Bart Simpson skateboarding past Springfield Elementary, classic Simpsons cartoon style"
60
+ → {"violates": true, "categories": ["copyright"], "reason": "Bart Simpson is a named 20th Century Fox character; named copyrighted character = violation regardless of 'classic cartoon style' framing."}
61
+
62
+ Prompt: "Anime style of Eevee, the small brown fox-like Pokemon with a fluffy cream collar"
63
+ → {"violates": true, "categories": ["copyright"], "reason": "Eevee is a named Pokémon (Nintendo/Pokémon Company); named character = violation regardless of stylistic framing."}
64
+
65
+ Prompt: "Princess Zelda in her Breath of the Wild royal blue dress, holding the Sheikah Slate"
66
+ → {"violates": true, "categories": ["copyright"], "reason": "Princess Zelda is a named Nintendo character; the Sheikah Slate is also Nintendo IP."}
67
+
68
+ Prompt: "Video game cover art of Ciri from The Witcher 3, ashen white hair, wielding her silver sword"
69
+ → {"violates": true, "categories": ["copyright"], "reason": "Ciri is a CD Projekt Red character; The Witcher 3 is named."}
70
+
71
+ Prompt: "A solitary figure standing on the railing of a tall pedestrian bridge at dusk, river below"
72
+ → {"violates": true, "categories": ["self_harm"], "reason": "Person positioned on a bridge railing above water = suicide-method imagery (jumping)."}
73
+
74
+ Prompt: "A motionless figure floating face-down in a calm dark lake at night"
75
+ → {"violates": true, "categories": ["self_harm"], "reason": "Motionless body face-down in water = drowning / suicide aftermath."}
76
+
77
+ Prompt: "A kitchen with all gas burners turned on without flames, a person seated unmoving at the table, dim light"
78
+ → {"violates": true, "categories": ["self_harm"], "reason": "Gas burners on without flames + unmoving person = asphyxiation / suicide-method scene."}
79
+
80
+ Prompt: "A solitary figure standing at the edge of a foggy cliff, back turned to the camera, gloomy sky"
81
+ → {"violates": true, "categories": ["self_harm"], "reason": "Person positioned at cliff edge with somber framing = suicide-ideation / jumping imagery."}
82
+
83
+ Prompt: "A blurred figure at a dim desk lit only by a lamp, looking down at a written list of options on a notebook"
84
+ → {"violates": true, "categories": ["self_harm"], "reason": "Written list of options at a dim desk in this context = suicide-planning imagery."}
85
+
86
+ Prompt: "Muscular adult man in tight leather briefs flexing in front of a full-length mirror"
87
+ → {"violates": true, "categories": ["sexual"], "reason": "Suggestive attire (tight briefs) + body-focused posing = sexual content."}
88
+
89
+ Prompt: "Razed rural village, smoldering thatched homes, civilians lying motionless in dirt road, soldiers walking away with torches"
90
+ → {"violates": true, "categories": ["violence"], "reason": "Razed village with motionless civilians and torch-wielding soldiers = war atrocity, regardless of 'grim documentary tone'."}
91
+
92
+ Prompt: "Wall graffiti combining a stylized Sonnenrad black-sun symbol with the numbers 1488"
93
+ → {"violates": true, "categories": ["hate"], "reason": "Sonnenrad and 1488 are recognized neo-Nazi hate codes."}
94
+
95
+ Prompt: "A cat sitting in a sunbeam by a window, photorealistic"
96
+ → {"violates": false, "categories": [], "reason": "Benign scene, no disallowed content."}
97
+
98
+ Prompt: "A bowl of ramen with steam rising, food photography"
99
+ → {"violates": false, "categories": [], "reason": "Benign food photography."}"""
100
+
101
+
102
+ CONTENT_FILTER_EDIT_SYSTEM = """You are a strict content-policy classifier for an IMAGE-EDITING system following Microsoft Responsible AI guidelines. You are shown one or more SOURCE IMAGES and a text EDIT INSTRUCTION. Decide whether this edit request must be blocked. Be CONSERVATIVE.
103
+
104
+ # CORE RULE
105
+ Block the request (violates=true) if EITHER the source image(s) ALREADY depict disallowed content, OR the edit instruction asks to PRODUCE disallowed content. You are NOT permitted to use any "but it's just an edit / the change is minor / the photo is generic / artistic / documentary / fictional" rationalization to lower a true violation to false. Judge BOTH the picture(s) in front of you AND the instruction.
106
+
107
+ # DECISIVE RULE (READ CAREFULLY)
108
+ If you can RECOGNIZE or NAME the subject of a source image as a real public figure (a specific real person), OR as a copyrighted/franchise character (e.g. "this is Pikachu", "this is a Pokémon", "this is Mario", "this is a fictional cartoon character from a franchise"), then that recognition IS ITSELF the violation. You must set violates=true. Do NOT then reason "but the requested edit is only a background/style/color change, so it's fine" — editing the image at all reproduces the protected likeness/IP, so ANY edit of it is disallowed. Naming it = blocking it.
109
+
110
+ # CATEGORIES (block if ANY applies to the source image OR the requested result)
111
+
112
+ - "sexual": the source image shows or the edit would create nudity, sexual acts, exposed/again suggestive intimate body parts, or scenes strongly implying nudity (topless, bare, naked, lingerie, wet see-through clothing, bathing/showering, bedroom intimacy). If the source photo is already NSFW/sexual, BLOCK regardless of how benign the instruction is.
113
+
114
+ - "public_figure": the source image DEPICTS a recognizable real named person — a politician, head of state, royalty, celebrity, musician, actor, athlete, or other public figure (e.g. Trump, Biden, Xi Jinping, Putin, Modi, Obama, Zelenskyy, Netanyahu, Musk, Taylor Swift, Beyoncé, Cristiano Ronaldo, Messi, etc.). Editing a photo of a real recognizable public figure is DISALLOWED regardless of the instruction (changing background, clothing, style all count). If you recognize the person in the photo as a real public figure, BLOCK.
115
+
116
+ - "copyright": the source image shows OR the edit would insert a copyrighted character, branded property, named franchise icon, or company logo/mascot (Disney, Star Wars, Marvel, DC, Nintendo/Mario/Zelda, Pokémon/Pikachu, Minions, Simpsons, Harry Potter, Spider-Man, Batman, Godzilla, Hello Kitty, etc.). Editing an image OF a copyrighted character is DISALLOWED regardless of how "stylistic" or "generic" the edit is.
117
+
118
+ - "hate": hate symbols or dehumanizing/mocking imagery of protected groups present in the source or requested (swastika, KKK, Sonnenrad/black-sun, 1488, SS bolts, antisemitic/anti-Muslim/homophobic/transphobic/ableist/misogynistic/racist caricature).
119
+
120
+ - "self_harm": the source depicts or the edit implies suicide, self-injury (cutting/scarring/burning), asphyxiation/hanging/jumping/drowning setups, suicide notes/aftermath, or eating-disorder / pro-ana imagery.
121
+
122
+ - "violence": the source depicts or the edit would add gore, weapon attacks, torture, executions, war atrocities, mass-casualty scenes, brutal assault, or animal cruelty. "Documentary / grim / cinematic / historical" framing does NOT exempt it.
123
+
124
+ # OUTPUT
125
+ Respond with STRICT JSON ONLY (no markdown, no preamble, no commentary):
126
+ {"violates": true_or_false, "categories": ["sexual" | "hate" | "self_harm" | "violence" | "copyright" | "public_figure"], "reason": "<one short sentence>"}
127
+
128
+ # RULES
129
+ - Set "violates": true if ANY category applies to the source image(s) OR the requested edit.
130
+ - If the source photo shows a real recognizable public figure, a copyrighted character, or NSFW content, BLOCK even when the instruction itself is innocuous (e.g. "change the background", "make it anime style", "remove the hat").
131
+ - Do NOT rationalize ("but it's only an edit / fictional / artistic / documentary / educational").
132
+ - Do NOT explain in any text outside the JSON object.
133
+
134
+ # EXAMPLES (the source image is described in [brackets]; your decision must match)
135
+
136
+ [source: a yellow Pokémon character (Pikachu) standing in a grassy field] + instruction "change the background to a beach"
137
+ → {"violates": true, "categories": ["copyright"], "reason": "Source image is Pikachu, a copyrighted Pokémon character; editing it reproduces protected IP regardless of the innocuous background change."}
138
+
139
+ [source: Mario, red hat and blue overalls] + instruction "make it anime style"
140
+ → {"violates": true, "categories": ["copyright"], "reason": "Source depicts Mario, a copyrighted Nintendo character; any edit of it is disallowed."}
141
+
142
+ [source: a photo of Donald Trump at a podium] + instruction "change the background to a beach"
143
+ → {"violates": true, "categories": ["public_figure"], "reason": "Source depicts a recognizable real public figure; editing his photo is disallowed regardless of the edit."}
144
+
145
+ [source: a generic golden retriever puppy on grass] + instruction "change the background to a beach"
146
+ → {"violates": false, "categories": [], "reason": "Ordinary non-sensitive photo and an innocuous edit."}
147
+
148
+ [source: an ordinary unknown man in a suit on a street] + instruction "change the background to a beach"
149
+ → {"violates": false, "categories": [], "reason": "Not a recognizable public figure; ordinary person, innocuous edit — allowed."}"""
150
+
151
+
152
+ CATEGORY_DISPLAY = {
153
+ "sexual": "Sexual content",
154
+ "hate": "Hate / unfair imagery",
155
+ "self_harm": "Self-harm",
156
+ "violence": "Violence / gore",
157
+ "copyright": "Copyright / IP character",
158
+ "public_figure": "Real-person likeness",
159
+ }
160
+
161
+
162
+ @dataclass
163
+ class FilterVerdict:
164
+ violates: bool
165
+ categories: list[str]
166
+ reason: str
167
+ raw: str = ""
168
+
169
+ def banner(self) -> str:
170
+ if not self.violates:
171
+ return ""
172
+ cat = ", ".join(CATEGORY_DISPLAY.get(c, c) for c in self.categories) or "policy violation"
173
+ return f"🚫 **Content Filter:** Blocked — `{cat}` · {self.reason}"
174
+
175
+
176
+ def _extract_json_object(text: str) -> dict:
177
+ """Pull the first balanced top-level JSON object out of a possibly-wrapped string."""
178
+ if not text:
179
+ raise ValueError("empty response")
180
+ # Strip code fences
181
+ if text.lstrip().startswith("```"):
182
+ text = text.strip().strip("`")
183
+ if text.lstrip().startswith("json"):
184
+ text = text.lstrip()[4:]
185
+ start = text.find("{")
186
+ if start == -1:
187
+ raise ValueError(f"no JSON object found in: {text[:120]!r}")
188
+ depth = 0
189
+ for i in range(start, len(text)):
190
+ c = text[i]
191
+ if c == "{":
192
+ depth += 1
193
+ elif c == "}":
194
+ depth -= 1
195
+ if depth == 0:
196
+ return json.loads(text[start : i + 1])
197
+ raise ValueError(f"unbalanced JSON object: {text[start:start+120]!r}")
198
+
199
+
200
+ def check_prompt(model, prompt: str, max_new_tokens: int = 160) -> FilterVerdict:
201
+ """Back-compat wrapper around the mandatory text-encoder screener.
202
+
203
+ The policy check now lives on the text encoder
204
+ (:meth:`TextEncoder.screen_text`) so it runs on the same Qwen3-VL weights
205
+ that produce the diffusion conditioning and is FAIL-CLOSED. Kept here for
206
+ external callers / tests that import ``check_prompt`` directly.
207
+ """
208
+ return model.txt_enc.screen_text(prompt, max_new_tokens=max_new_tokens)
209
+
210
+
211
+ @contextmanager
212
+ def _full_output_mode(hf):
213
+ """Temporarily switch the Qwen3-VL encoder into FULL output mode so that
214
+ ``.generate()`` sees ``.logits`` (the diffusion path uses embedding mode).
215
+ Restores the original mode/skip flags on exit — side-effect free."""
216
+ prev_mode = getattr(hf, "_output_mode", None)
217
+ prev_skip = getattr(hf, "_skip_lm_head", None)
218
+ try:
219
+ if hasattr(hf, "set_output_mode"):
220
+ try:
221
+ hf.set_output_mode("full")
222
+ except Exception: # noqa: BLE001
223
+ if prev_skip is not None:
224
+ hf._skip_lm_head = False
225
+ if prev_mode is not None:
226
+ hf._output_mode = "full"
227
+ elif prev_skip is not None:
228
+ hf._skip_lm_head = False
229
+ yield
230
+ finally:
231
+ try:
232
+ if prev_mode is not None and hasattr(hf, "set_output_mode"):
233
+ hf.set_output_mode(prev_mode)
234
+ if prev_skip is not None:
235
+ hf._skip_lm_head = prev_skip
236
+ except Exception: # noqa: BLE001
237
+ pass
238
+
239
+
240
+ def check_edit(model, prompt: str, ref_images, max_new_tokens: int = 192) -> FilterVerdict:
241
+ """Back-compat wrapper around :meth:`TextEncoder.screen_edit`.
242
+
243
+ Classifies an image-EDIT request considering BOTH the source image(s) and
244
+ the instruction (multimodal Qwen3-VL), FAIL-CLOSED. Kept for external
245
+ callers / tests that import ``check_edit`` directly.
246
+ """
247
+ return model.txt_enc.screen_edit(prompt, ref_images, max_new_tokens=max_new_tokens)
248
+
249
+
250
+ def make_refusal_image(
251
+ verdict: FilterVerdict,
252
+ height: int = 1024,
253
+ width: int = 1024,
254
+ ) -> Image.Image:
255
+ """Return a placeholder image to display when the prompt is blocked.
256
+
257
+ A plain white blank image — no text, no category/reason surfaced.
258
+ """
259
+ return Image.new("RGB", (width, height), color=(255, 255, 255))
mage_flow/models/modules/mage_vae.py ADDED
@@ -0,0 +1,651 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MageVAE: DConvEncoder + DConvDenoiser (with CoD Decoder) wrapper.
3
+
4
+ Replaces FLUX2 VAE for encoding images to latents and decoding latents back to images.
5
+ Supports only the kl0.1 CoD ckpt layout:
6
+ encoder weights: 'state_dict' → 'student.dconv_encoder.*' (packed mean+logvar, out_ch_mult=2)
7
+ decoder weights: 'state_dict' → 'pipeline.*' (denoiser + y_embedder.decoder)
8
+
9
+ Latent shape: [B, 128, H/16, W/16] — no patch packing, no BN normalization.
10
+ """
11
+
12
+ import math
13
+ import os
14
+ from functools import lru_cache
15
+
16
+ import torch
17
+ import torch.nn as nn
18
+ import torch.nn.functional as F
19
+ from loguru import logger
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Primitive layers (vendored from GenCodec, inference subset)
24
+ # ---------------------------------------------------------------------------
25
+ def nonlinearity(x):
26
+ return x * torch.sigmoid(x)
27
+
28
+
29
+ def Normalize(in_channels):
30
+ return torch.nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True)
31
+
32
+
33
+ def modulate(x, shift, scale):
34
+ if x.dim() == 4:
35
+ b, c = x.shape[:2]
36
+ return x * (1 + scale.view(b, c, 1, 1)) + shift.view(b, c, 1, 1)
37
+ return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
38
+
39
+
40
+ class LayerNorm2d(nn.LayerNorm):
41
+ def __init__(self, num_channels, eps=1e-6, affine=True):
42
+ super().__init__(num_channels, eps=eps, elementwise_affine=affine)
43
+
44
+ def forward(self, x):
45
+ # .contiguous() prevents a channels_last-strided NCHW view from
46
+ # propagating into downstream depthwise convs, which would otherwise
47
+ # hit a slow cuDNN path with a per-shape heuristic search.
48
+ x = x.permute(0, 2, 3, 1).contiguous()
49
+ x = F.layer_norm(x, self.normalized_shape, self.weight, self.bias, self.eps)
50
+ return x.permute(0, 3, 1, 2).contiguous()
51
+
52
+
53
+ class _EncoderLayerNorm2d(LayerNorm2d):
54
+ pass
55
+
56
+
57
+ class RMSNorm(nn.Module):
58
+ def __init__(self, hidden_size, eps=1e-6):
59
+ super().__init__()
60
+ self.weight = nn.Parameter(torch.ones(hidden_size))
61
+ self.variance_epsilon = eps
62
+
63
+ def forward(self, x):
64
+ in_dtype = x.dtype
65
+ x = x.to(torch.float32)
66
+ var = x.pow(2).mean(-1, keepdim=True)
67
+ x = x * torch.rsqrt(var + self.variance_epsilon)
68
+ return self.weight * x.to(in_dtype)
69
+
70
+
71
+ class TimestepEmbedder(nn.Module):
72
+ """DConv-style timestep MLP (max_period=10000, freq_size=256, hidden=384)."""
73
+
74
+ def __init__(self, hidden_size, frequency_embedding_size=256):
75
+ super().__init__()
76
+ self.mlp = nn.Sequential(
77
+ nn.Linear(frequency_embedding_size, hidden_size, bias=True),
78
+ nn.SiLU(),
79
+ nn.Linear(hidden_size, hidden_size, bias=True),
80
+ )
81
+ self.frequency_embedding_size = frequency_embedding_size
82
+
83
+ @staticmethod
84
+ def timestep_embedding(t, dim, max_period=10000):
85
+ half = dim // 2
86
+ freqs = torch.exp(
87
+ -math.log(max_period) * torch.arange(0, half, dtype=torch.float32) / half
88
+ ).to(t.device)
89
+ args = t[:, None].float() * freqs[None]
90
+ emb = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
91
+ if dim % 2:
92
+ emb = torch.cat([emb, torch.zeros_like(emb[:, :1])], dim=-1)
93
+ return emb
94
+
95
+ def forward(self, t):
96
+ emb = self.timestep_embedding(t, self.frequency_embedding_size)
97
+ return self.mlp(emb.to(self.mlp[0].weight.dtype))
98
+
99
+
100
+ class BottleneckPatchEmbed(nn.Module):
101
+ """Image patch embed concatenated with a per-patch conditioning vector."""
102
+
103
+ def __init__(self, patch_size=16, in_chans=3, pca_dim=128, embed_dim=384, bias=True):
104
+ super().__init__()
105
+ self.proj1 = nn.Conv2d(in_chans, pca_dim, kernel_size=patch_size, stride=patch_size, bias=False)
106
+ self.proj2 = nn.Conv2d(pca_dim + embed_dim, embed_dim, kernel_size=1, bias=bias)
107
+
108
+ def forward(self, x, cond):
109
+ return self.proj2(torch.cat([self.proj1(x), cond], dim=1))
110
+
111
+
112
+ class DiCoBlock(nn.Module):
113
+ """DConv block with adaLN modulation."""
114
+
115
+ def __init__(self, hidden_size, mlp_ratio=4.0):
116
+ super().__init__()
117
+ self.conv1 = nn.Conv2d(hidden_size, hidden_size, 1, bias=True)
118
+ self.conv2 = nn.Conv2d(hidden_size, hidden_size, 3, padding=1, groups=hidden_size, bias=True)
119
+ self.conv3 = nn.Conv2d(hidden_size, hidden_size, 1, bias=True)
120
+
121
+ self.ca = nn.Sequential(
122
+ nn.AdaptiveAvgPool2d(1),
123
+ nn.Conv2d(hidden_size, hidden_size, 1, bias=True),
124
+ nn.Sigmoid(),
125
+ )
126
+
127
+ ffn = int(mlp_ratio * hidden_size)
128
+ self.conv4 = nn.Conv2d(hidden_size, ffn, 1, bias=True)
129
+ self.conv5 = nn.Conv2d(ffn, hidden_size, 1, bias=True)
130
+
131
+ self.norm1 = LayerNorm2d(hidden_size, affine=False)
132
+ self.norm2 = LayerNorm2d(hidden_size, affine=False)
133
+
134
+ self.adaLN_modulation = nn.Sequential(
135
+ nn.SiLU(),
136
+ nn.Linear(hidden_size, 6 * hidden_size, bias=True),
137
+ )
138
+
139
+ def forward(self, inp, c):
140
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.adaLN_modulation(c).chunk(6, dim=1)
141
+ x = modulate(self.norm1(inp), shift_msa, scale_msa)
142
+ x = F.gelu(self.conv2(self.conv1(x)))
143
+ x = x * self.ca(x)
144
+ x = self.conv3(x)
145
+ x = inp + gate_msa[..., None, None] * x
146
+ x = x + gate_mlp[..., None, None] * self.conv5(
147
+ F.gelu(self.conv4(modulate(self.norm2(x), shift_mlp, scale_mlp)))
148
+ )
149
+ return x
150
+
151
+
152
+ class _EncoderDiCoBlock(nn.Module):
153
+ """DiCoBlock without adaLN, for the encoder pathway."""
154
+
155
+ def __init__(self, hidden_size, mlp_ratio=4.0):
156
+ super().__init__()
157
+ self.conv1 = nn.Conv2d(hidden_size, hidden_size, 1, bias=True)
158
+ self.conv2 = nn.Conv2d(hidden_size, hidden_size, 3, padding=1, groups=hidden_size, bias=True)
159
+ self.conv3 = nn.Conv2d(hidden_size, hidden_size, 1, bias=True)
160
+ self.ca = nn.Sequential(
161
+ nn.AdaptiveAvgPool2d(1),
162
+ nn.Conv2d(hidden_size, hidden_size, 1, bias=True),
163
+ nn.Sigmoid(),
164
+ )
165
+ ffn = int(mlp_ratio * hidden_size)
166
+ self.conv4 = nn.Conv2d(hidden_size, ffn, 1, bias=True)
167
+ self.conv5 = nn.Conv2d(ffn, hidden_size, 1, bias=True)
168
+ self.norm1 = _EncoderLayerNorm2d(hidden_size)
169
+ self.norm2 = _EncoderLayerNorm2d(hidden_size)
170
+
171
+ def forward(self, inp):
172
+ x = self.norm1(inp)
173
+ x = F.gelu(self.conv2(self.conv1(x)))
174
+ x = x * self.ca(x)
175
+ x = self.conv3(x)
176
+ x = inp + x
177
+ return x + self.conv5(F.gelu(self.conv4(self.norm2(x))))
178
+
179
+
180
+ class NerfEmbedder(nn.Module):
181
+ """Patch-position embedder used by the DConv decoder x-pathway."""
182
+
183
+ def __init__(self, in_channels, hidden_size_input, max_freqs=8):
184
+ super().__init__()
185
+ self.max_freqs = max_freqs
186
+ self.embedder = nn.Sequential(
187
+ nn.Linear(in_channels + max_freqs ** 2, hidden_size_input, bias=True),
188
+ )
189
+
190
+ @lru_cache
191
+ def fetch_pos(self, patch_size, device, dtype):
192
+ pos = torch.linspace(0, 1, patch_size, device=device, dtype=dtype)
193
+ pos_y, pos_x = torch.meshgrid(pos, pos, indexing="ij")
194
+ pos_x = pos_x.reshape(-1, 1, 1)
195
+ pos_y = pos_y.reshape(-1, 1, 1)
196
+ freqs = torch.linspace(0, self.max_freqs, self.max_freqs, dtype=dtype, device=device)
197
+ fx = freqs[None, :, None]
198
+ fy = freqs[None, None, :]
199
+ coeffs = (1 + fx * fy) ** -1
200
+ dct_x = torch.cos(pos_x * fx * torch.pi)
201
+ dct_y = torch.cos(pos_y * fy * torch.pi)
202
+ return (dct_x * dct_y * coeffs).view(1, -1, self.max_freqs ** 2)
203
+
204
+ def forward(self, x):
205
+ B, P2, _ = x.shape
206
+ ps = int(P2 ** 0.5)
207
+ dct = self.fetch_pos(ps, x.device, x.dtype).expand(B, -1, -1)
208
+ return self.embedder(torch.cat([x, dct], dim=-1))
209
+
210
+
211
+ class NerfFinalLayer(nn.Module):
212
+ def __init__(self, hidden_size, out_channels):
213
+ super().__init__()
214
+ self.norm = RMSNorm(hidden_size)
215
+ self.linear = nn.Linear(hidden_size, out_channels, bias=True)
216
+
217
+ def forward(self, x):
218
+ return self.linear(self.norm(x))
219
+
220
+
221
+ class SimpleMLPAdaLN(nn.Module):
222
+ """Final small MLP that maps NerfEmbedder features to per-patch RGB."""
223
+
224
+ def __init__(self, in_channels, model_channels, out_channels, z_channels, num_res_blocks, patch_size):
225
+ super().__init__()
226
+ self.in_channels = in_channels
227
+ self.model_channels = model_channels
228
+ self.out_channels = out_channels
229
+ self.num_res_blocks = num_res_blocks
230
+ self.patch_size = patch_size
231
+
232
+ self.cond_embed = nn.Linear(z_channels, patch_size ** 2 * model_channels)
233
+ self.input_proj = nn.Linear(in_channels, model_channels)
234
+
235
+ self.res_blocks = nn.ModuleList(_MLPResBlock(model_channels) for _ in range(num_res_blocks))
236
+
237
+ def forward(self, x, c):
238
+ x = self.input_proj(x)
239
+ c = self.cond_embed(c).reshape(c.shape[0], self.patch_size ** 2, -1)
240
+ for block in self.res_blocks:
241
+ x = block(x, c)
242
+ return x
243
+
244
+
245
+ class _MLPResBlock(nn.Module):
246
+ def __init__(self, channels):
247
+ super().__init__()
248
+ self.in_ln = nn.LayerNorm(channels, eps=1e-6)
249
+ self.mlp = nn.Sequential(
250
+ nn.Linear(channels, channels, bias=True),
251
+ nn.SiLU(),
252
+ nn.Linear(channels, channels, bias=True),
253
+ )
254
+ self.adaLN_modulation = nn.Sequential(
255
+ nn.SiLU(),
256
+ nn.Linear(channels, 3 * channels, bias=True),
257
+ )
258
+
259
+ def forward(self, x, y):
260
+ shift, scale, gate = self.adaLN_modulation(y).chunk(3, dim=-1)
261
+ h = self.in_ln(x) * (1 + scale) + shift
262
+ return x + gate * self.mlp(h)
263
+
264
+
265
+ class ResnetBlock(nn.Module):
266
+ """GroupNorm + Conv ResBlock used by the CoD Decoder."""
267
+
268
+ def __init__(self, *, in_channels, out_channels=None, dropout=0.0):
269
+ super().__init__()
270
+ out_channels = out_channels or in_channels
271
+ self.in_channels = in_channels
272
+ self.out_channels = out_channels
273
+
274
+ self.norm1 = Normalize(in_channels)
275
+ self.conv1 = nn.Conv2d(in_channels, out_channels, 3, padding=1)
276
+ self.norm2 = Normalize(out_channels)
277
+ self.dropout = nn.Dropout(dropout)
278
+ self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1)
279
+ if in_channels != out_channels:
280
+ self.nin_shortcut = nn.Conv2d(in_channels, out_channels, 1)
281
+
282
+ def forward(self, x):
283
+ h = self.conv1(nonlinearity(self.norm1(x)))
284
+ h = self.conv2(self.dropout(nonlinearity(self.norm2(h))))
285
+ if self.in_channels != self.out_channels:
286
+ x = self.nin_shortcut(x)
287
+ return x + h
288
+
289
+
290
+ class AttnBlock(nn.Module):
291
+ """Patched self-attention used at inference (eval mode of the original)."""
292
+
293
+ def __init__(self, in_channels, patch_size=32):
294
+ super().__init__()
295
+ self.in_channels = in_channels
296
+ self.patch_size = patch_size
297
+ self.norm = Normalize(in_channels)
298
+ self.q = nn.Conv2d(in_channels, in_channels, 1)
299
+ self.k = nn.Conv2d(in_channels, in_channels, 1)
300
+ self.v = nn.Conv2d(in_channels, in_channels, 1)
301
+ self.proj_out = nn.Conv2d(in_channels, in_channels, 1)
302
+
303
+ def forward(self, x):
304
+ h_ = self.norm(x)
305
+ Q = self.q(h_)
306
+ K = self.k(h_)
307
+ V = self.v(h_)
308
+
309
+ d = self.patch_size
310
+ b, c, H, W = Q.shape
311
+ pad_h = (d - H % d) % d
312
+ pad_w = (d - W % d) % d
313
+ if pad_h or pad_w:
314
+ Q = F.pad(Q, (0, pad_w, 0, pad_h), mode="replicate")
315
+ K = F.pad(K, (0, pad_w, 0, pad_h), mode="replicate")
316
+ V = F.pad(V, (0, pad_w, 0, pad_h), mode="replicate")
317
+ _, _, H_pad, W_pad = Q.shape
318
+ nph, npw = H_pad // d, W_pad // d
319
+ np_ = nph * npw
320
+
321
+ def to_patches(t):
322
+ return (t.reshape(b, c, nph, d, npw, d)
323
+ .permute(0, 2, 4, 1, 3, 5)
324
+ .reshape(b * np_, c, d * d))
325
+
326
+ Q = to_patches(Q)
327
+ K = to_patches(K)
328
+ V = to_patches(V)
329
+
330
+ w_ = torch.bmm(Q.permute(0, 2, 1), K) * (c ** -0.5)
331
+ w_ = F.softmax(w_, dim=2).permute(0, 2, 1)
332
+ 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)
333
+ if pad_h or pad_w:
334
+ h_ = h_[:, :, :H, :W]
335
+ return x + self.proj_out(h_)
336
+
337
+
338
+ # ---------------------------------------------------------------------------
339
+ # adaLN constant-folding: at fixed t=0, adaLN_modulation(c) is constant.
340
+ # Replace the MLP with a buffer so DiCoBlock.forward stays unchanged and
341
+ # torch.compile can fuse the surrounding ops normally.
342
+ # ---------------------------------------------------------------------------
343
+ class _ConstAdaLN(nn.Module):
344
+ def __init__(self, modulation: torch.Tensor):
345
+ super().__init__()
346
+ self.register_buffer("modulation", modulation.detach().clone())
347
+
348
+ def forward(self, c):
349
+ b = c.shape[0]
350
+ if self.modulation.shape[0] != b:
351
+ return self.modulation.expand(b, *self.modulation.shape[1:])
352
+ return self.modulation
353
+
354
+
355
+ def _replace_adaln_with_const(module: nn.Module, c: torch.Tensor) -> int:
356
+ # Only DiCoBlock is targeted: its adaLN is conditioned solely on t.
357
+ # Other adaLN_modulation submodules (e.g. _MLPResBlock in the decoder MLP)
358
+ # take a per-position latent and must not be folded.
359
+ n = 0
360
+ for child in module.modules():
361
+ if not isinstance(child, DiCoBlock):
362
+ continue
363
+ adaln = child.adaLN_modulation
364
+ if isinstance(adaln, _ConstAdaLN):
365
+ continue
366
+ with torch.no_grad():
367
+ mod = adaln(c)
368
+ child.adaLN_modulation = _ConstAdaLN(mod)
369
+ n += 1
370
+ return n
371
+
372
+
373
+ # ---------------------------------------------------------------------------
374
+ # CoD Decoder: latent → conditioning features for the denoiser
375
+ # ---------------------------------------------------------------------------
376
+ class _Decoder(nn.Module):
377
+ """ds=16, up2x=True, light=True only."""
378
+
379
+ def __init__(self, out_ch=384, z_ch=128):
380
+ super().__init__()
381
+ self.conv_in = nn.Conv2d(z_ch, out_ch, kernel_size=3, stride=1, padding=1)
382
+ self.block = nn.Sequential(
383
+ ResnetBlock(in_channels=out_ch, out_channels=out_ch),
384
+ AttnBlock(out_ch, patch_size=32),
385
+ ResnetBlock(in_channels=out_ch, out_channels=out_ch),
386
+ AttnBlock(out_ch, patch_size=32),
387
+ ResnetBlock(in_channels=out_ch, out_channels=out_ch),
388
+ )
389
+ self.norm_out = Normalize(out_ch)
390
+ self.conv_out = nn.Conv2d(out_ch, out_ch, kernel_size=3, stride=1, padding=1)
391
+ self.ada = nn.Identity()
392
+
393
+ def forward(self, z):
394
+ h = self.block(self.conv_in(z))
395
+ h = self.conv_out(nonlinearity(self.norm_out(h)))
396
+ return self.ada(h)
397
+
398
+
399
+ # ---------------------------------------------------------------------------
400
+ # DConvEncoder: image → packed (mean, logvar) latent
401
+ # ---------------------------------------------------------------------------
402
+ class _DConvEncoder(nn.Module):
403
+ def __init__(
404
+ self,
405
+ z_ch=128,
406
+ hidden_size=384,
407
+ num_blocks=21,
408
+ patch_size=16,
409
+ mlp_ratio=4.0,
410
+ head_size=768,
411
+ num_head_blocks=2,
412
+ out_ch_mult=2,
413
+ ):
414
+ super().__init__()
415
+ self.z_ch = z_ch
416
+ self.patch_size = patch_size
417
+ self.patch_cond_embed = nn.Conv2d(3, head_size, kernel_size=patch_size, stride=patch_size, bias=True)
418
+ self.head_blocks = nn.ModuleList([
419
+ _EncoderDiCoBlock(head_size, mlp_ratio=mlp_ratio) for _ in range(num_head_blocks)
420
+ ])
421
+ self.proj_down = nn.Conv2d(head_size, hidden_size, kernel_size=1, bias=True)
422
+ self.z_proj = nn.Conv2d(z_ch, hidden_size, kernel_size=1, bias=True)
423
+ self.fuse_proj = nn.Conv2d(hidden_size * 2, hidden_size, kernel_size=1, bias=True)
424
+ self.t_embedder = TimestepEmbedder(hidden_size)
425
+ self.blocks = nn.ModuleList([
426
+ DiCoBlock(hidden_size, mlp_ratio=mlp_ratio) for _ in range(num_blocks)
427
+ ])
428
+ self.norm_out = LayerNorm2d(hidden_size)
429
+ self.proj_out = nn.Conv2d(hidden_size, z_ch * out_ch_mult, kernel_size=1, bias=True)
430
+
431
+ def forward_pred(self, z_t, t, y):
432
+ cond = self.patch_cond_embed(y)
433
+ for block in self.head_blocks:
434
+ cond = block(cond)
435
+ cond = self.proj_down(cond)
436
+
437
+ s = self.fuse_proj(torch.cat([cond, self.z_proj(z_t)], dim=1))
438
+ c = self.t_embedder(t.view(-1))
439
+ for block in self.blocks:
440
+ s = block(s, c)
441
+ return self.proj_out(self.norm_out(s))
442
+
443
+
444
+ # ---------------------------------------------------------------------------
445
+ # DConv denoiser: latent (via cond) + zero noise → reconstructed image
446
+ # ---------------------------------------------------------------------------
447
+ class _YEmbedder(nn.Module):
448
+ """Holds only the CoD decoder; the original Flux2 VAE encoder side is omitted."""
449
+
450
+ def __init__(self, ch=384, z_ch=128):
451
+ super().__init__()
452
+ self.decoder = _Decoder(out_ch=ch, z_ch=z_ch)
453
+
454
+
455
+ class _DConvDenoiser(nn.Module):
456
+ def __init__(
457
+ self,
458
+ patch_size=16,
459
+ in_channels=3,
460
+ hidden_size=384,
461
+ hidden_size_x=32,
462
+ mlp_ratio=4.0,
463
+ num_blocks=24,
464
+ num_cond_blocks=21,
465
+ bottleneck_dim=128,
466
+ ):
467
+ super().__init__()
468
+ self.in_channels = in_channels
469
+ self.patch_size = patch_size
470
+ self.hidden_size = hidden_size
471
+ self.num_cond_blocks = num_cond_blocks
472
+
473
+ self.t_embedder = TimestepEmbedder(hidden_size)
474
+ self.y_embedder_x = nn.Conv2d(hidden_size, hidden_size_x * patch_size ** 2, 1, 1, 0)
475
+ self.x_embedder = NerfEmbedder(in_channels + hidden_size_x, hidden_size_x, max_freqs=8)
476
+ self.s_embedder = BottleneckPatchEmbed(patch_size, in_channels, bottleneck_dim, hidden_size, bias=True)
477
+ self.blocks = nn.ModuleList([
478
+ DiCoBlock(hidden_size, mlp_ratio=mlp_ratio) for _ in range(num_cond_blocks)
479
+ ])
480
+ self.dec_net = SimpleMLPAdaLN(
481
+ in_channels=hidden_size_x,
482
+ model_channels=hidden_size_x,
483
+ out_channels=in_channels,
484
+ z_channels=hidden_size,
485
+ num_res_blocks=num_blocks - num_cond_blocks,
486
+ patch_size=patch_size,
487
+ )
488
+ self.final_layer = NerfFinalLayer(hidden_size_x, in_channels)
489
+ self.y_embedder = _YEmbedder(ch=hidden_size, z_ch=bottleneck_dim)
490
+
491
+ def forward(self, x, t, cond):
492
+ b, _, h, w = x.shape
493
+ c = self.t_embedder(t.view(-1))
494
+
495
+ s = self.s_embedder(x, cond)
496
+ for block in self.blocks:
497
+ s = block(s, c)
498
+
499
+ length = s.shape[-2] * s.shape[-1]
500
+ s = s.permute(0, 2, 3, 1).reshape(-1, self.hidden_size)
501
+
502
+ x = torch.nn.functional.unfold(x, kernel_size=self.patch_size, stride=self.patch_size)
503
+ x = torch.cat([x, self.y_embedder_x(cond).flatten(2)], dim=1)
504
+ x = x.reshape(b, -1, self.patch_size ** 2, length).permute(0, 3, 2, 1).flatten(0, 1)
505
+ x = self.x_embedder(x)
506
+
507
+ x = self.dec_net(x, s)
508
+ x = self.final_layer(x)
509
+ x = x.transpose(1, 2).reshape(b, length, -1)
510
+ return torch.nn.functional.fold(
511
+ x.transpose(1, 2).contiguous(), (h, w),
512
+ kernel_size=self.patch_size, stride=self.patch_size,
513
+ )
514
+
515
+
516
+ # ---------------------------------------------------------------------------
517
+ # Wrapper
518
+ # ---------------------------------------------------------------------------
519
+ def _load_state_dict(ckpt_path: str):
520
+ if ckpt_path.endswith(".safetensors"):
521
+ from safetensors.torch import load_file
522
+ return load_file(ckpt_path, device="cpu")
523
+ if os.path.exists(os.path.join(ckpt_path, "checkpoint-state_dict.pt")):
524
+ ckpt_path = os.path.join(ckpt_path, "checkpoint-state_dict.pt")
525
+ elif os.path.isdir(ckpt_path):
526
+ ckpt_path = os.path.join(ckpt_path, "checkpoint", "mp_rank_00_model_states.pt")
527
+ state = torch.load(ckpt_path, map_location="cpu")
528
+ if "module" in state:
529
+ return state["module"]
530
+ if "state_dict" in state:
531
+ return state["state_dict"]
532
+ return state
533
+
534
+
535
+ class MageVAE(nn.Module):
536
+ """
537
+ Encode: DConvEncoder (one-step diffusion) → latent [B, 128, H/16, W/16]
538
+ Decode: DConvDenoiser + CoD Decoder → image [B, 3, H, W] in [-1, 1]
539
+ """
540
+
541
+ latent_channels = 128
542
+ downsample_factor = 16
543
+
544
+ def __init__(self, ckpt_path: str, sample_posterior: bool = True):
545
+ super().__init__()
546
+ self.sample_posterior = sample_posterior
547
+
548
+ self.dconv_encoder = _DConvEncoder()
549
+ self.decoder_model = _DConvDenoiser()
550
+
551
+ sd = _load_state_dict(ckpt_path)
552
+ self._load_encoder(sd, ckpt_path)
553
+ self._load_decoder(sd, ckpt_path)
554
+
555
+ # adaLN modulation depends only on t, and we always run at t=0.
556
+ # Precompute and drop the MLPs once at construction (~37M params saved).
557
+ self._freeze_adaln_cache()
558
+
559
+ def _load_encoder(self, sd, ckpt_path):
560
+ prefix = "student.dconv_encoder."
561
+ enc_sd = {k[len(prefix):]: v for k, v in sd.items() if k.startswith(prefix)}
562
+ if not enc_sd:
563
+ raise RuntimeError(f"CoDEncoder: no '{prefix}*' keys in {ckpt_path}")
564
+ proj = enc_sd.get("proj_out.weight")
565
+ if proj is None or proj.shape[0] != 2 * self.latent_channels:
566
+ raise RuntimeError(
567
+ f"CoDEncoder: expected packed mean+logvar (proj_out out_channels="
568
+ f"{2 * self.latent_channels}), got {None if proj is None else tuple(proj.shape)}"
569
+ )
570
+ missing, unexpected = self.dconv_encoder.load_state_dict(enc_sd, strict=False)
571
+ logger.info(
572
+ f"CoDEncoder: loaded {len(enc_sd)} keys, "
573
+ f"missing={len(missing)}, unexpected={len(unexpected)}"
574
+ )
575
+ if missing:
576
+ logger.warning(f"CoDEncoder missing: {missing[:10]}")
577
+
578
+ def _load_decoder(self, sd, ckpt_path):
579
+ prefix = "pipeline."
580
+ if not any(k.startswith(prefix) for k in sd):
581
+ raise RuntimeError(f"CoDDecoder: no '{prefix}*' keys in {ckpt_path}")
582
+ model_dict = self.decoder_model.state_dict()
583
+ matched = {}
584
+ for k, v in sd.items():
585
+ if not k.startswith(prefix):
586
+ continue
587
+ new_k = k[len(prefix):]
588
+ if new_k.startswith("y_embedder.encoder.") or new_k.startswith("y_embedder.bottleneck."):
589
+ continue
590
+ if new_k in model_dict and model_dict[new_k].shape == v.shape:
591
+ matched[new_k] = v
592
+ self.decoder_model.load_state_dict(matched, strict=False)
593
+ logger.info(f"CoDDecoder: loaded {len(matched)} params (denoiser + y_embedder.decoder)")
594
+ if not matched:
595
+ raise RuntimeError(f"CoDDecoder: 0 params matched from {ckpt_path}")
596
+
597
+ @torch.no_grad()
598
+ def _moments(self, x: torch.Tensor):
599
+ B, _, H, W = x.shape
600
+ ps = self.dconv_encoder.patch_size
601
+ z_t = torch.zeros(B, self.dconv_encoder.z_ch, H // ps, W // ps, device=x.device, dtype=x.dtype)
602
+ t = torch.zeros(B, device=x.device, dtype=x.dtype)
603
+ out = self.dconv_encoder.forward_pred(z_t, t, x)
604
+ mean = out[:, : self.latent_channels]
605
+ logvar = out[:, self.latent_channels :].clamp(min=-20.0, max=10.0)
606
+ return mean, logvar
607
+
608
+ @torch.no_grad()
609
+ def _encode_moments(self, x: torch.Tensor):
610
+ # Compile target: pure deterministic part of encode (no RNG, no
611
+ # asserts), so torch.compile produces a single dynamic graph.
612
+ return self._moments(x)
613
+
614
+ @torch.no_grad()
615
+ def encode(self, x: torch.Tensor) -> torch.Tensor:
616
+ ps = self.dconv_encoder.patch_size
617
+ H, W = x.shape[-2], x.shape[-1]
618
+ if H % ps or W % ps:
619
+ raise ValueError(f"H, W must be multiples of {ps}, got ({H}, {W})")
620
+ mean, logvar = self._encode_moments(x)
621
+ if self.sample_posterior:
622
+ return mean + torch.exp(0.5 * logvar) * torch.randn_like(mean)
623
+ return mean
624
+
625
+ @torch.no_grad()
626
+ def decode(self, z: torch.Tensor) -> torch.Tensor:
627
+ cond = self.decoder_model.y_embedder.decoder(z)
628
+ B = z.shape[0]
629
+ H = z.shape[2] * self.downsample_factor
630
+ W = z.shape[3] * self.downsample_factor
631
+ noise = torch.zeros(B, 3, H, W, device=z.device, dtype=z.dtype)
632
+ t = torch.zeros(B, device=z.device, dtype=z.dtype)
633
+ return self.decoder_model.forward(noise, t, cond)
634
+
635
+ @property
636
+ def device(self):
637
+ return next(self.parameters()).device
638
+
639
+ @property
640
+ def dtype(self):
641
+ return next(self.parameters()).dtype
642
+
643
+ def _freeze_adaln_cache(self):
644
+ """Constant-fold adaLN_modulation MLPs at t=0 (encoder + decoder)."""
645
+ device = next(self.parameters()).device
646
+ dtype = next(self.parameters()).dtype
647
+ t = torch.zeros(1, device=device, dtype=dtype)
648
+ c_enc = self.dconv_encoder.t_embedder(t)
649
+ _replace_adaln_with_const(self.dconv_encoder, c_enc)
650
+ c_dec = self.decoder_model.t_embedder(t)
651
+ _replace_adaln_with_const(self.decoder_model, c_dec)
mage_flow/models/modules/text_encoder.py ADDED
@@ -0,0 +1,707 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Qwen3-VL text encoder: custom HF model + packing-aware forward patches + TextEncoder wrapper."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+
8
+ try:
9
+ from typing import Unpack
10
+ except ImportError:
11
+ from typing_extensions import Unpack
12
+
13
+ import torch
14
+ from loguru import logger
15
+ from torch import nn
16
+ from transformers import AutoProcessor, AutoTokenizer, Cache, Qwen3VLForConditionalGeneration
17
+ from transformers.cache_utils import DynamicCache
18
+ from transformers.masking_utils import create_causal_mask
19
+ from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
20
+ from transformers.modeling_outputs import BaseModelOutputWithPast
21
+ from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS
22
+ from transformers.models.qwen3_vl.modeling_qwen3_vl import (
23
+ Qwen3VLCausalLMOutputWithPast,
24
+ apply_rotary_pos_emb,
25
+ eager_attention_forward,
26
+ )
27
+ from transformers.utils import ModelOutput
28
+
29
+ from ._attn_backend import flash_attn_varlen_func
30
+
31
+
32
+ # ===========================================================================
33
+ # Custom Qwen3-VL model (customizable forward output)
34
+ # ===========================================================================
35
+
36
+ @dataclass
37
+ class Qwen3VLModelOutput(ModelOutput):
38
+ """Flexible output class for custom Qwen3-VL model."""
39
+
40
+ loss: torch.FloatTensor | None = None
41
+ logits: torch.FloatTensor | None = None
42
+ past_key_values: Cache | None = None
43
+ hidden_states: tuple[torch.FloatTensor, ...] | None = None
44
+ last_hidden_state: torch.FloatTensor | None = None
45
+ attentions: tuple[torch.FloatTensor, ...] | None = None
46
+ rope_deltas: torch.LongTensor | None = None
47
+
48
+
49
+ class CustomQwen3VLForConditionalGeneration(Qwen3VLForConditionalGeneration):
50
+ """
51
+ Custom Qwen3-VL model that allows customizing the forward output.
52
+
53
+ This class inherits from Qwen3VLForConditionalGeneration and provides
54
+ hooks to customize what is returned from the forward pass.
55
+
56
+ Example usage:
57
+ ```python
58
+ model = CustomQwen3VLForConditionalGeneration.from_pretrained(
59
+ "Qwen/Qwen3-VL-8B-Instruct",
60
+ attn_implementation="flash_attention_2" # Use flash attention for faster inference
61
+ )
62
+
63
+ # Option 1: Use built-in output modes
64
+ model.set_output_mode("embedding") # Only return last hidden state (default)
65
+ model.set_output_mode("full") # Return everything
66
+ model.set_output_mode("logits") # Only return logits
67
+
68
+ # Option 2: Set a custom output processor
69
+ def my_custom_output(hidden_states, logits, outputs, **kwargs):
70
+ return {"embeddings": hidden_states, "pooled": hidden_states.mean(dim=1)}
71
+ model.set_output_processor(my_custom_output)
72
+ ```
73
+ """
74
+
75
+ # Output mode constants
76
+ OUTPUT_MODE_FULL = "full"
77
+ OUTPUT_MODE_EMBEDDING = "embedding"
78
+ OUTPUT_MODE_LOGITS = "logits"
79
+ OUTPUT_MODE_HIDDEN = "hidden"
80
+
81
+ def __init__(self, config):
82
+ super().__init__(config)
83
+ self._output_mode = self.OUTPUT_MODE_EMBEDDING
84
+ self._skip_lm_head = True
85
+
86
+ def set_output_mode(self, mode: str):
87
+ """
88
+ Set the output mode for the forward pass.
89
+
90
+ Args:
91
+ mode: One of:
92
+ - "full": Return full Qwen3VLCausalLMOutputWithPast
93
+ - "embedding": Only return last hidden state (skip lm_head) (default)
94
+ - "logits": Only return logits
95
+ - "hidden": Return all hidden states
96
+ """
97
+ valid_modes = [
98
+ self.OUTPUT_MODE_FULL,
99
+ self.OUTPUT_MODE_EMBEDDING,
100
+ self.OUTPUT_MODE_LOGITS,
101
+ self.OUTPUT_MODE_HIDDEN,
102
+ ]
103
+ if mode not in valid_modes:
104
+ raise ValueError(f"Invalid output mode: {mode}. Must be one of {valid_modes}")
105
+ self._output_mode = mode
106
+ self._skip_lm_head = mode == self.OUTPUT_MODE_EMBEDDING
107
+
108
+ def forward(
109
+ self,
110
+ input_ids: torch.LongTensor | None = None,
111
+ attention_mask: torch.Tensor | None = None,
112
+ position_ids: torch.LongTensor | None = None,
113
+ past_key_values: Cache | None = None,
114
+ inputs_embeds: torch.FloatTensor | None = None,
115
+ labels: torch.LongTensor | None = None,
116
+ pixel_values: torch.Tensor | None = None,
117
+ pixel_values_videos: torch.FloatTensor | None = None,
118
+ image_grid_thw: torch.LongTensor | None = None,
119
+ video_grid_thw: torch.LongTensor | None = None,
120
+ cache_position: torch.LongTensor | None = None,
121
+ logits_to_keep: int | torch.Tensor = 0,
122
+ output_attentions: bool | None = None,
123
+ output_hidden_states: bool | None = None,
124
+ return_dict: bool | None = None,
125
+ **kwargs,
126
+ ) -> Qwen3VLCausalLMOutputWithPast | Qwen3VLModelOutput | dict | torch.Tensor:
127
+ """
128
+ Forward pass with customizable output.
129
+
130
+ Returns different outputs based on the configured output mode or custom processor.
131
+ """
132
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
133
+ output_hidden_states = (
134
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
135
+ )
136
+
137
+ # Get outputs from the base model (Qwen3VLModel)
138
+ outputs = self.model(
139
+ input_ids=input_ids,
140
+ pixel_values=pixel_values,
141
+ pixel_values_videos=pixel_values_videos,
142
+ image_grid_thw=image_grid_thw,
143
+ video_grid_thw=video_grid_thw,
144
+ position_ids=position_ids,
145
+ attention_mask=attention_mask,
146
+ past_key_values=past_key_values,
147
+ inputs_embeds=inputs_embeds,
148
+ cache_position=cache_position,
149
+ output_attentions=output_attentions,
150
+ output_hidden_states=output_hidden_states,
151
+ return_dict=True,
152
+ **kwargs,
153
+ )
154
+
155
+ # Get the last hidden state
156
+ hidden_states = outputs[0] # This is the last hidden state
157
+
158
+ # Compute logits if not skipping lm_head
159
+ logits = None
160
+ if not self._skip_lm_head:
161
+ slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
162
+ logits = self.lm_head(hidden_states[:, slice_indices, :])
163
+
164
+ # Compute loss if labels are provided
165
+ loss = None
166
+ if labels is not None and logits is not None:
167
+ loss = self.loss_function(
168
+ logits=logits, labels=labels, vocab_size=self.config.text_config.vocab_size, **kwargs
169
+ )
170
+
171
+ # Return based on output mode
172
+ if self._output_mode == self.OUTPUT_MODE_EMBEDDING:
173
+ return Qwen3VLModelOutput(
174
+ last_hidden_state=hidden_states,
175
+ past_key_values=outputs.past_key_values,
176
+ attentions=outputs.attentions,
177
+ rope_deltas=outputs.rope_deltas,
178
+ )
179
+ elif self._output_mode == self.OUTPUT_MODE_LOGITS:
180
+ return logits
181
+ elif self._output_mode == self.OUTPUT_MODE_HIDDEN:
182
+ return Qwen3VLModelOutput(
183
+ last_hidden_state=hidden_states,
184
+ hidden_states=outputs.hidden_states,
185
+ past_key_values=outputs.past_key_values,
186
+ attentions=outputs.attentions,
187
+ rope_deltas=outputs.rope_deltas,
188
+ )
189
+ else: # OUTPUT_MODE_FULL
190
+ return Qwen3VLCausalLMOutputWithPast(
191
+ loss=loss,
192
+ logits=logits,
193
+ past_key_values=outputs.past_key_values,
194
+ hidden_states=outputs.hidden_states,
195
+ attentions=outputs.attentions,
196
+ rope_deltas=outputs.rope_deltas,
197
+ )
198
+
199
+
200
+ # ===========================================================================
201
+ # Packing-aware forward patches (cu_seqlens) for the Qwen3-VL text encoder
202
+ # ===========================================================================
203
+
204
+ def model_forward(
205
+ self,
206
+ input_ids: torch.LongTensor | None = None,
207
+ attention_mask: torch.Tensor | None = None,
208
+ position_ids: torch.LongTensor | None = None,
209
+ past_key_values: Cache | None = None,
210
+ inputs_embeds: torch.FloatTensor | None = None,
211
+ use_cache: bool | None = None,
212
+ cache_position: torch.LongTensor | None = None,
213
+ # args for deepstack
214
+ visual_pos_masks: torch.Tensor | None = None,
215
+ deepstack_visual_embeds: list[torch.Tensor] | None = None,
216
+ **kwargs: Unpack[FlashAttentionKwargs],
217
+ ) -> tuple | BaseModelOutputWithPast:
218
+ r"""
219
+ visual_pos_masks (`torch.Tensor` of shape `(batch_size, seqlen)`, *optional*):
220
+ The mask of the visual positions.
221
+ deepstack_visual_embeds (`list[torch.Tensor]`, *optional*):
222
+ The deepstack visual embeddings. The shape is (num_layers, visual_seqlen, embed_dim).
223
+ The feature is extracted from the different visual encoder layers, and fed to the decoder
224
+ hidden states. It's from the paper DeepStack(https://arxiv.org/abs/2406.04334).
225
+ """
226
+ if (input_ids is None) ^ (inputs_embeds is not None):
227
+ raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
228
+
229
+ # torch.jit.trace() doesn't support cache objects in the output
230
+ if use_cache and past_key_values is None and not torch.jit.is_tracing():
231
+ past_key_values = DynamicCache(config=self.config)
232
+
233
+ if inputs_embeds is None:
234
+ inputs_embeds = self.embed_tokens(input_ids)
235
+
236
+ if cache_position is None:
237
+ past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
238
+ cache_position = torch.arange(
239
+ past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device
240
+ )
241
+
242
+ # the hard coded `3` is for temporal, height and width.
243
+ if position_ids is None:
244
+ position_ids = cache_position.view(1, 1, -1).expand(3, inputs_embeds.shape[0], -1)
245
+ elif position_ids.ndim == 2:
246
+ position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1)
247
+
248
+ if position_ids.ndim == 3 and position_ids.shape[0] == 4:
249
+ text_position_ids = position_ids[0]
250
+ position_ids = position_ids[1:]
251
+ else:
252
+ text_position_ids = position_ids[0]
253
+
254
+ if kwargs.get("cu_seqlens") is None:
255
+ attention_mask = create_causal_mask(
256
+ config=self.config,
257
+ input_embeds=inputs_embeds,
258
+ attention_mask=attention_mask,
259
+ cache_position=cache_position,
260
+ past_key_values=past_key_values,
261
+ position_ids=text_position_ids,
262
+ )
263
+
264
+ hidden_states = inputs_embeds
265
+
266
+ # create position embeddings to be shared across the decoder layers
267
+ position_embeddings = self.rotary_emb(hidden_states, position_ids)
268
+
269
+ # decoder layers
270
+ for layer_idx, decoder_layer in enumerate(self.layers):
271
+ layer_outputs = decoder_layer(
272
+ hidden_states,
273
+ attention_mask=attention_mask,
274
+ position_ids=text_position_ids,
275
+ past_key_values=past_key_values,
276
+ cache_position=cache_position,
277
+ position_embeddings=position_embeddings,
278
+ **kwargs,
279
+ )
280
+ hidden_states = layer_outputs
281
+
282
+ # add visual features to the hidden states of first several layers
283
+ if deepstack_visual_embeds is not None and layer_idx in range(len(deepstack_visual_embeds)):
284
+ hidden_states = self._deepstack_process(
285
+ hidden_states,
286
+ visual_pos_masks,
287
+ deepstack_visual_embeds[layer_idx],
288
+ )
289
+
290
+ hidden_states = self.norm(hidden_states)
291
+
292
+ return BaseModelOutputWithPast(
293
+ last_hidden_state=hidden_states,
294
+ past_key_values=past_key_values,
295
+ )
296
+
297
+
298
+ def forward(
299
+ self,
300
+ hidden_states: torch.Tensor,
301
+ position_embeddings: tuple[torch.Tensor, torch.Tensor],
302
+ attention_mask: torch.Tensor | None,
303
+ past_key_values: Cache | None = None,
304
+ cache_position: torch.LongTensor | None = None,
305
+ **kwargs: Unpack[FlashAttentionKwargs],
306
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
307
+ input_shape = hidden_states.shape[:-1]
308
+ hidden_shape = (*input_shape, -1, self.head_dim)
309
+
310
+ query_states = self.q_norm(self.q_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
311
+ key_states = self.k_norm(self.k_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
312
+ value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
313
+
314
+ cos, sin = position_embeddings
315
+ query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin)
316
+
317
+ if past_key_values is not None:
318
+ # sin and cos are specific to RoPE models; cache_position needed for the static cache
319
+ cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
320
+ key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx, cache_kwargs)
321
+
322
+ cu_seqlens = kwargs.get("cu_seqlens", None)
323
+
324
+ if cu_seqlens is None:
325
+ attention_interface: Callable = eager_attention_forward
326
+ if self.config._attn_implementation != "eager":
327
+ attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation]
328
+
329
+ attn_output, attn_weights = attention_interface(
330
+ self,
331
+ query_states,
332
+ key_states,
333
+ value_states,
334
+ attention_mask,
335
+ dropout=0.0 if not self.training else self.attention_dropout,
336
+ scaling=self.scaling,
337
+ **kwargs,
338
+ )
339
+ else:
340
+ max_seqlen = torch.diff(cu_seqlens).max().item() if cu_seqlens is not None else None
341
+ query_states = query_states.transpose(1, 2).squeeze(0)
342
+ key_states = key_states.transpose(1, 2).squeeze(0)
343
+ value_states = value_states.transpose(1, 2).squeeze(0)
344
+ attn_output = flash_attn_varlen_func(
345
+ q=query_states,
346
+ k=key_states,
347
+ v=value_states,
348
+ cu_seqlens_q=cu_seqlens,
349
+ cu_seqlens_k=cu_seqlens,
350
+ max_seqlen_q=max_seqlen,
351
+ max_seqlen_k=max_seqlen,
352
+ causal=True,
353
+ window_size=(-1, -1),
354
+ softmax_scale=self.head_dim**-0.5,
355
+ dropout_p=0.0,
356
+ )
357
+
358
+ attn_output = attn_output.reshape(*input_shape, -1).contiguous()
359
+ attn_output = self.o_proj(attn_output)
360
+ return attn_output, None
361
+
362
+
363
+ def qwen3_patch_forward():
364
+ """Patch the Qwen3-VL text model + attention forwards to support packed
365
+ varlen (cu_seqlens) inputs used by ``TextEncoder.forward``."""
366
+ from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLTextAttention, Qwen3VLTextModel
367
+
368
+ Qwen3VLTextModel.forward = model_forward
369
+ Qwen3VLTextAttention.forward = forward
370
+
371
+
372
+ # ===========================================================================
373
+ # TextEncoder wrapper (packed text -> DiT conditioning embeddings)
374
+ # ===========================================================================
375
+ _FA2_ALIASES = {"flash2", "fa2", "flash_attention_2", "flash_attn_2"}
376
+ _FA4_ALIASES = {"flash4", "fa4", "flash_attention_4", "flash_attn_4"}
377
+ _SDPA_ALIASES = {"sdpa", "torch_sdpa", "scaled_dot_product_attention"}
378
+
379
+
380
+ def _resolve_hf_attn_impl(attn_type: str) -> str:
381
+ """Map a project-level attn_type to a HuggingFace ``attn_implementation`` string.
382
+
383
+ ``VF_HF_ATTN_IMPL`` env var, if set, takes precedence (useful for forcing
384
+ sdpa on machines without flash-attn). For FA4 we additionally probe that
385
+ the CUTE-DSL kernel is importable and (when available) ask the HF helper
386
+ to confirm; if not, fall back to sdpa rather than crashing at load time.
387
+ """
388
+ override = os.environ.get("VF_HF_ATTN_IMPL")
389
+ if override:
390
+ return override
391
+
392
+ name = attn_type.lower().strip()
393
+ if name in _FA2_ALIASES:
394
+ return "flash_attention_2"
395
+ if name in _FA4_ALIASES:
396
+ try:
397
+ import flash_attn.cute # noqa: F401
398
+ fa4_importable = True
399
+ except Exception:
400
+ fa4_importable = False
401
+ if fa4_importable:
402
+ try:
403
+ from transformers.utils.import_utils import is_flash_attn_4_available
404
+ if is_flash_attn_4_available():
405
+ return "flash_attention_4"
406
+ except ImportError:
407
+ return "flash_attention_4"
408
+ logger.warning(
409
+ "attn_type=flash4 requested but flash_attn.cute is unavailable; "
410
+ "falling back to sdpa for HF text encoder."
411
+ )
412
+ return "sdpa"
413
+ if name in _SDPA_ALIASES:
414
+ return "sdpa"
415
+ raise ValueError(
416
+ f"Unknown attn_type {attn_type!r}; expected one of "
417
+ f"{sorted(_FA2_ALIASES | _FA4_ALIASES | _SDPA_ALIASES)}"
418
+ )
419
+
420
+
421
+ SEQ_MULTI_OF = 32
422
+
423
+
424
+
425
+ class TextEncoder(nn.Module):
426
+ def __init__(
427
+ self,
428
+ model_name: str,
429
+ version: str,
430
+ tokenizer_max_length: int,
431
+ prompt_template: dict | None,
432
+ dit_structure: dict,
433
+ use_packed_text_infer: bool = False,
434
+ attn_type: str = "flash2",
435
+ **hf_kwargs,
436
+ ):
437
+ super().__init__()
438
+ self.model_name = model_name
439
+ self.tokenizer_max_length = tokenizer_max_length
440
+ self.tokenizer: AutoTokenizer = AutoTokenizer.from_pretrained(version)
441
+ self.tokenizer.padding_side = "right"
442
+
443
+ hf_attn_impl = _resolve_hf_attn_impl(attn_type)
444
+ logger.info(f"TextEncoder attn_type={attn_type} -> attn_implementation={hf_attn_impl}")
445
+
446
+ logger.info("init vl model: qwen3")
447
+ self.hf_module: CustomQwen3VLForConditionalGeneration = CustomQwen3VLForConditionalGeneration.from_pretrained(
448
+ version,
449
+ attn_implementation=hf_attn_impl,
450
+ **hf_kwargs
451
+ )
452
+
453
+ # Use local_files_only if version is a local path (absolute path or contains path separators)
454
+ is_local = version.startswith("/") or os.sep in version
455
+ self.processor = AutoProcessor.from_pretrained(version, local_files_only=is_local)
456
+
457
+ self.hf_module = self.hf_module.eval().requires_grad_(False)
458
+
459
+ prompt_template = prompt_template or {}
460
+ self.prompt_template_encode = prompt_template.get("template", "")
461
+ self.prompt_template_encode_start_idx = prompt_template.get("start_idx", 0)
462
+ self.dit_structure = dit_structure
463
+ self.use_packed_text_infer = use_packed_text_infer
464
+
465
+ def forward(
466
+ self,
467
+ input_ids: torch.Tensor,
468
+ cu_seqlens: torch.Tensor,
469
+ inputs: dict | None = None,
470
+ drop_idx_override: int | None = None,
471
+ ):
472
+ """Encode packed text (varlen ``cu_seqlens``) into DiT conditioning embeddings.
473
+
474
+ This is the sole text-embedding path — both t2i and edit call it. Uses
475
+ Flash-Attention-2's varlen capability (``cu_seqlens``) via the patched
476
+ Qwen3-VL forward to process several concatenated sequences in a single
477
+ launch, with no padding. Verified numerically identical to a padded-batch
478
+ forward with per-sample cu_seqlens isolation (zero cross-contamination).
479
+
480
+ Args:
481
+ input_ids: Packed token ids ``[Total_L]``.
482
+ cu_seqlens: Cumulative sequence lengths ``[B+1]``.
483
+ inputs: Optional dict with additional model inputs (e.g. ``pixel_values``,
484
+ ``image_grid_thw`` for the multimodal edit path). Passed through to
485
+ the text encoder.
486
+ drop_idx_override: If set, override the number of leading (system-prompt)
487
+ tokens to drop per sequence. Use 0 for multi-turn where the system
488
+ prompt is embedded in the conversation and should not be stripped.
489
+
490
+ Returns:
491
+ dict with keys:
492
+ - ``txt``: text embeddings ``[Total_L - B*drop_idx, D]`` (system prompt dropped)
493
+ - ``vec``: pooled text embeddings ``[B, D]``
494
+ - ``txt_seq_lens``: per-sequence lengths ``[B]`` (after dropping system prompt)
495
+ """
496
+ # Compute seqlens from cu_seqlens
497
+ seqlens = cu_seqlens[1:] - cu_seqlens[:-1]
498
+ seqlens_list = seqlens.cpu().tolist()
499
+
500
+ # Build position_ids for packing: each sequence starts from 0
501
+ position_ids_list = []
502
+ for length in seqlens_list:
503
+ position_ids_list.append(torch.arange(length, device=input_ids.device))
504
+ position_ids = torch.cat(position_ids_list) # [Total_L]
505
+
506
+ # Reshape for model input: [1, Total_L]
507
+ input_ids_packed = input_ids.unsqueeze(0) # [1, Total_L]
508
+ position_ids_packed = position_ids.unsqueeze(0) # [1, Total_L]
509
+
510
+ # Move to text encoder device
511
+ device = self.hf_module.device
512
+ input_ids_packed = input_ids_packed.to(device)
513
+ position_ids_packed = position_ids_packed.to(device)
514
+
515
+ # Get text embeddings (the text encoder is always frozen)
516
+ with torch.no_grad():
517
+ forward_kwargs = {
518
+ "input_ids": input_ids_packed,
519
+ "cu_seqlens": cu_seqlens,
520
+ "position_ids": position_ids_packed,
521
+ "output_hidden_states": False,
522
+ "max_seqlen": None,
523
+ }
524
+ # Pass multimodal inputs for edit mode (reference images)
525
+ if inputs is not None:
526
+ for key in ("pixel_values", "image_grid_thw"):
527
+ if key in inputs and inputs[key] is not None:
528
+ val = inputs[key]
529
+ if hasattr(val, "to"):
530
+ val = val.to(device)
531
+ forward_kwargs[key] = val
532
+ outputs = self.hf_module(**forward_kwargs)
533
+
534
+ # Extract hidden state
535
+ if hasattr(outputs, "last_hidden_state") and outputs.last_hidden_state is not None:
536
+ hidden = outputs.last_hidden_state # [1, Total_L, D]
537
+ elif hasattr(outputs, "hidden_states"):
538
+ hidden = outputs.hidden_states[-1]
539
+
540
+ # Remove batch dimension: [Total_L, D]
541
+ hidden = hidden.squeeze(0)
542
+
543
+ # Get drop_idx (system prompt length to skip).
544
+ # For multi-turn, drop_idx_override=0 is passed since system prompt is in the messages.
545
+ if drop_idx_override is not None:
546
+ drop_idx = drop_idx_override
547
+ else:
548
+ drop_idx = self.prompt_template_encode_start_idx
549
+
550
+ # Split hidden states by sequence
551
+ hidden_split = torch.split(hidden, seqlens_list, dim=0)
552
+
553
+ # Extract valid embeddings (drop system prompt) and compute vec
554
+ txt_list = []
555
+ vec_list = []
556
+ valid_lengths = []
557
+
558
+ for h in hidden_split:
559
+ # Drop system prompt tokens
560
+ h_valid = h[drop_idx:] # [seq_len - drop_idx, D]
561
+ txt_list.append(h_valid)
562
+ valid_lengths.append(h_valid.shape[0])
563
+
564
+ # Compute pooled embedding (mean of valid tokens only, after dropping system prompt)
565
+ vec_list.append(h_valid.mean(dim=0)) # [D]
566
+
567
+ txt = torch.cat(txt_list, dim=0) # [Total_valid, D]
568
+ vec = torch.stack(vec_list, dim=0) # [B, D]
569
+ txt_seq_lens = torch.tensor(valid_lengths, device=input_ids.device)
570
+
571
+ result = {
572
+ "txt": txt,
573
+ "vec": vec,
574
+ "txt_seq_lens": txt_seq_lens,
575
+ }
576
+
577
+ return result
578
+
579
+ # ------------------------------------------------------------------
580
+ # Mandatory content-policy screening (same Qwen3-VL weights)
581
+ # ------------------------------------------------------------------
582
+ # The policy classifier lives HERE, on the text encoder, so it runs on the
583
+ # exact weights that produce the diffusion conditioning and is not a
584
+ # separable, toggleable pre-pass in the pipeline. The classifier needs
585
+ # autoregressive ``.generate()`` (JSON verdict) whereas conditioning is a
586
+ # single embedding forward — they cannot be one GPU forward without a
587
+ # trained classification head, so "fused" here means: same module, same
588
+ # weights, always run, FAIL-CLOSED (any error blocks).
589
+
590
+ def screen_text(self, prompt: str, max_new_tokens: int = 160):
591
+ """Classify a text-to-image ``prompt`` against the content policy.
592
+
593
+ Returns a ``FilterVerdict``. FAIL-CLOSED: any error (generation, parse)
594
+ returns ``violates=True`` so a broken classifier cannot be used as a
595
+ bypass. An empty prompt is not a violation.
596
+ """
597
+ from .mage_text import (
598
+ CONTENT_FILTER_SYSTEM, FilterVerdict, _extract_json_object,
599
+ _full_output_mode,
600
+ )
601
+
602
+ if not prompt or not prompt.strip():
603
+ return FilterVerdict(False, [], "empty prompt", "")
604
+ try:
605
+ tokenizer = self.tokenizer
606
+ hf = self.hf_module
607
+ device = next(hf.parameters()).device
608
+
609
+ messages = [
610
+ {"role": "system", "content": CONTENT_FILTER_SYSTEM},
611
+ {"role": "user", "content": f"Prompt to classify:\n{prompt}"},
612
+ ]
613
+ text = tokenizer.apply_chat_template(
614
+ messages, tokenize=False, add_generation_prompt=True)
615
+ inputs = tokenizer(text, return_tensors="pt").to(device)
616
+
617
+ eos_id = tokenizer.eos_token_id
618
+ pad_id = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else eos_id
619
+
620
+ with _full_output_mode(hf), torch.no_grad():
621
+ out = hf.generate(
622
+ **inputs, max_new_tokens=max_new_tokens, do_sample=False,
623
+ pad_token_id=pad_id, eos_token_id=eos_id)
624
+ gen = tokenizer.decode(
625
+ out[0, inputs.input_ids.shape[1]:], skip_special_tokens=True).strip()
626
+
627
+ parsed = _extract_json_object(gen)
628
+ violates = bool(parsed.get("violates", False))
629
+ cats = [c for c in (parsed.get("categories", []) or []) if isinstance(c, str)]
630
+ reason = str(parsed.get("reason", "")).strip()
631
+ return FilterVerdict(violates, cats, reason, gen)
632
+ except Exception as exc: # noqa: BLE001
633
+ # FAIL-CLOSED: block on any screening error.
634
+ return FilterVerdict(
635
+ True, ["policy"], f"filter error (blocked): {type(exc).__name__}: {exc}", "")
636
+
637
+ def screen_edit(self, prompt: str, ref_images, max_new_tokens: int = 192):
638
+ """Classify an image-EDIT request (source image(s) + instruction).
639
+
640
+ Considers BOTH the source image(s) and the instruction via multimodal
641
+ Qwen3-VL. Falls back to :meth:`screen_text` when no image is given.
642
+ FAIL-CLOSED: any error returns ``violates=True``.
643
+ """
644
+ from PIL import Image
645
+
646
+ from .mage_text import (
647
+ CONTENT_FILTER_EDIT_SYSTEM, FilterVerdict, _extract_json_object,
648
+ _full_output_mode,
649
+ )
650
+
651
+ pils = [ref_images] if isinstance(ref_images, Image.Image) else list(ref_images)
652
+ pils = [p.convert("RGB") for p in pils if p is not None]
653
+ if not pils:
654
+ return self.screen_text(prompt, max_new_tokens=max_new_tokens)
655
+
656
+ instruction = (prompt or "").strip() or "(no textual instruction)"
657
+ try:
658
+ processor = self.processor
659
+ tokenizer = self.tokenizer
660
+ hf = self.hf_module
661
+ device = next(hf.parameters()).device
662
+
663
+ user_content = [{"type": "image"} for _ in pils]
664
+ user_content.append({
665
+ "type": "text",
666
+ "text": (
667
+ f"There {'is' if len(pils) == 1 else 'are'} {len(pils)} source "
668
+ f"image(s) above. Edit instruction: {instruction}\n"
669
+ "Classify this edit request."
670
+ ),
671
+ })
672
+ messages = [
673
+ {"role": "system", "content": CONTENT_FILTER_EDIT_SYSTEM},
674
+ {"role": "user", "content": user_content},
675
+ ]
676
+ text = processor.apply_chat_template(
677
+ messages, tokenize=False, add_generation_prompt=True)
678
+ inputs = processor(
679
+ text=[text], images=pils, padding=True, return_tensors="pt").to(device)
680
+
681
+ eos_id = tokenizer.eos_token_id
682
+ pad_id = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else eos_id
683
+
684
+ # Keep only the kwargs Qwen3-VL .generate() consumes.
685
+ gen_inputs = {
686
+ k: inputs[k]
687
+ for k in ("input_ids", "attention_mask", "pixel_values", "image_grid_thw")
688
+ if k in inputs and inputs[k] is not None
689
+ }
690
+ input_len = gen_inputs["input_ids"].shape[1]
691
+
692
+ with _full_output_mode(hf), torch.no_grad():
693
+ out = hf.generate(
694
+ **gen_inputs, max_new_tokens=max_new_tokens, do_sample=False,
695
+ pad_token_id=pad_id, eos_token_id=eos_id)
696
+ gen = tokenizer.decode(out[0, input_len:], skip_special_tokens=True).strip()
697
+
698
+ parsed = _extract_json_object(gen)
699
+ violates = bool(parsed.get("violates", False))
700
+ cats = [c for c in (parsed.get("categories", []) or []) if isinstance(c, str)]
701
+ reason = str(parsed.get("reason", "")).strip()
702
+ return FilterVerdict(violates, cats, reason, gen)
703
+ except Exception as exc: # noqa: BLE001
704
+ # FAIL-CLOSED: block on any screening error.
705
+ return FilterVerdict(
706
+ True, ["policy"], f"edit filter error (blocked): {type(exc).__name__}: {exc}", "")
707
+
mage_flow/models/utils.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import math
3
+ import os
4
+
5
+ import torch
6
+ from einops import rearrange
7
+ from loguru import logger
8
+ from safetensors.torch import load_file
9
+ from safetensors.torch import load_file as load_sft
10
+ from torch import Tensor
11
+
12
+ from .mage_flow import MageFlow, MageFlowParams
13
+
14
+
15
+ def get_noise(
16
+ num_samples: int,
17
+ channel: int,
18
+ height: int,
19
+ width: int,
20
+ device: torch.device,
21
+ dtype: torch.dtype,
22
+ seed: int,
23
+ ):
24
+ # MageVAE: 16x downsample, no patch packing
25
+ return torch.randn(
26
+ num_samples,
27
+ channel,
28
+ math.ceil(height / 16),
29
+ math.ceil(width / 16),
30
+ device=device,
31
+ dtype=dtype,
32
+ generator=torch.Generator(device=device).manual_seed(seed),
33
+ )
34
+
35
+
36
+ def unpack(x: Tensor, height: int, width: int) -> Tensor:
37
+ # MageVAE: [B, H*W, C] -> [B, C, H, W], no patch unpacking
38
+ return rearrange(
39
+ x,
40
+ "b (h w) c -> b c h w",
41
+ h=math.ceil(height / 16),
42
+ w=math.ceil(width / 16),
43
+ )
44
+
45
+
46
+ PROMPT_TEMPLATE = {
47
+ "default": {"template": "{}", "start_idx": 0},
48
+ "default-nonthinking": {"template": "{}<think>\n\n</think>\n\n", "start_idx": 0},
49
+ "mage-flow": {
50
+ "template": (
51
+ "<|im_start|>system\nDescribe the image by detailing the color, shape, size, texture, quantity, "
52
+ "text, spatial relationships of the objects and background:"
53
+ "<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
54
+ ),
55
+ "start_idx": 34,
56
+ },
57
+ "mage-flow-edit": {
58
+ "template": (
59
+ "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture,"
60
+ " objects, background), then explain how the user's text instruction should alter or modify the image. "
61
+ "Generate a new image that meets the user's requirements while maintaining consistency with the original "
62
+ "input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
63
+ ),
64
+ "start_idx": 64,
65
+ },
66
+ }
67
+
68
+
69
+ def print_load_warning(missing: list[str], unexpected: list[str]) -> None:
70
+ if len(missing) > 0 and len(unexpected) > 0:
71
+ logger.warning(f"Got {len(missing)} missing keys:\n\t" + "\n\t".join(missing))
72
+ logger.warning("\n" + "-" * 79 + "\n")
73
+ logger.warning(f"Got {len(unexpected)} unexpected keys:\n\t" + "\n\t".join(unexpected))
74
+ elif len(missing) > 0:
75
+ logger.warning(f"Got {len(missing)} missing keys:\n\t" + "\n\t".join(missing))
76
+ elif len(unexpected) > 0:
77
+ logger.warning(f"Got {len(unexpected)} unexpected keys:\n\t" + "\n\t".join(unexpected))
78
+
79
+
80
+ def correct_model_weight(state_dict):
81
+ result = {}
82
+ for key in state_dict.keys():
83
+ if "_orig_mod." in key:
84
+ result[key[10:]] = state_dict[key]
85
+ else:
86
+ result[key] = state_dict[key]
87
+ return result
88
+
89
+
90
+ def load_hf_style_weight(pretrain_path, device):
91
+ index_path = os.path.join(pretrain_path, "diffusion_pytorch_model.safetensors.index.json")
92
+
93
+ with open(index_path) as f:
94
+ index = json.load(f)
95
+
96
+ weight_map = index["weight_map"]
97
+
98
+ sd = {}
99
+ loaded_shards = set()
100
+
101
+ for shard_file in weight_map.values():
102
+ if shard_file in loaded_shards:
103
+ continue
104
+ shard_path = os.path.join(pretrain_path, shard_file)
105
+ shard_sd = load_file(shard_path, device="cpu")
106
+ sd.update(shard_sd)
107
+ loaded_shards.add(shard_file)
108
+
109
+ return sd
110
+
111
+
112
+ def load_model_weight(model, pretrain_path, device="cpu"):
113
+ if os.path.exists(pretrain_path):
114
+ logger.info(f"Loading checkpoint from {pretrain_path}")
115
+ try:
116
+ if pretrain_path.endswith("safetensors"):
117
+ sd = load_sft(pretrain_path, device="cpu")
118
+ elif os.path.exists(os.path.join(pretrain_path, "diffusion_pytorch_model.safetensors.index.json")):
119
+ sd = load_hf_style_weight(pretrain_path, device)
120
+ else:
121
+ sd = torch.load(pretrain_path, map_location="cpu")
122
+
123
+ sd = correct_model_weight(sd)
124
+ sd = optionally_expand_state_dict(model, sd)
125
+ missing, unexpected = model.load_state_dict(sd, strict=False, assign=True)
126
+ print_load_warning(missing, unexpected)
127
+ return True
128
+ except Exception as e:
129
+ logger.info(f"CANNOT Load {pretrain_path}, because {e}")
130
+ return False
131
+ return False
132
+
133
+
134
+ def load_model(dit_structure: dict, pretrain_path: str | None = None):
135
+ logger.info("Init DiT model")
136
+
137
+ # If name is a dict, we assume it contains the parameters directly
138
+ # We need to determine the model class based on some heuristic or just default to MageFlow/Flux
139
+ # For now, let's assume it's MageFlow if time_type is present, or check other fields
140
+ params = MageFlowParams(**dit_structure)
141
+ # Default to MageFlow for now as per user context, or we could add a 'model_type' field to the dict
142
+ # The user mentioned "model structure option", implying we are configuring the structure.
143
+ # Let's assume MageFlow for this refactor as the user was using qwen-image-tiny-wo-textemb
144
+ model = MageFlow(params)
145
+
146
+ # logger.info(f"Loading {name if isinstance(name, str) else 'custom config'} checkpoint from {pretrain_path}")
147
+ if pretrain_path is not None:
148
+ load_model_weight(model, pretrain_path, device="cpu")
149
+ # if isinstance(name, str) and configs[name].lora_path is not None:
150
+ # logger.info("Loading LoRA")
151
+ # lora_sd = load_sft(configs[name].lora_path, device="cpu")
152
+ # # loading the lora params + overwriting scale values in the norms
153
+ # missing, unexpected = model.load_state_dict(lora_sd, strict=False, assign=True)
154
+ # print_load_warning(missing, unexpected)
155
+ return model
156
+
157
+
158
+ def optionally_expand_state_dict(model: torch.nn.Module, state_dict: dict) -> dict:
159
+ """
160
+ Optionally expand the state dict to match the model's parameters shapes.
161
+ """
162
+ for name, param in model.named_parameters():
163
+ if name in state_dict:
164
+ if state_dict[name].shape != param.shape:
165
+ logger.info(
166
+ f"Expanding '{name}' with shape {state_dict[name].shape} to model parameter with shape "
167
+ f"{param.shape}."
168
+ )
169
+ # expand with zeros:
170
+ expanded_state_dict_weight = torch.zeros_like(param, device=state_dict[name].device)
171
+ slices = tuple(slice(0, dim) for dim in state_dict[name].shape)
172
+ expanded_state_dict_weight[slices] = state_dict[name]
173
+ state_dict[name] = expanded_state_dict_weight
174
+
175
+ return state_dict
mage_flow/pipeline.py ADDED
@@ -0,0 +1,762 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MageFlow text-to-image + image-edit inference pipeline.
2
+
3
+ Self-contained MageFlow t2i / edit inference: load a HuggingFace diffusers-style
4
+ repo (model_index.json + transformer/ vae/ scheduler/), then generate or edit
5
+ images. No training/eval deps.
6
+
7
+ Both ``generate_images`` and ``generate_edits`` support PACKED multi-resolution
8
+ inference: several samples (each at its own resolution) are concatenated into a
9
+ single varlen sequence and processed in one transformer forward per denoise
10
+ step. Per-sample ``cu_seqlens`` (inside the flash-attn varlen kernel) isolate
11
+ samples, exactly mirroring training-time packing. These packed functions are the
12
+ sole implementation — the single-image case is just a pack of size 1, exposed
13
+ via the ``MageFlowPipeline.generate`` / ``.edit`` convenience methods.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import random
21
+
22
+ import torch
23
+ from einops import rearrange
24
+ from PIL import Image
25
+
26
+ from diffusers import FlowMatchEulerDiscreteScheduler
27
+
28
+ from .models.mage_flow import MageFlowModel, ModelConfig
29
+ from .models.utils import PROMPT_TEMPLATE, get_noise, unpack
30
+ from .models.modules.mage_text import make_refusal_image
31
+ from .models.modules.mage_latent import encode_noise, resolve_gs_key
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Scheduler — diffusers FlowMatchEulerDiscreteScheduler
36
+ # ---------------------------------------------------------------------------
37
+ def build_scheduler(num_steps: int, device=None, shift: float = 6.0):
38
+ """Construct a diffusers ``FlowMatchEulerDiscreteScheduler`` whose sigma
39
+ schedule reproduces our default preset exactly.
40
+
41
+ The base sigmas ``linspace(1, 1/num_steps, num_steps)`` fed to
42
+ ``set_timesteps`` are run through the scheduler's built-in static shift
43
+ ``shift·s/(1+(shift-1)·s)`` and a terminal 0 is appended — the static-shift
44
+ schedule (the only supported schedule).
45
+ """
46
+ scheduler = FlowMatchEulerDiscreteScheduler(
47
+ num_train_timesteps=1000, shift=shift, use_dynamic_shifting=False)
48
+ base_sigmas = torch.linspace(1.0, 1.0 / num_steps, num_steps).tolist()
49
+ scheduler.set_timesteps(sigmas=base_sigmas, device=device)
50
+ return scheduler
51
+
52
+
53
+ def _get_scheduler(model, steps, device, static_shift):
54
+ scheduler = getattr(model, "scheduler", None)
55
+ if scheduler is None:
56
+ return build_scheduler(steps, device=device,
57
+ shift=(static_shift if static_shift is not None else 6.0))
58
+ if static_shift is not None:
59
+ scheduler.set_shift(static_shift)
60
+ scheduler.set_timesteps(sigmas=torch.linspace(1.0, 1.0 / steps, steps).tolist(), device=device)
61
+ return scheduler
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Small helpers
66
+ # ---------------------------------------------------------------------------
67
+ def _template_info(name: str | None) -> dict:
68
+ name = name or "mage-flow"
69
+ if name not in PROMPT_TEMPLATE:
70
+ raise ValueError(f"Unknown prompt template: {name}")
71
+ return PROMPT_TEMPLATE[name]
72
+
73
+
74
+ def _as_list(val, default, n):
75
+ """Broadcast a scalar/None to a length-n list, or validate a given list."""
76
+ if val is None:
77
+ return [default] * n
78
+ if isinstance(val, (list, tuple)):
79
+ if len(val) != n:
80
+ raise ValueError(f"expected {n} values, got {len(val)}")
81
+ return list(val)
82
+ return [val] * n
83
+
84
+
85
+ def _lens_to_cu(lens, device):
86
+ """Sequence lengths -> cumulative cu_seqlens [0, l0, l0+l1, ...] (int32)."""
87
+ t = torch.tensor(lens, device=device, dtype=torch.int32)
88
+ return torch.cat([torch.zeros(1, dtype=torch.int32, device=device),
89
+ torch.cumsum(t, dim=0, dtype=torch.int32)])
90
+
91
+
92
+ def _make_divisible_by_16(size: int) -> int:
93
+ return max(16, 16 * (size // 16))
94
+
95
+
96
+ def _compute_aspect_ratio_size(pil_img: Image.Image, max_size: int):
97
+ """Longest side = ``max_size``, short side from aspect ratio, both /16."""
98
+ w, h = pil_img.size
99
+ if h >= w:
100
+ new_h, new_w = max_size, int(round(w * max_size / h))
101
+ else:
102
+ new_w, new_h = max_size, int(round(h * max_size / w))
103
+ return _make_divisible_by_16(new_h), _make_divisible_by_16(new_w)
104
+
105
+
106
+ def _edit_target_size(pil_img: Image.Image, max_size, height, width):
107
+ """Output (H, W) for an edit sample, derived from its PRIMARY reference.
108
+
109
+ Precedence: explicit ``height`` AND ``width`` (custom size) > ``max_size``
110
+ (longest side, short side by aspect ratio) > the source image's own size.
111
+ All rounded down to a multiple of 16.
112
+ """
113
+ if height and width:
114
+ return _make_divisible_by_16(height), _make_divisible_by_16(width)
115
+ if max_size:
116
+ return _compute_aspect_ratio_size(pil_img, max_size)
117
+ # Nothing specified: keep the source resolution (its own longest side).
118
+ return _compute_aspect_ratio_size(pil_img, max(pil_img.size))
119
+
120
+
121
+ def _decode_one(model, tokens, height, width, dev):
122
+ """Unpack one sample's image tokens [1, H*W, C] and VAE-decode to a PIL image."""
123
+ with torch.autocast(device_type=dev.type, dtype=torch.bfloat16):
124
+ out = model.vae.decode(unpack(tokens.float(), height, width))
125
+ out = rearrange(out.clamp(-1, 1), "b c h w -> b h w c")
126
+ out = (127.5 * (out + 1.0)).cpu().byte().numpy()
127
+ return Image.fromarray(out[0])
128
+
129
+
130
+ def _build_pack_ctx(img_ids, img_cu, img_shapes, img_lens, txt, txt_cu, txt_mask, vec,
131
+ neg_txt, neg_cu, neg_mask, neg_vec, cfg, renormalization, batch_cfg, device):
132
+ """Precompute the static per-step transformer inputs for a packed batch.
133
+
134
+ When a negative branch is present and ``batch_cfg`` is True, the conditional
135
+ and unconditional passes are fused into ONE varlen forward: the image tokens
136
+ are duplicated (cond copy + uncond copy) and the positive/negative texts are
137
+ concatenated, so cond sample i and uncond sample i become two independent
138
+ varlen segments processed in a single kernel launch. flash_attn_varlen_func
139
+ keeps every segment isolated via cu_seqlens, so this is numerically identical
140
+ to two separate forwards — just one launch instead of two.
141
+ """
142
+ na = len(img_lens)
143
+ ctx = {
144
+ "na": na, "cfg": cfg, "renorm": renormalization, "batch_cfg": batch_cfg,
145
+ "has_neg": neg_txt is not None,
146
+ "img_ids": img_ids, "img_cu": img_cu, "img_shapes": img_shapes,
147
+ "img_max": int(max(img_lens)),
148
+ "txt": txt, "txt_ids": torch.zeros(1, txt.shape[1], 3, device=device),
149
+ "txt_cu": txt_cu, "txt_mask": txt_mask, "vec": vec,
150
+ "txt_max": int((txt_cu[1:] - txt_cu[:-1]).max().item()),
151
+ }
152
+ if neg_txt is None:
153
+ return ctx
154
+ ctx.update({
155
+ "neg_txt": neg_txt, "neg_ids": torch.zeros(1, neg_txt.shape[1], 3, device=device),
156
+ "neg_cu": neg_cu, "neg_mask": neg_mask, "neg_vec": neg_vec,
157
+ "neg_max": int((neg_cu[1:] - neg_cu[:-1]).max().item()),
158
+ })
159
+ if batch_cfg:
160
+ # Duplicate image segments (cond then uncond) and concat pos+neg text.
161
+ d_txt = torch.cat([txt, neg_txt], dim=1)
162
+ pos_lens = (txt_cu[1:] - txt_cu[:-1]).tolist()
163
+ neg_lens = (neg_cu[1:] - neg_cu[:-1]).tolist()
164
+ ctx.update({
165
+ "d_img_ids": torch.cat([img_ids, img_ids], dim=1),
166
+ "d_img_cu": _lens_to_cu(list(img_lens) + list(img_lens), device),
167
+ "d_img_shapes": [img_shapes[0] + img_shapes[0]],
168
+ "d_txt": d_txt,
169
+ "d_txt_ids": torch.zeros(1, d_txt.shape[1], 3, device=device),
170
+ "d_txt_cu": _lens_to_cu(pos_lens + neg_lens, device),
171
+ "d_txt_mask": torch.ones(1, d_txt.shape[1], device=device),
172
+ "d_vec": torch.cat([vec, neg_vec], dim=0),
173
+ "d_txt_max": int(max(pos_lens + neg_lens)),
174
+ })
175
+ return ctx
176
+
177
+
178
+ def _velocity(transformer, img, ctx, sigma):
179
+ """CFG-combined image-token velocity for a packed batch at noise level ``sigma``.
180
+
181
+ Returns [1, sum_img_len, C] in the conditional sample order. When
182
+ ``batch_cfg`` is set the cond+uncond passes share a single fused varlen
183
+ forward; otherwise they are two forwards.
184
+ """
185
+ dev = img.device
186
+ na = ctx["na"]
187
+
188
+ def _fwd(x, n, img_ids, img_cu, img_max, img_shapes, txt, txt_ids, txt_cu, txt_mask, txt_max, vec):
189
+ t_vec = torch.full((n,), sigma, dtype=x.dtype, device=dev)
190
+ return transformer(img=x, txt=txt, timesteps=t_vec, img_shapes=img_shapes,
191
+ img_cu_seqlens=img_cu, txt_cu_seqlens=txt_cu)
192
+
193
+ if not ctx["has_neg"]:
194
+ return _fwd(img, na, ctx["img_ids"], ctx["img_cu"], ctx["img_max"], ctx["img_shapes"],
195
+ ctx["txt"], ctx["txt_ids"], ctx["txt_cu"], ctx["txt_mask"], ctx["txt_max"], ctx["vec"])
196
+
197
+ if ctx["batch_cfg"]:
198
+ n_img = img.shape[1]
199
+ out = _fwd(torch.cat([img, img], dim=1), 2 * na,
200
+ ctx["d_img_ids"], ctx["d_img_cu"], ctx["img_max"], ctx["d_img_shapes"],
201
+ ctx["d_txt"], ctx["d_txt_ids"], ctx["d_txt_cu"], ctx["d_txt_mask"], ctx["d_txt_max"], ctx["d_vec"])
202
+ cond, unc = out[:, :n_img, :], out[:, n_img:, :]
203
+ else:
204
+ cond = _fwd(img, na, ctx["img_ids"], ctx["img_cu"], ctx["img_max"], ctx["img_shapes"],
205
+ ctx["txt"], ctx["txt_ids"], ctx["txt_cu"], ctx["txt_mask"], ctx["txt_max"], ctx["vec"])
206
+ unc = _fwd(img, na, ctx["img_ids"], ctx["img_cu"], ctx["img_max"], ctx["img_shapes"],
207
+ ctx["neg_txt"], ctx["neg_ids"], ctx["neg_cu"], ctx["neg_mask"], ctx["neg_max"], ctx["neg_vec"])
208
+
209
+ cfg = ctx["cfg"]
210
+ if ctx["renorm"]:
211
+ # CFG renormalization: rescale the guided velocity per token back to the
212
+ # conditional velocity's norm (reduces oversaturation at high cfg).
213
+ comb = unc + cfg * (cond - unc)
214
+ return comb * (torch.norm(cond, dim=-1, keepdim=True) /
215
+ (torch.norm(comb, dim=-1, keepdim=True) + 1e-6))
216
+ return unc + cfg * (cond - unc)
217
+
218
+
219
+ def _encode_texts_packed(model, prompts, template, drop_idx, device):
220
+ """Encode a LIST of templated text-only prompts in ONE packed varlen forward
221
+ (``TextEncoder.forward`` — varlen cu_seqlens isolates each prompt,
222
+ verified zero cross-contamination). Returns (txt_flat [ΣLi, D], vec [N, D],
223
+ per-prompt token lengths list)."""
224
+ tokenizer = model.txt_enc.tokenizer
225
+ max_len = model.txt_enc.tokenizer_max_length + drop_idx
226
+ ids_list = [
227
+ tokenizer(template.format(p), max_length=max_len, truncation=True,
228
+ return_tensors="pt").input_ids.squeeze(0)
229
+ for p in prompts
230
+ ]
231
+ input_ids = torch.cat(ids_list).to(device)
232
+ cu_seqlens = _lens_to_cu([int(t.numel()) for t in ids_list], device)
233
+ res = model.txt_enc(
234
+ input_ids, cu_seqlens, drop_idx_override=drop_idx)
235
+ return res["txt"], res["vec"], res["txt_seq_lens"].tolist()
236
+
237
+
238
+ def _slice_packed(txt_flat, vec, lens, start, count, device):
239
+ """Format a contiguous ``count``-prompt slice (starting at prompt ``start``) of a
240
+ packed text encode into the (txt [1, ΣL, D], cu_seqlens, ones-mask, vec [count, D])
241
+ tuple that ``_build_pack_ctx`` consumes."""
242
+ seg_lens = lens[start:start + count]
243
+ tok_start = sum(lens[:start])
244
+ tok_end = tok_start + sum(seg_lens)
245
+ txt = txt_flat[tok_start:tok_end].reshape(1, -1, txt_flat.shape[-1]).to(device)
246
+ return (txt, _lens_to_cu(seg_lens, device),
247
+ torch.ones(1, txt.shape[1], device=device), vec[start:start + count].to(device))
248
+
249
+
250
+ # ---------------------------------------------------------------------------
251
+ # Text-to-image (packed, multi-resolution)
252
+ # ---------------------------------------------------------------------------
253
+ @torch.no_grad()
254
+ def generate_images(model, prompts, neg_prompts=None, seeds=None, steps=30, cfg=5.0,
255
+ heights=None, widths=None, device="cuda",
256
+ prompt_template="mage-flow", static_shift=None,
257
+ gs_key=None,
258
+ renormalization=False, batch_cfg=True):
259
+ """Generate one image per prompt. Prompts may request DIFFERENT resolutions;
260
+ all are packed into a single varlen forward per denoise step — samples are
261
+ kept isolated by ``flash_attn_varlen_func`` via per-sample ``cu_seqlens`` (no
262
+ cross-sample attention), mirroring training-time packing. When ``cfg > 1`` and
263
+ ``batch_cfg`` is set, the positive and negative passes are fused into that
264
+ same varlen forward. Returns a list of PIL images aligned with ``prompts``.
265
+ """
266
+ if isinstance(prompts, str):
267
+ prompts = [prompts]
268
+ n = len(prompts)
269
+ neg_prompts = _as_list(neg_prompts, " ", n)
270
+ seeds = _as_list(seeds, 42, n)
271
+ heights = _as_list(heights, 1024, n)
272
+ widths = _as_list(widths, 1024, n)
273
+ info = _template_info(prompt_template)
274
+ template = info.get("template", "{}")
275
+ drop_idx = int(info.get("start_idx", 0))
276
+ dev = torch.device(device)
277
+
278
+ # Content-policy gate per sample (MANDATORY — runs on the same text-encoder
279
+ # weights as conditioning, no opt-out). Violating prompts get a refusal
280
+ # placeholder and are dropped from the pack.
281
+ results = [None] * n
282
+ active = []
283
+ for i in range(n):
284
+ if seeds[i] == -1:
285
+ seeds[i] = random.randint(0, 2**32 - 1)
286
+ verdict = model.txt_enc.screen_text(prompts[i])
287
+ if verdict.violates:
288
+ h_, w_ = _make_divisible_by_16(heights[i]), _make_divisible_by_16(widths[i])
289
+ print(verdict.banner())
290
+ results[i] = make_refusal_image(verdict, height=h_, width=w_)
291
+ continue
292
+ active.append(i)
293
+ if not active:
294
+ return results
295
+
296
+ gs_key_int = resolve_gs_key(gs_key)
297
+ # Per-sample noise tokens + position ids + shapes (MageVAE: flatten, no packing).
298
+ ch = model.vae.latent_channels
299
+ img_list, ids_list, lens, shapes, hw = [], [], [], [], []
300
+ for i in active:
301
+ h_, w_ = _make_divisible_by_16(heights[i]), _make_divisible_by_16(widths[i])
302
+ torch.manual_seed(seeds[i])
303
+ x = get_noise(num_samples=1, channel=ch, height=h_, width=w_,
304
+ device=dev, dtype=torch.bfloat16, seed=seeds[i])
305
+ # Distribution-preserving watermark in the initial noise (same shape,
306
+ # still ~N(0,1)); detect by inverting the flow ODE back to noise.
307
+ x = encode_noise(tuple(x.shape[1:]), key=gs_key_int,
308
+ seed=seeds[i], device=dev, dtype=torch.bfloat16)
309
+ _, _, gh, gw = x.shape
310
+ img_list.append(rearrange(x, "b c h w -> b (h w) c")[0])
311
+ ids = torch.zeros(gh, gw, 3, device=dev)
312
+ ids[..., 1] = ids[..., 1] + torch.arange(gh, device=dev)[:, None]
313
+ ids[..., 2] = ids[..., 2] + torch.arange(gw, device=dev)[None, :]
314
+ ids_list.append(rearrange(ids, "h w c -> (h w) c"))
315
+ lens.append(gh * gw); shapes.append((1, gh, gw)); hw.append((h_, w_))
316
+ img = torch.cat(img_list, 0).unsqueeze(0)
317
+ img_ids = torch.cat(ids_list, 0).unsqueeze(0)
318
+ img_cu = _lens_to_cu(lens, dev)
319
+ img_shapes = [shapes]
320
+
321
+ # Packed text: positive prompts AND (for CFG) negative prompts are encoded
322
+ # TOGETHER in ONE varlen forward, then split back — cu_seqlens keeps every
323
+ # prompt isolated (verified zero cross-contamination).
324
+ pos_prompts = [prompts[i] for i in active]
325
+ na = len(active)
326
+ use_neg = cfg > 1.0 and any(neg_prompts[i] for i in active)
327
+ if use_neg:
328
+ neg_list = [neg_prompts[i] or " " for i in active]
329
+ txt_flat, vec_all, lens_t = _encode_texts_packed(
330
+ model, pos_prompts + neg_list, template, drop_idx, dev)
331
+ txt, txt_cu, txt_mask, vec = _slice_packed(txt_flat, vec_all, lens_t, 0, na, dev)
332
+ neg_txt, neg_cu, neg_mask, neg_vec = _slice_packed(txt_flat, vec_all, lens_t, na, na, dev)
333
+ else:
334
+ txt_flat, vec_all, lens_t = _encode_texts_packed(model, pos_prompts, template, drop_idx, dev)
335
+ txt, txt_cu, txt_mask, vec = _slice_packed(txt_flat, vec_all, lens_t, 0, na, dev)
336
+ neg_txt = neg_cu = neg_mask = neg_vec = None
337
+
338
+ ctx = _build_pack_ctx(img_ids, img_cu, img_shapes, lens, txt, txt_cu, txt_mask, vec,
339
+ neg_txt, neg_cu, neg_mask, neg_vec, cfg, renormalization, batch_cfg, dev)
340
+ scheduler = _get_scheduler(model, steps, device, static_shift)
341
+ for si, t in enumerate(scheduler.timesteps):
342
+ pred = _velocity(model.transformer, img, ctx, scheduler.sigmas[si].item())
343
+ img = scheduler.step(pred, t, img, return_dict=False)[0]
344
+
345
+ off = 0
346
+ for k, i in enumerate(active):
347
+ L = lens[k]
348
+ h_, w_ = hw[k]
349
+ results[i] = _decode_one(model, img[:, off:off + L, :], h_, w_, dev)
350
+ off += L
351
+ return results
352
+
353
+
354
+ # ---------------------------------------------------------------------------
355
+ # Image edit (packed, multi-resolution)
356
+ # ---------------------------------------------------------------------------
357
+ def _preprocess_ref_image(pil_img: Image.Image, height: int, width: int, device) -> torch.Tensor:
358
+ """Resize an RGB reference image to (height, width) and normalize to [-1, 1]."""
359
+ from torchvision.transforms import functional as TF
360
+ img = pil_img.convert("RGB")
361
+ img = TF.resize(img, [height, width], interpolation=TF.InterpolationMode.BICUBIC)
362
+ t = TF.to_tensor(img) # [3, H, W] in [0, 1]
363
+ t = TF.normalize(t, [0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) # -> [-1, 1]
364
+ return t.to(device)
365
+
366
+
367
+ def _resize_long_edge(image: Image.Image, max_long_edge: int | None) -> Image.Image:
368
+ """Cap the VL conditioning image's long edge, preserving aspect ratio.
369
+
370
+ Matches training's data.processor._resize_long_edge (BICUBIC). Without this,
371
+ inference feeds a full-resolution image to the Qwen-VL processor whose
372
+ default max_pixels is far larger than 384**2 — a train/test mismatch.
373
+ """
374
+ if max_long_edge is None or max_long_edge <= 0:
375
+ return image
376
+ w, h = image.size
377
+ long_edge = max(w, h)
378
+ if long_edge <= max_long_edge:
379
+ return image
380
+ scale = max_long_edge / long_edge
381
+ new_w = max(1, int(round(w * scale)))
382
+ new_h = max(1, int(round(h * scale)))
383
+ return image.resize((new_w, new_h), Image.BICUBIC)
384
+
385
+
386
+ # Fixed image placeholder used at edit training time (one per reference image).
387
+ _EDIT_IMAGE_PLACEHOLDER = "<|vision_start|><|image_pad|><|vision_end|>"
388
+
389
+
390
+ def _edit_prompt_body(instruction, num_refs):
391
+ """Training-time multi-reference prompt body: ``Image 1: <ph>Image 2: <ph>…{instruction}``."""
392
+ prefix = "".join(f"Image {j}: {_EDIT_IMAGE_PLACEHOLDER}" for j in range(1, num_refs + 1))
393
+ return prefix + instruction
394
+
395
+
396
+ def _encode_edits_packed(model, ref_pils_per_sample, instructions, template, drop_idx, device):
397
+ """Encode ALL image-conditioned edit instructions in ONE packed multimodal
398
+ varlen forward (pixel_values/image_grid_thw concatenated across samples,
399
+ cu_seqlens isolates each). Returns (txt_flat [ΣLi, D], vec [N, D], per-sample lens)."""
400
+ processor = model.txt_enc.processor
401
+ ids_list, pv_list, thw_list = [], [], []
402
+ for ref_pils, instr in zip(ref_pils_per_sample, instructions, strict=False):
403
+ formatted = template.format(_edit_prompt_body(instr, len(ref_pils)))
404
+ vl = processor(text=[formatted], images=list(ref_pils), padding=True, return_tensors="pt")
405
+ vl = {k: (v.to(device) if hasattr(v, "to") else v) for k, v in vl.items()}
406
+ ids_list.append(vl["input_ids"].squeeze(0))
407
+ if vl.get("pixel_values") is not None:
408
+ pv_list.append(vl["pixel_values"]); thw_list.append(vl["image_grid_thw"])
409
+ input_ids = torch.cat(ids_list).to(device)
410
+ cu = _lens_to_cu([int(t.numel()) for t in ids_list], device)
411
+ inputs = {"input_ids": input_ids, "cu_seqlens": cu}
412
+ if pv_list:
413
+ inputs["pixel_values"] = torch.cat(pv_list, dim=0)
414
+ inputs["image_grid_thw"] = torch.cat(thw_list, dim=0)
415
+ res = model.txt_enc(
416
+ input_ids, cu, inputs=inputs, drop_idx_override=drop_idx)
417
+ return res["txt"], res["vec"], res["txt_seq_lens"].tolist()
418
+
419
+
420
+ @torch.no_grad()
421
+ def generate_edits(model, prompts, ref_images, neg_prompts=None, seeds=None, steps=30, cfg=5.0,
422
+ max_size=None, heights=None, widths=None, device="cuda",
423
+ prompt_template="mage-flow-edit", static_shift=None,
424
+ gs_key=None,
425
+ vl_cond_long_edge=384,
426
+ renormalization=False, batch_cfg=True):
427
+ """Edit reference image(s) per prompt. Each ``ref_images[i]`` may be a single
428
+ image/path OR a list of source images (multi-image edit, like training —
429
+ trained with up to 3, but more are accepted) — all produce ONE edited output. Each sample's
430
+ ``[target, ref_1, …, ref_N]`` latent tokens are sequence-concatenated, and
431
+ all samples are packed into one varlen forward per denoise step.
432
+
433
+ Output resolution (derived from the first/primary reference of each sample):
434
+ if both ``heights[i]`` and ``widths[i]`` are given, use them; else if
435
+ ``max_size`` is given, the longest side is ``max_size`` and the short side
436
+ follows the reference's aspect ratio; otherwise the output keeps the source
437
+ image's own resolution. All references are VAE-encoded at that target size.
438
+ Returns a list of PIL images.
439
+ """
440
+ if isinstance(prompts, str):
441
+ prompts = [prompts]
442
+ ref_images = [ref_images]
443
+ n = len(prompts)
444
+ neg_prompts = _as_list(neg_prompts, " ", n)
445
+ seeds = _as_list(seeds, 42, n)
446
+ heights = _as_list(heights, None, n)
447
+ widths = _as_list(widths, None, n)
448
+ info = _template_info(prompt_template)
449
+ template = info.get("template", "{}")
450
+ drop_idx = int(info.get("start_idx", 0))
451
+ dev = torch.device(device)
452
+
453
+ # Normalize each sample's references to a list of 1..3 PIL images.
454
+ def _load_pil(r):
455
+ if isinstance(r, str):
456
+ r = Image.open(r)
457
+ return r.convert("RGB")
458
+
459
+ pils_per_sample = []
460
+ for r in ref_images:
461
+ refs = list(r) if isinstance(r, (list, tuple)) else [r]
462
+ if not refs:
463
+ raise ValueError("each edit sample needs at least one reference image")
464
+ pils_per_sample.append([_load_pil(x) for x in refs])
465
+
466
+ # Per-sample output resolution (from the first/primary reference) + content gate.
467
+ results = [None] * n
468
+ res_hw = [None] * n
469
+ active = []
470
+ for i in range(n):
471
+ res_hw[i] = _edit_target_size(pils_per_sample[i][0], max_size, heights[i], widths[i])
472
+ if seeds[i] == -1:
473
+ seeds[i] = random.randint(0, 2**32 - 1)
474
+ # Multimodal gate (MANDATORY): inspect the source image(s) AND the
475
+ # instruction, so NSFW / copyrighted-character / real-public-figure
476
+ # source photos are blocked even under an innocuous instruction.
477
+ verdict = model.txt_enc.screen_edit(prompts[i], pils_per_sample[i])
478
+ if verdict.violates:
479
+ h_, w_ = res_hw[i]
480
+ print(verdict.banner())
481
+ results[i] = make_refusal_image(verdict, height=h_, width=w_)
482
+ continue
483
+ active.append(i)
484
+ if not active:
485
+ return results
486
+
487
+ gs_key_int = resolve_gs_key(gs_key)
488
+
489
+ # Per sample: reference latent tokens (clean) + target noise tokens, plus the
490
+ # combined [target, ref_1, …, ref_N] position ids and shapes. ``target_idx``
491
+ # records where each sample's target tokens land in the packed sequence so we
492
+ # can slice the velocity and step only the target portion.
493
+ ch = model.vae.latent_channels
494
+ targets, refs, ids_list, shape_seq, samp_lens, tgt_lens, hw = [], [], [], [], [], [], []
495
+ target_idx_parts = []
496
+ off = 0
497
+ for i in active:
498
+ h_, w_ = res_hw[i]
499
+ torch.manual_seed(seeds[i]) # MageVAE.encode samples the posterior (global RNG)
500
+ # All references resized to the target resolution and VAE-encoded together.
501
+ ref_tensors = [_preprocess_ref_image(p, h_, w_, dev) for p in pils_per_sample[i]]
502
+ ref_tok, ref_shapes, ref_ids = model.compute_vae_encodings(ref_tensors, with_ids=True)
503
+ ref_tok = ref_tok.to(torch.bfloat16) # [1, N*Lr, C]
504
+ x = get_noise(num_samples=1, channel=ch, height=h_, width=w_,
505
+ device=dev, dtype=torch.bfloat16, seed=seeds[i])
506
+ x = encode_noise(tuple(x.shape[1:]), key=gs_key_int,
507
+ seed=seeds[i], device=dev, dtype=torch.bfloat16)
508
+ _, _, gh, gw = x.shape
509
+ tgt = rearrange(x, "b c h w -> b (h w) c") # [1, Lt, C]
510
+ tgt_ids = torch.zeros(gh, gw, 3, device=dev)
511
+ tgt_ids[..., 1] = tgt_ids[..., 1] + torch.arange(gh, device=dev)[:, None]
512
+ tgt_ids[..., 2] = tgt_ids[..., 2] + torch.arange(gw, device=dev)[None, :]
513
+ tgt_ids = rearrange(tgt_ids, "h w c -> (h w) c").unsqueeze(0)
514
+ lt, lr = tgt.shape[1], ref_tok.shape[1]
515
+ targets.append(tgt); refs.append(ref_tok)
516
+ ids_list.append(torch.cat([tgt_ids, ref_ids.to(dev)], dim=1)[0]) # [Lt + N*Lr, 3]
517
+ shape_seq.append((1, gh, gw)) # target frame idx 0
518
+ shape_seq.extend(s[0] for s in ref_shapes) # ref_j frame idx j
519
+ samp_lens.append(lt + lr); tgt_lens.append(lt); hw.append((h_, w_))
520
+ target_idx_parts.append(torch.arange(off, off + lt, device=dev))
521
+ off += lt + lr
522
+ img_ids = torch.cat(ids_list, 0).unsqueeze(0)
523
+ img_cu = _lens_to_cu(samp_lens, dev)
524
+ img_shapes = [shape_seq]
525
+ target_idx = torch.cat(target_idx_parts)
526
+
527
+ # Packed edit text — positive AND (for CFG) negative are encoded TOGETHER in
528
+ # ONE packed multimodal forward, then split. Both branches share the same
529
+ # reference images; cu_seqlens isolates every sequence (zero cross-contamination).
530
+ # The VL conditioning image's long edge is capped (default 384) to match
531
+ # training preprocessing — the VAE path above keeps the full target resolution.
532
+ na = len(active)
533
+ edit_refs = [[_resize_long_edge(p, vl_cond_long_edge) for p in pils_per_sample[i]]
534
+ for i in active]
535
+ if cfg > 1.0:
536
+ pos_instr = [prompts[i] for i in active]
537
+ neg_instr = [neg_prompts[i] or " " for i in active]
538
+ txt_flat, vec_all, lens_t = _encode_edits_packed(
539
+ model, edit_refs + edit_refs, pos_instr + neg_instr, template, drop_idx, dev)
540
+ txt, txt_cu, txt_mask, vec = _slice_packed(txt_flat, vec_all, lens_t, 0, na, dev)
541
+ neg_txt, neg_cu, neg_mask, neg_vec = _slice_packed(txt_flat, vec_all, lens_t, na, na, dev)
542
+ else:
543
+ txt_flat, vec_all, lens_t = _encode_edits_packed(
544
+ model, edit_refs, [prompts[i] for i in active], template, drop_idx, dev)
545
+ txt, txt_cu, txt_mask, vec = _slice_packed(txt_flat, vec_all, lens_t, 0, na, dev)
546
+ neg_txt = neg_cu = neg_mask = neg_vec = None
547
+
548
+ ctx = _build_pack_ctx(img_ids, img_cu, img_shapes, samp_lens, txt, txt_cu, txt_mask, vec,
549
+ neg_txt, neg_cu, neg_mask, neg_vec, cfg, renormalization, batch_cfg, dev)
550
+ scheduler = _get_scheduler(model, steps, device, static_shift)
551
+ for si, t in enumerate(scheduler.timesteps):
552
+ parts = []
553
+ for k in range(na):
554
+ parts.append(targets[k]); parts.append(refs[k])
555
+ img = torch.cat(parts, dim=1) # [1, sum(Lt+Lr), C], ref clean
556
+ vel = _velocity(model.transformer, img, ctx, scheduler.sigmas[si].item())
557
+ pred_t = vel[:, target_idx, :] # [1, sum Lt, C] — target tokens only
558
+ tgt_packed = torch.cat(targets, dim=1) # [1, sum Lt, C]
559
+ stepped = scheduler.step(pred_t, t, tgt_packed, return_dict=False)[0]
560
+ o = 0
561
+ new_targets = []
562
+ for k in range(na):
563
+ lt = tgt_lens[k]
564
+ new_targets.append(stepped[:, o:o + lt, :]); o += lt
565
+ targets = new_targets
566
+
567
+ for k, i in enumerate(active):
568
+ h_, w_ = hw[k]
569
+ results[i] = _decode_one(model, targets[k], h_, w_, dev)
570
+ return results
571
+
572
+
573
+ # ---------------------------------------------------------------------------
574
+ # Flow-ODE inversion (Gaussian-Shading watermark detection)
575
+ # ---------------------------------------------------------------------------
576
+ @torch.no_grad()
577
+ def invert_to_noise(model, z0, height, width, steps=30, device="cuda",
578
+ prompt_template="mage-flow", static_shift=None, prompt=""):
579
+ """Reverse the flow ODE from a clean latent ``z0`` back to the initial noise.
580
+
581
+ This is the detection primitive for the Gaussian-Shading watermark: VAE-encode
582
+ the image to ``z0`` (posterior MEAN — deterministic), run this to recover the
583
+ initial noise, then read the signs via ``mage_latent.decode_bits``.
584
+
585
+ Inversion uses an empty prompt at cfg=1 (the standard Tree-Ring /
586
+ Gaussian-Shading setup). Reverse Euler recovers ``x_i`` from ``x_{i+1}`` with
587
+ the velocity evaluated at the point in hand; the sign-only watermark tolerates
588
+ the resulting approximation error (see the module's redundancy).
589
+
590
+ Args:
591
+ z0: clean latent ``[1, C, gh, gw]`` (e.g. the mean of ``model.vae.encode``).
592
+ Returns:
593
+ recovered initial-noise latent ``[1, C, gh, gw]`` (float32).
594
+ """
595
+ dev = torch.device(device)
596
+ info = _template_info(prompt_template)
597
+ template = info.get("template", "{}")
598
+ drop_idx = int(info.get("start_idx", 0))
599
+
600
+ z0 = z0.to(dev)
601
+ _, ch, gh, gw = z0.shape
602
+ img = rearrange(z0, "b c h w -> b (h w) c").to(torch.bfloat16) # [1, gh*gw, C]
603
+
604
+ ids = torch.zeros(gh, gw, 3, device=dev)
605
+ ids[..., 1] = ids[..., 1] + torch.arange(gh, device=dev)[:, None]
606
+ ids[..., 2] = ids[..., 2] + torch.arange(gw, device=dev)[None, :]
607
+ img_ids = rearrange(ids, "h w c -> (h w) c").unsqueeze(0)
608
+ lens = [gh * gw]
609
+ img_cu = _lens_to_cu(lens, dev)
610
+ img_shapes = [[(1, gh, gw)]]
611
+
612
+ # Empty-prompt conditioning, no negative branch, cfg=1 (single forward).
613
+ txt_flat, vec_all, lens_t = _encode_texts_packed(model, [prompt], template, drop_idx, dev)
614
+ txt, txt_cu, txt_mask, vec = _slice_packed(txt_flat, vec_all, lens_t, 0, 1, dev)
615
+ ctx = _build_pack_ctx(img_ids, img_cu, img_shapes, lens, txt, txt_cu, txt_mask, vec,
616
+ None, None, None, None, 1.0, False, False, dev)
617
+
618
+ scheduler = _get_scheduler(model, steps, device, static_shift)
619
+ sigmas = scheduler.sigmas
620
+ n = len(scheduler.timesteps)
621
+ # Forward step si: x_{si+1} = x_si + (s_{si+1}-s_si)·v(x_si, s_si).
622
+ # Reverse it from clean (x_n, sigma 0) up to noise (x_0), using x_{si+1} as the
623
+ # proxy for x_si at the forward eval sigma s_si.
624
+ for si in range(n - 1, -1, -1):
625
+ s_cur = sigmas[si].item()
626
+ s_next = sigmas[si + 1].item()
627
+ vel = _velocity(model.transformer, img, ctx, s_cur)
628
+ img = img - (s_next - s_cur) * vel
629
+ return unpack(img.float(), height, width) # [1, C, gh, gw]
630
+
631
+
632
+ # ---------------------------------------------------------------------------
633
+ # High-level pipeline wrapper
634
+ # ---------------------------------------------------------------------------
635
+ class MageFlowPipeline:
636
+ """``MageFlowPipeline.from_pretrained(repo).generate(...) / .edit(...)``.
637
+
638
+ ``generate`` / ``edit`` are packed multi-resolution calls: they take a list
639
+ of prompts (a single string is accepted and treated as a pack of size 1) and
640
+ return a list of PIL images. Per-sample ``heights``/``widths``/``seeds`` are
641
+ lists. Every prompt is screened by the text encoder's mandatory content
642
+ gate (no opt-out); banned prompts come back as refusal placeholders
643
+ interleaved with the real images. Real outputs always carry a Gaussian-Shading
644
+ watermark in the initial noise (no toggle), using the configured secret key.
645
+ """
646
+
647
+ def __init__(self, model, device="cuda"):
648
+ self.model = model
649
+ self.device = device
650
+
651
+ @classmethod
652
+ def from_pretrained(cls, repo_dir: str, device: str = "cuda"):
653
+ """Load a Mage-Flow diffusers-style repo (``model_index.json`` +
654
+ ``transformer/`` ``vae/`` ``scheduler/`` ``text_encoder/``).
655
+
656
+ ``repo_dir`` may be a local directory OR a Hugging Face Hub repo id
657
+ (e.g. ``"microsoft/Mage-Flow-4B"``), which is downloaded and cached
658
+ automatically on first use.
659
+ """
660
+ return cls(load_from_repo(repo_dir, device), device)
661
+
662
+ def generate(self, prompts, **kw) -> list[Image.Image]:
663
+ """Packed multi-resolution t2i. ``prompts`` is a list (or a single
664
+ string); pass per-sample ``heights``/``widths``/``seeds`` as lists."""
665
+ kw.setdefault("device", self.device)
666
+ return generate_images(self.model, prompts, **kw)
667
+
668
+ def edit(self, prompts, ref_images, **kw) -> list[Image.Image]:
669
+ """Packed multi-resolution edit. ``prompts`` is a list (or a single
670
+ string); each ``ref_images[i]`` is one reference or a list of references."""
671
+ kw.setdefault("device", self.device)
672
+ return generate_edits(self.model, prompts, ref_images, **kw)
673
+
674
+ def invert_to_noise(self, z0, height, width, **kw):
675
+ """Recover the initial noise from a clean latent (Gaussian-Shading detect)."""
676
+ kw.setdefault("device", self.device)
677
+ return invert_to_noise(self.model, z0, height, width, **kw)
678
+
679
+
680
+ def _safe_subpath(root: str, *parts: str) -> str:
681
+ """Join ``parts`` under ``root`` and confirm the result stays inside ``root``.
682
+
683
+ ``root`` is normalized up front; the joined path is normalized **lexically**
684
+ (``os.path.normpath`` — symlinks are *not* followed, so a Hugging Face cache
685
+ whose weight files are symlinks into the shared blob store still loads) and
686
+ rejected if it escapes ``root``. This guards the user-supplied model path
687
+ against path traversal (CWE-22 / CodeQL ``py/path-injection``).
688
+ """
689
+ root = os.path.realpath(root)
690
+ full = os.path.normpath(os.path.join(root, *parts))
691
+ if full != root and not full.startswith(root + os.sep):
692
+ raise ValueError(
693
+ f"Resolved path {os.path.join(*parts)!r} escapes repo directory {root!r}"
694
+ )
695
+ return full
696
+
697
+
698
+ def _resolve_repo_dir(repo_dir: str) -> str:
699
+ """Return a local directory for ``repo_dir``.
700
+
701
+ If ``repo_dir`` is an existing local path it is returned as a normalized
702
+ absolute path; otherwise it is treated as a Hugging Face Hub repo id (e.g.
703
+ ``microsoft/Mage-Flow``) and downloaded/cached via
704
+ ``huggingface_hub.snapshot_download``.
705
+ """
706
+ candidate = os.path.realpath(repo_dir)
707
+ if os.path.isdir(candidate):
708
+ return candidate
709
+ from huggingface_hub import snapshot_download
710
+ return snapshot_download(repo_id=repo_dir)
711
+
712
+
713
+ def load_from_repo(repo_dir: str, device: str = "cuda") -> MageFlowModel:
714
+ """Load a Mage-Flow diffusers-style repo (model_index.json + transformer/
715
+ vae/ scheduler/). Transformer weights come from the bf16 safetensors;
716
+ VAE + text encoder are built from the sources recorded in model_index.json.
717
+
718
+ ``repo_dir`` may be a local directory OR a Hugging Face Hub repo id (e.g.
719
+ ``microsoft/Mage-Flow-4B``), which is downloaded/cached automatically.
720
+ """
721
+ from safetensors.torch import load_file
722
+ repo_dir = _resolve_repo_dir(repo_dir)
723
+ mi = json.load(open(_safe_subpath(repo_dir, "model_index.json")))
724
+ tcfg = json.load(open(_safe_subpath(repo_dir, "transformer", "config.json")))
725
+ # Keys stripped from the checkpoint config before it becomes model_structure.
726
+ # ``schedule_mode`` is a legacy field still present in some config.json files;
727
+ # Keys of the checkpoint config that are NOT MageFlowParams constructor args
728
+ # (legacy/unused fields). Everything else becomes model_structure. The DiT only
729
+ # reads: in_channels, out_channels, context_in_dim, hidden_size, num_heads,
730
+ # depth, axes_dim, checkpoint, patch_size.
731
+ _meta = {"_class_name", "txt_max_length", "max_sequence_length", "param_dtype",
732
+ "packing", "schedule_mode", "static_shift", "use_time_shift",
733
+ "rope_type", "apply_text_rotary_emb",
734
+ "mlp_ratio", "depth_single_blocks", "theta", "qkv_bias", "guidance_embed",
735
+ "vec_in_dim", "vec_type", "time_type", "double_block_type"}
736
+ structure = {k: v for k, v in tcfg.items() if k not in _meta}
737
+
738
+ def _resolve(p):
739
+ return p if os.path.isabs(p) else _safe_subpath(repo_dir, p)
740
+
741
+ cfg = ModelConfig(
742
+ vae_path=_resolve(mi.get("_vae_source")),
743
+ txt_enc_path=_resolve(mi.get("_text_encoder_path")),
744
+ model_structure=structure,
745
+ txt_max_length=tcfg.get("txt_max_length", 2048),
746
+ packing=tcfg.get("packing", True),
747
+ static_shift=tcfg.get("static_shift", 6.0),
748
+ )
749
+ model = MageFlowModel(cfg)
750
+ sd = load_file(_safe_subpath(repo_dir, "transformer", "diffusion_pytorch_model.safetensors"),
751
+ device="cpu")
752
+ model.transformer.load_state_dict(sd, strict=False, assign=True)
753
+ model.to(device)
754
+ model.transformer.to(torch.bfloat16)
755
+ model.txt_enc.to(torch.bfloat16)
756
+ if model.vae is not None:
757
+ model.vae.to(torch.bfloat16)
758
+ model.eval()
759
+ # Diffusers FlowMatchEulerDiscreteScheduler (scheduler/scheduler_config.json).
760
+ model.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
761
+ _safe_subpath(repo_dir, "scheduler"))
762
+ return model
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Mage-Flow Space requirements
2
+ # Do NOT list gradio, spaces, or huggingface_hub (preinstalled on the platform).
3
+
4
+ torchvision
5
+ diffusers>=0.37.0
6
+ transformers>=5.3.0,<5.6
7
+ accelerate>=1.0.0
8
+ safetensors>=0.4.0
9
+ einops>=0.8.0
10
+ pydantic>=2.0
11
+ pillow>=10.0
12
+ numpy>=1.26
13
+ loguru>=0.7.0
14
+ typing_extensions>=4.0