Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- .gitattributes +3 -0
- README.md +36 -8
- app.py +208 -0
- mage_flow/__init__.py +18 -0
- mage_flow/assets/cuisine.jpg +3 -0
- mage_flow/assets/dog.jpg +3 -0
- mage_flow/assets/portrait.jpg +3 -0
- mage_flow/models/__init__.py +3 -0
- mage_flow/models/mage_flow.py +364 -0
- mage_flow/models/modules/__init__.py +0 -0
- mage_flow/models/modules/_attn_backend.py +229 -0
- mage_flow/models/modules/mage_latent.py +119 -0
- mage_flow/models/modules/mage_layers.py +725 -0
- mage_flow/models/modules/mage_text.py +259 -0
- mage_flow/models/modules/mage_vae.py +651 -0
- mage_flow/models/modules/text_encoder.py +707 -0
- mage_flow/models/utils.py +175 -0
- mage_flow/pipeline.py +762 -0
- requirements.txt +14 -0
.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
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
mage_flow/assets/dog.jpg
ADDED
|
Git LFS Details
|
mage_flow/assets/portrait.jpg
ADDED
|
Git LFS Details
|
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
|