Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- README.md +38 -6
- app.py +241 -0
- requirements.txt +10 -0
- rti/__init__.py +6 -0
- rti/lora.py +58 -0
- rti/minit2i.py +553 -0
- rti/minit2i_text.py +63 -0
- rti/model.py +410 -0
- rti/sampling.py +42 -0
README.md
CHANGED
|
@@ -1,13 +1,45 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.30.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: RTI - Elastic Token Compression
|
| 3 |
+
emoji: 🧩
|
| 4 |
+
colorFrom: pink
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.30.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: Pixel-space t2i with a region-token budget dial
|
| 10 |
+
python_version: "3.12"
|
| 11 |
+
startup_duration_timeout: 30m
|
| 12 |
+
license: mit
|
| 13 |
+
models:
|
| 14 |
+
- eduardzamfir/RTI
|
| 15 |
+
- MiniT2I/MiniT2I
|
| 16 |
---
|
| 17 |
|
| 18 |
+
# RTI — Elastic Token Compression for Pixel-Space Diffusion Transformers
|
| 19 |
+
|
| 20 |
+
Interactive demo for **RTI (Region Token Interface)** — [arXiv:2608.29281](https://arxiv.org/abs/2608.29281).
|
| 21 |
+
A frozen MiniT2I pixel-space diffusion backbone runs its middle blocks on
|
| 22 |
+
`R ≪ N` content-shaped region tokens instead of all patches. One adapter
|
| 23 |
+
checkpoint serves **every** budget: the region count was drawn at random during
|
| 24 |
+
fine-tuning, so you can set R at inference to move along the quality/compute
|
| 25 |
+
curve.
|
| 26 |
+
|
| 27 |
+
**Budget dial (R at 512×512, N = 1024 patch tokens):**
|
| 28 |
+
|
| 29 |
+
| R | Meaning | Speedup (paper) |
|
| 30 |
+
|---|---|---|
|
| 31 |
+
| 1024 | dense backbone | 1.0× |
|
| 32 |
+
| 512 | matches dense quality | ~2.1× |
|
| 33 |
+
| 256 | small quality drop | ~2.6× |
|
| 34 |
+
| 64 | extreme compression | — |
|
| 35 |
+
|
| 36 |
+
Two released arms are preloaded — **RTI-B/16** and **RTI-L/16** (LoRA + the
|
| 37 |
+
small interface on the frozen MiniT2I trunk) — sharing a single FLAN-T5-Large
|
| 38 |
+
prompt encoder. Prompts are encoded at fp32, sampling runs bf16 Euler
|
| 39 |
+
flow-matching with CFG 5.0 over 100 steps, exactly as in the authors'
|
| 40 |
+
reference implementation (`rti.sampling.sample`).
|
| 41 |
+
|
| 42 |
+
The `rti/` package in this Space is the inference subset of
|
| 43 |
+
[eduardzamfir/RTI](https://github.com/eduardzamfir/RTI) (MIT license). Example
|
| 44 |
+
prompts are the ones the authors showcase on the
|
| 45 |
+
[project page](https://eduardzamfir.github.io/rti).
|
app.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Gradio demo for RTI — Elastic Token Compression for Pixel-Space Diffusion
|
| 2 |
+
Transformers (arXiv:2608.29281).
|
| 3 |
+
|
| 4 |
+
One adapter checkpoint (LoRA + interface on a frozen MiniT2I backbone) serves
|
| 5 |
+
EVERY region-token budget: pick R at inference to trade quality for compute.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 11 |
+
|
| 12 |
+
import spaces # MUST come before any torch / CUDA-touching import
|
| 13 |
+
|
| 14 |
+
import random
|
| 15 |
+
import time
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
import gradio as gr
|
| 19 |
+
from huggingface_hub import hf_hub_download
|
| 20 |
+
from transformers import AutoTokenizer, T5EncoderModel
|
| 21 |
+
|
| 22 |
+
from rti import build_rti
|
| 23 |
+
from rti.sampling import sample
|
| 24 |
+
|
| 25 |
+
ADAPTER_REPO = "eduardzamfir/RTI"
|
| 26 |
+
DTYPE = torch.bfloat16
|
| 27 |
+
MAX_SEED = 2**31 - 1
|
| 28 |
+
IMAGE_SIZE = 512
|
| 29 |
+
N_PATCH_TOKENS = 1024 # 32x32 patch grid at 512/16 — R = 1024 means dense
|
| 30 |
+
|
| 31 |
+
# Authors' released arms: B/16 (62 MB adapter) and L/16 (137 MB adapter).
|
| 32 |
+
VARIANTS = {
|
| 33 |
+
"RTI-B/16 (base)": ("MiniT2I-B/16", "rti_b16_40k.pt"),
|
| 34 |
+
"RTI-L/16 (large)": ("MiniT2I-L/16", "rti_l16_40k.pt"),
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
CSS = """
|
| 38 |
+
#col-container { max-width: 1100px; margin: 0 auto; }
|
| 39 |
+
.dark .gradio-container { color: var(--body-text-color); }
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
# Prompts the authors showcase on the RTI project page / paper figures.
|
| 43 |
+
EXAMPLES = [
|
| 44 |
+
["A polished white marble statue of a young woman with intricately carved curls and deeply folded drapery, displayed in a museum under warm directional light against a dark background, fine chisel marks visible in the stone."],
|
| 45 |
+
["Golden-hour street photograph of a rain-soaked crosswalk in Tokyo, umbrellas and neon reflections, shot on Portra 400 with a 35mm Summicron, shallow depth of field."],
|
| 46 |
+
["A lone red fox standing in deep snow at dawn, individual guard hairs and whiskers sharp, breath visible, flat white field and pale sky behind, wildlife photograph with a long lens."],
|
| 47 |
+
["Reflection in an astronaut's gold visor: the surface of Mars with a rocket lifting off in the middle distance, exhaust plume and a rolling dust cloud, the astronaut's own gloved hand raised, ultra detailed, black sky."],
|
| 48 |
+
["A small friendly robot in a bright research lab holding a white sign that says exactly HELLO in bold black letters. No other readable text."],
|
| 49 |
+
["The Mona Lisa rendered as a Minecraft scene, blocky voxel textures."],
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _load(variant_label):
|
| 54 |
+
variant, ckpt_name = VARIANTS[variant_label]
|
| 55 |
+
ckpt = hf_hub_download(ADAPTER_REPO, ckpt_name)
|
| 56 |
+
model = build_rti(
|
| 57 |
+
variant, ckpt=ckpt, device="cuda", dtype=DTYPE
|
| 58 |
+
).eval()
|
| 59 |
+
return model
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
print("[app] loading RTI backbones (adapters + MiniT2I trunk)…", flush=True)
|
| 63 |
+
MODELS = {label: _load(label) for label in VARIANTS}
|
| 64 |
+
print("[app] models loaded", flush=True)
|
| 65 |
+
|
| 66 |
+
# Shared FLAN-T5-Large prompt encoder (fp32, as in the authors' reference code).
|
| 67 |
+
TEXT_ENCODER = "google/flan-t5-large"
|
| 68 |
+
print("[app] loading FLAN-T5-Large prompt encoder…", flush=True)
|
| 69 |
+
TOKENIZER = AutoTokenizer.from_pretrained(TEXT_ENCODER)
|
| 70 |
+
TEXT_ENC = (
|
| 71 |
+
T5EncoderModel.from_pretrained(TEXT_ENCODER, torch_dtype=torch.float32)
|
| 72 |
+
.to("cuda")
|
| 73 |
+
.eval()
|
| 74 |
+
)
|
| 75 |
+
print("[app] prompt encoder loaded", flush=True)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@torch.no_grad()
|
| 79 |
+
def _encode(prompts):
|
| 80 |
+
tok = TOKENIZER(
|
| 81 |
+
list(prompts),
|
| 82 |
+
padding="max_length",
|
| 83 |
+
truncation=True,
|
| 84 |
+
max_length=256,
|
| 85 |
+
return_tensors="pt",
|
| 86 |
+
)
|
| 87 |
+
out = TEXT_ENC(
|
| 88 |
+
input_ids=tok.input_ids.to("cuda"),
|
| 89 |
+
attention_mask=tok.attention_mask.to("cuda"),
|
| 90 |
+
)
|
| 91 |
+
return out.last_hidden_state.float(), tok.attention_mask.float()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
@spaces.GPU(duration=120)
|
| 95 |
+
def generate(
|
| 96 |
+
prompt: str,
|
| 97 |
+
variant_label: str = "RTI-B/16 (base)",
|
| 98 |
+
budget: int = 512,
|
| 99 |
+
guidance_scale: float = 5.0,
|
| 100 |
+
num_inference_steps: int = 100,
|
| 101 |
+
seed: int = 0,
|
| 102 |
+
randomize_seed: bool = True,
|
| 103 |
+
progress=gr.Progress(track_tqdm=True),
|
| 104 |
+
):
|
| 105 |
+
"""Generate a 512x512 image from a text prompt at a chosen region-token budget.
|
| 106 |
+
|
| 107 |
+
Args:
|
| 108 |
+
prompt: what to generate.
|
| 109 |
+
variant_label: which released RTI adapter to use (B/16 or L/16).
|
| 110 |
+
budget: region-token count R for the model's core blocks. 1024 = dense
|
| 111 |
+
backbone (1.0x), 512 ≈ 1.8-2.1x faster, 256 ≈ 2.2-2.6x faster, down
|
| 112 |
+
to 64 for maximum compression.
|
| 113 |
+
guidance_scale: classifier-free guidance scale (paper uses 5.0).
|
| 114 |
+
num_inference_steps: Euler flow-matching steps (paper uses 100).
|
| 115 |
+
seed: RNG seed for reproducibility.
|
| 116 |
+
randomize_seed: draw a fresh seed each run instead of using `seed`.
|
| 117 |
+
"""
|
| 118 |
+
if not prompt or not prompt.strip():
|
| 119 |
+
raise gr.Error("Please enter a prompt.")
|
| 120 |
+
if randomize_seed:
|
| 121 |
+
seed = random.randint(0, MAX_SEED)
|
| 122 |
+
seed = int(seed)
|
| 123 |
+
budget = int(budget)
|
| 124 |
+
num_inference_steps = int(num_inference_steps)
|
| 125 |
+
|
| 126 |
+
model = MODELS[variant_label]
|
| 127 |
+
ctx, mask = _encode([prompt.strip()])
|
| 128 |
+
g = torch.Generator(device="cuda").manual_seed(seed)
|
| 129 |
+
|
| 130 |
+
t0 = time.perf_counter()
|
| 131 |
+
imgs = sample(
|
| 132 |
+
model,
|
| 133 |
+
ctx,
|
| 134 |
+
mask,
|
| 135 |
+
budget=budget,
|
| 136 |
+
steps=num_inference_steps,
|
| 137 |
+
cfg_scale=float(guidance_scale),
|
| 138 |
+
image_size=IMAGE_SIZE,
|
| 139 |
+
noise_scale=2.0,
|
| 140 |
+
t_eps=0.001,
|
| 141 |
+
cfg_interval=(0.0, 1.0),
|
| 142 |
+
generator=g,
|
| 143 |
+
device="cuda",
|
| 144 |
+
dtype=DTYPE,
|
| 145 |
+
progress=True,
|
| 146 |
+
)
|
| 147 |
+
elapsed = time.perf_counter() - t0
|
| 148 |
+
|
| 149 |
+
from torchvision.transforms.functional import to_pil_image
|
| 150 |
+
|
| 151 |
+
image = to_pil_image(imgs[0].clamp(0, 1))
|
| 152 |
+
mode = "dense (R = N = 1024)" if budget >= N_PATCH_TOKENS else f"R = {budget}"
|
| 153 |
+
return image, seed, f"{elapsed:.1f}s · {mode}"
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
|
| 157 |
+
gr.Markdown(
|
| 158 |
+
"""
|
| 159 |
+
# 🧩 RTI — Elastic Token Compression for Pixel-Space Diffusion Transformers
|
| 160 |
+
One adapter checkpoint serves **every** region-token budget. Set the budget
|
| 161 |
+
dial below to move along the quality/compute curve: **R = 1024** runs the
|
| 162 |
+
dense backbone, **512** matches dense quality at ~2× speed, **256** trades a
|
| 163 |
+
little quality for ~2.6× speed, down to **64** for extreme compression.
|
| 164 |
+
Images are 512×512, generated pixel-space (no VAE).
|
| 165 |
+
[Paper](https://arxiv.org/abs/2608.29281) ·
|
| 166 |
+
[Code](https://github.com/eduardzamfir/RTI) ·
|
| 167 |
+
[Adapters](https://huggingface.co/eduardzamfir/RTI) ·
|
| 168 |
+
[Backbone: MiniT2I](https://huggingface.co/MiniT2I/MiniT2I)
|
| 169 |
+
"""
|
| 170 |
+
)
|
| 171 |
+
with gr.Row():
|
| 172 |
+
with gr.Column(scale=1):
|
| 173 |
+
prompt = gr.Textbox(
|
| 174 |
+
label="Prompt",
|
| 175 |
+
placeholder="Describe the image you want to generate…",
|
| 176 |
+
lines=3,
|
| 177 |
+
)
|
| 178 |
+
variant_label = gr.Radio(
|
| 179 |
+
choices=list(VARIANTS.keys()),
|
| 180 |
+
value="RTI-B/16 (base)",
|
| 181 |
+
label="Model",
|
| 182 |
+
)
|
| 183 |
+
budget = gr.Slider(
|
| 184 |
+
label="Region-token budget R (compute dial)",
|
| 185 |
+
minimum=64,
|
| 186 |
+
maximum=1024,
|
| 187 |
+
step=64,
|
| 188 |
+
value=512,
|
| 189 |
+
info="1024 = dense backbone · 512 ≈ 2.1× speed · 256 ≈ 2.6× speed",
|
| 190 |
+
)
|
| 191 |
+
run_btn = gr.Button("Generate", variant="primary")
|
| 192 |
+
with gr.Accordion("Advanced settings", open=False):
|
| 193 |
+
guidance_scale = gr.Slider(
|
| 194 |
+
label="Guidance scale",
|
| 195 |
+
minimum=1.0,
|
| 196 |
+
maximum=10.0,
|
| 197 |
+
step=0.1,
|
| 198 |
+
value=5.0,
|
| 199 |
+
)
|
| 200 |
+
num_inference_steps = gr.Slider(
|
| 201 |
+
label="Inference steps",
|
| 202 |
+
minimum=10,
|
| 203 |
+
maximum=150,
|
| 204 |
+
step=1,
|
| 205 |
+
value=100,
|
| 206 |
+
)
|
| 207 |
+
with gr.Row():
|
| 208 |
+
seed = gr.Slider(
|
| 209 |
+
label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0
|
| 210 |
+
)
|
| 211 |
+
randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
|
| 212 |
+
with gr.Column(scale=1):
|
| 213 |
+
output = gr.Image(label="Result", height=512)
|
| 214 |
+
status = gr.Markdown(label="Status")
|
| 215 |
+
|
| 216 |
+
gr.Examples(
|
| 217 |
+
examples=EXAMPLES,
|
| 218 |
+
inputs=[prompt],
|
| 219 |
+
outputs=[output, seed, status],
|
| 220 |
+
fn=generate,
|
| 221 |
+
cache_examples=True,
|
| 222 |
+
cache_mode="lazy",
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
inputs = [
|
| 226 |
+
prompt,
|
| 227 |
+
variant_label,
|
| 228 |
+
budget,
|
| 229 |
+
guidance_scale,
|
| 230 |
+
num_inference_steps,
|
| 231 |
+
seed,
|
| 232 |
+
randomize_seed,
|
| 233 |
+
]
|
| 234 |
+
run_btn.click(
|
| 235 |
+
generate, inputs=inputs, outputs=[output, seed, status], api_name="generate"
|
| 236 |
+
)
|
| 237 |
+
prompt.submit(
|
| 238 |
+
generate, inputs=inputs, outputs=[output, seed, status], api_name="generate"
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
demo.launch(mcp_server=True)
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch
|
| 2 |
+
torchvision
|
| 3 |
+
transformers>=4.40
|
| 4 |
+
safetensors
|
| 5 |
+
pillow
|
| 6 |
+
numpy
|
| 7 |
+
tqdm
|
| 8 |
+
einops
|
| 9 |
+
accelerate
|
| 10 |
+
sentencepiece
|
rti/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Region Token Interface (RTI) for pixel-space diffusion transformers."""
|
| 2 |
+
from .model import RTIMiniT2I, RegionInterface, build_rti, RTI_models
|
| 3 |
+
from .minit2i import MiniT2I, MiniT2I_models, load_pretrained
|
| 4 |
+
|
| 5 |
+
__all__ = ["RTIMiniT2I", "RegionInterface", "build_rti", "RTI_models",
|
| 6 |
+
"MiniT2I", "MiniT2I_models", "load_pretrained"]
|
rti/lora.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LoRA adapter for a frozen nn.Linear."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import math
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class LoRALinear(nn.Module):
|
| 11 |
+
def __init__(
|
| 12 |
+
self,
|
| 13 |
+
base_linear: nn.Linear,
|
| 14 |
+
rank: int = 8,
|
| 15 |
+
alpha: float | None = None,
|
| 16 |
+
dropout: float = 0.0,
|
| 17 |
+
):
|
| 18 |
+
super().__init__()
|
| 19 |
+
if rank <= 0:
|
| 20 |
+
raise ValueError(f"LoRA rank must be positive; got {rank}")
|
| 21 |
+
if alpha is None:
|
| 22 |
+
alpha = float(rank)
|
| 23 |
+
self.rank = rank
|
| 24 |
+
self.alpha = float(alpha)
|
| 25 |
+
self.scale = self.alpha / rank
|
| 26 |
+
self.base = base_linear
|
| 27 |
+
|
| 28 |
+
in_f = base_linear.in_features
|
| 29 |
+
out_f = base_linear.out_features
|
| 30 |
+
# Match base's device/dtype so post-construction LoRA wrapping
|
| 31 |
+
# (e.g. apply_lora_and_freeze after the model is on GPU) doesn't leave
|
| 32 |
+
# the adapter params on CPU.
|
| 33 |
+
device = base_linear.weight.device
|
| 34 |
+
dtype = base_linear.weight.dtype
|
| 35 |
+
self.lora_A = nn.Parameter(torch.empty(rank, in_f, device=device, dtype=dtype))
|
| 36 |
+
self.lora_B = nn.Parameter(torch.zeros(out_f, rank, device=device, dtype=dtype))
|
| 37 |
+
nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
|
| 38 |
+
|
| 39 |
+
self.lora_dropout = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
|
| 40 |
+
# When False the adapter is skipped and forward == base(x). Lets callers
|
| 41 |
+
# evaluate the pristine frozen base (e.g. a self-distillation teacher)
|
| 42 |
+
# without unwrapping. Defaults True — backward-compatible.
|
| 43 |
+
self.enabled = True
|
| 44 |
+
|
| 45 |
+
@property
|
| 46 |
+
def in_features(self) -> int:
|
| 47 |
+
return self.base.in_features
|
| 48 |
+
|
| 49 |
+
@property
|
| 50 |
+
def out_features(self) -> int:
|
| 51 |
+
return self.base.out_features
|
| 52 |
+
|
| 53 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 54 |
+
out = self.base(x)
|
| 55 |
+
if not self.enabled:
|
| 56 |
+
return out
|
| 57 |
+
update = self.lora_dropout(x) @ self.lora_A.t() @ self.lora_B.t()
|
| 58 |
+
return out + update * self.scale
|
rti/minit2i.py
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MiniT2I: pixel-space MMDiT text-to-image backbone.
|
| 2 |
+
|
| 3 |
+
Port of the released model (https://huggingface.co/MiniT2I/MiniT2I) that matches
|
| 4 |
+
the published checkpoints key for key, so RoPE and block internals must not be
|
| 5 |
+
"simplified".
|
| 6 |
+
|
| 7 |
+
There is no AdaLN: conditioning runs entirely through the joint text+image
|
| 8 |
+
attention stream, with frozen FLAN-T5-Large embeddings as the text stream. The
|
| 9 |
+
time and pooled-text `vec` exist in the checkpoint but the blocks ignore them.
|
| 10 |
+
|
| 11 |
+
Flow matching: x_t = t*img + (1-t)*noise with noise scaled by 2; the net predicts
|
| 12 |
+
x0, and v = (x0 - x_t) / clamp(1-t). t=1 is clean, t=0 is noise.
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import math
|
| 17 |
+
import os
|
| 18 |
+
from dataclasses import dataclass, replace
|
| 19 |
+
from typing import Optional
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
import torch.nn.functional as F
|
| 23 |
+
from torch import nn
|
| 24 |
+
|
| 25 |
+
MINIT2I_HF_REPO = "MiniT2I/MiniT2I"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
# Primitives (verbatim from upstream mmdit.py)
|
| 30 |
+
# ---------------------------------------------------------------------------
|
| 31 |
+
def rotate_half(x: torch.Tensor) -> torch.Tensor:
|
| 32 |
+
# NOTE: interleaved variant (reshape → 2 → -1), NOT the half-split variant.
|
| 33 |
+
# The checkpoint's RoPE depends on this exact form.
|
| 34 |
+
x1, x2 = x.reshape(*x.shape[:-1], 2, -1).unbind(dim=-2)
|
| 35 |
+
return torch.cat((-x2, x1), dim=-1)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class RMSNorm(nn.Module):
|
| 39 |
+
def __init__(self, dim: int, eps: float = 1e-6):
|
| 40 |
+
super().__init__()
|
| 41 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 42 |
+
self.eps = eps
|
| 43 |
+
|
| 44 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 45 |
+
y = x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
|
| 46 |
+
return y * self.weight
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class TimestepEmbedder(nn.Module):
|
| 50 |
+
"""Kept for state-dict compatibility; its output is unused in the forward."""
|
| 51 |
+
|
| 52 |
+
def __init__(self, hidden_size: int, frequency_embedding_size: int = 256):
|
| 53 |
+
super().__init__()
|
| 54 |
+
self.frequency_embedding_size = frequency_embedding_size
|
| 55 |
+
self.mlp = nn.Sequential(
|
| 56 |
+
nn.Linear(frequency_embedding_size, hidden_size),
|
| 57 |
+
nn.SiLU(),
|
| 58 |
+
nn.Linear(hidden_size, hidden_size),
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
def forward(self, t: torch.Tensor) -> torch.Tensor:
|
| 62 |
+
half = self.frequency_embedding_size // 2
|
| 63 |
+
freqs = torch.exp(
|
| 64 |
+
-math.log(10000.0)
|
| 65 |
+
* torch.arange(half, device=t.device, dtype=torch.float32)
|
| 66 |
+
/ half
|
| 67 |
+
)
|
| 68 |
+
args = t.float()[:, None] * freqs[None]
|
| 69 |
+
emb = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
|
| 70 |
+
return self.mlp(emb.to(dtype=self.mlp[0].weight.dtype))
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class BottleneckPatchEmbed(nn.Module):
|
| 74 |
+
"""proj1: (in_ch -> pca) strided conv; proj2: (pca -> D) 1x1 conv."""
|
| 75 |
+
|
| 76 |
+
def __init__(self, img_size=512, patch_size=16, in_channels=3, pca_channels=128, hidden_size=768):
|
| 77 |
+
super().__init__()
|
| 78 |
+
self.img_size = img_size
|
| 79 |
+
self.patch_size = patch_size
|
| 80 |
+
self.proj1 = nn.Conv2d(in_channels, pca_channels, kernel_size=patch_size, stride=patch_size, bias=False)
|
| 81 |
+
self.proj2 = nn.Conv2d(pca_channels, hidden_size, kernel_size=1, stride=1, bias=True)
|
| 82 |
+
|
| 83 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 84 |
+
x = self.proj2(self.proj1(x))
|
| 85 |
+
return x.flatten(2).transpose(1, 2)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class SwiGLUMlp(nn.Module):
|
| 89 |
+
def __init__(self, in_features: int, hidden_features: int):
|
| 90 |
+
super().__init__()
|
| 91 |
+
hidden_dim = (hidden_features + 7) // 8 * 8
|
| 92 |
+
self.w1 = nn.Linear(in_features, hidden_dim, bias=False)
|
| 93 |
+
self.w3 = nn.Linear(in_features, hidden_dim, bias=False)
|
| 94 |
+
self.w2 = nn.Linear(hidden_dim, in_features, bias=False)
|
| 95 |
+
|
| 96 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 97 |
+
return self.w2(F.silu(self.w1(x)) * self.w3(x))
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class TextRotaryEmbedding1D(nn.Module):
|
| 101 |
+
def __init__(self, head_dim: int, theta: float = 10000.0):
|
| 102 |
+
super().__init__()
|
| 103 |
+
self.head_dim = head_dim
|
| 104 |
+
self.theta = theta
|
| 105 |
+
|
| 106 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 107 |
+
b, length, h, d = x.shape
|
| 108 |
+
inv = 1.0 / (self.theta ** (torch.arange(0, d, 2, device=x.device, dtype=torch.float32) / d))
|
| 109 |
+
pos = torch.arange(length, device=x.device, dtype=torch.float32)
|
| 110 |
+
angles = torch.einsum("l,f->lf", pos, inv)
|
| 111 |
+
angles = torch.cat([angles, angles], dim=-1)
|
| 112 |
+
cos = angles.cos().to(dtype=x.dtype)
|
| 113 |
+
sin = angles.sin().to(dtype=x.dtype)
|
| 114 |
+
return x * cos[None, :, None, :] + rotate_half(x) * sin[None, :, None, :]
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
class VisionRotaryEmbeddingFast(nn.Module):
|
| 118 |
+
def __init__(self, head_dim: int, theta: float = 10000.0):
|
| 119 |
+
super().__init__()
|
| 120 |
+
self.dim = head_dim // 2
|
| 121 |
+
self.theta = theta
|
| 122 |
+
|
| 123 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 124 |
+
length = x.shape[1]
|
| 125 |
+
side = int(math.sqrt(length))
|
| 126 |
+
if side * side != length:
|
| 127 |
+
raise ValueError(f"image token length must be square, got {length}")
|
| 128 |
+
freqs = 1.0 / (
|
| 129 |
+
self.theta
|
| 130 |
+
** (torch.arange(0, self.dim, 2, device=x.device, dtype=torch.float32)[: self.dim // 2] / self.dim)
|
| 131 |
+
)
|
| 132 |
+
t = torch.arange(side, device=x.device, dtype=torch.float32)
|
| 133 |
+
base = torch.einsum("l,f->lf", t, freqs)
|
| 134 |
+
f_h, f_w = torch.broadcast_tensors(base[:, None, :], base[None, :, :])
|
| 135 |
+
angles = torch.cat([f_h, f_w], dim=-1)
|
| 136 |
+
angles = torch.cat([angles, angles], dim=-1).reshape(length, -1)
|
| 137 |
+
cos = angles.cos().to(dtype=x.dtype)
|
| 138 |
+
sin = angles.sin().to(dtype=x.dtype)
|
| 139 |
+
return x * cos[None, :, None, :] + rotate_half(x) * sin[None, :, None, :]
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class MultiModalRotaryEmbeddingFast(nn.Module):
|
| 143 |
+
def __init__(self, head_dim: int):
|
| 144 |
+
super().__init__()
|
| 145 |
+
self.text_rope = TextRotaryEmbedding1D(head_dim)
|
| 146 |
+
self.vision_rope = VisionRotaryEmbeddingFast(head_dim)
|
| 147 |
+
|
| 148 |
+
def forward(self, x: torch.Tensor, txt_len: int) -> torch.Tensor:
|
| 149 |
+
txt = self.text_rope(x[:, :txt_len])
|
| 150 |
+
img = self.vision_rope(x[:, txt_len:])
|
| 151 |
+
return torch.cat([txt, img], dim=1)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
# ---------------------------------------------------------------------------
|
| 155 |
+
# Blocks
|
| 156 |
+
# ---------------------------------------------------------------------------
|
| 157 |
+
class PlainTextTransformerBlock(nn.Module):
|
| 158 |
+
def __init__(self, hidden_size=768, num_heads=12, head_dim=64, mlp_ratio=2.6667):
|
| 159 |
+
super().__init__()
|
| 160 |
+
self.num_heads = num_heads
|
| 161 |
+
self.head_dim = head_dim
|
| 162 |
+
inner_dim = num_heads * head_dim
|
| 163 |
+
self.norm1 = RMSNorm(hidden_size)
|
| 164 |
+
self.norm2 = RMSNorm(hidden_size)
|
| 165 |
+
self.qkv = nn.Linear(hidden_size, inner_dim * 3)
|
| 166 |
+
self.attn_proj = nn.Linear(inner_dim, hidden_size)
|
| 167 |
+
self.mlp = SwiGLUMlp(hidden_size, int(hidden_size * mlp_ratio))
|
| 168 |
+
self.q_norm = RMSNorm(head_dim)
|
| 169 |
+
self.k_norm = RMSNorm(head_dim)
|
| 170 |
+
self.rope = TextRotaryEmbedding1D(head_dim)
|
| 171 |
+
|
| 172 |
+
def forward(self, txt: torch.Tensor) -> torch.Tensor:
|
| 173 |
+
b, length, _ = txt.shape
|
| 174 |
+
qkv = self.qkv(self.norm1(txt)).reshape(b, length, 3, self.num_heads, self.head_dim)
|
| 175 |
+
q, k, v = qkv[:, :, 0], qkv[:, :, 1], qkv[:, :, 2]
|
| 176 |
+
q = self.rope(self.q_norm(q))
|
| 177 |
+
k = self.rope(self.k_norm(k))
|
| 178 |
+
attn = torch.einsum("bqhd,bkhd->bhqk", q, k) * (self.head_dim ** -0.5)
|
| 179 |
+
out = torch.einsum("bhqk,bkhd->bqhd", attn.softmax(dim=-1), v).reshape(b, length, -1)
|
| 180 |
+
txt = txt + self.attn_proj(out)
|
| 181 |
+
txt = txt + self.mlp(self.norm2(txt))
|
| 182 |
+
return txt
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
class DoubleStreamDiTBlock(nn.Module):
|
| 186 |
+
def __init__(self, hidden_size=768, txt_hidden_size=768, num_heads=12, head_dim=64, mlp_ratio=2.6667):
|
| 187 |
+
super().__init__()
|
| 188 |
+
self.hidden_size = hidden_size
|
| 189 |
+
self.txt_hidden_size = txt_hidden_size
|
| 190 |
+
self.num_heads = num_heads
|
| 191 |
+
self.head_dim = head_dim
|
| 192 |
+
inner_dim = num_heads * head_dim
|
| 193 |
+
self.img_norm1 = RMSNorm(hidden_size)
|
| 194 |
+
self.img_norm2 = RMSNorm(hidden_size)
|
| 195 |
+
self.txt_norm1 = RMSNorm(txt_hidden_size)
|
| 196 |
+
self.txt_norm2 = RMSNorm(txt_hidden_size)
|
| 197 |
+
self.img_qkv = nn.Linear(hidden_size, inner_dim * 3)
|
| 198 |
+
self.txt_qkv = nn.Linear(txt_hidden_size, inner_dim * 3)
|
| 199 |
+
self.q_norm = RMSNorm(head_dim)
|
| 200 |
+
self.k_norm = RMSNorm(head_dim)
|
| 201 |
+
self.rope = MultiModalRotaryEmbeddingFast(head_dim)
|
| 202 |
+
self.img_attn_proj = nn.Linear(inner_dim, hidden_size)
|
| 203 |
+
self.txt_attn_proj = nn.Linear(inner_dim, txt_hidden_size)
|
| 204 |
+
self.img_mlp = SwiGLUMlp(hidden_size, int(hidden_size * mlp_ratio))
|
| 205 |
+
self.txt_mlp = SwiGLUMlp(txt_hidden_size, int(txt_hidden_size * mlp_ratio))
|
| 206 |
+
|
| 207 |
+
def forward(self, x: torch.Tensor, txt: torch.Tensor, vec=None):
|
| 208 |
+
b, li, _ = x.shape
|
| 209 |
+
lt = txt.shape[1]
|
| 210 |
+
x_norm = self.img_norm1(x)
|
| 211 |
+
txt_norm = self.txt_norm1(txt)
|
| 212 |
+
qkv_i = self.img_qkv(x_norm).reshape(b, li, 3, self.num_heads, self.head_dim)
|
| 213 |
+
qkv_t = self.txt_qkv(txt_norm).reshape(b, lt, 3, self.num_heads, self.head_dim)
|
| 214 |
+
q_i, k_i, v_i = qkv_i[:, :, 0], qkv_i[:, :, 1], qkv_i[:, :, 2]
|
| 215 |
+
q_t, k_t, v_t = qkv_t[:, :, 0], qkv_t[:, :, 1], qkv_t[:, :, 2]
|
| 216 |
+
q_i, k_i = self.q_norm(q_i), self.k_norm(k_i)
|
| 217 |
+
q_t, k_t = self.q_norm(q_t), self.k_norm(k_t)
|
| 218 |
+
q = self.rope(torch.cat([q_t, q_i], dim=1), txt_len=lt)
|
| 219 |
+
k = self.rope(torch.cat([k_t, k_i], dim=1), txt_len=lt)
|
| 220 |
+
v = torch.cat([v_t, v_i], dim=1)
|
| 221 |
+
attn = torch.einsum("bqhd,bkhd->bhqk", q, k) * (self.head_dim ** -0.5)
|
| 222 |
+
out = torch.einsum("bhqk,bkhd->bqhd", attn.softmax(dim=-1), v)
|
| 223 |
+
x = x + self.img_attn_proj(out[:, lt:].reshape(b, li, -1))
|
| 224 |
+
txt = txt + self.txt_attn_proj(out[:, :lt].reshape(b, lt, -1))
|
| 225 |
+
x = x + self.img_mlp(self.img_norm2(x))
|
| 226 |
+
txt = txt + self.txt_mlp(self.txt_norm2(txt))
|
| 227 |
+
return x, txt
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
class FinalLayer(nn.Module):
|
| 231 |
+
def __init__(self, hidden_size=768, patch_size=16, out_channels=3):
|
| 232 |
+
super().__init__()
|
| 233 |
+
self.patch_size = patch_size
|
| 234 |
+
self.out_channels = out_channels
|
| 235 |
+
self.norm_final = RMSNorm(hidden_size)
|
| 236 |
+
self.linear = nn.Linear(hidden_size, patch_size * patch_size * out_channels)
|
| 237 |
+
|
| 238 |
+
def forward(self, x: torch.Tensor, vec=None) -> torch.Tensor:
|
| 239 |
+
return self.linear(self.norm_final(x))
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def get_1d_sincos_pos_embed(embed_dim: int, pos: torch.Tensor) -> torch.Tensor:
|
| 243 |
+
omega = torch.arange(embed_dim // 2, device=pos.device, dtype=torch.float32)
|
| 244 |
+
omega = 1.0 / (10000 ** (omega / (embed_dim / 2.0)))
|
| 245 |
+
out = torch.einsum("m,d->md", pos.reshape(-1), omega)
|
| 246 |
+
return torch.cat([out.sin(), out.cos()], dim=1)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def get_2d_sincos_pos_embed(embed_dim: int, grid_size: int, device, dtype) -> torch.Tensor:
|
| 250 |
+
grid_h = torch.arange(grid_size, device=device, dtype=torch.float32)
|
| 251 |
+
grid_w = torch.arange(grid_size, device=device, dtype=torch.float32)
|
| 252 |
+
grid = torch.meshgrid(grid_w, grid_h, indexing="xy")
|
| 253 |
+
grid = torch.stack(grid, dim=0).reshape(2, 1, grid_size, grid_size)
|
| 254 |
+
emb_h = get_1d_sincos_pos_embed(embed_dim // 2, grid[0])
|
| 255 |
+
emb_w = get_1d_sincos_pos_embed(embed_dim // 2, grid[1])
|
| 256 |
+
return torch.cat([emb_h, emb_w], dim=1).to(dtype=dtype)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
# ---------------------------------------------------------------------------
|
| 260 |
+
# Config + net
|
| 261 |
+
# ---------------------------------------------------------------------------
|
| 262 |
+
@dataclass
|
| 263 |
+
class MMJiTConfig:
|
| 264 |
+
image_size: int = 512
|
| 265 |
+
patch_size: int = 16
|
| 266 |
+
in_channels: int = 3
|
| 267 |
+
txt_input_size: int = 1024 # FLAN-T5-Large hidden size
|
| 268 |
+
hidden_size: int = 768
|
| 269 |
+
txt_hidden_size: int = 768
|
| 270 |
+
cond_vec_size: int = 768
|
| 271 |
+
depth_double: int = 17
|
| 272 |
+
txt_preamble_depth: int = 2
|
| 273 |
+
num_heads: int = 12
|
| 274 |
+
head_dim: int = 64
|
| 275 |
+
mlp_ratio: float = 2.6667
|
| 276 |
+
pca_channels: int = 128
|
| 277 |
+
prompt_length: int = 256
|
| 278 |
+
n_T: int = 100
|
| 279 |
+
prediction: str = "x"
|
| 280 |
+
sampler: str = "euler"
|
| 281 |
+
cfg_channels: int = 3
|
| 282 |
+
cfg_interval: tuple = (0.0, 1.0)
|
| 283 |
+
llm: str = "google/flan-t5-large"
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
class MMJiT(nn.Module):
|
| 287 |
+
"""The `net` sub-tree. State-dict matches the `model.net.<...>` checkpoint keys."""
|
| 288 |
+
|
| 289 |
+
def __init__(self, cfg: MMJiTConfig):
|
| 290 |
+
super().__init__()
|
| 291 |
+
self.cfg = cfg
|
| 292 |
+
self.latent_img_size = cfg.image_size // cfg.patch_size
|
| 293 |
+
self.img_embedder = BottleneckPatchEmbed(
|
| 294 |
+
cfg.image_size, cfg.patch_size, cfg.in_channels, cfg.pca_channels, cfg.hidden_size
|
| 295 |
+
)
|
| 296 |
+
self.txt_embedder = nn.Linear(cfg.txt_input_size, cfg.txt_hidden_size, bias=False)
|
| 297 |
+
self.mask_token = nn.Parameter(torch.zeros(1, 1, cfg.txt_input_size))
|
| 298 |
+
self.t_embedder = TimestepEmbedder(cfg.cond_vec_size)
|
| 299 |
+
self.pooled_embedder = nn.Linear(cfg.txt_input_size, cfg.cond_vec_size, bias=False)
|
| 300 |
+
self.txt_preamble_blocks = nn.ModuleList(
|
| 301 |
+
[
|
| 302 |
+
PlainTextTransformerBlock(cfg.txt_hidden_size, cfg.num_heads, cfg.head_dim, cfg.mlp_ratio)
|
| 303 |
+
for _ in range(cfg.txt_preamble_depth)
|
| 304 |
+
]
|
| 305 |
+
)
|
| 306 |
+
self.double_blocks = nn.ModuleList(
|
| 307 |
+
[
|
| 308 |
+
DoubleStreamDiTBlock(
|
| 309 |
+
cfg.hidden_size, cfg.txt_hidden_size, cfg.num_heads, cfg.head_dim, cfg.mlp_ratio
|
| 310 |
+
)
|
| 311 |
+
for _ in range(cfg.depth_double)
|
| 312 |
+
]
|
| 313 |
+
)
|
| 314 |
+
self.final_layer = FinalLayer(cfg.hidden_size, cfg.patch_size, cfg.in_channels)
|
| 315 |
+
|
| 316 |
+
def unpatchify(self, x: torch.Tensor) -> torch.Tensor:
|
| 317 |
+
b = x.shape[0]
|
| 318 |
+
p = self.cfg.patch_size
|
| 319 |
+
c = self.cfg.in_channels
|
| 320 |
+
h = w = int(math.sqrt(x.shape[1]))
|
| 321 |
+
x = x.reshape(b, h, w, p, p, c)
|
| 322 |
+
x = torch.einsum("nhwpqc->nchpwq", x)
|
| 323 |
+
return x.reshape(b, c, h * p, w * p)
|
| 324 |
+
|
| 325 |
+
def forward(self, img: torch.Tensor, t: torch.Tensor, context: torch.Tensor, attn_mask: torch.Tensor) -> torch.Tensor:
|
| 326 |
+
if img.ndim == 4 and img.shape[1] != self.cfg.in_channels:
|
| 327 |
+
img = img.permute(0, 3, 1, 2)
|
| 328 |
+
attn_mask = attn_mask.to(device=context.device)
|
| 329 |
+
context = torch.where(attn_mask[:, :, None] > 0.5, context, self.mask_token.to(dtype=context.dtype))
|
| 330 |
+
x = self.img_embedder(img)
|
| 331 |
+
pos = get_2d_sincos_pos_embed(self.cfg.hidden_size, self.latent_img_size, x.device, x.dtype)
|
| 332 |
+
x = x + pos[None]
|
| 333 |
+
t_vec = self.t_embedder(t)
|
| 334 |
+
txt = self.txt_embedder(context.to(dtype=self.txt_embedder.weight.dtype))
|
| 335 |
+
pooled_text = context.mean(dim=1)
|
| 336 |
+
vec = t_vec + self.pooled_embedder(pooled_text.to(dtype=self.pooled_embedder.weight.dtype))
|
| 337 |
+
for block in self.txt_preamble_blocks:
|
| 338 |
+
txt = block(txt)
|
| 339 |
+
for block in self.double_blocks:
|
| 340 |
+
x, txt = block(x, txt, vec)
|
| 341 |
+
combined = torch.cat([txt, x], dim=1)
|
| 342 |
+
out = self.final_layer(combined, vec)
|
| 343 |
+
img_out = out[:, txt.shape[1]:, :]
|
| 344 |
+
return self.unpatchify(img_out)
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
# ---------------------------------------------------------------------------
|
| 348 |
+
# Wrapper (mirrors upstream DiffusionModel): flow-matching + Euler/CFG sampler
|
| 349 |
+
# ---------------------------------------------------------------------------
|
| 350 |
+
class MiniT2I(nn.Module):
|
| 351 |
+
"""Holds `self.net = MMJiT(cfg)` under the same name the checkpoint uses.
|
| 352 |
+
|
| 353 |
+
`forward` returns the x0 prediction (`predict='x'`), so the repo's existing
|
| 354 |
+
x→velocity machinery applies. `sample()` runs the native Euler + CFG loop.
|
| 355 |
+
"""
|
| 356 |
+
|
| 357 |
+
NOISE_SCALE = 2.0
|
| 358 |
+
T_EPS = 0.001
|
| 359 |
+
|
| 360 |
+
def __init__(self, cfg: MMJiTConfig):
|
| 361 |
+
super().__init__()
|
| 362 |
+
self.cfg = cfg
|
| 363 |
+
self.net = MMJiT(cfg)
|
| 364 |
+
|
| 365 |
+
@property
|
| 366 |
+
def image_size(self) -> int:
|
| 367 |
+
return self.cfg.image_size
|
| 368 |
+
|
| 369 |
+
def forward(self, img: torch.Tensor, t: torch.Tensor, context: torch.Tensor, attn_mask: torch.Tensor) -> torch.Tensor:
|
| 370 |
+
return self.net(img, t, context, attn_mask)
|
| 371 |
+
|
| 372 |
+
def pred_velocity(self, x, t, context, mask):
|
| 373 |
+
x0 = self.net(x, t, context, mask)
|
| 374 |
+
return (x0 - x) / torch.clamp(1 - t[:, None, None, None], min=self.T_EPS)
|
| 375 |
+
|
| 376 |
+
def cfg_velocity(self, x, t, context, mask, cfg_scale: float):
|
| 377 |
+
b = x.shape[0]
|
| 378 |
+
xx = torch.cat([x, x], dim=0)
|
| 379 |
+
tt = torch.cat([t, t], dim=0)
|
| 380 |
+
cc = torch.cat([context, context], dim=0)
|
| 381 |
+
mm = torch.cat([mask, torch.zeros_like(mask)], dim=0) # null branch = all-masked
|
| 382 |
+
out = self.pred_velocity(xx, tt, cc, mm)
|
| 383 |
+
cond, uncond = out[:b], out[b:]
|
| 384 |
+
lo, hi = self.cfg.cfg_interval
|
| 385 |
+
use_cfg = ((t >= lo) & (t <= hi)).to(out.dtype)[:, None, None, None]
|
| 386 |
+
scale = torch.where(use_cfg > 0, out.new_tensor(cfg_scale), out.new_tensor(1.0))
|
| 387 |
+
return uncond + (cond - uncond) * scale
|
| 388 |
+
|
| 389 |
+
@torch.no_grad()
|
| 390 |
+
def sample(self, context, mask, cfg_scale: float = 2.5, steps: Optional[int] = None,
|
| 391 |
+
generator=None, progress: bool = False) -> torch.Tensor:
|
| 392 |
+
"""Euler flow-matching sampler. Returns pixels in [-1, 1]."""
|
| 393 |
+
steps = int(steps or self.cfg.n_T)
|
| 394 |
+
b = context.shape[0]
|
| 395 |
+
device = context.device
|
| 396 |
+
dtype = next(self.parameters()).dtype
|
| 397 |
+
x = torch.randn(
|
| 398 |
+
b, self.cfg.in_channels, self.cfg.image_size, self.cfg.image_size,
|
| 399 |
+
generator=generator, device=device, dtype=dtype,
|
| 400 |
+
) * self.NOISE_SCALE
|
| 401 |
+
timesteps = torch.linspace(0.0, 1.0, steps + 1, device=device, dtype=dtype)
|
| 402 |
+
iterator = range(steps)
|
| 403 |
+
if progress:
|
| 404 |
+
from tqdm.auto import tqdm
|
| 405 |
+
iterator = tqdm(iterator)
|
| 406 |
+
context = context.to(dtype)
|
| 407 |
+
mask = mask.to(dtype)
|
| 408 |
+
for i in iterator:
|
| 409 |
+
t_cur = timesteps[i].expand(b)
|
| 410 |
+
t_next = timesteps[i + 1].expand(b)
|
| 411 |
+
if cfg_scale != 1.0:
|
| 412 |
+
v = self.cfg_velocity(x, t_cur, context, mask, cfg_scale)
|
| 413 |
+
else:
|
| 414 |
+
v = self.pred_velocity(x, t_cur, context, mask)
|
| 415 |
+
x = x + (t_next - t_cur)[:, None, None, None] * v
|
| 416 |
+
return x.clamp(-1, 1)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
# ---------------------------------------------------------------------------
|
| 420 |
+
# Registry + pretrained loader
|
| 421 |
+
# ---------------------------------------------------------------------------
|
| 422 |
+
# cfg_vec_size / txt_hidden_size follow hidden_size for both released variants.
|
| 423 |
+
MiniT2I_configs = {
|
| 424 |
+
"MiniT2I-B/16": MMJiTConfig(
|
| 425 |
+
hidden_size=768, txt_hidden_size=768, cond_vec_size=768,
|
| 426 |
+
depth_double=17, num_heads=12, head_dim=64, mlp_ratio=2.6667,
|
| 427 |
+
),
|
| 428 |
+
"MiniT2I-L/16": MMJiTConfig(
|
| 429 |
+
hidden_size=1248, txt_hidden_size=1248, cond_vec_size=1248,
|
| 430 |
+
depth_double=23, num_heads=24, head_dim=52, mlp_ratio=2.7051282051282053,
|
| 431 |
+
),
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
MiniT2I_HF_SUBFOLDER = {
|
| 435 |
+
"MiniT2I-B/16": "minit2i-b-16",
|
| 436 |
+
"MiniT2I-L/16": "minit2i-l-16",
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
def _build(variant: str):
|
| 441 |
+
base_cfg = MiniT2I_configs[variant]
|
| 442 |
+
|
| 443 |
+
def ctor(**overrides) -> MiniT2I:
|
| 444 |
+
# Accept common repo kwargs (image_size, in_channels, patch_size, ...) as
|
| 445 |
+
# dataclass field overrides; ignore anything not in MMJiTConfig.
|
| 446 |
+
fields = {f for f in base_cfg.__dataclass_fields__}
|
| 447 |
+
cfg = replace(base_cfg, **{k: v for k, v in overrides.items() if k in fields})
|
| 448 |
+
return MiniT2I(cfg)
|
| 449 |
+
|
| 450 |
+
return ctor
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
MiniT2I_models = {name: _build(name) for name in MiniT2I_configs}
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
def default_cfg_scale(variant: str) -> float:
|
| 457 |
+
"""Recommended classifier-free guidance scale per released variant."""
|
| 458 |
+
return 6.0 if "L/" in variant else 2.5
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def _cache_dir(cache_dir: Optional[str]) -> Optional[str]:
|
| 462 |
+
# L/16 is 3.65 GB — keep off the boot nvme by default (see CLAUDE.md).
|
| 463 |
+
return cache_dir or os.environ.get("MINIT2I_CACHE") or None
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
@torch.no_grad()
|
| 467 |
+
def load_pretrained(
|
| 468 |
+
variant: str,
|
| 469 |
+
model: Optional[MiniT2I] = None,
|
| 470 |
+
*,
|
| 471 |
+
cache_dir: Optional[str] = None,
|
| 472 |
+
device: str | torch.device = "cpu",
|
| 473 |
+
dtype: Optional[torch.dtype] = None,
|
| 474 |
+
) -> MiniT2I:
|
| 475 |
+
"""Download the released diffusers safetensors and load them into `model`.
|
| 476 |
+
|
| 477 |
+
Checkpoint keys are prefixed `model.net.`; we strip that and load into
|
| 478 |
+
`model.net` with strict=True (the unused t/pooled embedders exist in the
|
| 479 |
+
checkpoint, so strict passes cleanly).
|
| 480 |
+
"""
|
| 481 |
+
from huggingface_hub import hf_hub_download
|
| 482 |
+
from safetensors.torch import load_file
|
| 483 |
+
|
| 484 |
+
if variant not in MiniT2I_HF_SUBFOLDER:
|
| 485 |
+
raise KeyError(f"unknown MiniT2I variant {variant!r}; choices: {list(MiniT2I_HF_SUBFOLDER)}")
|
| 486 |
+
if model is None:
|
| 487 |
+
model = MiniT2I_models[variant]()
|
| 488 |
+
|
| 489 |
+
sub = MiniT2I_HF_SUBFOLDER[variant]
|
| 490 |
+
path = hf_hub_download(
|
| 491 |
+
MINIT2I_HF_REPO,
|
| 492 |
+
f"{sub}/transformer/diffusion_pytorch_model.safetensors",
|
| 493 |
+
cache_dir=_cache_dir(cache_dir),
|
| 494 |
+
)
|
| 495 |
+
raw = load_file(path)
|
| 496 |
+
prefix = "model.net."
|
| 497 |
+
net_sd = {}
|
| 498 |
+
for k, v in raw.items():
|
| 499 |
+
if k.startswith(prefix):
|
| 500 |
+
net_sd[k[len(prefix):]] = v
|
| 501 |
+
elif k.startswith("net."):
|
| 502 |
+
net_sd[k[len("net."):]] = v
|
| 503 |
+
else:
|
| 504 |
+
net_sd[k] = v
|
| 505 |
+
missing, unexpected = model.net.load_state_dict(net_sd, strict=True)
|
| 506 |
+
assert not missing and not unexpected, f"key mismatch: missing={missing} unexpected={unexpected}"
|
| 507 |
+
|
| 508 |
+
model = model.to(device)
|
| 509 |
+
if dtype is not None:
|
| 510 |
+
model = model.to(dtype)
|
| 511 |
+
return model
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
@torch.no_grad()
|
| 515 |
+
def load_pretrained_repatched(
|
| 516 |
+
variant: str,
|
| 517 |
+
model: MiniT2I,
|
| 518 |
+
*,
|
| 519 |
+
cache_dir: Optional[str] = None,
|
| 520 |
+
device: str | torch.device = "cpu",
|
| 521 |
+
dtype: Optional[torch.dtype] = None,
|
| 522 |
+
) -> tuple[MiniT2I, list]:
|
| 523 |
+
"""Load the released $512$/p16 weights into a RE-PATCHED model (e.g. p=32 @1024).
|
| 524 |
+
|
| 525 |
+
Both p16@512 and p32@1024 tokenize to a $32{\\times}32$ grid, so the entire
|
| 526 |
+
transformer trunk (blocks, text path, rope, embedders) transfers by shape.
|
| 527 |
+
Only the patch conv (`img_embedder.proj1`) and the output head
|
| 528 |
+
(`final_layer.linear`) depend on patch size and are left at init --- these must
|
| 529 |
+
be finetuned. Returns (model, list of re-initialised keys)."""
|
| 530 |
+
from huggingface_hub import hf_hub_download
|
| 531 |
+
from safetensors.torch import load_file
|
| 532 |
+
sub = MiniT2I_HF_SUBFOLDER[variant]
|
| 533 |
+
path = hf_hub_download(
|
| 534 |
+
MINIT2I_HF_REPO, f"{sub}/transformer/diffusion_pytorch_model.safetensors",
|
| 535 |
+
cache_dir=_cache_dir(cache_dir))
|
| 536 |
+
raw = load_file(path)
|
| 537 |
+
net_sd = {}
|
| 538 |
+
for k, v in raw.items():
|
| 539 |
+
for pre in ("model.net.", "net."):
|
| 540 |
+
if k.startswith(pre):
|
| 541 |
+
k = k[len(pre):]; break
|
| 542 |
+
net_sd[k] = v
|
| 543 |
+
tgt = model.net.state_dict()
|
| 544 |
+
keep = {k: v for k, v in net_sd.items() if k in tgt and tgt[k].shape == v.shape}
|
| 545 |
+
reinit = sorted(set(tgt) - set(keep))
|
| 546 |
+
model.net.load_state_dict(keep, strict=False)
|
| 547 |
+
if int(os.environ.get("RANK", "0")) == 0:
|
| 548 |
+
print(f"[repatch] transferred {len(keep)}/{len(tgt)} trunk tensors; "
|
| 549 |
+
f"re-init {len(reinit)}: {reinit}", flush=True)
|
| 550 |
+
model = model.to(device)
|
| 551 |
+
if dtype is not None:
|
| 552 |
+
model = model.to(dtype)
|
| 553 |
+
return model, reinit
|
rti/minit2i_text.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FLAN-T5-Large prompt encoder used by MiniT2I."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
_T5_NAME = "google/flan-t5-large"
|
| 9 |
+
_SINGLETON: dict[tuple, "T5PromptEncoder"] = {}
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class T5PromptEncoder:
|
| 13 |
+
def __init__(
|
| 14 |
+
self,
|
| 15 |
+
model_name: str = _T5_NAME,
|
| 16 |
+
device: str | torch.device = "cuda",
|
| 17 |
+
dtype: torch.dtype = torch.float32,
|
| 18 |
+
max_len: int = 256,
|
| 19 |
+
cache_dir: Optional[str] = None,
|
| 20 |
+
):
|
| 21 |
+
from transformers import AutoTokenizer, T5EncoderModel
|
| 22 |
+
|
| 23 |
+
self.max_len = max_len
|
| 24 |
+
self.device = torch.device(device)
|
| 25 |
+
self.dtype = dtype
|
| 26 |
+
self.tokenizer = AutoTokenizer.from_pretrained(model_name, cache_dir=cache_dir)
|
| 27 |
+
self.model = (
|
| 28 |
+
T5EncoderModel.from_pretrained(model_name, cache_dir=cache_dir)
|
| 29 |
+
.to(self.device, dtype=dtype)
|
| 30 |
+
.eval()
|
| 31 |
+
)
|
| 32 |
+
for p in self.model.parameters():
|
| 33 |
+
p.requires_grad_(False)
|
| 34 |
+
|
| 35 |
+
@torch.no_grad()
|
| 36 |
+
def encode(self, prompts: list[str], max_len: Optional[int] = None):
|
| 37 |
+
max_len = int(max_len or self.max_len)
|
| 38 |
+
tok = self.tokenizer(
|
| 39 |
+
list(prompts),
|
| 40 |
+
padding="max_length",
|
| 41 |
+
truncation=True,
|
| 42 |
+
max_length=max_len,
|
| 43 |
+
return_tensors="pt",
|
| 44 |
+
)
|
| 45 |
+
input_ids = tok.input_ids.to(self.device)
|
| 46 |
+
attn_mask = tok.attention_mask.to(self.device)
|
| 47 |
+
out = self.model(input_ids=input_ids, attention_mask=attn_mask)
|
| 48 |
+
context = out.last_hidden_state.to(self.dtype)
|
| 49 |
+
return context, attn_mask.to(self.dtype)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def get_prompt_encoder(
|
| 53 |
+
model_name: str = _T5_NAME,
|
| 54 |
+
device: str | torch.device = "cuda",
|
| 55 |
+
dtype: torch.dtype = torch.float32,
|
| 56 |
+
max_len: int = 256,
|
| 57 |
+
cache_dir: Optional[str] = None,
|
| 58 |
+
) -> T5PromptEncoder:
|
| 59 |
+
"""Cached accessor so repeated calls reuse a single loaded T5 encoder."""
|
| 60 |
+
key = (model_name, str(device), dtype, max_len, cache_dir)
|
| 61 |
+
if key not in _SINGLETON:
|
| 62 |
+
_SINGLETON[key] = T5PromptEncoder(model_name, device, dtype, max_len, cache_dir)
|
| 63 |
+
return _SINGLETON[key]
|
rti/model.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Region Token Interface (RTI): a frozen MiniT2I backbone whose middle blocks run
|
| 2 |
+
on R region tokens instead of all N patches."""
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import math
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn as nn
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
|
| 12 |
+
from .minit2i import (
|
| 13 |
+
rotate_half, RMSNorm, SwiGLUMlp, TextRotaryEmbedding1D,
|
| 14 |
+
get_2d_sincos_pos_embed, MiniT2I_models, MiniT2I,
|
| 15 |
+
)
|
| 16 |
+
from .lora import LoRALinear
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _vision_rope_angles(side: int, head_dim: int, device, theta: float = 10000.0):
|
| 20 |
+
"""Per-token 2D rope PHASES [side*side, head_dim] (row-major)."""
|
| 21 |
+
half = head_dim // 2
|
| 22 |
+
q = half // 2
|
| 23 |
+
freqs = 1.0 / (theta ** (torch.arange(0, q, device=device, dtype=torch.float32) / q))
|
| 24 |
+
ys, xs = torch.meshgrid(torch.arange(side, device=device, dtype=torch.float32),
|
| 25 |
+
torch.arange(side, device=device, dtype=torch.float32), indexing="ij")
|
| 26 |
+
ay = ys.reshape(-1)[:, None] * freqs[None]
|
| 27 |
+
ax = xs.reshape(-1)[:, None] * freqs[None]
|
| 28 |
+
ang = torch.cat([ay, ax], dim=-1)
|
| 29 |
+
return torch.cat([ang, ang], dim=-1)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _vision_rope_tables(side: int, head_dim: int, device, dtype, theta: float = 10000.0):
|
| 33 |
+
ang = _vision_rope_angles(side, head_dim, device, theta)
|
| 34 |
+
return ang.cos().to(dtype), ang.sin().to(dtype)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _text_rope_tables(length: int, head_dim: int, device, dtype, theta: float = 10000.0):
|
| 38 |
+
half = head_dim // 2
|
| 39 |
+
freqs = 1.0 / (theta ** (torch.arange(0, half, device=device, dtype=torch.float32) / half))
|
| 40 |
+
ang = torch.arange(length, device=device, dtype=torch.float32)[:, None] * freqs[None]
|
| 41 |
+
ang = torch.cat([ang, ang], dim=-1)
|
| 42 |
+
return ang.cos().to(dtype), ang.sin().to(dtype)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _apply_img_rope(x, cos, sin):
|
| 46 |
+
"""x [B, N, H, hd]; cos/sin [B, N, hd] -> rotary-applied x."""
|
| 47 |
+
cos = cos.unsqueeze(2).to(x.dtype)
|
| 48 |
+
sin = sin.unsqueeze(2).to(x.dtype)
|
| 49 |
+
return x * cos + rotate_half(x) * sin
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
_HILBERT_CACHE: dict = {}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _hilbert_perm(side: int, device) -> torch.Tensor:
|
| 56 |
+
"""Row-major token indices in Hilbert-curve order for a side x side grid
|
| 57 |
+
(side = 2^n). perm[d] is the flat index of the d-th point on the curve."""
|
| 58 |
+
key = (side, str(device))
|
| 59 |
+
if key not in _HILBERT_CACHE:
|
| 60 |
+
idx = []
|
| 61 |
+
for d in range(side * side):
|
| 62 |
+
rx = ry = 0
|
| 63 |
+
x = y = 0
|
| 64 |
+
t = d
|
| 65 |
+
s = 1
|
| 66 |
+
while s < side:
|
| 67 |
+
rx = 1 & (t // 2)
|
| 68 |
+
ry = 1 & (t ^ rx)
|
| 69 |
+
if ry == 0:
|
| 70 |
+
if rx == 1:
|
| 71 |
+
x = s - 1 - x
|
| 72 |
+
y = s - 1 - y
|
| 73 |
+
x, y = y, x
|
| 74 |
+
x += s * rx
|
| 75 |
+
y += s * ry
|
| 76 |
+
t //= 4
|
| 77 |
+
s *= 2
|
| 78 |
+
idx.append(y * side + x)
|
| 79 |
+
_HILBERT_CACHE[key] = torch.tensor(idx, dtype=torch.long, device=device)
|
| 80 |
+
return _HILBERT_CACHE[key]
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _attn_fast(q, k, v, scale, key_bias=None):
|
| 84 |
+
"""SDPA/flash joint attention; q/k/v [B, L, H, d] -> [B, L, H, d]."""
|
| 85 |
+
qq, kk, vv = (t.transpose(1, 2) for t in (q, k, v))
|
| 86 |
+
mask = None
|
| 87 |
+
if key_bias is not None:
|
| 88 |
+
mask = key_bias[:, None, None, :].to(qq.dtype)
|
| 89 |
+
out = F.scaled_dot_product_attention(qq, kk, vv, attn_mask=mask, scale=scale)
|
| 90 |
+
return out.transpose(1, 2)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class RegionInterface(nn.Module):
|
| 94 |
+
"""Read: soft-pool each region's members into one token. Write: give each
|
| 95 |
+
member its own delta from its region's delta. Initialised to mean-pool and
|
| 96 |
+
broadcast."""
|
| 97 |
+
|
| 98 |
+
def __init__(self, dim, n_size=11):
|
| 99 |
+
super().__init__()
|
| 100 |
+
self.read_score = nn.Linear(dim, 1)
|
| 101 |
+
self.size_emb = nn.Embedding(n_size, dim)
|
| 102 |
+
self.write = nn.Linear(2 * dim, dim) # [member | region_delta] -> member delta
|
| 103 |
+
nn.init.zeros_(self.read_score.weight)
|
| 104 |
+
nn.init.zeros_(self.read_score.bias) # -> mean
|
| 105 |
+
nn.init.zeros_(self.size_emb.weight) # -> no-op
|
| 106 |
+
with torch.no_grad(): # write -> copy the region-delta half
|
| 107 |
+
self.write.weight.zero_()
|
| 108 |
+
self.write.weight[:, dim:].copy_(torch.eye(dim))
|
| 109 |
+
self.write.bias.zero_()
|
| 110 |
+
|
| 111 |
+
def pool(self, xc, lab, R, cos_c, sin_c):
|
| 112 |
+
"""xc [B,li,D] in curve order, lab [B,li] in [0,R) -> tokens [B,R,D] and
|
| 113 |
+
their rope, taken at the members' mean position. Runs in fp32."""
|
| 114 |
+
b, li, D = xc.shape
|
| 115 |
+
odt = xc.dtype
|
| 116 |
+
xf = xc.float()
|
| 117 |
+
w = self.read_score(xc).squeeze(-1).float() # [B,li]
|
| 118 |
+
cmax = xf.new_full((b, R), -1e9).scatter_reduce_(1, lab, w, reduce="amax", include_self=False)
|
| 119 |
+
e = (w - torch.gather(cmax, 1, lab)).exp()
|
| 120 |
+
denom = torch.gather(xf.new_zeros(b, R).scatter_add_(1, lab, e), 1, lab).clamp_min(1e-9)
|
| 121 |
+
wsoft = (e / denom).unsqueeze(-1) # [B,li,1] segment-softmax
|
| 122 |
+
idxD = lab.unsqueeze(-1).expand(-1, -1, D)
|
| 123 |
+
cell = xf.new_zeros(b, R, D).scatter_add_(1, idxD, xf * wsoft)
|
| 124 |
+
cnt = xf.new_zeros(b, R).scatter_add_(1, lab, torch.ones_like(w))
|
| 125 |
+
sz = self.size_emb(cnt.clamp_min(1).log2().long()
|
| 126 |
+
.clamp_max(self.size_emb.num_embeddings - 1)).float()
|
| 127 |
+
cell = cell + sz
|
| 128 |
+
hd = cos_c.shape[-1]
|
| 129 |
+
cch = lab.unsqueeze(-1).expand(-1, -1, hd)
|
| 130 |
+
cden = cnt.clamp_min(1).unsqueeze(-1)
|
| 131 |
+
cos_cell = xf.new_zeros(b, R, hd).scatter_add_(1, cch, cos_c.float()) / cden
|
| 132 |
+
sin_cell = xf.new_zeros(b, R, hd).scatter_add_(1, cch, sin_c.float()) / cden
|
| 133 |
+
return cell.to(odt), cos_cell.to(odt), sin_cell.to(odt)
|
| 134 |
+
|
| 135 |
+
def unpool(self, xc, cell_delta, lab):
|
| 136 |
+
"""cell_delta [B,R,D] -> per-member delta [B,li,D]."""
|
| 137 |
+
b, li, D = xc.shape
|
| 138 |
+
cell_per_tok = torch.gather(cell_delta, 1, lab.unsqueeze(-1).expand(-1, -1, D))
|
| 139 |
+
return self.write(torch.cat([xc, cell_per_tok], dim=-1))
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class DoubleBlock(nn.Module):
|
| 143 |
+
"""MMDiT double-stream block; same parameters as the MiniT2I backbone."""
|
| 144 |
+
|
| 145 |
+
def __init__(self, hidden, txt_hidden, num_heads, head_dim, mlp_ratio):
|
| 146 |
+
super().__init__()
|
| 147 |
+
self.num_heads, self.head_dim = num_heads, head_dim
|
| 148 |
+
self.scale = head_dim ** -0.5
|
| 149 |
+
inner = num_heads * head_dim
|
| 150 |
+
self.img_norm1 = RMSNorm(hidden)
|
| 151 |
+
self.img_norm2 = RMSNorm(hidden)
|
| 152 |
+
self.txt_norm1 = RMSNorm(txt_hidden)
|
| 153 |
+
self.txt_norm2 = RMSNorm(txt_hidden)
|
| 154 |
+
self.img_qkv = nn.Linear(hidden, inner * 3)
|
| 155 |
+
self.txt_qkv = nn.Linear(txt_hidden, inner * 3)
|
| 156 |
+
self.q_norm = RMSNorm(head_dim)
|
| 157 |
+
self.k_norm = RMSNorm(head_dim)
|
| 158 |
+
self.text_rope = TextRotaryEmbedding1D(head_dim)
|
| 159 |
+
self.img_attn_proj = nn.Linear(inner, hidden)
|
| 160 |
+
self.txt_attn_proj = nn.Linear(inner, txt_hidden)
|
| 161 |
+
self.img_mlp = SwiGLUMlp(hidden, int(hidden * mlp_ratio))
|
| 162 |
+
self.txt_mlp = SwiGLUMlp(txt_hidden, int(txt_hidden * mlp_ratio))
|
| 163 |
+
|
| 164 |
+
def _qkv(self, x_norm, qkv, N):
|
| 165 |
+
b = x_norm.shape[0]
|
| 166 |
+
t = qkv(x_norm).reshape(b, N, 3, self.num_heads, self.head_dim)
|
| 167 |
+
return t[:, :, 0], t[:, :, 1], t[:, :, 2]
|
| 168 |
+
|
| 169 |
+
def forward(self, x, txt, cos_full, sin_full):
|
| 170 |
+
b, li, _ = x.shape
|
| 171 |
+
lt = txt.shape[1]
|
| 172 |
+
q_i, k_i, v_i = self._qkv(self.img_norm1(x), self.img_qkv, li)
|
| 173 |
+
q_t, k_t, v_t = self._qkv(self.txt_norm1(txt), self.txt_qkv, lt)
|
| 174 |
+
q_i, k_i = self.q_norm(q_i), self.k_norm(k_i)
|
| 175 |
+
q_t, k_t = self.q_norm(q_t), self.k_norm(k_t)
|
| 176 |
+
q_t, k_t = self.text_rope(q_t), self.text_rope(k_t)
|
| 177 |
+
cos, sin = cos_full[None].expand(b, -1, -1), sin_full[None].expand(b, -1, -1)
|
| 178 |
+
q_i, k_i = _apply_img_rope(q_i, cos, sin), _apply_img_rope(k_i, cos, sin)
|
| 179 |
+
q = torch.cat([q_t, q_i], dim=1)
|
| 180 |
+
k = torch.cat([k_t, k_i], dim=1)
|
| 181 |
+
v = torch.cat([v_t, v_i], dim=1)
|
| 182 |
+
out = _attn_fast(q, k, v, self.scale)
|
| 183 |
+
x = x + self.img_attn_proj(out[:, lt:].reshape(b, li, -1))
|
| 184 |
+
txt = txt + self.txt_attn_proj(out[:, :lt].reshape(b, lt, -1))
|
| 185 |
+
x = x + self.img_mlp(self.img_norm2(x))
|
| 186 |
+
txt = txt + self.txt_mlp(self.txt_norm2(txt))
|
| 187 |
+
return x, txt
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
class RTIMiniT2I(nn.Module):
|
| 191 |
+
"""MiniT2I with the Region Token Interface over blocks `span[0]..span[1]`."""
|
| 192 |
+
|
| 193 |
+
def __init__(
|
| 194 |
+
self,
|
| 195 |
+
base: MiniT2I,
|
| 196 |
+
*,
|
| 197 |
+
span: tuple[int, int] = (3, 13),
|
| 198 |
+
capacity: float = 0.125,
|
| 199 |
+
lora_rank: int = 32,
|
| 200 |
+
lora_alpha: Optional[float] = None,
|
| 201 |
+
lora_dropout: float = 0.0,
|
| 202 |
+
):
|
| 203 |
+
super().__init__()
|
| 204 |
+
net = base.net
|
| 205 |
+
cfg = net.cfg
|
| 206 |
+
self.cfg = cfg
|
| 207 |
+
self.span = tuple(span)
|
| 208 |
+
self.num_heads, self.head_dim = cfg.num_heads, cfg.head_dim
|
| 209 |
+
self.side = net.latent_img_size
|
| 210 |
+
self.patch_size = cfg.patch_size
|
| 211 |
+
self.in_channels = cfg.in_channels
|
| 212 |
+
self.hidden = cfg.hidden_size
|
| 213 |
+
self.capacity = float(capacity)
|
| 214 |
+
self.span_R: Optional[int] = None # explicit budget; else capacity*N
|
| 215 |
+
|
| 216 |
+
self.img_embedder = net.img_embedder
|
| 217 |
+
self.txt_embedder = net.txt_embedder
|
| 218 |
+
self.mask_token = net.mask_token
|
| 219 |
+
self.t_embedder = net.t_embedder
|
| 220 |
+
self.pooled_embedder = net.pooled_embedder
|
| 221 |
+
self.txt_preamble_blocks = net.txt_preamble_blocks
|
| 222 |
+
self.final_layer = net.final_layer
|
| 223 |
+
|
| 224 |
+
self.blocks = nn.ModuleList()
|
| 225 |
+
for i in range(cfg.depth_double):
|
| 226 |
+
blk = DoubleBlock(cfg.hidden_size, cfg.txt_hidden_size, cfg.num_heads,
|
| 227 |
+
cfg.head_dim, cfg.mlp_ratio)
|
| 228 |
+
miss, unexp = blk.load_state_dict(net.double_blocks[i].state_dict(), strict=False)
|
| 229 |
+
assert not unexp, f"block[{i}] init unexpected keys {unexp}"
|
| 230 |
+
self.blocks.append(blk)
|
| 231 |
+
|
| 232 |
+
self.iface = RegionInterface(cfg.hidden_size)
|
| 233 |
+
self._setup_lora_and_freeze(lora_rank, lora_alpha, lora_dropout)
|
| 234 |
+
for p in self.iface.parameters():
|
| 235 |
+
p.requires_grad = True
|
| 236 |
+
|
| 237 |
+
def _setup_lora_and_freeze(self, rank, alpha, dropout):
|
| 238 |
+
for p in self.parameters():
|
| 239 |
+
p.requires_grad = False
|
| 240 |
+
|
| 241 |
+
def _wrap(blk):
|
| 242 |
+
for name in ("img_qkv", "txt_qkv", "img_attn_proj", "txt_attn_proj"):
|
| 243 |
+
setattr(blk, name, LoRALinear(getattr(blk, name), rank=rank, alpha=alpha, dropout=dropout))
|
| 244 |
+
for mlp in (blk.img_mlp, blk.txt_mlp):
|
| 245 |
+
mlp.w1 = LoRALinear(mlp.w1, rank=rank, alpha=alpha, dropout=dropout)
|
| 246 |
+
mlp.w2 = LoRALinear(mlp.w2, rank=rank, alpha=alpha, dropout=dropout)
|
| 247 |
+
mlp.w3 = LoRALinear(mlp.w3, rank=rank, alpha=alpha, dropout=dropout)
|
| 248 |
+
|
| 249 |
+
for blk in self.blocks:
|
| 250 |
+
_wrap(blk)
|
| 251 |
+
for blk in self.txt_preamble_blocks: # text preamble also LoRA
|
| 252 |
+
blk.qkv = LoRALinear(blk.qkv, rank=rank, alpha=alpha, dropout=dropout)
|
| 253 |
+
blk.attn_proj = LoRALinear(blk.attn_proj, rank=rank, alpha=alpha, dropout=dropout)
|
| 254 |
+
for w in ("w1", "w2", "w3"):
|
| 255 |
+
setattr(blk.mlp, w, LoRALinear(getattr(blk.mlp, w), rank=rank, alpha=alpha, dropout=dropout))
|
| 256 |
+
self.final_layer.linear = LoRALinear(self.final_layer.linear, rank=rank, alpha=alpha, dropout=dropout)
|
| 257 |
+
|
| 258 |
+
def trainable_state(self):
|
| 259 |
+
"""LoRA and interface tensors, i.e. what a checkpoint stores."""
|
| 260 |
+
return {k: v for k, v in self.state_dict().items()
|
| 261 |
+
if ("lora_" in k) or k.startswith("iface.")}
|
| 262 |
+
|
| 263 |
+
@torch.no_grad()
|
| 264 |
+
def region_labels(self, x, R):
|
| 265 |
+
"""Region ids [B, li] in curve order: cut at the R-1 largest adjacent
|
| 266 |
+
feature distances along the curve."""
|
| 267 |
+
b, li, _ = x.shape
|
| 268 |
+
perm = _hilbert_perm(int(math.sqrt(li)), x.device)
|
| 269 |
+
xp = x[:, perm].float()
|
| 270 |
+
bs = (xp[:, 1:] - xp[:, :-1]).norm(dim=-1)
|
| 271 |
+
cuts = bs.topk(R - 1, dim=1).indices
|
| 272 |
+
st = torch.zeros(b, li, device=x.device)
|
| 273 |
+
st.scatter_(1, cuts + 1, 1.0)
|
| 274 |
+
return st.cumsum(dim=1).long()
|
| 275 |
+
|
| 276 |
+
@staticmethod
|
| 277 |
+
def _core_on_regions(blk, cell, txt, cos_cell, sin_cell):
|
| 278 |
+
"""Run one trunk block on the R region tokens, using the pooled rope."""
|
| 279 |
+
b, R, _ = cell.shape
|
| 280 |
+
lt = txt.shape[1]
|
| 281 |
+
q_i, k_i, v_i = blk._qkv(blk.img_norm1(cell), blk.img_qkv, R)
|
| 282 |
+
q_t, k_t, v_t = blk._qkv(blk.txt_norm1(txt), blk.txt_qkv, lt)
|
| 283 |
+
q_i, k_i = blk.q_norm(q_i), blk.k_norm(k_i)
|
| 284 |
+
q_t, k_t = blk.q_norm(q_t), blk.k_norm(k_t)
|
| 285 |
+
q_t, k_t = blk.text_rope(q_t), blk.text_rope(k_t)
|
| 286 |
+
q_i, k_i = _apply_img_rope(q_i, cos_cell, sin_cell), _apply_img_rope(k_i, cos_cell, sin_cell)
|
| 287 |
+
q = torch.cat([q_t, q_i], 1)
|
| 288 |
+
k = torch.cat([k_t, k_i], 1)
|
| 289 |
+
v = torch.cat([v_t, v_i], 1)
|
| 290 |
+
out = _attn_fast(q, k, v, blk.scale)
|
| 291 |
+
cell = cell + blk.img_attn_proj(out[:, lt:].reshape(b, R, -1))
|
| 292 |
+
txt = txt + blk.txt_attn_proj(out[:, :lt].reshape(b, lt, -1))
|
| 293 |
+
cell = cell + blk.img_mlp(blk.img_norm2(cell))
|
| 294 |
+
txt = txt + blk.txt_mlp(blk.txt_norm2(txt))
|
| 295 |
+
return cell, txt
|
| 296 |
+
|
| 297 |
+
def _decode(self, x, txt):
|
| 298 |
+
combined = torch.cat([txt, x], dim=1)
|
| 299 |
+
out = self.final_layer(combined)
|
| 300 |
+
img_out = out[:, txt.shape[1]:, :]
|
| 301 |
+
b, p, c = img_out.shape[0], self.patch_size, self.in_channels
|
| 302 |
+
h = w = int(math.sqrt(img_out.shape[1]))
|
| 303 |
+
img_out = img_out.reshape(b, h, w, p, p, c)
|
| 304 |
+
img_out = torch.einsum("nhwpqc->nchpwq", img_out)
|
| 305 |
+
return img_out.reshape(b, c, h * p, w * p)
|
| 306 |
+
|
| 307 |
+
def forward_xpred(self, img, t, context, mask, *, capacity_override: Optional[float] = None,
|
| 308 |
+
budget: Optional[int] = None):
|
| 309 |
+
"""Predict the clean image. `budget` (or `capacity_override`) sets R."""
|
| 310 |
+
if img.ndim == 4 and img.shape[1] != self.in_channels:
|
| 311 |
+
img = img.permute(0, 3, 1, 2)
|
| 312 |
+
mask = mask.to(device=context.device)
|
| 313 |
+
context = torch.where(mask[:, :, None] > 0.5, context, self.mask_token.to(dtype=context.dtype))
|
| 314 |
+
x = self.img_embedder(img)
|
| 315 |
+
pos = get_2d_sincos_pos_embed(self.hidden, self.side, x.device, x.dtype)
|
| 316 |
+
x = x + pos[None]
|
| 317 |
+
txt = self.txt_embedder(context.to(dtype=self.txt_embedder.weight.dtype))
|
| 318 |
+
for blk in self.txt_preamble_blocks:
|
| 319 |
+
txt = blk(txt)
|
| 320 |
+
cos_full, sin_full = _vision_rope_tables(self.side, self.head_dim, x.device, x.dtype)
|
| 321 |
+
|
| 322 |
+
b, li, _ = x.shape
|
| 323 |
+
s0, s1 = self.span
|
| 324 |
+
if budget is not None:
|
| 325 |
+
R = int(budget)
|
| 326 |
+
elif self.span_R is not None:
|
| 327 |
+
R = int(self.span_R)
|
| 328 |
+
else:
|
| 329 |
+
cap = capacity_override if capacity_override is not None else self.capacity
|
| 330 |
+
R = int(round(cap * li))
|
| 331 |
+
R = max(1, min(R, li))
|
| 332 |
+
|
| 333 |
+
if R >= li: # identity budget -> dense
|
| 334 |
+
for blk in self.blocks:
|
| 335 |
+
x, txt = blk(x, txt, cos_full, sin_full)
|
| 336 |
+
self.last_labels = None
|
| 337 |
+
return self._decode(x, txt)
|
| 338 |
+
|
| 339 |
+
for i in range(s0): # prelude, dense
|
| 340 |
+
x, txt = self.blocks[i](x, txt, cos_full, sin_full)
|
| 341 |
+
|
| 342 |
+
# Read
|
| 343 |
+
perm = _hilbert_perm(int(math.sqrt(li)), x.device)
|
| 344 |
+
lab = self.region_labels(x, R)
|
| 345 |
+
xc = x[:, perm]
|
| 346 |
+
cos_c = cos_full[perm][None].expand(b, -1, -1)
|
| 347 |
+
sin_c = sin_full[perm][None].expand(b, -1, -1)
|
| 348 |
+
cell, cos_cell, sin_cell = self.iface.pool(xc, lab, R, cos_c, sin_c)
|
| 349 |
+
cell_in = cell
|
| 350 |
+
self.last_labels = lab.detach()
|
| 351 |
+
|
| 352 |
+
# core on the R region tokens
|
| 353 |
+
for i in range(s0, s1 + 1):
|
| 354 |
+
cell, txt = self._core_on_regions(self.blocks[i], cell, txt, cos_cell, sin_cell)
|
| 355 |
+
|
| 356 |
+
# Write
|
| 357 |
+
x = x.index_copy(1, perm, xc + self.iface.unpool(xc, cell - cell_in, lab))
|
| 358 |
+
|
| 359 |
+
for i in range(s1 + 1, len(self.blocks)): # coda, dense
|
| 360 |
+
x, txt = self.blocks[i](x, txt, cos_full, sin_full)
|
| 361 |
+
return self._decode(x, txt)
|
| 362 |
+
|
| 363 |
+
def forward(self, img, t, context, mask, **kw):
|
| 364 |
+
return self.forward_xpred(img, t, context, mask, **kw)
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def build_rti(variant: str = "MiniT2I-B/16", *, span: Optional[tuple[int, int]] = None,
|
| 368 |
+
capacity: float = 0.125, lora_rank: int = 32, lora_alpha=None,
|
| 369 |
+
lora_dropout: float = 0.0, pretrained: bool = True, cache_dir=None,
|
| 370 |
+
ckpt: Optional[str] = None, device="cpu", dtype=None):
|
| 371 |
+
"""Build RTI on a pretrained MiniT2I backbone and optionally load an adapter
|
| 372 |
+
checkpoint. `span` and `lora_rank` are read from the checkpoint when present."""
|
| 373 |
+
meta = {}
|
| 374 |
+
if ckpt is not None:
|
| 375 |
+
_raw = torch.load(ckpt, map_location="cpu", weights_only=False)
|
| 376 |
+
meta = _raw.get("meta", {}) if isinstance(_raw, dict) else {}
|
| 377 |
+
del _raw
|
| 378 |
+
if span is None:
|
| 379 |
+
span = tuple(meta["span"]) if "span" in meta else ((3, 13) if "B/16" in variant else (3, 19))
|
| 380 |
+
if "lora_rank" in meta:
|
| 381 |
+
lora_rank = int(meta["lora_rank"])
|
| 382 |
+
if "variant" in meta and meta["variant"] != variant:
|
| 383 |
+
raise ValueError(f"checkpoint is for {meta['variant']}, but --variant is {variant}")
|
| 384 |
+
base = MiniT2I_models[variant]()
|
| 385 |
+
if pretrained:
|
| 386 |
+
from .minit2i import load_pretrained
|
| 387 |
+
load_pretrained(variant, base, cache_dir=cache_dir, device="cpu")
|
| 388 |
+
model = RTIMiniT2I(base, span=span, capacity=capacity, lora_rank=lora_rank,
|
| 389 |
+
lora_alpha=lora_alpha, lora_dropout=lora_dropout)
|
| 390 |
+
if ckpt is not None:
|
| 391 |
+
sd = torch.load(ckpt, map_location="cpu", weights_only=False)
|
| 392 |
+
sd = sd.get("state_dict") or sd.get("ema") or sd.get("model") or sd
|
| 393 |
+
sd = {k: v for k, v in sd.items() if not k.startswith("teacher_")}
|
| 394 |
+
# legacy key name
|
| 395 |
+
sd = {(k.replace("span_iface.", "iface.", 1) if k.startswith("span_iface.") else k): v
|
| 396 |
+
for k, v in sd.items()}
|
| 397 |
+
miss, unexp = model.load_state_dict(sd, strict=False)
|
| 398 |
+
assert not unexp, f"checkpoint has keys this model does not define: {unexp[:8]}"
|
| 399 |
+
n_loaded = len(sd)
|
| 400 |
+
print(f"[rti] loaded {n_loaded} adapter tensors from {ckpt}")
|
| 401 |
+
model = model.to(device)
|
| 402 |
+
if dtype is not None:
|
| 403 |
+
model = model.to(dtype)
|
| 404 |
+
return model
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
RTI_models = {
|
| 408 |
+
"RTI-B/16": lambda **kw: build_rti("MiniT2I-B/16", **kw),
|
| 409 |
+
"RTI-L/16": lambda **kw: build_rti("MiniT2I-L/16", **kw),
|
| 410 |
+
}
|
rti/sampling.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Euler flow-matching sampler with interval classifier-free guidance."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@torch.no_grad()
|
| 10 |
+
def sample(model, ctx, mask, *, budget: int, steps: int = 100, cfg_scale: float = 5.0,
|
| 11 |
+
image_size: int = 512, noise_scale: float = 2.0, t_eps: float = 0.001,
|
| 12 |
+
cfg_interval: tuple[float, float] = (0.0, 1.0), generator=None,
|
| 13 |
+
device: Optional[str] = None, dtype=torch.bfloat16, progress: bool = False):
|
| 14 |
+
"""model: an RTIMiniT2I. ctx, mask: encoded prompts [B,L,D] and [B,L].
|
| 15 |
+
budget: region-token budget R; R >= N runs dense.
|
| 16 |
+
Returns images in [0,1], [B,3,image_size,image_size], on CPU."""
|
| 17 |
+
device = device or next(model.parameters()).device
|
| 18 |
+
B = ctx.shape[0]
|
| 19 |
+
ctx = ctx.to(device, dtype)
|
| 20 |
+
mask = mask.to(device, dtype)
|
| 21 |
+
null = torch.zeros_like(mask)
|
| 22 |
+
x = torch.randn(B, 3, image_size, image_size, generator=generator,
|
| 23 |
+
device=device, dtype=dtype) * noise_scale
|
| 24 |
+
ts = torch.linspace(0.0, 1.0, steps + 1, device=device, dtype=dtype)
|
| 25 |
+
lo, hi = cfg_interval
|
| 26 |
+
it = range(steps)
|
| 27 |
+
if progress:
|
| 28 |
+
from tqdm.auto import tqdm
|
| 29 |
+
it = tqdm(it)
|
| 30 |
+
with torch.autocast(device.type if hasattr(device, "type") else "cuda", dtype=dtype):
|
| 31 |
+
for i in it:
|
| 32 |
+
t0 = float(ts[i])
|
| 33 |
+
tt = torch.full((2 * B,), t0, device=device, dtype=x.dtype)
|
| 34 |
+
x2 = torch.cat([x, x])
|
| 35 |
+
x0 = model.forward_xpred(x2, tt, torch.cat([ctx, ctx]),
|
| 36 |
+
torch.cat([mask, null]), budget=budget)
|
| 37 |
+
v = (x0 - x2) / max(1.0 - t0, t_eps)
|
| 38 |
+
v_cond, v_uncond = v.chunk(2, 0)
|
| 39 |
+
s = cfg_scale if lo <= t0 <= hi else 1.0
|
| 40 |
+
v = v_uncond + (v_cond - v_uncond) * s
|
| 41 |
+
x = x + (ts[i + 1] - ts[i]) * v
|
| 42 |
+
return ((x.float().clamp(-1, 1) + 1) / 2).cpu()
|