multimodalart HF Staff commited on
Commit
ccf7a99
·
verified ·
1 Parent(s): 6eff13b

Upload folder using huggingface_hub

Browse files
Files changed (9) hide show
  1. README.md +38 -6
  2. app.py +241 -0
  3. requirements.txt +10 -0
  4. rti/__init__.py +6 -0
  5. rti/lora.py +58 -0
  6. rti/minit2i.py +553 -0
  7. rti/minit2i_text.py +63 -0
  8. rti/model.py +410 -0
  9. rti/sampling.py +42 -0
README.md CHANGED
@@ -1,13 +1,45 @@
1
  ---
2
- title: Rti Pixel Diffusion
3
- emoji: 🌖
4
- colorFrom: red
5
  colorTo: purple
6
  sdk: gradio
7
  sdk_version: 6.30.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: 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()