WaveCut commited on
Commit
f085bc2
·
verified ·
1 Parent(s): d3c4524

Add SeFi Image ZeroGPU app

Browse files
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ .gradio/
4
+ outputs/
5
+ .env
6
+ sefi-cache/
LICENSE-SEFI-INFERENCE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SeFi-Image Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,13 +1,34 @@
1
  ---
2
- title: Sefi Image Zerogpu
3
- emoji: 🦀
4
- colorFrom: indigo
5
- colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.19.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: SeFi Image ZeroGPU
 
 
 
3
  sdk: gradio
4
  sdk_version: 6.19.0
 
5
  app_file: app.py
6
+ python_version: 3.12
7
+ license: cc-by-nc-4.0
8
+ models:
9
+ - SeFi-Image/SeFi-Image-1B-Base
10
+ - SeFi-Image/SeFi-Image-2B-Base
11
+ - SeFi-Image/SeFi-Image-5B-Base
12
+ - SeFi-Image/SeFi-Image-1B-turbo
13
+ - SeFi-Image/SeFi-Image-2B-turbo
14
+ - SeFi-Image/SeFi-Image-5B-turbo
15
  ---
16
 
17
+ # SeFi Image ZeroGPU
18
+
19
+ Public Gradio ZeroGPU Space for SeFi-Image 1B, 2B, and 5B Base/Turbo checkpoints.
20
+
21
+ The model defaults match the SeFi model cards:
22
+
23
+ | Family | Models | Steps | Guidance |
24
+ | --- | --- | ---: | ---: |
25
+ | Base | 1B, 2B, 5B | 50 | 4.0 |
26
+ | Turbo | 1B, 2B, 5B | 4 | 1.0 |
27
+
28
+ The SeFi checkpoints are gated under CC BY-NC 4.0. The Space uses an `HF_TOKEN`
29
+ secret to download model files, so the Space owner account must first be approved
30
+ for each selected SeFi model repository.
31
+
32
+ The vendored `sefi/` inference package comes from
33
+ `jmliu206/SeFi-Image` at commit `2f02744a187639ee41a296f8177cbbe7e5f333f5`
34
+ and is covered by `LICENSE-SEFI-INFERENCE`.
app.py ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import gc
4
+ import os
5
+ import random
6
+ import threading
7
+ import traceback
8
+ from dataclasses import dataclass
9
+
10
+ import gradio as gr
11
+ import spaces
12
+ import torch
13
+ from huggingface_hub import hf_hub_download
14
+
15
+ from sefi import SEFIInferencePipeline
16
+
17
+
18
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
19
+ os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
20
+
21
+ CACHE_DIR = os.getenv(
22
+ "SEFI_CACHE_DIR",
23
+ "/data/sefi-cache" if os.path.isdir("/data") else "/tmp/sefi-cache",
24
+ )
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class ModelPreset:
29
+ label: str
30
+ repo_id: str
31
+ family: str
32
+ steps: int
33
+ guidance: float
34
+
35
+
36
+ MODEL_PRESETS: dict[str, ModelPreset] = {
37
+ "1b-base": ModelPreset(
38
+ label="SeFi-Image 1B Base",
39
+ repo_id="SeFi-Image/SeFi-Image-1B-Base",
40
+ family="base",
41
+ steps=50,
42
+ guidance=4.0,
43
+ ),
44
+ "2b-base": ModelPreset(
45
+ label="SeFi-Image 2B Base",
46
+ repo_id="SeFi-Image/SeFi-Image-2B-Base",
47
+ family="base",
48
+ steps=50,
49
+ guidance=4.0,
50
+ ),
51
+ "5b-base": ModelPreset(
52
+ label="SeFi-Image 5B Base",
53
+ repo_id="SeFi-Image/SeFi-Image-5B-Base",
54
+ family="base",
55
+ steps=50,
56
+ guidance=4.0,
57
+ ),
58
+ "1b-turbo": ModelPreset(
59
+ label="SeFi-Image 1B Turbo",
60
+ repo_id="SeFi-Image/SeFi-Image-1B-turbo",
61
+ family="turbo",
62
+ steps=4,
63
+ guidance=1.0,
64
+ ),
65
+ "2b-turbo": ModelPreset(
66
+ label="SeFi-Image 2B Turbo",
67
+ repo_id="SeFi-Image/SeFi-Image-2B-turbo",
68
+ family="turbo",
69
+ steps=4,
70
+ guidance=1.0,
71
+ ),
72
+ "5b-turbo": ModelPreset(
73
+ label="SeFi-Image 5B Turbo",
74
+ repo_id="SeFi-Image/SeFi-Image-5B-turbo",
75
+ family="turbo",
76
+ steps=4,
77
+ guidance=1.0,
78
+ ),
79
+ }
80
+
81
+ DEFAULT_MODEL = "1b-turbo"
82
+ TURBO_STEPS = {4, 8, 10}
83
+
84
+ _MODEL_LOCK = threading.Lock()
85
+ _LOADED_MODEL_KEY: str | None = None
86
+ _LOADED_PIPE: SEFIInferencePipeline | None = None
87
+
88
+
89
+ def _hf_token() -> str | None:
90
+ token = os.getenv("HF_TOKEN")
91
+ return token.strip() if token and token.strip() else None
92
+
93
+
94
+ def _model_choices() -> list[tuple[str, str]]:
95
+ return [(preset.label, key) for key, preset in MODEL_PRESETS.items()]
96
+
97
+
98
+ def _torch_cleanup() -> None:
99
+ gc.collect()
100
+ if torch.cuda.is_available():
101
+ torch.cuda.empty_cache()
102
+ torch.cuda.ipc_collect()
103
+
104
+
105
+ def _clear_loaded_model() -> None:
106
+ global _LOADED_MODEL_KEY, _LOADED_PIPE
107
+ _LOADED_PIPE = None
108
+ _LOADED_MODEL_KEY = None
109
+ _torch_cleanup()
110
+
111
+
112
+ def _load_pipe(model_key: str) -> SEFIInferencePipeline:
113
+ global _LOADED_MODEL_KEY, _LOADED_PIPE
114
+
115
+ preset = MODEL_PRESETS[model_key]
116
+ with _MODEL_LOCK:
117
+ if _LOADED_PIPE is not None and _LOADED_MODEL_KEY == model_key:
118
+ return _LOADED_PIPE
119
+
120
+ _clear_loaded_model()
121
+ pipe = SEFIInferencePipeline.from_pretrained(
122
+ preset.repo_id,
123
+ cache_dir=CACHE_DIR,
124
+ device="cuda",
125
+ dtype="bf16",
126
+ )
127
+ _LOADED_MODEL_KEY = model_key
128
+ _LOADED_PIPE = pipe
129
+ return pipe
130
+
131
+
132
+ def _friendly_error(exc: BaseException, repo_id: str | None = None) -> str:
133
+ text = str(exc)
134
+ lowered = text.lower()
135
+ gated = (
136
+ "requires approval" in lowered
137
+ or "gated" in lowered
138
+ or "401" in lowered
139
+ or "403" in lowered
140
+ )
141
+ if gated:
142
+ repo_hint = f" for `{repo_id}`" if repo_id else ""
143
+ return (
144
+ f"Model access is not approved{repo_hint}. Open the model page while "
145
+ "logged in as the Space owner, accept the SeFi non-commercial gate, "
146
+ "and retry. The Space already has `HF_TOKEN` configured as a secret."
147
+ )
148
+
149
+ return f"{type(exc).__name__}: {text}"
150
+
151
+
152
+ def model_defaults(model_key: str):
153
+ preset = MODEL_PRESETS[model_key]
154
+ return (
155
+ gr.update(value=preset.steps),
156
+ gr.update(value=preset.guidance),
157
+ (
158
+ f"Selected `{preset.repo_id}`. Defaults: "
159
+ f"{preset.steps} steps, guidance {preset.guidance}."
160
+ ),
161
+ )
162
+
163
+
164
+ def check_access(model_key: str) -> str:
165
+ preset = MODEL_PRESETS[model_key]
166
+ try:
167
+ hf_hub_download(
168
+ repo_id=preset.repo_id,
169
+ filename="sefi_config.yaml",
170
+ cache_dir=CACHE_DIR,
171
+ token=_hf_token(),
172
+ )
173
+ except Exception as exc:
174
+ return _friendly_error(exc, preset.repo_id)
175
+
176
+ return f"Access OK for `{preset.repo_id}`."
177
+
178
+
179
+ def estimate_duration(
180
+ model_key: str,
181
+ prompt: str,
182
+ steps: int,
183
+ guidance_scale: float,
184
+ width: int,
185
+ height: int,
186
+ seed: int,
187
+ randomize_seed: bool,
188
+ ) -> int:
189
+ del prompt, guidance_scale, width, height, seed, randomize_seed
190
+ scale_seconds = {"1b": 120, "2b": 180, "5b": 240}
191
+ key_prefix = model_key.split("-", 1)[0]
192
+ load_budget = scale_seconds.get(key_prefix, 180)
193
+ step_budget = int(max(steps, 1)) * (10 if key_prefix == "5b" else 7)
194
+ return min(max(load_budget + step_budget, 120), 900)
195
+
196
+
197
+ @spaces.GPU(duration=estimate_duration)
198
+ def generate(
199
+ model_key: str,
200
+ prompt: str,
201
+ steps: int,
202
+ guidance_scale: float,
203
+ width: int,
204
+ height: int,
205
+ seed: int,
206
+ randomize_seed: bool,
207
+ ):
208
+ prompt = prompt.strip()
209
+ if not prompt:
210
+ return None, "Enter a prompt.", seed
211
+
212
+ preset = MODEL_PRESETS[model_key]
213
+ steps = int(steps)
214
+ guidance_scale = float(guidance_scale)
215
+ width = int(width)
216
+ height = int(height)
217
+
218
+ if preset.family == "turbo":
219
+ if steps not in TURBO_STEPS:
220
+ return (
221
+ None,
222
+ "Turbo checkpoints currently support 4, 8, or 10 denoising steps.",
223
+ seed,
224
+ )
225
+ if guidance_scale != 1.0:
226
+ return None, "Turbo checkpoints should use guidance 1.0.", seed
227
+
228
+ if randomize_seed:
229
+ seed = random.randint(0, 2**31 - 1)
230
+
231
+ try:
232
+ if torch.cuda.is_available():
233
+ torch.backends.cuda.matmul.allow_tf32 = True
234
+
235
+ pipe = _load_pipe(model_key)
236
+ images = pipe(
237
+ prompt,
238
+ num_inference_steps=steps,
239
+ guidance_scale=guidance_scale,
240
+ height=height,
241
+ width=width,
242
+ seed=int(seed),
243
+ )
244
+ except Exception as exc:
245
+ traceback.print_exc()
246
+ return None, _friendly_error(exc, preset.repo_id), seed
247
+
248
+ if not images:
249
+ return None, "Generation finished without an image.", seed
250
+
251
+ return (
252
+ images[0],
253
+ (
254
+ f"Generated with `{preset.repo_id}` at {width}x{height}, "
255
+ f"{steps} steps, guidance {guidance_scale}, seed {seed}."
256
+ ),
257
+ seed,
258
+ )
259
+
260
+
261
+ with gr.Blocks(title="SeFi Image ZeroGPU") as demo:
262
+ gr.Markdown("# SeFi Image ZeroGPU")
263
+
264
+ with gr.Row():
265
+ with gr.Column(scale=1, min_width=320):
266
+ model = gr.Dropdown(
267
+ label="Model",
268
+ choices=_model_choices(),
269
+ value=DEFAULT_MODEL,
270
+ interactive=True,
271
+ )
272
+ prompt = gr.Textbox(
273
+ label="Prompt",
274
+ value="A blue ceramic mug on a white desk.",
275
+ lines=4,
276
+ max_lines=8,
277
+ )
278
+ with gr.Row():
279
+ steps = gr.Slider(
280
+ minimum=1,
281
+ maximum=60,
282
+ step=1,
283
+ value=MODEL_PRESETS[DEFAULT_MODEL].steps,
284
+ label="Steps",
285
+ )
286
+ guidance = gr.Slider(
287
+ minimum=1.0,
288
+ maximum=8.0,
289
+ step=0.1,
290
+ value=MODEL_PRESETS[DEFAULT_MODEL].guidance,
291
+ label="Guidance",
292
+ )
293
+ with gr.Row():
294
+ width = gr.Slider(
295
+ minimum=512,
296
+ maximum=1536,
297
+ step=16,
298
+ value=1024,
299
+ label="Width",
300
+ )
301
+ height = gr.Slider(
302
+ minimum=512,
303
+ maximum=1536,
304
+ step=16,
305
+ value=1024,
306
+ label="Height",
307
+ )
308
+ with gr.Row():
309
+ seed = gr.Number(
310
+ label="Seed",
311
+ value=42,
312
+ precision=0,
313
+ minimum=0,
314
+ maximum=2**31 - 1,
315
+ )
316
+ randomize_seed = gr.Checkbox(label="Randomize", value=False)
317
+ with gr.Row():
318
+ run = gr.Button("Generate", variant="primary")
319
+ access = gr.Button("Check Access")
320
+
321
+ with gr.Column(scale=1, min_width=360):
322
+ image = gr.Image(label="Image", type="pil", format="png")
323
+ status = gr.Markdown(
324
+ (
325
+ f"Selected `{MODEL_PRESETS[DEFAULT_MODEL].repo_id}`. Defaults: "
326
+ f"{MODEL_PRESETS[DEFAULT_MODEL].steps} steps, "
327
+ f"guidance {MODEL_PRESETS[DEFAULT_MODEL].guidance}."
328
+ )
329
+ )
330
+
331
+ model.change(model_defaults, inputs=model, outputs=[steps, guidance, status])
332
+ access.click(check_access, inputs=model, outputs=status)
333
+ run.click(
334
+ generate,
335
+ inputs=[
336
+ model,
337
+ prompt,
338
+ steps,
339
+ guidance,
340
+ width,
341
+ height,
342
+ seed,
343
+ randomize_seed,
344
+ ],
345
+ outputs=[image, status, seed],
346
+ api_name="generate",
347
+ concurrency_limit=1,
348
+ )
349
+
350
+ demo.queue(default_concurrency_limit=1)
351
+
352
+ if __name__ == "__main__":
353
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio==6.19.0
2
+ spaces>=0.50.0
3
+ torch>=2.9.1
4
+ torchvision>=0.24.0
5
+ diffusers>=0.39.0
6
+ transformers>=5.13.0
7
+ accelerate>=1.12.0
8
+ safetensors>=0.7.0
9
+ huggingface_hub[hf_xet]>=1.22.0
10
+ hf-xet>=1.2.0
11
+ omegaconf>=2.3.0
12
+ pillow>=12.0.0
sefi/README.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SeFi Python Package
2
+
3
+ Reusable Python package for SeFi-Image inference. See `../README.md` for
4
+ installation, model checkpoints, and generation examples.
5
+
6
+ The package includes:
7
+
8
+ - checkpoint-derived model metadata
9
+ - checkpoint staging
10
+ - pipeline wrapper
11
+ - prompt/output helpers
12
+ - command-line interface
13
+
14
+ Weights and model-specific config are loaded from a local checkpoint artifact or
15
+ Hugging Face repo id passed through `--checkpoint` or
16
+ `SEFIInferencePipeline.from_pretrained(...)`. The artifact root should include
17
+ `sefi_config.yaml`.
sefi/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ """SEFI text-to-image inference package."""
2
+
3
+ from .pipeline import SEFIInferencePipeline
4
+ from .registry import ModelSpec, infer_model_spec
5
+
6
+ __all__ = [
7
+ "ModelSpec",
8
+ "SEFIInferencePipeline",
9
+ "infer_model_spec",
10
+ ]
sefi/builder.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inference-only component builder for SEFI models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from dataclasses import dataclass
8
+
9
+ import torch
10
+
11
+ from .modeling import (
12
+ Flux2SEFITransformer2DModel,
13
+ Qwen3VLTextEncoder,
14
+ TextureLatentCodec,
15
+ build_texture_vae,
16
+ )
17
+
18
+
19
+ SEFI_SCALE_PRESETS = {
20
+ "0p5b": {
21
+ "attention_head_dim": 128,
22
+ "num_attention_heads": 12,
23
+ "num_layers": 3,
24
+ "num_single_layers": 10,
25
+ "joint_attention_dim": 6144,
26
+ },
27
+ "1b": {
28
+ "attention_head_dim": 128,
29
+ "num_attention_heads": 16,
30
+ "num_layers": 4,
31
+ "num_single_layers": 12,
32
+ "joint_attention_dim": 6144,
33
+ },
34
+ "2b": {
35
+ "attention_head_dim": 128,
36
+ "num_attention_heads": 20,
37
+ "num_layers": 4,
38
+ "num_single_layers": 16,
39
+ "joint_attention_dim": 6144,
40
+ },
41
+ "3b": {
42
+ "attention_head_dim": 128,
43
+ "num_attention_heads": 22,
44
+ "num_layers": 5,
45
+ "num_single_layers": 18,
46
+ "joint_attention_dim": 7680,
47
+ },
48
+ "4b": {
49
+ "attention_head_dim": 128,
50
+ "num_attention_heads": 24,
51
+ "num_layers": 5,
52
+ "num_single_layers": 20,
53
+ "joint_attention_dim": 7680,
54
+ },
55
+ "5b": {
56
+ "attention_head_dim": 128,
57
+ "num_attention_heads": 26,
58
+ "num_layers": 6,
59
+ "num_single_layers": 21,
60
+ "joint_attention_dim": 7680,
61
+ },
62
+ "6b": {
63
+ "attention_head_dim": 128,
64
+ "num_attention_heads": 28,
65
+ "num_layers": 6,
66
+ "num_single_layers": 22,
67
+ "joint_attention_dim": 7680,
68
+ },
69
+ "8b": {
70
+ "attention_head_dim": 128,
71
+ "num_attention_heads": 30,
72
+ "num_layers": 7,
73
+ "num_single_layers": 24,
74
+ "joint_attention_dim": 7680,
75
+ },
76
+ "9b": {
77
+ "attention_head_dim": 128,
78
+ "num_attention_heads": 32,
79
+ "num_layers": 8,
80
+ "num_single_layers": 24,
81
+ "joint_attention_dim": 12288,
82
+ },
83
+ }
84
+
85
+ SEFI_MODEL_NAME_TO_SCALE = {
86
+ "flux2-klein-base-0p5b-sefi": "0p5b",
87
+ "flux2-klein-base-1b-sefi": "1b",
88
+ "flux2-klein-base-2b-sefi": "2b",
89
+ "flux2-klein-base-3b-sefi": "3b",
90
+ "flux2-klein-base-4b-sefi": "4b",
91
+ "flux2-klein-base-5b-sefi": "5b",
92
+ "flux2-klein-base-6b-sefi": "6b",
93
+ "flux2-klein-base-8b-sefi": "8b",
94
+ "flux2-klein-base-9b-sefi": "9b",
95
+ }
96
+
97
+ QWEN3VL_TEXT_HIDDEN_DIMS = {
98
+ "qwen3vl_2b": 2048,
99
+ "qwen3vl_4b": 2560,
100
+ "qwen3vl_8b": 4096,
101
+ }
102
+
103
+ @dataclass
104
+ class SEFIComponents:
105
+ transformer: torch.nn.Module
106
+ text_encoder: torch.nn.Module
107
+ texture_codec: torch.nn.Module
108
+ noise_scheduler: object
109
+ pipeline_cls: type
110
+ semantic_channels: int
111
+ texture_channels: int
112
+ total_channels: int
113
+
114
+
115
+ def _resolve_transformer_scale(config) -> str:
116
+ model_cfg = config.model
117
+ scale = str(model_cfg.get("transformer_scale", "")).strip().lower()
118
+ if scale:
119
+ if scale not in set(SEFI_SCALE_PRESETS) | {"custom"}:
120
+ raise ValueError(
121
+ "model.transformer_scale must be one of "
122
+ f"{list(SEFI_SCALE_PRESETS) + ['custom']}. Got: {scale}"
123
+ )
124
+ return scale
125
+
126
+ model_name = str(model_cfg.model_name)
127
+ try:
128
+ return SEFI_MODEL_NAME_TO_SCALE[model_name]
129
+ except KeyError as exc:
130
+ raise ValueError(
131
+ f"Unsupported SEFI model.model_name: {model_name}. "
132
+ f"Expected one of {sorted(SEFI_MODEL_NAME_TO_SCALE)}."
133
+ ) from exc
134
+
135
+
136
+ def _derive_semantic_channels(config) -> int:
137
+ value = config.model.get("semantic_channels", None)
138
+ if value is None:
139
+ raise ValueError("Config requires model.semantic_channels for inference.")
140
+ return int(value)
141
+
142
+
143
+ def _texture_vae_config_path(texture_vae_cfg) -> str:
144
+ name = str(texture_vae_cfg.get("name", "")).strip().lower()
145
+ base_path = str(texture_vae_cfg.get("base_path", "")).strip()
146
+ if not base_path:
147
+ raise ValueError("model.texture_vae.base_path is required.")
148
+ if name == "sd1.5":
149
+ return os.path.join(base_path, "config.json")
150
+ if name in {"flux1", "flux2"}:
151
+ return os.path.join(base_path, "vae", "config.json")
152
+ raise ValueError(
153
+ f"Unsupported model.texture_vae.name: {name}. "
154
+ "Expected one of ['sd1.5', 'flux1', 'flux2']."
155
+ )
156
+
157
+
158
+ def _derive_texture_channels(config) -> int:
159
+ config_path = _texture_vae_config_path(config.model.texture_vae)
160
+ if not os.path.isfile(config_path):
161
+ raise FileNotFoundError(f"Texture VAE config not found: {config_path}")
162
+ with open(config_path, "r", encoding="utf-8") as handle:
163
+ texture_vae_config = json.load(handle)
164
+ latent_channels = texture_vae_config.get("latent_channels", None)
165
+ if latent_channels is None:
166
+ raise ValueError(f"Texture VAE config must contain latent_channels: {config_path}")
167
+ return int(latent_channels) * 4
168
+
169
+
170
+ def _derive_text_output_dim(config) -> int:
171
+ text_cfg = config.model.text_encoder
172
+ model_name = str(text_cfg.model_name)
173
+ if model_name not in QWEN3VL_TEXT_HIDDEN_DIMS:
174
+ raise ValueError(
175
+ f"Unsupported SEFI text_encoder.model_name: {model_name}. "
176
+ f"Expected one of {sorted(QWEN3VL_TEXT_HIDDEN_DIMS)}."
177
+ )
178
+ hidden_layers = tuple(int(x) for x in text_cfg.hidden_layers)
179
+ return int(QWEN3VL_TEXT_HIDDEN_DIMS[model_name]) * len(hidden_layers)
180
+
181
+
182
+ def text_encoder_signature(config) -> tuple:
183
+ text_cfg = config.model.text_encoder
184
+ return (
185
+ str(text_cfg.model_name),
186
+ str(text_cfg.get("weights_root", "outputs/model_weights")),
187
+ int(text_cfg.max_length),
188
+ tuple(int(x) for x in text_cfg.hidden_layers),
189
+ )
190
+
191
+
192
+ def build_transformer_config(config, *, total_channels: int, text_output_dim: int) -> dict:
193
+ from diffusers import Flux2Transformer2DModel
194
+
195
+ model_cfg = config.model
196
+ transformer_cfg_path = str(model_cfg.assets.transformer_config_path)
197
+ transformer_cfg = Flux2Transformer2DModel.load_config(
198
+ transformer_cfg_path,
199
+ subfolder="transformer",
200
+ local_files_only=True,
201
+ )
202
+ transformer_cfg = dict(transformer_cfg)
203
+
204
+ transformer_scale = _resolve_transformer_scale(config)
205
+ if transformer_scale == "custom":
206
+ overrides = model_cfg.get("transformer_overrides", {})
207
+ required_keys = (
208
+ "attention_head_dim",
209
+ "num_attention_heads",
210
+ "num_layers",
211
+ "num_single_layers",
212
+ "joint_attention_dim",
213
+ )
214
+ missing = [key for key in required_keys if key not in overrides]
215
+ if missing:
216
+ raise ValueError(
217
+ "model.transformer_overrides is missing required keys for custom "
218
+ f"SEFI model: {missing}"
219
+ )
220
+ for key in required_keys:
221
+ transformer_cfg[key] = int(overrides[key])
222
+ if "mlp_ratio" in overrides:
223
+ transformer_cfg["mlp_ratio"] = float(overrides["mlp_ratio"])
224
+ else:
225
+ transformer_cfg.update(SEFI_SCALE_PRESETS[transformer_scale])
226
+
227
+ joint_attention_dim = int(transformer_cfg["joint_attention_dim"])
228
+ if joint_attention_dim != int(text_output_dim):
229
+ raise ValueError(
230
+ "Text dimension mismatch: "
231
+ f"text_encoder output_dim={text_output_dim}, "
232
+ f"transformer joint_attention_dim={joint_attention_dim}."
233
+ )
234
+
235
+ transformer_cfg["in_channels"] = int(total_channels)
236
+ transformer_cfg["out_channels"] = int(total_channels)
237
+ transformer_cfg["guidance_embeds"] = False
238
+ return transformer_cfg
239
+
240
+
241
+ def build_lightweight_transformer(config, *, total_channels: int, text_output_dim: int):
242
+ transformer_cfg = build_transformer_config(
243
+ config,
244
+ total_channels=total_channels,
245
+ text_output_dim=text_output_dim,
246
+ )
247
+ return Flux2SEFITransformer2DModel(
248
+ backbone_config=transformer_cfg,
249
+ text_input_dim=int(text_output_dim),
250
+ )
251
+
252
+
253
+ def build_components(config, *, component_dtype: torch.dtype) -> SEFIComponents:
254
+ from diffusers import FlowMatchEulerDiscreteScheduler, Flux2KleinPipeline
255
+
256
+ model_cfg = config.model
257
+
258
+ texture_vae = build_texture_vae(
259
+ model_cfg.texture_vae,
260
+ torch_dtype=component_dtype,
261
+ )
262
+ texture_codec = TextureLatentCodec(
263
+ texture_vae=texture_vae,
264
+ texture_vae_name=str(model_cfg.texture_vae.name),
265
+ )
266
+
267
+ noise_scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
268
+ str(model_cfg.assets.scheduler_path),
269
+ subfolder="scheduler",
270
+ local_files_only=True,
271
+ )
272
+
273
+ semantic_channels = _derive_semantic_channels(config)
274
+ texture_channels = int(texture_codec.texture_channels)
275
+ total_channels = int(semantic_channels + texture_channels)
276
+
277
+ text_cfg = model_cfg.text_encoder
278
+ text_encoder = Qwen3VLTextEncoder(
279
+ model_name=str(text_cfg.model_name),
280
+ weights_root=str(text_cfg.get("weights_root", "outputs/model_weights")),
281
+ max_length=int(text_cfg.max_length),
282
+ hidden_layers=[int(x) for x in text_cfg.hidden_layers],
283
+ torch_dtype=component_dtype,
284
+ )
285
+
286
+ transformer = build_lightweight_transformer(
287
+ config,
288
+ total_channels=total_channels,
289
+ text_output_dim=int(text_encoder.output_dim),
290
+ )
291
+
292
+ return SEFIComponents(
293
+ transformer=transformer,
294
+ text_encoder=text_encoder,
295
+ texture_codec=texture_codec,
296
+ noise_scheduler=noise_scheduler,
297
+ pipeline_cls=Flux2KleinPipeline,
298
+ semantic_channels=semantic_channels,
299
+ texture_channels=texture_channels,
300
+ total_channels=total_channels,
301
+ )
sefi/checkpoints.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Checkpoint staging helpers for SEFI inference."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ CONFIG_FILENAMES = ("sefi_config.yaml", "config.yaml")
10
+
11
+
12
+ def _download_hf_snapshot(
13
+ repo_id: str,
14
+ *,
15
+ cache_dir: str | os.PathLike[str],
16
+ ) -> str:
17
+ try:
18
+ from huggingface_hub import snapshot_download
19
+ except ImportError as exc:
20
+ raise RuntimeError(
21
+ "Checkpoint is not a local path. Install huggingface_hub or pass a "
22
+ "local --checkpoint path."
23
+ ) from exc
24
+
25
+ return snapshot_download(
26
+ repo_id=repo_id,
27
+ cache_dir=str(cache_dir),
28
+ local_files_only=False,
29
+ )
30
+
31
+
32
+ def checkpoint_root(path: str | os.PathLike[str]) -> Path:
33
+ resolved = Path(path).expanduser()
34
+ return resolved if resolved.is_dir() else resolved.parent
35
+
36
+
37
+ def resolve_config_path(
38
+ checkpoint_path: str | os.PathLike[str],
39
+ config_path: str | os.PathLike[str] | None = None,
40
+ ) -> str:
41
+ root = checkpoint_root(checkpoint_path)
42
+
43
+ if config_path:
44
+ candidate = Path(config_path).expanduser()
45
+ if not candidate.is_absolute():
46
+ rooted = root / candidate
47
+ if rooted.is_file():
48
+ return str(rooted)
49
+ if candidate.is_file():
50
+ return str(candidate)
51
+ raise FileNotFoundError(f"Config file not found: {config_path}")
52
+
53
+ for filename in CONFIG_FILENAMES:
54
+ candidate = root / filename
55
+ if candidate.is_file():
56
+ return str(candidate)
57
+
58
+ expected = ", ".join(CONFIG_FILENAMES)
59
+ raise FileNotFoundError(
60
+ f"SEFI config not found under checkpoint root {root}. "
61
+ f"Expected one of: {expected}. Use --config to override."
62
+ )
63
+
64
+
65
+ def ensure_local_path(
66
+ checkpoint: str,
67
+ *,
68
+ cache_dir: str | os.PathLike[str],
69
+ ) -> str:
70
+ if not checkpoint:
71
+ raise ValueError(
72
+ "No checkpoint was provided. Pass a local path or Hugging Face repo id "
73
+ "with --checkpoint."
74
+ )
75
+
76
+ path = Path(checkpoint).expanduser()
77
+ if path.exists():
78
+ return str(path)
79
+
80
+ return _download_hf_snapshot(
81
+ checkpoint,
82
+ cache_dir=cache_dir,
83
+ )
84
+
85
+
86
+ def resolve_checkpoint_to_local(
87
+ *,
88
+ checkpoint: str,
89
+ cache_dir: str | os.PathLike[str],
90
+ ) -> tuple[str, str]:
91
+ local_path = ensure_local_path(
92
+ checkpoint,
93
+ cache_dir=cache_dir,
94
+ )
95
+ return local_path, checkpoint
sefi/cli.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Command line entry point for SeFi-Image inference."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from dataclasses import asdict
7
+
8
+ from .distributed import (
9
+ build_rank_generator,
10
+ setup_distributed,
11
+ shard_indices_interleaved,
12
+ wait_for_everyone,
13
+ )
14
+ from .io import expand_prompts, load_prompts, save_images, write_manifest
15
+ from .pipeline import SEFIInferencePipeline
16
+
17
+
18
+ def _parse_args() -> argparse.Namespace:
19
+ parser = argparse.ArgumentParser(description=__doc__)
20
+ parser.add_argument("--prompt", default="")
21
+ parser.add_argument("--prompt-file", default="")
22
+ parser.add_argument("--output-dir", default="outputs/inference")
23
+ parser.add_argument("--cache-dir", default="outputs/model_weights/sefi_inference")
24
+ parser.add_argument(
25
+ "--checkpoint",
26
+ required=True,
27
+ help="Local checkpoint path or Hugging Face repo id.",
28
+ )
29
+ parser.add_argument(
30
+ "--config",
31
+ default="",
32
+ help="Optional config path. Defaults to sefi_config.yaml under --checkpoint.",
33
+ )
34
+ parser.add_argument("--steps", type=int, default=None)
35
+ parser.add_argument("--guidance-scale", type=float, default=None)
36
+ parser.add_argument("--height", type=int, default=None)
37
+ parser.add_argument("--width", type=int, default=None)
38
+ parser.add_argument("--batch-size", type=int, default=1)
39
+ parser.add_argument("--num-images-per-prompt", type=int, default=1)
40
+ parser.add_argument("--seed", type=int, default=20260616)
41
+ parser.add_argument("--device", default="")
42
+ parser.add_argument("--dtype", choices=("bf16", "fp32"), default="")
43
+ parser.add_argument("--delta-t", type=float, default=None)
44
+ parser.add_argument("--timestep-shift-alpha", type=float, default=None)
45
+ parser.add_argument("--debug-assert-schedule", action="store_true")
46
+ parser.add_argument("--autoguidance-config", default="")
47
+ parser.add_argument("--autoguidance-checkpoint", default="")
48
+ parser.add_argument("--guidance-interval-sigma-lo", type=float, default=None)
49
+ parser.add_argument("--guidance-interval-sigma-hi", type=float, default=None)
50
+ return parser.parse_args()
51
+
52
+
53
+ def main() -> None:
54
+ args = _parse_args()
55
+ prompts = load_prompts(
56
+ prompt=args.prompt or None,
57
+ prompt_file=args.prompt_file or None,
58
+ )
59
+ items = expand_prompts(prompts, args.num_images_per_prompt)
60
+
61
+ rank, world_size, device, is_main, accelerator = setup_distributed()
62
+ local_indices = shard_indices_interleaved(len(items), rank, world_size)
63
+ local_items = [items[index] for index in local_indices]
64
+ local_prompts = [item.prompt for item in local_items]
65
+
66
+ pipe = SEFIInferencePipeline.from_pretrained(
67
+ args.checkpoint,
68
+ cache_dir=args.cache_dir,
69
+ config=args.config or None,
70
+ device=args.device or str(device),
71
+ dtype=args.dtype or None,
72
+ delta_t=args.delta_t,
73
+ timestep_shift_alpha=args.timestep_shift_alpha,
74
+ debug_assert_schedule=args.debug_assert_schedule,
75
+ autoguidance_config=args.autoguidance_config or None,
76
+ autoguidance_checkpoint=args.autoguidance_checkpoint or None,
77
+ guidance_interval_sigma_lo=args.guidance_interval_sigma_lo,
78
+ guidance_interval_sigma_hi=args.guidance_interval_sigma_hi,
79
+ )
80
+
81
+ generator = build_rank_generator(device, args.seed, rank)
82
+ images = pipe(
83
+ local_prompts,
84
+ num_inference_steps=args.steps,
85
+ guidance_scale=args.guidance_scale,
86
+ height=args.height,
87
+ width=args.width,
88
+ batch_size=args.batch_size,
89
+ generator=generator,
90
+ )
91
+ save_images(output_dir=args.output_dir, items=local_items, images=images, rank=rank)
92
+ wait_for_everyone(accelerator)
93
+
94
+ if is_main:
95
+ write_manifest(
96
+ args.output_dir,
97
+ {
98
+ "model": pipe.spec.name,
99
+ "model_spec": asdict(pipe.spec),
100
+ "checkpoint_path": pipe.checkpoint_path,
101
+ "checkpoint_uri": pipe.checkpoint_uri,
102
+ "num_prompts": len(prompts),
103
+ "num_images": len(items),
104
+ "seed": args.seed,
105
+ "world_size": world_size,
106
+ },
107
+ )
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()
sefi/config.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Config loading helpers for SEFI inference."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from omegaconf import OmegaConf
8
+
9
+
10
+ def _resolve_relative_path(value, base_dir: Path) -> str:
11
+ if value is None:
12
+ return value
13
+ raw = str(value).strip()
14
+ if not raw:
15
+ return raw
16
+ path = Path(raw).expanduser()
17
+ if path.is_absolute():
18
+ return str(path)
19
+ return str(base_dir / path)
20
+
21
+
22
+ def _patch_path(config, dotted_path: str, base_dir: Path) -> None:
23
+ parts = dotted_path.split(".")
24
+ node = config
25
+ for part in parts[:-1]:
26
+ if part not in node:
27
+ return
28
+ node = node[part]
29
+ leaf = parts[-1]
30
+ if leaf in node:
31
+ node[leaf] = _resolve_relative_path(node[leaf], base_dir)
32
+
33
+
34
+ def _resolve_artifact_paths(config, base_dir: Path):
35
+ for dotted_path in (
36
+ "model.assets.transformer_config_path",
37
+ "model.assets.scheduler_path",
38
+ "model.texture_vae.base_path",
39
+ "model.text_encoder.weights_root",
40
+ ):
41
+ _patch_path(config, dotted_path, base_dir)
42
+ return config
43
+
44
+
45
+ def load_config(config_path: str | Path):
46
+ path = Path(config_path).expanduser()
47
+ if not path.is_file():
48
+ raise FileNotFoundError(f"Config file not found: {path}")
49
+ config = OmegaConf.load(path)
50
+ config = _resolve_artifact_paths(config, path.parent)
51
+ OmegaConf.resolve(config)
52
+ return config
sefi/distributed.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Small distributed helpers for CLI inference."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+
7
+
8
+ def setup_distributed():
9
+ try:
10
+ from accelerate import Accelerator
11
+ except ModuleNotFoundError:
12
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
13
+ return 0, 1, device, True, None
14
+
15
+ accelerator = Accelerator()
16
+ return (
17
+ int(accelerator.process_index),
18
+ int(accelerator.num_processes),
19
+ accelerator.device,
20
+ bool(accelerator.is_main_process),
21
+ accelerator,
22
+ )
23
+
24
+
25
+ def wait_for_everyone(accelerator) -> None:
26
+ if accelerator is not None:
27
+ accelerator.wait_for_everyone()
28
+
29
+
30
+ def shard_indices_interleaved(total: int, rank: int, world_size: int) -> list[int]:
31
+ return list(range(int(rank), int(total), int(world_size)))
32
+
33
+
34
+ def build_rank_generator(device: torch.device, seed: int, rank: int) -> torch.Generator:
35
+ return torch.Generator(device=str(device)).manual_seed(int(seed) + int(rank))
sefi/io.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Prompt and output helpers for SEFI inference."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict, dataclass
7
+ from pathlib import Path
8
+ from typing import Iterable
9
+
10
+ from PIL import Image
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class GenerationItem:
15
+ index: int
16
+ prompt_index: int
17
+ repeat_index: int
18
+ prompt: str
19
+
20
+ @property
21
+ def file_stem(self) -> str:
22
+ if self.repeat_index == 0:
23
+ return f"{self.prompt_index:06d}"
24
+ return f"{self.prompt_index:06d}_{self.repeat_index:02d}"
25
+
26
+
27
+ def load_prompts(*, prompt: str | None, prompt_file: str | None) -> list[str]:
28
+ prompts: list[str] = []
29
+ if prompt:
30
+ prompts.append(prompt)
31
+ if prompt_file:
32
+ with open(prompt_file, "r", encoding="utf-8") as handle:
33
+ prompts.extend(line.strip() for line in handle if line.strip())
34
+ if not prompts:
35
+ raise ValueError("Provide --prompt or --prompt-file.")
36
+ return prompts
37
+
38
+
39
+ def expand_prompts(prompts: Iterable[str], num_images_per_prompt: int) -> list[GenerationItem]:
40
+ if num_images_per_prompt <= 0:
41
+ raise ValueError("num_images_per_prompt must be > 0.")
42
+
43
+ items: list[GenerationItem] = []
44
+ index = 0
45
+ for prompt_index, prompt in enumerate(prompts):
46
+ for repeat_index in range(num_images_per_prompt):
47
+ items.append(
48
+ GenerationItem(
49
+ index=index,
50
+ prompt_index=prompt_index,
51
+ repeat_index=repeat_index,
52
+ prompt=prompt,
53
+ )
54
+ )
55
+ index += 1
56
+ return items
57
+
58
+
59
+ def save_images(
60
+ *,
61
+ output_dir: str | Path,
62
+ items: list[GenerationItem],
63
+ images: list[Image.Image],
64
+ rank: int = 0,
65
+ ) -> None:
66
+ if len(items) != len(images):
67
+ raise ValueError(f"items/images length mismatch: {len(items)} != {len(images)}")
68
+
69
+ out = Path(output_dir)
70
+ out.mkdir(parents=True, exist_ok=True)
71
+ metadata_path = out / f"metadata_rank{rank:03d}.jsonl"
72
+ with metadata_path.open("a", encoding="utf-8") as meta:
73
+ for item, image in zip(items, images):
74
+ image_path = out / f"{item.file_stem}.png"
75
+ image.save(image_path)
76
+ row = asdict(item)
77
+ row["image"] = image_path.name
78
+ meta.write(json.dumps(row, ensure_ascii=False) + "\n")
79
+
80
+
81
+ def write_manifest(output_dir: str | Path, payload: dict) -> None:
82
+ out = Path(output_dir)
83
+ out.mkdir(parents=True, exist_ok=True)
84
+ with (out / "inference_manifest.json").open("w", encoding="utf-8") as handle:
85
+ json.dump(payload, handle, ensure_ascii=False, indent=2, sort_keys=True)
sefi/modeling/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SEFI inference model components."""
2
+
3
+ from .flux2_sefi_transformer import Flux2SEFITransformer2DModel
4
+ from .qwen3vl_text_encoder import Qwen3VLTextEncoder
5
+ from .texture_latent_codec import TextureLatentCodec
6
+ from .texture_vae_factory import build_texture_vae
7
+
8
+ __all__ = [
9
+ "Flux2SEFITransformer2DModel",
10
+ "Qwen3VLTextEncoder",
11
+ "TextureLatentCodec",
12
+ "build_texture_vae",
13
+ ]
sefi/modeling/flux2_sefi_transformer.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Flux2 SEFI transformer wrapper with explicit dual timestep embedding."""
2
+
3
+ import inspect
4
+ from typing import Any
5
+
6
+ import torch
7
+ import torch.nn as nn
8
+ from torch import Tensor
9
+
10
+ from diffusers import Flux2Transformer2DModel
11
+ from diffusers.models.embeddings import TimestepEmbedding, Timesteps
12
+
13
+ try:
14
+ from diffusers.utils import USE_PEFT_BACKEND, scale_lora_layers, unscale_lora_layers
15
+ except Exception: # pragma: no cover - compatibility fallback
16
+ USE_PEFT_BACKEND = False
17
+
18
+ def scale_lora_layers(model, scale):
19
+ del model, scale
20
+
21
+ def unscale_lora_layers(model, scale):
22
+ del model, scale
23
+
24
+ class SEFIDualTimestepEmbeddings(nn.Module):
25
+ """SEFI dual timestep embeddings: concat([emb_sem, emb_tex])."""
26
+
27
+ def __init__(self, in_channels: int, embedding_dim: int, bias: bool = False):
28
+ super().__init__()
29
+ if embedding_dim % 2 != 0:
30
+ raise ValueError(
31
+ f"SEFI dual timestep embedding requires even embedding_dim, got {embedding_dim}."
32
+ )
33
+
34
+ half_dim = embedding_dim // 2
35
+ self.time_proj = Timesteps(
36
+ num_channels=int(in_channels),
37
+ flip_sin_to_cos=True,
38
+ downscale_freq_shift=0,
39
+ )
40
+ self.semantic_embedder = TimestepEmbedding(
41
+ in_channels=int(in_channels),
42
+ time_embed_dim=half_dim,
43
+ sample_proj_bias=bias,
44
+ )
45
+ self.texture_embedder = TimestepEmbedding(
46
+ in_channels=int(in_channels),
47
+ time_embed_dim=half_dim,
48
+ sample_proj_bias=bias,
49
+ )
50
+
51
+ def forward(self, timestep_sem: Tensor, timestep_tex: Tensor) -> Tensor:
52
+ sem_proj = self.time_proj(timestep_sem)
53
+ tex_proj = self.time_proj(timestep_tex)
54
+ sem_emb = self.semantic_embedder(sem_proj.to(timestep_sem.dtype))
55
+ tex_emb = self.texture_embedder(tex_proj.to(timestep_tex.dtype))
56
+ return torch.cat([sem_emb, tex_emb], dim=-1)
57
+
58
+
59
+ class Flux2SEFITransformer2DModel(nn.Module):
60
+ """Flux2 transformer wrapper for SEFI inference."""
61
+
62
+ def __init__(
63
+ self,
64
+ backbone_config: dict,
65
+ text_input_dim: int,
66
+ ):
67
+ super().__init__()
68
+
69
+ self.backbone = Flux2Transformer2DModel.from_config(backbone_config)
70
+ # SEFI handles semantic/texture timesteps explicitly and does not reuse guidance semantics.
71
+ self.backbone.time_guidance_embed = nn.Identity()
72
+ self._double_mod_img_kwarg, self._double_mod_txt_kwarg = (
73
+ self._resolve_double_stream_modulation_kwargs()
74
+ )
75
+ self._single_mod_kwarg = self._resolve_single_stream_modulation_kwarg()
76
+
77
+ self.dual_time_embed = SEFIDualTimestepEmbeddings(
78
+ in_channels=int(self.backbone.config.timestep_guidance_channels),
79
+ embedding_dim=int(self.backbone.inner_dim),
80
+ bias=False,
81
+ )
82
+
83
+ expected_text_dim = int(self.backbone.config.joint_attention_dim)
84
+ if int(text_input_dim) != expected_text_dim:
85
+ raise ValueError(
86
+ f"Text embedding dim mismatch: text={text_input_dim}, "
87
+ f"transformer expects={expected_text_dim}."
88
+ )
89
+
90
+ def _resolve_double_stream_modulation_kwargs(self) -> tuple[str, str]:
91
+ if not self.backbone.transformer_blocks:
92
+ raise ValueError("Flux2 backbone must define at least one double-stream block.")
93
+ params = inspect.signature(
94
+ self.backbone.transformer_blocks[0].forward
95
+ ).parameters
96
+ if "temb_mod_img" in params and "temb_mod_txt" in params:
97
+ return "temb_mod_img", "temb_mod_txt"
98
+ if "temb_mod_params_img" in params and "temb_mod_params_txt" in params:
99
+ return "temb_mod_params_img", "temb_mod_params_txt"
100
+ raise ValueError(
101
+ "Unsupported Flux2TransformerBlock.forward signature. "
102
+ "Expected temb_mod_img/temb_mod_txt or "
103
+ "temb_mod_params_img/temb_mod_params_txt."
104
+ )
105
+
106
+ def _resolve_single_stream_modulation_kwarg(self) -> str:
107
+ if not self.backbone.single_transformer_blocks:
108
+ raise ValueError("Flux2 backbone must define at least one single-stream block.")
109
+ params = inspect.signature(
110
+ self.backbone.single_transformer_blocks[0].forward
111
+ ).parameters
112
+ if "temb_mod" in params:
113
+ return "temb_mod"
114
+ if "temb_mod_params" in params:
115
+ return "temb_mod_params"
116
+ raise ValueError(
117
+ "Unsupported Flux2SingleTransformerBlock.forward signature. "
118
+ "Expected temb_mod or temb_mod_params."
119
+ )
120
+
121
+ def _format_single_stream_modulation(self, single_stream_mod):
122
+ if self._single_mod_kwarg != "temb_mod_params":
123
+ return single_stream_mod
124
+ if (
125
+ isinstance(single_stream_mod, tuple)
126
+ and len(single_stream_mod) == 1
127
+ and isinstance(single_stream_mod[0], tuple)
128
+ and len(single_stream_mod[0]) == 3
129
+ ):
130
+ return single_stream_mod[0]
131
+ return single_stream_mod
132
+
133
+ def enable_gradient_checkpointing(self):
134
+ self.backbone.enable_gradient_checkpointing()
135
+
136
+ def forward(
137
+ self,
138
+ hidden_states: Tensor,
139
+ timestep_sem: Tensor,
140
+ timestep_tex: Tensor,
141
+ encoder_hidden_states: Tensor,
142
+ txt_ids: Tensor,
143
+ img_ids: Tensor,
144
+ joint_attention_kwargs: dict[str, Any] | None = None,
145
+ ) -> Tensor:
146
+ model_device = hidden_states.device
147
+ model_dtype = next(self.backbone.parameters()).dtype
148
+
149
+ hidden_states = hidden_states.to(device=model_device, dtype=model_dtype)
150
+
151
+ encoder_hidden_states = encoder_hidden_states.to(
152
+ device=model_device, dtype=model_dtype
153
+ )
154
+
155
+ timestep_sem = timestep_sem.to(device=model_device, dtype=model_dtype)
156
+ timestep_tex = timestep_tex.to(device=model_device, dtype=model_dtype)
157
+
158
+ if joint_attention_kwargs is not None:
159
+ joint_attention_kwargs = joint_attention_kwargs.copy()
160
+ lora_scale = joint_attention_kwargs.pop("scale", 1.0)
161
+ else:
162
+ lora_scale = 1.0
163
+
164
+ # 0) LoRA scaling (keep semantics aligned with Flux2 forward).
165
+ if USE_PEFT_BACKEND:
166
+ scale_lora_layers(self.backbone, lora_scale)
167
+
168
+ num_txt_tokens = encoder_hidden_states.shape[1]
169
+ # 1) SEFI dual-time embedding + modulation parameters.
170
+ temb = self.dual_time_embed(timestep_sem * 1000, timestep_tex * 1000)
171
+
172
+ double_stream_mod_img = self.backbone.double_stream_modulation_img(temb)
173
+ double_stream_mod_txt = self.backbone.double_stream_modulation_txt(temb)
174
+ single_stream_mod = self.backbone.single_stream_modulation(temb)
175
+ single_stream_block_mod = self._format_single_stream_modulation(single_stream_mod)
176
+
177
+ # 2) Input projection for image/text streams.
178
+ hidden_states = self.backbone.x_embedder(hidden_states)
179
+ encoder_hidden_states = self.backbone.context_embedder(encoder_hidden_states)
180
+
181
+ if img_ids.ndim == 3:
182
+ img_ids = img_ids[0]
183
+ if txt_ids.ndim == 3:
184
+ txt_ids = txt_ids[0]
185
+
186
+ image_rotary_emb = self.backbone.pos_embed(img_ids)
187
+ text_rotary_emb = self.backbone.pos_embed(txt_ids)
188
+ concat_rotary_emb = (
189
+ torch.cat([text_rotary_emb[0], image_rotary_emb[0]], dim=0),
190
+ torch.cat([text_rotary_emb[1], image_rotary_emb[1]], dim=0),
191
+ )
192
+
193
+ # 3) Double-stream transformer blocks.
194
+ for block in self.backbone.transformer_blocks:
195
+ if torch.is_grad_enabled() and self.backbone.gradient_checkpointing:
196
+ encoder_hidden_states, hidden_states = self.backbone._gradient_checkpointing_func(
197
+ block,
198
+ hidden_states,
199
+ encoder_hidden_states,
200
+ double_stream_mod_img,
201
+ double_stream_mod_txt,
202
+ concat_rotary_emb,
203
+ joint_attention_kwargs,
204
+ )
205
+ else:
206
+ encoder_hidden_states, hidden_states = block(
207
+ hidden_states=hidden_states,
208
+ encoder_hidden_states=encoder_hidden_states,
209
+ **{
210
+ self._double_mod_img_kwarg: double_stream_mod_img,
211
+ self._double_mod_txt_kwarg: double_stream_mod_txt,
212
+ },
213
+ image_rotary_emb=concat_rotary_emb,
214
+ joint_attention_kwargs=joint_attention_kwargs,
215
+ )
216
+
217
+ hidden_states = torch.cat([encoder_hidden_states, hidden_states], dim=1)
218
+
219
+ # 4) Single-stream transformer blocks.
220
+ for block in self.backbone.single_transformer_blocks:
221
+ if torch.is_grad_enabled() and self.backbone.gradient_checkpointing:
222
+ hidden_states = self.backbone._gradient_checkpointing_func(
223
+ block,
224
+ hidden_states,
225
+ None,
226
+ single_stream_block_mod,
227
+ concat_rotary_emb,
228
+ joint_attention_kwargs,
229
+ )
230
+ else:
231
+ hidden_states = block(
232
+ hidden_states=hidden_states,
233
+ encoder_hidden_states=None,
234
+ **{self._single_mod_kwarg: single_stream_block_mod},
235
+ image_rotary_emb=concat_rotary_emb,
236
+ joint_attention_kwargs=joint_attention_kwargs,
237
+ )
238
+
239
+ # 5) Output layers.
240
+ hidden_states = hidden_states[:, num_txt_tokens:, ...]
241
+ hidden_states = self.backbone.norm_out(hidden_states, temb)
242
+ model_pred = self.backbone.proj_out(hidden_states)
243
+
244
+ if USE_PEFT_BACKEND:
245
+ unscale_lora_layers(self.backbone, lora_scale)
246
+
247
+ return model_pred
sefi/modeling/qwen3vl_text_encoder.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Qwen3-VL text encoder wrapper for SEFI T2I inference."""
2
+
3
+ import os
4
+ from typing import Sequence
5
+
6
+ import torch
7
+ import torch.nn as nn
8
+ from torch import Tensor
9
+ from transformers import AutoProcessor, Qwen3VLForConditionalGeneration
10
+
11
+
12
+ QWEN3VL_MODEL_PATHS = {
13
+ "qwen3vl_2b": "Qwen3-VL-2B-Instruct",
14
+ "qwen3vl_4b": "Qwen3-VL-4B-Instruct",
15
+ "qwen3vl_8b": "Qwen3-VL-8B-Instruct",
16
+ }
17
+
18
+
19
+ def resolve_qwen3vl_model_path(
20
+ model_name: str,
21
+ weights_root: str = "outputs/model_weights",
22
+ ) -> str:
23
+ if model_name not in QWEN3VL_MODEL_PATHS:
24
+ raise ValueError(
25
+ f"Unsupported Qwen3-VL model: {model_name}. "
26
+ f"Supported: {list(QWEN3VL_MODEL_PATHS.keys())}"
27
+ )
28
+
29
+ model_path = os.path.join(weights_root, QWEN3VL_MODEL_PATHS[model_name])
30
+ if not os.path.exists(model_path):
31
+ raise FileNotFoundError(
32
+ f"Qwen3-VL model not found: {model_path}. "
33
+ "Please download weights to outputs/model_weights first."
34
+ )
35
+ return model_path
36
+
37
+
38
+ class Qwen3VLTextEncoder(nn.Module):
39
+ """Text embedding wrapper using Qwen3-VL language model."""
40
+
41
+ def __init__(
42
+ self,
43
+ model_name: str,
44
+ weights_root: str = "outputs/model_weights",
45
+ max_length: int = 512,
46
+ hidden_layers: Sequence[int] = (9, 18, 27),
47
+ torch_dtype: torch.dtype = torch.bfloat16,
48
+ ):
49
+ super().__init__()
50
+
51
+ self.model_name = model_name
52
+ self.max_length = int(max_length)
53
+ self.hidden_layers = tuple(int(x) for x in hidden_layers)
54
+
55
+ model_path = resolve_qwen3vl_model_path(model_name, weights_root=weights_root)
56
+ self.processor = AutoProcessor.from_pretrained(model_path, local_files_only=True)
57
+ self.tokenizer = self.processor.tokenizer
58
+
59
+ self.model = Qwen3VLForConditionalGeneration.from_pretrained(
60
+ model_path,
61
+ torch_dtype=torch_dtype,
62
+ local_files_only=True,
63
+ device_map="cpu",
64
+ )
65
+
66
+ # Keep only text tower to save memory.
67
+ if hasattr(self.model, "model") and hasattr(self.model.model, "visual"):
68
+ del self.model.model.visual
69
+
70
+ self.model.eval()
71
+
72
+ text_hidden_size = int(self.model.config.text_config.hidden_size)
73
+ self.output_dim = text_hidden_size * len(self.hidden_layers)
74
+
75
+ def _build_chat_text(self, caption: str) -> str:
76
+ messages = [{"role": "user", "content": [{"type": "text", "text": caption}]}]
77
+ try:
78
+ return self.processor.apply_chat_template(
79
+ messages,
80
+ tokenize=False,
81
+ add_generation_prompt=True,
82
+ enable_thinking=False,
83
+ )
84
+ except TypeError:
85
+ return self.processor.apply_chat_template(
86
+ messages,
87
+ tokenize=False,
88
+ add_generation_prompt=True,
89
+ )
90
+
91
+ @staticmethod
92
+ def _prepare_text_ids(x: Tensor, t_coord: Tensor | None = None) -> Tensor:
93
+ batch, seq_len, _ = x.shape
94
+ out_ids = []
95
+
96
+ for i in range(batch):
97
+ t = torch.arange(1) if t_coord is None else t_coord[i]
98
+ h = torch.arange(1)
99
+ w = torch.arange(1)
100
+ l = torch.arange(seq_len)
101
+ coords = torch.cartesian_prod(t, h, w, l)
102
+ out_ids.append(coords)
103
+
104
+ return torch.stack(out_ids)
105
+
106
+ @torch.no_grad()
107
+ def encode(self, captions: list[str], dtype: torch.dtype | None = None) -> tuple[Tensor, Tensor]:
108
+ device = next(self.model.parameters()).device
109
+ model_dtype = next(self.model.parameters()).dtype
110
+ if dtype is None:
111
+ dtype = model_dtype
112
+
113
+ chat_texts = [self._build_chat_text(caption) for caption in captions]
114
+ tokenized = self.tokenizer(
115
+ chat_texts,
116
+ return_tensors="pt",
117
+ padding="max_length",
118
+ truncation=True,
119
+ max_length=self.max_length,
120
+ )
121
+ input_ids = tokenized["input_ids"].to(device)
122
+ attention_mask = tokenized["attention_mask"].to(device)
123
+
124
+ output = self.model.model(
125
+ input_ids=input_ids,
126
+ attention_mask=attention_mask,
127
+ output_hidden_states=True,
128
+ use_cache=False,
129
+ return_dict=True,
130
+ )
131
+
132
+ hidden_states = output.hidden_states
133
+ max_idx = len(hidden_states) - 1
134
+ for layer_idx in self.hidden_layers:
135
+ if layer_idx > max_idx:
136
+ raise ValueError(
137
+ f"Requested hidden layer {layer_idx}, but model only provides up to {max_idx}."
138
+ )
139
+
140
+ stacked = torch.stack([hidden_states[idx] for idx in self.hidden_layers], dim=1)
141
+ stacked = stacked.to(dtype=dtype)
142
+
143
+ batch, num_layers, seq_len, hidden_dim = stacked.shape
144
+ prompt_embeds = stacked.permute(0, 2, 1, 3).reshape(batch, seq_len, num_layers * hidden_dim)
145
+ text_ids = self._prepare_text_ids(prompt_embeds).to(device)
146
+
147
+ return prompt_embeds, text_ids
sefi/modeling/texture_latent_codec.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Texture latent codec for SEFI-T2I."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ from torch import Tensor
8
+
9
+
10
+ class TextureLatentCodec(nn.Module):
11
+ """Encode/decode and normalize texture latents for SEFI."""
12
+
13
+ def __init__(
14
+ self,
15
+ texture_vae: nn.Module,
16
+ texture_vae_name: str,
17
+ ):
18
+ super().__init__()
19
+ self.texture_vae = texture_vae
20
+ self.texture_vae_name = str(texture_vae_name)
21
+ self._use_flux2_bn = self.texture_vae_name == "flux2"
22
+
23
+ config = getattr(texture_vae, "config", None)
24
+ latent_channels = getattr(config, "latent_channels", None)
25
+ if latent_channels is None:
26
+ raise ValueError(
27
+ "Texture VAE config must provide latent_channels for channel derivation."
28
+ )
29
+ self.latent_channels = int(latent_channels)
30
+ self.texture_channels = int(self.latent_channels * 4)
31
+
32
+ if self._use_flux2_bn:
33
+ if not hasattr(texture_vae, "bn"):
34
+ raise ValueError(
35
+ f"Texture VAE '{self.texture_vae_name}' requires bn stats but no bn module found."
36
+ )
37
+ eps = float(getattr(config, "batch_norm_eps", 1e-6))
38
+ bn_mean = texture_vae.bn.running_mean.view(1, -1, 1, 1).float()
39
+ bn_std = torch.sqrt(texture_vae.bn.running_var.view(1, -1, 1, 1).float() + eps)
40
+ self.register_buffer("texture_bn_mean", bn_mean, persistent=False)
41
+ self.register_buffer("texture_bn_std", bn_std, persistent=False)
42
+ self.scaling_factor = None
43
+ self.shift_factor = None
44
+ else:
45
+ scaling_factor = float(getattr(config, "scaling_factor", 1.0))
46
+ shift_factor = float(getattr(config, "shift_factor", 0.0) or 0.0)
47
+ if scaling_factor <= 0:
48
+ raise ValueError(
49
+ f"Invalid scaling_factor={scaling_factor} for texture VAE {self.texture_vae_name}."
50
+ )
51
+ self.scaling_factor = scaling_factor
52
+ self.shift_factor = shift_factor
53
+
54
+ @property
55
+ def vae_dtype(self) -> torch.dtype:
56
+ return next(self.texture_vae.parameters()).dtype
57
+
58
+ @torch.no_grad()
59
+ def _encode_raw(self, images: Tensor) -> Tensor:
60
+ return self.texture_vae.encode(images.to(dtype=self.vae_dtype)).latent_dist.mode()
61
+
62
+ def _normalize_raw(self, raw_latents: Tensor) -> Tensor:
63
+ return (raw_latents - self.shift_factor) * self.scaling_factor
64
+
65
+ def _denormalize_raw(self, normed_latents: Tensor) -> Tensor:
66
+ return normed_latents / self.scaling_factor + self.shift_factor
67
+
68
+ def _normalize_patchified(self, patchified_latents: Tensor) -> Tensor:
69
+ bn_mean = self.texture_bn_mean.to(patchified_latents.device, patchified_latents.dtype)
70
+ bn_std = self.texture_bn_std.to(patchified_latents.device, patchified_latents.dtype)
71
+ return (patchified_latents - bn_mean) / bn_std
72
+
73
+ def _denormalize_patchified(self, patchified_latents: Tensor) -> Tensor:
74
+ bn_mean = self.texture_bn_mean.to(patchified_latents.device, patchified_latents.dtype)
75
+ bn_std = self.texture_bn_std.to(patchified_latents.device, patchified_latents.dtype)
76
+ return patchified_latents * bn_std + bn_mean
77
+
78
+ @torch.no_grad()
79
+ def encode_texture(self, images: Tensor, pipeline_cls) -> Tensor:
80
+ raw_latents = self._encode_raw(images)
81
+ if self._use_flux2_bn:
82
+ patchified = pipeline_cls._patchify_latents(raw_latents)
83
+ patchified = self._normalize_patchified(patchified)
84
+ else:
85
+ normed_raw = self._normalize_raw(raw_latents)
86
+ patchified = pipeline_cls._patchify_latents(normed_raw)
87
+
88
+ if patchified.shape[1] != self.texture_channels:
89
+ raise ValueError(
90
+ f"Texture channels mismatch: derived={self.texture_channels}, got={patchified.shape[1]}."
91
+ )
92
+
93
+ return patchified
94
+
95
+ @torch.no_grad()
96
+ def decode_texture(self, texture_latents: Tensor, pipeline_cls) -> Tensor:
97
+ if self._use_flux2_bn:
98
+ patchified = self._denormalize_patchified(texture_latents)
99
+ raw_latents = pipeline_cls._unpatchify_latents(patchified)
100
+ else:
101
+ raw_normed = pipeline_cls._unpatchify_latents(texture_latents)
102
+ raw_latents = self._denormalize_raw(raw_normed)
103
+ return self.texture_vae.decode(raw_latents, return_dict=False)[0]
sefi/modeling/texture_vae_factory.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Texture VAE factory for SEFI-T2I."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+
7
+ import torch
8
+
9
+ from .vae_registry import load_vae_from_path
10
+
11
+ SUPPORTED_TEXTURE_VAE_NAMES = {
12
+ "sd1.5",
13
+ "flux1",
14
+ "flux2",
15
+ }
16
+
17
+
18
+ def _normalize_texture_vae_name(name: str) -> str:
19
+ normalized = str(name).strip().lower()
20
+ if normalized not in SUPPORTED_TEXTURE_VAE_NAMES:
21
+ raise ValueError(
22
+ f"Unsupported model.texture_vae.name={name}. "
23
+ f"Expected one of {sorted(SUPPORTED_TEXTURE_VAE_NAMES)}."
24
+ )
25
+ return normalized
26
+
27
+
28
+ def build_texture_vae(texture_vae_cfg: Mapping, *, torch_dtype: torch.dtype):
29
+ """Build the final texture VAE packaged in a SEFI inference artifact."""
30
+ name = _normalize_texture_vae_name(str(texture_vae_cfg.get("name", "")))
31
+ base_path = str(texture_vae_cfg.get("base_path", "")).strip()
32
+ if not base_path:
33
+ raise ValueError("model.texture_vae.base_path is required.")
34
+
35
+ load_name = "flux2" if name == "flux2" else name
36
+ return load_vae_from_path(
37
+ load_name,
38
+ base_path,
39
+ torch_dtype=torch_dtype,
40
+ local_files_only=True,
41
+ )
sefi/modeling/vae_registry.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """VAE loading helpers for SEFI inference artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+
7
+
8
+ def load_vae_from_path(
9
+ model_name: str,
10
+ base_path: str,
11
+ *,
12
+ torch_dtype: torch.dtype | None = None,
13
+ local_files_only: bool = True,
14
+ ):
15
+ """Load a final inference VAE from an explicit artifact path."""
16
+ import diffusers
17
+
18
+ kwargs = {"local_files_only": local_files_only}
19
+ if torch_dtype is not None:
20
+ kwargs["torch_dtype"] = torch_dtype
21
+
22
+ if model_name == "sd1.5":
23
+ return diffusers.models.AutoencoderKL.from_pretrained(base_path, **kwargs)
24
+
25
+ if model_name in {"flux", "flux1"}:
26
+ return diffusers.models.AutoencoderKL.from_pretrained(
27
+ base_path,
28
+ subfolder="vae",
29
+ **kwargs,
30
+ )
31
+
32
+ if model_name == "flux2":
33
+ return diffusers.models.AutoencoderKLFlux2.from_pretrained(
34
+ base_path,
35
+ subfolder="vae",
36
+ **kwargs,
37
+ )
38
+
39
+ raise ValueError(
40
+ f"Unsupported texture VAE model_name={model_name}. "
41
+ "Supported values: sd1.5, flux1, flux2."
42
+ )
sefi/pipeline.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SeFi-Image inference pipeline wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Iterable
7
+
8
+ import torch
9
+ from PIL import Image
10
+
11
+ from .checkpoints import (
12
+ resolve_config_path,
13
+ resolve_checkpoint_to_local,
14
+ )
15
+ from .registry import ModelSpec, infer_model_spec
16
+ from .resolution import resolve_image_size
17
+ from .runtime import load_runtime_symbols
18
+
19
+
20
+ SUPPORTED_DISTILL_STEPS = {4, 8, 10}
21
+
22
+
23
+ class SEFIInferencePipeline:
24
+ """Inference wrapper for SeFi-Image checkpoints."""
25
+
26
+ def __init__(
27
+ self,
28
+ *,
29
+ spec: ModelSpec,
30
+ runner,
31
+ checkpoint_path: str,
32
+ checkpoint_uri: str,
33
+ ) -> None:
34
+ self.spec = spec
35
+ self.runner = runner
36
+ self.checkpoint_path = checkpoint_path
37
+ self.checkpoint_uri = checkpoint_uri
38
+
39
+ @classmethod
40
+ def from_pretrained(
41
+ cls,
42
+ checkpoint: str,
43
+ *,
44
+ cache_dir: str | Path = "outputs/model_weights/sefi_inference",
45
+ config: str | Path | None = None,
46
+ device: str | None = None,
47
+ dtype: str | None = None,
48
+ delta_t: float | None = None,
49
+ timestep_shift_alpha: float | None = None,
50
+ debug_assert_schedule: bool = False,
51
+ autoguidance_config: str | None = None,
52
+ autoguidance_checkpoint: str | None = None,
53
+ guidance_interval_sigma_lo: float | None = None,
54
+ guidance_interval_sigma_hi: float | None = None,
55
+ ) -> "SEFIInferencePipeline":
56
+ runner_cls, load_config = load_runtime_symbols()
57
+
58
+ local_checkpoint, checkpoint_uri = resolve_checkpoint_to_local(
59
+ checkpoint=checkpoint,
60
+ cache_dir=cache_dir,
61
+ )
62
+ resolved_config = load_config(resolve_config_path(local_checkpoint, config))
63
+ spec = infer_model_spec(
64
+ resolved_config,
65
+ checkpoint_uri=checkpoint_uri,
66
+ checkpoint_path=local_checkpoint,
67
+ )
68
+ resolved_device = device or ("cuda" if torch.cuda.is_available() else "cpu")
69
+ resolved_dtype = dtype or spec.default_dtype
70
+ resolved_delta_t = delta_t if delta_t is not None else spec.default_delta_t
71
+ resolved_timestep_shift_alpha = (
72
+ timestep_shift_alpha
73
+ if timestep_shift_alpha is not None
74
+ else spec.default_timestep_shift_alpha
75
+ )
76
+
77
+ runner = runner_cls(
78
+ resolved_config,
79
+ checkpoint_path=local_checkpoint,
80
+ device=resolved_device,
81
+ debug_assert_schedule=debug_assert_schedule,
82
+ delta_t_override=resolved_delta_t,
83
+ inference_dtype=resolved_dtype,
84
+ timestep_shift_alpha=resolved_timestep_shift_alpha,
85
+ autoguidance_config_path=autoguidance_config,
86
+ autoguidance_checkpoint_path=autoguidance_checkpoint,
87
+ guidance_interval_sigma_lo=guidance_interval_sigma_lo,
88
+ guidance_interval_sigma_hi=guidance_interval_sigma_hi,
89
+ )
90
+
91
+ return cls(
92
+ spec=spec,
93
+ runner=runner,
94
+ checkpoint_path=local_checkpoint,
95
+ checkpoint_uri=checkpoint_uri,
96
+ )
97
+
98
+ def __call__(
99
+ self,
100
+ prompts: str | Iterable[str],
101
+ *,
102
+ num_inference_steps: int | None = None,
103
+ guidance_scale: float | None = None,
104
+ height: int | None = None,
105
+ width: int | None = None,
106
+ batch_size: int | None = None,
107
+ seed: int | None = None,
108
+ generator: torch.Generator | None = None,
109
+ ) -> list[Image.Image]:
110
+ prompt_list = [prompts] if isinstance(prompts, str) else list(prompts)
111
+ if not prompt_list:
112
+ return []
113
+
114
+ steps = int(
115
+ num_inference_steps
116
+ if num_inference_steps is not None
117
+ else self.spec.default_steps
118
+ )
119
+ guidance = float(
120
+ guidance_scale
121
+ if guidance_scale is not None
122
+ else self.spec.default_guidance_scale
123
+ )
124
+ size = resolve_image_size(
125
+ height=height,
126
+ width=width,
127
+ default_height=self.spec.default_height,
128
+ default_width=self.spec.default_width,
129
+ )
130
+
131
+ if self.spec.is_distilled:
132
+ if steps not in SUPPORTED_DISTILL_STEPS:
133
+ raise ValueError(
134
+ "SEFI Turbo models currently support "
135
+ f"{sorted(SUPPORTED_DISTILL_STEPS)} steps, got {steps}."
136
+ )
137
+ if guidance != 1.0:
138
+ raise ValueError("SEFI Turbo models should run with guidance_scale=1.0.")
139
+
140
+ bs = int(batch_size or len(prompt_list))
141
+ if bs <= 0:
142
+ raise ValueError("batch_size must be > 0.")
143
+
144
+ gen = generator
145
+ if gen is None and seed is not None:
146
+ gen = torch.Generator(device=str(self.runner.device)).manual_seed(int(seed))
147
+
148
+ images: list[Image.Image] = []
149
+ for start in range(0, len(prompt_list), bs):
150
+ chunk = prompt_list[start : start + bs]
151
+ images.extend(
152
+ self.runner.generate_batch(
153
+ prompts=chunk,
154
+ num_inference_steps=steps,
155
+ guidance_scale=guidance,
156
+ height=size.height,
157
+ width=size.width,
158
+ generator=gen,
159
+ )
160
+ )
161
+ return images
sefi/registry.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Checkpoint-derived model metadata for SeFi-Image inference."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Literal
9
+
10
+ from omegaconf import OmegaConf
11
+
12
+
13
+ ModelFamily = Literal["base", "rl", "turbo"]
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class ModelSpec:
18
+ name: str
19
+ family: ModelFamily
20
+ scale: str
21
+ default_height: int = 1024
22
+ default_width: int = 1024
23
+ default_steps: int = 50
24
+ default_guidance_scale: float = 4.0
25
+ default_delta_t: float | None = None
26
+ default_timestep_shift_alpha: float = 1.0
27
+ default_dtype: str = "bf16"
28
+
29
+ @property
30
+ def is_distilled(self) -> bool:
31
+ return self.family == "turbo"
32
+
33
+
34
+ def _string_option(config, *keys: str) -> str:
35
+ for key in keys:
36
+ value = OmegaConf.select(config, key, default=None)
37
+ if value is not None:
38
+ text = str(value).strip()
39
+ if text:
40
+ return text
41
+ return ""
42
+
43
+
44
+ def _int_option(config, *keys: str, default: int) -> int:
45
+ for key in keys:
46
+ value = OmegaConf.select(config, key, default=None)
47
+ if value is not None:
48
+ return int(value)
49
+ return int(default)
50
+
51
+
52
+ def _float_option(config, *keys: str, default: float | None) -> float | None:
53
+ for key in keys:
54
+ value = OmegaConf.select(config, key, default=None)
55
+ if value is not None:
56
+ return float(value)
57
+ return default
58
+
59
+
60
+ def _checkpoint_hint(checkpoint_uri: str, checkpoint_path: str) -> str:
61
+ parts = [checkpoint_uri, checkpoint_path]
62
+ path = Path(checkpoint_path)
63
+ parts.extend(str(part) for part in path.parts[-4:])
64
+ return " ".join(parts).lower()
65
+
66
+
67
+ def _normalize_family(value: str) -> ModelFamily | None:
68
+ text = value.strip().lower().replace("_", "-")
69
+ if text in {"base", "sft"}:
70
+ return "base"
71
+ if text in {"rl", "reward", "posttrain", "post-training"}:
72
+ return "rl"
73
+ if text in {"turbo", "distill", "distilled", "dmd", "dmd2"}:
74
+ return "turbo"
75
+ return None
76
+
77
+
78
+ def _infer_family(config, checkpoint_uri: str, checkpoint_path: str) -> ModelFamily:
79
+ configured = _string_option(
80
+ config,
81
+ "inference.family",
82
+ "inference.variant",
83
+ "model.family",
84
+ "model.variant",
85
+ )
86
+ if configured:
87
+ family = _normalize_family(configured)
88
+ if family is None:
89
+ raise ValueError(
90
+ "Unsupported SeFi-Image model family in config: "
91
+ f"{configured}. Expected base, rl, or turbo."
92
+ )
93
+ return family
94
+
95
+ hint = _checkpoint_hint(checkpoint_uri, checkpoint_path)
96
+ normalized = re.sub(r"[^a-z0-9]+", "-", hint)
97
+ if "turbo" in normalized or "distill" in normalized or "dmd" in normalized:
98
+ return "turbo"
99
+ if re.search(r"(^|-)rl($|-)", normalized) or "scalar" in normalized:
100
+ return "rl"
101
+ if "base" in normalized or "sft" in normalized:
102
+ return "base"
103
+
104
+ raise ValueError(
105
+ "Could not infer SeFi-Image checkpoint family from checkpoint name. "
106
+ "Use a checkpoint path or Hugging Face repo id containing Base, RL, or "
107
+ "Turbo, or add inference.family to sefi_config.yaml."
108
+ )
109
+
110
+
111
+ def _infer_scale(config, checkpoint_uri: str, checkpoint_path: str) -> str:
112
+ configured = _string_option(config, "model.transformer_scale")
113
+ if configured and configured != "custom":
114
+ return configured.lower()
115
+
116
+ model_name = _string_option(config, "model.model_name").lower()
117
+ match = re.search(r"([0-9]+(?:p[0-9]+)?b)", model_name)
118
+ if match:
119
+ return match.group(1)
120
+
121
+ hint = _checkpoint_hint(checkpoint_uri, checkpoint_path)
122
+ match = re.search(r"([0-9]+(?:p[0-9]+)?b)", hint)
123
+ if match:
124
+ return match.group(1).lower()
125
+
126
+ raise ValueError(
127
+ "Could not infer SeFi-Image model scale from config or checkpoint name."
128
+ )
129
+
130
+
131
+ def _default_name(family: ModelFamily, scale: str) -> str:
132
+ public_scale = scale.upper().replace("P", ".")
133
+ suffix = {"base": "Base", "rl": "RL", "turbo": "turbo"}[family]
134
+ return f"SeFi-Image-{public_scale}-{suffix}"
135
+
136
+
137
+ def _default_steps(family: ModelFamily) -> int:
138
+ return 4 if family == "turbo" else 50
139
+
140
+
141
+ def _default_guidance_scale(family: ModelFamily) -> float:
142
+ return 1.0 if family == "turbo" else 4.0
143
+
144
+
145
+ def _default_timestep_shift_alpha(family: ModelFamily) -> float:
146
+ return 0.3 if family in {"base", "rl"} else 1.0
147
+
148
+
149
+ def _default_dtype(config) -> str:
150
+ dtype = _string_option(config, "inference.dtype", "training.mixed_precision")
151
+ dtype = dtype.lower()
152
+ if dtype in {"bf16", "bfloat16"}:
153
+ return "bf16"
154
+ if dtype in {"fp32", "float32", "no", "none"}:
155
+ return "fp32"
156
+ return "bf16"
157
+
158
+
159
+ def infer_model_spec(
160
+ config,
161
+ *,
162
+ checkpoint_uri: str,
163
+ checkpoint_path: str,
164
+ ) -> ModelSpec:
165
+ family = _infer_family(config, checkpoint_uri, checkpoint_path)
166
+ scale = _infer_scale(config, checkpoint_uri, checkpoint_path)
167
+ resolution = _int_option(config, "data.resolution", default=1024)
168
+ height = _int_option(config, "inference.height", "data.height", default=resolution)
169
+ width = _int_option(config, "inference.width", "data.width", default=resolution)
170
+ name = _string_option(config, "inference.model_name", "model.display_name")
171
+
172
+ return ModelSpec(
173
+ name=name or _default_name(family, scale),
174
+ family=family,
175
+ scale=scale,
176
+ default_height=height,
177
+ default_width=width,
178
+ default_steps=_int_option(
179
+ config,
180
+ "inference.steps",
181
+ "inference.default_steps",
182
+ default=_default_steps(family),
183
+ ),
184
+ default_guidance_scale=_float_option(
185
+ config,
186
+ "inference.guidance_scale",
187
+ "inference.default_guidance_scale",
188
+ default=_default_guidance_scale(family),
189
+ )
190
+ or _default_guidance_scale(family),
191
+ default_delta_t=_float_option(
192
+ config,
193
+ "inference.delta_t",
194
+ default=None,
195
+ ),
196
+ default_timestep_shift_alpha=_float_option(
197
+ config,
198
+ "inference.timestep_shift_alpha",
199
+ default=_default_timestep_shift_alpha(family),
200
+ )
201
+ or _default_timestep_shift_alpha(family),
202
+ default_dtype=_default_dtype(config),
203
+ )
sefi/resolution.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Resolution helpers for SEFI inference."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class ImageSize:
10
+ height: int
11
+ width: int
12
+
13
+
14
+ def resolve_image_size(
15
+ *,
16
+ height: int | None,
17
+ width: int | None,
18
+ default_height: int,
19
+ default_width: int,
20
+ ) -> ImageSize:
21
+ resolved = ImageSize(
22
+ height=int(height if height is not None else default_height),
23
+ width=int(width if width is not None else default_width),
24
+ )
25
+ if resolved.height <= 0 or resolved.width <= 0:
26
+ raise ValueError(f"Image size must be positive, got {resolved}.")
27
+ if resolved.height % 16 != 0 or resolved.width % 16 != 0:
28
+ raise ValueError(
29
+ "SEFI image size must be divisible by 16, "
30
+ f"got height={resolved.height}, width={resolved.width}."
31
+ )
32
+ return resolved
sefi/runner.py ADDED
@@ -0,0 +1,793 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SEFI T2I inference runner with three-phase masked denoising."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ import os
8
+ from typing import Optional
9
+
10
+ import torch
11
+ from PIL import Image
12
+ from torch import Tensor
13
+
14
+ from .builder import (
15
+ build_components,
16
+ build_lightweight_transformer,
17
+ _derive_semantic_channels,
18
+ _derive_text_output_dim,
19
+ _derive_texture_channels,
20
+ text_encoder_signature,
21
+ )
22
+ from .config import load_config
23
+ from .modeling import Qwen3VLTextEncoder
24
+
25
+
26
+ def _resolve_weight_dtype(config, *, override: Optional[str] = None) -> torch.dtype:
27
+ if override is not None:
28
+ normalized = str(override).strip().lower()
29
+ if normalized == "bf16":
30
+ return torch.bfloat16
31
+ if normalized in {"fp32", "float32"}:
32
+ return torch.float32
33
+ raise ValueError(
34
+ f"Unsupported inference dtype: {override}. Expected one of ['bf16', 'fp32']."
35
+ )
36
+
37
+ precision = str(getattr(config.training, "mixed_precision", "bf16")).lower()
38
+ if precision == "fp16":
39
+ return torch.float16
40
+ if precision in {"fp32", "float32", "no"}:
41
+ return torch.float32
42
+ return torch.bfloat16
43
+
44
+
45
+ def _training_sefi_cfg(config):
46
+ cfg = config.training.get("sefi", None)
47
+ if cfg is not None:
48
+ return cfg
49
+ raise ValueError("Config requires training.sefi section.")
50
+
51
+
52
+ def _apply_timestep_shift_unit_interval(u_unit: Tensor, alpha: float) -> Tensor:
53
+ """Apply t' = alpha*t / (1 + (alpha-1)*t) on unit coordinate u in [0, 1]."""
54
+ alpha = float(alpha)
55
+ if alpha <= 0:
56
+ raise ValueError(f"timestep_shift_alpha must be > 0, got {alpha}")
57
+ if alpha == 1.0:
58
+ return u_unit
59
+ denominator = 1.0 + (alpha - 1.0) * u_unit
60
+ return (alpha * u_unit) / denominator
61
+
62
+
63
+ def _combine_guided_velocity(base_pred: Tensor, cond_pred: Tensor, guidance_scale: float) -> Tensor:
64
+ """Shared guidance formula: base + scale * (conditioned - base)."""
65
+ return base_pred + float(guidance_scale) * (cond_pred - base_pred)
66
+
67
+
68
+ def _resolve_guidance_interval_sigma(
69
+ sigma_lo: Optional[float],
70
+ sigma_hi: Optional[float],
71
+ ) -> tuple[Optional[float], Optional[float]]:
72
+ if sigma_lo is None and sigma_hi is None:
73
+ return None, None
74
+ if sigma_lo is None or sigma_hi is None:
75
+ raise ValueError(
76
+ "Limited interval guidance requires both "
77
+ "guidance_interval_sigma_lo and guidance_interval_sigma_hi, or neither."
78
+ )
79
+
80
+ sigma_lo = float(sigma_lo)
81
+ sigma_hi = float(sigma_hi)
82
+ if not math.isfinite(sigma_lo) or not math.isfinite(sigma_hi):
83
+ raise ValueError("guidance interval sigma thresholds must be finite.")
84
+ if sigma_lo < 0.0 or sigma_hi < 0.0:
85
+ raise ValueError("guidance interval sigma thresholds must be >= 0.")
86
+ if sigma_lo >= sigma_hi:
87
+ raise ValueError("guidance_interval_sigma_lo must be < guidance_interval_sigma_hi.")
88
+ return sigma_lo, sigma_hi
89
+
90
+
91
+ def _guidance_interval_is_active(
92
+ sigma: Tensor | float,
93
+ sigma_lo: Optional[float],
94
+ sigma_hi: Optional[float],
95
+ ) -> bool:
96
+ if sigma_lo is None and sigma_hi is None:
97
+ return True
98
+ if sigma_lo is None or sigma_hi is None:
99
+ raise ValueError("guidance interval sigma bounds must be paired.")
100
+
101
+ sigma_value = float(sigma.item()) if isinstance(sigma, Tensor) else float(sigma)
102
+ return float(sigma_lo) < sigma_value <= float(sigma_hi)
103
+
104
+
105
+ def _normalize_optional_path(path: Optional[str]) -> str:
106
+ if path is None:
107
+ return ""
108
+ return str(path).strip()
109
+
110
+
111
+ def _resolve_autoguidance_paths(
112
+ autoguidance_config_path: Optional[str],
113
+ autoguidance_checkpoint_path: Optional[str],
114
+ ) -> tuple[str, str]:
115
+ config_path = _normalize_optional_path(autoguidance_config_path)
116
+ checkpoint_path = _normalize_optional_path(autoguidance_checkpoint_path)
117
+ if bool(config_path) != bool(checkpoint_path):
118
+ raise ValueError(
119
+ "AutoGuidance requires both --autoguidance_config and "
120
+ "--autoguidance_checkpoint, or neither."
121
+ )
122
+ return config_path, checkpoint_path
123
+
124
+
125
+ def _validate_autoguidance_guidance_scale(enabled: bool, guidance_scale: float) -> None:
126
+ if enabled and float(guidance_scale) <= 1.0:
127
+ raise ValueError("AutoGuidance requires guidance_scale > 1.0.")
128
+
129
+
130
+ def _resolve_checkpoint_file(checkpoint_path: str) -> str:
131
+ if os.path.isdir(checkpoint_path):
132
+ transformer_dir = os.path.join(checkpoint_path, "transformer")
133
+ sharded_safetensors = os.path.join(
134
+ transformer_dir,
135
+ "diffusion_pytorch_model.safetensors.index.json",
136
+ )
137
+ if os.path.isfile(sharded_safetensors):
138
+ return sharded_safetensors
139
+
140
+ safetensors_state = os.path.join(
141
+ transformer_dir,
142
+ "diffusion_pytorch_model.safetensors",
143
+ )
144
+ if os.path.isfile(safetensors_state):
145
+ return safetensors_state
146
+
147
+ torch_state = os.path.join(transformer_dir, "diffusion_pytorch_model.bin")
148
+ if os.path.isfile(torch_state):
149
+ return torch_state
150
+
151
+ raise FileNotFoundError(
152
+ f"Unsupported SEFI inference checkpoint directory: {checkpoint_path}. "
153
+ "Expected transformer/diffusion_pytorch_model.safetensors or "
154
+ "transformer/diffusion_pytorch_model.safetensors.index.json."
155
+ )
156
+
157
+ if not os.path.exists(checkpoint_path):
158
+ raise FileNotFoundError(f"Checkpoint path not found: {checkpoint_path}")
159
+
160
+ return checkpoint_path
161
+
162
+
163
+ def _extract_state_dict(checkpoint: dict) -> dict:
164
+ if not isinstance(checkpoint, dict):
165
+ raise ValueError("Checkpoint must be a dict-like object.")
166
+
167
+ if "model_state_dict" in checkpoint and isinstance(checkpoint["model_state_dict"], dict):
168
+ return checkpoint["model_state_dict"]
169
+ if "module" in checkpoint and isinstance(checkpoint["module"], dict):
170
+ return checkpoint["module"]
171
+ if "state_dict" in checkpoint and isinstance(checkpoint["state_dict"], dict):
172
+ return checkpoint["state_dict"]
173
+ if checkpoint and all(isinstance(v, torch.Tensor) for v in checkpoint.values()):
174
+ return checkpoint
175
+
176
+ raise ValueError(
177
+ "Unsupported checkpoint format. Expected one of: "
178
+ "model_state_dict / module / state_dict / plain state_dict."
179
+ )
180
+
181
+
182
+ def _load_checkpoint_payload(checkpoint_file: str):
183
+ if checkpoint_file.endswith(".safetensors.index.json"):
184
+ from safetensors.torch import load_file
185
+
186
+ with open(checkpoint_file, "r", encoding="utf-8") as handle:
187
+ index = json.load(handle)
188
+ weight_map = index.get("weight_map", None)
189
+ if not isinstance(weight_map, dict) or not weight_map:
190
+ raise ValueError(f"Invalid safetensors index file: {checkpoint_file}")
191
+
192
+ base_dir = os.path.dirname(checkpoint_file)
193
+ state_dict = {}
194
+ for shard_name in sorted(set(weight_map.values())):
195
+ shard_path = os.path.join(base_dir, shard_name)
196
+ if not os.path.isfile(shard_path):
197
+ raise FileNotFoundError(f"Missing safetensors shard: {shard_path}")
198
+ state_dict.update(load_file(shard_path))
199
+ return state_dict
200
+
201
+ if checkpoint_file.endswith(".safetensors"):
202
+ from safetensors.torch import load_file
203
+
204
+ return load_file(checkpoint_file)
205
+
206
+ return torch.load(checkpoint_file, map_location="cpu")
207
+
208
+
209
+ def _strip_prefix_if_needed(state_dict: dict, prefix: str) -> dict:
210
+ if state_dict and all(k.startswith(prefix) for k in state_dict):
211
+ return {k[len(prefix) :]: v for k, v in state_dict.items()}
212
+ return state_dict
213
+
214
+
215
+ def _load_transformer_state_dict_strict_shapes(
216
+ transformer,
217
+ checkpoint_path: str,
218
+ *,
219
+ label: str,
220
+ ) -> None:
221
+ checkpoint_file = _resolve_checkpoint_file(checkpoint_path)
222
+ print(f"Loading {label} checkpoint from {checkpoint_file}")
223
+ payload = _load_checkpoint_payload(checkpoint_file)
224
+ state_dict = _extract_state_dict(payload)
225
+ state_dict = _strip_prefix_if_needed(state_dict, "module.")
226
+
227
+ target_state = transformer.state_dict()
228
+ compatible_state = {}
229
+ mismatched = []
230
+ for key, value in state_dict.items():
231
+ if key not in target_state:
232
+ continue
233
+ if tuple(value.shape) != tuple(target_state[key].shape):
234
+ mismatched.append(
235
+ f"{key}: checkpoint={tuple(value.shape)} vs model={tuple(target_state[key].shape)}"
236
+ )
237
+ continue
238
+ compatible_state[key] = value
239
+
240
+ if mismatched:
241
+ raise ValueError(f"{label} checkpoint has shape-mismatched keys: {mismatched[:10]}")
242
+ if not compatible_state:
243
+ raise ValueError(
244
+ f"{label} checkpoint has zero loadable parameters for the constructed model: "
245
+ f"{checkpoint_path}"
246
+ )
247
+
248
+ missing, unexpected = transformer.load_state_dict(compatible_state, strict=False)
249
+ if missing:
250
+ print(f" Warning - {label} missing keys: {missing[:10]}")
251
+ if unexpected:
252
+ print(f" Warning - {label} unexpected keys: {unexpected[:10]}")
253
+
254
+
255
+ class SEFIInferenceRunner:
256
+ """Inference runner for SEFI-T2I with three-phase masked denoising."""
257
+
258
+ def __init__(
259
+ self,
260
+ config,
261
+ *,
262
+ checkpoint_path: str = "",
263
+ device: str = "cuda",
264
+ debug_assert_schedule: bool = False,
265
+ delta_t_override: Optional[float] = None,
266
+ inference_dtype: Optional[str] = None,
267
+ timestep_shift_alpha: float = 1.0,
268
+ autoguidance_config_path: Optional[str] = None,
269
+ autoguidance_checkpoint_path: Optional[str] = None,
270
+ guidance_interval_sigma_lo: Optional[float] = None,
271
+ guidance_interval_sigma_hi: Optional[float] = None,
272
+ ):
273
+ from diffusers.pipelines.flux2.image_processor import Flux2ImageProcessor
274
+
275
+ self.config = config
276
+ self.device = torch.device(device)
277
+ self.component_dtype = _resolve_weight_dtype(config)
278
+ self.weight_dtype = _resolve_weight_dtype(config, override=inference_dtype)
279
+ (
280
+ self.autoguidance_config_path,
281
+ self.autoguidance_checkpoint_path,
282
+ ) = _resolve_autoguidance_paths(
283
+ autoguidance_config_path,
284
+ autoguidance_checkpoint_path,
285
+ )
286
+ self.autoguidance_enabled = bool(self.autoguidance_config_path)
287
+ self.autoguidance_transformer = None
288
+ self.autoguidance_text_encoder = None
289
+ self.autoguidance_reuse_main_text_encoder = True
290
+ (
291
+ self.guidance_interval_sigma_lo,
292
+ self.guidance_interval_sigma_hi,
293
+ ) = _resolve_guidance_interval_sigma(
294
+ guidance_interval_sigma_lo,
295
+ guidance_interval_sigma_hi,
296
+ )
297
+ self.guidance_interval_enabled = self.guidance_interval_sigma_lo is not None
298
+
299
+ components = build_components(config, component_dtype=self.component_dtype)
300
+ self.transformer = components.transformer.to(
301
+ device=self.device,
302
+ dtype=self.weight_dtype,
303
+ ).eval()
304
+ self.text_encoder = components.text_encoder.to(
305
+ device=self.device,
306
+ dtype=self.component_dtype,
307
+ ).eval()
308
+ self.texture_codec = components.texture_codec.to(
309
+ device=self.device,
310
+ dtype=self.component_dtype,
311
+ ).eval()
312
+ self.noise_scheduler = components.noise_scheduler
313
+ self.pipeline_cls = components.pipeline_cls
314
+ self.semantic_channels = int(components.semantic_channels)
315
+ self.texture_channels = int(components.texture_channels)
316
+ self.total_channels = int(components.total_channels)
317
+
318
+ self.debug_assert_schedule = bool(debug_assert_schedule)
319
+ self.timestep_shift_alpha = float(timestep_shift_alpha)
320
+ if self.timestep_shift_alpha <= 0:
321
+ raise ValueError(
322
+ "timestep_shift_alpha must be > 0. "
323
+ f"Got {self.timestep_shift_alpha}."
324
+ )
325
+ self._configure_delta_t(delta_t_override)
326
+ shift_enabled = self.timestep_shift_alpha != 1.0
327
+ print(
328
+ "Inference timestep schedule: "
329
+ f"timestep_shift_alpha={self.timestep_shift_alpha:.6f}, "
330
+ f"delta_t={self.delta_t:.6f}, shift_enabled={shift_enabled}"
331
+ )
332
+ if self.guidance_interval_enabled:
333
+ print(
334
+ "Limited interval guidance enabled on base sigma: "
335
+ f"({self.guidance_interval_sigma_lo:.6f}, "
336
+ f"{self.guidance_interval_sigma_hi:.6f}]"
337
+ )
338
+
339
+ texture_vae_cfg = self.texture_codec.texture_vae.config
340
+ self.vae_scale_factor = 2 ** (len(texture_vae_cfg.block_out_channels) - 1)
341
+ self.image_processor = Flux2ImageProcessor(
342
+ vae_scale_factor=self.vae_scale_factor * 2
343
+ )
344
+
345
+ for module in (self.transformer, self.text_encoder, self.texture_codec):
346
+ for param in module.parameters():
347
+ param.requires_grad = False
348
+
349
+ if checkpoint_path:
350
+ self.load_checkpoint(checkpoint_path)
351
+
352
+ if self.autoguidance_enabled:
353
+ self._load_autoguidance_model()
354
+
355
+ def _configure_delta_t(self, delta_t_override: Optional[float]) -> None:
356
+ sefi_cfg = _training_sefi_cfg(self.config)
357
+
358
+ delta_t_min_raw = sefi_cfg.get("delta_t_min", None)
359
+ delta_t_max_raw = sefi_cfg.get("delta_t_max", None)
360
+ if delta_t_min_raw is None or delta_t_max_raw is None:
361
+ raise ValueError("training.sefi.delta_t_min and delta_t_max are required.")
362
+
363
+ self.delta_t_min = float(delta_t_min_raw)
364
+ self.delta_t_max = float(delta_t_max_raw)
365
+ if self.delta_t_min < 0 or self.delta_t_min > 1:
366
+ raise ValueError("training.sefi.delta_t_min must be in [0, 1].")
367
+ if self.delta_t_max < 0 or self.delta_t_max > 1:
368
+ raise ValueError("training.sefi.delta_t_max must be in [0, 1].")
369
+ if self.delta_t_min > self.delta_t_max:
370
+ raise ValueError("training.sefi.delta_t_min must be <= delta_t_max.")
371
+
372
+ if delta_t_override is None:
373
+ self.delta_t = self.delta_t_max
374
+ print(
375
+ "Warning: --delta-t not provided. "
376
+ f"Using training.sefi.delta_t_max={self.delta_t_max:.6f} for inference."
377
+ )
378
+ return
379
+
380
+ self.delta_t = float(delta_t_override)
381
+ if self.delta_t < 0 or self.delta_t > 1:
382
+ raise ValueError("inference delta_t must be in [0, 1].")
383
+ if self.delta_t < self.delta_t_min or self.delta_t > self.delta_t_max:
384
+ print(
385
+ "Warning: inference delta_t is outside training range "
386
+ f"[{self.delta_t_min:.6f}, {self.delta_t_max:.6f}]. "
387
+ f"Got delta_t={self.delta_t:.6f}."
388
+ )
389
+
390
+ def load_checkpoint(self, checkpoint_path: str):
391
+ ckpt_file = _resolve_checkpoint_file(checkpoint_path)
392
+ print(f"Loading checkpoint from {ckpt_file}")
393
+ ckpt = _load_checkpoint_payload(ckpt_file)
394
+ state_dict = _extract_state_dict(ckpt)
395
+ state_dict = _strip_prefix_if_needed(state_dict, "module.")
396
+
397
+ missing, unexpected = self.transformer.load_state_dict(state_dict, strict=False)
398
+ if missing:
399
+ raise ValueError(f"Checkpoint is missing transformer keys: {missing[:10]}")
400
+ if unexpected:
401
+ raise ValueError(f"Checkpoint has unexpected transformer keys: {unexpected[:10]}")
402
+
403
+ def _load_autoguidance_model(self) -> None:
404
+ autoguidance_config = load_config(self.autoguidance_config_path)
405
+ self.autoguidance_config = autoguidance_config
406
+
407
+ ag_semantic_channels = _derive_semantic_channels(autoguidance_config)
408
+ ag_texture_channels = _derive_texture_channels(autoguidance_config)
409
+ if ag_semantic_channels != self.semantic_channels:
410
+ raise ValueError(
411
+ "AutoGuidance semantic channel mismatch: "
412
+ f"main={self.semantic_channels}, small={ag_semantic_channels}."
413
+ )
414
+ if ag_texture_channels != self.texture_channels:
415
+ raise ValueError(
416
+ "AutoGuidance texture channel mismatch: "
417
+ f"main={self.texture_channels}, small={ag_texture_channels}."
418
+ )
419
+
420
+ ag_text_output_dim = _derive_text_output_dim(autoguidance_config)
421
+ autoguidance_transformer = build_lightweight_transformer(
422
+ autoguidance_config,
423
+ total_channels=self.total_channels,
424
+ text_output_dim=ag_text_output_dim,
425
+ )
426
+ _load_transformer_state_dict_strict_shapes(
427
+ autoguidance_transformer,
428
+ self.autoguidance_checkpoint_path,
429
+ label="AutoGuidance",
430
+ )
431
+ self.autoguidance_transformer = autoguidance_transformer.to(
432
+ device=self.device,
433
+ dtype=self.weight_dtype,
434
+ ).eval()
435
+ for param in self.autoguidance_transformer.parameters():
436
+ param.requires_grad = False
437
+
438
+ self.autoguidance_reuse_main_text_encoder = (
439
+ text_encoder_signature(self.config) == text_encoder_signature(autoguidance_config)
440
+ )
441
+ if self.autoguidance_reuse_main_text_encoder:
442
+ print("AutoGuidance reuses main prompt embeddings.")
443
+ else:
444
+ text_cfg = autoguidance_config.model.text_encoder
445
+ self.autoguidance_text_encoder = Qwen3VLTextEncoder(
446
+ model_name=str(text_cfg.model_name),
447
+ weights_root=str(text_cfg.get("weights_root", "outputs/model_weights")),
448
+ max_length=int(text_cfg.max_length),
449
+ hidden_layers=[int(x) for x in text_cfg.hidden_layers],
450
+ torch_dtype=self.component_dtype,
451
+ ).to(device=self.device, dtype=self.component_dtype).eval()
452
+ if int(self.autoguidance_text_encoder.output_dim) != int(ag_text_output_dim):
453
+ raise ValueError(
454
+ "AutoGuidance text encoder output dim mismatch: "
455
+ f"loaded={self.autoguidance_text_encoder.output_dim}, "
456
+ f"expected={ag_text_output_dim}."
457
+ )
458
+ for param in self.autoguidance_text_encoder.parameters():
459
+ param.requires_grad = False
460
+ print("AutoGuidance uses a separate small-model text encoder.")
461
+
462
+ print(
463
+ "Loaded AutoGuidance model: "
464
+ f"config={self.autoguidance_config_path}, "
465
+ f"checkpoint={self.autoguidance_checkpoint_path}"
466
+ )
467
+
468
+ def _timesteps_and_sigmas(
469
+ self,
470
+ u_continuous: Tensor,
471
+ *,
472
+ n_dim: int,
473
+ dtype: torch.dtype,
474
+ ) -> tuple[Tensor, Tensor]:
475
+ num_steps = int(self.noise_scheduler.config.num_train_timesteps)
476
+ indices = (u_continuous * (num_steps - 1)).long().clamp(0, num_steps - 1)
477
+
478
+ timesteps = self.noise_scheduler.timesteps[indices.cpu()].to(self.device)
479
+ sigmas = self.noise_scheduler.sigmas[indices.cpu()].to(
480
+ device=self.device,
481
+ dtype=dtype,
482
+ )
483
+ while sigmas.ndim < n_dim:
484
+ sigmas = sigmas.unsqueeze(-1)
485
+ return timesteps, sigmas
486
+
487
+ def _assert_shifted_schedule(
488
+ self,
489
+ u_base_unit: Tensor,
490
+ u_sem_raw_schedule: Tensor,
491
+ eps: float = 1e-6,
492
+ ) -> None:
493
+ if u_base_unit.ndim != 1 or u_sem_raw_schedule.ndim != 1:
494
+ raise ValueError("u_base_unit and u_sem_raw_schedule must be 1D tensors.")
495
+ if u_base_unit.shape != u_sem_raw_schedule.shape:
496
+ raise ValueError("u_base_unit and u_sem_raw_schedule must have the same shape.")
497
+
498
+ expected_u_max = 1.0 + self.delta_t
499
+ if abs(float(u_base_unit[0].item()) - 0.0) > eps:
500
+ raise ValueError(
501
+ f"Invalid u_base_unit[0], expected 0, got {float(u_base_unit[0].item()):.6f}"
502
+ )
503
+ if abs(float(u_base_unit[-1].item()) - 1.0) > eps:
504
+ raise ValueError(
505
+ f"Invalid u_base_unit[-1], expected 1, got {float(u_base_unit[-1].item()):.6f}"
506
+ )
507
+ if abs(float(u_sem_raw_schedule[0].item()) - 0.0) > eps:
508
+ raise ValueError(
509
+ "Invalid shifted schedule start, expected 0, "
510
+ f"got {float(u_sem_raw_schedule[0].item()):.6f}"
511
+ )
512
+ if abs(float(u_sem_raw_schedule[-1].item()) - expected_u_max) > eps:
513
+ raise ValueError(
514
+ "Invalid shifted schedule end, expected 1+delta_t, "
515
+ f"got {float(u_sem_raw_schedule[-1].item()):.6f}, "
516
+ f"expected={expected_u_max:.6f}"
517
+ )
518
+
519
+ diffs = u_sem_raw_schedule[1:] - u_sem_raw_schedule[:-1]
520
+ if torch.any(diffs < -eps):
521
+ index = int(torch.nonzero(diffs < -eps, as_tuple=False)[0, 0].item())
522
+ raise ValueError(
523
+ "Shifted u_sem_raw schedule must be monotonic non-decreasing, "
524
+ f"but got decrease at step {index}: "
525
+ f"{float(u_sem_raw_schedule[index].item()):.6f} -> "
526
+ f"{float(u_sem_raw_schedule[index + 1].item()):.6f}"
527
+ )
528
+
529
+ def _assert_dual_time_invariants(
530
+ self,
531
+ u_sem: Tensor,
532
+ u_tex: Tensor,
533
+ sigmas_sem: Tensor,
534
+ sigmas_tex: Tensor,
535
+ eps: float = 1e-6,
536
+ ) -> None:
537
+ u_violation = u_sem < u_tex
538
+ if torch.any(u_violation):
539
+ index = int(torch.nonzero(u_violation, as_tuple=False)[0, 0].item())
540
+ raise ValueError(
541
+ "Dual-time invariant violated: expected u_sem >= u_tex, got "
542
+ f"u_sem[{index}]={float(u_sem[index].item()):.6f}, "
543
+ f"u_tex[{index}]={float(u_tex[index].item()):.6f}."
544
+ )
545
+
546
+ sigma_violation = sigmas_sem > (sigmas_tex + eps)
547
+ if torch.any(sigma_violation):
548
+ index = int(torch.nonzero(sigma_violation, as_tuple=False)[0, 0].item())
549
+ sigma_sem_flat = sigmas_sem.reshape(sigmas_sem.shape[0], -1)
550
+ sigma_tex_flat = sigmas_tex.reshape(sigmas_tex.shape[0], -1)
551
+ raise ValueError(
552
+ "Dual-time invariant violated: expected sigmas_sem <= sigmas_tex, got "
553
+ f"sigmas_sem[{index}]={float(sigma_sem_flat[index, 0].item()):.6f}, "
554
+ f"sigmas_tex[{index}]={float(sigma_tex_flat[index, 0].item()):.6f}."
555
+ )
556
+
557
+ def _prepare_latents(
558
+ self,
559
+ *,
560
+ batch_size: int,
561
+ height: int,
562
+ width: int,
563
+ generator: Optional[torch.Generator],
564
+ ) -> tuple[Tensor, Tensor, int, int]:
565
+ height = 2 * (int(height) // (self.vae_scale_factor * 2))
566
+ width = 2 * (int(width) // (self.vae_scale_factor * 2))
567
+
568
+ latents = torch.randn(
569
+ (batch_size, self.total_channels, height // 2, width // 2),
570
+ generator=generator,
571
+ device=self.device,
572
+ dtype=self.weight_dtype,
573
+ )
574
+ latent_ids = self.pipeline_cls._prepare_latent_ids(latents).to(self.device)
575
+ return latents, latent_ids, height, width
576
+
577
+ def _predict_velocity(
578
+ self,
579
+ transformer,
580
+ *,
581
+ packed_latents: Tensor,
582
+ timesteps_sem: Tensor,
583
+ timesteps_tex: Tensor,
584
+ encoder_hidden_states: Tensor,
585
+ txt_ids: Tensor,
586
+ img_ids: Tensor,
587
+ ) -> Tensor:
588
+ pred = transformer(
589
+ hidden_states=packed_latents,
590
+ timestep_sem=timesteps_sem / 1000,
591
+ timestep_tex=timesteps_tex / 1000,
592
+ encoder_hidden_states=encoder_hidden_states,
593
+ txt_ids=txt_ids,
594
+ img_ids=img_ids,
595
+ )
596
+ pred = pred[:, : packed_latents.size(1)]
597
+ return self.pipeline_cls._unpack_latents_with_ids(pred, img_ids)
598
+
599
+ @torch.no_grad()
600
+ def generate_batch(
601
+ self,
602
+ *,
603
+ prompts: list[str],
604
+ num_inference_steps: int,
605
+ guidance_scale: float,
606
+ height: int,
607
+ width: int,
608
+ generator: Optional[torch.Generator] = None,
609
+ ) -> list[Image.Image]:
610
+ if num_inference_steps <= 0:
611
+ raise ValueError("num_inference_steps must be > 0")
612
+
613
+ batch_size = len(prompts)
614
+ if batch_size == 0:
615
+ return []
616
+
617
+ prompt_embeds, text_ids = self.text_encoder.encode(prompts, dtype=self.weight_dtype)
618
+ _validate_autoguidance_guidance_scale(
619
+ self.autoguidance_enabled,
620
+ guidance_scale,
621
+ )
622
+
623
+ if self.autoguidance_enabled:
624
+ if self.autoguidance_reuse_main_text_encoder:
625
+ autoguidance_prompt_embeds = prompt_embeds
626
+ autoguidance_text_ids = text_ids
627
+ else:
628
+ autoguidance_prompt_embeds, autoguidance_text_ids = (
629
+ self.autoguidance_text_encoder.encode(
630
+ prompts,
631
+ dtype=self.weight_dtype,
632
+ )
633
+ )
634
+ neg_prompt_embeds = None
635
+ neg_text_ids = None
636
+ elif guidance_scale > 1.0:
637
+ neg_prompts = [""] * batch_size
638
+ neg_prompt_embeds, neg_text_ids = self.text_encoder.encode(
639
+ neg_prompts,
640
+ dtype=self.weight_dtype,
641
+ )
642
+ autoguidance_prompt_embeds = None
643
+ autoguidance_text_ids = None
644
+ else:
645
+ autoguidance_prompt_embeds = None
646
+ autoguidance_text_ids = None
647
+ neg_prompt_embeds = None
648
+ neg_text_ids = None
649
+
650
+ latents, latent_ids, _, _ = self._prepare_latents(
651
+ batch_size=batch_size,
652
+ height=height,
653
+ width=width,
654
+ generator=generator,
655
+ )
656
+
657
+ u_base_unit = torch.linspace(
658
+ 0.0,
659
+ 1.0,
660
+ steps=num_inference_steps + 1,
661
+ device=self.device,
662
+ dtype=torch.float32,
663
+ )
664
+ u_shifted_unit = _apply_timestep_shift_unit_interval(
665
+ u_base_unit,
666
+ self.timestep_shift_alpha,
667
+ )
668
+ _, base_sigmas_schedule = self._timesteps_and_sigmas(
669
+ u_shifted_unit,
670
+ n_dim=1,
671
+ dtype=torch.float32,
672
+ )
673
+ u_sem_raw_schedule = u_shifted_unit * (1.0 + self.delta_t)
674
+ if self.debug_assert_schedule:
675
+ self._assert_shifted_schedule(
676
+ u_base_unit=u_base_unit,
677
+ u_sem_raw_schedule=u_sem_raw_schedule,
678
+ )
679
+
680
+ for step in range(num_inference_steps):
681
+ u_sem_raw_cur = torch.full(
682
+ (batch_size,),
683
+ float(u_sem_raw_schedule[step].item()),
684
+ device=self.device,
685
+ )
686
+ u_sem_raw_next = torch.full(
687
+ (batch_size,),
688
+ float(u_sem_raw_schedule[step + 1].item()),
689
+ device=self.device,
690
+ )
691
+
692
+ u_tex_cur = torch.clamp(u_sem_raw_cur - self.delta_t, min=0.0, max=1.0)
693
+ u_sem_cur = torch.clamp(u_sem_raw_cur, max=1.0)
694
+ u_tex_next = torch.clamp(u_sem_raw_next - self.delta_t, min=0.0, max=1.0)
695
+ u_sem_next = torch.clamp(u_sem_raw_next, max=1.0)
696
+
697
+ timesteps_sem_cur, sigmas_sem_cur = self._timesteps_and_sigmas(
698
+ u_sem_cur,
699
+ n_dim=latents.ndim,
700
+ dtype=latents.dtype,
701
+ )
702
+ timesteps_tex_cur, sigmas_tex_cur = self._timesteps_and_sigmas(
703
+ u_tex_cur,
704
+ n_dim=latents.ndim,
705
+ dtype=latents.dtype,
706
+ )
707
+ _, sigmas_sem_next = self._timesteps_and_sigmas(
708
+ u_sem_next,
709
+ n_dim=latents.ndim,
710
+ dtype=latents.dtype,
711
+ )
712
+ _, sigmas_tex_next = self._timesteps_and_sigmas(
713
+ u_tex_next,
714
+ n_dim=latents.ndim,
715
+ dtype=latents.dtype,
716
+ )
717
+ if self.debug_assert_schedule:
718
+ self._assert_dual_time_invariants(
719
+ u_sem_cur,
720
+ u_tex_cur,
721
+ sigmas_sem_cur,
722
+ sigmas_tex_cur,
723
+ )
724
+
725
+ guidance_active = _guidance_interval_is_active(
726
+ base_sigmas_schedule[step],
727
+ self.guidance_interval_sigma_lo,
728
+ self.guidance_interval_sigma_hi,
729
+ )
730
+ packed_latents = self.pipeline_cls._pack_latents(latents)
731
+ pred_cond = self._predict_velocity(
732
+ self.transformer,
733
+ packed_latents=packed_latents,
734
+ timesteps_sem=timesteps_sem_cur,
735
+ timesteps_tex=timesteps_tex_cur,
736
+ encoder_hidden_states=prompt_embeds,
737
+ txt_ids=text_ids,
738
+ img_ids=latent_ids,
739
+ )
740
+
741
+ if not guidance_active:
742
+ velocity = pred_cond
743
+ elif self.autoguidance_enabled:
744
+ pred_base = self._predict_velocity(
745
+ self.autoguidance_transformer,
746
+ packed_latents=packed_latents,
747
+ timesteps_sem=timesteps_sem_cur,
748
+ timesteps_tex=timesteps_tex_cur,
749
+ encoder_hidden_states=autoguidance_prompt_embeds,
750
+ txt_ids=autoguidance_text_ids,
751
+ img_ids=latent_ids,
752
+ )
753
+ velocity = _combine_guided_velocity(
754
+ pred_base,
755
+ pred_cond,
756
+ guidance_scale,
757
+ )
758
+ elif guidance_scale > 1.0:
759
+ pred_uncond = self._predict_velocity(
760
+ self.transformer,
761
+ packed_latents=packed_latents,
762
+ timesteps_sem=timesteps_sem_cur,
763
+ timesteps_tex=timesteps_tex_cur,
764
+ encoder_hidden_states=neg_prompt_embeds,
765
+ txt_ids=neg_text_ids,
766
+ img_ids=latent_ids,
767
+ )
768
+ velocity = _combine_guided_velocity(
769
+ pred_uncond,
770
+ pred_cond,
771
+ guidance_scale,
772
+ )
773
+ else:
774
+ velocity = pred_cond
775
+
776
+ vel_sem = velocity[:, : self.semantic_channels]
777
+ vel_tex = velocity[:, self.semantic_channels :]
778
+ lat_sem = latents[:, : self.semantic_channels]
779
+ lat_tex = latents[:, self.semantic_channels :]
780
+
781
+ dt_sem = sigmas_sem_next - sigmas_sem_cur
782
+ dt_tex = sigmas_tex_next - sigmas_tex_cur
783
+
784
+ lat_sem = lat_sem + dt_sem * vel_sem
785
+ lat_tex = lat_tex + dt_tex * vel_tex
786
+ latents = torch.cat([lat_sem, lat_tex], dim=1)
787
+
788
+ texture_latents = latents[:, self.semantic_channels :]
789
+ decoded = self.texture_codec.decode_texture(
790
+ texture_latents.to(dtype=self.component_dtype),
791
+ pipeline_cls=self.pipeline_cls,
792
+ )
793
+ return self.image_processor.postprocess(decoded, output_type="pil")
sefi/runtime.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Runtime helpers for the bundled SEFI inference package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .config import load_config
8
+
9
+
10
+ def load_runtime_symbols() -> tuple[Any, Any]:
11
+ from .runner import SEFIInferenceRunner
12
+
13
+ return SEFIInferenceRunner, load_config