multimodalart HF Staff commited on
Commit
2c8150b
·
verified ·
1 Parent(s): 2d65e61

Initial UniSpace demo

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +4 -0
  2. LICENSE-UNISPACE +29 -0
  3. README.md +47 -15
  4. THIRD_PARTY.md +19 -0
  5. app.py +507 -719
  6. examples/background_change.png +3 -0
  7. examples/color_alter.png +3 -0
  8. examples/material_alter.png +3 -0
  9. examples/subject-add.png +3 -0
  10. patch-reparameterization/configs/decoder/ViTXL_nockpt/config.json +1 -1
  11. patch-reparameterization/configs/release/pr-dinov2-imagenet256-generation.yaml +55 -0
  12. patch-reparameterization/configs/release/pr-dinov2-imagenet256.yaml +66 -0
  13. patch-reparameterization/configs/release/pr-qwen-vit-imagenet256.yaml +26 -0
  14. patch-reparameterization/configs/release/pr-siglip2-imagenet256-generation.yaml +55 -0
  15. patch-reparameterization/configs/release/pr-siglip2-imagenet256.yaml +73 -0
  16. patch-reparameterization/src/eval/__init__.py +580 -0
  17. patch-reparameterization/src/eval/clip_t.py +363 -0
  18. patch-reparameterization/src/eval/clip_test.py +567 -0
  19. patch-reparameterization/src/eval/clip_zero_shot.py +714 -0
  20. patch-reparameterization/src/eval/clip_zero_shot_bigvision.py +562 -0
  21. patch-reparameterization/src/eval/fid.py +238 -0
  22. patch-reparameterization/src/eval/ref_iqa.py +93 -0
  23. patch-reparameterization/src/eval/utils.py +155 -0
  24. patch-reparameterization/src/eval_only.py +927 -0
  25. patch-reparameterization/src/sample_ddp.py +753 -0
  26. patch-reparameterization/src/stage1/encoders/__init__.py +5 -0
  27. patch-reparameterization/src/stage1/encoders/dinov2_unified.py +124 -0
  28. patch-reparameterization/src/stage1/encoders/qwen3_frozen.py +130 -0
  29. patch-reparameterization/src/stage1/encoders/qwen3_unified.py +11 -5
  30. patch-reparameterization/src/stage2/__init__.py +88 -0
  31. patch-reparameterization/src/stage2/models/DDT.py +919 -0
  32. patch-reparameterization/src/stage2/models/__init__.py +15 -0
  33. patch-reparameterization/src/stage2/models/lightningDiT.py +305 -0
  34. patch-reparameterization/src/stage2/models/model_utils.py +438 -0
  35. patch-reparameterization/src/stage2/transport/__init__.py +79 -0
  36. patch-reparameterization/src/stage2/transport/integrators.py +134 -0
  37. patch-reparameterization/src/stage2/transport/path.py +242 -0
  38. patch-reparameterization/src/stage2/transport/transport.py +614 -0
  39. patch-reparameterization/src/stage2/transport/utils.py +29 -0
  40. patch-reparameterization/src/stage2/transport/xpred.py +131 -0
  41. requirements.txt +13 -16
  42. unispace/LICENSE-APACHE +201 -0
  43. unispace/NOTICE +29 -0
  44. unispace/modeling/__init__.py +4 -1
  45. unispace/modeling/autoencoder.py +382 -0
  46. unispace/modeling/autoencoder_flux2.py +412 -0
  47. unispace/modeling/siglip/__init__.py +98 -0
  48. unispace/modeling/siglip/configuration_siglip.py +287 -0
  49. unispace/modeling/siglip/convert_siglip_to_hf.py +401 -0
  50. unispace/modeling/siglip/image_processing_siglip.py +230 -0
.gitattributes CHANGED
@@ -37,3 +37,7 @@ examples/background_change-input.png filter=lfs diff=lfs merge=lfs -text
37
  examples/color_alter-input.png filter=lfs diff=lfs merge=lfs -text
38
  examples/material_alter-input.png filter=lfs diff=lfs merge=lfs -text
39
  examples/motion_change-input.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
37
  examples/color_alter-input.png filter=lfs diff=lfs merge=lfs -text
38
  examples/material_alter-input.png filter=lfs diff=lfs merge=lfs -text
39
  examples/motion_change-input.png filter=lfs diff=lfs merge=lfs -text
40
+ examples/background_change.png filter=lfs diff=lfs merge=lfs -text
41
+ examples/color_alter.png filter=lfs diff=lfs merge=lfs -text
42
+ examples/material_alter.png filter=lfs diff=lfs merge=lfs -text
43
+ examples/subject-add.png filter=lfs diff=lfs merge=lfs -text
LICENSE-UNISPACE ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 UniSpace 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.
22
+
23
+ ---
24
+
25
+ Note: The `unispace/` subdirectory (the unified multimodal model) contains
26
+ code originally derived from the ByteDance Bagel project and is licensed
27
+ under the Apache License, Version 2.0. See unispace/LICENSE-APACHE and
28
+ unispace/NOTICE for details. The remaining components are licensed under the
29
+ MIT License above.
README.md CHANGED
@@ -1,33 +1,65 @@
1
  ---
2
  title: UniSpace
3
- emoji: 🎨
4
  colorFrom: blue
5
  colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.25.0
8
  app_file: app.py
9
- short_description: UniSpace unified image generation and editing
10
  python_version: "3.12"
11
  startup_duration_timeout: 1h
 
 
12
  ---
13
 
14
- # UniSpace: Unified Visual Representation and Scalable Multimodal Modeling
15
 
16
- This Space demos **UniSpace** — a unified vision model supporting text-to-image generation and instruction-based image editing using a Qwen3-8B Mixture-of-Transformers backbone with a patch-reparameterized visual tokenizer.
 
 
 
17
 
18
- ## Modes
 
 
19
 
20
- - **Text-to-Image Generation**: Provide a text prompt to generate an image from scratch.
21
- - **Image Editing**: Provide an input image and an editing instruction.
22
 
23
- ## Model
 
 
24
 
25
- - **Checkpoint**: `yjb6/UniSpace` (SFT step 0012000, ~61 GB bf16)
26
- - **VAE**: PR-Qwen-ViT patch-reparameterized tokenizer
27
- - **Backbone**: Qwen3-8B with Mixture-of-Transformers (separate gen/und expert branches)
 
28
 
29
- ## References
 
 
30
 
31
- - [Paper](https://huggingface.co/papers/2608.08676)
32
- - [GitHub](https://github.com/yjb6/UniSpace)
33
- - [Project Page](https://yjb6.github.io/UniSpace/)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: UniSpace
3
+ emoji: 🛰️
4
  colorFrom: blue
5
  colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.25.0
8
  app_file: app.py
9
+ short_description: Unified text-to-image generation and instruction editing
10
  python_version: "3.12"
11
  startup_duration_timeout: 1h
12
+ pinned: false
13
+ license: mit
14
  ---
15
 
16
+ # UniSpace
17
 
18
+ Interactive demo of **UniSpace: Unified Visual Representation and Scalable
19
+ Multimodal Modeling** — an 8B Qwen3 Mixture-of-Transformers built on a single
20
+ patch-reparameterized ("PR") visual tokenizer that handles both image
21
+ generation and instruction-based image editing.
22
 
23
+ * Paper: <https://huggingface.co/papers/2608.08676>
24
+ * Weights: <https://huggingface.co/yjb6/UniSpace>
25
+ * Code: <https://github.com/yjb6/UniSpace>
26
 
27
+ ## What runs here
 
28
 
29
+ The Space loads the released `unispace-sft-0012000` checkpoint together with
30
+ the `pr-qwen-vit` tokenizer/decoder and exposes the authors' two reference
31
+ inference paths:
32
 
33
+ | Tab | Reference script | Settings |
34
+ |---|---|---|
35
+ | Text to image | `eval/gen/gen_images_qwen3_unified_mot.py` | 1024², 50 steps, CFG 10, `timestep_shift=0.112`, `cfg_renorm_min=0.9` |
36
+ | Instruction editing | `eval/gen/gen_images_imgedit_qwen3_unified_mot.py` | `--match-ref-size --ref-use-mar` (mar_1024 buckets), 50 steps, CFG 10, `timestep_shift=0.112`, `cfg_renorm_min=0.0` |
37
 
38
+ Both share one model instance built with the release configuration
39
+ (`max_latent_size=96`, `share_unified2llm=False`, `use_spatial_merge_und=True`,
40
+ `use_spatial_merge_gen=False`, `latent_patch_size=1`).
41
 
42
+ The sampling code in `unispace_infer.py` is copied verbatim from those two
43
+ scripts; only the argparse/torchrun driver was removed. Model construction
44
+ differs from the scripts in two ways that are required by the hosting
45
+ environment, not by the model:
46
+
47
+ 1. The base Qwen3-8B weights are never downloaded — every `language_model.*`
48
+ tensor is present in the UniSpace checkpoint, so only its config and
49
+ tokenizer are fetched.
50
+ 2. The 17-shard fp32 checkpoint is streamed one shard at a time, cast to
51
+ bf16 and released, and the PatchReparam encoder loads only the Qwen3-VL
52
+ vision tower (instead of the full 17.5 GB VLM with `device_map="cuda:N"`,
53
+ which is incompatible with ZeroGPU).
54
+
55
+ ## Credits & licensing
56
+
57
+ UniSpace code and weights are MIT-licensed (© 2026 UniSpace Authors); the
58
+ vendored `unispace/` and `patch-reparameterization/` sources carry the
59
+ upstream Apache-2.0 / MIT notices (`LICENSE-UNISPACE`, `unispace/NOTICE`,
60
+ `THIRD_PARTY.md`).
61
+
62
+ The four editing examples in `examples/` are the input images from the
63
+ UniSpace project page gallery, which come from **GEdit-Bench** (MIT, see
64
+ `THIRD_PARTY.md`). Text-to-image example prompts are taken from the UniSpace
65
+ repository's own test prompt set.
THIRD_PARTY.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Third-party evaluation components
2
+
3
+ The repository-level MIT license does not relicense third-party benchmarks,
4
+ datasets, models, or services. Obtain external assets from their official
5
+ sources and follow their terms.
6
+
7
+ | Component | How this repository uses it | Upstream terms |
8
+ |---|---|---|
9
+ | GenEval | Adapted prompt/evaluation utilities under `unispace/eval/gen/geneval/` | MIT notices are retained in the source files. |
10
+ | DPG-Bench | Evaluation package under `unispace/eval/gen/dpg/` | See the bundled `unispace/eval/gen/dpg/LICENSE`. |
11
+ | VIEScore | Adapted evaluator utilities under `unispace/eval/gen/gedit/viescore/` | MIT; the upstream license is bundled in that directory. |
12
+ | GEdit-Bench | Downloaded externally at evaluation time; the dataset is not redistributed here. | The official `stepfun-ai/GEdit-Bench` dataset card declares MIT. |
13
+ | OneIG-Bench | `score.sh` invokes a separately obtained official checkout; official prompts, models, and scorer code are not redistributed here. | The official dataset card declares CC BY-NC 4.0. The official GitHub scorer repository did not provide a root license when checked for this preview, so users must review its current terms before use or redistribution. |
14
+ | ImgEdit | The repository contains adapted benchmark orchestration and score aggregation; benchmark data is external. | The official `PKU-YuanGroup/ImgEdit` repository did not provide a root license when checked for this preview. Obtain the benchmark separately and review its current data, model, and judge terms before use or redistribution. |
15
+
16
+ Model dependencies such as Qwen3, SigLIP2, DINOv2, and evaluator checkpoints
17
+ are not redistributed by this source tree and remain subject to their own
18
+ licenses. API-based judging also remains subject to the selected provider's
19
+ terms.
app.py CHANGED
@@ -1,766 +1,554 @@
1
- """UniSpace: Unified Visual Representation and Scalable Multimodal Modeling.
2
 
3
- Gradio demo for text-to-image generation and instruction-based image editing
4
- using the UniSpace model (Qwen3-8B MoT + PR-Qwen-ViT tokenizer).
 
5
  """
6
 
7
  import os
8
- # Allocator config for memory pressure (large model, transient allocation spikes)
 
 
 
 
9
  os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
10
 
11
- import sys
12
- import time
13
- import gc
14
- import shutil
15
- from pathlib import Path
16
- from dataclasses import dataclass
17
-
18
- import spaces # MUST come before torch / any CUDA-touching import
19
- import torch
20
- import numpy as np
21
- from PIL import Image
22
- import gradio as gr
23
-
24
- # ─── Path setup ─────────────────────────────────────────────────────────────
25
- # The Space repo includes the unispace/ and patch-reparameterization/ packages
26
- # The unispace/ package uses absolute imports like `from modeling.X` so we add
27
- # the unispace/ directory itself to sys.path
28
- SPACE_ROOT = Path(__file__).parent
29
- sys.path.insert(0, str(SPACE_ROOT / "unispace"))
30
- sys.path.insert(0, str(SPACE_ROOT / "patch-reparameterization" / "src"))
31
-
32
- # ─── HF model paths ────────────────────────────────────────────────────────
33
- UNISPACE_CKPT_ID = "yjb6/UniSpace"
34
- UNISPACE_SFT_SUBDIR = "unispace-sft-0012000"
35
- QWEN3_8B_ID = "Qwen/Qwen3-8B" # for config + tokenizer only (no weights download)
36
- QWEN3_VL_8B_ID = "Qwen/Qwen3-VL-8B-Instruct" # for VAE encoder backbone
37
- VAE_CONFIG_PATH = str(SPACE_ROOT / "patch-reparameterization" / "configs" / "vae" / "qwen3-temp-run-mar1024_eval.yaml")
38
-
39
- # ─── Inference config (from eval_sft_0012000.example.json) ─────────────────
40
- CFG_SCALE = 10.0
41
- INFERENCE_STEPS = 50
42
- TIMESTEP_SHIFT = 0.112
43
- MAX_LATENT_SIZE = 96
44
- RESOLUTION = 1024
45
- SHARE_UNIFIED2LLM = False
46
- USE_SPATIAL_MERGE_UND = True
47
-
48
- print(f"[app.py] SPACE_ROOT={SPACE_ROOT}", flush=True)
49
- print(f"[app.py] VAE_CONFIG_PATH={VAE_CONFIG_PATH}", flush=True)
50
-
51
-
52
- # ─── Model loading ──────────────────────────────────────────────────────────
53
- def _download_vae_assets():
54
- """Download the PR-Qwen-ViT tokenizer checkpoint and normalization stats."""
55
- from huggingface_hub import hf_hub_download
56
- vae_ckpt = hf_hub_download(UNISPACE_CKPT_ID, "encoders/pr-qwen-vit-tokenizer.pt", repo_type="model")
57
- stats_ckpt = hf_hub_download(UNISPACE_CKPT_ID, "stats/pr-qwen-vit-normalization-stats.pt", repo_type="model")
58
- return vae_ckpt, stats_ckpt
59
-
60
-
61
- def _download_safetensors_index():
62
- """Download the safetensors index json for the SFT checkpoint."""
63
- from huggingface_hub import hf_hub_download
64
- return hf_hub_download(
65
- UNISPACE_CKPT_ID,
66
- f"{UNISPACE_SFT_SUBDIR}/model.safetensors.index.json",
67
- repo_type="model",
68
- )
69
 
 
 
 
 
 
70
 
71
- def _make_vae_config_yaml(stats_path, ckpt_path):
72
- """Create a local VAE config yaml with resolved paths."""
73
- import yaml
74
- config = {
75
- "stage_1": {
76
- "target": "stage1.PatchReparam",
77
- "params": {
78
- "encoder_cls": "Qwen3Unified",
79
- "encoder_config_path": QWEN3_VL_8B_ID,
80
- "encoder_input_size": 256,
81
- "encoder_params": {
82
- "model_name": QWEN3_VL_8B_ID,
83
- "recon_proj_dim": 128,
84
- "pixel_format": "c_major",
85
- },
86
- "decoder_config_path": "configs/decoder/ViTXL_nockpt",
87
- "decoder_cls": "ViTDecoder",
88
- "noise_tau": 0.8,
89
- "reshape_to_2d": True,
90
- "normalization_stat_path": stats_path,
91
- },
92
- "checkpoint": {
93
- "path": ckpt_path,
94
- "use_ema": True,
95
- },
96
- },
97
- }
98
- tmp_path = "/tmp/vae_config_resolved.yaml"
99
- with open(tmp_path, "w") as f:
100
- yaml.dump(config, f, default_flow_style=False)
101
- return tmp_path
102
-
103
-
104
- def load_unispace_model():
105
- """Load the UniSpace model for T2I generation and image editing."""
106
- from transformers import AutoTokenizer, AutoConfig
107
-
108
- # Import model code
109
- from modeling.qwen3.configuration_qwen3 import Qwen3Config
110
- from modeling.unimm.unimm_mot import UnimmMoT, UnimmConfig
111
- from modeling.unimm.qwen3_mot import Qwen3MoTForConditionalGeneration
112
- from modeling.autoencoder_rae import load_rae
113
-
114
- import torch._dynamo
115
- torch._dynamo.config.disable = True
116
-
117
- device = "cuda"
118
-
119
- # 1. Download VAE assets
120
- print("[load] Downloading VAE assets...", flush=True)
121
- vae_ckpt_path, stats_path = _download_vae_assets()
122
- print(f"[load] VAE checkpoint: {vae_ckpt_path}", flush=True)
123
- print(f"[load] Stats: {stats_path}", flush=True)
124
-
125
- # 2. Create resolved VAE config
126
- vae_config_path = _make_vae_config_yaml(stats_path, vae_ckpt_path)
127
-
128
- # 3. Load Qwen3-8B config (no weights — we use SFT checkpoint)
129
- print("[load] Loading Qwen3-8B config...", flush=True)
130
- llm_config = Qwen3Config.from_pretrained(QWEN3_8B_ID)
131
- llm_config.qk_norm = True
132
- llm_config.tie_word_embeddings = False
133
- llm_config._attn_implementation = "sdpa"
134
- # Ensure pad_token_id exists (transformers 5.x may not set it)
135
- if not hasattr(llm_config, "pad_token_id") or llm_config.pad_token_id is None:
136
- llm_config.pad_token_id = llm_config.eos_token_id
137
- # Fix rope_scaling for transformers 5.x: 'default' was removed, use 'linear' with factor=1.0
138
- if hasattr(llm_config, "rope_scaling") and isinstance(llm_config.rope_scaling, dict):
139
- if llm_config.rope_scaling.get("rope_type") == "default":
140
- llm_config.rope_scaling = {
141
- "rope_type": "linear",
142
- "factor": 1.0,
143
- "rope_theta": llm_config.rope_scaling.get("rope_theta", 1000000.0),
144
- }
145
-
146
- # 4. Load VAE (RAE / PatchReparam)
147
- print("[load] Loading RAE VAE...", flush=True)
148
- vae_model, vae_config = load_rae(
149
- local_path=vae_config_path,
150
- rae_accelerate_root=str(SPACE_ROOT / "patch-reparameterization"),
151
- )
152
 
153
- # 5. Build UnimmConfig
154
- config = UnimmConfig(
155
- visual_gen=True,
156
- visual_und=True, # needed for editing
157
- llm_config=llm_config,
158
- vit_config=None,
159
- vae_config=vae_config,
160
- vit_max_num_patch_per_side=MAX_LATENT_SIZE,
161
- connector_act="gelu_pytorch_tanh",
162
- latent_patch_size=1,
163
- max_latent_size=MAX_LATENT_SIZE,
164
- use_qwen_vit=False,
165
- use_moe=True,
166
- use_qwen3_unified=True,
167
- share_unified2llm=SHARE_UNIFIED2LLM,
168
- use_spatial_merge=False,
169
- use_spatial_merge_und=USE_SPATIAL_MERGE_UND,
170
- use_mrope=False,
171
- )
172
 
173
- # 6. Instantiate model from config (empty weights — no Qwen3-8B download)
174
- print("[load] Instantiating Qwen3MoTForConditionalGeneration (no weight download)...", flush=True)
175
- language_model = Qwen3MoTForConditionalGeneration.__new__(Qwen3MoTForConditionalGeneration)
176
- Qwen3MoTForConditionalGeneration.__init__(language_model, llm_config)
177
-
178
- # 7. Load tokenizer
179
- print("[load] Loading tokenizer...", flush=True)
180
- tokenizer = AutoTokenizer.from_pretrained(QWEN3_8B_ID)
181
-
182
- # 8. Build UnimmMoT
183
- print("[load] Building UnimmMoT...", flush=True)
184
- model = UnimmMoT(language_model, None, tokenizer, config)
185
-
186
- new_token_ids = {
187
- "bos_token_id": tokenizer.bos_token_id if tokenizer.bos_token_id else tokenizer.encode("<|endoftext|>")[0],
188
- "eos_token_id": tokenizer.eos_token_id if tokenizer.eos_token_id else tokenizer.encode("<|endoftext|>")[0],
189
- "pad_token_id": tokenizer.pad_token_id or tokenizer.eos_token_id,
190
- "start_of_image": tokenizer.encode("<|vision_start|>")[0],
191
- "end_of_image": tokenizer.encode("<|vision_end|>")[0],
192
- }
193
-
194
- # 9. Load SFT checkpoint
195
- print("[load] Loading SFT checkpoint...", flush=True)
196
- from safetensors.torch import load_file
197
- from huggingface_hub import snapshot_download
198
-
199
- # Download all SFT shards
200
- sft_dir = snapshot_download(
201
- UNISPACE_CKPT_ID,
202
- allow_patterns=[f"{UNISPACE_SFT_SUBDIR}/*.safetensors", f"{UNISPACE_SFT_SUBDIR}/*.json"],
203
- repo_type="model",
204
- )
205
- sft_dir = os.path.join(sft_dir, UNISPACE_SFT_SUBDIR)
206
-
207
- # Load all shards into a single state dict
208
- index_path = os.path.join(sft_dir, "model.safetensors.index.json")
209
- import json
210
- with open(index_path) as f:
211
- index = json.load(f)
212
-
213
- state_dict = {}
214
- shard_files = sorted(set(index["weight_map"].values()))
215
- print(f"[load] Loading {len(shard_files)} shards...", flush=True)
216
- for shard_file in shard_files:
217
- shard_path = os.path.join(sft_dir, shard_file)
218
- print(f" Loading {shard_file}...", flush=True)
219
- shard_sd = load_file(shard_path, device="cpu")
220
- state_dict.update(shard_sd)
221
- del shard_sd
222
- gc.collect()
223
 
224
- # Handle ExactMergerConnector format
225
- if "unified2llm_und.norm.weight" in state_dict and hasattr(model, "unified2llm_und"):
226
- fc1_w = state_dict["unified2llm_und.fc1.weight"]
227
- fc2_w = state_dict["unified2llm_und.fc2.weight"]
228
- nm_w = state_dict["unified2llm_und.norm.weight"]
229
- model.rebuild_unified2llm_und_as_exact_merger(
230
- total_in=fc1_w.shape[1],
231
- merger_hidden=fc1_w.shape[0],
232
- out_dim=fc2_w.shape[0],
233
- sem_per_patch=nm_w.shape[0],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  )
235
- print(f"[load] Rebuilt unified2llm_und as ExactMergerConnector", flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
- state_dict.pop("latent_pos_embed.pos_embed", None)
238
- state_dict.pop("vit_pos_embed.pos_embed", None)
239
 
240
- msg = model.load_state_dict(state_dict, strict=False)
241
- print(f"[load] Checkpoint loaded: missing={len(msg.missing_keys)}, unexpected={len(msg.unexpected_keys)}", flush=True)
242
- if msg.missing_keys:
243
- print(f" Missing: {msg.missing_keys[:10]}...", flush=True)
244
- if msg.unexpected_keys:
245
- print(f" Unexpected: {msg.unexpected_keys[:10]}...", flush=True)
246
 
247
- del state_dict
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  gc.collect()
 
 
249
 
250
- # 10. Move to device
251
- print("[load] Moving model to cuda (bf16)...", flush=True)
252
- model = model.to(device).to(dtype=torch.bfloat16).eval()
253
- vae_model = vae_model.to(device).eval()
254
- print("[load] Model loaded successfully!", flush=True)
255
-
256
- return model, tokenizer, new_token_ids, vae_model
257
-
258
-
259
- # ─── Load model at module scope ────────────────────────────────────────────
260
- print("=" * 60, flush=True)
261
- print("[app.py] Loading UniSpace model...", flush=True)
262
- print("=" * 60, flush=True)
263
- MODEL, TOKENIZER, NEW_TOKEN_IDS, VAE_MODEL = load_unispace_model()
264
- print("[app.py] Model loaded.", flush=True)
265
-
266
- # Import NaiveCache for use in inference functions
267
- from modeling.unimm.qwen3 import NaiveCache
268
-
269
-
270
- # ─── Helper functions ──────────────────────────────────────────────────────
271
- def _move_to_device(d, device):
272
- for k, v in d.items():
273
- if isinstance(v, torch.Tensor):
274
- d[k] = v.to(device)
275
- return d
276
-
277
-
278
- def _decode_latent(latent, h, w, latent_ch, vae_model, use_spatial_merge_gen, device_type="cuda"):
279
- if use_spatial_merge_gen:
280
- from modeling.unimm.unimm_mot import UnimmMoT
281
- latent = UnimmMoT.spatial_unshuffle(latent, h, w)
282
- latent = latent.reshape(1, h, w, latent_ch).permute(0, 3, 1, 2)
283
- with torch.amp.autocast(device_type, dtype=torch.bfloat16):
284
- image = vae_model.decode(latent)
285
- return ((image * 0.5 + 0.5).clamp(0, 1)[0].permute(1, 2, 0) * 255).to(torch.uint8).cpu().numpy()
286
-
287
-
288
- def _encode_ref_image(pil_image, vae_model, mot_model, image_size, device,
289
- ref_max_size=None, ref_min_size=256, ref_max_pixels=1_048_576,
290
- ref_use_mar=False, ref_mar_resolution=1024):
291
- """Encode a reference image via the RAE und branch for editing."""
292
- import torchvision.transforms.functional as TF
293
- from data.transforms import MaxLongEdgeMinShortEdgeResize
294
- from data.data_utils import get_flattened_position_ids_extrapolate
295
-
296
- img = pil_image.convert("RGB")
297
-
298
- if ref_use_mar:
299
- target_h, target_w = _compute_target_size_from_ref(pil_image, ref_mar_resolution)
300
- img = img.resize((target_w, target_h), Image.BICUBIC)
301
- h_img, w_img = target_h, target_w
302
- elif ref_max_size is not None:
303
- resizer = MaxLongEdgeMinShortEdgeResize(
304
- max_size=ref_max_size, min_size=ref_min_size,
305
- stride=32, max_pixels=ref_max_pixels,
306
- )
307
- img = resizer(img)
308
- w_img, h_img = img.size
309
- else:
310
- img = img.resize((image_size, image_size), Image.BICUBIC)
311
- h_img = w_img = image_size
312
-
313
- img_tensor = TF.to_tensor(img) * 2.0 - 1.0
314
- img_tensor = img_tensor.unsqueeze(0).to(device)
315
-
316
- with torch.no_grad(), torch.amp.autocast("cuda", dtype=torch.bfloat16):
317
- z = vae_model.encode(img_tensor)
318
-
319
- h_grid = h_img // 16
320
- w_grid = w_img // 16
321
- z_tokens = z[0].permute(1, 2, 0).reshape(h_grid * w_grid, -1).to(dtype=torch.bfloat16)
322
-
323
- if mot_model.config.use_spatial_merge_und:
324
- from modeling.unimm.unimm_mot import UnimmMoT
325
- merged_tokens = UnimmMoT.spatial_merge(z_tokens, h_grid, w_grid)
326
- patch_size = 32
327
- else:
328
- merged_tokens = z_tokens
329
- patch_size = 16
330
-
331
- pos_ids = get_flattened_position_ids_extrapolate(
332
- h_img, w_img, patch_size=patch_size,
333
- max_num_patches_per_side=mot_model.max_latent_size,
334
- ).to(device)
335
-
336
- return merged_tokens, pos_ids
337
-
338
-
339
- def _compute_target_size_from_ref(ref_image, resolution, stride=16):
340
- from data.data_utils import MULTI_RESOLUTION_MAP
341
- bucket_key = f"mar_{resolution}"
342
- if bucket_key in MULTI_RESOLUTION_MAP:
343
- bucket = MULTI_RESOLUTION_MAP[bucket_key]
344
- pil_w, pil_h = ref_image.size
345
- ref_ratio = pil_h / pil_w
346
- best_key = min(bucket.keys(), key=lambda k: abs(float(k) - ref_ratio))
347
- H, W = bucket[best_key]
348
- return (int(H), int(W))
349
- else:
350
- pil_w, pil_h = ref_image.size
351
- if pil_w >= pil_h:
352
- target_w = resolution
353
- target_h = resolution * pil_h / pil_w
354
- else:
355
- target_h = resolution
356
- target_w = resolution * pil_w / pil_h
357
- target_h = max(stride, round(target_h / stride) * stride)
358
- target_w = max(stride, round(target_w / stride) * stride)
359
- return (int(target_h), int(target_w))
360
-
361
-
362
- def _prepare_unified_vit_input(curr_kvlens, curr_rope, vit_tokens, pos_ids, new_token_ids, device):
363
- packed_text_ids, packed_text_indexes = [], []
364
- packed_vit_token_indexes = []
365
- packed_position_ids_list, packed_seqlens, packed_indexes = [], [], []
366
- packed_key_value_indexes = []
367
-
368
- _curr = curr = 0
369
- newlens, new_rope_out = [], []
370
- for curr_kvlen, curr_position_id in zip(curr_kvlens, curr_rope):
371
- packed_key_value_indexes.extend(range(curr, curr + curr_kvlen))
372
- curr += curr_kvlen
373
-
374
- packed_text_ids.append(new_token_ids["start_of_image"])
375
- packed_text_indexes.append(_curr)
376
- packed_indexes.append(curr)
377
- curr += 1
378
- _curr += 1
379
-
380
- num_img_tokens = vit_tokens.shape[0]
381
- packed_vit_token_indexes.extend(range(_curr, _curr + num_img_tokens))
382
- packed_indexes.extend(range(curr, curr + num_img_tokens))
383
- curr += num_img_tokens
384
- _curr += num_img_tokens
385
-
386
- packed_text_ids.append(new_token_ids["end_of_image"])
387
- packed_text_indexes.append(_curr)
388
- packed_indexes.append(curr)
389
- curr += 1
390
- _curr += 1
391
-
392
- pos_1d = torch.full((num_img_tokens + 2,), curr_position_id, dtype=torch.long)
393
- packed_position_ids_list.append(pos_1d)
394
- packed_seqlens.append(num_img_tokens + 2)
395
- newlens.append(curr_kvlen + num_img_tokens + 2)
396
- new_rope_out.append(curr_position_id + num_img_tokens + 2)
397
-
398
- return {
399
- "packed_text_ids": torch.tensor(packed_text_ids, dtype=torch.long).to(device),
400
- "packed_text_indexes": torch.tensor(packed_text_indexes, dtype=torch.long).to(device),
401
- "packed_vit_tokens": vit_tokens,
402
- "packed_vit_token_indexes": torch.tensor(packed_vit_token_indexes, dtype=torch.long).to(device),
403
- "packed_vit_position_ids": pos_ids,
404
- "packed_position_ids": torch.cat(packed_position_ids_list).to(device),
405
- "packed_seqlens": torch.tensor(packed_seqlens, dtype=torch.int).to(device),
406
- "packed_indexes": torch.tensor(packed_indexes, dtype=torch.long).to(device),
407
- "packed_key_value_indexes": torch.tensor(packed_key_value_indexes, dtype=torch.long).to(device),
408
- "key_values_lens": torch.tensor(curr_kvlens, dtype=torch.int).to(device),
409
- }, newlens, new_rope_out
410
-
411
-
412
- # ─── Inference functions ───────────────────────────────────────────────────
413
-
414
- @spaces.GPU(duration=240, size="xlarge")
415
- def generate_image(
416
  prompt: str,
417
- steps: int = 50,
418
- cfg_scale: float = 10.0,
419
- seed: int = 42,
420
  resolution: int = 1024,
 
 
421
  progress=gr.Progress(track_tqdm=True),
422
  ):
423
- """Generate an image from a text prompt using UniSpace flow-matching.
424
 
425
  Args:
426
  prompt: Text description of the image to generate.
427
- steps: Number of denoising steps (default 50).
428
- cfg_scale: Classifier-free guidance scale (default 10.0).
429
- seed: Random seed for reproducibility (default 42).
430
- resolution: Output image resolution (default 1024).
 
431
 
432
  Returns:
433
- Generated PIL Image.
434
  """
435
- device = "cuda"
436
- gen_model = MODEL
437
- tokenizer = TOKENIZER
438
- new_token_ids = NEW_TOKEN_IDS
439
- vae_model = VAE_MODEL
440
-
441
- generator = torch.Generator("cpu").manual_seed(seed) if seed is not None else None
442
-
443
- _llm_cfg = gen_model.config.llm_config
444
- num_layers = (
445
- _llm_cfg.text_config.num_hidden_layers
446
- if hasattr(_llm_cfg, "text_config") and _llm_cfg.text_config is not _llm_cfg
447
- else _llm_cfg.num_hidden_layers
448
- )
449
- past_key_values = NaiveCache(num_layers)
450
- newlens = [0]
451
- new_rope = [0]
452
-
453
- # 1. Encode prompt
454
- generation_input_text, newlens, new_rope = gen_model.prepare_prompts(
455
- curr_kvlens=newlens, curr_rope=new_rope,
456
- prompts=[prompt], tokenizer=tokenizer,
457
  new_token_ids=new_token_ids,
 
 
458
  )
459
- generation_input_text = _move_to_device(generation_input_text, device)
460
- with torch.no_grad(), torch.amp.autocast("cuda", dtype=torch.bfloat16):
461
- past_key_values = gen_model.forward_cache_update_text(past_key_values, **generation_input_text)
462
-
463
- # 2. Prepare VAE latent slots
464
- generation_input = gen_model.prepare_vae_latent(
465
- curr_kvlens=newlens, curr_rope=new_rope,
466
- image_sizes=[(resolution, resolution)],
467
- new_token_ids=new_token_ids, generator=generator,
468
- )
469
- generation_input = _move_to_device(generation_input, device)
470
-
471
- # 3. CFG prefix (text-free)
472
- cfg_past_key_values = NaiveCache(num_layers)
473
- cfg_newlens = [0]
474
- cfg_new_rope = [0]
475
- cfg_prompt_input, cfg_newlens, cfg_new_rope = gen_model.prepare_cfg_prompts(
476
- curr_kvlens=cfg_newlens, curr_rope=cfg_new_rope,
477
- num_images=1, tokenizer=tokenizer, new_token_ids=new_token_ids,
478
- )
479
- cfg_prompt_input = _move_to_device(cfg_prompt_input, device)
480
- with torch.no_grad(), torch.amp.autocast("cuda", dtype=torch.bfloat16):
481
- cfg_past_key_values = gen_model.forward_cache_update_text(cfg_past_key_values, **cfg_prompt_input)
482
-
483
- generation_input_cfg = gen_model.prepare_vae_latent_cfg(
484
- curr_kvlens=cfg_newlens, curr_rope=cfg_new_rope,
485
- image_sizes=[(resolution, resolution)],
486
- )
487
- generation_input_cfg = _move_to_device(generation_input_cfg, device)
488
-
489
- # 4. Flow matching denoising
490
- with torch.no_grad(), torch.amp.autocast("cuda", dtype=torch.bfloat16):
491
- unpacked_latent = gen_model.generate_image(
492
- past_key_values=past_key_values,
493
- num_timesteps=steps,
494
- cfg_text_scale=cfg_scale,
495
- cfg_interval=[0, 1.0],
496
- cfg_renorm_min=0.9,
497
- timestep_shift=TIMESTEP_SHIFT,
498
- cfg_text_past_key_values=cfg_past_key_values,
499
- cfg_text_packed_position_ids=generation_input_cfg["cfg_packed_position_ids"],
500
- cfg_text_key_values_lens=generation_input_cfg["cfg_key_values_lens"],
501
- cfg_text_packed_query_indexes=generation_input_cfg["cfg_packed_query_indexes"],
502
- cfg_text_packed_key_value_indexes=generation_input_cfg["cfg_packed_key_value_indexes"],
503
- inference_mode="flash",
504
- **generation_input,
505
- )
506
 
507
- # 5. VAE decode
508
- latent_ch = gen_model.config.vae_config.z_channels
509
- latent_ds = gen_model.config.vae_config.downsample
510
- h = w = resolution // latent_ds
511
- use_merge_gen = gen_model.config.use_spatial_merge_gen
512
- arr = _decode_latent(unpacked_latent[0].float(), h, w, latent_ch, vae_model, use_merge_gen)
513
 
514
- return Image.fromarray(arr)
515
-
516
-
517
- @spaces.GPU(duration=240, size="xlarge")
518
- def edit_image(
519
- input_image: Image.Image,
520
  instruction: str,
521
- steps: int = 50,
522
- cfg_scale: float = 4.0,
523
  seed: int = 42,
524
- resolution: int = 1024,
525
  progress=gr.Progress(track_tqdm=True),
526
  ):
527
- """Edit an image using an instruction with UniSpace.
528
 
529
  Args:
530
- input_image: The source image to edit.
531
- instruction: Editing instruction (e.g., "Change the background to a forest").
532
- steps: Number of denoising steps (default 50).
533
- cfg_scale: Classifier-free guidance scale (default 4.0).
534
- seed: Random seed for reproducibility (default 42).
535
- resolution: Target resolution (default 1024).
536
 
537
  Returns:
538
- Edited PIL Image.
539
  """
540
- device = "cuda"
541
- gen_model = MODEL
542
- tokenizer = TOKENIZER
543
- new_token_ids = NEW_TOKEN_IDS
544
- vae_model = VAE_MODEL
545
-
546
- generator = torch.Generator("cpu").manual_seed(seed) if seed is not None else None
547
-
548
- _llm_cfg = gen_model.config.llm_config
549
- num_layers = (
550
- _llm_cfg.text_config.num_hidden_layers
551
- if hasattr(_llm_cfg, "text_config") and _llm_cfg.text_config is not _llm_cfg
552
- else _llm_cfg.num_hidden_layers
553
- )
554
-
555
- # Compute target size from input image aspect ratio
556
- from data.data_utils import pil_img2rgb
557
- src_image = pil_img2rgb(input_image)
558
- target_h, target_w = _compute_target_size_from_ref(src_image, resolution)
559
- h, w = target_h, target_w
560
-
561
- # Encode ref image via RAE und branch
562
- vit_tokens, pos_ids = _encode_ref_image(
563
- src_image, vae_model, gen_model, 448, device,
564
- ref_use_mar=True, ref_mar_resolution=resolution,
565
- )
566
-
567
- # === Conditional path ===
568
- past_key_values = NaiveCache(num_layers)
569
- newlens = [0]
570
- new_rope = [0]
571
-
572
- vit_inp, newlens, new_rope = _prepare_unified_vit_input(
573
- newlens, new_rope, vit_tokens, pos_ids, new_token_ids, device)
574
- with torch.no_grad(), torch.amp.autocast("cuda", dtype=torch.bfloat16):
575
- past_key_values = gen_model.forward_cache_update_unified_vit(past_key_values, **vit_inp)
576
-
577
- # user\n{instruction}
578
- inp_text, newlens, new_rope = gen_model.prepare_prompts(
579
- curr_kvlens=newlens, curr_rope=new_rope,
580
- prompts=[f"user\n{instruction}"], tokenizer=tokenizer,
581
- new_token_ids=new_token_ids, raw=True,
582
- )
583
- inp_text = _move_to_device(inp_text, device)
584
- with torch.no_grad(), torch.amp.autocast("cuda", dtype=torch.bfloat16):
585
- past_key_values = gen_model.forward_cache_update_text(past_key_values, **inp_text)
586
-
587
- # assistant\n
588
- inp_asst, newlens, new_rope = gen_model.prepare_prompts(
589
- curr_kvlens=newlens, curr_rope=new_rope,
590
- prompts=["assistant\n"], tokenizer=tokenizer,
591
- new_token_ids=new_token_ids, raw=True,
592
- )
593
- inp_asst = _move_to_device(inp_asst, device)
594
- with torch.no_grad(), torch.amp.autocast("cuda", dtype=torch.bfloat16):
595
- past_key_values = gen_model.forward_cache_update_text(past_key_values, **inp_asst)
596
-
597
- # Target image latent slot
598
- generation_input = gen_model.prepare_vae_latent(
599
- curr_kvlens=newlens, curr_rope=new_rope,
600
- image_sizes=[(h, w)], new_token_ids=new_token_ids, generator=generator,
601
- )
602
- generation_input = _move_to_device(generation_input, device)
603
-
604
- # === Unconditional path (only ref image, drop instruction) ===
605
- cfg_past_key_values = NaiveCache(num_layers)
606
- cfg_newlens = [0]
607
- cfg_new_rope = [0]
608
-
609
- vit_inp_cfg, cfg_newlens, cfg_new_rope = _prepare_unified_vit_input(
610
- cfg_newlens, cfg_new_rope, vit_tokens, pos_ids, new_token_ids, device)
611
- with torch.no_grad(), torch.amp.autocast("cuda", dtype=torch.bfloat16):
612
- cfg_past_key_values = gen_model.forward_cache_update_unified_vit(cfg_past_key_values, **vit_inp_cfg)
613
-
614
- inp_asst_cfg, cfg_newlens, cfg_new_rope = gen_model.prepare_prompts(
615
- curr_kvlens=cfg_newlens, curr_rope=cfg_new_rope,
616
- prompts=["assistant\n"], tokenizer=tokenizer,
617
- new_token_ids=new_token_ids, raw=True,
618
- )
619
- inp_asst_cfg = _move_to_device(inp_asst_cfg, device)
620
- with torch.no_grad(), torch.amp.autocast("cuda", dtype=torch.bfloat16):
621
- cfg_past_key_values = gen_model.forward_cache_update_text(cfg_past_key_values, **inp_asst_cfg)
622
-
623
- generation_input_cfg = gen_model.prepare_vae_latent_cfg(
624
- curr_kvlens=cfg_newlens, curr_rope=cfg_new_rope,
625
- image_sizes=[(h, w)],
626
  )
627
- generation_input_cfg = _move_to_device(generation_input_cfg, device)
628
-
629
- # === Flow matching generation ===
630
- with torch.no_grad(), torch.amp.autocast("cuda", dtype=torch.bfloat16):
631
- unpacked_latent = gen_model.generate_image(
632
- past_key_values=past_key_values,
633
- num_timesteps=steps,
634
- cfg_text_scale=cfg_scale,
635
- cfg_interval=[0, 1.0],
636
- cfg_renorm_min=0.0,
637
- timestep_shift=TIMESTEP_SHIFT,
638
- cfg_text_past_key_values=cfg_past_key_values,
639
- cfg_text_packed_position_ids=generation_input_cfg["cfg_packed_position_ids"],
640
- cfg_text_key_values_lens=generation_input_cfg["cfg_key_values_lens"],
641
- cfg_text_packed_query_indexes=generation_input_cfg["cfg_packed_query_indexes"],
642
- cfg_text_packed_key_value_indexes=generation_input_cfg["cfg_packed_key_value_indexes"],
643
- inference_mode="flash",
644
- **generation_input,
645
- )
646
-
647
- # === VAE decode ===
648
- latent_ch = gen_model.config.vae_config.z_channels
649
- latent_ds = gen_model.config.vae_config.downsample
650
- h_lat = h // latent_ds
651
- w_lat = w // latent_ds
652
- use_merge_gen = getattr(gen_model.config, "use_spatial_merge_gen", False)
653
- arr = _decode_latent(unpacked_latent[0].float(), h_lat, w_lat, latent_ch, vae_model, use_merge_gen)
654
-
655
- return Image.fromarray(arr)
656
-
657
-
658
- # ─── Gradio UI ─────────────────────────────────────────────────────────────
659
- CUSTOM_CSS = """
660
- #main-title { text-align: center; }
661
- #main-desc { text-align: center; max-width: 700px; margin: 0 auto; }
 
 
 
662
  """
663
 
664
- with gr.Blocks(theme=gr.themes.Citrus(), css=CUSTOM_CSS) as demo:
665
- gr.Markdown(
666
- """
667
- # 🎨 UniSpace: Unified Visual Representation and Scalable Multimodal Modeling
 
 
 
 
 
 
 
 
 
 
668
 
669
- UniSpace is a unified vision model supporting text-to-image generation and instruction-based image editing,
670
- built on a Qwen3-8B Mixture-of-Transformers backbone with a patch-reparameterized visual tokenizer.
671
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
  )
673
-
674
- with gr.Tabs():
675
- # ─── Text-to-Image Tab ────────────────────────────────────────────
676
- with gr.Tab("✏️ Text-to-Image"):
677
- with gr.Row():
678
- with gr.Column(scale=1):
679
- t2i_prompt = gr.Textbox(
680
- label="Prompt",
681
- placeholder="Describe the image you want to generate...",
682
- lines=3,
683
- value="A festive red and gold promotional poster for Chinese New Year. In the center, a 3D rendered gift box is open, revealing cosmetics products. Large, bold golden Chinese characters at the top read '新春大促' (New Year Sale). Below it, smaller white text says '全场五折起' (50% off all items). The background features traditional cloud patterns and lanterns. A button at the bottom reads '立即抢购' (Buy Now).",
684
- )
685
- t2i_btn = gr.Button("Generate", variant="primary", size="lg")
686
- with gr.Accordion("Advanced Settings", open=False):
687
- t2i_steps = gr.Slider(10, 100, value=50, step=1, label="Steps")
688
- t2i_cfg = gr.Slider(1.0, 20.0, value=10.0, step=0.5, label="CFG Scale")
689
- t2i_seed = gr.Number(value=42, label="Seed", precision=0)
690
- t2i_res = gr.Slider(256, 1024, value=1024, step=128, label="Resolution")
691
-
692
- with gr.Column(scale=1):
693
- t2i_output = gr.Image(label="Generated Image", type="pil")
694
-
695
- t2i_btn.click(
696
- fn=generate_image,
697
- inputs=[t2i_prompt, t2i_steps, t2i_cfg, t2i_seed, t2i_res],
698
- outputs=t2i_output,
699
- )
700
-
701
- gr.Examples(
702
- examples=[
703
- ["An expansive field, blanketed by the soft light of morning, cradles a collection of eight cabbages, their green heads round and plump. These vegetables are nestled among rows of rich soil, dotted with glistening droplets of dew that cling to their crinkled leaves. As wisps of mist begin to lift, the cabbages lie poised, ready for the day's impending harvest."],
704
- ["A photo of a press conference with microphones and a blurred crowd in the background, realistic news style"],
705
- ["A screenshot of a modern landing page for a tech startup, clean UI, blue and white color scheme"],
706
- ["A candid street photography shot of people walking in busy Tokyo, daytime, realistic lighting"],
707
- ],
708
- inputs=t2i_prompt,
709
- fn=generate_image,
710
- cache_examples=True,
711
- cache_mode="lazy",
712
- outputs=t2i_output,
713
- )
714
-
715
- # ─── Image Editing Tab ────────────────────────────────────────────
716
- with gr.Tab("🖼️ Image Editing"):
717
- with gr.Row():
718
- with gr.Column(scale=1):
719
- edit_input = gr.Image(label="Input Image", type="pil")
720
- edit_instruction = gr.Textbox(
721
- label="Edit Instruction",
722
- placeholder="Describe the edit you want to apply...",
723
- lines=2,
724
- value="Change the background to a forest.",
725
- )
726
- edit_btn = gr.Button("Apply Edit", variant="primary", size="lg")
727
- with gr.Accordion("Advanced Settings", open=False):
728
- edit_steps = gr.Slider(10, 100, value=50, step=1, label="Steps")
729
- edit_cfg = gr.Slider(1.0, 20.0, value=4.0, step=0.5, label="CFG Scale")
730
- edit_seed = gr.Number(value=42, label="Seed", precision=0)
731
- edit_res = gr.Slider(256, 1024, value=1024, step=128, label="Resolution")
732
-
733
- with gr.Column(scale=1):
734
- edit_output = gr.Image(label="Edited Image", type="pil")
735
-
736
- edit_btn.click(
737
- fn=edit_image,
738
- inputs=[edit_input, edit_instruction, edit_steps, edit_cfg, edit_seed, edit_res],
739
- outputs=edit_output,
740
- )
741
-
742
- gr.Examples(
743
- examples=[
744
- ["examples/background_change-input.png", "Change the background to a forest."],
745
- ["examples/color_alter-input.png", "Alter the color of the mirror frame to orange."],
746
- ["examples/material_alter-input.png", "Change the cup in hand to ceramic."],
747
- ["examples/motion_change-input.png", "Make the expression more sorrowful."],
748
- ],
749
- inputs=[edit_input, edit_instruction],
750
- fn=edit_image,
751
- cache_examples=True,
752
- cache_mode="lazy",
753
- outputs=edit_output,
754
- )
755
-
756
- gr.Markdown(
757
- """
758
- ---
759
- **Model**: [yjb6/UniSpace](https://huggingface.co/yjb6/UniSpace) |
760
- **Paper**: [2608.08676](https://arxiv.org/abs/2608.08676) |
761
- **Code**: [GitHub](https://github.com/yjb6/UniSpace) |
762
- **Project Page**: [yjb6.github.io/UniSpace](https://yjb6.github.io/UniSpace/)
763
- """
764
  )
765
 
766
- demo.launch(mcp_server=True)
 
 
1
+ """UniSpace — unified text-to-image generation and instruction-based image editing.
2
 
3
+ Paper: UniSpace: Unified Visual Representation and Scalable Multimodal Modeling
4
+ Model: https://huggingface.co/yjb6/UniSpace
5
+ Code: https://github.com/yjb6/UniSpace
6
  """
7
 
8
  import os
9
+
10
+ # Must be set before torch / the UniSpace modeling code is imported.
11
+ os.environ.setdefault("ATTENTION_BACKEND", "torch_sdpa")
12
+ os.environ.setdefault("USE_FLEX_ATTENTION", "0")
13
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
14
  os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
15
 
16
+ import spaces # noqa: E402 (must precede torch / CUDA-touching imports)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ import gc # noqa: E402
19
+ import json # noqa: E402
20
+ import random # noqa: E402
21
+ import sys # noqa: E402
22
+ import time # noqa: E402
23
 
24
+ import gradio as gr # noqa: E402
25
+ import torch # noqa: E402
26
+ from huggingface_hub import hf_hub_download # noqa: E402
27
+ from omegaconf import OmegaConf # noqa: E402
28
+ from safetensors import safe_open # noqa: E402
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ ROOT = os.path.dirname(os.path.abspath(__file__))
31
+ PR_ROOT = os.path.join(ROOT, "patch-reparameterization")
32
+ sys.path.insert(0, os.path.join(PR_ROOT, "src"))
33
+ sys.path.insert(0, os.path.join(ROOT, "unispace"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
+ MODEL_REPO = "yjb6/UniSpace"
36
+ CKPT_DIR = "unispace-sft-0012000"
37
+ QWEN_LLM = "Qwen/Qwen3-8B"
38
+ QWEN_VL = "Qwen/Qwen3-VL-8B-Instruct"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ # Official inference settings (unispace/scripts/eval/run_release_generation.sh +
41
+ # unispace/eval/gen/eval_sft_0012000.example.json).
42
+ MAX_LATENT_SIZE = 96
43
+ TIMESTEP_SHIFT = 0.112
44
+ DEFAULT_STEPS = 50
45
+ DEFAULT_CFG = 10.0
46
+ GEN_CFG_RENORM_MIN = 0.9 # gen_images_qwen3_unified_mot.py main()
47
+ EDIT_CFG_RENORM_MIN = 0.0 # gen_images_imgedit_qwen3_unified_mot.py argparse default
48
+ EDIT_RESOLUTION = 1024
49
+
50
+ DEVICE = "cuda"
51
+ DEVICE_TYPE = "cuda"
52
+
53
+
54
+ def _log(msg):
55
+ print(f"[unispace] {msg}", flush=True)
56
+
57
+
58
+ def _free_cached_file(path):
59
+ """Delete a hub-cached file (symlink + blob) to keep the Space disk small."""
60
+ try:
61
+ real = os.path.realpath(path)
62
+ if os.path.islink(path):
63
+ os.unlink(path)
64
+ if os.path.exists(real):
65
+ os.remove(real)
66
+ except OSError as exc: # pragma: no cover
67
+ _log(f"could not free {path}: {exc}")
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # 1. Vision-tower-only loader for the PatchReparam encoder.
72
+ #
73
+ # stage1/encoders/qwen3_unified.py loads the *full* Qwen3-VL-8B-Instruct
74
+ # (17.5 GB) only to keep `.visual`, and does so with device_map="cuda:N",
75
+ # which is incompatible with the ZeroGPU hijack. We swap in a loader that
76
+ # builds the vision tower on CPU and pulls its weights from the single shard
77
+ # that holds them (2.7 GB).
78
+ # ---------------------------------------------------------------------------
79
+ import transformers.models.qwen3_vl.modeling_qwen3_vl as _q3vl_mod # noqa: E402
80
+ from transformers import AutoConfig, AutoTokenizer # noqa: E402
81
+ from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLVisionModel # noqa: E402
82
+
83
+ try:
84
+ from transformers.modeling_utils import no_init_weights
85
+ except ImportError: # pragma: no cover
86
+ from contextlib import nullcontext as no_init_weights
87
+
88
+
89
+ class _VisionOnlyQwen3VL:
90
+ """Minimal stand-in exposing the two attributes Qwen3Unified touches."""
91
+
92
+ def __init__(self, visual):
93
+ self.visual = visual
94
+ self.model = None
95
+
96
+ @classmethod
97
+ def from_pretrained(cls, model_name, torch_dtype=None, device_map=None, **kwargs):
98
+ cfg = AutoConfig.from_pretrained(model_name)
99
+ vis_cfg = cfg.vision_config
100
+ vis_cfg._attn_implementation = "sdpa"
101
+ t0 = time.time()
102
+ with no_init_weights():
103
+ visual = Qwen3VLVisionModel(vis_cfg)
104
+ visual = visual.to(torch.float32)
105
+ shard = hf_hub_download(model_name, "model-00004-of-00004.safetensors")
106
+ prefix = "model.visual."
107
+ state = {}
108
+ with safe_open(shard, framework="pt", device="cpu") as f:
109
+ for key in f.keys():
110
+ if key.startswith(prefix):
111
+ state[key[len(prefix):]] = f.get_tensor(key).to(torch.float32)
112
+ missing, unexpected = visual.load_state_dict(state, strict=False)
113
+ _log(
114
+ f"vision tower: {len(state)} tensors, missing={len(missing)}, "
115
+ f"unexpected={len(unexpected)} ({time.time() - t0:.1f}s)"
116
  )
117
+ if missing:
118
+ _log(f"vision tower missing keys (first 10): {missing[:10]}")
119
+ del state
120
+ _free_cached_file(shard)
121
+ gc.collect()
122
+ return cls(visual)
123
+
124
+
125
+ _q3vl_mod.Qwen3VLForConditionalGeneration = _VisionOnlyQwen3VL
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # 2. PatchReparam ("RAE") tokenizer / decoder
130
+ # ---------------------------------------------------------------------------
131
+ from modeling.autoencoder_unified import load_unified_vae # noqa: E402
132
+ from modeling.qwen3.configuration_qwen3 import Qwen3Config # noqa: E402
133
+ from modeling.unimm.qwen3_mot import Qwen3MoTForConditionalGeneration # noqa: E402
134
+ from modeling.unimm.unimm_mot import UnimmConfig, UnimmMoT # noqa: E402
135
 
136
+ import unispace_infer as ui # noqa: E402
 
137
 
 
 
 
 
 
 
138
 
139
+ def _build_vae_config_yaml():
140
+ src = os.path.join(PR_ROOT, "configs", "vae", "qwen3-temp-run-mar1024_eval.yaml")
141
+ cfg = OmegaConf.load(src)
142
+ cfg.stage_1.params.encoder_config_path = QWEN_VL
143
+ cfg.stage_1.params.encoder_params.model_name = QWEN_VL
144
+ cfg.stage_1.params.normalization_stat_path = hf_hub_download(
145
+ MODEL_REPO, "stats/pr-qwen-vit-normalization-stats.pt"
146
+ )
147
+ cfg.stage_1.checkpoint.path = hf_hub_download(
148
+ MODEL_REPO, "encoders/pr-qwen-vit-tokenizer.pt"
149
+ )
150
+ out = os.path.join(ROOT, "_vae_eval_resolved.yaml")
151
+ OmegaConf.save(cfg, out, resolve=False)
152
+ return out, str(cfg.stage_1.checkpoint.path)
153
+
154
+
155
+ _log("loading PatchReparam tokenizer/decoder ...")
156
+ _t0 = time.time()
157
+ _vae_yaml, _vae_ckpt = _build_vae_config_yaml()
158
+ vae_model, vae_config = load_unified_vae(local_path=_vae_yaml, patch_reparam_root=PR_ROOT)
159
+ vae_model = vae_model.eval()
160
+ _free_cached_file(_vae_ckpt)
161
+ gc.collect()
162
+ _log(
163
+ f"PatchReparam ready in {time.time() - _t0:.1f}s "
164
+ f"(downsample={vae_config.downsample}, z_channels={vae_config.z_channels})"
165
+ )
166
+
167
+
168
+ # ---------------------------------------------------------------------------
169
+ # 3. Qwen3-8B Mixture-of-Transformers backbone
170
+ #
171
+ # Every `language_model.*` tensor lives in the UniSpace checkpoint, so the base
172
+ # Qwen3-8B weights are never needed — only its config + tokenizer. The 17-shard
173
+ # fp32 checkpoint (61 GB) is streamed one shard at a time, cast to bf16 and
174
+ # released, so peak disk stays around 4 GB.
175
+ # ---------------------------------------------------------------------------
176
+ _log("building Qwen3-8B MoT backbone from config ...")
177
+ _t0 = time.time()
178
+ llm_config = Qwen3Config.from_pretrained(QWEN_LLM)
179
+ llm_config.qk_norm = True
180
+ llm_config.tie_word_embeddings = False
181
+ llm_config._attn_implementation = "sdpa"
182
+
183
+ _orig_linear_reset = torch.nn.Linear.reset_parameters
184
+ _orig_embedding_reset = torch.nn.Embedding.reset_parameters
185
+ torch.nn.Linear.reset_parameters = lambda self: None
186
+ torch.nn.Embedding.reset_parameters = lambda self: None
187
+ torch.set_default_dtype(torch.bfloat16)
188
+ try:
189
+ with no_init_weights():
190
+ language_model = Qwen3MoTForConditionalGeneration(llm_config)
191
+ finally:
192
+ torch.set_default_dtype(torch.float32)
193
+ torch.nn.Linear.reset_parameters = _orig_linear_reset
194
+ torch.nn.Embedding.reset_parameters = _orig_embedding_reset
195
+ _log(f"backbone skeleton built in {time.time() - _t0:.1f}s")
196
+
197
+ unimm_config = UnimmConfig(
198
+ visual_gen=True,
199
+ visual_und=True,
200
+ llm_config=llm_config,
201
+ vit_config=None,
202
+ vae_config=vae_config,
203
+ vit_max_num_patch_per_side=MAX_LATENT_SIZE,
204
+ connector_act="gelu_pytorch_tanh",
205
+ latent_patch_size=1,
206
+ max_latent_size=MAX_LATENT_SIZE,
207
+ use_qwen_vit=False,
208
+ use_moe=True,
209
+ use_qwen3_unified=True,
210
+ share_unified2llm=False,
211
+ use_spatial_merge=False,
212
+ use_spatial_merge_gen=False,
213
+ use_spatial_merge_und=True,
214
+ use_mrope=False,
215
+ )
216
+
217
+ tokenizer = AutoTokenizer.from_pretrained(QWEN_LLM)
218
+ model = UnimmMoT(language_model, None, tokenizer, unimm_config)
219
+ del language_model
220
+ gc.collect()
221
+
222
+ new_token_ids = {
223
+ "bos_token_id": tokenizer.encode("<|im_start|>")[0],
224
+ "eos_token_id": tokenizer.encode("<|im_end|>")[0],
225
+ "pad_token_id": tokenizer.pad_token_id or tokenizer.eos_token_id,
226
+ "start_of_image": tokenizer.encode("<|vision_start|>")[0],
227
+ "end_of_image": tokenizer.encode("<|vision_end|>")[0],
228
+ }
229
+
230
+ # The two positional embeddings are frozen 2-D sincos tables rebuilt at
231
+ # construction time — the official edit script drops them from the checkpoint.
232
+ SKIP_KEYS = {"latent_pos_embed.pos_embed", "vit_pos_embed.pos_embed"}
233
+
234
+ _log("streaming UniSpace SFT checkpoint ...")
235
+ _t0 = time.time()
236
+ _index = json.load(
237
+ open(hf_hub_download(MODEL_REPO, f"{CKPT_DIR}/model.safetensors.index.json"))
238
+ )
239
+ _shards = sorted(set(_index["weight_map"].values()))
240
+ _loaded = set()
241
+ for _i, _shard in enumerate(_shards, 1):
242
+ _path = hf_hub_download(MODEL_REPO, f"{CKPT_DIR}/{_shard}")
243
+ _sd = {}
244
+ with safe_open(_path, framework="pt", device="cpu") as f:
245
+ for _k in f.keys():
246
+ if _k in SKIP_KEYS:
247
+ continue
248
+ _sd[_k] = f.get_tensor(_k).to(torch.bfloat16)
249
+ _msg = model.load_state_dict(_sd, strict=False)
250
+ if _msg.unexpected_keys:
251
+ _log(f" shard {_i}: unexpected {_msg.unexpected_keys[:5]}")
252
+ _loaded.update(_sd.keys())
253
+ del _sd, _msg
254
  gc.collect()
255
+ _free_cached_file(_path)
256
+ _log(f" shard {_i}/{len(_shards)} loaded ({time.time() - _t0:.0f}s elapsed)")
257
 
258
+ _expected = set(model.state_dict().keys())
259
+ _missing = sorted(_expected - _loaded - SKIP_KEYS)
260
+ _log(f"checkpoint loaded: {len(_loaded)} tensors, missing={len(_missing)}")
261
+ if _missing:
262
+ _log(f"MISSING KEYS (first 20): {_missing[:20]}")
263
+
264
+ model = model.to(torch.bfloat16).eval()
265
+ for _p in model.parameters():
266
+ _p.requires_grad_(False)
267
+ for _p in vae_model.parameters():
268
+ _p.requires_grad_(False)
269
+ gc.collect()
270
+
271
+ _log("moving weights to GPU ...")
272
+ model = model.to(DEVICE)
273
+ vae_model = vae_model.to(DEVICE)
274
+ _log("model ready")
275
+
276
+
277
+ # ---------------------------------------------------------------------------
278
+ # 4. Gradio handlers
279
+ # ---------------------------------------------------------------------------
280
+ MAX_SEED = 2**31 - 1
281
+
282
+
283
+ def _duration_generate(prompt="", steps=DEFAULT_STEPS, *args, **kwargs):
284
+ try:
285
+ steps = int(steps)
286
+ except (TypeError, ValueError):
287
+ steps = DEFAULT_STEPS
288
+ return min(240, 40 + int(steps * 1.6))
289
+
290
+
291
+ def _duration_edit(image=None, instruction="", steps=DEFAULT_STEPS, *args, **kwargs):
292
+ try:
293
+ steps = int(steps)
294
+ except (TypeError, ValueError):
295
+ steps = DEFAULT_STEPS
296
+ return min(280, 55 + int(steps * 2.0))
297
+
298
+
299
+ @spaces.GPU(duration=_duration_generate)
300
+ def generate(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  prompt: str,
302
+ steps: int = DEFAULT_STEPS,
303
+ cfg_scale: float = DEFAULT_CFG,
 
304
  resolution: int = 1024,
305
+ seed: int = 42,
306
+ randomize_seed: bool = False,
307
  progress=gr.Progress(track_tqdm=True),
308
  ):
309
+ """Generate a square image from a text prompt with UniSpace.
310
 
311
  Args:
312
  prompt: Text description of the image to generate.
313
+ steps: Number of flow-matching sampling steps.
314
+ cfg_scale: Classifier-free guidance strength.
315
+ resolution: Output width/height in pixels.
316
+ seed: Random seed.
317
+ randomize_seed: Draw a fresh random seed instead of using `seed`.
318
 
319
  Returns:
320
+ The generated PIL image, the seed that was used, and a timing note.
321
  """
322
+ if not prompt or not prompt.strip():
323
+ raise gr.Error("Please enter a prompt.")
324
+ if randomize_seed:
325
+ seed = random.randint(0, MAX_SEED)
326
+ seed = int(seed)
327
+ t0 = time.perf_counter()
328
+ images = ui.generate_image(
329
+ prompt=prompt.strip(),
330
+ num_timesteps=int(steps),
331
+ cfg_scale=float(cfg_scale),
332
+ cfg_interval=[0, 1.0],
333
+ cfg_renorm_min=GEN_CFG_RENORM_MIN,
334
+ timestep_shift=TIMESTEP_SHIFT,
335
+ max_t=1.0,
336
+ num_images=1,
337
+ resolution=int(resolution),
338
+ inference_mode="flash",
339
+ device=DEVICE,
340
+ device_type=DEVICE_TYPE,
341
+ gen_model=model,
342
+ tokenizer=tokenizer,
 
343
  new_token_ids=new_token_ids,
344
+ vae_model=vae_model,
345
+ seed=seed,
346
  )
347
+ elapsed = time.perf_counter() - t0
348
+ return images[0], seed, f"{resolution}×{resolution} · {steps} steps · {elapsed:.1f}s"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
 
 
 
 
 
 
350
 
351
+ @spaces.GPU(duration=_duration_edit)
352
+ def edit(
353
+ image,
 
 
 
354
  instruction: str,
355
+ steps: int = DEFAULT_STEPS,
356
+ cfg_scale: float = DEFAULT_CFG,
357
  seed: int = 42,
358
+ randomize_seed: bool = False,
359
  progress=gr.Progress(track_tqdm=True),
360
  ):
361
+ """Edit an image by following a natural-language instruction.
362
 
363
  Args:
364
+ image: The source image to edit.
365
+ instruction: What to change, e.g. "Change the background to a forest.".
366
+ steps: Number of flow-matching sampling steps.
367
+ cfg_scale: Classifier-free guidance strength on the instruction.
368
+ seed: Random seed.
369
+ randomize_seed: Draw a fresh random seed instead of using `seed`.
370
 
371
  Returns:
372
+ The edited PIL image, the seed that was used, and a timing note.
373
  """
374
+ if image is None:
375
+ raise gr.Error("Please upload an image to edit.")
376
+ if not instruction or not instruction.strip():
377
+ raise gr.Error("Please enter an editing instruction.")
378
+ if randomize_seed:
379
+ seed = random.randint(0, MAX_SEED)
380
+ seed = int(seed)
381
+ ref = image.convert("RGB")
382
+ target_size = ui.compute_target_size_from_ref(ref, EDIT_RESOLUTION)
383
+ t0 = time.perf_counter()
384
+ out = ui.editing_image(
385
+ gen_model=model,
386
+ tokenizer=tokenizer,
387
+ new_token_ids=new_token_ids,
388
+ vae_model=vae_model,
389
+ ref_image=ref,
390
+ prompt=instruction.strip(),
391
+ ref_image_size=448,
392
+ ref_max_size=None,
393
+ ref_min_size=256,
394
+ ref_use_mar=True,
395
+ ref_mar_resolution=EDIT_RESOLUTION,
396
+ target_size=target_size,
397
+ num_timesteps=int(steps),
398
+ cfg_text_scale=float(cfg_scale),
399
+ cfg_interval=[0, 1.0],
400
+ cfg_renorm_min=EDIT_CFG_RENORM_MIN,
401
+ timestep_shift=TIMESTEP_SHIFT,
402
+ device=DEVICE,
403
+ device_type=DEVICE_TYPE,
404
+ seed=seed,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  )
406
+ elapsed = time.perf_counter() - t0
407
+ return out, seed, f"{target_size[1]}×{target_size[0]} · {steps} steps · {elapsed:.1f}s"
408
+
409
+
410
+ # Thin wrappers for gr.Examples. They deliberately take no `gr.Progress`
411
+ # parameter: Gradio injects the Progress object at the index of that parameter
412
+ # and would otherwise land it in `steps` when an example row supplies fewer
413
+ # values than the full signature.
414
+ def generate_example(prompt: str):
415
+ """Run the text-to-image example row with default settings."""
416
+ return generate(prompt)
417
+
418
+
419
+ def edit_example(image, instruction: str):
420
+ """Run the image-editing example row with default settings."""
421
+ return edit(image, instruction)
422
+
423
+
424
+ # ---------------------------------------------------------------------------
425
+ # 5. UI
426
+ # ---------------------------------------------------------------------------
427
+ GEN_EXAMPLES = [
428
+ ["A photo of a press conference with microphones and a blurred crowd in the background, realistic news style"],
429
+ ["A watercolor painting of a lighthouse during a winter sunrise."],
430
+ ["An ink wash painting style image illustrating a Chinese poem. A solitary boat floats on a misty river near mountains. Vertical traditional Chinese text on the right reads: '孤舟蓑笠翁,独钓寒江雪'. The mood is serene and melancholic. A red seal stamp (chop) is visible in the bottom left corner."],
431
+ ["A realistic photo of a modern airport terminal sign hanging from the ceiling. The sign has a yellow background with black text. It shows directions with arrows. The text is in three languages: English 'Departures', Chinese '出发', and Japanese '出発'. An icon of an airplane taking off is next to the text. The background shows a blurred terminal hall with travelers."],
432
+ ["A professional product photo of a sneaker on a white background, studio lighting, advertising style"],
433
+ ]
434
+
435
+ EDIT_EXAMPLES = [
436
+ ["examples/background_change.png", "Change the background to a forest."],
437
+ ["examples/color_alter.png", "Alter the color of the mirror frame to orange."],
438
+ ["examples/material_alter.png", "Change the cup in hand to ceramic."],
439
+ ["examples/subject-add.png", "Add a balloon decoration strip below the airplane."],
440
+ ]
441
+
442
+ CSS = """
443
+ #col-container { max-width: 1100px; margin: 0 auto; }
444
  """
445
 
446
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="UniSpace") as demo:
447
+ with gr.Column(elem_id="col-container"):
448
+ gr.Markdown(
449
+ """
450
+ # 🛰️ UniSpace
451
+ **Unified Visual Representation and Scalable Multimodal Modeling** — an 8B
452
+ Mixture-of-Transformers that generates and edits images on top of a single
453
+ patch-reparameterized visual tokenizer.
454
+
455
+ [Paper](https://huggingface.co/papers/2608.08676) ·
456
+ [Model](https://huggingface.co/yjb6/UniSpace) ·
457
+ [Code](https://github.com/yjb6/UniSpace)
458
+ """
459
+ )
460
 
461
+ with gr.Tabs():
462
+ with gr.Tab("Text to image"):
463
+ with gr.Row():
464
+ with gr.Column():
465
+ g_prompt = gr.Textbox(
466
+ label="Prompt",
467
+ lines=3,
468
+ placeholder="A watercolor painting of a lighthouse during a winter sunrise.",
469
+ )
470
+ g_run = gr.Button("Generate", variant="primary")
471
+ with gr.Accordion("Advanced settings", open=False):
472
+ g_resolution = gr.Radio(
473
+ choices=[512, 768, 1024],
474
+ value=1024,
475
+ label="Resolution",
476
+ )
477
+ g_steps = gr.Slider(
478
+ 8, 60, value=DEFAULT_STEPS, step=1, label="Sampling steps"
479
+ )
480
+ g_cfg = gr.Slider(
481
+ 1.0, 15.0, value=DEFAULT_CFG, step=0.5,
482
+ label="Guidance scale (CFG)",
483
+ )
484
+ g_seed = gr.Slider(
485
+ 0, MAX_SEED, value=42, step=1, label="Seed"
486
+ )
487
+ g_rand = gr.Checkbox(value=False, label="Randomize seed")
488
+ with gr.Column():
489
+ g_out = gr.Image(label="Result", type="pil", height=520)
490
+ g_used_seed = gr.Number(label="Seed used", interactive=False)
491
+ g_info = gr.Textbox(label="Run info", interactive=False)
492
+
493
+ gr.Examples(
494
+ examples=GEN_EXAMPLES,
495
+ inputs=[g_prompt],
496
+ outputs=[g_out, g_used_seed, g_info],
497
+ fn=generate_example,
498
+ cache_examples=True,
499
+ cache_mode="lazy",
500
+ label="Prompts from the UniSpace release",
501
+ )
502
+
503
+ with gr.Tab("Instruction editing"):
504
+ with gr.Row():
505
+ with gr.Column():
506
+ e_image = gr.Image(label="Input image", type="pil", height=340)
507
+ e_prompt = gr.Textbox(
508
+ label="Editing instruction",
509
+ lines=2,
510
+ placeholder="Change the background to a forest.",
511
+ )
512
+ e_run = gr.Button("Edit", variant="primary")
513
+ with gr.Accordion("Advanced settings", open=False):
514
+ e_steps = gr.Slider(
515
+ 8, 60, value=DEFAULT_STEPS, step=1, label="Sampling steps"
516
+ )
517
+ e_cfg = gr.Slider(
518
+ 1.0, 15.0, value=DEFAULT_CFG, step=0.5,
519
+ label="Guidance scale (CFG)",
520
+ )
521
+ e_seed = gr.Slider(
522
+ 0, MAX_SEED, value=42, step=1, label="Seed"
523
+ )
524
+ e_rand = gr.Checkbox(value=False, label="Randomize seed")
525
+ with gr.Column():
526
+ e_out = gr.Image(label="Result", type="pil", height=520)
527
+ e_used_seed = gr.Number(label="Seed used", interactive=False)
528
+ e_info = gr.Textbox(label="Run info", interactive=False)
529
+
530
+ gr.Examples(
531
+ examples=EDIT_EXAMPLES,
532
+ inputs=[e_image, e_prompt],
533
+ outputs=[e_out, e_used_seed, e_info],
534
+ fn=edit_example,
535
+ cache_examples=True,
536
+ cache_mode="lazy",
537
+ label="Examples from the UniSpace project page (GEdit-Bench, MIT)",
538
+ )
539
+
540
+ g_run.click(
541
+ fn=generate,
542
+ inputs=[g_prompt, g_steps, g_cfg, g_resolution, g_seed, g_rand],
543
+ outputs=[g_out, g_used_seed, g_info],
544
+ api_name="generate",
545
  )
546
+ e_run.click(
547
+ fn=edit,
548
+ inputs=[e_image, e_prompt, e_steps, e_cfg, e_seed, e_rand],
549
+ outputs=[e_out, e_used_seed, e_info],
550
+ api_name="edit",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  )
552
 
553
+ if __name__ == "__main__":
554
+ demo.queue().launch(mcp_server=True)
examples/background_change.png ADDED

Git LFS Details

  • SHA256: 5800cc25b9b218c50fe07149011dccdab11b5d052bdc5353bc814979c23b019b
  • Pointer size: 131 Bytes
  • Size of remote file: 985 kB
examples/color_alter.png ADDED

Git LFS Details

  • SHA256: f2ac68e17b1568633f57f132b53b0416c1e1d8aa43424f52ba88f790bdfc6aa0
  • Pointer size: 131 Bytes
  • Size of remote file: 1,000 kB
examples/material_alter.png ADDED

Git LFS Details

  • SHA256: 3ef8390457b07ca1af04d6211f57754c6e93432389f8e569f7c24f554d623226
  • Pointer size: 131 Bytes
  • Size of remote file: 372 kB
examples/subject-add.png ADDED

Git LFS Details

  • SHA256: 37e0231cfe34ecc59ac75669fa05624b01764f7a27d100e7bf46f78edcffe162
  • Pointer size: 131 Bytes
  • Size of remote file: 493 kB
patch-reparameterization/configs/decoder/ViTXL_nockpt/config.json CHANGED
@@ -21,7 +21,7 @@
21
  "num_attention_heads": 12,
22
  "num_channels": 3,
23
  "num_hidden_layers": 12,
24
- "patch_size": 16,
25
  "qkv_bias": true,
26
  "torch_dtype": "float32",
27
  "transformers_version": "4.42.3",
 
21
  "num_attention_heads": 12,
22
  "num_channels": 3,
23
  "num_hidden_layers": 12,
24
+ "patch_size": "SHOULD BE RELOADED",
25
  "qkv_bias": true,
26
  "torch_dtype": "float32",
27
  "transformers_version": "4.42.3",
patch-reparameterization/configs/release/pr-dinov2-imagenet256-generation.yaml ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Release generation config for PR-DINOv2.
2
+ stage_1:
3
+ target: stage1.PatchReparam
4
+ params:
5
+ encoder_cls: DINOv2Unified
6
+ encoder_config_path: ${oc.env:MODEL_ROOT}/huggingface.co/facebook/dinov2-with-registers-base
7
+ encoder_input_size: 224
8
+ encoder_params:
9
+ model_name: ${oc.env:MODEL_ROOT}/huggingface.co/facebook/dinov2-with-registers-base
10
+ recon_proj_dim: 128
11
+ decoder_config_path: configs/decoder/ViTXL_nockpt
12
+ noise_tau: 0.8
13
+ decode_noise: true
14
+ reshape_to_2d: true
15
+ normalization_stat_path: ${oc.env:PR_STATS_ROOT}/pr-dinov2-normalization-stats.pt
16
+ norm_feature: merged_tokens
17
+ checkpoint:
18
+ path: ${oc.env:PR_CHECKPOINT_ROOT}/pr-dinov2-tokenizer.pt
19
+ use_ema: true
20
+ stage_2:
21
+ target: stage2.models.DDT.DiTwDDTHead
22
+ params:
23
+ input_size: 16
24
+ patch_size: 1
25
+ in_channels: 896
26
+ hidden_size: [1152, 2048]
27
+ depth: [28, 2]
28
+ num_heads: [16, 16]
29
+ mlp_ratio: 4.0
30
+ class_dropout_prob: 0.1
31
+ num_classes: 1000
32
+ use_qknorm: false
33
+ use_swiglu: true
34
+ use_rope: true
35
+ use_rmsnorm: true
36
+ wo_shift: false
37
+ use_pos_embed: true
38
+ ckpt: ${oc.env:PR_CHECKPOINT_ROOT}/pr-dinov2-dit.pt
39
+ transport:
40
+ params: {path_type: Linear, prediction: velocity, loss_weight: null, time_dist_type: logit-normal_0_1}
41
+ sampler:
42
+ mode: ODE
43
+ params: {sampling_method: euler, num_steps: 50, atol: 1.0e-6, rtol: 1.0e-3, reverse: false}
44
+ guidance: {method: cfg, scale: 1.0, t_min: 0.0, t_max: 1.0}
45
+ misc:
46
+ save_latents: false
47
+ latent_size: [896, 16, 16]
48
+ num_classes: 1000
49
+ time_dist_shift_dim: 229376
50
+ time_dist_shift_base: 4096
51
+ training:
52
+ encoder_output_key: merged_tokens_norm
53
+ sem_dim: 768
54
+ recon_loss_weight: 18.0
55
+ normalize_channel_weight: true
patch-reparameterization/configs/release/pr-dinov2-imagenet256.yaml ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Release evaluation config for PR-DINOv2.
2
+ # Set MODEL_ROOT, PR_CHECKPOINT_ROOT, and PR_STATS_ROOT before running.
3
+ stage_1:
4
+ target: stage1.PatchReparam
5
+ params:
6
+ encoder_cls: DINOv2Unified
7
+ encoder_config_path: ${oc.env:MODEL_ROOT}/huggingface.co/facebook/dinov2-with-registers-base
8
+ encoder_input_size: 224
9
+ encoder_params:
10
+ model_name: ${oc.env:MODEL_ROOT}/huggingface.co/facebook/dinov2-with-registers-base
11
+ recon_proj_dim: 128
12
+ decoder_config_path: configs/decoder/ViTXL_nockpt
13
+ noise_tau: 0.8
14
+ decode_noise: false
15
+ reshape_to_2d: true
16
+ normalization_stat_path: ${oc.env:PR_STATS_ROOT}/pr-dinov2-normalization-stats.pt
17
+ checkpoint:
18
+ path: ${oc.env:PR_CHECKPOINT_ROOT}/pr-dinov2-tokenizer.pt
19
+ use_ema: true
20
+
21
+ stage_2:
22
+ target: stage2.models.DDT.DiTwDDTHead
23
+ params:
24
+ input_size: 16
25
+ patch_size: 1
26
+ in_channels: 896
27
+ hidden_size: [1152, 2048]
28
+ depth: [28, 2]
29
+ num_heads: [16, 16]
30
+ mlp_ratio: 4.0
31
+ class_dropout_prob: 0.1
32
+ num_classes: 1000
33
+ use_qknorm: false
34
+ use_swiglu: true
35
+ use_rope: true
36
+ use_rmsnorm: true
37
+ wo_shift: false
38
+ use_pos_embed: true
39
+ ckpt: ${oc.env:PR_CHECKPOINT_ROOT}/pr-dinov2-dit.pt
40
+
41
+ transport:
42
+ params:
43
+ path_type: Linear
44
+ prediction: velocity
45
+ loss_weight: null
46
+ time_dist_type: logit-normal_0_1
47
+ sampler:
48
+ mode: ODE
49
+ params: {sampling_method: euler, num_steps: 50, atol: 1.0e-6, rtol: 1.0e-3, reverse: false}
50
+ guidance: {method: cfg, scale: 1.0, t_min: 0.0, t_max: 1.0}
51
+ misc:
52
+ save_latents: false
53
+ latent_size: [896, 16, 16]
54
+ num_classes: 1000
55
+ time_dist_shift_dim: 229376
56
+ time_dist_shift_base: 4096
57
+ training:
58
+ encoder_output_key: merged_tokens_norm
59
+ sem_dim: 768
60
+ recon_loss_weight: 18.0
61
+ normalize_channel_weight: true
62
+ eval:
63
+ data_path: ${oc.env:IMAGENET_ROOT}/val
64
+ reference_npz_path: ${oc.env:IMAGENET_VAL_NPZ}
65
+ metrics: [psnr, ssim, rfid]
66
+ eval_weights: ema_only
patch-reparameterization/configs/release/pr-qwen-vit-imagenet256.yaml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Release reconstruction config for PR-Qwen-ViT (Table 3).
2
+ # Set MODEL_ROOT, PR_CHECKPOINT_ROOT, and PR_STATS_ROOT before running.
3
+ stage_1:
4
+ target: stage1.PatchReparam
5
+ params:
6
+ encoder_cls: Qwen3Unified
7
+ encoder_config_path: ${oc.env:QWEN_MODEL_ROOT}/Qwen/Qwen3-VL-8B-Instruct
8
+ encoder_input_size: 256
9
+ encoder_params:
10
+ model_name: ${oc.env:QWEN_MODEL_ROOT}/Qwen/Qwen3-VL-8B-Instruct
11
+ recon_proj_dim: 128
12
+ pixel_format: c_major
13
+ decoder_config_path: configs/decoder/ViTXL_nockpt
14
+ decoder_cls: ViTDecoder
15
+ noise_tau: 0.8
16
+ reshape_to_2d: true
17
+ normalization_stat_path: ${oc.env:PR_STATS_ROOT}/pr-qwen-vit-normalization-stats.pt
18
+ checkpoint:
19
+ path: ${oc.env:PR_CHECKPOINT_ROOT}/pr-qwen-vit-tokenizer.pt
20
+ use_ema: true
21
+
22
+ eval:
23
+ data_path: ${oc.env:IMAGENET_ROOT}/val
24
+ reference_npz_path: ${oc.env:IMAGENET_VAL_NPZ}
25
+ metrics: [psnr, ssim, rfid]
26
+ eval_weights: ema_only
patch-reparameterization/configs/release/pr-siglip2-imagenet256-generation.yaml ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Release generation config for PR-SigLIP2.
2
+ stage_1:
3
+ target: stage1.PatchReparam
4
+ params:
5
+ encoder_cls: SigLIP2Unified
6
+ encoder_config_path: ${oc.env:MODEL_ROOT}/siglip2-base-patch16-256/main
7
+ encoder_input_size: 256
8
+ encoder_params:
9
+ model_name: ${oc.env:MODEL_ROOT}/siglip2-base-patch16-256/main
10
+ recon_proj_dim: 128
11
+ decoder_config_path: configs/decoder/ViTXL_nockpt
12
+ noise_tau: 0.8
13
+ decode_noise: true
14
+ reshape_to_2d: true
15
+ normalization_stat_path: ${oc.env:PR_STATS_ROOT}/pr-siglip2-normalization-stats.pt
16
+ norm_feature: merged_tokens
17
+ checkpoint:
18
+ path: ${oc.env:PR_CHECKPOINT_ROOT}/pr-siglip2-tokenizer.pt
19
+ use_ema: true
20
+ stage_2:
21
+ target: stage2.models.DDT.DiTwDDTHead
22
+ params:
23
+ input_size: 16
24
+ patch_size: 1
25
+ in_channels: 896
26
+ hidden_size: [1152, 2048]
27
+ depth: [28, 2]
28
+ num_heads: [16, 16]
29
+ mlp_ratio: 4.0
30
+ class_dropout_prob: 0.1
31
+ num_classes: 1000
32
+ use_qknorm: false
33
+ use_swiglu: true
34
+ use_rope: true
35
+ use_rmsnorm: true
36
+ wo_shift: false
37
+ use_pos_embed: true
38
+ ckpt: ${oc.env:PR_CHECKPOINT_ROOT}/pr-siglip2-dit.pt
39
+ transport:
40
+ params: {path_type: Linear, prediction: velocity, loss_weight: null, time_dist_type: logit-normal_0_1}
41
+ sampler:
42
+ mode: ODE
43
+ params: {sampling_method: euler, num_steps: 50, atol: 1.0e-6, rtol: 1.0e-3, reverse: false}
44
+ guidance: {method: cfg, scale: 1.0, t_min: 0.0, t_max: 1.0}
45
+ misc:
46
+ save_latents: false
47
+ latent_size: [896, 16, 16]
48
+ num_classes: 1000
49
+ time_dist_shift_dim: 229376
50
+ time_dist_shift_base: 4096
51
+ training:
52
+ encoder_output_key: merged_tokens_norm
53
+ sem_dim: 768
54
+ recon_loss_weight: 18.0
55
+ normalize_channel_weight: true
patch-reparameterization/configs/release/pr-siglip2-imagenet256.yaml ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Release evaluation config for PR-SigLIP2.
2
+ # Set MODEL_ROOT, PR_CHECKPOINT_ROOT, and PR_STATS_ROOT before running.
3
+ stage_1:
4
+ target: stage1.PatchReparam
5
+ params:
6
+ encoder_cls: SigLIP2wTwoBackbone
7
+ encoder_config_path: ${oc.env:MODEL_ROOT}/siglip2-base-patch16-256/main
8
+ encoder_input_size: 256
9
+ encoder_params:
10
+ sementic_model_name: ${oc.env:MODEL_ROOT}/siglip2-base-patch16-256/main
11
+ recon_model_name: ${oc.env:MODEL_ROOT}/siglip2-base-patch16-256/main
12
+ num_tokens: 256
13
+ recon_post_norm_type: no_norm
14
+ sementic_post_norm_type: no_norm
15
+ merger_cls: ConcatMerger
16
+ merger_kwargs:
17
+ recon_proj_dim: 128
18
+ recon_proj_cls: LinearAdapter
19
+ recon_proj_kwargs: {}
20
+ decoder_config_path: configs/decoder/ViTXL_nockpt
21
+ noise_tau: 0.8
22
+ decode_noise: false
23
+ reshape_to_2d: true
24
+ checkpoint:
25
+ path: ${oc.env:PR_SIGLIP2_TOKENIZER_CKPT}
26
+ use_ema: true
27
+
28
+ stage_2:
29
+ target: stage2.models.DDT.DiTwDDTHead
30
+ params:
31
+ input_size: 16
32
+ patch_size: 1
33
+ in_channels: 896
34
+ hidden_size: [1152, 2048]
35
+ depth: [28, 2]
36
+ num_heads: [16, 16]
37
+ mlp_ratio: 4.0
38
+ class_dropout_prob: 0.1
39
+ num_classes: 1000
40
+ use_qknorm: false
41
+ use_swiglu: true
42
+ use_rope: true
43
+ use_rmsnorm: true
44
+ wo_shift: false
45
+ use_pos_embed: true
46
+ ckpt: ${oc.env:PR_CHECKPOINT_ROOT}/pr-siglip2-dit.pt
47
+
48
+ transport:
49
+ params:
50
+ path_type: Linear
51
+ prediction: velocity
52
+ loss_weight: null
53
+ time_dist_type: logit-normal_0_1
54
+ sampler:
55
+ mode: ODE
56
+ params: {sampling_method: euler, num_steps: 50, atol: 1.0e-6, rtol: 1.0e-3, reverse: false}
57
+ guidance: {method: cfg, scale: 1.0, t_min: 0.0, t_max: 1.0}
58
+ misc:
59
+ save_latents: false
60
+ latent_size: [896, 16, 16]
61
+ num_classes: 1000
62
+ time_dist_shift_dim: 229376
63
+ time_dist_shift_base: 4096
64
+ training:
65
+ encoder_output_key: merged_tokens_norm
66
+ sem_dim: 768
67
+ recon_loss_weight: 18.0
68
+ normalize_channel_weight: true
69
+ eval:
70
+ data_path: ${oc.env:IMAGENET_ROOT}/val
71
+ reference_npz_path: ${oc.env:IMAGENET_VAL_NPZ}
72
+ metrics: [psnr, ssim, rfid]
73
+ eval_weights: ema_only
patch-reparameterization/src/eval/__init__.py ADDED
@@ -0,0 +1,580 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .ref_iqa import calculate_psnr, calculate_lpips, calculate_ssim
2
+ from .fid import calculate_rfid, calculate_gfid
3
+ import numpy as np
4
+ import torch
5
+ import torch.nn.functional as F
6
+ from PIL import Image
7
+ from torch.cuda.amp import autocast
8
+ from typing import Union, Dict, Optional
9
+ try:
10
+ from accelerate import Accelerator
11
+ except ImportError:
12
+ Accelerator = None
13
+ from torch.utils.data import DataLoader, Subset
14
+ from tqdm import tqdm
15
+ import os
16
+ import sys
17
+ from functools import lru_cache
18
+ @lru_cache(maxsize=4)
19
+ def _load_encoder_cached(model_path: str, device_str: str):
20
+ """
21
+ 懒加载并缓存 encoder(按 model_path + device 缓存,进程内只加载一次)。
22
+ 使用 transformers.AutoModel + AutoProcessor。
23
+ """
24
+ from transformers import AutoModel, AutoProcessor
25
+ print(f"[CosSim] Loading encoder from {model_path} onto {device_str} (cached)...")
26
+ processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True)
27
+ model = AutoModel.from_pretrained(model_path, trust_remote_code=True)
28
+ model = model.to(device_str).eval()
29
+ return model, processor
30
+
31
+
32
+ @torch.no_grad()
33
+ def calculate_encoder_cos_sim(
34
+ ref_arr: np.ndarray,
35
+ rec_arr: np.ndarray,
36
+ encoder_model: str,
37
+ device: torch.device,
38
+ batch_size: int = 64,
39
+ ) -> float:
40
+ """
41
+ 用指定 encoder 对 GT 图像和重建图像编码,计算 image embedding 的 cos 相似度均值。
42
+
43
+ Args:
44
+ ref_arr: GT 图像 [N, H, W, C] uint8
45
+ rec_arr: 重建图像 [N, H, W, C] uint8
46
+ encoder_model: HuggingFace 模型路径或名称
47
+ device: 计算设备
48
+ batch_size: 每批编码的图像数
49
+
50
+ Returns:
51
+ 所有样本的 cos 相似度均值 (float)
52
+ """
53
+ device_str = "cuda" if device.type == "cuda" else "cpu"
54
+ model, processor = _load_encoder_cached(encoder_model, device_str)
55
+
56
+ N = len(ref_arr)
57
+ cos_sims = []
58
+
59
+ for start in range(0, N, batch_size):
60
+ end = min(start + batch_size, N)
61
+ ref_batch = [Image.fromarray(img) for img in ref_arr[start:end]]
62
+ rec_batch = [Image.fromarray(img) for img in rec_arr[start:end]]
63
+
64
+ # 预处理:使用 processor 处理两组图像
65
+ ref_inputs = processor(images=ref_batch, return_tensors="pt").to(device_str)
66
+ rec_inputs = processor(images=rec_batch, return_tensors="pt").to(device_str)
67
+
68
+ # 提取 image embedding
69
+ ref_feats = model.get_image_features(**ref_inputs) # [B, D]
70
+ rec_feats = model.get_image_features(**rec_inputs) # [B, D]
71
+
72
+ # L2 归一化后计算 cos 相似度
73
+ ref_feats = F.normalize(ref_feats.float(), dim=-1)
74
+ rec_feats = F.normalize(rec_feats.float(), dim=-1)
75
+ batch_cos = (ref_feats * rec_feats).sum(dim=-1) # [B]
76
+ cos_sims.append(batch_cos.cpu())
77
+
78
+ cos_sim_mean = torch.cat(cos_sims).mean().item()
79
+ return cos_sim_mean
80
+
81
+
82
+ def compute_reconstruction_metrics(
83
+ ref_arr: np.ndarray,
84
+ rec_arr: np.ndarray,
85
+ device: torch.device,
86
+ batch_size: int = 128,
87
+ metrics_to_compute = ("psnr", "ssim", "rfid"),
88
+ disable_bar: bool = True,
89
+ encoder_model: Optional[str] = None,
90
+ psnr_resize: Optional[int] = None,
91
+ l1_resize: Optional[int] = None,
92
+ ) -> Dict[str, float]:
93
+ """
94
+ Compute reconstruction metrics between reference and reconstructed images.
95
+
96
+ Args:
97
+ ref_arr: Reference images [N, H, W, C] uint8
98
+ rec_arr: Reconstructed images [N, H, W, C] uint8
99
+ device: Device for computation
100
+ batch_size: Batch size for metric computation
101
+
102
+ Returns:
103
+ Dictionary with metrics: eval/psnr, eval/ssim, eval/rfid
104
+ Note: LPIPS is not computed here since it's already tracked during training
105
+ """
106
+ device_str = "cuda" if device.type == "cuda" else "cpu"
107
+ # For paired metrics (psnr, ssim, l1, cos_sim), align ref/rec count
108
+ n_paired = min(len(ref_arr), len(rec_arr))
109
+ ref_paired = ref_arr[:n_paired]
110
+ rec_paired = rec_arr[:n_paired]
111
+ results_dict = {}
112
+ if 'psnr' in metrics_to_compute:
113
+ psnr = calculate_psnr(ref_paired, rec_paired, batch_size, device_str, disable_bar=disable_bar, psnr_resize=psnr_resize)
114
+ results_dict["psnr"] = psnr
115
+ if 'ssim' in metrics_to_compute:
116
+ ssim = calculate_ssim(ref_paired, rec_paired, batch_size, device_str, disable_bar=disable_bar)
117
+ results_dict["ssim"] = ssim
118
+ if 'rfid' in metrics_to_compute:
119
+ rfid = calculate_rfid(ref_arr, rec_arr, batch_size, device_str)
120
+ results_dict["rfid"] = rfid
121
+ if 'l1' in metrics_to_compute:
122
+ # L1 = mean(|rec - ref|), same as train: (recon - images).abs().mean()
123
+ ref_f = ref_paired.astype(np.float32) / 255.0
124
+ rec_f = rec_paired.astype(np.float32) / 255.0
125
+ if l1_resize is not None:
126
+ # resize via torch interpolate to keep consistent with psnr_resize style
127
+ # Process in small batches to avoid CUDA OOM on large datasets
128
+ device_str = "cuda" if device.type == "cuda" else "cpu"
129
+ resize_batch = 64
130
+ n = ref_f.shape[0]
131
+ l1_sum = 0.0
132
+ l1_count = 0
133
+ for i in range(0, n, resize_batch):
134
+ ref_b = torch.from_numpy(ref_f[i:i+resize_batch]).permute(0, 3, 1, 2).to(device_str)
135
+ rec_b = torch.from_numpy(rec_f[i:i+resize_batch]).permute(0, 3, 1, 2).to(device_str)
136
+ ref_b = F.interpolate(ref_b, size=(l1_resize, l1_resize), mode="bilinear", align_corners=False)
137
+ rec_b = F.interpolate(rec_b, size=(l1_resize, l1_resize), mode="bilinear", align_corners=False)
138
+ l1_sum += (ref_b - rec_b).abs().sum().item()
139
+ l1_count += ref_b.numel()
140
+ l1 = l1_sum / l1_count
141
+ else:
142
+ l1 = np.abs(rec_f - ref_f).mean()
143
+ results_dict["l1"] = float(l1)
144
+ if 'cos_sim' in metrics_to_compute:
145
+ assert encoder_model is not None, \
146
+ "'cos_sim' metric requires encoder_model (HuggingFace model path) to be specified."
147
+ print(f"[CosSim] Computing encoder cos_sim with model: {encoder_model}")
148
+ cos_sim = calculate_encoder_cos_sim(
149
+ ref_paired, rec_paired,
150
+ encoder_model=encoder_model,
151
+ device=device,
152
+ batch_size=batch_size,
153
+ )
154
+ results_dict["cos_sim"] = cos_sim
155
+ assert len(results_dict) > 0, "No metrics were computed."
156
+ return results_dict
157
+ def compute_generation_metrics(
158
+ ref_arr: np.ndarray,
159
+ rec_arr: np.ndarray,
160
+ device: torch.device,
161
+ batch_size: int = 128,
162
+ ):
163
+ device_str = "cuda" if device.type == "cuda" else "cpu"
164
+ # only eval FID
165
+ fid = calculate_gfid(rec_arr, ref_arr, batch_size, device_str)
166
+ return {
167
+ 'fid': fid
168
+ }
169
+ @torch.no_grad()
170
+ def evaluate_generation_distributed(
171
+ model_fn,
172
+ sample_fn,
173
+ latent_size, # for noise
174
+ additional_model_kwargs,
175
+ use_guidance: bool,
176
+ pr,
177
+ val_dataset,
178
+ num_samples: int,
179
+ batch_size: int,
180
+ rank: Union[int, None] = None,
181
+ world_size: Union[int, None] = None,
182
+ device: torch.device = None,
183
+ experiment_dir: str = None,
184
+ global_step: int = None,
185
+ autocast_kwargs: dict = None,
186
+ metric_batch_size: int = 128,
187
+ reference_npz_path: Optional[str] = None,
188
+ accelerator: Union[Accelerator, None] = None,
189
+ ) -> Optional[Dict[str, float]]:
190
+ """
191
+ Evaluate generation metrics using all GPUs in a distributed manner.
192
+
193
+ Args:
194
+ val_dataset: Validation dataset
195
+ batch_size: Batch size per GPU for generation
196
+ rank: Current GPU rank (deprecated, use accelerator instead)
197
+ world_size: Total number of GPUs (deprecated, use accelerator instead)
198
+ device: Device to use (deprecated, use accelerator instead)
199
+ experiment_dir: Experiment directory
200
+ global_step: Current training step
201
+ autocast_kwargs: Autocast configuration
202
+ metric_batch_size: Batch size for metric computation (on rank 0)
203
+ reference_npz_path: Optional path to existing reference NPZ file
204
+ accelerator: Accelerator instance (preferred over rank/world_size/device)
205
+
206
+ Returns:
207
+ Dictionary of metrics (only on rank 0, None on other ranks)
208
+ """
209
+ # Support both Accelerator and legacy DDP interface
210
+ if accelerator is not None:
211
+ rank = accelerator.process_index
212
+ world_size = accelerator.num_processes
213
+ device = accelerator.device
214
+ is_main_process = accelerator.is_main_process
215
+ wait_for_everyone = accelerator.wait_for_everyone
216
+ else:
217
+ # Legacy DDP mode
218
+ import torch.distributed as dist
219
+ if rank is None or world_size is None or device is None:
220
+ raise ValueError("Either accelerator must be provided, or rank/world_size/device must be provided")
221
+ is_main_process = (rank == 0)
222
+ wait_for_everyone = dist.barrier
223
+
224
+ # model.eval()
225
+ # Save shard NPZ
226
+ temp_dir = os.path.join(experiment_dir, "eval_npzs")
227
+ if is_main_process:
228
+ print(f"\n[Eval] Starting distributed sampling evaluation at step {global_step}")
229
+ os.makedirs(temp_dir, exist_ok=True)
230
+
231
+ # Wait for rank 0 to create the directory before other ranks try to save
232
+ wait_for_everyone()
233
+ # print(f"[Rank {rank}] Starting sampling...")
234
+ # Each rank processes its shard
235
+ N = min(len(val_dataset), num_samples)
236
+ chunk = N // world_size
237
+
238
+ if rank < world_size - 1:
239
+ start = rank * chunk
240
+ end = (rank + 1) * chunk
241
+ else:
242
+ # Last rank takes the remainder (and handles N < world_size gracefully)
243
+ start = rank * chunk
244
+ end = N
245
+
246
+ rank_indices = list(range(start, end))
247
+ subset = Subset(val_dataset, rank_indices)
248
+ loader = DataLoader(
249
+ subset,
250
+ batch_size=batch_size,
251
+ shuffle=False,
252
+ num_workers=4,
253
+ pin_memory=True,
254
+ drop_last=False,
255
+ )
256
+
257
+ # Reconstruct images on this rank
258
+ generations = []
259
+ iterator = tqdm(loader, desc=f"[Rank {rank}] Sampling", file=sys.stdout) if is_main_process else loader
260
+
261
+ with torch.inference_mode():
262
+ for _, label in iterator: # don't actually need images at sampling time
263
+ n = label.size(0)
264
+ z = torch.randn(n, *latent_size, device = device)
265
+ y = label.to(device)
266
+ if use_guidance:
267
+ z = torch.cat([z, z], dim=0)
268
+ y_null = torch.full((n,), null_label, device=device)
269
+ y = torch.cat([y, y_null], dim=0)
270
+ model_kwargs = dict(y=y, **additional_model_kwargs)
271
+ with autocast(**autocast_kwargs):
272
+ samples = sample_fn(z, model_fn, **model_kwargs)[-1]
273
+ if use_guidance:
274
+ samples = samples.chunk(2, dim = 0)
275
+ if callable(pr) and not hasattr(pr, 'decode'):
276
+ samples = pr(samples)
277
+ else:
278
+ samples = pr.decode(samples)
279
+ if hasattr(samples, 'x_rec'):
280
+ samples = samples.x_rec
281
+ samples = samples.clamp(0, 1)
282
+ gen_np = samples.mul(255).permute(0, 2, 3, 1).to("cpu", dtype=torch.uint8).numpy()
283
+ for img in gen_np:
284
+ generations.append(img)
285
+
286
+ generations = np.stack(generations)
287
+ shard_path = os.path.join(temp_dir, f"gen_{global_step:07d}_{rank:02d}.npz")
288
+ np.savez(shard_path, arr_0=generations)
289
+
290
+ if is_main_process:
291
+ print(f"[Rank {rank}] Saved {len(generations)} generation to {shard_path}")
292
+
293
+ # Wait for all ranks to finish generation
294
+ wait_for_everyone()
295
+
296
+ # Rank 0 computes metrics
297
+ metrics = None
298
+ if is_main_process:
299
+ # Combine all generation shards
300
+ all_gens = []
301
+ for r in range(world_size):
302
+ shard_file = os.path.join(temp_dir, f"gen_{global_step:07d}_{r:02d}.npz")
303
+ shard_data = np.load(shard_file)["arr_0"]
304
+ all_gens.append(shard_data)
305
+
306
+ combined_recons = np.concatenate(all_gens, axis=0)[:num_samples]
307
+ print(f"[Eval] Combined generation NPZ shape: {combined_recons.shape}")
308
+
309
+ # Load reference NPZ
310
+ ref_npz_path = reference_npz_path
311
+
312
+ if not os.path.exists(ref_npz_path):
313
+ raise FileNotFoundError(f"Reference NPZ not found at {ref_npz_path}")
314
+
315
+ ref_stats = np.load(ref_npz_path)
316
+ print(f"[Eval] Loaded reference NPZ from {ref_npz_path}")
317
+
318
+ # Compute metrics
319
+ print("[Eval] Computing metrics...")
320
+ metrics = compute_generation_metrics(
321
+ ref_stats,
322
+ combined_recons,
323
+ device,
324
+ metric_batch_size,
325
+ )
326
+
327
+ # Print results
328
+ print(f"[Eval] Step {global_step} Metrics:")
329
+ for key, value in metrics.items():
330
+ print(f" {key}: {value:.6f}")
331
+
332
+ # Cleanup reconstruction shards
333
+ for r in range(world_size):
334
+ shard_file = os.path.join(temp_dir, f"gen_{global_step:07d}_{r:02d}.npz")
335
+ if os.path.exists(shard_file):
336
+ os.remove(shard_file)
337
+
338
+ wait_for_everyone()
339
+ return metrics
340
+ @torch.no_grad()
341
+ def evaluate_reconstruction_distributed(
342
+ model,
343
+ val_dataset,
344
+ num_samples: int,
345
+ batch_size: int,
346
+ rank: Union[int, None] = None,
347
+ world_size: Union[int, None] = None,
348
+ device: torch.device = None,
349
+ experiment_dir: str = None,
350
+ global_step: int = None,
351
+ autocast_kwargs: dict = None,
352
+ metric_batch_size: int = 128,
353
+ reference_npz_path: Optional[str] = None,
354
+ metrics_to_compute: Optional[list] = ("psnr", "ssim", "rfid"),
355
+ accelerator: Union[Accelerator, None] = None,
356
+ save_recon_dir: Optional[str] = None,
357
+ image_features_mode: Optional[str] = None,
358
+ encoder_cos_sim_model: Optional[str] = None,
359
+ psnr_resize: Optional[int] = None,
360
+ l1_resize: Optional[int] = None,
361
+ ) -> Optional[Dict[str, float]]:
362
+ """
363
+ Evaluate reconstruction metrics using all GPUs in a distributed manner.
364
+
365
+ Args:
366
+ model: Model to evaluate (should be in eval mode)
367
+ val_dataset: Validation dataset
368
+ batch_size: Batch size per GPU for reconstruction
369
+ rank: Current GPU rank (deprecated, use accelerator instead)
370
+ world_size: Total number of GPUs (deprecated, use accelerator instead)
371
+ device: Device to use (deprecated, use accelerator instead)
372
+ experiment_dir: Experiment directory
373
+ global_step: Current training step
374
+ autocast_kwargs: Autocast configuration
375
+ metric_batch_size: Batch size for metric computation (on rank 0)
376
+ reference_npz_path: Optional path to existing reference NPZ file
377
+ accelerator: Accelerator instance (preferred over rank/world_size/device)
378
+
379
+ Returns:
380
+ Dictionary of metrics (only on rank 0, None on other ranks)
381
+ """
382
+ # Support both Accelerator and legacy DDP interface
383
+ if accelerator is not None:
384
+ rank = accelerator.process_index
385
+ world_size = accelerator.num_processes
386
+ device = accelerator.device
387
+ is_main_process = accelerator.is_main_process
388
+ wait_for_everyone = accelerator.wait_for_everyone
389
+ else:
390
+ # Legacy DDP mode
391
+ import torch.distributed as dist
392
+ if rank is None or world_size is None or device is None:
393
+ raise ValueError("Either accelerator must be provided, or rank/world_size/device must be provided")
394
+ is_main_process = (rank == 0)
395
+ wait_for_everyone = dist.barrier
396
+
397
+ model.eval()
398
+ # Save shard NPZ
399
+ temp_dir = os.path.join(experiment_dir, "eval_npzs")
400
+ if is_main_process:
401
+ print(f"\n[Eval] Starting distributed reconstruction evaluation at step {global_step}")
402
+ os.makedirs(temp_dir, exist_ok=True)
403
+ # Wait for rank 0 to create the directory before other ranks try to save
404
+ wait_for_everyone()
405
+ # print(f"[Rank {rank}] Starting reconstruction...")
406
+ # Each rank processes its shard
407
+ N = min(len(val_dataset), num_samples)
408
+ chunk = N // world_size
409
+
410
+ if rank < world_size - 1:
411
+ start = rank * chunk
412
+ end = (rank + 1) * chunk
413
+ else:
414
+ # Last rank takes the remainder (and handles N < world_size gracefully)
415
+ start = rank * chunk
416
+ end = N
417
+
418
+ rank_indices = list(range(start, end))
419
+ subset = Subset(val_dataset, rank_indices)
420
+ loader = DataLoader(
421
+ subset,
422
+ batch_size=batch_size,
423
+ shuffle=False,
424
+ num_workers=4,
425
+ pin_memory=True,
426
+ drop_last=False,
427
+ )
428
+
429
+ # Reconstruct images on this rank, keep originals for distributed metrics
430
+ reconstructions = []
431
+ originals = []
432
+ iterator = tqdm(loader, desc=f"[Rank {rank}] Reconstructing", file=sys.stdout) if is_main_process else loader
433
+
434
+ with torch.inference_mode():
435
+ for images, _ in iterator:
436
+ images = images.to(device, non_blocking=True)
437
+ # Save originals before model forward
438
+ orig_np = images.clamp(0, 1).mul(255).permute(0, 2, 3, 1).to("cpu", dtype=torch.uint8).numpy()
439
+ with autocast(**autocast_kwargs):
440
+ if image_features_mode is not None:
441
+ recon = model(images, image_features_mode=image_features_mode)
442
+ else:
443
+ recon = model(images)
444
+
445
+ # Handle DecodeOutput from split_adaln decoder
446
+ if hasattr(recon, 'x_rec'):
447
+ recon = recon.x_rec
448
+ # Convert to numpy uint8 [H, W, C]
449
+ recon = recon.clamp(0, 1)
450
+ recon_np = recon.mul(255).permute(0, 2, 3, 1).to("cpu", dtype=torch.uint8).numpy()
451
+
452
+ for img in recon_np:
453
+ reconstructions.append(img)
454
+ for img in orig_np:
455
+ originals.append(img)
456
+
457
+ reconstructions = np.stack(reconstructions)
458
+ originals = np.stack(originals)
459
+
460
+ # Save shard for backward compat
461
+ shard_path = os.path.join(temp_dir, f"recon_{global_step:07d}_{rank:02d}.npz")
462
+ np.savez(shard_path, arr_0=reconstructions)
463
+ if is_main_process:
464
+ print(f"[Rank {rank}] Saved {len(reconstructions)} reconstructions to {shard_path}")
465
+
466
+ # Wait for all ranks to finish reconstruction
467
+ wait_for_everyone()
468
+
469
+ # ── Distributed metrics: each rank computes on its own subset ──────────
470
+ import torch.distributed as dist
471
+ from .fid import compute_inception_features, calculate_rfid_from_features
472
+ local_n = len(reconstructions)
473
+
474
+ # PSNR / SSIM: compute locally then all_reduce weighted sum
475
+ scalar_metrics_to_compute = [m for m in (metrics_to_compute or []) if m in ('psnr', 'ssim', 'lpips')]
476
+ local_scalar = {}
477
+ if scalar_metrics_to_compute:
478
+ if is_main_process:
479
+ print(f"[Eval] Rank {rank}: computing {scalar_metrics_to_compute} on {local_n} images...")
480
+ local_scalar = compute_reconstruction_metrics(
481
+ originals, reconstructions, device, metric_batch_size,
482
+ metrics_to_compute=scalar_metrics_to_compute,
483
+ disable_bar=is_main_process,
484
+ encoder_model=encoder_cos_sim_model,
485
+ psnr_resize=psnr_resize,
486
+ l1_resize=l1_resize,
487
+ )
488
+
489
+ # Weighted all_reduce: sum(metric * count) / total_count
490
+ global_scalar = {}
491
+ count_t = torch.tensor(float(local_n), device=device)
492
+ distributed = world_size > 1 and dist.is_available() and dist.is_initialized()
493
+ if distributed:
494
+ dist.all_reduce(count_t, op=dist.ReduceOp.SUM)
495
+ for k, v in local_scalar.items():
496
+ val_t = torch.tensor(v * local_n, dtype=torch.float64, device=device)
497
+ if distributed:
498
+ dist.all_reduce(val_t, op=dist.ReduceOp.SUM)
499
+ global_scalar[k] = (val_t / count_t).item()
500
+
501
+ # rFID: each rank computes inception features, all_gather, rank 0 computes FID
502
+ global_rfid = {}
503
+ if 'rfid' in (metrics_to_compute or []):
504
+ if is_main_process:
505
+ print(f"[Eval] Rank {rank}: computing inception features on {local_n} images...")
506
+ local_recon_feats = compute_inception_features(reconstructions, metric_batch_size, device)
507
+ local_ref_feats = compute_inception_features(originals, metric_batch_size, device)
508
+
509
+ recon_t = torch.from_numpy(local_recon_feats).to(device)
510
+ ref_t = torch.from_numpy(local_ref_feats).to(device)
511
+
512
+ if distributed:
513
+ all_recon = [torch.zeros_like(recon_t) for _ in range(world_size)]
514
+ all_ref = [torch.zeros_like(ref_t) for _ in range(world_size)]
515
+ dist.all_gather(all_recon, recon_t)
516
+ dist.all_gather(all_ref, ref_t)
517
+ else:
518
+ all_recon = [recon_t]
519
+ all_ref = [ref_t]
520
+
521
+ if is_main_process:
522
+ combined_recon_feats = torch.cat(all_recon, dim=0).cpu().double().numpy()
523
+ combined_ref_feats = torch.cat(all_ref, dim=0).cpu().double().numpy()
524
+ rfid = calculate_rfid_from_features(combined_recon_feats, combined_ref_feats)
525
+ global_rfid = {'rfid': rfid}
526
+
527
+ metrics = None
528
+ if is_main_process:
529
+ metrics = {**global_scalar, **global_rfid}
530
+ # Persist the all-reduced count so release validation proves how many
531
+ # examples were actually processed rather than trusting CLI intent.
532
+ metrics["_evaluated_samples"] = int(count_t.item())
533
+ print(f"[Eval] Step {global_step} Metrics:")
534
+ for key, value in metrics.items():
535
+ print(f" {key}: {value:.6f}")
536
+
537
+ # Optionally save reconstructed images
538
+ if save_recon_dir:
539
+ import threading
540
+ recons_copy = np.copy(reconstructions)
541
+ save_dir = save_recon_dir
542
+ def _save_recon_images():
543
+ os.makedirs(save_dir, exist_ok=True)
544
+ for i, img in enumerate(recons_copy):
545
+ Image.fromarray(img).save(os.path.join(save_dir, f"recon_{i:06d}.png"))
546
+ print(f"[Eval] Saved {len(recons_copy)} reconstruction images to {save_dir}")
547
+ threading.Thread(target=_save_recon_images, daemon=False).start()
548
+
549
+ # Cleanup shards
550
+ for r in range(world_size):
551
+ shard_file = os.path.join(temp_dir, f"recon_{global_step:07d}_{r:02d}.npz")
552
+ if os.path.exists(shard_file):
553
+ os.remove(shard_file)
554
+
555
+ wait_for_everyone()
556
+ model.train()
557
+
558
+ return metrics
559
+ if __name__ == "__main__":
560
+ import argparse
561
+ parser = argparse.ArgumentParser()
562
+ parser.add_argument("--ref-img", type=str, default="samples/imagenet-256-val.npz")
563
+ parser.add_argument("--rec-img", type=str, default="samples/sdvae-ft-mse-f8d4.npz")
564
+ parser.add_argument("--bs", type=int, default=128)
565
+ args = parser.parse_args()
566
+
567
+ # Load images
568
+ device = "cuda"
569
+ ref_img = np.load(args.ref_img)["arr_0"]
570
+ rec_img = np.load(args.rec_img)["arr_0"]
571
+ print(f"Loaded images: ref: {ref_img.shape}, rec: {rec_img.shape}")
572
+
573
+ psnr = calculate_psnr(ref_img, rec_img, args.bs, device)
574
+ print(f"PSNR: {psnr:.6f}")
575
+ lpips = calculate_lpips(ref_img, rec_img, args.bs, device)
576
+ print(f"LPIPS: {lpips:.6f}")
577
+ ssim_val = calculate_ssim(ref_img, rec_img, args.bs, device)
578
+ print(f"SSIM: {ssim_val:.6f}")
579
+ rfid = calculate_rfid(ref_img, rec_img, args.bs, device)
580
+ print(f"rFID: {rfid:.6f}")
patch-reparameterization/src/eval/clip_t.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn.functional as F
3
+ from torch.utils.data import DataLoader
4
+ from torchvision import datasets
5
+ from transformers import SiglipModel, SiglipProcessor
6
+ from tqdm import tqdm
7
+ import os
8
+
9
+ # =================配置部分=================
10
+ # 本地模型路径 (包含 model.safetensors, config.json, preprocessor_config.json 等)
11
+ # MODEL_PATH = HF_HOME 下 siglip2-base-patch16-256
12
+ # MODEL_PATH = CKPT_ROOT 下 siglip2/test/ep-last/
13
+ MODEL_PATH = os.environ.get("SIGLIP2_MODEL_PATH", "google/siglip2-base-patch16-256")
14
+ # ImageNet 验证集根目录 (即包含 n01440764 等文件夹的上一级目录)
15
+ IMAGENET_VAL_PATH = os.path.join(os.environ.get("CLIP_BENCH_ROOT", "/path/to/clip_benchmark"), "imagenet-1k", "val")
16
+
17
+ BATCH_SIZE = 256
18
+ NUM_WORKERS = 8
19
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
20
+
21
+ # =================ImageNet 类别标签=================
22
+ # 标准 ImageNet-1k 的 1000 个类别名称 (对应 WordNet ID 排序)
23
+ # 为了代码简洁,这里列出前几个作为示例,完整列表请在代码下方查看说明或从外部加载
24
+ # 这里我们使用一个简单的函数来获取标签,通常实际评测会使用 OpenAI 的 1000 个类名
25
+ try:
26
+ # 尝试从网络加载标准标签,如果无法联网请手动替换 IMAGENET_CLASSES 列表
27
+ import requests
28
+ url = "https://raw.githubusercontent.com/pytorch/hub/master/imagenet_classes.txt"
29
+ IMAGENET_CLASSES = requests.get(url).text.strip().split('\n')
30
+ print(f"成功加载 {len(IMAGENET_CLASSES)} 个类别标签。")
31
+ except Exception as e:
32
+ print("无法下载标签,请确保联网或手动填入 IMAGENET_CLASSES。")
33
+ # 示例: IMAGENET_CLASSES = ["tench", "goldfish", ...]
34
+ # raise e
35
+ IMAGENET_CLASSES = (
36
+ "tench", "goldfish", "great white shark", "tiger shark", "hammerhead shark", "electric ray",
37
+ "stingray", "rooster", "hen", "ostrich", "brambling", "goldfinch", "house finch", "junco",
38
+ "indigo bunting", "American robin", "bulbul", "jay", "magpie", "chickadee", "American dipper",
39
+ "kite (bird of prey)", "bald eagle", "vulture", "great grey owl", "fire salamander",
40
+ "smooth newt", "newt", "spotted salamander", "axolotl", "American bullfrog", "tree frog",
41
+ "tailed frog", "loggerhead sea turtle", "leatherback sea turtle", "mud turtle", "terrapin",
42
+ "box turtle", "banded gecko", "green iguana", "Carolina anole",
43
+ "desert grassland whiptail lizard", "agama", "frilled-necked lizard", "alligator lizard",
44
+ "Gila monster", "European green lizard", "chameleon", "Komodo dragon", "Nile crocodile",
45
+ "American alligator", "triceratops", "worm snake", "ring-necked snake",
46
+ "eastern hog-nosed snake", "smooth green snake", "kingsnake", "garter snake", "water snake",
47
+ "vine snake", "night snake", "boa constrictor", "African rock python", "Indian cobra",
48
+ "green mamba", "sea snake", "Saharan horned viper", "eastern diamondback rattlesnake",
49
+ "sidewinder rattlesnake", "trilobite", "harvestman", "scorpion", "yellow garden spider",
50
+ "barn spider", "European garden spider", "southern black widow", "tarantula", "wolf spider",
51
+ "tick", "centipede", "black grouse", "ptarmigan", "ruffed grouse", "prairie grouse", "peafowl",
52
+ "quail", "partridge", "african grey parrot", "macaw", "sulphur-crested cockatoo", "lorikeet",
53
+ "coucal", "bee eater", "hornbill", "hummingbird", "jacamar", "toucan", "duck",
54
+ "red-breasted merganser", "goose", "black swan", "tusker", "echidna", "platypus", "wallaby",
55
+ "koala", "wombat", "jellyfish", "sea anemone", "brain coral", "flatworm", "nematode", "conch",
56
+ "snail", "slug", "sea slug", "chiton", "chambered nautilus", "Dungeness crab", "rock crab",
57
+ "fiddler crab", "red king crab", "American lobster", "spiny lobster", "crayfish", "hermit crab",
58
+ "isopod", "white stork", "black stork", "spoonbill", "flamingo", "little blue heron",
59
+ "great egret", "bittern bird", "crane bird", "limpkin", "common gallinule", "American coot",
60
+ "bustard", "ruddy turnstone", "dunlin", "common redshank", "dowitcher", "oystercatcher",
61
+ "pelican", "king penguin", "albatross", "grey whale", "killer whale", "dugong", "sea lion",
62
+ "Chihuahua", "Japanese Chin", "Maltese", "Pekingese", "Shih Tzu", "King Charles Spaniel",
63
+ "Papillon", "toy terrier", "Rhodesian Ridgeback", "Afghan Hound", "Basset Hound", "Beagle",
64
+ "Bloodhound", "Bluetick Coonhound", "Black and Tan Coonhound", "Treeing Walker Coonhound",
65
+ "English foxhound", "Redbone Coonhound", "borzoi", "Irish Wolfhound", "Italian Greyhound",
66
+ "Whippet", "Ibizan Hound", "Norwegian Elkhound", "Otterhound", "Saluki", "Scottish Deerhound",
67
+ "Weimaraner", "Staffordshire Bull Terrier", "American Staffordshire Terrier",
68
+ "Bedlington Terrier", "Border Terrier", "Kerry Blue Terrier", "Irish Terrier",
69
+ "Norfolk Terrier", "Norwich Terrier", "Yorkshire Terrier", "Wire Fox Terrier",
70
+ "Lakeland Terrier", "Sealyham Terrier", "Airedale Terrier", "Cairn Terrier",
71
+ "Australian Terrier", "Dandie Dinmont Terrier", "Boston Terrier", "Miniature Schnauzer",
72
+ "Giant Schnauzer", "Standard Schnauzer", "Scottish Terrier", "Tibetan Terrier",
73
+ "Australian Silky Terrier", "Soft-coated Wheaten Terrier", "West Highland White Terrier",
74
+ "Lhasa Apso", "Flat-Coated Retriever", "Curly-coated Retriever", "Golden Retriever",
75
+ "Labrador Retriever", "Chesapeake Bay Retriever", "German Shorthaired Pointer", "Vizsla",
76
+ "English Setter", "Irish Setter", "Gordon Setter", "Brittany dog", "Clumber Spaniel",
77
+ "English Springer Spaniel", "Welsh Springer Spaniel", "Cocker Spaniel", "Sussex Spaniel",
78
+ "Irish Water Spaniel", "Kuvasz", "Schipperke", "Groenendael dog", "Malinois", "Briard",
79
+ "Australian Kelpie", "Komondor", "Old English Sheepdog", "Shetland Sheepdog", "collie",
80
+ "Border Collie", "Bouvier des Flandres dog", "Rottweiler", "German Shepherd Dog", "Dobermann",
81
+ "Miniature Pinscher", "Greater Swiss Mountain Dog", "Bernese Mountain Dog",
82
+ "Appenzeller Sennenhund", "Entlebucher Sennenhund", "Boxer", "Bullmastiff", "Tibetan Mastiff",
83
+ "French Bulldog", "Great Dane", "St. Bernard", "husky", "Alaskan Malamute", "Siberian Husky",
84
+ "Dalmatian", "Affenpinscher", "Basenji", "pug", "Leonberger", "Newfoundland dog",
85
+ "Great Pyrenees dog", "Samoyed", "Pomeranian", "Chow Chow", "Keeshond", "brussels griffon",
86
+ "Pembroke Welsh Corgi", "Cardigan Welsh Corgi", "Toy Poodle", "Miniature Poodle",
87
+ "Standard Poodle", "Mexican hairless dog (xoloitzcuintli)", "grey wolf", "Alaskan tundra wolf",
88
+ "red wolf or maned wolf", "coyote", "dingo", "dhole", "African wild dog", "hyena", "red fox",
89
+ "kit fox", "Arctic fox", "grey fox", "tabby cat", "tiger cat", "Persian cat", "Siamese cat",
90
+ "Egyptian Mau", "cougar", "lynx", "leopard", "snow leopard", "jaguar", "lion", "tiger",
91
+ "cheetah", "brown bear", "American black bear", "polar bear", "sloth bear", "mongoose",
92
+ "meerkat", "tiger beetle", "ladybug", "ground beetle", "longhorn beetle", "leaf beetle",
93
+ "dung beetle", "rhinoceros beetle", "weevil", "fly", "bee", "ant", "grasshopper",
94
+ "cricket insect", "stick insect", "cockroach", "praying mantis", "cicada", "leafhopper",
95
+ "lacewing", "dragonfly", "damselfly", "red admiral butterfly", "ringlet butterfly",
96
+ "monarch butterfly", "small white butterfly", "sulphur butterfly", "gossamer-winged butterfly",
97
+ "starfish", "sea urchin", "sea cucumber", "cottontail rabbit", "hare", "Angora rabbit",
98
+ "hamster", "porcupine", "fox squirrel", "marmot", "beaver", "guinea pig", "common sorrel horse",
99
+ "zebra", "pig", "wild boar", "warthog", "hippopotamus", "ox", "water buffalo", "bison",
100
+ "ram (adult male sheep)", "bighorn sheep", "Alpine ibex", "hartebeest", "impala (antelope)",
101
+ "gazelle", "arabian camel", "llama", "weasel", "mink", "European polecat",
102
+ "black-footed ferret", "otter", "skunk", "badger", "armadillo", "three-toed sloth", "orangutan",
103
+ "gorilla", "chimpanzee", "gibbon", "siamang", "guenon", "patas monkey", "baboon", "macaque",
104
+ "langur", "black-and-white colobus", "proboscis monkey", "marmoset", "white-headed capuchin",
105
+ "howler monkey", "titi monkey", "Geoffroy's spider monkey", "common squirrel monkey",
106
+ "ring-tailed lemur", "indri", "Asian elephant", "African bush elephant", "red panda",
107
+ "giant panda", "snoek fish", "eel", "silver salmon", "rock beauty fish", "clownfish",
108
+ "sturgeon", "gar fish", "lionfish", "pufferfish", "abacus", "abaya", "academic gown",
109
+ "accordion", "acoustic guitar", "aircraft carrier", "airliner", "airship", "altar", "ambulance",
110
+ "amphibious vehicle", "analog clock", "apiary", "apron", "trash can", "assault rifle",
111
+ "backpack", "bakery", "balance beam", "balloon", "ballpoint pen", "Band-Aid", "banjo",
112
+ "baluster / handrail", "barbell", "barber chair", "barbershop", "barn", "barometer", "barrel",
113
+ "wheelbarrow", "baseball", "basketball", "bassinet", "bassoon", "swimming cap", "bath towel",
114
+ "bathtub", "station wagon", "lighthouse", "beaker", "military hat (bearskin or shako)",
115
+ "beer bottle", "beer glass", "bell tower", "baby bib", "tandem bicycle", "bikini",
116
+ "ring binder", "binoculars", "birdhouse", "boathouse", "bobsleigh", "bolo tie", "poke bonnet",
117
+ "bookcase", "bookstore", "bottle cap", "hunting bow", "bow tie", "brass memorial plaque", "bra",
118
+ "breakwater", "breastplate", "broom", "bucket", "buckle", "bulletproof vest",
119
+ "high-speed train", "butcher shop", "taxicab", "cauldron", "candle", "cannon", "canoe",
120
+ "can opener", "cardigan", "car mirror", "carousel", "tool kit", "cardboard box / carton",
121
+ "car wheel", "automated teller machine", "cassette", "cassette player", "castle", "catamaran",
122
+ "CD player", "cello", "mobile phone", "chain", "chain-link fence", "chain mail", "chainsaw",
123
+ "storage chest", "chiffonier", "bell or wind chime", "china cabinet", "Christmas stocking",
124
+ "church", "movie theater", "cleaver", "cliff dwelling", "cloak", "clogs", "cocktail shaker",
125
+ "coffee mug", "coffeemaker", "spiral or coil", "combination lock", "computer keyboard",
126
+ "candy store", "container ship", "convertible", "corkscrew", "cornet", "cowboy boot",
127
+ "cowboy hat", "cradle", "construction crane", "crash helmet", "crate", "infant bed",
128
+ "Crock Pot", "croquet ball", "crutch", "cuirass", "dam", "desk", "desktop computer",
129
+ "rotary dial telephone", "diaper", "digital clock", "digital watch", "dining table",
130
+ "dishcloth", "dishwasher", "disc brake", "dock", "dog sled", "dome", "doormat", "drilling rig",
131
+ "drum", "drumstick", "dumbbell", "Dutch oven", "electric fan", "electric guitar",
132
+ "electric locomotive", "entertainment center", "envelope", "espresso machine", "face powder",
133
+ "feather boa", "filing cabinet", "fireboat", "fire truck", "fire screen", "flagpole", "flute",
134
+ "folding chair", "football helmet", "forklift", "fountain", "fountain pen", "four-poster bed",
135
+ "freight car", "French horn", "frying pan", "fur coat", "garbage truck",
136
+ "gas mask or respirator", "gas pump", "goblet", "go-kart", "golf ball", "golf cart", "gondola",
137
+ "gong", "gown", "grand piano", "greenhouse", "radiator grille", "grocery store", "guillotine",
138
+ "hair clip", "hair spray", "half-track", "hammer", "hamper", "hair dryer", "hand-held computer",
139
+ "handkerchief", "hard disk drive", "harmonica", "harp", "combine harvester", "hatchet",
140
+ "holster", "home theater", "honeycomb", "hook", "hoop skirt", "gymnastic horizontal bar",
141
+ "horse-drawn vehicle", "hourglass", "iPod", "clothes iron", "carved pumpkin", "jeans", "jeep",
142
+ "T-shirt", "jigsaw puzzle", "rickshaw", "joystick", "kimono", "knee pad", "knot", "lab coat",
143
+ "ladle", "lampshade", "laptop computer", "lawn mower", "lens cap", "letter opener", "library",
144
+ "lifeboat", "lighter", "limousine", "ocean liner", "lipstick", "slip-on shoe", "lotion",
145
+ "music speaker", "loupe magnifying glass", "sawmill", "magnetic compass", "messenger bag",
146
+ "mailbox", "tights", "one-piece bathing suit", "manhole cover", "maraca", "marimba", "mask",
147
+ "matchstick", "maypole", "maze", "measuring cup", "medicine cabinet", "megalith", "microphone",
148
+ "microwave oven", "military uniform", "milk can", "minibus", "miniskirt", "minivan", "missile",
149
+ "mitten", "mixing bowl", "mobile home", "ford model t", "modem", "monastery", "monitor",
150
+ "moped", "mortar and pestle", "graduation cap", "mosque", "mosquito net", "vespa",
151
+ "mountain bike", "tent", "computer mouse", "mousetrap", "moving van", "muzzle", "metal nail",
152
+ "neck brace", "necklace", "baby pacifier", "notebook computer", "obelisk", "oboe", "ocarina",
153
+ "odometer", "oil filter", "pipe organ", "oscilloscope", "overskirt", "bullock cart",
154
+ "oxygen mask", "product packet / packaging", "paddle", "paddle wheel", "padlock", "paintbrush",
155
+ "pajamas", "palace", "pan flute", "paper towel", "parachute", "parallel bars", "park bench",
156
+ "parking meter", "railroad car", "patio", "payphone", "pedestal", "pencil case",
157
+ "pencil sharpener", "perfume", "Petri dish", "photocopier", "plectrum", "Pickelhaube",
158
+ "picket fence", "pickup truck", "pier", "piggy bank", "pill bottle", "pillow", "ping-pong ball",
159
+ "pinwheel", "pirate ship", "drink pitcher", "block plane", "planetarium", "plastic bag",
160
+ "plate rack", "farm plow", "plunger", "Polaroid camera", "pole", "police van", "poncho",
161
+ "pool table", "soda bottle", "plant pot", "potter's wheel", "power drill", "prayer rug",
162
+ "printer", "prison", "missile", "projector", "hockey puck", "punching bag", "purse", "quill",
163
+ "quilt", "race car", "racket", "radiator", "radio", "radio telescope", "rain barrel",
164
+ "recreational vehicle", "fishing casting reel", "reflex camera", "refrigerator",
165
+ "remote control", "restaurant", "revolver", "rifle", "rocking chair", "rotisserie", "eraser",
166
+ "rugby ball", "ruler measuring stick", "sneaker", "safe", "safety pin", "salt shaker", "sandal",
167
+ "sarong", "saxophone", "scabbard", "weighing scale", "school bus", "schooner", "scoreboard",
168
+ "CRT monitor", "screw", "screwdriver", "seat belt", "sewing machine", "shield", "shoe store",
169
+ "shoji screen / room divider", "shopping basket", "shopping cart", "shovel", "shower cap",
170
+ "shower curtain", "ski", "balaclava ski mask", "sleeping bag", "slide rule", "sliding door",
171
+ "slot machine", "snorkel", "snowmobile", "snowplow", "soap dispenser", "soccer ball", "sock",
172
+ "solar thermal collector", "sombrero", "soup bowl", "keyboard space bar", "space heater",
173
+ "space shuttle", "spatula", "motorboat", "spider web", "spindle", "sports car", "spotlight",
174
+ "stage", "steam locomotive", "through arch bridge", "steel drum", "stethoscope", "scarf",
175
+ "stone wall", "stopwatch", "stove", "strainer", "tram", "stretcher", "couch", "stupa",
176
+ "submarine", "suit", "sundial", "sunglasses", "sunglasses", "sunscreen", "suspension bridge",
177
+ "mop", "sweatshirt", "swim trunks / shorts", "swing", "electrical switch", "syringe",
178
+ "table lamp", "tank", "tape player", "teapot", "teddy bear", "television", "tennis ball",
179
+ "thatched roof", "front curtain", "thimble", "threshing machine", "throne", "tile roof",
180
+ "toaster", "tobacco shop", "toilet seat", "torch", "totem pole", "tow truck", "toy store",
181
+ "tractor", "semi-trailer truck", "tray", "trench coat", "tricycle", "trimaran", "tripod",
182
+ "triumphal arch", "trolleybus", "trombone", "hot tub", "turnstile", "typewriter keyboard",
183
+ "umbrella", "unicycle", "upright piano", "vacuum cleaner", "vase", "vaulted or arched ceiling",
184
+ "velvet fabric", "vending machine", "vestment", "viaduct", "violin", "volleyball",
185
+ "waffle iron", "wall clock", "wallet", "wardrobe", "military aircraft", "sink",
186
+ "washing machine", "water bottle", "water jug", "water tower", "whiskey jug", "whistle",
187
+ "hair wig", "window screen", "window shade", "Windsor tie", "wine bottle", "airplane wing",
188
+ "wok", "wooden spoon", "wool", "split-rail fence", "shipwreck", "sailboat", "yurt", "website",
189
+ "comic book", "crossword", "traffic or street sign", "traffic light", "dust jacket", "menu",
190
+ "plate", "guacamole", "consomme", "hot pot", "trifle", "ice cream", "popsicle", "baguette",
191
+ "bagel", "pretzel", "cheeseburger", "hot dog", "mashed potatoes", "cabbage", "broccoli",
192
+ "cauliflower", "zucchini", "spaghetti squash", "acorn squash", "butternut squash", "cucumber",
193
+ "artichoke", "bell pepper", "cardoon", "mushroom", "Granny Smith apple", "strawberry", "orange",
194
+ "lemon", "fig", "pineapple", "banana", "jackfruit", "cherimoya (custard apple)", "pomegranate",
195
+ "hay", "carbonara", "chocolate syrup", "dough", "meatloaf", "pizza", "pot pie", "burrito",
196
+ "red wine", "espresso", "tea cup", "eggnog", "mountain", "bubble", "cliff", "coral reef",
197
+ "geyser", "lakeshore", "promontory", "sandbar", "beach", "valley", "volcano", "baseball player",
198
+ "bridegroom", "scuba diver", "rapeseed", "daisy", "yellow lady's slipper", "corn", "acorn",
199
+ "rose hip", "horse chestnut seed", "coral fungus", "agaric", "gyromitra", "stinkhorn mushroom",
200
+ "earth star fungus", "hen of the woods mushroom", "bolete", "corn cob", "toilet paper"
201
+ )
202
+
203
+ IMAGENET_CLASSES = tuple(label.lower() for label in IMAGENET_CLASSES)
204
+
205
+ # SigLIP 2 推荐的 Prompt 模板 (也可以使用 ensemble)
206
+ TEMPLATE = "a photo of a {}"
207
+
208
+ def load_model(model_path):
209
+ """加载本地 SigLIP 模型和处理器"""
210
+ print(f"正在加载模型: {model_path} ...")
211
+ # SigLIP 2 在 HF 中通常复用 SiglipModel 结构
212
+ model = SiglipModel.from_pretrained(model_path, torch_dtype=torch.float16)
213
+ processor = SiglipProcessor.from_pretrained(model_path)
214
+ model.to(DEVICE)
215
+ model.eval()
216
+ return model, processor
217
+
218
+ def build_zero_shot_classifier(model, processor, class_names, template):
219
+ """
220
+ 预计算文本嵌入 (Text Embeddings) 作为分类器权重
221
+ """
222
+ print("构建 Zero-Shot 分类器 (文本编码)...")
223
+
224
+ # 构建 Prompts: ["a photo of a tench", "a photo of a goldfish", ...]
225
+ texts = [template.format(label) for label in class_names]
226
+
227
+ # 分批处理文本以防 OOM
228
+ text_features_list = []
229
+ batch_size = 256
230
+
231
+ # 修复1: 使用 torch.amp.autocast('cuda') 替代过期的 torch.cuda.amp.autocast()
232
+ with torch.no_grad(), torch.amp.autocast('cuda'):
233
+ for i in range(0, len(texts), batch_size):
234
+ batch_texts = texts[i : i + batch_size]
235
+ inputs = processor(
236
+ text=batch_texts,
237
+ padding="max_length",
238
+ max_length=64, # 修复2: 显式指定 max_length (SigLIP 通常为 64),解决 padding 失败导致的维度错误
239
+ return_tensors="pt",
240
+ truncation=True
241
+ ).to(DEVICE)
242
+
243
+ # 获取文本特征
244
+ text_outputs = model.get_text_features(**inputs)
245
+
246
+ # 归一化 (SigLIP/CLIP 必须步骤)
247
+ text_outputs = text_outputs / text_outputs.norm(p=2, dim=-1, keepdim=True)
248
+ text_features_list.append(text_outputs)
249
+
250
+ # 合并所有类别的特征 [1000, Hidden_Dim]
251
+ text_classifier = torch.cat(text_features_list, dim=0)
252
+ return text_classifier
253
+
254
+ class ImageNetDataset(torch.utils.data.Dataset):
255
+ """
256
+ 自定义 Dataset 包装器,利用 SiglipProcessor 处理图像
257
+ """
258
+ def __init__(self, root_dir, processor):
259
+ # 使用 ImageFolder 自动处理文件夹结构和标签索引
260
+ self.dataset = datasets.ImageFolder(root_dir)
261
+ self.processor = processor
262
+
263
+ def __len__(self):
264
+ return len(self.dataset)
265
+
266
+ def __getitem__(self, idx):
267
+ image, label = self.dataset[idx]
268
+ # 这里的 image 是 PIL Image
269
+ return image, label
270
+
271
+ def collate_fn(batch, processor):
272
+ """
273
+ 自定义 collate_fn,将 PIL 图片列表转换为 Tensor
274
+ """
275
+ images, labels = zip(*batch)
276
+ # processor 处理图片: Resize, Rescale, Normalize
277
+ # 修复: zip(*batch) 返回的是 tuple,必须转为 list 才能被 processor 接受
278
+ inputs = processor(images=list(images), return_tensors="pt")
279
+ return inputs["pixel_values"], torch.tensor(labels)
280
+
281
+ def evaluate(model, classifier, dataloader):
282
+ """执行评测循环"""
283
+ print("开始评测 ImageNet-1k...")
284
+ top1_correct = 0
285
+ top5_correct = 0
286
+ total_samples = 0
287
+
288
+ # SigLIP 的 Logit Scale (Temperature)
289
+ logit_scale = model.logit_scale.exp()
290
+ # 如果是 SigLIP 2,bias 也是需要的
291
+ logit_bias = model.logit_bias if hasattr(model, 'logit_bias') else 0
292
+
293
+ # 修复3: 同步更新 evaluate 中的 autocast 写法
294
+ with torch.no_grad(), torch.amp.autocast('cuda'):
295
+ for pixel_values, targets in tqdm(dataloader):
296
+ pixel_values = pixel_values.to(DEVICE).to(torch.float16)
297
+ targets = targets.to(DEVICE)
298
+
299
+ # 1. 图像编码
300
+ # SigLIP image encoder 输出
301
+ image_features = model.get_image_features(pixel_values=pixel_values)
302
+
303
+ # 2. 归一化图像特征
304
+ image_features = image_features / image_features.norm(p=2, dim=-1, keepdim=True)
305
+
306
+ # 3. 计算 Logits
307
+ # Logits = (Image_Feat @ Text_Feat.T) * Scale + Bias
308
+ logits = (torch.matmul(image_features, classifier.t()) * logit_scale) + logit_bias
309
+
310
+ # 4. 计算准确率
311
+ # Top-1
312
+ pred_top1 = logits.argmax(dim=1)
313
+ top1_correct += (pred_top1 == targets).sum().item()
314
+
315
+ # Top-5
316
+ _, pred_top5 = logits.topk(5, dim=1)
317
+ top5_correct += (pred_top5 == targets.unsqueeze(1)).sum().item()
318
+
319
+ total_samples += targets.size(0)
320
+
321
+ acc1 = top1_correct / total_samples
322
+ acc5 = top5_correct / total_samples
323
+
324
+ print(f"\n====== 评测完成 ======")
325
+ print(f"Total Samples: {total_samples}")
326
+ print(f"Top-1 Accuracy: {acc1 * 100:.2f}%")
327
+ print(f"Top-5 Accuracy: {acc5 * 100:.2f}%")
328
+
329
+ return acc1, acc5
330
+
331
+ # =================主执行逻辑=================
332
+ if __name__ == "__main__":
333
+ from functools import partial
334
+
335
+ # 1. 加载模型
336
+ # 确保你的本地路径正确
337
+ if not os.path.exists(MODEL_PATH):
338
+ raise FileNotFoundError(f"找不到模型路径: {MODEL_PATH}")
339
+
340
+ model, processor = load_model(MODEL_PATH)
341
+
342
+ # 2. 构建 Zero-Shot 文本分类器
343
+ # 注意:IMAGENET_CLASSES 必须与 datasets.ImageFolder 的类别顺序一致
344
+ # ImageFolder 默认按照文件夹名称(n01440764...)的字母顺序排序
345
+ # 这里我们假设下载的 IMAGENET_CLASSES 是已经按照 WordNet ID 排序好的
346
+ classifier = build_zero_shot_classifier(model, processor, IMAGENET_CLASSES, TEMPLATE)
347
+
348
+ # 3. 准备数据加载器
349
+ # 使用 partial 将 processor 传入 collate_fn
350
+ collate_func = partial(collate_fn, processor=processor)
351
+
352
+ dataset = ImageNetDataset(IMAGENET_VAL_PATH, processor)
353
+ dataloader = DataLoader(
354
+ dataset,
355
+ batch_size=BATCH_SIZE,
356
+ shuffle=False,
357
+ num_workers=NUM_WORKERS,
358
+ collate_fn=collate_func,
359
+ pin_memory=True
360
+ )
361
+
362
+ # 4. 开始评测
363
+ evaluate(model, classifier, dataloader)
patch-reparameterization/src/eval/clip_test.py ADDED
@@ -0,0 +1,567 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Zero-shot evaluation script for VTP.
4
+
5
+ Modified from OpenCLIP.
6
+
7
+ Usage:
8
+ python tools/test_zero_shot_hf.py --model_path /path/to/vtp_hf_model --data_path /path/to/imagenet/val
9
+
10
+ # With custom settings
11
+ python tools/test_zero_shot_hf.py \
12
+ --model_path /path/to/vtp_hf_model \
13
+ --data_path /path/to/imagenet/val \
14
+ --batch_size 256 \
15
+ --device cuda:0 \
16
+ --precision bf16
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import sys
23
+ from functools import partial
24
+ from itertools import islice
25
+ from pathlib import Path
26
+ from typing import Callable, List, Sequence, Tuple, Union
27
+
28
+ import torch
29
+ import torch.nn.functional as F
30
+ from torch.utils.data import DataLoader
31
+ from torchvision import transforms
32
+ from torchvision.datasets import ImageFolder
33
+ from tqdm import tqdm
34
+
35
+ project_root = Path(__file__).parent.parent
36
+ sys.path.insert(0, str(project_root))
37
+
38
+ from vtp.models.vtp_hf import VTPConfig, VTPModel
39
+ from vtp.tokenizers import get_tokenizer
40
+
41
+
42
+ # ============================================================================
43
+ # ImageNet classnames and templates (copied from tools/zero_shot/templates.py)
44
+ # ============================================================================
45
+
46
+ IMAGENET_CLASSNAMES = (
47
+ "tench", "goldfish", "great white shark", "tiger shark", "hammerhead shark", "electric ray",
48
+ "stingray", "rooster", "hen", "ostrich", "brambling", "goldfinch", "house finch", "junco",
49
+ "indigo bunting", "American robin", "bulbul", "jay", "magpie", "chickadee", "American dipper",
50
+ "kite (bird of prey)", "bald eagle", "vulture", "great grey owl", "fire salamander",
51
+ "smooth newt", "newt", "spotted salamander", "axolotl", "American bullfrog", "tree frog",
52
+ "tailed frog", "loggerhead sea turtle", "leatherback sea turtle", "mud turtle", "terrapin",
53
+ "box turtle", "banded gecko", "green iguana", "Carolina anole",
54
+ "desert grassland whiptail lizard", "agama", "frilled-necked lizard", "alligator lizard",
55
+ "Gila monster", "European green lizard", "chameleon", "Komodo dragon", "Nile crocodile",
56
+ "American alligator", "triceratops", "worm snake", "ring-necked snake",
57
+ "eastern hog-nosed snake", "smooth green snake", "kingsnake", "garter snake", "water snake",
58
+ "vine snake", "night snake", "boa constrictor", "African rock python", "Indian cobra",
59
+ "green mamba", "sea snake", "Saharan horned viper", "eastern diamondback rattlesnake",
60
+ "sidewinder rattlesnake", "trilobite", "harvestman", "scorpion", "yellow garden spider",
61
+ "barn spider", "European garden spider", "southern black widow", "tarantula", "wolf spider",
62
+ "tick", "centipede", "black grouse", "ptarmigan", "ruffed grouse", "prairie grouse", "peafowl",
63
+ "quail", "partridge", "african grey parrot", "macaw", "sulphur-crested cockatoo", "lorikeet",
64
+ "coucal", "bee eater", "hornbill", "hummingbird", "jacamar", "toucan", "duck",
65
+ "red-breasted merganser", "goose", "black swan", "tusker", "echidna", "platypus", "wallaby",
66
+ "koala", "wombat", "jellyfish", "sea anemone", "brain coral", "flatworm", "nematode", "conch",
67
+ "snail", "slug", "sea slug", "chiton", "chambered nautilus", "Dungeness crab", "rock crab",
68
+ "fiddler crab", "red king crab", "American lobster", "spiny lobster", "crayfish", "hermit crab",
69
+ "isopod", "white stork", "black stork", "spoonbill", "flamingo", "little blue heron",
70
+ "great egret", "bittern bird", "crane bird", "limpkin", "common gallinule", "American coot",
71
+ "bustard", "ruddy turnstone", "dunlin", "common redshank", "dowitcher", "oystercatcher",
72
+ "pelican", "king penguin", "albatross", "grey whale", "killer whale", "dugong", "sea lion",
73
+ "Chihuahua", "Japanese Chin", "Maltese", "Pekingese", "Shih Tzu", "King Charles Spaniel",
74
+ "Papillon", "toy terrier", "Rhodesian Ridgeback", "Afghan Hound", "Basset Hound", "Beagle",
75
+ "Bloodhound", "Bluetick Coonhound", "Black and Tan Coonhound", "Treeing Walker Coonhound",
76
+ "English foxhound", "Redbone Coonhound", "borzoi", "Irish Wolfhound", "Italian Greyhound",
77
+ "Whippet", "Ibizan Hound", "Norwegian Elkhound", "Otterhound", "Saluki", "Scottish Deerhound",
78
+ "Weimaraner", "Staffordshire Bull Terrier", "American Staffordshire Terrier",
79
+ "Bedlington Terrier", "Border Terrier", "Kerry Blue Terrier", "Irish Terrier",
80
+ "Norfolk Terrier", "Norwich Terrier", "Yorkshire Terrier", "Wire Fox Terrier",
81
+ "Lakeland Terrier", "Sealyham Terrier", "Airedale Terrier", "Cairn Terrier",
82
+ "Australian Terrier", "Dandie Dinmont Terrier", "Boston Terrier", "Miniature Schnauzer",
83
+ "Giant Schnauzer", "Standard Schnauzer", "Scottish Terrier", "Tibetan Terrier",
84
+ "Australian Silky Terrier", "Soft-coated Wheaten Terrier", "West Highland White Terrier",
85
+ "Lhasa Apso", "Flat-Coated Retriever", "Curly-coated Retriever", "Golden Retriever",
86
+ "Labrador Retriever", "Chesapeake Bay Retriever", "German Shorthaired Pointer", "Vizsla",
87
+ "English Setter", "Irish Setter", "Gordon Setter", "Brittany dog", "Clumber Spaniel",
88
+ "English Springer Spaniel", "Welsh Springer Spaniel", "Cocker Spaniel", "Sussex Spaniel",
89
+ "Irish Water Spaniel", "Kuvasz", "Schipperke", "Groenendael dog", "Malinois", "Briard",
90
+ "Australian Kelpie", "Komondor", "Old English Sheepdog", "Shetland Sheepdog", "collie",
91
+ "Border Collie", "Bouvier des Flandres dog", "Rottweiler", "German Shepherd Dog", "Dobermann",
92
+ "Miniature Pinscher", "Greater Swiss Mountain Dog", "Bernese Mountain Dog",
93
+ "Appenzeller Sennenhund", "Entlebucher Sennenhund", "Boxer", "Bullmastiff", "Tibetan Mastiff",
94
+ "French Bulldog", "Great Dane", "St. Bernard", "husky", "Alaskan Malamute", "Siberian Husky",
95
+ "Dalmatian", "Affenpinscher", "Basenji", "pug", "Leonberger", "Newfoundland dog",
96
+ "Great Pyrenees dog", "Samoyed", "Pomeranian", "Chow Chow", "Keeshond", "brussels griffon",
97
+ "Pembroke Welsh Corgi", "Cardigan Welsh Corgi", "Toy Poodle", "Miniature Poodle",
98
+ "Standard Poodle", "Mexican hairless dog (xoloitzcuintli)", "grey wolf", "Alaskan tundra wolf",
99
+ "red wolf or maned wolf", "coyote", "dingo", "dhole", "African wild dog", "hyena", "red fox",
100
+ "kit fox", "Arctic fox", "grey fox", "tabby cat", "tiger cat", "Persian cat", "Siamese cat",
101
+ "Egyptian Mau", "cougar", "lynx", "leopard", "snow leopard", "jaguar", "lion", "tiger",
102
+ "cheetah", "brown bear", "American black bear", "polar bear", "sloth bear", "mongoose",
103
+ "meerkat", "tiger beetle", "ladybug", "ground beetle", "longhorn beetle", "leaf beetle",
104
+ "dung beetle", "rhinoceros beetle", "weevil", "fly", "bee", "ant", "grasshopper",
105
+ "cricket insect", "stick insect", "cockroach", "praying mantis", "cicada", "leafhopper",
106
+ "lacewing", "dragonfly", "damselfly", "red admiral butterfly", "ringlet butterfly",
107
+ "monarch butterfly", "small white butterfly", "sulphur butterfly", "gossamer-winged butterfly",
108
+ "starfish", "sea urchin", "sea cucumber", "cottontail rabbit", "hare", "Angora rabbit",
109
+ "hamster", "porcupine", "fox squirrel", "marmot", "beaver", "guinea pig", "common sorrel horse",
110
+ "zebra", "pig", "wild boar", "warthog", "hippopotamus", "ox", "water buffalo", "bison",
111
+ "ram (adult male sheep)", "bighorn sheep", "Alpine ibex", "hartebeest", "impala (antelope)",
112
+ "gazelle", "arabian camel", "llama", "weasel", "mink", "European polecat",
113
+ "black-footed ferret", "otter", "skunk", "badger", "armadillo", "three-toed sloth", "orangutan",
114
+ "gorilla", "chimpanzee", "gibbon", "siamang", "guenon", "patas monkey", "baboon", "macaque",
115
+ "langur", "black-and-white colobus", "proboscis monkey", "marmoset", "white-headed capuchin",
116
+ "howler monkey", "titi monkey", "Geoffroy's spider monkey", "common squirrel monkey",
117
+ "ring-tailed lemur", "indri", "Asian elephant", "African bush elephant", "red panda",
118
+ "giant panda", "snoek fish", "eel", "silver salmon", "rock beauty fish", "clownfish",
119
+ "sturgeon", "gar fish", "lionfish", "pufferfish", "abacus", "abaya", "academic gown",
120
+ "accordion", "acoustic guitar", "aircraft carrier", "airliner", "airship", "altar", "ambulance",
121
+ "amphibious vehicle", "analog clock", "apiary", "apron", "trash can", "assault rifle",
122
+ "backpack", "bakery", "balance beam", "balloon", "ballpoint pen", "Band-Aid", "banjo",
123
+ "baluster / handrail", "barbell", "barber chair", "barbershop", "barn", "barometer", "barrel",
124
+ "wheelbarrow", "baseball", "basketball", "bassinet", "bassoon", "swimming cap", "bath towel",
125
+ "bathtub", "station wagon", "lighthouse", "beaker", "military hat (bearskin or shako)",
126
+ "beer bottle", "beer glass", "bell tower", "baby bib", "tandem bicycle", "bikini",
127
+ "ring binder", "binoculars", "birdhouse", "boathouse", "bobsleigh", "bolo tie", "poke bonnet",
128
+ "bookcase", "bookstore", "bottle cap", "hunting bow", "bow tie", "brass memorial plaque", "bra",
129
+ "breakwater", "breastplate", "broom", "bucket", "buckle", "bulletproof vest",
130
+ "high-speed train", "butcher shop", "taxicab", "cauldron", "candle", "cannon", "canoe",
131
+ "can opener", "cardigan", "car mirror", "carousel", "tool kit", "cardboard box / carton",
132
+ "car wheel", "automated teller machine", "cassette", "cassette player", "castle", "catamaran",
133
+ "CD player", "cello", "mobile phone", "chain", "chain-link fence", "chain mail", "chainsaw",
134
+ "storage chest", "chiffonier", "bell or wind chime", "china cabinet", "Christmas stocking",
135
+ "church", "movie theater", "cleaver", "cliff dwelling", "cloak", "clogs", "cocktail shaker",
136
+ "coffee mug", "coffeemaker", "spiral or coil", "combination lock", "computer keyboard",
137
+ "candy store", "container ship", "convertible", "corkscrew", "cornet", "cowboy boot",
138
+ "cowboy hat", "cradle", "construction crane", "crash helmet", "crate", "infant bed",
139
+ "Crock Pot", "croquet ball", "crutch", "cuirass", "dam", "desk", "desktop computer",
140
+ "rotary dial telephone", "diaper", "digital clock", "digital watch", "dining table",
141
+ "dishcloth", "dishwasher", "disc brake", "dock", "dog sled", "dome", "doormat", "drilling rig",
142
+ "drum", "drumstick", "dumbbell", "Dutch oven", "electric fan", "electric guitar",
143
+ "electric locomotive", "entertainment center", "envelope", "espresso machine", "face powder",
144
+ "feather boa", "filing cabinet", "fireboat", "fire truck", "fire screen", "flagpole", "flute",
145
+ "folding chair", "football helmet", "forklift", "fountain", "fountain pen", "four-poster bed",
146
+ "freight car", "French horn", "frying pan", "fur coat", "garbage truck",
147
+ "gas mask or respirator", "gas pump", "goblet", "go-kart", "golf ball", "golf cart", "gondola",
148
+ "gong", "gown", "grand piano", "greenhouse", "radiator grille", "grocery store", "guillotine",
149
+ "hair clip", "hair spray", "half-track", "hammer", "hamper", "hair dryer", "hand-held computer",
150
+ "handkerchief", "hard disk drive", "harmonica", "harp", "combine harvester", "hatchet",
151
+ "holster", "home theater", "honeycomb", "hook", "hoop skirt", "gymnastic horizontal bar",
152
+ "horse-drawn vehicle", "hourglass", "iPod", "clothes iron", "carved pumpkin", "jeans", "jeep",
153
+ "T-shirt", "jigsaw puzzle", "rickshaw", "joystick", "kimono", "knee pad", "knot", "lab coat",
154
+ "ladle", "lampshade", "laptop computer", "lawn mower", "lens cap", "letter opener", "library",
155
+ "lifeboat", "lighter", "limousine", "ocean liner", "lipstick", "slip-on shoe", "lotion",
156
+ "music speaker", "loupe magnifying glass", "sawmill", "magnetic compass", "messenger bag",
157
+ "mailbox", "tights", "one-piece bathing suit", "manhole cover", "maraca", "marimba", "mask",
158
+ "matchstick", "maypole", "maze", "measuring cup", "medicine cabinet", "megalith", "microphone",
159
+ "microwave oven", "military uniform", "milk can", "minibus", "miniskirt", "minivan", "missile",
160
+ "mitten", "mixing bowl", "mobile home", "ford model t", "modem", "monastery", "monitor",
161
+ "moped", "mortar and pestle", "graduation cap", "mosque", "mosquito net", "vespa",
162
+ "mountain bike", "tent", "computer mouse", "mousetrap", "moving van", "muzzle", "metal nail",
163
+ "neck brace", "necklace", "baby pacifier", "notebook computer", "obelisk", "oboe", "ocarina",
164
+ "odometer", "oil filter", "pipe organ", "oscilloscope", "overskirt", "bullock cart",
165
+ "oxygen mask", "product packet / packaging", "paddle", "paddle wheel", "padlock", "paintbrush",
166
+ "pajamas", "palace", "pan flute", "paper towel", "parachute", "parallel bars", "park bench",
167
+ "parking meter", "railroad car", "patio", "payphone", "pedestal", "pencil case",
168
+ "pencil sharpener", "perfume", "Petri dish", "photocopier", "plectrum", "Pickelhaube",
169
+ "picket fence", "pickup truck", "pier", "piggy bank", "pill bottle", "pillow", "ping-pong ball",
170
+ "pinwheel", "pirate ship", "drink pitcher", "block plane", "planetarium", "plastic bag",
171
+ "plate rack", "farm plow", "plunger", "Polaroid camera", "pole", "police van", "poncho",
172
+ "pool table", "soda bottle", "plant pot", "potter's wheel", "power drill", "prayer rug",
173
+ "printer", "prison", "missile", "projector", "hockey puck", "punching bag", "purse", "quill",
174
+ "quilt", "race car", "racket", "radiator", "radio", "radio telescope", "rain barrel",
175
+ "recreational vehicle", "fishing casting reel", "reflex camera", "refrigerator",
176
+ "remote control", "restaurant", "revolver", "rifle", "rocking chair", "rotisserie", "eraser",
177
+ "rugby ball", "ruler measuring stick", "sneaker", "safe", "safety pin", "salt shaker", "sandal",
178
+ "sarong", "saxophone", "scabbard", "weighing scale", "school bus", "schooner", "scoreboard",
179
+ "CRT monitor", "screw", "screwdriver", "seat belt", "sewing machine", "shield", "shoe store",
180
+ "shoji screen / room divider", "shopping basket", "shopping cart", "shovel", "shower cap",
181
+ "shower curtain", "ski", "balaclava ski mask", "sleeping bag", "slide rule", "sliding door",
182
+ "slot machine", "snorkel", "snowmobile", "snowplow", "soap dispenser", "soccer ball", "sock",
183
+ "solar thermal collector", "sombrero", "soup bowl", "keyboard space bar", "space heater",
184
+ "space shuttle", "spatula", "motorboat", "spider web", "spindle", "sports car", "spotlight",
185
+ "stage", "steam locomotive", "through arch bridge", "steel drum", "stethoscope", "scarf",
186
+ "stone wall", "stopwatch", "stove", "strainer", "tram", "stretcher", "couch", "stupa",
187
+ "submarine", "suit", "sundial", "sunglasses", "sunglasses", "sunscreen", "suspension bridge",
188
+ "mop", "sweatshirt", "swim trunks / shorts", "swing", "electrical switch", "syringe",
189
+ "table lamp", "tank", "tape player", "teapot", "teddy bear", "television", "tennis ball",
190
+ "thatched roof", "front curtain", "thimble", "threshing machine", "throne", "tile roof",
191
+ "toaster", "tobacco shop", "toilet seat", "torch", "totem pole", "tow truck", "toy store",
192
+ "tractor", "semi-trailer truck", "tray", "trench coat", "tricycle", "trimaran", "tripod",
193
+ "triumphal arch", "trolleybus", "trombone", "hot tub", "turnstile", "typewriter keyboard",
194
+ "umbrella", "unicycle", "upright piano", "vacuum cleaner", "vase", "vaulted or arched ceiling",
195
+ "velvet fabric", "vending machine", "vestment", "viaduct", "violin", "volleyball",
196
+ "waffle iron", "wall clock", "wallet", "wardrobe", "military aircraft", "sink",
197
+ "washing machine", "water bottle", "water jug", "water tower", "whiskey jug", "whistle",
198
+ "hair wig", "window screen", "window shade", "Windsor tie", "wine bottle", "airplane wing",
199
+ "wok", "wooden spoon", "wool", "split-rail fence", "shipwreck", "sailboat", "yurt", "website",
200
+ "comic book", "crossword", "traffic or street sign", "traffic light", "dust jacket", "menu",
201
+ "plate", "guacamole", "consomme", "hot pot", "trifle", "ice cream", "popsicle", "baguette",
202
+ "bagel", "pretzel", "cheeseburger", "hot dog", "mashed potatoes", "cabbage", "broccoli",
203
+ "cauliflower", "zucchini", "spaghetti squash", "acorn squash", "butternut squash", "cucumber",
204
+ "artichoke", "bell pepper", "cardoon", "mushroom", "Granny Smith apple", "strawberry", "orange",
205
+ "lemon", "fig", "pineapple", "banana", "jackfruit", "cherimoya (custard apple)", "pomegranate",
206
+ "hay", "carbonara", "chocolate syrup", "dough", "meatloaf", "pizza", "pot pie", "burrito",
207
+ "red wine", "espresso", "tea cup", "eggnog", "mountain", "bubble", "cliff", "coral reef",
208
+ "geyser", "lakeshore", "promontory", "sandbar", "beach", "valley", "volcano", "baseball player",
209
+ "bridegroom", "scuba diver", "rapeseed", "daisy", "yellow lady's slipper", "corn", "acorn",
210
+ "rose hip", "horse chestnut seed", "coral fungus", "agaric", "gyromitra", "stinkhorn mushroom",
211
+ "earth star fungus", "hen of the woods mushroom", "bolete", "corn cob", "toilet paper"
212
+ )
213
+
214
+ OPENAI_IMAGENET_TEMPLATES = (
215
+ lambda c: f'a bad photo of a {c}.',
216
+ lambda c: f'a photo of many {c}.',
217
+ lambda c: f'a sculpture of a {c}.',
218
+ lambda c: f'a photo of the hard to see {c}.',
219
+ lambda c: f'a low resolution photo of the {c}.',
220
+ lambda c: f'a rendering of a {c}.',
221
+ lambda c: f'graffiti of a {c}.',
222
+ lambda c: f'a bad photo of the {c}.',
223
+ lambda c: f'a cropped photo of the {c}.',
224
+ lambda c: f'a tattoo of a {c}.',
225
+ lambda c: f'the embroidered {c}.',
226
+ lambda c: f'a photo of a hard to see {c}.',
227
+ lambda c: f'a bright photo of a {c}.',
228
+ lambda c: f'a photo of a clean {c}.',
229
+ lambda c: f'a photo of a dirty {c}.',
230
+ lambda c: f'a dark photo of the {c}.',
231
+ lambda c: f'a drawing of a {c}.',
232
+ lambda c: f'a photo of my {c}.',
233
+ lambda c: f'the plastic {c}.',
234
+ lambda c: f'a photo of the cool {c}.',
235
+ lambda c: f'a close-up photo of a {c}.',
236
+ lambda c: f'a black and white photo of the {c}.',
237
+ lambda c: f'a painting of the {c}.',
238
+ lambda c: f'a painting of a {c}.',
239
+ lambda c: f'a pixelated photo of the {c}.',
240
+ lambda c: f'a sculpture of the {c}.',
241
+ lambda c: f'a bright photo of the {c}.',
242
+ lambda c: f'a cropped photo of a {c}.',
243
+ lambda c: f'a plastic {c}.',
244
+ lambda c: f'a photo of the dirty {c}.',
245
+ lambda c: f'a jpeg corrupted photo of a {c}.',
246
+ lambda c: f'a blurry photo of the {c}.',
247
+ lambda c: f'a photo of the {c}.',
248
+ lambda c: f'a good photo of the {c}.',
249
+ lambda c: f'a rendering of the {c}.',
250
+ lambda c: f'a {c} in a video game.',
251
+ lambda c: f'a photo of one {c}.',
252
+ lambda c: f'a doodle of a {c}.',
253
+ lambda c: f'a close-up photo of the {c}.',
254
+ lambda c: f'a photo of a {c}.',
255
+ lambda c: f'the origami {c}.',
256
+ lambda c: f'the {c} in a video game.',
257
+ lambda c: f'a sketch of a {c}.',
258
+ lambda c: f'a doodle of the {c}.',
259
+ lambda c: f'a origami {c}.',
260
+ lambda c: f'a low resolution photo of a {c}.',
261
+ lambda c: f'the toy {c}.',
262
+ lambda c: f'a rendition of the {c}.',
263
+ lambda c: f'a photo of the clean {c}.',
264
+ lambda c: f'a photo of a large {c}.',
265
+ lambda c: f'a rendition of a {c}.',
266
+ lambda c: f'a photo of a nice {c}.',
267
+ lambda c: f'a photo of a weird {c}.',
268
+ lambda c: f'a blurry photo of a {c}.',
269
+ lambda c: f'a cartoon {c}.',
270
+ lambda c: f'art of a {c}.',
271
+ lambda c: f'a sketch of the {c}.',
272
+ lambda c: f'a embroidered {c}.',
273
+ lambda c: f'a pixelated photo of a {c}.',
274
+ lambda c: f'itap of the {c}.',
275
+ lambda c: f'a jpeg corrupted photo of the {c}.',
276
+ lambda c: f'a good photo of a {c}.',
277
+ lambda c: f'a plushie {c}.',
278
+ lambda c: f'a photo of the nice {c}.',
279
+ lambda c: f'a photo of the small {c}.',
280
+ lambda c: f'a photo of the weird {c}.',
281
+ lambda c: f'the cartoon {c}.',
282
+ lambda c: f'art of the {c}.',
283
+ lambda c: f'a drawing of the {c}.',
284
+ lambda c: f'a photo of the large {c}.',
285
+ lambda c: f'a black and white photo of a {c}.',
286
+ lambda c: f'the plushie {c}.',
287
+ lambda c: f'a dark photo of a {c}.',
288
+ lambda c: f'itap of a {c}.',
289
+ lambda c: f'graffiti of the {c}.',
290
+ lambda c: f'a toy {c}.',
291
+ lambda c: f'itap of my {c}.',
292
+ lambda c: f'a photo of a cool {c}.',
293
+ lambda c: f'a photo of a small {c}.',
294
+ lambda c: f'a tattoo of the {c}.',
295
+ )
296
+
297
+
298
+ # ============================================================================
299
+ # Utility functions
300
+ # ============================================================================
301
+
302
+ def batched(iterable, n: int):
303
+ """Batch data into lists of length n. The last batch may be shorter."""
304
+ it = iter(iterable)
305
+ while True:
306
+ batch = list(islice(it, n))
307
+ if not batch:
308
+ break
309
+ yield batch
310
+
311
+
312
+ def accuracy(output: torch.Tensor, target: torch.Tensor, topk: Tuple[int, ...] = (1,)) -> List[float]:
313
+ """Calculate top-k accuracy."""
314
+ pred = output.topk(max(topk), 1, True, True)[1].t()
315
+ correct = pred.eq(target.view(1, -1).expand_as(pred))
316
+ return [float(correct[:k].reshape(-1).float().sum(0, keepdim=True).cpu().numpy()) for k in topk]
317
+
318
+
319
+ def get_input_dtype(precision: str) -> torch.dtype:
320
+ """Get input dtype based on precision string."""
321
+ if precision in ('bf16', 'bfloat16'):
322
+ return torch.bfloat16
323
+ elif precision in ('fp16', 'float16'):
324
+ return torch.float16
325
+ return torch.float32
326
+
327
+
328
+ def get_autocast_context(precision: str, device_type: str = 'cuda'):
329
+ """Get autocast context manager based on precision."""
330
+ from contextlib import nullcontext
331
+ if precision in ('bf16', 'bfloat16'):
332
+ return partial(torch.amp.autocast, device_type=device_type, dtype=torch.bfloat16)
333
+ elif precision in ('fp16', 'float16'):
334
+ return partial(torch.amp.autocast, device_type=device_type, dtype=torch.float16)
335
+ return nullcontext
336
+
337
+
338
+ # ============================================================================
339
+ # Zero-shot classifier
340
+ # ============================================================================
341
+
342
+ def build_zero_shot_classifier(
343
+ model: VTPModel,
344
+ tokenizer,
345
+ classnames: Sequence[str],
346
+ templates: Sequence[Callable],
347
+ num_classes_per_batch: int = 10,
348
+ device: torch.device = None,
349
+ use_tqdm: bool = True,
350
+ ) -> torch.Tensor:
351
+ """Build zero-shot classifier weights.
352
+
353
+ Args:
354
+ model: VTPModel instance with get_clip_text_feature method.
355
+ tokenizer: Text tokenizer.
356
+ classnames: Sequence of class names.
357
+ templates: Sequence of template functions.
358
+ num_classes_per_batch: Number of classes to process per batch.
359
+ device: Device to use.
360
+ use_tqdm: Whether to show progress bar.
361
+
362
+ Returns:
363
+ Zero-shot classifier weights, shape (embed_dim, num_classes).
364
+ """
365
+ num_templates = len(templates)
366
+ num_classes = len(classnames)
367
+
368
+ if use_tqdm:
369
+ num_iter = (num_classes - 1) // num_classes_per_batch + 1
370
+ iter_wrap = partial(tqdm, total=num_iter, desc="Building classifier", unit_scale=num_classes_per_batch)
371
+ else:
372
+ iter_wrap = iter
373
+
374
+ def _process_batch(batch_classnames: List[str]) -> torch.Tensor:
375
+ num_batch_classes = len(batch_classnames)
376
+ texts = [template(c) for c in batch_classnames for template in templates]
377
+ tokens = tokenizer(texts).to(device)
378
+
379
+ # Use VTPModel's get_clip_text_feature method
380
+ text_features = model.get_clip_text_feature(tokens, normalize=True)
381
+
382
+ # Reshape and average over templates
383
+ text_features = text_features.reshape(num_batch_classes, num_templates, -1).mean(dim=1)
384
+ text_features = F.normalize(text_features, dim=1)
385
+ return text_features.T # (embed_dim, num_batch_classes)
386
+
387
+ with torch.no_grad():
388
+ batched_embeds = [
389
+ _process_batch(batch)
390
+ for batch in iter_wrap(batched(classnames, num_classes_per_batch))
391
+ ]
392
+ zeroshot_weights = torch.cat(batched_embeds, dim=1)
393
+
394
+ return zeroshot_weights
395
+
396
+
397
+ # ============================================================================
398
+ # Evaluation
399
+ # ============================================================================
400
+
401
+ def evaluate(
402
+ model: VTPModel,
403
+ classifier: torch.Tensor,
404
+ dataloader: DataLoader,
405
+ device: torch.device,
406
+ precision: str = 'fp32',
407
+ ) -> Tuple[float, float]:
408
+ """Run zero-shot evaluation.
409
+
410
+ Args:
411
+ model: VTPModel instance.
412
+ classifier: Zero-shot classifier weights, shape (embed_dim, num_classes).
413
+ dataloader: ImageNet validation dataloader.
414
+ device: Device to use.
415
+ precision: Precision for inference.
416
+
417
+ Returns:
418
+ Tuple of (top1_accuracy, top5_accuracy).
419
+ """
420
+ autocast_ctx = get_autocast_context(precision, device_type=device.type)
421
+ input_dtype = get_input_dtype(precision)
422
+
423
+ top1, top5, n = 0.0, 0.0, 0
424
+
425
+ with torch.inference_mode():
426
+ for images, targets in tqdm(dataloader, desc="Evaluating"):
427
+ images = images.to(device=device, dtype=input_dtype)
428
+ targets = targets.to(device)
429
+
430
+ with autocast_ctx():
431
+ image_features = model.get_clip_image_feature(images, normalize=True)
432
+ logits = 100.0 * image_features @ classifier
433
+
434
+ acc1, acc5 = accuracy(logits, targets, topk=(1, 5))
435
+ top1 += acc1
436
+ top5 += acc5
437
+ n += images.size(0)
438
+
439
+ top1 = top1 / n * 100
440
+ top5 = top5 / n * 100
441
+ return top1, top5
442
+
443
+
444
+ # ============================================================================
445
+ # Main
446
+ # ============================================================================
447
+
448
+ def create_imagenet_dataloader(
449
+ data_path: str,
450
+ image_size: int = 256,
451
+ batch_size: int = 128,
452
+ num_workers: int = 8,
453
+ ) -> DataLoader:
454
+ """Create ImageNet validation dataloader."""
455
+ preprocess = transforms.Compose([
456
+ transforms.Resize((image_size, image_size)),
457
+ transforms.ToTensor(),
458
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
459
+ ])
460
+
461
+ dataset = ImageFolder(root=data_path, transform=preprocess)
462
+ dataloader = DataLoader(
463
+ dataset,
464
+ batch_size=batch_size,
465
+ shuffle=False,
466
+ num_workers=num_workers,
467
+ pin_memory=True,
468
+ )
469
+ return dataloader
470
+
471
+
472
+ def main():
473
+ parser = argparse.ArgumentParser(description="Zero-shot ImageNet evaluation for VTP HuggingFace model")
474
+ parser.add_argument("--model_path", type=str, required=True,
475
+ help="Path to VTP HuggingFace model directory")
476
+ parser.add_argument("--data_path", type=str, required=True,
477
+ help="Path to ImageNet validation dataset")
478
+ parser.add_argument("--batch_size", type=int, default=128,
479
+ help="Batch size for evaluation")
480
+ parser.add_argument("--num_workers", type=int, default=8,
481
+ help="Number of dataloader workers")
482
+ parser.add_argument("--device", type=str, default="cuda:0",
483
+ help="Device to use (e.g., cuda:0, cpu)")
484
+ parser.add_argument("--precision", type=str, default="fp32",
485
+ choices=["fp32", "fp16", "bf16"],
486
+ help="Precision for inference")
487
+ args = parser.parse_args()
488
+
489
+ device = torch.device(args.device)
490
+
491
+ print("=" * 60)
492
+ print("Zero-Shot ImageNet Evaluation (VTP HuggingFace)")
493
+ print("=" * 60)
494
+ print(f"Model path: {args.model_path}")
495
+ print(f"Data path: {args.data_path}")
496
+ print(f"Device: {device}")
497
+ print(f"Precision: {args.precision}")
498
+ print(f"Batch size: {args.batch_size}")
499
+ print()
500
+
501
+ # Load model
502
+ print("Loading model...")
503
+ model = VTPModel.from_pretrained(args.model_path)
504
+ model = model.to(device)
505
+ model.eval()
506
+
507
+ # Get image size from config
508
+ image_size = model.config.image_size
509
+ context_length = model.config.text_context_length
510
+ print(f"Image size: {image_size}")
511
+ print(f"Context length: {context_length}")
512
+
513
+ # Load tokenizer
514
+ print("Loading tokenizer...")
515
+ tokenizer = get_tokenizer('ViT-B-32', context_length=context_length)
516
+
517
+ # Create dataloader
518
+ print(f"Loading ImageNet validation: {args.data_path}")
519
+ dataloader = create_imagenet_dataloader(
520
+ args.data_path,
521
+ image_size=image_size,
522
+ batch_size=args.batch_size,
523
+ num_workers=args.num_workers,
524
+ )
525
+ num_samples = len(dataloader.dataset)
526
+ print(f"Number of samples: {num_samples}")
527
+ if num_samples != 50000:
528
+ print(f" [Warning] Expected 50000 samples for ImageNet validation, got {num_samples}")
529
+ print()
530
+
531
+ # Build classifier
532
+ print("Building zero-shot classifier...")
533
+ autocast_ctx = get_autocast_context(args.precision, device_type=device.type)
534
+ with autocast_ctx():
535
+ classifier = build_zero_shot_classifier(
536
+ model,
537
+ tokenizer,
538
+ classnames=IMAGENET_CLASSNAMES,
539
+ templates=OPENAI_IMAGENET_TEMPLATES,
540
+ num_classes_per_batch=10,
541
+ device=device,
542
+ use_tqdm=True,
543
+ )
544
+ print()
545
+
546
+ # Evaluate
547
+ print("Running evaluation...")
548
+ top1, top5 = evaluate(
549
+ model,
550
+ classifier,
551
+ dataloader,
552
+ device=device,
553
+ precision=args.precision,
554
+ )
555
+
556
+ print()
557
+ print("=" * 60)
558
+ print("Results:")
559
+ print(f" ImageNet Zero-Shot Top-1 Accuracy: {top1:.2f}%")
560
+ print(f" ImageNet Zero-Shot Top-5 Accuracy: {top5:.2f}%")
561
+ print("=" * 60)
562
+
563
+ return {"top1": top1, "top5": top5}
564
+
565
+
566
+ if __name__ == "__main__":
567
+ main()
patch-reparameterization/src/eval/clip_zero_shot.py ADDED
@@ -0,0 +1,714 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Zero-shot evaluation script for SigLIP2.
4
+
5
+ Modified from OpenCLIP.
6
+
7
+ Usage:
8
+ python clip_zero_shot.py --model_path /path/to/siglip2_model --data_path /path/to/imagenet/val
9
+
10
+ # With custom settings
11
+ python clip_zero_shot.py \
12
+ --model_path /path/to/siglip2_model \
13
+ --data_path /path/to/imagenet/val \
14
+ --batch_size 256 \
15
+ --device cuda:0 \
16
+ --precision bf16
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import logging
24
+ import sys
25
+ import string
26
+ import re
27
+
28
+ from functools import partial
29
+ from itertools import islice
30
+ from pathlib import Path
31
+ from typing import Callable, List, Optional, Sequence, Tuple, Union
32
+
33
+ import torch
34
+ import torch.nn as nn
35
+ import torch.nn.functional as F
36
+ from torch.utils.data import DataLoader
37
+ from torchvision import transforms
38
+ from torchvision.datasets import ImageFolder
39
+ from PIL import Image
40
+ from tqdm import tqdm
41
+ from transformers import AutoProcessor, SiglipModel
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ # ============================================================================
47
+ # ImageNet classnames and templates (copied from tools/zero_shot/templates.py)
48
+ # ============================================================================
49
+
50
+ IMAGENET_CLASSNAMES = (
51
+ "tench", "goldfish", "great white shark", "tiger shark", "hammerhead shark", "electric ray",
52
+ "stingray", "rooster", "hen", "ostrich", "brambling", "goldfinch", "house finch", "junco",
53
+ "indigo bunting", "American robin", "bulbul", "jay", "magpie", "chickadee", "American dipper",
54
+ "kite (bird of prey)", "bald eagle", "vulture", "great grey owl", "fire salamander",
55
+ "smooth newt", "newt", "spotted salamander", "axolotl", "American bullfrog", "tree frog",
56
+ "tailed frog", "loggerhead sea turtle", "leatherback sea turtle", "mud turtle", "terrapin",
57
+ "box turtle", "banded gecko", "green iguana", "Carolina anole",
58
+ "desert grassland whiptail lizard", "agama", "frilled-necked lizard", "alligator lizard",
59
+ "Gila monster", "European green lizard", "chameleon", "Komodo dragon", "Nile crocodile",
60
+ "American alligator", "triceratops", "worm snake", "ring-necked snake",
61
+ "eastern hog-nosed snake", "smooth green snake", "kingsnake", "garter snake", "water snake",
62
+ "vine snake", "night snake", "boa constrictor", "African rock python", "Indian cobra",
63
+ "green mamba", "sea snake", "Saharan horned viper", "eastern diamondback rattlesnake",
64
+ "sidewinder rattlesnake", "trilobite", "harvestman", "scorpion", "yellow garden spider",
65
+ "barn spider", "European garden spider", "southern black widow", "tarantula", "wolf spider",
66
+ "tick", "centipede", "black grouse", "ptarmigan", "ruffed grouse", "prairie grouse", "peafowl",
67
+ "quail", "partridge", "african grey parrot", "macaw", "sulphur-crested cockatoo", "lorikeet",
68
+ "coucal", "bee eater", "hornbill", "hummingbird", "jacamar", "toucan", "duck",
69
+ "red-breasted merganser", "goose", "black swan", "tusker", "echidna", "platypus", "wallaby",
70
+ "koala", "wombat", "jellyfish", "sea anemone", "brain coral", "flatworm", "nematode", "conch",
71
+ "snail", "slug", "sea slug", "chiton", "chambered nautilus", "Dungeness crab", "rock crab",
72
+ "fiddler crab", "red king crab", "American lobster", "spiny lobster", "crayfish", "hermit crab",
73
+ "isopod", "white stork", "black stork", "spoonbill", "flamingo", "little blue heron",
74
+ "great egret", "bittern bird", "crane bird", "limpkin", "common gallinule", "American coot",
75
+ "bustard", "ruddy turnstone", "dunlin", "common redshank", "dowitcher", "oystercatcher",
76
+ "pelican", "king penguin", "albatross", "grey whale", "killer whale", "dugong", "sea lion",
77
+ "Chihuahua", "Japanese Chin", "Maltese", "Pekingese", "Shih Tzu", "King Charles Spaniel",
78
+ "Papillon", "toy terrier", "Rhodesian Ridgeback", "Afghan Hound", "Basset Hound", "Beagle",
79
+ "Bloodhound", "Bluetick Coonhound", "Black and Tan Coonhound", "Treeing Walker Coonhound",
80
+ "English foxhound", "Redbone Coonhound", "borzoi", "Irish Wolfhound", "Italian Greyhound",
81
+ "Whippet", "Ibizan Hound", "Norwegian Elkhound", "Otterhound", "Saluki", "Scottish Deerhound",
82
+ "Weimaraner", "Staffordshire Bull Terrier", "American Staffordshire Terrier",
83
+ "Bedlington Terrier", "Border Terrier", "Kerry Blue Terrier", "Irish Terrier",
84
+ "Norfolk Terrier", "Norwich Terrier", "Yorkshire Terrier", "Wire Fox Terrier",
85
+ "Lakeland Terrier", "Sealyham Terrier", "Airedale Terrier", "Cairn Terrier",
86
+ "Australian Terrier", "Dandie Dinmont Terrier", "Boston Terrier", "Miniature Schnauzer",
87
+ "Giant Schnauzer", "Standard Schnauzer", "Scottish Terrier", "Tibetan Terrier",
88
+ "Australian Silky Terrier", "Soft-coated Wheaten Terrier", "West Highland White Terrier",
89
+ "Lhasa Apso", "Flat-Coated Retriever", "Curly-coated Retriever", "Golden Retriever",
90
+ "Labrador Retriever", "Chesapeake Bay Retriever", "German Shorthaired Pointer", "Vizsla",
91
+ "English Setter", "Irish Setter", "Gordon Setter", "Brittany dog", "Clumber Spaniel",
92
+ "English Springer Spaniel", "Welsh Springer Spaniel", "Cocker Spaniel", "Sussex Spaniel",
93
+ "Irish Water Spaniel", "Kuvasz", "Schipperke", "Groenendael dog", "Malinois", "Briard",
94
+ "Australian Kelpie", "Komondor", "Old English Sheepdog", "Shetland Sheepdog", "collie",
95
+ "Border Collie", "Bouvier des Flandres dog", "Rottweiler", "German Shepherd Dog", "Dobermann",
96
+ "Miniature Pinscher", "Greater Swiss Mountain Dog", "Bernese Mountain Dog",
97
+ "Appenzeller Sennenhund", "Entlebucher Sennenhund", "Boxer", "Bullmastiff", "Tibetan Mastiff",
98
+ "French Bulldog", "Great Dane", "St. Bernard", "husky", "Alaskan Malamute", "Siberian Husky",
99
+ "Dalmatian", "Affenpinscher", "Basenji", "pug", "Leonberger", "Newfoundland dog",
100
+ "Great Pyrenees dog", "Samoyed", "Pomeranian", "Chow Chow", "Keeshond", "brussels griffon",
101
+ "Pembroke Welsh Corgi", "Cardigan Welsh Corgi", "Toy Poodle", "Miniature Poodle",
102
+ "Standard Poodle", "Mexican hairless dog (xoloitzcuintli)", "grey wolf", "Alaskan tundra wolf",
103
+ "red wolf or maned wolf", "coyote", "dingo", "dhole", "African wild dog", "hyena", "red fox",
104
+ "kit fox", "Arctic fox", "grey fox", "tabby cat", "tiger cat", "Persian cat", "Siamese cat",
105
+ "Egyptian Mau", "cougar", "lynx", "leopard", "snow leopard", "jaguar", "lion", "tiger",
106
+ "cheetah", "brown bear", "American black bear", "polar bear", "sloth bear", "mongoose",
107
+ "meerkat", "tiger beetle", "ladybug", "ground beetle", "longhorn beetle", "leaf beetle",
108
+ "dung beetle", "rhinoceros beetle", "weevil", "fly", "bee", "ant", "grasshopper",
109
+ "cricket insect", "stick insect", "cockroach", "praying mantis", "cicada", "leafhopper",
110
+ "lacewing", "dragonfly", "damselfly", "red admiral butterfly", "ringlet butterfly",
111
+ "monarch butterfly", "small white butterfly", "sulphur butterfly", "gossamer-winged butterfly",
112
+ "starfish", "sea urchin", "sea cucumber", "cottontail rabbit", "hare", "Angora rabbit",
113
+ "hamster", "porcupine", "fox squirrel", "marmot", "beaver", "guinea pig", "common sorrel horse",
114
+ "zebra", "pig", "wild boar", "warthog", "hippopotamus", "ox", "water buffalo", "bison",
115
+ "ram (adult male sheep)", "bighorn sheep", "Alpine ibex", "hartebeest", "impala (antelope)",
116
+ "gazelle", "arabian camel", "llama", "weasel", "mink", "European polecat",
117
+ "black-footed ferret", "otter", "skunk", "badger", "armadillo", "three-toed sloth", "orangutan",
118
+ "gorilla", "chimpanzee", "gibbon", "siamang", "guenon", "patas monkey", "baboon", "macaque",
119
+ "langur", "black-and-white colobus", "proboscis monkey", "marmoset", "white-headed capuchin",
120
+ "howler monkey", "titi monkey", "Geoffroy's spider monkey", "common squirrel monkey",
121
+ "ring-tailed lemur", "indri", "Asian elephant", "African bush elephant", "red panda",
122
+ "giant panda", "snoek fish", "eel", "silver salmon", "rock beauty fish", "clownfish",
123
+ "sturgeon", "gar fish", "lionfish", "pufferfish", "abacus", "abaya", "academic gown",
124
+ "accordion", "acoustic guitar", "aircraft carrier", "airliner", "airship", "altar", "ambulance",
125
+ "amphibious vehicle", "analog clock", "apiary", "apron", "trash can", "assault rifle",
126
+ "backpack", "bakery", "balance beam", "balloon", "ballpoint pen", "Band-Aid", "banjo",
127
+ "baluster / handrail", "barbell", "barber chair", "barbershop", "barn", "barometer", "barrel",
128
+ "wheelbarrow", "baseball", "basketball", "bassinet", "bassoon", "swimming cap", "bath towel",
129
+ "bathtub", "station wagon", "lighthouse", "beaker", "military hat (bearskin or shako)",
130
+ "beer bottle", "beer glass", "bell tower", "baby bib", "tandem bicycle", "bikini",
131
+ "ring binder", "binoculars", "birdhouse", "boathouse", "bobsleigh", "bolo tie", "poke bonnet",
132
+ "bookcase", "bookstore", "bottle cap", "hunting bow", "bow tie", "brass memorial plaque", "bra",
133
+ "breakwater", "breastplate", "broom", "bucket", "buckle", "bulletproof vest",
134
+ "high-speed train", "butcher shop", "taxicab", "cauldron", "candle", "cannon", "canoe",
135
+ "can opener", "cardigan", "car mirror", "carousel", "tool kit", "cardboard box / carton",
136
+ "car wheel", "automated teller machine", "cassette", "cassette player", "castle", "catamaran",
137
+ "CD player", "cello", "mobile phone", "chain", "chain-link fence", "chain mail", "chainsaw",
138
+ "storage chest", "chiffonier", "bell or wind chime", "china cabinet", "Christmas stocking",
139
+ "church", "movie theater", "cleaver", "cliff dwelling", "cloak", "clogs", "cocktail shaker",
140
+ "coffee mug", "coffeemaker", "spiral or coil", "combination lock", "computer keyboard",
141
+ "candy store", "container ship", "convertible", "corkscrew", "cornet", "cowboy boot",
142
+ "cowboy hat", "cradle", "construction crane", "crash helmet", "crate", "infant bed",
143
+ "Crock Pot", "croquet ball", "crutch", "cuirass", "dam", "desk", "desktop computer",
144
+ "rotary dial telephone", "diaper", "digital clock", "digital watch", "dining table",
145
+ "dishcloth", "dishwasher", "disc brake", "dock", "dog sled", "dome", "doormat", "drilling rig",
146
+ "drum", "drumstick", "dumbbell", "Dutch oven", "electric fan", "electric guitar",
147
+ "electric locomotive", "entertainment center", "envelope", "espresso machine", "face powder",
148
+ "feather boa", "filing cabinet", "fireboat", "fire truck", "fire screen", "flagpole", "flute",
149
+ "folding chair", "football helmet", "forklift", "fountain", "fountain pen", "four-poster bed",
150
+ "freight car", "French horn", "frying pan", "fur coat", "garbage truck",
151
+ "gas mask or respirator", "gas pump", "goblet", "go-kart", "golf ball", "golf cart", "gondola",
152
+ "gong", "gown", "grand piano", "greenhouse", "radiator grille", "grocery store", "guillotine",
153
+ "hair clip", "hair spray", "half-track", "hammer", "hamper", "hair dryer", "hand-held computer",
154
+ "handkerchief", "hard disk drive", "harmonica", "harp", "combine harvester", "hatchet",
155
+ "holster", "home theater", "honeycomb", "hook", "hoop skirt", "gymnastic horizontal bar",
156
+ "horse-drawn vehicle", "hourglass", "iPod", "clothes iron", "carved pumpkin", "jeans", "jeep",
157
+ "T-shirt", "jigsaw puzzle", "rickshaw", "joystick", "kimono", "knee pad", "knot", "lab coat",
158
+ "ladle", "lampshade", "laptop computer", "lawn mower", "lens cap", "letter opener", "library",
159
+ "lifeboat", "lighter", "limousine", "ocean liner", "lipstick", "slip-on shoe", "lotion",
160
+ "music speaker", "loupe magnifying glass", "sawmill", "magnetic compass", "messenger bag",
161
+ "mailbox", "tights", "one-piece bathing suit", "manhole cover", "maraca", "marimba", "mask",
162
+ "matchstick", "maypole", "maze", "measuring cup", "medicine cabinet", "megalith", "microphone",
163
+ "microwave oven", "military uniform", "milk can", "minibus", "miniskirt", "minivan", "missile",
164
+ "mitten", "mixing bowl", "mobile home", "ford model t", "modem", "monastery", "monitor",
165
+ "moped", "mortar and pestle", "graduation cap", "mosque", "mosquito net", "vespa",
166
+ "mountain bike", "tent", "computer mouse", "mousetrap", "moving van", "muzzle", "metal nail",
167
+ "neck brace", "necklace", "baby pacifier", "notebook computer", "obelisk", "oboe", "ocarina",
168
+ "odometer", "oil filter", "pipe organ", "oscilloscope", "overskirt", "bullock cart",
169
+ "oxygen mask", "product packet / packaging", "paddle", "paddle wheel", "padlock", "paintbrush",
170
+ "pajamas", "palace", "pan flute", "paper towel", "parachute", "parallel bars", "park bench",
171
+ "parking meter", "railroad car", "patio", "payphone", "pedestal", "pencil case",
172
+ "pencil sharpener", "perfume", "Petri dish", "photocopier", "plectrum", "Pickelhaube",
173
+ "picket fence", "pickup truck", "pier", "piggy bank", "pill bottle", "pillow", "ping-pong ball",
174
+ "pinwheel", "pirate ship", "drink pitcher", "block plane", "planetarium", "plastic bag",
175
+ "plate rack", "farm plow", "plunger", "Polaroid camera", "pole", "police van", "poncho",
176
+ "pool table", "soda bottle", "plant pot", "potter's wheel", "power drill", "prayer rug",
177
+ "printer", "prison", "missile", "projector", "hockey puck", "punching bag", "purse", "quill",
178
+ "quilt", "race car", "racket", "radiator", "radio", "radio telescope", "rain barrel",
179
+ "recreational vehicle", "fishing casting reel", "reflex camera", "refrigerator",
180
+ "remote control", "restaurant", "revolver", "rifle", "rocking chair", "rotisserie", "eraser",
181
+ "rugby ball", "ruler measuring stick", "sneaker", "safe", "safety pin", "salt shaker", "sandal",
182
+ "sarong", "saxophone", "scabbard", "weighing scale", "school bus", "schooner", "scoreboard",
183
+ "CRT monitor", "screw", "screwdriver", "seat belt", "sewing machine", "shield", "shoe store",
184
+ "shoji screen / room divider", "shopping basket", "shopping cart", "shovel", "shower cap",
185
+ "shower curtain", "ski", "balaclava ski mask", "sleeping bag", "slide rule", "sliding door",
186
+ "slot machine", "snorkel", "snowmobile", "snowplow", "soap dispenser", "soccer ball", "sock",
187
+ "solar thermal collector", "sombrero", "soup bowl", "keyboard space bar", "space heater",
188
+ "space shuttle", "spatula", "motorboat", "spider web", "spindle", "sports car", "spotlight",
189
+ "stage", "steam locomotive", "through arch bridge", "steel drum", "stethoscope", "scarf",
190
+ "stone wall", "stopwatch", "stove", "strainer", "tram", "stretcher", "couch", "stupa",
191
+ "submarine", "suit", "sundial", "sunglasses", "sunglasses", "sunscreen", "suspension bridge",
192
+ "mop", "sweatshirt", "swim trunks / shorts", "swing", "electrical switch", "syringe",
193
+ "table lamp", "tank", "tape player", "teapot", "teddy bear", "television", "tennis ball",
194
+ "thatched roof", "front curtain", "thimble", "threshing machine", "throne", "tile roof",
195
+ "toaster", "tobacco shop", "toilet seat", "torch", "totem pole", "tow truck", "toy store",
196
+ "tractor", "semi-trailer truck", "tray", "trench coat", "tricycle", "trimaran", "tripod",
197
+ "triumphal arch", "trolleybus", "trombone", "hot tub", "turnstile", "typewriter keyboard",
198
+ "umbrella", "unicycle", "upright piano", "vacuum cleaner", "vase", "vaulted or arched ceiling",
199
+ "velvet fabric", "vending machine", "vestment", "viaduct", "violin", "volleyball",
200
+ "waffle iron", "wall clock", "wallet", "wardrobe", "military aircraft", "sink",
201
+ "washing machine", "water bottle", "water jug", "water tower", "whiskey jug", "whistle",
202
+ "hair wig", "window screen", "window shade", "Windsor tie", "wine bottle", "airplane wing",
203
+ "wok", "wooden spoon", "wool", "split-rail fence", "shipwreck", "sailboat", "yurt", "website",
204
+ "comic book", "crossword", "traffic or street sign", "traffic light", "dust jacket", "menu",
205
+ "plate", "guacamole", "consomme", "hot pot", "trifle", "ice cream", "popsicle", "baguette",
206
+ "bagel", "pretzel", "cheeseburger", "hot dog", "mashed potatoes", "cabbage", "broccoli",
207
+ "cauliflower", "zucchini", "spaghetti squash", "acorn squash", "butternut squash", "cucumber",
208
+ "artichoke", "bell pepper", "cardoon", "mushroom", "Granny Smith apple", "strawberry", "orange",
209
+ "lemon", "fig", "pineapple", "banana", "jackfruit", "cherimoya (custard apple)", "pomegranate",
210
+ "hay", "carbonara", "chocolate syrup", "dough", "meatloaf", "pizza", "pot pie", "burrito",
211
+ "red wine", "espresso", "tea cup", "eggnog", "mountain", "bubble", "cliff", "coral reef",
212
+ "geyser", "lakeshore", "promontory", "sandbar", "beach", "valley", "volcano", "baseball player",
213
+ "bridegroom", "scuba diver", "rapeseed", "daisy", "yellow lady's slipper", "corn", "acorn",
214
+ "rose hip", "horse chestnut seed", "coral fungus", "agaric", "gyromitra", "stinkhorn mushroom",
215
+ "earth star fungus", "hen of the woods mushroom", "bolete", "corn cob", "toilet paper"
216
+ )
217
+
218
+ CLIP_PAPER_PROMPT_TEMPLATES = [
219
+ 'a bad photo of a {}.', 'a photo of many {}.', 'a sculpture of a {}.',
220
+ 'a photo of the hard to see {}.', 'a low resolution photo of the {}.',
221
+ 'a rendering of a {}.', 'graffiti of a {}.', 'a bad photo of the {}.',
222
+ 'a cropped photo of the {}.', 'a tattoo of a {}.', 'the embroidered {}.',
223
+ 'a photo of a hard to see {}.', 'a bright photo of a {}.',
224
+ 'a photo of a clean {}.', 'a photo of a dirty {}.', 'a dark photo of the {}.',
225
+ 'a drawing of a {}.', 'a photo of my {}.', 'the plastic {}.',
226
+ 'a photo of the cool {}.', 'a close-up photo of a {}.',
227
+ 'a black and white photo of the {}.', 'a painting of the {}.',
228
+ 'a painting of a {}.', 'a pixelated photo of the {}.', 'a sculpture of the {}.',
229
+ 'a bright photo of the {}.', 'a cropped photo of a {}.', 'a plastic {}.',
230
+ 'a photo of the dirty {}.', 'a jpeg corrupted photo of a {}.',
231
+ 'a blurry photo of the {}.', 'a photo of the {}.', 'a good photo of the {}.',
232
+ 'a rendering of the {}.', 'a {} in a video game.', 'a photo of one {}.',
233
+ 'a doodle of a {}.', 'a close-up photo of the {}.', 'a photo of a {}.',
234
+ 'the origami {}.', 'the {} in a video game.', 'a sketch of a {}.',
235
+ 'a doodle of the {}.', 'a origami {}.', 'a low resolution photo of a {}.',
236
+ 'the toy {}.', 'a rendition of the {}.', 'a photo of the clean {}.',
237
+ 'a photo of a large {}.', 'a rendition of a {}.', 'a photo of a nice {}.',
238
+ 'a photo of a weird {}.', 'a blurry photo of a {}.', 'a cartoon {}.',
239
+ 'art of a {}.', 'a sketch of the {}.', 'a embroidered {}.',
240
+ 'a pixelated photo of a {}.', 'itap of the {}.',
241
+ 'a jpeg corrupted photo of the {}.', 'a good photo of a {}.',
242
+ 'a plushie {}.', 'a photo of the nice {}.', 'a photo of the small {}.',
243
+ 'a photo of the weird {}.', 'the cartoon {}.', 'art of the {}.',
244
+ 'a drawing of the {}.', 'a photo of the large {}.',
245
+ 'a black and white photo of a {}.', 'the plushie {}.',
246
+ 'a dark photo of a {}.', 'itap of a {}.', 'graffiti of the {}.',
247
+ 'a toy {}.', 'itap of my {}.', 'a photo of a cool {}.',
248
+ 'a photo of a small {}.', 'a tattoo of the {}.', '{}',
249
+ ]
250
+ def canonicalize_text(text: str, *, keep_punctuation_exact_string: Optional[str] = None) -> str:
251
+ """Canonicalize text (lowercase, remove punctuation, normalize whitespace).
252
+ From big_vision.evaluators.proj.image_text.prompt_engineering.
253
+ """
254
+ text = text.replace("_", " ")
255
+ if keep_punctuation_exact_string:
256
+ text = keep_punctuation_exact_string.join(
257
+ part.translate(str.maketrans("", "", string.punctuation))
258
+ for part in text.split(keep_punctuation_exact_string)
259
+ )
260
+ else:
261
+ text = text.translate(str.maketrans("", "", string.punctuation))
262
+ text = text.lower()
263
+ text = re.sub(r"\s+", " ", text)
264
+ return text.strip()
265
+ def get_canonicalized_class_names(class_names: List[str], first_only: bool = True) -> List[str]:
266
+ """Canonicalize class names, optionally keeping first alias only."""
267
+ out = []
268
+ for name in class_names:
269
+ n = canonicalize_text(name, keep_punctuation_exact_string=",")
270
+ if first_only:
271
+ n = n.split(",")[0].strip() if "," in n else n
272
+ out.append(n)
273
+ return out
274
+
275
+
276
+ def get_canonicalized_templates(templates: List[str]) -> List[str]:
277
+ """Canonicalize prompt templates, preserving '{}' placeholder."""
278
+ return [
279
+ canonicalize_text(t, keep_punctuation_exact_string="{}")
280
+ for t in templates
281
+ ]
282
+ def substitute_prompt(template: str, class_name: str) -> str:
283
+ """Substitute '{}' in template with class_name. From discriminative_classifier."""
284
+ placeholder = "{}"
285
+ parts = template.split(placeholder)
286
+ assert len(parts) == 2, f"Template must have exactly one '{placeholder}': {template}"
287
+ return parts[0] + class_name + parts[1]
288
+ # ============================================================================
289
+ # Utility functions
290
+ # ============================================================================
291
+
292
+ def batched(iterable, n: int):
293
+ """Batch data into lists of length n. The last batch may be shorter."""
294
+ it = iter(iterable)
295
+ while True:
296
+ batch = list(islice(it, n))
297
+ if not batch:
298
+ break
299
+ yield batch
300
+
301
+
302
+ def accuracy(output: torch.Tensor, target: torch.Tensor, topk: Tuple[int, ...] = (1,)) -> List[float]:
303
+ """Calculate top-k accuracy."""
304
+ pred = output.topk(max(topk), 1, True, True)[1].t()
305
+ correct = pred.eq(target.view(1, -1).expand_as(pred))
306
+ return [float(correct[:k].reshape(-1).float().sum(0, keepdim=True).cpu().numpy()) for k in topk]
307
+
308
+
309
+ def get_input_dtype(precision: str) -> torch.dtype:
310
+ """Get input dtype based on precision string."""
311
+ if precision in ('bf16', 'bfloat16'):
312
+ return torch.bfloat16
313
+ elif precision in ('fp16', 'float16'):
314
+ return torch.float16
315
+ return torch.float32
316
+
317
+
318
+ def get_autocast_context(precision: str, device_type: str = 'cuda'):
319
+ """Get autocast context manager based on precision."""
320
+ from contextlib import nullcontext
321
+ if precision in ('bf16', 'bfloat16'):
322
+ return partial(torch.amp.autocast, device_type=device_type, dtype=torch.bfloat16)
323
+ elif precision in ('fp16', 'float16'):
324
+ return partial(torch.amp.autocast, device_type=device_type, dtype=torch.float16)
325
+ return nullcontext
326
+
327
+
328
+ # ============================================================================
329
+ # Zero-shot classifier
330
+ # ============================================================================
331
+
332
+ @torch.no_grad()
333
+ def build_zero_shot_classifier(
334
+ model: AutoModel,
335
+ processor,
336
+ classnames: Sequence[str],
337
+ templates: Sequence[Callable],
338
+ num_classes_per_batch: int = 10,
339
+ device: torch.device = None,
340
+ use_tqdm: bool = True,
341
+ canonicalize: bool = True
342
+
343
+ ) -> torch.Tensor:
344
+ """Build zero-shot classifier weights.
345
+
346
+ Args:
347
+ model: SigLIP2 model instance.
348
+ processor: SigLIP processor for text tokenization.
349
+ classnames: Sequence of class names.
350
+ templates: Sequence of template functions.
351
+ num_classes_per_batch: Number of classes to process per batch.
352
+ device: Device to use.
353
+ use_tqdm: Whether to show progress bar.
354
+
355
+ Returns:
356
+ Zero-shot classifier weights, shape (embed_dim, num_classes).
357
+ """
358
+ if canonicalize:
359
+ classnames = get_canonicalized_class_names(list(classnames))
360
+ templates = get_canonicalized_templates(list(templates))
361
+
362
+ num_templates = len(templates)
363
+ num_classes = len(classnames)
364
+ # 将classnames全部转为小写
365
+ classnames = [c.lower() for c in classnames]
366
+ if use_tqdm:
367
+ num_iter = (num_classes - 1) // num_classes_per_batch + 1
368
+ iter_wrap = partial(tqdm, total=num_iter, desc="Building classifier", unit_scale=num_classes_per_batch)
369
+ else:
370
+ iter_wrap = iter
371
+
372
+ def _process_batch(batch_classnames: List[str]) -> torch.Tensor:
373
+ num_batch_classes = len(batch_classnames)
374
+ texts = [
375
+ substitute_prompt(tpl, c)
376
+ for c in batch_classnames
377
+ for tpl in templates
378
+ ]
379
+ # Use processor to tokenize texts
380
+ inputs = processor(text=texts, return_tensors="pt", padding="max_length", max_length=64 , truncation=True)
381
+ # inputs = processor(text=texts, return_tensors="pt", padding="max_length", max_length=64)
382
+
383
+ input_ids = inputs["input_ids"].to(device)
384
+ attention_mask = inputs.get("attention_mask", None)
385
+ if attention_mask is not None:
386
+ attention_mask = attention_mask.to(device)
387
+
388
+ # Get text features from SigLIP model (with projection head)
389
+ with torch.no_grad():
390
+ text_features = model.get_text_features(
391
+ input_ids=input_ids,
392
+ attention_mask=attention_mask,
393
+ )
394
+
395
+ # Normalize text features
396
+ text_features = F.normalize(text_features, dim=-1)
397
+
398
+ # Reshape and average over templates
399
+ text_features = text_features.reshape(num_batch_classes, num_templates, -1).mean(dim=1)
400
+ text_features = F.normalize(text_features, dim=1)
401
+ return text_features.T # (embed_dim, num_batch_classes)
402
+
403
+ with torch.no_grad():
404
+ batched_embeds = [
405
+ _process_batch(batch)
406
+ for batch in iter_wrap(batched(classnames, num_classes_per_batch))
407
+ ]
408
+ zeroshot_weights = torch.cat(batched_embeds, dim=1)
409
+
410
+ return zeroshot_weights
411
+
412
+
413
+ # ============================================================================
414
+ # Evaluation
415
+ # ============================================================================
416
+ @torch.no_grad()
417
+ def evaluate(
418
+ model: AutoModel,
419
+ processor,
420
+ classifier: torch.Tensor,
421
+ dataloader: DataLoader,
422
+ device: torch.device,
423
+ precision: str = 'fp32',
424
+ ) -> Tuple[float, float]:
425
+ """Run zero-shot evaluation.
426
+
427
+ Args:
428
+ model: SigLIP2 model instance.
429
+ processor: SigLIP processor for image preprocessing.
430
+ classifier: Zero-shot classifier weights, shape (embed_dim, num_classes).
431
+ dataloader: ImageNet validation dataloader.
432
+ device: Device to use.
433
+ precision: Precision for inference.
434
+
435
+ Returns:
436
+ Tuple of (top1_accuracy, top5_accuracy).
437
+ """
438
+ autocast_ctx = get_autocast_context(precision, device_type=device.type)
439
+ input_dtype = get_input_dtype(precision)
440
+
441
+ top1, top5, n = 0.0, 0.0, 0
442
+
443
+ with torch.inference_mode():
444
+ for images, targets in tqdm(dataloader, desc="Evaluating"):
445
+ images = images.to(device=device, dtype=input_dtype)
446
+ targets = targets.to(device)
447
+
448
+ with autocast_ctx():
449
+ # Get image features from SigLIP model (with projection head)
450
+ image_features = model.get_image_features(pixel_values=images)
451
+
452
+ # Normalize image features
453
+ image_features = F.normalize(image_features, dim=-1)
454
+ logits = image_features @ classifier
455
+ logits = model.logit_scale.exp() * logits + model.logit_bias
456
+ acc1, acc5 = accuracy(logits, targets, topk=(1, 5))
457
+ top1 += acc1
458
+ top5 += acc5
459
+ n += images.size(0)
460
+
461
+ top1 = top1 / n * 100
462
+ top5 = top5 / n * 100
463
+ return top1, top5
464
+
465
+
466
+ # ============================================================================
467
+ # PatchReparam encoder wrapper and dataloader for training integration
468
+ # ============================================================================
469
+
470
+
471
+ @torch.no_grad()
472
+ class PatchReparamZeroShotWrapper(nn.Module):
473
+ """Wrapper to run zero-shot eval with PatchReparam encoder (image) + full SigLIP (text + logit).
474
+
475
+ Used when integrating zero-shot into training: image features come from the
476
+ current PatchReparam encoder (with output_head), text features and logit scale/bias
477
+ from a frozen full SiglipModel.
478
+ """
479
+
480
+ def __init__(self, encoder: nn.Module, full_siglip: SiglipModel):
481
+ super().__init__()
482
+ self.encoder = encoder
483
+ self._full_siglip = full_siglip
484
+ # Expose logit parameters for evaluate()
485
+ self.logit_scale = full_siglip.logit_scale
486
+ self.logit_bias = full_siglip.logit_bias
487
+
488
+ def get_image_features(self, pixel_values: torch.Tensor) -> torch.Tensor:
489
+ """Compute image features using PatchReparam encoder + output head (same space as SigLIP)."""
490
+ features = self.encoder.get_image_features(pixel_values)
491
+
492
+ return F.normalize(features, dim=-1)
493
+
494
+ def get_text_features(
495
+ self,
496
+ input_ids: torch.Tensor,
497
+ attention_mask: Optional[torch.Tensor] = None,
498
+ ) -> torch.Tensor:
499
+ """Delegate to full SigLIP text tower."""
500
+ return self._full_siglip.get_text_features(
501
+ input_ids=input_ids,
502
+ attention_mask=attention_mask,
503
+ )
504
+
505
+
506
+ @torch.no_grad()
507
+ def create_imagenet_dataloader_patch_reparam(
508
+ data_path: str,
509
+ encoder_input_size: int,
510
+ encoder_mean: Union[torch.Tensor, List[float]],
511
+ encoder_std: Union[torch.Tensor, List[float]],
512
+ batch_size: int = 128,
513
+ num_workers: int = 8,
514
+ ) -> DataLoader:
515
+ """Create ImageNet val dataloader with PatchReparam encoder preprocessing (resize + normalize).
516
+
517
+ Use this when evaluating PatchReparam encoder zero-shot so input matches encoder expectations.
518
+ """
519
+ if isinstance(encoder_mean, torch.Tensor):
520
+ mean = encoder_mean.squeeze().tolist()
521
+ else:
522
+ mean = encoder_mean
523
+ if isinstance(encoder_std, torch.Tensor):
524
+ std = encoder_std.squeeze().tolist()
525
+ else:
526
+ std = encoder_std
527
+ transform = transforms.Compose([
528
+ transforms.Resize(encoder_input_size, interpolation=transforms.InterpolationMode.BICUBIC),
529
+ transforms.CenterCrop(encoder_input_size),
530
+ transforms.ToTensor(),
531
+ transforms.Normalize(mean=mean, std=std),
532
+ ])
533
+ dataset = ImageFolder(root=data_path, transform=transform)
534
+ return DataLoader(
535
+ dataset,
536
+ batch_size=batch_size,
537
+ shuffle=False,
538
+ num_workers=num_workers,
539
+ pin_memory=True,
540
+ )
541
+
542
+
543
+ # ============================================================================
544
+ # Main
545
+ # ============================================================================
546
+
547
+ def create_imagenet_dataloader(
548
+ data_path: str,
549
+ processor,
550
+ batch_size: int = 128,
551
+ num_workers: int = 8,
552
+ ) -> DataLoader:
553
+ """Create ImageNet validation dataloader using processor for preprocessing."""
554
+ from PIL import Image
555
+
556
+ class ImageNetDataset(ImageFolder):
557
+ def __getitem__(self, index):
558
+ path, target = self.samples[index]
559
+ image = Image.open(path).convert('RGB')
560
+ # Use processor to preprocess image
561
+ inputs = processor(images=image, return_tensors="pt")
562
+ pixel_values = inputs["pixel_values"].squeeze(0) # Remove batch dimension
563
+ return pixel_values, target
564
+
565
+ dataset = ImageNetDataset(root=data_path)
566
+ # 验证类别顺序
567
+
568
+ dataloader = DataLoader(
569
+ dataset,
570
+ batch_size=batch_size,
571
+ shuffle=False,
572
+ num_workers=num_workers,
573
+ pin_memory=True,
574
+ )
575
+ return dataloader
576
+
577
+
578
+ def main():
579
+ parser = argparse.ArgumentParser(description="Zero-shot ImageNet evaluation for SigLIP2")
580
+ parser.add_argument("--model_path", type=str, required=True,
581
+ help="Path to SigLIP2 model directory")
582
+ parser.add_argument("--data_path", type=str, required=True,
583
+ help="Path to ImageNet validation dataset")
584
+ parser.add_argument("--batch_size", type=int, default=128,
585
+ help="Batch size for evaluation")
586
+ parser.add_argument("--num_workers", type=int, default=8,
587
+ help="Number of dataloader workers")
588
+ parser.add_argument("--device", type=str, default="cuda:0",
589
+ help="Device to use (e.g., cuda:0, cpu)")
590
+ parser.add_argument("--precision", type=str, default="fp32",
591
+ choices=["fp32", "fp16", "bf16"],
592
+ help="Precision for inference")
593
+ args = parser.parse_args()
594
+
595
+ device = torch.device(args.device)
596
+
597
+ print("=" * 60)
598
+ print("Zero-Shot ImageNet Evaluation (SigLIP2)")
599
+ print("=" * 60)
600
+ print(f"Model path: {args.model_path}")
601
+ print(f"Data path: {args.data_path}")
602
+ print(f"Device: {device}")
603
+ print(f"Precision: {args.precision}")
604
+ print(f"Batch size: {args.batch_size}")
605
+ print()
606
+
607
+ # Load model and processor
608
+ print("Loading model and processor...")
609
+ # model = AutoModel.from_pretrained(args.model_path, trust_remote_code=True)
610
+ model = SiglipModel.from_pretrained(args.model_path)
611
+ processor = AutoProcessor.from_pretrained(args.model_path)
612
+ model = model.to(device)
613
+ model.eval()
614
+
615
+ # Debug: print model structure
616
+ print(f"Model type: {type(model).__name__}")
617
+ if hasattr(model, 'vision_model'):
618
+ print(f" Has vision_model: {type(model.vision_model).__name__}")
619
+ if hasattr(model, 'text_model'):
620
+ print(f" Has text_model: {type(model.text_model).__name__}")
621
+ if hasattr(model, 'get_image_features'):
622
+ print(f" Has get_image_features method")
623
+ if hasattr(model, 'get_text_features'):
624
+ print(f" Has get_text_features method")
625
+ print()
626
+
627
+ # Get image size from processor
628
+ if hasattr(processor, 'image_processor'):
629
+ image_size = processor.image_processor.size
630
+ print(f"Image size: {image_size}")
631
+ print()
632
+
633
+ # Create dataloader
634
+ print(f"Loading ImageNet validation: {args.data_path}")
635
+ dataloader = create_imagenet_dataloader(
636
+ args.data_path,
637
+ processor=processor,
638
+ batch_size=args.batch_size,
639
+ num_workers=args.num_workers,
640
+ )
641
+
642
+ dataset = ImageFolder(root=args.data_path)
643
+ class_to_idx = dataset.class_to_idx # 字典:{文件夹名: 索引}
644
+ sorted_classes = sorted(class_to_idx.items(), key=lambda x: x[1]) # 按索引排序
645
+
646
+ # 检查前几个类别是否匹配
647
+ for i, (folder_name, idx) in enumerate(sorted_classes[:10]):
648
+ print(f"Index {idx}: {folder_name} -> {IMAGENET_CLASSNAMES[idx]}")
649
+
650
+ num_samples = len(dataloader.dataset)
651
+ print(f"Number of samples: {num_samples}")
652
+ if num_samples != 50000:
653
+ print(f" [Warning] Expected 50000 samples for ImageNet validation, got {num_samples}")
654
+ print()
655
+
656
+ # Build classifier
657
+ print("Building zero-shot classifier...")
658
+ autocast_ctx = get_autocast_context(args.precision, device_type=device.type)
659
+ with autocast_ctx():
660
+ classifier = build_zero_shot_classifier(
661
+ model,
662
+ processor,
663
+ classnames=IMAGENET_CLASSNAMES,
664
+ templates=CLIP_PAPER_PROMPT_TEMPLATES,
665
+ num_classes_per_batch=10,
666
+ device=device,
667
+ use_tqdm=True,
668
+ canonicalize=True,
669
+ )
670
+ print()
671
+
672
+ # Evaluate
673
+ print("Running evaluation...")
674
+ top1, top5 = evaluate(
675
+ model,
676
+ processor,
677
+ classifier,
678
+ dataloader,
679
+ device=device,
680
+ precision=args.precision,
681
+ )
682
+
683
+ print()
684
+ print("=" * 60)
685
+ print("Results:")
686
+ print(f" ImageNet Zero-Shot Top-1 Accuracy: {top1:.2f}%")
687
+ print(f" ImageNet Zero-Shot Top-5 Accuracy: {top5:.2f}%")
688
+ print("=" * 60)
689
+
690
+ results = {"top1": top1, "top5": top5}
691
+ # Write results to model_path
692
+ out_dir = Path(args.model_path)
693
+ out_dir.mkdir(parents=True, exist_ok=True)
694
+ out_json = out_dir / "zeroshot_imagenet_results.json"
695
+ with open(out_json, "w", encoding="utf-8") as f:
696
+ json.dump(
697
+ {
698
+ "top1": top1,
699
+ "top5": top5,
700
+ "data_path": args.data_path,
701
+ "precision": args.precision,
702
+ "batch_size": args.batch_size,
703
+ },
704
+ f,
705
+ indent=2,
706
+ ensure_ascii=False,
707
+ )
708
+ print(f"Results written to {out_json}")
709
+
710
+ return results
711
+
712
+
713
+ if __name__ == "__main__":
714
+ main()
patch-reparameterization/src/eval/clip_zero_shot_bigvision.py ADDED
@@ -0,0 +1,562 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Zero-shot ImageNet evaluation, fully aligned with big_vision discriminative_classifier.
4
+
5
+ Differences from clip_zero_shot.py:
6
+ - Image: resize(224) | value_range(-1,1) [no mean/std]
7
+ - Logit: raw cosine similarity (no logit_scale, no logit_bias)
8
+ - Class names: canonicalize (lowercase, remove punctuation, normalize whitespace)
9
+ - Templates: CLIP_PAPER 77 templates with canonicalize
10
+ - Text: average embeddings per class, then L2 normalize
11
+
12
+ Usage:
13
+ python clip_zero_shot_bigvision.py --model_path /path/to/model --data_path /path/to/imagenet/val
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import re
21
+ import string
22
+ from functools import partial
23
+ from itertools import islice
24
+ from pathlib import Path
25
+ from typing import Callable, List, Optional, Sequence, Tuple, Union
26
+
27
+ import torch
28
+ import torch.nn.functional as F
29
+ from torch.utils.data import DataLoader
30
+ from torchvision import transforms
31
+ from torchvision.datasets import ImageFolder
32
+ from tqdm import tqdm
33
+ from transformers import AutoProcessor, SiglipModel
34
+
35
+
36
+ # ============================================================================
37
+ # Aligned with big_vision prompt_engineering
38
+ # ============================================================================
39
+
40
+ def canonicalize_text(text: str, *, keep_punctuation_exact_string: Optional[str] = None) -> str:
41
+ """Canonicalize text (lowercase, remove punctuation, normalize whitespace).
42
+ From big_vision.evaluators.proj.image_text.prompt_engineering.
43
+ """
44
+ text = text.replace("_", " ")
45
+ if keep_punctuation_exact_string:
46
+ text = keep_punctuation_exact_string.join(
47
+ part.translate(str.maketrans("", "", string.punctuation))
48
+ for part in text.split(keep_punctuation_exact_string)
49
+ )
50
+ else:
51
+ text = text.translate(str.maketrans("", "", string.punctuation))
52
+ text = text.lower()
53
+ text = re.sub(r"\s+", " ", text)
54
+ return text.strip()
55
+
56
+
57
+ # CLIP ImageNet class names (from big_vision.datasets.imagenet.class_names)
58
+ CLIP_IMAGENET_CLASS_NAMES = [
59
+ 'tench', 'goldfish', 'great white shark', 'tiger shark', 'hammerhead shark',
60
+ 'electric ray', 'stingray', 'rooster', 'hen', 'ostrich', 'brambling',
61
+ 'goldfinch', 'house finch', 'junco', 'indigo bunting', 'American robin',
62
+ 'bulbul', 'jay', 'magpie', 'chickadee', 'American dipper',
63
+ 'kite (bird of prey)', 'bald eagle', 'vulture', 'great grey owl',
64
+ 'fire salamander', 'smooth newt', 'newt', 'spotted salamander', 'axolotl',
65
+ 'American bullfrog', 'tree frog', 'tailed frog', 'loggerhead sea turtle',
66
+ 'leatherback sea turtle', 'mud turtle', 'terrapin', 'box turtle',
67
+ 'banded gecko', 'green iguana', 'Carolina anole',
68
+ 'desert grassland whiptail lizard', 'agama', 'frilled-necked lizard',
69
+ 'alligator lizard', 'Gila monster', 'European green lizard', 'chameleon',
70
+ 'Komodo dragon', 'Nile crocodile', 'American alligator', 'triceratops',
71
+ 'worm snake', 'ring-necked snake', 'eastern hog-nosed snake',
72
+ 'smooth green snake', 'kingsnake', 'garter snake', 'water snake',
73
+ 'vine snake', 'night snake', 'boa constrictor', 'African rock python',
74
+ 'Indian cobra', 'green mamba', 'sea snake', 'Saharan horned viper',
75
+ 'eastern diamondback rattlesnake', 'sidewinder rattlesnake', 'trilobite',
76
+ 'harvestman', 'scorpion', 'yellow garden spider', 'barn spider',
77
+ 'European garden spider', 'southern black widow', 'tarantula',
78
+ 'wolf spider', 'tick', 'centipede', 'black grouse', 'ptarmigan',
79
+ 'ruffed grouse', 'prairie grouse', 'peafowl', 'quail', 'partridge',
80
+ 'african grey parrot', 'macaw', 'sulphur-crested cockatoo', 'lorikeet',
81
+ 'coucal', 'bee eater', 'hornbill', 'hummingbird', 'jacamar', 'toucan',
82
+ 'duck', 'red-breasted merganser', 'goose', 'black swan', 'tusker',
83
+ 'echidna', 'platypus', 'wallaby', 'koala', 'wombat', 'jellyfish',
84
+ 'sea anemone', 'brain coral', 'flatworm', 'nematode', 'conch', 'snail',
85
+ 'slug', 'sea slug', 'chiton', 'chambered nautilus', 'Dungeness crab',
86
+ 'rock crab', 'fiddler crab', 'red king crab', 'American lobster',
87
+ 'spiny lobster', 'crayfish', 'hermit crab', 'isopod', 'white stork',
88
+ 'black stork', 'spoonbill', 'flamingo', 'little blue heron', 'great egret',
89
+ 'bittern bird', 'crane bird', 'limpkin', 'common gallinule',
90
+ 'American coot', 'bustard', 'ruddy turnstone', 'dunlin', 'common redshank',
91
+ 'dowitcher', 'oystercatcher', 'pelican', 'king penguin', 'albatross',
92
+ 'grey whale', 'killer whale', 'dugong', 'sea lion', 'Chihuahua',
93
+ 'Japanese Chin', 'Maltese', 'Pekingese', 'Shih Tzu', 'King Charles Spaniel',
94
+ 'Papillon', 'toy terrier', 'Rhodesian Ridgeback', 'Afghan Hound',
95
+ 'Basset Hound', 'Beagle', 'Bloodhound', 'Bluetick Coonhound',
96
+ 'Black and Tan Coonhound', 'Treeing Walker Coonhound', 'English foxhound',
97
+ 'Redbone Coonhound', 'borzoi', 'Irish Wolfhound', 'Italian Greyhound',
98
+ 'Whippet', 'Ibizan Hound', 'Norwegian Elkhound', 'Otterhound', 'Saluki',
99
+ 'Scottish Deerhound', 'Weimaraner', 'Staffordshire Bull Terrier',
100
+ 'American Staffordshire Terrier', 'Bedlington Terrier', 'Border Terrier',
101
+ 'Kerry Blue Terrier', 'Irish Terrier', 'Norfolk Terrier', 'Norwich Terrier',
102
+ 'Yorkshire Terrier', 'Wire Fox Terrier', 'Lakeland Terrier',
103
+ 'Sealyham Terrier', 'Airedale Terrier', 'Cairn Terrier',
104
+ 'Australian Terrier', 'Dandie Dinmont Terrier', 'Boston Terrier',
105
+ 'Miniature Schnauzer', 'Giant Schnauzer', 'Standard Schnauzer',
106
+ 'Scottish Terrier', 'Tibetan Terrier', 'Australian Silky Terrier',
107
+ 'Soft-coated Wheaten Terrier', 'West Highland White Terrier', 'Lhasa Apso',
108
+ 'Flat-Coated Retriever', 'Curly-coated Retriever', 'Golden Retriever',
109
+ 'Labrador Retriever', 'Chesapeake Bay Retriever',
110
+ 'German Shorthaired Pointer', 'Vizsla', 'English Setter', 'Irish Setter',
111
+ 'Gordon Setter', 'Brittany dog', 'Clumber Spaniel',
112
+ 'English Springer Spaniel', 'Welsh Springer Spaniel', 'Cocker Spaniel',
113
+ 'Sussex Spaniel', 'Irish Water Spaniel', 'Kuvasz', 'Schipperke',
114
+ 'Groenendael dog', 'Malinois', 'Briard', 'Australian Kelpie', 'Komondor',
115
+ 'Old English Sheepdog', 'Shetland Sheepdog', 'collie', 'Border Collie',
116
+ 'Bouvier des Flandres dog', 'Rottweiler', 'German Shepherd Dog',
117
+ 'Dobermann', 'Miniature Pinscher', 'Greater Swiss Mountain Dog',
118
+ 'Bernese Mountain Dog', 'Appenzeller Sennenhund', 'Entlebucher Sennenhund',
119
+ 'Boxer', 'Bullmastiff', 'Tibetan Mastiff', 'French Bulldog', 'Great Dane',
120
+ 'St. Bernard', 'husky', 'Alaskan Malamute', 'Siberian Husky', 'Dalmatian',
121
+ 'Affenpinscher', 'Basenji', 'pug', 'Leonberger', 'Newfoundland dog',
122
+ 'Great Pyrenees dog', 'Samoyed', 'Pomeranian', 'Chow Chow', 'Keeshond',
123
+ 'brussels griffon', 'Pembroke Welsh Corgi', 'Cardigan Welsh Corgi',
124
+ 'Toy Poodle', 'Miniature Poodle', 'Standard Poodle',
125
+ 'Mexican hairless dog (xoloitzcuintli)', 'grey wolf', 'Alaskan tundra wolf',
126
+ 'red wolf or maned wolf', 'coyote', 'dingo', 'dhole', 'African wild dog',
127
+ 'hyena', 'red fox', 'kit fox', 'Arctic fox', 'grey fox', 'tabby cat',
128
+ 'tiger cat', 'Persian cat', 'Siamese cat', 'Egyptian Mau', 'cougar', 'lynx',
129
+ 'leopard', 'snow leopard', 'jaguar', 'lion', 'tiger', 'cheetah',
130
+ 'brown bear', 'American black bear', 'polar bear', 'sloth bear', 'mongoose',
131
+ 'meerkat', 'tiger beetle', 'ladybug', 'ground beetle', 'longhorn beetle',
132
+ 'leaf beetle', 'dung beetle', 'rhinoceros beetle', 'weevil', 'fly', 'bee',
133
+ 'ant', 'grasshopper', 'cricket insect', 'stick insect', 'cockroach',
134
+ 'praying mantis', 'cicada', 'leafhopper', 'lacewing', 'dragonfly',
135
+ 'damselfly', 'red admiral butterfly', 'ringlet butterfly',
136
+ 'monarch butterfly', 'small white butterfly', 'sulphur butterfly',
137
+ 'gossamer-winged butterfly', 'starfish', 'sea urchin', 'sea cucumber',
138
+ 'cottontail rabbit', 'hare', 'Angora rabbit', 'hamster', 'porcupine',
139
+ 'fox squirrel', 'marmot', 'beaver', 'guinea pig', 'common sorrel horse',
140
+ 'zebra', 'pig', 'wild boar', 'warthog', 'hippopotamus', 'ox',
141
+ 'water buffalo', 'bison', 'ram (adult male sheep)', 'bighorn sheep',
142
+ 'Alpine ibex', 'hartebeest', 'impala (antelope)', 'gazelle',
143
+ 'arabian camel', 'llama', 'weasel', 'mink', 'European polecat',
144
+ 'black-footed ferret', 'otter', 'skunk', 'badger', 'armadillo',
145
+ 'three-toed sloth', 'orangutan', 'gorilla', 'chimpanzee', 'gibbon',
146
+ 'siamang', 'guenon', 'patas monkey', 'baboon', 'macaque', 'langur',
147
+ 'black-and-white colobus', 'proboscis monkey', 'marmoset',
148
+ 'white-headed capuchin', 'howler monkey', 'titi monkey',
149
+ "Geoffroy's spider monkey", 'common squirrel monkey', 'ring-tailed lemur',
150
+ 'indri', 'Asian elephant', 'African bush elephant', 'red panda',
151
+ 'giant panda', 'snoek fish', 'eel', 'silver salmon', 'rock beauty fish',
152
+ 'clownfish', 'sturgeon', 'gar fish', 'lionfish', 'pufferfish', 'abacus',
153
+ 'abaya', 'academic gown', 'accordion', 'acoustic guitar', 'aircraft carrier',
154
+ 'airliner', 'airship', 'altar', 'ambulance', 'amphibious vehicle',
155
+ 'analog clock', 'apiary', 'apron', 'trash can', 'assault rifle',
156
+ 'backpack', 'bakery', 'balance beam', 'balloon', 'ballpoint pen',
157
+ 'Band-Aid', 'banjo', 'baluster / handrail', 'barbell', 'barber chair',
158
+ 'barbershop', 'barn', 'barometer', 'barrel', 'wheelbarrow', 'baseball',
159
+ 'basketball', 'bassinet', 'bassoon', 'swimming cap', 'bath towel',
160
+ 'bathtub', 'station wagon', 'lighthouse', 'beaker',
161
+ 'military hat (bearskin or shako)', 'beer bottle', 'beer glass',
162
+ 'bell tower', 'baby bib', 'tandem bicycle', 'bikini', 'ring binder',
163
+ 'binoculars', 'birdhouse', 'boathouse', 'bobsleigh', 'bolo tie',
164
+ 'poke bonnet', 'bookcase', 'bookstore', 'bottle cap', 'hunting bow',
165
+ 'bow tie', 'brass memorial plaque', 'bra', 'breakwater', 'breastplate',
166
+ 'broom', 'bucket', 'buckle', 'bulletproof vest', 'high-speed train',
167
+ 'butcher shop', 'taxicab', 'cauldron', 'candle', 'cannon', 'canoe',
168
+ 'can opener', 'cardigan', 'car mirror', 'carousel', 'tool kit',
169
+ 'cardboard box / carton', 'car wheel', 'automated teller machine',
170
+ 'cassette', 'cassette player', 'castle', 'catamaran', 'CD player',
171
+ 'cello', 'mobile phone', 'chain', 'chain-link fence', 'chain mail',
172
+ 'chainsaw', 'storage chest', 'chiffonier', 'bell or wind chime',
173
+ 'china cabinet', 'Christmas stocking', 'church', 'movie theater',
174
+ 'cleaver', 'cliff dwelling', 'cloak', 'clogs', 'cocktail shaker',
175
+ 'coffee mug', 'coffeemaker', 'spiral or coil', 'combination lock',
176
+ 'computer keyboard', 'candy store', 'container ship', 'convertible',
177
+ 'corkscrew', 'cornet', 'cowboy boot', 'cowboy hat', 'cradle',
178
+ 'construction crane', 'crash helmet', 'crate', 'infant bed',
179
+ 'Crock Pot', 'croquet ball', 'crutch', 'cuirass', 'dam', 'desk',
180
+ 'desktop computer', 'rotary dial telephone', 'diaper', 'digital clock',
181
+ 'digital watch', 'dining table', 'dishcloth', 'dishwasher', 'disc brake',
182
+ 'dock', 'dog sled', 'dome', 'doormat', 'drilling rig', 'drum', 'drumstick',
183
+ 'dumbbell', 'Dutch oven', 'electric fan', 'electric guitar',
184
+ 'electric locomotive', 'entertainment center', 'envelope',
185
+ 'espresso machine', 'face powder', 'feather boa', 'filing cabinet',
186
+ 'fireboat', 'fire truck', 'fire screen', 'flagpole', 'flute',
187
+ 'folding chair', 'football helmet', 'forklift', 'fountain', 'fountain pen',
188
+ 'four-poster bed', 'freight car', 'French horn', 'frying pan', 'fur coat',
189
+ 'garbage truck', 'gas mask or respirator', 'gas pump', 'goblet', 'go-kart',
190
+ 'golf ball', 'golf cart', 'gondola', 'gong', 'gown', 'grand piano',
191
+ 'greenhouse', 'radiator grille', 'grocery store', 'guillotine',
192
+ 'hair clip', 'hair spray', 'half-track', 'hammer', 'hamper', 'hair dryer',
193
+ 'hand-held computer', 'handkerchief', 'hard disk drive', 'harmonica',
194
+ 'harp', 'combine harvester', 'hatchet', 'holster', 'home theater',
195
+ 'honeycomb', 'hook', 'hoop skirt', 'gymnastic horizontal bar',
196
+ 'horse-drawn vehicle', 'hourglass', 'iPod', 'clothes iron',
197
+ 'carved pumpkin', 'jeans', 'jeep', 'T-shirt', 'jigsaw puzzle', 'rickshaw',
198
+ 'joystick', 'kimono', 'knee pad', 'knot', 'lab coat', 'ladle', 'lampshade',
199
+ 'laptop computer', 'lawn mower', 'lens cap', 'letter opener', 'library',
200
+ 'lifeboat', 'lighter', 'limousine', 'ocean liner', 'lipstick',
201
+ 'slip-on shoe', 'lotion', 'music speaker', 'loupe magnifying glass',
202
+ 'sawmill', 'magnetic compass', 'messenger bag', 'mailbox', 'tights',
203
+ 'one-piece bathing suit', 'manhole cover', 'maraca', 'marimba', 'mask',
204
+ 'matchstick', 'maypole', 'maze', 'measuring cup', 'medicine cabinet',
205
+ 'megalith', 'microphone', 'microwave oven', 'military uniform', 'milk can',
206
+ 'minibus', 'miniskirt', 'minivan', 'missile', 'mitten', 'mixing bowl',
207
+ 'mobile home', 'ford model t', 'modem', 'monastery', 'monitor', 'moped',
208
+ 'mortar and pestle', 'graduation cap', 'mosque', 'mosquito net', 'vespa',
209
+ 'mountain bike', 'tent', 'computer mouse', 'mousetrap', 'moving van',
210
+ 'muzzle', 'metal nail', 'neck brace', 'necklace', 'baby pacifier',
211
+ 'notebook computer', 'obelisk', 'oboe', 'ocarina', 'odometer', 'oil filter',
212
+ 'pipe organ', 'oscilloscope', 'overskirt', 'bullock cart', 'oxygen mask',
213
+ 'product packet / packaging', 'paddle', 'paddle wheel', 'padlock',
214
+ 'paintbrush', 'pajamas', 'palace', 'pan flute', 'paper towel', 'parachute',
215
+ 'parallel bars', 'park bench', 'parking meter', 'railroad car', 'patio',
216
+ 'payphone', 'pedestal', 'pencil case', 'pencil sharpener', 'perfume',
217
+ 'Petri dish', 'photocopier', 'plectrum', 'Pickelhaube', 'picket fence',
218
+ 'pickup truck', 'pier', 'piggy bank', 'pill bottle', 'pillow',
219
+ 'ping-pong ball', 'pinwheel', 'pirate ship', 'drink pitcher',
220
+ 'block plane', 'planetarium', 'plastic bag', 'plate rack', 'farm plow',
221
+ 'plunger', 'Polaroid camera', 'pole', 'police van', 'poncho', 'pool table',
222
+ 'soda bottle', 'plant pot', "potter's wheel", 'power drill', 'prayer rug',
223
+ 'printer', 'prison', 'missile', 'projector', 'hockey puck', 'punching bag',
224
+ 'purse', 'quill', 'quilt', 'race car', 'racket', 'radiator', 'radio',
225
+ 'radio telescope', 'rain barrel', 'recreational vehicle',
226
+ 'fishing casting reel', 'reflex camera', 'refrigerator', 'remote control',
227
+ 'restaurant', 'revolver', 'rifle', 'rocking chair', 'rotisserie', 'eraser',
228
+ 'rugby ball', 'ruler measuring stick', 'sneaker', 'safe', 'safety pin',
229
+ 'salt shaker', 'sandal', 'sarong', 'saxophone', 'scabbard', 'weighing scale',
230
+ 'school bus', 'schooner', 'scoreboard', 'CRT monitor', 'screw', 'screwdriver',
231
+ 'seat belt', 'sewing machine', 'shield', 'shoe store',
232
+ 'shoji screen / room divider', 'shopping basket', 'shopping cart', 'shovel',
233
+ 'shower cap', 'shower curtain', 'ski', 'balaclava ski mask', 'sleeping bag',
234
+ 'slide rule', 'sliding door', 'slot machine', 'snorkel', 'snowmobile',
235
+ 'snowplow', 'soap dispenser', 'soccer ball', 'sock',
236
+ 'solar thermal collector', 'sombrero', 'soup bowl', 'keyboard space bar',
237
+ 'space heater', 'space shuttle', 'spatula', 'motorboat', 'spider web',
238
+ 'spindle', 'sports car', 'spotlight', 'stage', 'steam locomotive',
239
+ 'through arch bridge', 'steel drum', 'stethoscope', 'scarf', 'stone wall',
240
+ 'stopwatch', 'stove', 'strainer', 'tram', 'stretcher', 'couch', 'stupa',
241
+ 'submarine', 'suit', 'sundial', 'sunglasses', 'sunglasses', 'sunscreen',
242
+ 'suspension bridge', 'mop', 'sweatshirt', 'swim trunks / shorts', 'swing',
243
+ 'electrical switch', 'syringe', 'table lamp', 'tank', 'tape player',
244
+ 'teapot', 'teddy bear', 'television', 'tennis ball', 'thatched roof',
245
+ 'front curtain', 'thimble', 'threshing machine', 'throne', 'tile roof',
246
+ 'toaster', 'tobacco shop', 'toilet seat', 'torch', 'totem pole', 'tow truck',
247
+ 'toy store', 'tractor', 'semi-trailer truck', 'tray', 'trench coat',
248
+ 'tricycle', 'trimaran', 'tripod', 'triumphal arch', 'trolleybus',
249
+ 'trombone', 'hot tub', 'turnstile', 'typewriter keyboard', 'umbrella',
250
+ 'unicycle', 'upright piano', 'vacuum cleaner', 'vase',
251
+ 'vaulted or arched ceiling', 'velvet fabric', 'vending machine', 'vestment',
252
+ 'viaduct', 'violin', 'volleyball', 'waffle iron', 'wall clock', 'wallet',
253
+ 'wardrobe', 'military aircraft', 'sink', 'washing machine', 'water bottle',
254
+ 'water jug', 'water tower', 'whiskey jug', 'whistle', 'hair wig',
255
+ 'window screen', 'window shade', 'Windsor tie', 'wine bottle',
256
+ 'airplane wing', 'wok', 'wooden spoon', 'wool', 'split-rail fence',
257
+ 'shipwreck', 'sailboat', 'yurt', 'website', 'comic book', 'crossword',
258
+ 'traffic or street sign', 'traffic light', 'dust jacket', 'menu', 'plate',
259
+ 'guacamole', 'consomme', 'hot pot', 'trifle', 'ice cream', 'popsicle',
260
+ 'baguette', 'bagel', 'pretzel', 'cheeseburger', 'hot dog',
261
+ 'mashed potatoes', 'cabbage', 'broccoli', 'cauliflower', 'zucchini',
262
+ 'spaghetti squash', 'acorn squash', 'butternut squash', 'cucumber',
263
+ 'artichoke', 'bell pepper', 'cardoon', 'mushroom', 'Granny Smith apple',
264
+ 'strawberry', 'orange', 'lemon', 'fig', 'pineapple', 'banana', 'jackfruit',
265
+ 'cherimoya (custard apple)', 'pomegranate', 'hay', 'carbonara',
266
+ 'chocolate syrup', 'dough', 'meatloaf', 'pizza', 'pot pie', 'burrito',
267
+ 'red wine', 'espresso', 'tea cup', 'eggnog', 'mountain', 'bubble',
268
+ 'cliff', 'coral reef', 'geyser', 'lakeshore', 'promontory', 'sandbar',
269
+ 'beach', 'valley', 'volcano', 'baseball player', 'bridegroom',
270
+ 'scuba diver', 'rapeseed', 'daisy', 'yellow lady\'s slipper', 'corn',
271
+ 'acorn', 'rose hip', 'horse chestnut seed', 'coral fungus', 'agaric',
272
+ 'gyromitra', 'stinkhorn mushroom', 'earth star fungus',
273
+ 'hen of the woods mushroom', 'bolete', 'corn cob', 'toilet paper',
274
+ ]
275
+
276
+
277
+ # CLIP_PAPER templates (from big_vision prompt_engineering_constants)
278
+ CLIP_PAPER_PROMPT_TEMPLATES = [
279
+ 'a bad photo of a {}.', 'a photo of many {}.', 'a sculpture of a {}.',
280
+ 'a photo of the hard to see {}.', 'a low resolution photo of the {}.',
281
+ 'a rendering of a {}.', 'graffiti of a {}.', 'a bad photo of the {}.',
282
+ 'a cropped photo of the {}.', 'a tattoo of a {}.', 'the embroidered {}.',
283
+ 'a photo of a hard to see {}.', 'a bright photo of a {}.',
284
+ 'a photo of a clean {}.', 'a photo of a dirty {}.', 'a dark photo of the {}.',
285
+ 'a drawing of a {}.', 'a photo of my {}.', 'the plastic {}.',
286
+ 'a photo of the cool {}.', 'a close-up photo of a {}.',
287
+ 'a black and white photo of the {}.', 'a painting of the {}.',
288
+ 'a painting of a {}.', 'a pixelated photo of the {}.', 'a sculpture of the {}.',
289
+ 'a bright photo of the {}.', 'a cropped photo of a {}.', 'a plastic {}.',
290
+ 'a photo of the dirty {}.', 'a jpeg corrupted photo of a {}.',
291
+ 'a blurry photo of the {}.', 'a photo of the {}.', 'a good photo of the {}.',
292
+ 'a rendering of the {}.', 'a {} in a video game.', 'a photo of one {}.',
293
+ 'a doodle of a {}.', 'a close-up photo of the {}.', 'a photo of a {}.',
294
+ 'the origami {}.', 'the {} in a video game.', 'a sketch of a {}.',
295
+ 'a doodle of the {}.', 'a origami {}.', 'a low resolution photo of a {}.',
296
+ 'the toy {}.', 'a rendition of the {}.', 'a photo of the clean {}.',
297
+ 'a photo of a large {}.', 'a rendition of a {}.', 'a photo of a nice {}.',
298
+ 'a photo of a weird {}.', 'a blurry photo of a {}.', 'a cartoon {}.',
299
+ 'art of a {}.', 'a sketch of the {}.', 'a embroidered {}.',
300
+ 'a pixelated photo of a {}.', 'itap of the {}.',
301
+ 'a jpeg corrupted photo of the {}.', 'a good photo of a {}.',
302
+ 'a plushie {}.', 'a photo of the nice {}.', 'a photo of the small {}.',
303
+ 'a photo of the weird {}.', 'the cartoon {}.', 'art of the {}.',
304
+ 'a drawing of the {}.', 'a photo of the large {}.',
305
+ 'a black and white photo of a {}.', 'the plushie {}.',
306
+ 'a dark photo of a {}.', 'itap of a {}.', 'graffiti of the {}.',
307
+ 'a toy {}.', 'itap of my {}.', 'a photo of a cool {}.',
308
+ 'a photo of a small {}.', 'a tattoo of the {}.', '{}',
309
+ ]
310
+
311
+
312
+ def get_canonicalized_class_names(class_names: List[str], first_only: bool = True) -> List[str]:
313
+ """Canonicalize class names, optionally keeping first alias only."""
314
+ out = []
315
+ for name in class_names:
316
+ n = canonicalize_text(name, keep_punctuation_exact_string=",")
317
+ if first_only:
318
+ n = n.split(",")[0].strip() if "," in n else n
319
+ out.append(n)
320
+ return out
321
+
322
+
323
+ def get_canonicalized_templates(templates: List[str]) -> List[str]:
324
+ """Canonicalize prompt templates, preserving '{}' placeholder."""
325
+ return [
326
+ canonicalize_text(t, keep_punctuation_exact_string="{}")
327
+ for t in templates
328
+ ]
329
+
330
+
331
+ def substitute_prompt(template: str, class_name: str) -> str:
332
+ """Substitute '{}' in template with class_name. From discriminative_classifier."""
333
+ placeholder = "{}"
334
+ parts = template.split(placeholder)
335
+ assert len(parts) == 2, f"Template must have exactly one '{placeholder}': {template}"
336
+ return parts[0] + class_name + parts[1]
337
+
338
+
339
+ # ============================================================================
340
+ # Image preprocessing: resize(224) | value_range(-1,1) [big_vision default]
341
+ # ============================================================================
342
+
343
+ def get_bigvision_imagenet_transform(image_size: int = 224):
344
+ """Transform matching big_vision pp_img='resize(224)|value_range(-1,1)'."""
345
+ return transforms.Compose([
346
+ transforms.Resize((image_size, image_size), interpolation=transforms.InterpolationMode.BILINEAR),
347
+ transforms.ToTensor(),
348
+ transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), # [0,1] -> [-1,1]
349
+ ])
350
+
351
+
352
+ # ============================================================================
353
+ # Utility
354
+ # ============================================================================
355
+
356
+ def batched(iterable, n: int):
357
+ it = iter(iterable)
358
+ while True:
359
+ batch = list(islice(it, n))
360
+ if not batch:
361
+ break
362
+ yield batch
363
+
364
+
365
+ def accuracy(output: torch.Tensor, target: torch.Tensor, topk: Tuple[int, ...] = (1,)) -> List[float]:
366
+ pred = output.topk(max(topk), 1, True, True)[1].t()
367
+ correct = pred.eq(target.view(1, -1).expand_as(pred))
368
+ return [float(correct[:k].reshape(-1).float().sum(0, keepdim=True).cpu().numpy()) for k in topk]
369
+
370
+
371
+ def get_input_dtype(precision: str) -> torch.dtype:
372
+ if precision in ('bf16', 'bfloat16'):
373
+ return torch.bfloat16
374
+ if precision in ('fp16', 'float16'):
375
+ return torch.float16
376
+ return torch.float32
377
+
378
+
379
+ def get_autocast_context(precision: str, device_type: str = 'cuda'):
380
+ from contextlib import nullcontext
381
+ if precision in ('bf16', 'bfloat16'):
382
+ return partial(torch.amp.autocast, device_type=device_type, dtype=torch.bfloat16)
383
+ if precision in ('fp16', 'float16'):
384
+ return partial(torch.amp.autocast, device_type=device_type, dtype=torch.float16)
385
+ return nullcontext
386
+
387
+
388
+ # ============================================================================
389
+ # Zero-shot classifier (big_vision aligned: average per class, normalize)
390
+ # ============================================================================
391
+
392
+ @torch.no_grad()
393
+ def build_zero_shot_classifier(
394
+ model,
395
+ processor,
396
+ classnames: Sequence[str],
397
+ templates: Sequence[str],
398
+ num_classes_per_batch: int = 10,
399
+ device: torch.device = None,
400
+ use_tqdm: bool = True,
401
+ canonicalize: bool = True,
402
+ ) -> torch.Tensor:
403
+ """Build classifier weights. Aligned with discriminative_classifier._average_embeddings."""
404
+ if canonicalize:
405
+ classnames = get_canonicalized_class_names(list(classnames))
406
+ templates = get_canonicalized_templates(list(templates))
407
+
408
+ num_templates = len(templates)
409
+ num_classes = len(classnames)
410
+
411
+ if use_tqdm:
412
+ num_iter = (num_classes - 1) // num_classes_per_batch + 1
413
+ iter_wrap = partial(tqdm, total=num_iter, desc="Building classifier", unit_scale=num_classes_per_batch)
414
+ else:
415
+ iter_wrap = iter
416
+
417
+ def _process_batch(batch_classnames: List[str]) -> torch.Tensor:
418
+ num_batch_classes = len(batch_classnames)
419
+ texts = [
420
+ substitute_prompt(tpl, c)
421
+ for c in batch_classnames
422
+ for tpl in templates
423
+ ]
424
+ inputs = processor(text=texts, return_tensors="pt", padding="max_length", max_length=64, truncation=True)
425
+ input_ids = inputs["input_ids"].to(device)
426
+ attention_mask = inputs.get("attention_mask")
427
+ if attention_mask is not None:
428
+ attention_mask = attention_mask.to(device)
429
+
430
+ text_features = model.get_text_features(input_ids=input_ids, attention_mask=attention_mask)
431
+ text_features = F.normalize(text_features, dim=-1)
432
+ text_features = text_features.reshape(num_batch_classes, num_templates, -1).mean(dim=1)
433
+ text_features = F.normalize(text_features, dim=1)
434
+ return text_features.T # (embed_dim, num_batch_classes)
435
+
436
+ batched_embeds = [
437
+ _process_batch(batch)
438
+ for batch in iter_wrap(batched(classnames, num_classes_per_batch))
439
+ ]
440
+ return torch.cat(batched_embeds, dim=1)
441
+
442
+
443
+ # ============================================================================
444
+ # Evaluation (big_vision aligned: raw cosine, no logit_scale/logit_bias)
445
+ # ============================================================================
446
+
447
+ @torch.no_grad()
448
+ def evaluate(
449
+ model,
450
+ classifier: torch.Tensor,
451
+ dataloader: DataLoader,
452
+ device: torch.device,
453
+ precision: str = 'fp32',
454
+ ) -> Tuple[float, float]:
455
+ """Evaluate. Uses raw cosine similarity like discriminative_classifier."""
456
+ autocast_ctx = get_autocast_context(precision, device_type=device.type)
457
+ input_dtype = get_input_dtype(precision)
458
+
459
+ top1, top5, n = 0.0, 0.0, 0
460
+ with torch.inference_mode():
461
+ for images, targets in tqdm(dataloader, desc="Evaluating"):
462
+ images = images.to(device=device, dtype=input_dtype)
463
+ targets = targets.to(device)
464
+
465
+ with autocast_ctx():
466
+ image_features = model.get_image_features(pixel_values=images)
467
+ image_features = F.normalize(image_features, dim=-1)
468
+ # Raw cosine similarity (no logit_scale, no logit_bias)
469
+ logits = image_features @ classifier
470
+
471
+ acc1, acc5 = accuracy(logits, targets, topk=(1, 5))
472
+ top1 += acc1
473
+ top5 += acc5
474
+ n += images.size(0)
475
+
476
+ return top1 / n * 100, top5 / n * 100
477
+
478
+
479
+ # ============================================================================
480
+ # Main
481
+ # ============================================================================
482
+
483
+ def main():
484
+ parser = argparse.ArgumentParser(
485
+ description="Zero-shot ImageNet evaluation (big_vision discriminative_classifier aligned)"
486
+ )
487
+ parser.add_argument("--model_path", type=str, required=True)
488
+ parser.add_argument("--data_path", type=str, required=True)
489
+ parser.add_argument("--batch_size", type=int, default=128)
490
+ parser.add_argument("--num_workers", type=int, default=8)
491
+ parser.add_argument("--device", type=str, default="cuda:0")
492
+ parser.add_argument("--precision", type=str, default="fp32", choices=["fp32", "fp16", "bf16"])
493
+ parser.add_argument("--image_size", type=int, default=224,
494
+ help="Image size for resize (big_vision default 224)")
495
+ parser.add_argument("--no_canonicalize", action="store_true",
496
+ help="Disable canonicalize of class names and templates")
497
+ args = parser.parse_args()
498
+
499
+ device = torch.device(args.device)
500
+
501
+ print("=" * 60)
502
+ print("Zero-Shot ImageNet (big_vision aligned)")
503
+ print("=" * 60)
504
+ print(f"Model: {args.model_path}")
505
+ print(f"Data: {args.data_path}")
506
+ print(f"Image: resize({args.image_size})|value_range(-1,1)")
507
+ print(f"Logit: raw cosine (no scale/bias)")
508
+ print(f"Canonicalize: {not args.no_canonicalize}")
509
+ print()
510
+
511
+ model = SiglipModel.from_pretrained(args.model_path)
512
+ processor = AutoProcessor.from_pretrained(args.model_path)
513
+ model = model.to(device)
514
+ model.eval()
515
+
516
+ transform = get_bigvision_imagenet_transform(args.image_size)
517
+ dataset = ImageFolder(root=args.data_path, transform=transform)
518
+ dataloader = DataLoader(
519
+ dataset,
520
+ batch_size=args.batch_size,
521
+ shuffle=False,
522
+ num_workers=args.num_workers,
523
+ pin_memory=True,
524
+ )
525
+
526
+ print("Building zero-shot classifier...")
527
+ autocast_ctx = get_autocast_context(args.precision, device_type=device.type)
528
+ with autocast_ctx():
529
+ classifier = build_zero_shot_classifier(
530
+ model,
531
+ processor,
532
+ classnames=CLIP_IMAGENET_CLASS_NAMES,
533
+ templates=CLIP_PAPER_PROMPT_TEMPLATES,
534
+ num_classes_per_batch=10,
535
+ device=device,
536
+ use_tqdm=True,
537
+ canonicalize=not args.no_canonicalize,
538
+ )
539
+
540
+ print("Evaluating...")
541
+ top1, top5 = evaluate(model, classifier, dataloader, device=device, precision=args.precision)
542
+
543
+ print()
544
+ print("=" * 60)
545
+ print("Results:")
546
+ print(f" Top-1: {top1:.2f}%")
547
+ print(f" Top-5: {top5:.2f}%")
548
+ print("=" * 60)
549
+
550
+ out_dir = Path(args.model_path)
551
+ out_dir.mkdir(parents=True, exist_ok=True)
552
+ out_json = out_dir / "zeroshot_imagenet_bigvision_results.json"
553
+ with open(out_json, "w", encoding="utf-8") as f:
554
+ json.dump(
555
+ {"top1": top1, "top5": top5, "aligned": "big_vision", "data_path": args.data_path},
556
+ f, indent=2, ensure_ascii=False,
557
+ )
558
+ print(f"Results written to {out_json}")
559
+
560
+
561
+ if __name__ == "__main__":
562
+ main()
patch-reparameterization/src/eval/fid.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ from pathlib import Path
3
+ import numpy as np
4
+
5
+ from torch_fidelity import calculate_metrics
6
+ from .utils import ImgArrDataset
7
+
8
+ from torch_fidelity.feature_extractor_inceptionv3 import FeatureExtractorInceptionV3 # original torch-fidelity :contentReference[oaicite:2]{index=2}
9
+ import scipy.linalg
10
+ import torch
11
+
12
+ def _fid_from_moments(mu1, sigma1, mu2, sigma2) -> float:
13
+ # FID formula :contentReference[oaicite:3]{index=3}
14
+
15
+ mu1 = np.asarray(mu1, dtype=np.float64)
16
+ mu2 = np.asarray(mu2, dtype=np.float64)
17
+ sigma1 = np.asarray(sigma1, dtype=np.float64)
18
+ sigma2 = np.asarray(sigma2, dtype=np.float64)
19
+
20
+ diff = mu1 - mu2
21
+ covmean = scipy.linalg.sqrtm(sigma1 @ sigma2)
22
+
23
+ if np.iscomplexobj(covmean): # numerical noise
24
+ covmean = covmean.real
25
+
26
+ fid = diff.dot(diff) + np.trace(sigma1 + sigma2 - 2.0 * covmean)
27
+ return float(max(fid, 0.0))
28
+
29
+
30
+ @torch.no_grad()
31
+ def _compute_inception_moments_from_arr(arr: np.ndarray, batch_size: int, device: str) -> tuple[np.ndarray, np.ndarray]:
32
+ """
33
+ Uses torch-fidelity's InceptionV3 feature extractor to get 2048-d pool features.
34
+ Assumes arr is [N,H,W,C] or [N,C,H,W], uint8 (0..255) or float (0..1 or 0..255).
35
+ """
36
+ # Convert to torch in NCHW uint8 as the safest default.
37
+ x = arr
38
+ if x.ndim != 4:
39
+ raise ValueError(f"Expected 4D array, got shape {x.shape}")
40
+
41
+ if x.shape[-1] == 3: # NHWC -> NCHW
42
+ x = np.transpose(x, (0, 3, 1, 2))
43
+
44
+ if x.dtype != np.uint8:
45
+ # If float in [0,1], scale up; otherwise assume already 0..255-ish
46
+ x_f = x.astype(np.float32)
47
+ if x_f.max() <= 1.5:
48
+ x_f = x_f * 255.0
49
+ x = np.clip(x_f, 0, 255).astype(np.uint8)
50
+
51
+ xt = torch.from_numpy(x).to(device=device, dtype=torch.uint8)
52
+
53
+ fe = FeatureExtractorInceptionV3(name="inception-v3-compat", features_list=['2048']).to(device).eval() # preregistered extractor name :contentReference[oaicite:4]{index=4}
54
+
55
+ feats = []
56
+ for i in range(0, xt.shape[0], batch_size):
57
+ batch = xt[i : i + batch_size]
58
+ f = fe(batch)[0] # (B, 2048)
59
+ feats.append(f.detach().cpu())
60
+
61
+ feats = torch.cat(feats, dim=0).double().numpy() # (N, 2048) float64
62
+ mu = feats.mean(axis=0)
63
+ sigma = np.cov(feats, rowvar=False)
64
+ return mu, sigma
65
+
66
+
67
+ @torch.no_grad()
68
+ def compute_inception_features(arr: np.ndarray, batch_size: int, device: str) -> np.ndarray:
69
+ """Returns raw 2048-d InceptionV3 features for distributed FID computation."""
70
+ x = arr
71
+ if x.ndim != 4:
72
+ raise ValueError(f"Expected 4D array, got shape {x.shape}")
73
+ if x.shape[-1] == 3: # NHWC -> NCHW
74
+ x = np.transpose(x, (0, 3, 1, 2))
75
+ if x.dtype != np.uint8:
76
+ x_f = x.astype(np.float32)
77
+ if x_f.max() <= 1.5:
78
+ x_f = x_f * 255.0
79
+ x = np.clip(x_f, 0, 255).astype(np.uint8)
80
+ xt = torch.from_numpy(x).to(device=device, dtype=torch.uint8)
81
+ fe = FeatureExtractorInceptionV3(name="inception-v3-compat", features_list=['2048']).to(device).eval()
82
+ feats = []
83
+ for i in range(0, xt.shape[0], batch_size):
84
+ batch = xt[i: i + batch_size]
85
+ f = fe(batch)[0]
86
+ feats.append(f.detach().cpu())
87
+ return torch.cat(feats, dim=0).double().numpy() # (N, 2048)
88
+
89
+
90
+ def calculate_rfid_from_features(features1: np.ndarray, features2: np.ndarray) -> float:
91
+ """Compute FID from pre-computed inception features (for distributed use)."""
92
+ mu1 = features1.mean(axis=0)
93
+ sigma1 = np.cov(features1, rowvar=False)
94
+ mu2 = features2.mean(axis=0)
95
+ sigma2 = np.cov(features2, rowvar=False)
96
+ return _fid_from_moments(mu1, sigma1, mu2, sigma2)
97
+
98
+
99
+ def calculate_gfid(
100
+ arr1: np.ndarray,
101
+ ref_arr: dict,
102
+ batch_size: int = 64,
103
+ device: str = "cuda",
104
+ ) -> float:
105
+ mu_ref, sigma_ref = ref_arr['mu'], ref_arr['sigma']
106
+ mu_gen, sigma_gen = _compute_inception_moments_from_arr(arr1, batch_size=batch_size, device=device)
107
+ return _fid_from_moments(mu_gen, sigma_gen, mu_ref, sigma_ref)
108
+
109
+ def calculate_rfid(
110
+ arr1,
111
+ arr2=None,
112
+ bs=64,
113
+ device="cuda",
114
+ fid_statistics_file=None,
115
+ ):
116
+ arr1_ds = ImgArrDataset(arr1)
117
+
118
+ if fid_statistics_file is not None:
119
+ metrics_kwargs = dict(
120
+ input1=arr1_ds,
121
+ input2=None,
122
+ fid_statistics_file=fid_statistics_file,
123
+ batch_size=bs,
124
+ fid=True,
125
+ cuda=(device == "cuda"),
126
+ )
127
+ else:
128
+ if arr2 is None:
129
+ raise ValueError("Either arr2 or fid_statistics_file must be provided.")
130
+ arr2_ds = ImgArrDataset(arr2)
131
+ metrics_kwargs = dict(
132
+ input1=arr1_ds,
133
+ input2=arr2_ds,
134
+ batch_size=bs,
135
+ fid=True,
136
+ cuda=(device == "cuda"),
137
+ )
138
+
139
+ metrics = calculate_metrics(**metrics_kwargs)
140
+ return metrics["frechet_inception_distance"]
141
+
142
+
143
+ if __name__ == "__main__":
144
+ import argparse
145
+ from pathlib import Path
146
+ import numpy as np
147
+ import torch
148
+
149
+ parser = argparse.ArgumentParser(description="Compute FID using original torch-fidelity")
150
+ parser.add_argument("--arr1", type=str, required=True,
151
+ help="Path to generated images array (.npy or .npz). Array should be N x H x W x 3 or N x 3 x H x W.")
152
+ parser.add_argument("--arr2", type=str, default=None,
153
+ help="Optional path to reference images array (.npy or .npz). If set, uses torch_fidelity.calculate_metrics.")
154
+
155
+ # reference stats path (npz with mu/sigma keys)
156
+ parser.add_argument("--ref", "--fid-statistics-file", dest="ref", type=str, default=None,
157
+ help="Path to reference stats (.npz with mu/sigma, mu_s/sigma_s, mu_clip/sigma_clip, etc.). "
158
+ "If set, computes moments for arr1 using torch-fidelity InceptionV3 and FID vs these stats.")
159
+
160
+ # which keys in the ref npz to use
161
+ parser.add_argument("--ref-mu-key", type=str, default="mu",
162
+ help="Key in --ref .npz for reference mean (default: mu).")
163
+ parser.add_argument("--ref-sigma-key", type=str, default="sigma",
164
+ help="Key in --ref .npz for reference covariance (default: sigma).")
165
+
166
+ parser.add_argument("--batch-size", type=int, default=64)
167
+ parser.add_argument("--device", type=str, default="cuda", choices=["cuda", "cpu"])
168
+ parser.add_argument("--no-scipy", action="store_true",
169
+ help="Do not use scipy.linalg.sqrtm; use torch eig fallback (slower/less identical).")
170
+
171
+ args = parser.parse_args()
172
+
173
+ # exactly one of arr2 or ref must be provided
174
+ if (args.arr2 is None) == (args.ref is None):
175
+ parser.error("Specify exactly one of --arr2 or --ref/--fid-statistics-file")
176
+
177
+ def load_array(path: str) -> np.ndarray:
178
+ p = Path(path)
179
+ if not p.exists():
180
+ raise FileNotFoundError(f"File not found: {p}")
181
+ if p.suffix == ".npy":
182
+ return np.load(p)
183
+ elif p.suffix == ".npz":
184
+ z = np.load(p)
185
+ # common conventions: 'arr_0' or 'images'
186
+ if "arr_0" in z.files:
187
+ return z["arr_0"]
188
+ if "images" in z.files:
189
+ return z["images"]
190
+ raise KeyError(f"{p} is .npz but has no 'arr_0' or 'images'. Keys={z.files}")
191
+ else:
192
+ raise ValueError(f"Unsupported array file type: {p.suffix} (expected .npy or .npz)")
193
+
194
+ arr1 = load_array(args.arr1)
195
+ print("[INFO] arr1:", arr1.shape, arr1.dtype)
196
+
197
+ if args.arr2 is not None:
198
+ arr2 = load_array(args.arr2)
199
+ print("[INFO] arr2:", arr2.shape, arr2.dtype)
200
+
201
+ fid = calculate_rfid(
202
+ arr1=arr1,
203
+ arr2=arr2,
204
+ bs=args.batch_size,
205
+ device=args.device,
206
+ fid_statistics_file=None, # unused in upstream path
207
+ )
208
+ print(f"[RESULT] FID: {fid:.6f}")
209
+ raise SystemExit(0)
210
+
211
+ # stats mode: arr1 vs ref npz moments
212
+ ref_path = Path(args.ref)
213
+ if not ref_path.exists():
214
+ raise FileNotFoundError(f"Ref stats not found: {ref_path}")
215
+
216
+ if ref_path.suffix != ".npz":
217
+ raise ValueError(
218
+ f"--ref must be a .npz containing mu/sigma. Got: {ref_path.suffix}. "
219
+ "Original torch-fidelity cannot use .pt stats without input2."
220
+ )
221
+
222
+ stats = np.load(ref_path)
223
+ if args.ref_mu_key not in stats.files or args.ref_sigma_key not in stats.files:
224
+ raise KeyError(
225
+ f"Missing '{args.ref_mu_key}'/'{args.ref_sigma_key}' in {ref_path}. "
226
+ f"Available keys: {list(stats.files)}"
227
+ )
228
+ print(f"[INFO] ref stats: {ref_path}")
229
+ # If you want to respect --no-scipy, thread it through:
230
+ # (edit your fid_arr_vs_npzstats / _fid_from_moments accordingly)
231
+ fid = calculate_gfid(
232
+ arr1=arr1,
233
+ ref_arr=stats,
234
+ batch_size=args.batch_size,
235
+ device=args.device,
236
+ # optionally add: use_scipy=(not args.no_scipy)
237
+ )
238
+ print(f"[RESULT] FID: {fid:.6f}")
patch-reparameterization/src/eval/ref_iqa.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import namedtuple
2
+ import numpy as np
3
+ import torch
4
+ import math
5
+ from tqdm import trange
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+ from torchvision import models
9
+ from torchmetrics.functional.image.ssim import structural_similarity_index_measure
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Optional
14
+ from .utils import LPIPS, to_torch_tensor
15
+
16
+ @torch.no_grad()
17
+ def calculate_psnr(arr1, arr2, bs, device="cuda", disable_bar: bool = True, psnr_resize: Optional[int] = None) -> float:
18
+ """
19
+ Computes PSNR between two single images or batches of images.
20
+ PSNR is averaged over the batch if given as (B, C, H, W).
21
+
22
+ Args:
23
+ psnr_resize: If set, both images are resized to (psnr_resize x psnr_resize)
24
+ before computing PSNR. Useful for evaluating at lower resolutions.
25
+ """
26
+ B = arr1.shape[0]
27
+ n_batches = math.ceil(B / bs)
28
+ psnr = torch.zeros(B, device=device)
29
+
30
+ for i in trange(n_batches, desc="Calculating PSNR", file=sys.stdout, disable=disable_bar):
31
+ start_idx = i * bs
32
+ end_idx = min((i + 1) * bs, B)
33
+ # PSNR expects input in [0, 1], (B, C, H, W)
34
+ batch_arr1 = to_torch_tensor(arr1[start_idx:end_idx]).to(device)
35
+ batch_arr2 = to_torch_tensor(arr2[start_idx:end_idx]).to(device)
36
+ if psnr_resize is not None:
37
+ batch_arr1 = F.interpolate(batch_arr1, size=(psnr_resize, psnr_resize), mode="bilinear", align_corners=False)
38
+ batch_arr2 = F.interpolate(batch_arr2, size=(psnr_resize, psnr_resize), mode="bilinear", align_corners=False)
39
+ batch_mse = torch.mean((batch_arr1 - batch_arr2) ** 2, dim=[1,2,3]) # shape [bs]
40
+ batch_mse = torch.clamp(batch_mse, min=1e-10)
41
+ batch_psnr = 20.0 * torch.log10(1 / torch.sqrt(batch_mse)) # shape [bs]
42
+ psnr[start_idx:end_idx] = batch_psnr
43
+ return psnr.mean().item()
44
+ @torch.no_grad()
45
+ def calculate_lpips(arr1, arr2, bs, device="cuda", disable_bar: bool = True) -> float:
46
+ """
47
+ Computes LPIPS between two single images or batches of images.
48
+ LPIPS is averaged over the batch if given as (B, C, H, W).
49
+ """
50
+ B = arr1.shape[0]
51
+ n_batches = math.ceil(B / bs)
52
+ loss_fn = LPIPS().eval().to(device)
53
+ lpips = torch.zeros(B, device=device)
54
+
55
+ for i in trange(n_batches, desc="Calculating LPIPS", file=sys.stdout, disable=disable_bar):
56
+ start_idx = i * bs
57
+ end_idx = min((i + 1) * bs, B)
58
+ # LPIPS expects input in [-1, 1], (B, C, H, W)
59
+ batch_arr1 = (to_torch_tensor(arr1[start_idx:end_idx]).to(device) - 0.5) * 2.
60
+ batch_arr2 = (to_torch_tensor(arr2[start_idx:end_idx]).to(device) - 0.5) * 2.
61
+ batch_lpips = loss_fn(batch_arr1, batch_arr2).squeeze() # shape [bs]
62
+ lpips[start_idx:end_idx] = batch_lpips
63
+ return lpips.mean().item()
64
+
65
+
66
+ ######################################################
67
+ # 4. SSIM Calculation
68
+ ######################################################
69
+ @torch.no_grad()
70
+ def calculate_ssim(arr1, arr2, bs, device="cuda", disable_bar: bool = True) -> float:
71
+ """
72
+ Computes SSIM between two single images or batches of images.
73
+ SSIM is averaged over the batch if given as (B, C, H, W).
74
+ """
75
+ B = arr1.shape[0]
76
+ n_batches = math.ceil(B / bs)
77
+ ssim_val = torch.zeros(B, device=device)
78
+
79
+ for i in trange(n_batches, desc="Calculating SSIM", file=sys.stdout, disable=disable_bar):
80
+ start_idx = i * bs
81
+ end_idx = min((i + 1) * bs, B)
82
+ # SSIM expects input in [0, 1], (B, C, H, W)
83
+ batch_arr1 = to_torch_tensor(arr1[start_idx:end_idx]).to(device)
84
+ batch_arr2 = to_torch_tensor(arr2[start_idx:end_idx]).to(device)
85
+ # SSIM expects input in [0, 1], (B, C, H, W)
86
+ batch_ssim = structural_similarity_index_measure(
87
+ target=batch_arr1,
88
+ preds=batch_arr2,
89
+ data_range=1.0,
90
+ reduction="none"
91
+ )
92
+ ssim_val[start_idx:end_idx] = batch_ssim
93
+ return ssim_val.mean().item()
patch-reparameterization/src/eval/utils.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import namedtuple
2
+ import numpy as np
3
+ import torch
4
+ import math
5
+ import torch.nn as nn
6
+ from torchvision import models
7
+ from torch.utils.data import Dataset
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ # insert ../ as a module path
13
+ current_dir = Path(__file__).resolve().parent
14
+ parent_dir = current_dir.parent
15
+ if str(parent_dir) not in sys.path:
16
+ sys.path.insert(0, str(parent_dir))
17
+ from disc.lpips_utils import get_ckpt_path
18
+
19
+ def normalize_tensor(x, eps=1e-10):
20
+ norm_factor = torch.sqrt(torch.sum(x**2, dim=1, keepdim=True))
21
+ return x / (norm_factor + eps)
22
+
23
+
24
+ def spatial_average(x, keepdim=True):
25
+ return x.mean([2, 3],keepdim=keepdim)
26
+
27
+ class ScalingLayer(nn.Module):
28
+ def __init__(self):
29
+ super(ScalingLayer, self).__init__()
30
+ self.register_buffer('shift', torch.Tensor([-.030, -.088, -.188])[None, :, None, None])
31
+ self.register_buffer('scale', torch.Tensor([.458, .448, .450])[None, :, None, None])
32
+
33
+ def forward(self, inp):
34
+ return (inp - self.shift) / self.scale
35
+
36
+ class NetLinLayer(nn.Module):
37
+ """ A single linear layer which does a 1x1 conv """
38
+ def __init__(self, chn_in, chn_out=1, use_dropout=False):
39
+ super(NetLinLayer, self).__init__()
40
+ layers = [nn.Dropout(), ] if (use_dropout) else []
41
+ layers += [nn.Conv2d(chn_in, chn_out, 1, stride=1, padding=0, bias=False), ]
42
+ self.model = nn.Sequential(*layers)
43
+
44
+ class VGG16(torch.nn.Module):
45
+ def __init__(self, requires_grad=False):
46
+ super(VGG16, self).__init__()
47
+ vgg_pretrained_features = models.vgg16(weights='DEFAULT').features
48
+ self.slice1 = torch.nn.Sequential()
49
+ self.slice2 = torch.nn.Sequential()
50
+ self.slice3 = torch.nn.Sequential()
51
+ self.slice4 = torch.nn.Sequential()
52
+ self.slice5 = torch.nn.Sequential()
53
+ self.N_slices = 5
54
+ for x in range(4):
55
+ self.slice1.add_module(str(x), vgg_pretrained_features[x])
56
+ for x in range(4, 9):
57
+ self.slice2.add_module(str(x), vgg_pretrained_features[x])
58
+ for x in range(9, 16):
59
+ self.slice3.add_module(str(x), vgg_pretrained_features[x])
60
+ for x in range(16, 23):
61
+ self.slice4.add_module(str(x), vgg_pretrained_features[x])
62
+ for x in range(23, 30):
63
+ self.slice5.add_module(str(x), vgg_pretrained_features[x])
64
+ if not requires_grad:
65
+ for param in self.parameters():
66
+ param.requires_grad = False
67
+
68
+ def forward(self, X):
69
+ h = self.slice1(X)
70
+ h_relu1_2 = h
71
+ h = self.slice2(h)
72
+ h_relu2_2 = h
73
+ h = self.slice3(h)
74
+ h_relu3_3 = h
75
+ h = self.slice4(h)
76
+ h_relu4_3 = h
77
+ h = self.slice5(h)
78
+ h_relu5_3 = h
79
+ vgg_outputs = namedtuple("VggOutputs", ['relu1_2', 'relu2_2', 'relu3_3', 'relu4_3', 'relu5_3'])
80
+ out = vgg_outputs(h_relu1_2, h_relu2_2, h_relu3_3, h_relu4_3, h_relu5_3)
81
+ return out
82
+
83
+ class LPIPS(nn.Module):
84
+ # Learned perceptual metric
85
+ def __init__(self, use_dropout=True):
86
+ super().__init__()
87
+ self.scaling_layer = ScalingLayer()
88
+ self.chns = [64, 128, 256, 512, 512] # vg16 features
89
+ self.net = VGG16(requires_grad=False)
90
+ self.lin0 = NetLinLayer(self.chns[0], use_dropout=use_dropout)
91
+ self.lin1 = NetLinLayer(self.chns[1], use_dropout=use_dropout)
92
+ self.lin2 = NetLinLayer(self.chns[2], use_dropout=use_dropout)
93
+ self.lin3 = NetLinLayer(self.chns[3], use_dropout=use_dropout)
94
+ self.lin4 = NetLinLayer(self.chns[4], use_dropout=use_dropout)
95
+ self.load_from_pretrained()
96
+ for param in self.parameters():
97
+ param.requires_grad = False
98
+
99
+ def load_from_pretrained(self, name="vgg_lpips"):
100
+ ckpt = get_ckpt_path(name)
101
+ self.load_state_dict(
102
+ torch.load(
103
+ ckpt,
104
+ map_location=torch.device("cpu")),
105
+ strict=False
106
+ )
107
+ print("loaded pretrained LPIPS loss from {}".format(ckpt))
108
+
109
+ def forward(self, input, target):
110
+ in0_input, in1_input = (self.scaling_layer(input), self.scaling_layer(target))
111
+ outs0, outs1 = self.net(in0_input), self.net(in1_input)
112
+ feats0, feats1, diffs = {}, {}, {}
113
+ lins = [self.lin0, self.lin1, self.lin2, self.lin3, self.lin4]
114
+ for kk in range(len(self.chns)):
115
+ feats0[kk], feats1[kk] = normalize_tensor(outs0[kk]), normalize_tensor(outs1[kk])
116
+ diffs[kk] = (feats0[kk] - feats1[kk]) ** 2
117
+
118
+ res = [spatial_average(lins[kk].model(diffs[kk]), keepdim=True) for kk in range(len(self.chns))]
119
+ val = res[0]
120
+ for l in range(1, len(self.chns)):
121
+ val += res[l]
122
+ return val
123
+
124
+ class ImgArrDataset(Dataset):
125
+ """
126
+ Serve as the wrapper purpose for `torch-fidelity` to calculate the metrics (FID),
127
+ which expects [B, C, H, W] tensor in uint8. We are returning a [C, H, W] tensor.
128
+ """
129
+ def __init__(self, arr):
130
+ self.arr = arr
131
+
132
+ def __len__(self):
133
+ return len(self.arr)
134
+
135
+ def __getitem__(self, idx):
136
+ return torch.from_numpy(self.arr[idx]).permute(2, 0, 1)
137
+
138
+
139
+ ######################################################
140
+ # 1. Convert NumPy arrays to Torch tensors, if needed
141
+ ######################################################
142
+ def to_torch_tensor(np_array):
143
+ """
144
+ Converts a NumPy array of shape (B, H, W, C) to a (B, C, H, W) float32 Torch tensor.
145
+ Expects image to be in [0, 1] or [0, 255].
146
+ Adjust if necessary.
147
+ """
148
+ tensor = torch.from_numpy(np_array).permute(0, 3, 1, 2)
149
+
150
+ # If your data is in [0, 255], convert to [0, 1].
151
+ if tensor.max() > 1.0:
152
+ tensor = tensor.float() / 255.0
153
+ else:
154
+ tensor = tensor.float()
155
+ return tensor
patch-reparameterization/src/eval_only.py ADDED
@@ -0,0 +1,927 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) Meta Platforms.
3
+ # Licensed under the MIT license.
4
+ """
5
+ Standalone evaluation script for PatchReparam models (Stage-1 / Stage-2 checkpoints).
6
+
7
+ Loads a checkpoint, runs reconstruction metrics (PSNR, SSIM, rFID) and optionally
8
+ zero-shot ImageNet classification. Supports both single-GPU and multi-GPU via
9
+ accelerate launch.
10
+
11
+ Usage:
12
+ # Single GPU (with checkpoint)
13
+ python eval_only.py --config configs/stage2/xxx.yaml --ckpt path/to/ep-last.pt
14
+
15
+ # Pretrained only (no checkpoint, model created from config)
16
+ python eval_only.py --config configs/stage1/pretrained/xxx.yaml --output-dir ./eval_pretrained
17
+
18
+ # Multi-GPU
19
+ accelerate launch --num_processes 4 eval_only.py --config configs/stage2/xxx.yaml --ckpt path/to/ep-last.pt
20
+
21
+ # Override eval data from config
22
+ python eval_only.py --config xxx.yaml --ckpt xxx.pt --eval-data /path/to/val/ --reference-npz /path/to/val.npz
23
+
24
+ # Save reconstructed images (PNG) to eval_results/recon_images/
25
+ python eval_only.py --config xxx.yaml --ckpt xxx.pt --save-recon-images
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import json
32
+ import logging
33
+ import math
34
+ import os
35
+ import sys
36
+ from pathlib import Path
37
+
38
+ import numpy as np
39
+ import torch
40
+ from accelerate import Accelerator
41
+ from omegaconf import OmegaConf
42
+ from utils.config_utils import load_config # noqa: F401 (registers oc.env resolver)
43
+ from torch.cuda.amp import autocast
44
+ from torch.utils.data import DataLoader, Subset
45
+ from torchvision import transforms
46
+ from torchvision.datasets import ImageFolder
47
+ from tqdm import tqdm
48
+
49
+ from eval import evaluate_reconstruction_distributed, compute_reconstruction_metrics
50
+ from eval.clip_zero_shot import (
51
+ IMAGENET_CLASSNAMES,
52
+ CLIP_PAPER_PROMPT_TEMPLATES,
53
+ PatchReparamZeroShotWrapper,
54
+ build_zero_shot_classifier,
55
+ create_imagenet_dataloader_patch_reparam,
56
+ evaluate as evaluate_zero_shot,
57
+ )
58
+ from stage1 import PatchReparam
59
+ from utils.train_utils import center_crop_arr, parse_configs
60
+
61
+
62
+ class ExternalVAEWrapper(torch.nn.Module):
63
+ """Wraps diffusers AutoencoderKL to match the PatchReparam forward interface.
64
+
65
+ Input: images in [0, 1], shape (B, 3, H, W)
66
+ Output: reconstructed images in [0, 1], shape (B, 3, H, W)
67
+ """
68
+
69
+ def __init__(self, vae):
70
+ super().__init__()
71
+ self.vae = vae
72
+ self.scaling_factor = vae.config.scaling_factor
73
+
74
+ @torch.no_grad()
75
+ def forward(self, images, **kwargs):
76
+ # Disable autocast: Flux VAE requires fp32 precision for accurate reconstruction
77
+ with torch.amp.autocast("cuda", enabled=False):
78
+ images = images.float()
79
+ # AutoencoderKL expects [-1, 1]
80
+ z = self.vae.encode(images * 2 - 1).latent_dist.sample() * self.scaling_factor
81
+ decoded = self.vae.decode(z / self.scaling_factor).sample
82
+ return (decoded + 1) / 2 # back to [0, 1]
83
+
84
+ class Qwen3VarlenWrapper(torch.nn.Module):
85
+ """Wraps Qwen3Unified PatchReparam to match the standard model(images) eval interface.
86
+
87
+ Input: images in [0, 1], shape (B, 3, 256, 256)
88
+ Output: reconstructed images in [0, 1], shape (B, 3, 256, 256)
89
+ """
90
+
91
+ def __init__(self, pr):
92
+ super().__init__()
93
+ self.pr = pr
94
+
95
+ def forward(self, images, **kwargs):
96
+ # images: [B, 3, H, W] in [0, 1]
97
+ B = images.shape[0]
98
+ H, W = images.shape[2], images.shape[3]
99
+ p = self.pr.decoder.config.patch_size
100
+ tp = int(self.pr.encoder.temporal_patch_size)
101
+ H_grid, W_grid = H // p, W // p
102
+
103
+ # 从模型取 normalize 参数,不 hardcode
104
+ enc_mean = self.pr.encoder_mean.to(images.device) # [1, 3, 1, 1]
105
+ enc_std = self.pr.encoder_std.to(images.device) # [1, 3, 1, 1]
106
+ frames = images.unsqueeze(1).expand(-1, tp, -1, -1, -1).contiguous() # [B, tp, 3, H, W]
107
+ frames = (frames - enc_mean.unsqueeze(1)) / enc_std.unsqueeze(1)
108
+ frames = frames.reshape(B, tp, 3, H_grid, p, W_grid, p)
109
+ pixel_format = getattr(self.pr.encoder, 'pixel_format', 't_major')
110
+ if pixel_format == 'c_major':
111
+ # C-major: [B, H_g, W_g, C, tp, p, p] → [B*N, C*tp*p*p]
112
+ frames = frames.permute(0, 3, 5, 2, 1, 4, 6).contiguous().reshape(B * H_grid * W_grid, 3 * tp * p * p)
113
+ else:
114
+ # T-major(旧行为,兼容已有 checkpoint)
115
+ frames = frames.permute(0, 3, 5, 1, 2, 4, 6).contiguous().reshape(B * H_grid * W_grid * tp, 3, p, p)
116
+ grid_thw = torch.tensor([[1, H_grid, W_grid]], dtype=torch.long, device=images.device).expand(B, -1)
117
+
118
+ x_recs = self.pr(varlen=True, pixel_values=frames, grid_thw=grid_thw)
119
+ # fast path (all same res) returns tensor [B,3,H,W]; slow path returns list
120
+ if isinstance(x_recs, list):
121
+ x_recs = torch.stack(x_recs)
122
+ return x_recs.clamp(0, 1)
123
+
124
+
125
+ try:
126
+ from peft import LoraConfig, get_peft_model, TaskType
127
+ PEFT_AVAILABLE = True
128
+ except ImportError:
129
+ PEFT_AVAILABLE = False
130
+ class LoraConfig:
131
+ pass
132
+ class TaskType:
133
+ FEATURE_EXTRACTION = None
134
+
135
+
136
+ def safe_load_checkpoint(path: str, map_location: str = "cpu"):
137
+ try:
138
+ return torch.load(path, map_location=map_location, weights_only=False)
139
+ except TypeError:
140
+ return torch.load(path, map_location=map_location)
141
+
142
+
143
+ def load_pr_from_checkpoint(
144
+ pr_config,
145
+ ckpt_path: str,
146
+ device: torch.device,
147
+ use_ema: bool = True,
148
+ ) -> PatchReparam:
149
+ """Build PatchReparam from config and load weights from checkpoint."""
150
+ from utils.model_utils import get_obj_from_str
151
+
152
+ cfg = OmegaConf.to_container(pr_config, resolve=True) if hasattr(pr_config, "_metadata") else dict(pr_config)
153
+ target = cfg.get("target")
154
+ params = cfg.get("params", {})
155
+ if not target:
156
+ raise KeyError("Config must have 'target' for PatchReparam instantiation.")
157
+
158
+ pr = get_obj_from_str(target)(**params)
159
+ pr = pr.to(device).eval()
160
+ pr.requires_grad_(False)
161
+
162
+ ckpt = safe_load_checkpoint(ckpt_path, map_location="cpu")
163
+ use_lora = ckpt.get("use_lora", False)
164
+ lora_config = ckpt.get("lora_config", None)
165
+
166
+ if use_lora and lora_config and PEFT_AVAILABLE:
167
+ if isinstance(lora_config, dict) and "default" in lora_config:
168
+ lora_config = lora_config["default"]
169
+ if isinstance(lora_config, dict):
170
+ lora_config = LoraConfig(
171
+ task_type=TaskType.FEATURE_EXTRACTION,
172
+ r=int(lora_config.get("r", 8)),
173
+ lora_alpha=int(lora_config.get("lora_alpha", 16)),
174
+ target_modules=lora_config.get("target_modules", ["q_proj", "v_proj", "k_proj", "out_proj"]),
175
+ lora_dropout=float(lora_config.get("lora_dropout", 0.1)),
176
+ )
177
+ pr.encoder = get_peft_model(pr.encoder, lora_config)
178
+ logging.info("Applied LoRA adapter from checkpoint.")
179
+
180
+ state_dict = ckpt.get("ema" if use_ema else "model")
181
+ if state_dict is None:
182
+ raise KeyError(f"Checkpoint must contain 'ema' or 'model'. Got keys: {list(ckpt.keys())}")
183
+
184
+ # Handle DDP/compile prefixes
185
+ clean_state = {}
186
+ for k, v in state_dict.items():
187
+ clean_k = k
188
+ for prefix in ["module._orig_mod.", "_orig_mod.", "module."]:
189
+ if clean_k.startswith(prefix):
190
+ clean_k = clean_k[len(prefix):]
191
+ break
192
+ clean_state[clean_k] = v
193
+
194
+ missing, unexpected = pr.load_state_dict(clean_state, strict=False)
195
+ if missing:
196
+ logging.warning(f"Missing keys ({len(missing)}): {missing[:5]}...")
197
+ if unexpected:
198
+ logging.warning(f"Unexpected keys ({len(unexpected)}): {unexpected[:5]}...")
199
+ logging.info(f"Loaded {'EMA' if use_ema else 'model'} weights from {ckpt_path}")
200
+ return pr
201
+
202
+
203
+ def create_pr_from_config(pr_config, device: torch.device) -> PatchReparam:
204
+ """Build PatchReparam from config only (pretrained weights, no checkpoint loading)."""
205
+ from utils.model_utils import get_obj_from_str
206
+
207
+ cfg = OmegaConf.to_container(pr_config, resolve=True) if hasattr(pr_config, "_metadata") else dict(pr_config)
208
+ target = cfg.get("target")
209
+ params = cfg.get("params", {})
210
+ if not target:
211
+ raise KeyError("Config must have 'target' for PatchReparam instantiation.")
212
+
213
+ pr = get_obj_from_str(target)(**params)
214
+ pr = pr.to(device).eval()
215
+ pr.requires_grad_(False)
216
+ logging.info("Created PatchReparam from config (pretrained, no checkpoint).")
217
+ return pr
218
+
219
+
220
+ def parse_args():
221
+ parser = argparse.ArgumentParser(description="Evaluate PatchReparam checkpoint (reconstruction + optional zero-shot).")
222
+ parser.add_argument("--config", type=str, required=True, help="YAML config with stage_1 section.")
223
+ parser.add_argument("--ckpt", type=str, default=None, help="Path to checkpoint (.pt). If omitted, model is created from config only (pretrained).")
224
+ parser.add_argument("--eval-data", type=str, default=None, help="Eval dataset path (ImageFolder). Overrides config.")
225
+ parser.add_argument("--reference-npz", type=str, default=None, help="Reference NPZ for metrics. Overrides config.")
226
+ parser.add_argument("--output-dir", type=str, default=None, help="Experiment dir (parent of checkpoints). Default: ckpt's parent's parent.")
227
+ parser.add_argument("--image-size", type=int, default=256, help="Image size for center crop.")
228
+ parser.add_argument("--batch-size", type=int, default=64, help="Batch size per GPU for reconstruction.")
229
+ parser.add_argument("--metrics", type=str, nargs="+", default=None, help="Metrics: psnr, ssim, rfid. Default from config.")
230
+ parser.add_argument("--eval-weights", type=str, choices=["both", "ema_only", "model_only"], default="ema_only",
231
+ help="Which weights to eval: both (default), ema_only, model_only. Overrides config eval.eval_weights.")
232
+ parser.add_argument("--no-zeroshot", action="store_true", help="Disable zero-shot eval even if enabled in config.")
233
+ parser.add_argument("--save-recon-images", action="store_true", help="Save reconstructed images as PNG files to eval_results/recon_images/.")
234
+ parser.add_argument("--image-features-mode", type=str, default=None,
235
+ help="For SigLIP2 encoder: which tokens to use for reconstruction. Overrides config eval.eval_modes when set.")
236
+ parser.add_argument("--precision", choices=["fp32", "fp16", "bf16"], default="bf16")
237
+ parser.add_argument("--encoder-cos-sim-model", type=str, default=None,
238
+ help="HuggingFace model path for 'cos_sim' metric. Overrides config eval.encoder_cos_sim_model.")
239
+ parser.add_argument("--psnr-resize", type=int, default=None,
240
+ help="If set, resize images to this resolution (square) before computing PSNR. Overrides config eval.psnr_resize.")
241
+ parser.add_argument("--l1-resize", type=int, default=None,
242
+ help="If set, resize images to this resolution (square) before computing L1. Overrides config eval.l1_resize.")
243
+ # Denoise augmentation: encode -> noise -> DiT denoise -> decode
244
+ parser.add_argument("--denoise-augment", action="store_true",
245
+ help="Enable denoise augmentation: encode GT, add flow-matching noise, DiT denoise, then decode.")
246
+ parser.add_argument("--dit-config", type=str, default=None,
247
+ help="DiT sampling config YAML (must contain stage_2, transport, misc sections).")
248
+ parser.add_argument("--denoise-t", type=float, default=0.1,
249
+ help="Noise level t for flow-matching interpolation (0~1). Default 0.1.")
250
+ parser.add_argument("--denoise-steps", type=int, default=1,
251
+ help="Number of euler steps from t to 0. Default 1.")
252
+ parser.add_argument("--denoise-t-list", type=str, default=None,
253
+ help="Comma-separated t schedule (e.g. '0.8,0.67,0.5,0.0'). Overrides --denoise-t/--denoise-steps.")
254
+ parser.add_argument("--denoise-shift", type=float, default=None,
255
+ help="Time shift value. When set, auto-compute t schedule from shifted 50-step euler, taking last --denoise-steps steps from t_start.")
256
+ parser.add_argument("--denoise-shift-total-steps", type=int, default=50,
257
+ help="Total euler steps for shift schedule computation. Default 50.")
258
+ parser.add_argument("--save-denoise-latents", action="store_true",
259
+ help="Save denoised latents (.pt) and decoded images (.png) under output-dir/denoise_latents/.")
260
+ parser.add_argument("--num-samples", type=int, default=None,
261
+ help="Limit number of eval samples. Default: use all.")
262
+ # Recon gFID: reconstruct train images per-class, compute gFID vs precomputed stats
263
+ parser.add_argument("--recon-gfid", action="store_true",
264
+ help="Reconstruct train images (per-class sampling) and compute gFID.")
265
+ parser.add_argument("--train-data", type=str,
266
+ default=os.environ.get("DATA_ROOT", "/path/to/ImageNet1k") + "/train/",
267
+ help="Train dataset path (ImageFolder) for --recon-gfid.")
268
+ parser.add_argument("--gfid-npz", type=str,
269
+ default=os.path.join(os.environ.get("FID_STATS_ROOT", "/path/to/fid_stats"), "VIRTUAL_imagenet256_labeled.npz"),
270
+ help="Precomputed inception stats NPZ (keys: mu, sigma) for gFID.")
271
+ parser.add_argument("--samples-per-class", type=int, default=50,
272
+ help="Number of samples per class for --recon-gfid. Default 50.")
273
+ return parser.parse_args()
274
+
275
+
276
+ def sample_per_class_indices(dataset, samples_per_class):
277
+ """Return indices for uniform per-class sampling from an ImageFolder dataset."""
278
+ from collections import defaultdict
279
+ class_to_indices = defaultdict(list)
280
+ for idx, (_, label) in enumerate(dataset.samples):
281
+ class_to_indices[label].append(idx)
282
+ selected = []
283
+ for label in sorted(class_to_indices.keys()):
284
+ indices = class_to_indices[label]
285
+ if len(indices) >= samples_per_class:
286
+ selected.extend(indices[:samples_per_class])
287
+ else:
288
+ selected.extend(indices)
289
+ return selected
290
+
291
+
292
+ @torch.no_grad()
293
+ def evaluate_recon_gfid(
294
+ model,
295
+ dataset,
296
+ gfid_npz_path,
297
+ batch_size,
298
+ autocast_kwargs,
299
+ accelerator,
300
+ experiment_dir=None,
301
+ num_samples=None,
302
+ image_features_mode=None,
303
+ save_recon_dir=None,
304
+ ):
305
+ """Reconstruct train images and compute gFID vs precomputed inception stats."""
306
+ from eval.fid import calculate_gfid
307
+ rank = accelerator.process_index
308
+ world_size = accelerator.num_processes
309
+ device = accelerator.device
310
+ is_main = accelerator.is_main_process
311
+
312
+ model.eval()
313
+ N = num_samples if num_samples else len(dataset)
314
+ chunk = N // world_size
315
+ start = rank * chunk
316
+ end = (rank + 1) * chunk if rank < world_size - 1 else N
317
+
318
+ subset = Subset(dataset, list(range(start, end)))
319
+ loader = DataLoader(subset, batch_size=batch_size, shuffle=False,
320
+ num_workers=4, pin_memory=True, drop_last=False)
321
+
322
+ reconstructions = []
323
+ iterator = tqdm(loader, desc=f"[Rank {rank}] Recon gFID", file=sys.stdout) if is_main else loader
324
+
325
+ with torch.inference_mode():
326
+ for images, _ in iterator:
327
+ images = images.to(device, non_blocking=True)
328
+ with autocast(**autocast_kwargs):
329
+ if image_features_mode is not None:
330
+ recon = model(images, image_features_mode=image_features_mode)
331
+ else:
332
+ recon = model(images)
333
+ if hasattr(recon, 'x_rec'):
334
+ recon = recon.x_rec
335
+ recon = recon.clamp(0, 1)
336
+ recon_np = recon.mul(255).permute(0, 2, 3, 1).to("cpu", dtype=torch.uint8).numpy()
337
+ for img in recon_np:
338
+ reconstructions.append(img)
339
+
340
+ reconstructions = np.stack(reconstructions)
341
+
342
+ # Gather across ranks
343
+ temp_dir = os.path.join(experiment_dir or ".", "eval_npzs_gfid")
344
+ if is_main:
345
+ os.makedirs(temp_dir, exist_ok=True)
346
+ accelerator.wait_for_everyone()
347
+
348
+ shard_path = os.path.join(temp_dir, f"recon_gfid_{rank:02d}.npz")
349
+ np.savez(shard_path, arr_0=reconstructions)
350
+ accelerator.wait_for_everyone()
351
+
352
+ metrics = None
353
+ if is_main:
354
+ all_recons = []
355
+ for r in range(world_size):
356
+ shard = np.load(os.path.join(temp_dir, f"recon_gfid_{r:02d}.npz"))["arr_0"]
357
+ all_recons.append(shard)
358
+ combined = np.concatenate(all_recons, axis=0)[:N]
359
+
360
+ ref_stats = np.load(gfid_npz_path)
361
+ print(f"[ReconGFID] Recon shape: {combined.shape}")
362
+
363
+ # Save reconstructed images in background thread (non-blocking, before metrics)
364
+ if save_recon_dir:
365
+ import threading
366
+ recons_copy = np.copy(combined)
367
+ def _save_images():
368
+ from PIL import Image as _Image
369
+ os.makedirs(save_recon_dir, exist_ok=True)
370
+ for i, img in enumerate(recons_copy):
371
+ _Image.fromarray(img).save(os.path.join(save_recon_dir, f"recon_{i:06d}.png"))
372
+ print(f"[ReconGFID] Saved {len(recons_copy)} images to {save_recon_dir}")
373
+ t = threading.Thread(target=_save_images, daemon=False)
374
+ t.start()
375
+ print(f"[ReconGFID] Saving {len(combined)} images to {save_recon_dir} in background...")
376
+
377
+ device_str = "cuda" if device.type == "cuda" else "cpu"
378
+ gfid = calculate_gfid(combined, ref_stats, batch_size=128, device=device_str)
379
+ metrics = {"gfid": float(gfid)}
380
+ print(f"[ReconGFID] gFID: {gfid:.4f}")
381
+
382
+ # cleanup
383
+ for r in range(world_size):
384
+ p = os.path.join(temp_dir, f"recon_gfid_{r:02d}.npz")
385
+ if os.path.exists(p):
386
+ os.remove(p)
387
+
388
+ accelerator.wait_for_everyone()
389
+ return metrics
390
+
391
+
392
+ def compute_denoise_t_list(t_start, denoise_steps, shift, shift_total_steps=50):
393
+ """Compute shifted t schedule from t_start to 0, taking last k steps from full schedule."""
394
+ t_uniform = 1 - torch.linspace(0, 1, shift_total_steps + 1)
395
+ t_shifted = shift * t_uniform / (1 + (shift - 1) * t_uniform)
396
+ # Find first index where t_shifted <= t_start
397
+ below = (t_shifted <= t_start).nonzero(as_tuple=True)[0]
398
+ if len(below) == 0:
399
+ return torch.tensor([t_start, 0.0])
400
+ start_idx = below[0].item()
401
+ # Take denoise_steps steps from start_idx, always end at 0
402
+ remaining = t_shifted[start_idx:]
403
+ if denoise_steps >= len(remaining):
404
+ return remaining
405
+ # Evenly sample denoise_steps points from remaining (including endpoints)
406
+ indices = torch.linspace(0, len(remaining) - 1, denoise_steps + 1).long()
407
+ return remaining[indices]
408
+
409
+
410
+ def euler_denoise(x, dit_model, labels, t_start, steps, t_list=None):
411
+ """Euler ODE integration from t_start to 0.
412
+
413
+ Args:
414
+ t_list: Optional pre-computed t schedule tensor (including endpoints).
415
+ If provided, t_start and steps are ignored.
416
+ """
417
+ if t_list is not None:
418
+ ts = t_list.to(x.device)
419
+ else:
420
+ ts = torch.linspace(t_start, 0, steps + 1, device=x.device)
421
+ for i in range(len(ts) - 1):
422
+ t_cur = ts[i]
423
+ dt = ts[i + 1] - ts[i] # negative (going from t to 0)
424
+ t_batch = torch.full((x.shape[0],), t_cur, device=x.device)
425
+ v = dit_model(x, t_batch, y=labels)
426
+ x = x + v * dt
427
+ return x
428
+
429
+
430
+ @torch.no_grad()
431
+ def evaluate_denoise_reconstruction(
432
+ pr,
433
+ dit_model,
434
+ dataset,
435
+ denoise_t,
436
+ denoise_steps,
437
+ batch_size,
438
+ autocast_kwargs,
439
+ reference_npz_path,
440
+ metrics_to_compute,
441
+ accelerator,
442
+ experiment_dir=None,
443
+ encoder_cos_sim_model=None,
444
+ psnr_resize=None,
445
+ l1_resize=None,
446
+ num_samples=None,
447
+ save_recon_dir=None,
448
+ t_list=None,
449
+ save_latents_dir=None,
450
+ ):
451
+ """Evaluate reconstruction with denoise augmentation (encode -> noise -> DiT denoise -> decode)."""
452
+ rank = accelerator.process_index
453
+ world_size = accelerator.num_processes
454
+ device = accelerator.device
455
+ is_main = accelerator.is_main_process
456
+
457
+ pr.eval()
458
+ dit_model.eval()
459
+
460
+ N = min(len(dataset), num_samples) if num_samples else len(dataset)
461
+ chunk = N // world_size
462
+ start = rank * chunk
463
+ end = (rank + 1) * chunk if rank < world_size - 1 else N
464
+
465
+ subset = Subset(dataset, list(range(start, end)))
466
+ loader = DataLoader(subset, batch_size=batch_size, shuffle=False,
467
+ num_workers=4, pin_memory=True, drop_last=False)
468
+
469
+ reconstructions = []
470
+ iterator = tqdm(loader, desc=f"[Rank {rank}] Denoise Recon", file=sys.stdout) if is_main else loader
471
+
472
+ with torch.inference_mode():
473
+ for images, labels in iterator:
474
+ images = images.to(device, non_blocking=True)
475
+ labels = labels.to(device, non_blocking=True)
476
+ with autocast(**autocast_kwargs):
477
+ # 1. encode -> normalized latent
478
+ enc = pr.encode(images, return_aux=True)
479
+ z = enc.merged_tokens_norm # [B, C, H, W]
480
+
481
+ # 2. flow-matching noise: x_t = (1-t)*data + t*noise
482
+ noise = torch.randn_like(z)
483
+ x_t = (1 - denoise_t) * z + denoise_t * noise
484
+
485
+ # 3. DiT euler denoise with GT label
486
+ z_hat = euler_denoise(x_t, dit_model, labels, denoise_t, denoise_steps, t_list=t_list)
487
+
488
+ # 4. decode (z_hat is [B,C,H,W] normalized, decode will denormalize)
489
+ recon = pr.decode(z_hat)
490
+
491
+ if hasattr(recon, 'x_rec'):
492
+ recon = recon.x_rec
493
+ recon = recon.clamp(0, 1)
494
+ recon_np = recon.mul(255).permute(0, 2, 3, 1).to("cpu", dtype=torch.uint8).numpy()
495
+
496
+ # Save denoised latents, decoded images, and GT images
497
+ if save_latents_dir is not None:
498
+ from PIL import Image as _Image
499
+ latents_sub = os.path.join(save_latents_dir, "latents")
500
+ gt_sub = os.path.join(save_latents_dir, "gt")
501
+ os.makedirs(latents_sub, exist_ok=True)
502
+ os.makedirs(gt_sub, exist_ok=True)
503
+ gt_np = images.clamp(0, 1).mul(255).permute(0, 2, 3, 1).to("cpu", dtype=torch.uint8).numpy()
504
+ for j in range(z_hat.shape[0]):
505
+ global_idx = start + len(reconstructions) + j
506
+ torch.save(z_hat[j].cpu(), os.path.join(latents_sub, f"{global_idx:06d}.pt"))
507
+ _Image.fromarray(recon_np[j]).save(os.path.join(save_latents_dir, f"{global_idx:06d}.png"))
508
+ _Image.fromarray(gt_np[j]).save(os.path.join(gt_sub, f"{global_idx:06d}.png"))
509
+
510
+ for img in recon_np:
511
+ reconstructions.append(img)
512
+
513
+ reconstructions = np.stack(reconstructions)
514
+
515
+ # Gather across ranks via temp npz files
516
+ temp_dir = os.path.join(experiment_dir or ".", "eval_npzs_denoise")
517
+ if is_main:
518
+ os.makedirs(temp_dir, exist_ok=True)
519
+ accelerator.wait_for_everyone()
520
+
521
+ shard_path = os.path.join(temp_dir, f"denoise_recon_{rank:02d}.npz")
522
+ np.savez(shard_path, arr_0=reconstructions)
523
+ accelerator.wait_for_everyone()
524
+
525
+ metrics = None
526
+ if is_main:
527
+ all_recons = []
528
+ for r in range(world_size):
529
+ shard = np.load(os.path.join(temp_dir, f"denoise_recon_{r:02d}.npz"))["arr_0"]
530
+ all_recons.append(shard)
531
+ combined = np.concatenate(all_recons, axis=0)[:N]
532
+
533
+ ref_images = np.load(reference_npz_path)["arr_0"][:N]
534
+ print(f"[DenoiseEval] Combined shape: {combined.shape}, ref shape: {ref_images.shape}")
535
+
536
+ # Save reconstructed images in background thread (non-blocking, before metrics)
537
+ if save_recon_dir:
538
+ import threading
539
+ recons_copy = np.copy(combined)
540
+ def _save_images():
541
+ from PIL import Image as _Image
542
+ os.makedirs(save_recon_dir, exist_ok=True)
543
+ for i, img in enumerate(recons_copy):
544
+ _Image.fromarray(img).save(os.path.join(save_recon_dir, f"recon_{i:06d}.png"))
545
+ print(f"[DenoiseEval] Saved {len(recons_copy)} images to {save_recon_dir}")
546
+ t = threading.Thread(target=_save_images, daemon=False)
547
+ t.start()
548
+ print(f"[DenoiseEval] Saving {len(combined)} images to {save_recon_dir} in background...")
549
+
550
+ metrics = compute_reconstruction_metrics(
551
+ ref_images, combined, accelerator.device, 128,
552
+ metrics_to_compute=metrics_to_compute,
553
+ encoder_model=encoder_cos_sim_model,
554
+ psnr_resize=psnr_resize,
555
+ l1_resize=l1_resize,
556
+ )
557
+ print(f"[DenoiseEval] denoise_t={denoise_t}, steps={denoise_steps}")
558
+ for k, v in metrics.items():
559
+ print(f" {k}: {v:.6f}")
560
+
561
+ # cleanup
562
+ for r in range(world_size):
563
+ p = os.path.join(temp_dir, f"denoise_recon_{r:02d}.npz")
564
+ if os.path.exists(p):
565
+ os.remove(p)
566
+
567
+ accelerator.wait_for_everyone()
568
+ return metrics
569
+
570
+
571
+ def main():
572
+ args = parse_args()
573
+
574
+ mixed_precision = "fp16" if args.precision == "fp16" else ("bf16" if args.precision == "bf16" else "no")
575
+ accelerator = Accelerator(mixed_precision=mixed_precision)
576
+
577
+ # Only rank 0 logs; suppress other ranks to avoid duplicated output
578
+ root_logger = logging.getLogger()
579
+ if accelerator.is_main_process:
580
+ logging.basicConfig(
581
+ level=logging.INFO,
582
+ format="[%(asctime)s] [%(name)s] %(levelname)s: %(message)s",
583
+ datefmt="%Y-%m-%d %H:%M:%S",
584
+ force=True,
585
+ )
586
+ else:
587
+ root_logger.setLevel(logging.CRITICAL + 1)
588
+ for h in list(root_logger.handlers):
589
+ root_logger.removeHandler(h)
590
+ logger = logging.getLogger(__name__)
591
+
592
+ full_cfg = OmegaConf.load(args.config)
593
+
594
+ # External VAE mode (e.g. Flux/SDXL VAE): config has vae_decoder instead of stage_1
595
+ vae_decoder_cfg = full_cfg.get("vae_decoder", None)
596
+ use_external_vae = vae_decoder_cfg is not None
597
+
598
+ pr_config = None
599
+ if not use_external_vae:
600
+ pr_config, *_ = parse_configs(full_cfg)
601
+ if pr_config is None:
602
+ raise ValueError("Config must contain stage_1 or vae_decoder section.")
603
+
604
+ eval_section = full_cfg.get("eval", None)
605
+ eval_data = args.eval_data or (eval_section.get("data_path") if eval_section else None)
606
+ reference_npz = args.reference_npz or (eval_section.get("reference_npz_path") if eval_section else None)
607
+ eval_metrics = args.metrics or (list(eval_section.get("metrics", ["psnr", "ssim", "rfid"])) if eval_section else ["psnr", "ssim", "rfid"])
608
+ encoder_cos_sim_model = args.encoder_cos_sim_model or (eval_section.get("encoder_cos_sim_model") if eval_section else None)
609
+ psnr_resize = args.psnr_resize or (eval_section.get("psnr_resize") if eval_section else None)
610
+ l1_resize = args.l1_resize or (eval_section.get("l1_resize") if eval_section else None)
611
+ eval_weights = args.eval_weights or (eval_section.get("eval_weights", "both") if eval_section else "both")
612
+ if args.image_features_mode is not None:
613
+ eval_modes = [args.image_features_mode]
614
+ else:
615
+ eval_modes = list(eval_section.get("eval_modes", [None]) if eval_section else [None])
616
+ # print(eval_modes)
617
+ if use_external_vae:
618
+ models_to_eval = ["external_vae"]
619
+ elif args.ckpt is None:
620
+ models_to_eval = ["pretrained"]
621
+ else:
622
+ models_to_eval = ["ema", "model"] if eval_weights == "both" else (["ema"] if eval_weights == "ema_only" else ["model"])
623
+
624
+ if not eval_data or not reference_npz:
625
+ raise ValueError("Must specify --eval-data and --reference-npz, or ensure config eval.data_path and eval.reference_npz_path are set.")
626
+ if 'cos_sim' in eval_metrics:
627
+ assert encoder_cos_sim_model, (
628
+ "'cos_sim' metric requires --encoder-cos-sim-model or eval.encoder_cos_sim_model in config."
629
+ )
630
+
631
+ # exp_dir: same level as checkpoints (e.g. ckpts/stage1/exp_name/); when no ckpt, require output_dir
632
+ if args.ckpt is not None:
633
+ exp_dir = Path(args.output_dir) if args.output_dir else Path(args.ckpt).parent.parent
634
+ else:
635
+ exp_dir = Path(args.output_dir) if args.output_dir else Path("./eval_pretrained")
636
+ output_dir = str(exp_dir) # for eval_npzs temp files
637
+ eval_results_dir = exp_dir / "eval_results"
638
+ ckpt_version = "external_vae" if use_external_vae else ("pretrained" if args.ckpt is None else Path(args.ckpt).stem)
639
+ ckpt_output_dir = eval_results_dir / ckpt_version
640
+ os.makedirs(ckpt_output_dir, exist_ok=True)
641
+
642
+ # Save config and startup args for reproducibility (main process only)
643
+ if accelerator.is_main_process:
644
+ config_save_path = ckpt_output_dir / "eval_config.yaml"
645
+ OmegaConf.save(full_cfg, config_save_path)
646
+ logger.info("Config saved to %s", config_save_path)
647
+ args_dict = vars(args)
648
+ # Ensure JSON-serializable (e.g. None, bool, int, str, list)
649
+ args_save_path = ckpt_output_dir / "eval_args.json"
650
+ with open(args_save_path, "w", encoding="utf-8") as f:
651
+ json.dump(args_dict, f, indent=2, ensure_ascii=False)
652
+ logger.info("Startup args saved to %s", args_save_path)
653
+
654
+ device = accelerator.device
655
+ eval_dataset = ImageFolder(
656
+ str(eval_data),
657
+ transform=transforms.Compose([
658
+ transforms.Lambda(lambda pil_image: center_crop_arr(pil_image, args.image_size)),
659
+ transforms.ToTensor(),
660
+ ]),
661
+ )
662
+ logger.info(f"Eval dataset: {eval_data}, {len(eval_dataset)} images. Models to eval: {models_to_eval} eval_mode:{eval_modes}")
663
+
664
+ autocast_kwargs = {
665
+ "enabled": accelerator.mixed_precision != "no",
666
+ "dtype": torch.bfloat16 if accelerator.mixed_precision == "bf16" else torch.float16 if accelerator.mixed_precision == "fp16" else torch.float32,
667
+ }
668
+
669
+ do_zeroshot = (
670
+ not args.no_zeroshot
671
+ and eval_section
672
+ and eval_section.get("zero_shot", {}).get("enabled", False)
673
+ )
674
+ zs_cfg = eval_section.get("zero_shot", {}) if eval_section else {}
675
+ zs_data_path = str(zs_cfg.get("data_path") or zs_cfg.get("imagenet_val_path", ""))
676
+ zs_batch_size = int(zs_cfg.get("batch_size", 128))
677
+ zs_num_workers = int(zs_cfg.get("num_workers", 8))
678
+
679
+ # ── Load DiT model if denoise augmentation is enabled ──
680
+ dit_model = None
681
+ if args.denoise_augment:
682
+ if not args.dit_config:
683
+ raise ValueError("--dit-config is required when --denoise-augment is set.")
684
+ from utils.model_utils import instantiate_from_config as _instantiate
685
+ dit_cfg = OmegaConf.load(args.dit_config)
686
+ _, dit_model_config, _, _, _, _, _, _ = parse_configs(dit_cfg)
687
+ logger.info("Loading DiT model for denoise augmentation...")
688
+ dit_model = _instantiate(dit_model_config).to(device).eval()
689
+ dit_model.requires_grad_(False)
690
+ logger.info("DiT loaded. denoise_t=%.3f, denoise_steps=%d", args.denoise_t, args.denoise_steps)
691
+
692
+ # ── Build per-class sampled train dataset if recon-gfid is enabled ──
693
+ gfid_dataset = None
694
+ if args.recon_gfid:
695
+ full_train = ImageFolder(
696
+ str(args.train_data),
697
+ transform=transforms.Compose([
698
+ transforms.Lambda(lambda pil_image: center_crop_arr(pil_image, args.image_size)),
699
+ transforms.ToTensor(),
700
+ ]),
701
+ )
702
+ selected_indices = sample_per_class_indices(full_train, args.samples_per_class)
703
+ gfid_dataset = Subset(full_train, selected_indices)
704
+ logger.info(f"Recon gFID: {len(gfid_dataset)} images ({args.samples_per_class}/class) from {args.train_data}")
705
+
706
+ evaluated_samples = min(len(eval_dataset), args.num_samples) if args.num_samples else len(eval_dataset)
707
+ results = {
708
+ "ckpt": args.ckpt,
709
+ "config": args.config,
710
+ "evaluation": {
711
+ "dataset_size": len(eval_dataset),
712
+ "requested_samples": args.num_samples,
713
+ "evaluated_samples": evaluated_samples,
714
+ },
715
+ }
716
+ zs_initialized = False
717
+
718
+ for model_name in models_to_eval:
719
+ use_ema = model_name == "ema"
720
+ if use_external_vae:
721
+ from diffusers import AutoencoderKL
722
+ vae_path = vae_decoder_cfg.get("path") if hasattr(vae_decoder_cfg, "get") else str(vae_decoder_cfg)
723
+ logger.info("Loading external VAE from %s", vae_path)
724
+ external_vae = AutoencoderKL.from_pretrained(vae_path).to(device).eval()
725
+ external_vae.requires_grad_(False)
726
+ logger.info("External VAE loaded: scaling_factor=%s", external_vae.config.scaling_factor)
727
+ pr = ExternalVAEWrapper(external_vae)
728
+ elif args.ckpt is None:
729
+ logger.info("Creating pretrained model from config...")
730
+ pr = create_pr_from_config(pr_config, device)
731
+ else:
732
+ logger.info("Loading %s weights...", model_name)
733
+ pr = load_pr_from_checkpoint(pr_config, args.ckpt, device, use_ema=use_ema)
734
+
735
+ # Qwen3Unified 用 varlen 接口,需要 wrapper 转成标准 model(images) 接口
736
+ try:
737
+ encoder_cls = OmegaConf.to_container(pr_config, resolve=True).get("params", {}).get("encoder_cls", "")
738
+ except Exception:
739
+ encoder_cls = ""
740
+ if encoder_cls == "Qwen3Unified":
741
+ pr = Qwen3VarlenWrapper(pr)
742
+ logger.info("Wrapped PatchReparam with Qwen3VarlenWrapper for varlen eval.")
743
+
744
+ pr = accelerator.prepare(pr)
745
+ eval_mod = accelerator.unwrap_model(pr)
746
+
747
+ model_results = {}
748
+
749
+ # ── Denoise augmentation mode ──
750
+ if args.denoise_augment and dit_model is not None:
751
+ # Build t_list: priority: --denoise-t-list > --denoise-shift > linspace
752
+ denoise_t_list = None
753
+ if args.denoise_t_list is not None:
754
+ denoise_t_list = torch.tensor([float(x) for x in args.denoise_t_list.split(",")])
755
+ logger.info("Using manual denoise t_list: %s", denoise_t_list.tolist())
756
+ elif args.denoise_shift is not None:
757
+ denoise_t_list = compute_denoise_t_list(
758
+ args.denoise_t, args.denoise_steps, args.denoise_shift, args.denoise_shift_total_steps
759
+ )
760
+ logger.info("Using shift-computed denoise t_list (shift=%.1f, steps=%d): %s",
761
+ args.denoise_shift, args.denoise_steps, denoise_t_list.tolist())
762
+ else:
763
+ logger.info("Running denoise augmentation evaluation for %s (t=%.3f, steps=%d)...",
764
+ model_name, args.denoise_t, args.denoise_steps)
765
+ save_recon_dir = None
766
+ if args.save_recon_images and accelerator.is_main_process:
767
+ save_recon_dir = str(ckpt_output_dir / "recon_images" / f"{model_name}_denoise_t{args.denoise_t}_s{args.denoise_steps}")
768
+ # Use gfid_dataset (per-class train) if --recon-gfid is also set, else eval_dataset
769
+ denoise_dataset = gfid_dataset if (args.recon_gfid and gfid_dataset is not None) else eval_dataset
770
+ denoise_stats = evaluate_denoise_reconstruction(
771
+ pr=eval_mod,
772
+ dit_model=dit_model,
773
+ dataset=denoise_dataset,
774
+ denoise_t=args.denoise_t,
775
+ denoise_steps=args.denoise_steps,
776
+ batch_size=args.batch_size,
777
+ autocast_kwargs=autocast_kwargs,
778
+ reference_npz_path=args.gfid_npz if (args.recon_gfid and gfid_dataset is not None) else reference_npz,
779
+ metrics_to_compute=eval_metrics,
780
+ accelerator=accelerator,
781
+ experiment_dir=output_dir,
782
+ encoder_cos_sim_model=encoder_cos_sim_model,
783
+ psnr_resize=psnr_resize,
784
+ l1_resize=l1_resize,
785
+ num_samples=args.num_samples,
786
+ save_recon_dir=save_recon_dir,
787
+ t_list=denoise_t_list,
788
+ save_latents_dir=os.path.join(output_dir, "denoise_latents") if args.save_denoise_latents else None,
789
+ )
790
+ if denoise_stats:
791
+ model_results[f"denoise_t{args.denoise_t}_s{args.denoise_steps}"] = {
792
+ k: float(v) for k, v in denoise_stats.items()
793
+ }
794
+ if accelerator.is_main_process and denoise_stats:
795
+ logger.info("%s denoise augment metrics: %s", model_name, denoise_stats)
796
+ elif args.recon_gfid and gfid_dataset is not None:
797
+ # ── Recon gFID mode: reconstruct train images, compute gFID vs precomputed stats ──
798
+ logger.info("Running recon gFID evaluation for %s (%d images)...", model_name, len(gfid_dataset))
799
+ save_recon_dir = None
800
+ if args.save_recon_images and accelerator.is_main_process:
801
+ save_recon_dir = str(ckpt_output_dir / "recon_images" / f"{model_name}_recon_gfid")
802
+ gfid_stats = evaluate_recon_gfid(
803
+ model=eval_mod,
804
+ dataset=gfid_dataset,
805
+ gfid_npz_path=args.gfid_npz,
806
+ batch_size=args.batch_size,
807
+ autocast_kwargs=autocast_kwargs,
808
+ accelerator=accelerator,
809
+ experiment_dir=output_dir,
810
+ num_samples=len(gfid_dataset),
811
+ image_features_mode=eval_modes[0] if eval_modes[0] else None,
812
+ save_recon_dir=save_recon_dir,
813
+ )
814
+ if gfid_stats:
815
+ model_results["recon_gfid"] = {k: float(v) for k, v in gfid_stats.items()}
816
+ if accelerator.is_main_process and gfid_stats:
817
+ logger.info("%s recon gFID metrics: %s", model_name, gfid_stats)
818
+ else:
819
+ # ── Normal reconstruction evaluation ──
820
+ logger.info("Running reconstruction evaluation for %s...", model_name)
821
+ for eval_mode in eval_modes:
822
+ print(eval_mode)
823
+ save_recon_dir = None
824
+ if args.save_recon_images and accelerator.is_main_process:
825
+ subdir = model_name if eval_mode is None else f"{model_name}_{eval_mode}"
826
+ save_recon_dir = str(ckpt_output_dir / "recon_images" / subdir)
827
+ eval_stats = evaluate_reconstruction_distributed(
828
+ eval_mod,
829
+ eval_dataset,
830
+ args.num_samples if args.num_samples else len(eval_dataset),
831
+ batch_size=args.batch_size,
832
+ experiment_dir=output_dir,
833
+ global_step=0,
834
+ autocast_kwargs=autocast_kwargs,
835
+ reference_npz_path=reference_npz,
836
+ metrics_to_compute=eval_metrics,
837
+ accelerator=accelerator,
838
+ save_recon_dir=save_recon_dir,
839
+ image_features_mode=eval_mode,
840
+ encoder_cos_sim_model=encoder_cos_sim_model,
841
+ psnr_resize=psnr_resize,
842
+ l1_resize=l1_resize,
843
+ )
844
+ if eval_stats:
845
+ recon_key = "reconstruction" if eval_mode is None else f"reconstruction_{eval_mode}"
846
+ model_results[recon_key] = {k: float(v) for k, v in eval_stats.items()}
847
+ if accelerator.is_main_process and eval_stats:
848
+ mode_tag = "" if eval_mode is None else f" [{eval_mode}]"
849
+ logger.info("%s reconstruction metrics%s: %s", model_name, mode_tag, eval_stats)
850
+
851
+ if do_zeroshot and zs_data_path and accelerator.is_main_process and not use_external_vae:
852
+ enc_cls = eval_mod.encoder.__class__.__name__
853
+ if "siglip" not in enc_cls.lower():
854
+ logger.warning("Zero-shot skipped for %s: encoder %s is not SigLIP-based.", model_name, enc_cls)
855
+ else:
856
+ try:
857
+ from transformers import AutoProcessor, SiglipModel
858
+ sp = full_cfg.get("stage_1")
859
+ params = getattr(sp, "params", None) if sp is not None else None
860
+ enc_params = getattr(params, "encoder_params", None) if params is not None else None
861
+ siglip_path = None
862
+ if enc_params is not None:
863
+ siglip_path = getattr(enc_params, "model_name", None) or getattr(enc_params, "sementic_model_name", None)
864
+ if not siglip_path and params is not None:
865
+ siglip_path = getattr(params, "encoder_config_path", None)
866
+ siglip_path = str(siglip_path) if siglip_path else ""
867
+ if siglip_path:
868
+ if not zs_initialized:
869
+ logger.info("Loading full SigLIP for zero-shot from %s", siglip_path)
870
+ zs_full_siglip = SiglipModel.from_pretrained(siglip_path).to(device).eval()
871
+ zs_processor = AutoProcessor.from_pretrained(siglip_path)
872
+ zs_classifier = build_zero_shot_classifier(
873
+ zs_full_siglip,
874
+ zs_processor,
875
+ classnames=IMAGENET_CLASSNAMES,
876
+ templates=CLIP_PAPER_PROMPT_TEMPLATES,
877
+ num_classes_per_batch=10,
878
+ device=device,
879
+ use_tqdm=True,
880
+ )
881
+ zs_dataloader = create_imagenet_dataloader_patch_reparam(
882
+ zs_data_path,
883
+ eval_mod.encoder_input_size,
884
+ eval_mod.encoder_mean,
885
+ eval_mod.encoder_std,
886
+ batch_size=zs_batch_size,
887
+ num_workers=zs_num_workers,
888
+ )
889
+ zs_initialized = True
890
+ zs_wrapper = PatchReparamZeroShotWrapper(eval_mod.encoder, zs_full_siglip)
891
+ zs_wrapper.eval()
892
+ zs_top1, zs_top5 = evaluate_zero_shot(
893
+ zs_wrapper,
894
+ zs_processor,
895
+ zs_classifier,
896
+ zs_dataloader,
897
+ device=device,
898
+ precision=args.precision,
899
+ )
900
+ model_results["zeroshot"] = {"top1": float(zs_top1), "top5": float(zs_top5)}
901
+ logger.info("%s zero-shot top1: %.2f, top5: %.2f", model_name, zs_top1, zs_top5)
902
+ else:
903
+ logger.warning("Zero-shot disabled: no SigLIP path in config.")
904
+ except Exception as e:
905
+ logger.warning("Zero-shot failed for %s: %s", model_name, e)
906
+
907
+ results[model_name] = model_results
908
+
909
+ if do_zeroshot and not zs_data_path and accelerator.is_main_process:
910
+ logger.warning("Zero-shot enabled but data_path not set; skipping.")
911
+
912
+ accelerator.wait_for_everyone()
913
+ if accelerator.is_main_process and results:
914
+ result_stem = "external_vae" if use_external_vae else ("pretrained" if args.ckpt is None else Path(args.ckpt).stem)
915
+ result_path = ckpt_output_dir / f"{result_stem}.json"
916
+ suffix = 1
917
+ while result_path.exists():
918
+ result_path = ckpt_output_dir / f"{result_stem}_{suffix}.json"
919
+ suffix += 1
920
+ with open(result_path, "w", encoding="utf-8") as f:
921
+ json.dump(results, f, indent=2, ensure_ascii=False)
922
+ logger.info("Evaluation results saved to %s", result_path)
923
+ logger.info("Evaluation done.")
924
+
925
+
926
+ if __name__ == "__main__":
927
+ main()
patch-reparameterization/src/sample_ddp.py ADDED
@@ -0,0 +1,753 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This source code is licensed under the license found in the
2
+ # LICENSE file in the root directory of this source tree.
3
+
4
+ """
5
+ Samples a large number of images from a pre-trained stage-2 model using DDP and
6
+ stores results for downstream metrics. For single-device sampling, use sample.py.
7
+
8
+ When --reference-npz is provided, FID is computed after sampling completes.
9
+ Reference NPZ can be either:
10
+ - Precomputed stats (keys: mu, sigma) from Inception features
11
+ - Raw images (key: arr_0) - FID will be computed via torch_fidelity
12
+ """
13
+ import sys
14
+ import os
15
+
16
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
17
+
18
+ import argparse
19
+ import json
20
+ import logging
21
+ import math
22
+ from typing import Callable, Optional
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ import numpy as np
27
+ import torch
28
+ import torch.distributed as dist
29
+ from PIL import Image
30
+ from torch.cuda.amp import autocast
31
+ from tqdm import tqdm
32
+ from pathlib import Path
33
+ from omegaconf import OmegaConf
34
+ from utils.config_utils import load_config # noqa: F401 (registers oc.env resolver)
35
+ from utils.model_utils import instantiate_from_config
36
+ from stage1 import PatchReparam
37
+ from stage2.models import Stage2ModelProtocol
38
+ from stage2.transport import create_transport, Sampler
39
+ from utils.train_utils import parse_configs, center_crop_arr
40
+ from eval.fid import calculate_gfid, calculate_rfid
41
+ from torchvision.datasets import ImageFolder
42
+ from torchvision import transforms
43
+
44
+
45
+ def _safe_load_checkpoint(path: str, map_location: str = "cpu"):
46
+ try:
47
+ return torch.load(path, map_location=map_location, weights_only=False)
48
+ except TypeError:
49
+ # For older PyTorch versions without weights_only arg
50
+ return torch.load(path, map_location=map_location)
51
+
52
+
53
+ def _load_patch_reparam_checkpoint(pr: PatchReparam, ckpt_path: str, use_ema: bool = True) -> None:
54
+ """
55
+ Load checkpoint weights into PatchReparam, choosing between EMA and model weights.
56
+ Mirrors the logic in eval_inversion_error._load_patch_reparam_checkpoint.
57
+ """
58
+ logger.info("Loading PatchReparam checkpoint from %s (use_ema=%s)", ckpt_path, use_ema)
59
+ ckpt = _safe_load_checkpoint(ckpt_path, map_location="cpu")
60
+ state_dict = ckpt.get("ema" if use_ema else "model")
61
+ if state_dict is None:
62
+ raise KeyError(f"Checkpoint must contain 'ema' or 'model'. Got keys: {list(ckpt.keys())}")
63
+
64
+ clean_state = {}
65
+ for k, v in state_dict.items():
66
+ clean_k = k
67
+ for prefix in ["module._orig_mod.", "_orig_mod.", "module."]:
68
+ if clean_k.startswith(prefix):
69
+ clean_k = clean_k[len(prefix):]
70
+ break
71
+ clean_state[clean_k] = v
72
+
73
+ missing, unexpected = pr.load_state_dict(clean_state, strict=False)
74
+ if missing:
75
+ logger.warning("PatchReparam checkpoint missing %d keys (first 5): %s...", len(missing), missing[:5])
76
+ if unexpected:
77
+ logger.warning("PatchReparam checkpoint unexpected %d keys (first 5): %s...", len(unexpected), unexpected[:5])
78
+ logger.info("PatchReparam checkpoint loaded successfully.")
79
+
80
+
81
+ def create_npz_from_sample_folder(sample_dir, num=50_000):
82
+ """
83
+ Builds a single .npz file from a folder of .png samples.
84
+ """
85
+ samples = []
86
+ for i in tqdm(range(num), desc="Building .npz file from samples"):
87
+ sample_pil = Image.open(f"{sample_dir}/{i:06d}.png")
88
+ sample_np = np.asarray(sample_pil).astype(np.uint8)
89
+ samples.append(sample_np)
90
+ samples = np.stack(samples)
91
+ assert samples.shape == (num, samples.shape[1], samples.shape[2], 3)
92
+ npz_path = f"{sample_dir}.npz"
93
+ np.savez(npz_path, arr_0=samples)
94
+ logger.info("Saved .npz file to %s [shape=%s].", npz_path, samples.shape)
95
+ return npz_path
96
+
97
+
98
+ def compute_fid_from_samples(
99
+ gen_npz_path: str,
100
+ reference_npz_path: str,
101
+ device: torch.device,
102
+ batch_size: int = 128,
103
+ ) -> float:
104
+ """
105
+ Compute FID between generated samples and reference.
106
+ Reference NPZ can be:
107
+ - Precomputed stats: keys 'mu', 'sigma' (Inception moments)
108
+ - Raw images: key 'arr_0'
109
+ """
110
+ gen_data = np.load(gen_npz_path)
111
+ gen_arr = gen_data["arr_0"]
112
+
113
+ ref_data = np.load(reference_npz_path)
114
+ ref_files = list(ref_data.files)
115
+
116
+ device_str = "cuda" if device.type == "cuda" else "cpu"
117
+
118
+ if "mu" in ref_files and "sigma" in ref_files:
119
+ fid = calculate_gfid(gen_arr, ref_data, batch_size=batch_size, device=device_str)
120
+ elif "arr_0" in ref_files:
121
+ ref_arr = ref_data["arr_0"]
122
+ fid = calculate_rfid(gen_arr, ref_arr, bs=batch_size, device=device_str)
123
+ else:
124
+ raise KeyError(
125
+ f"Reference NPZ must have 'mu'/'sigma' (precomputed stats) or 'arr_0' (raw images). "
126
+ f"Got keys: {ref_files}"
127
+ )
128
+ return float(fid)
129
+
130
+ def build_label_sampler(
131
+ sampling_mode: str,
132
+ num_classes: int,
133
+ num_fid_samples: int,
134
+ total_samples: int,
135
+ samples_needed_this_device: int,
136
+ batch_size: int,
137
+ device: torch.device,
138
+ rank: int,
139
+ iterations: int,
140
+ seed: int,
141
+ eval_data: Optional[str] = None,
142
+ image_size: int = 256,
143
+ ) -> Callable[[int], torch.Tensor]:
144
+ """Create a callable that returns a batch of labels for the given step index.
145
+ When mode='dataset', labels follow val dataset order (same as training eval)."""
146
+
147
+ if sampling_mode == "dataset":
148
+ if not eval_data or not os.path.isdir(eval_data):
149
+ raise ValueError(
150
+ "dataset mode requires --eval-data (or config eval.data_path) pointing to ImageFolder val dir."
151
+ )
152
+ dataset = ImageFolder(
153
+ eval_data,
154
+ transform=transforms.Compose([
155
+ transforms.Lambda(lambda x: center_crop_arr(x, image_size)),
156
+ transforms.ToTensor(),
157
+ ]),
158
+ )
159
+ N = min(len(dataset), num_fid_samples)
160
+ world_size = total_samples // samples_needed_this_device
161
+ chunk = total_samples // world_size # == samples_needed_this_device
162
+ start = rank * chunk
163
+ end = min(start + chunk, total_samples)
164
+ rank_indices = list(range(start, end))
165
+ device_pool = torch.tensor([dataset.targets[i] for i in rank_indices], dtype=torch.long)
166
+ needed = iterations * batch_size
167
+ if len(device_pool) < needed:
168
+ gen = torch.Generator().manual_seed(seed + rank)
169
+ device_pool = torch.cat([
170
+ device_pool,
171
+ torch.randint(0, num_classes, (needed - len(device_pool),), generator=gen),
172
+ ])
173
+ device_pool = device_pool[:needed].view(iterations, batch_size)
174
+
175
+ def dataset_sampler(step_idx: int) -> torch.Tensor:
176
+ return device_pool[step_idx].to(device)
177
+
178
+ return dataset_sampler
179
+
180
+ if sampling_mode == "random":
181
+ def random_sampler(_step_idx: int) -> torch.Tensor:
182
+ return torch.randint(0, num_classes, (batch_size,), device=device)
183
+
184
+ return random_sampler
185
+
186
+ if sampling_mode == "equal":
187
+ if num_fid_samples % num_classes != 0:
188
+ raise ValueError(
189
+ f"Equal label sampling requires num_fid_samples ({num_fid_samples}) to be divisible by num_classes ({num_classes})."
190
+ )
191
+
192
+ labels_per_class = num_fid_samples // num_classes
193
+ base_pool = torch.arange(num_classes, dtype=torch.long).repeat_interleave(labels_per_class)
194
+
195
+ generator = torch.Generator()
196
+ generator.manual_seed(seed)
197
+ permutation = torch.randperm(base_pool.numel(), generator=generator)
198
+ base_pool = base_pool[permutation]
199
+
200
+ if total_samples > num_fid_samples:
201
+ tail = torch.randint(0, num_classes, (total_samples - num_fid_samples,), generator=generator)
202
+ global_pool = torch.cat([base_pool, tail], dim=0)
203
+ else:
204
+ global_pool = base_pool
205
+
206
+ start = rank * samples_needed_this_device
207
+ end = start + samples_needed_this_device
208
+ device_pool = global_pool[start:end]
209
+ device_pool = device_pool.view(iterations, batch_size)
210
+
211
+ def equal_sampler(step_idx: int) -> torch.Tensor:
212
+ labels = device_pool[step_idx]
213
+ return labels.to(device)
214
+
215
+ return equal_sampler
216
+ raise ValueError(f"Unknown label sampling mode: {sampling_mode}")
217
+
218
+ def main(args):
219
+ """Run sampling with distributed execution."""
220
+ if not torch.cuda.is_available():
221
+ raise RuntimeError("Sampling with DDP requires at least one GPU. Use sample.py for single-device usage.")
222
+
223
+ torch.backends.cuda.matmul.allow_tf32 = args.tf32
224
+ torch.backends.cudnn.allow_tf32 = args.tf32
225
+ torch.set_grad_enabled(False)
226
+
227
+ dist.init_process_group("nccl")
228
+ rank = dist.get_rank()
229
+ world_size = dist.get_world_size()
230
+ device_idx = rank % torch.cuda.device_count()
231
+ torch.cuda.set_device(device_idx)
232
+ device = torch.device("cuda", device_idx)
233
+
234
+ if rank == 0:
235
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
236
+ else:
237
+ logger.addHandler(logging.NullHandler())
238
+ logger.propagate = False
239
+
240
+ seed = args.global_seed * world_size + rank
241
+ torch.manual_seed(seed)
242
+ torch.cuda.manual_seed(seed)
243
+ if rank == 0:
244
+ logger.info("Starting rank=%s, seed=%s, world_size=%s.", rank, seed, world_size)
245
+
246
+ use_bf16 = args.precision == "bf16"
247
+ if use_bf16 and not torch.cuda.is_bf16_supported():
248
+ raise ValueError("Requested bf16 precision, but the current CUDA device does not support bfloat16.")
249
+ autocast_kwargs = dict(dtype=torch.bfloat16, enabled=use_bf16)
250
+ cfg = OmegaConf.load(args.config)
251
+ if args.ckpt is not None:
252
+ OmegaConf.update(cfg, "stage_2.ckpt", args.ckpt, force_add=True)
253
+ if args.cfg_scale is not None:
254
+ OmegaConf.update(cfg, "guidance.scale", args.cfg_scale, force_add=True)
255
+ if args.precomputed_latents_dir is not None:
256
+ val = None if args.precomputed_latents_dir.lower() == "none" else args.precomputed_latents_dir
257
+ OmegaConf.update(cfg, "misc.precomputed_latents_dir", val, force_add=True)
258
+ pr_config, model_config, transport_config, sampler_config, guidance_config, misc, _, _ = parse_configs(cfg)
259
+ # ── VAE mode: if vae_decoder is specified, use external VAE instead of PatchReparam ──
260
+ vae_decoder_cfg = cfg.get("vae_decoder", None)
261
+ use_external_vae = vae_decoder_cfg is not None
262
+
263
+ if not use_external_vae and (pr_config is None or model_config is None):
264
+ raise ValueError("Config must provide both stage_1 and stage_2 entries (or use vae_decoder).")
265
+ if use_external_vae and model_config is None:
266
+ raise ValueError("Config must provide stage_2 section.")
267
+
268
+ misc = {} if misc is None else dict(misc)
269
+
270
+ latent_size = tuple(int(dim) for dim in misc.get("latent_size", (768, 16, 16)))
271
+ shift_dim = misc.get("time_dist_shift_dim", math.prod(latent_size))
272
+ shift_base = misc.get("time_dist_shift_base", 4096)
273
+ time_dist_shift = math.sqrt(shift_dim / shift_base)
274
+ if rank == 0:
275
+ logger.info("Using time_dist_shift=%.4f.", time_dist_shift)
276
+
277
+ # ------------------------------------------------------------------
278
+ # Optional DiT latent caching / reuse
279
+ # ------------------------------------------------------------------
280
+ precomputed_latents_dir = None
281
+ save_latents = False
282
+ latents_dir = None
283
+ if misc is not None:
284
+ precomputed_latents_dir = misc.get("precomputed_latents_dir", None)
285
+ save_latents = bool(misc.get("save_latents", False))
286
+ latents_dir = misc.get("latents_dir", None)
287
+
288
+ external_vae = None
289
+ vae_scaling_factor = None
290
+ pr = None
291
+ proj_down_weight = None
292
+ proj_norm_mode = None
293
+ proj_norm_mean = None
294
+ proj_norm_std = None
295
+
296
+ if use_external_vae:
297
+ from diffusers import AutoencoderKL
298
+ vae_path = vae_decoder_cfg.get("path") if hasattr(vae_decoder_cfg, "get") else str(vae_decoder_cfg)
299
+ logger.info(f"Loading external VAE from {vae_path}")
300
+ external_vae = AutoencoderKL.from_pretrained(vae_path).to(device).eval()
301
+ external_vae.requires_grad_(False)
302
+ vae_scaling_factor = external_vae.config.scaling_factor
303
+ logger.info(f"External VAE loaded: scaling_factor={vae_scaling_factor}")
304
+
305
+ # Optional projection (for high-dim experiment decoding)
306
+ proj_cfg = vae_decoder_cfg.get("projection", None) if hasattr(vae_decoder_cfg, "get") else None
307
+ if proj_cfg is not None:
308
+ proj_cfg = OmegaConf.to_container(proj_cfg, resolve=True) if hasattr(proj_cfg, '_dict') or type(proj_cfg).__name__ == 'DictConfig' else dict(proj_cfg)
309
+ weight_path = proj_cfg.get("weight_path", None)
310
+ if not weight_path or not os.path.exists(weight_path):
311
+ raise FileNotFoundError(f"Projection weight_path required for sampling: {weight_path}")
312
+ proj_up_weight = torch.load(weight_path, map_location="cpu").to(device)
313
+ proj_down_weight = proj_up_weight.T.contiguous()
314
+ logger.info(f"Loaded projection matrix from {weight_path}: {proj_up_weight.shape}")
315
+
316
+ proj_norm_mode = proj_cfg.get("norm_mode", "global")
317
+ if proj_norm_mode == "per_dim":
318
+ norm_stat_path = proj_cfg.get("norm_stat_path", None)
319
+ if norm_stat_path and os.path.exists(norm_stat_path):
320
+ stats = torch.load(norm_stat_path, map_location="cpu")
321
+ proj_norm_mean = stats["mean"].to(device)
322
+ proj_norm_std = torch.sqrt(stats["var"].to(device) + 1e-5)
323
+ logger.info(f"Loaded per-dim norm stats from {norm_stat_path}")
324
+ else:
325
+ raise FileNotFoundError(f"per_dim norm requires norm_stat_path: {norm_stat_path}")
326
+ else:
327
+ pr: PatchReparam = instantiate_from_config(pr_config).to(device)
328
+ pr_ckpt_cfg = pr_config.get("checkpoint") if hasattr(pr_config, "get") else None
329
+ if pr_ckpt_cfg:
330
+ if isinstance(pr_ckpt_cfg, (str, Path)):
331
+ ckpt_path = str(pr_ckpt_cfg)
332
+ use_ema = True
333
+ else:
334
+ ckpt_path = pr_ckpt_cfg.get("path") or pr_ckpt_cfg.get("ckpt_path")
335
+ use_ema = pr_ckpt_cfg.get("use_ema", True)
336
+ if ckpt_path and str(ckpt_path).strip():
337
+ _load_patch_reparam_checkpoint(pr, str(ckpt_path), use_ema=use_ema)
338
+ else:
339
+ logger.warning("PatchReparam checkpoint config present but path is empty.")
340
+ else:
341
+ logger.info("No PatchReparam checkpoint specified.")
342
+ pr.eval()
343
+
344
+ model: Stage2ModelProtocol = instantiate_from_config(model_config).to(device)
345
+ model.eval()
346
+
347
+ transport_params = {}
348
+ if transport_config is not None:
349
+ transport_params = dict(transport_config.get("params", {}))
350
+ transport = create_transport(
351
+ **transport_params,
352
+ time_dist_shift=time_dist_shift,
353
+ )
354
+ sampler = Sampler(transport)
355
+
356
+ sampler_config = {} if sampler_config is None else dict(sampler_config)
357
+ sampler_mode = sampler_config.get("mode", "ODE")
358
+ sampler_params = dict(sampler_config.get("params", {}))
359
+ mode = sampler_mode.upper()
360
+ if mode == "ODE":
361
+ sample_fn = sampler.sample_ode(**sampler_params)
362
+ elif mode == "SDE":
363
+ sample_fn = sampler.sample_sde(**sampler_params)
364
+ else:
365
+ raise NotImplementedError(f"Invalid sampling mode {sampler_mode}.")
366
+
367
+ guidance_config = {} if guidance_config is None else dict(guidance_config)
368
+
369
+ def guidance_value(key: str, default: float):
370
+ if key in guidance_config:
371
+ return guidance_config[key]
372
+ dashed_key = key.replace("_", "-")
373
+ return guidance_config.get(dashed_key, default)
374
+
375
+ guidance_scale = guidance_config.get("scale", 1.0)
376
+ guidance_method = guidance_config.get("method", "cfg")
377
+ t_min = guidance_value("t_min", 0.0)
378
+ t_max = guidance_value("t_max", 1.0)
379
+
380
+ guid_model_forward = None
381
+ if guidance_scale > 1.0 and guidance_method == "autoguidance":
382
+ guid_model_config = guidance_config.get("guidance_model")
383
+ if guid_model_config is None:
384
+ raise ValueError("Please provide a guidance model config when using autoguidance.")
385
+ guid_model: Stage2ModelProtocol = instantiate_from_config(guid_model_config).to(device)
386
+ guid_model.eval()
387
+ guid_model_forward = guid_model.forward
388
+
389
+ num_classes = int(misc.get("num_classes", 1000))
390
+ null_label = int(misc.get("null_label", num_classes))
391
+
392
+ model_target = model_config.get("target", "stage2")
393
+ model_string_name = str(model_target).split(".")[-1]
394
+ ckpt_path = model_config.get("ckpt")
395
+ ckpt_string_name = "pretrained" if not ckpt_path else os.path.splitext(os.path.basename(str(ckpt_path)))[0]
396
+ sampling_method = sampler_params.get("sampling_method", "na")
397
+ num_steps = sampler_params.get("num_steps", sampler_params.get("steps", "na"))
398
+ guidance_tag = f"cfg-{guidance_scale:.2f}"
399
+ base_components = [model_string_name, ckpt_string_name, guidance_tag, f"bs{args.per_proc_batch_size}"]
400
+ logger.info(f"args.per_proc_batch_size{args.per_proc_batch_size}")
401
+ if mode == "ODE":
402
+ detail_components = [mode, str(num_steps), str(sampling_method), args.precision]
403
+ else:
404
+ diffusion_form = sampler_params.get("diffusion_form", "na")
405
+ last_step = sampler_params.get("last_step", "na")
406
+ last_step_size = sampler_params.get("last_step_size", "na")
407
+ detail_components = [mode, str(num_steps), str(sampling_method), str(diffusion_form), str(last_step), str(last_step_size), args.precision]
408
+ folder_name = "-".join(component.replace(os.sep, "-") for component in base_components + detail_components)
409
+ possible_folder_name = os.environ.get('SAVE_FOLDER', None)
410
+ if possible_folder_name:
411
+ sample_folder_dir = os.path.join(args.sample_dir, possible_folder_name)
412
+ else:
413
+ sample_folder_dir = os.path.join(args.sample_dir, folder_name)
414
+ # Parent dir for result files (one level above image folder)
415
+ result_parent_dir = Path(sample_folder_dir).parent
416
+ folder_base = Path(sample_folder_dir).name
417
+
418
+ if rank == 0:
419
+ os.makedirs(sample_folder_dir, exist_ok=True)
420
+ logger.info("Saving .png samples at %s", sample_folder_dir)
421
+
422
+ # Determine directory to read/write DiT latents.
423
+ if latents_dir is None:
424
+ latents_dir = os.path.join(sample_folder_dir, "latents")
425
+
426
+ # Save config and startup args for reproducibility (in parent dir, not inside image folder)
427
+ config_save_path = result_parent_dir / f"{folder_base}_sample_config.yaml"
428
+ OmegaConf.save(cfg, config_save_path)
429
+ logger.info("Config saved to %s", config_save_path)
430
+ args_dict = vars(args)
431
+ args_save_path = result_parent_dir / f"{folder_base}_sample_args.json"
432
+ with open(args_save_path, "w", encoding="utf-8") as f:
433
+ json.dump(args_dict, f, indent=2, ensure_ascii=False)
434
+ logger.info("Args saved to %s", args_save_path)
435
+ # Broadcast latents_dir from rank 0 so all ranks agree.
436
+ latents_dir_obj = {"latents_dir": latents_dir}
437
+ obj_list = [latents_dir_obj]
438
+ dist.broadcast_object_list(obj_list, src=0)
439
+ latents_dir = obj_list[0]["latents_dir"]
440
+ if save_latents and latents_dir is not None and rank == 0:
441
+ os.makedirs(latents_dir, exist_ok=True)
442
+ dist.barrier()
443
+
444
+ # If npz already exists with enough samples, skip sampling to avoid redundant computation
445
+ npz_path = f"{sample_folder_dir}.npz"
446
+ skip_sampling = False
447
+ if rank == 0:
448
+ logger.info("NPZ path: %s, isfile: %s", npz_path, os.path.isfile(npz_path))
449
+ if os.path.isfile(npz_path):
450
+ try:
451
+ data = np.load(npz_path)
452
+ arr = data["arr_0"]
453
+ logger.info("NPZ already exists at %s with %d samples.", npz_path, arr.shape[0])
454
+ if arr.shape[0] >= args.num_fid_samples:
455
+ skip_sampling = True
456
+ logger.info("NPZ already exists at %s with %d samples, skipping sampling.", npz_path, arr.shape[0])
457
+ except Exception as e:
458
+ logger.exception("Error loading NPZ: %s", e)
459
+ # Fall back to existing PNG samples if they are complete enough.
460
+ try:
461
+ existing_files = [
462
+ name
463
+ for name in os.listdir(sample_folder_dir)
464
+ if os.path.isfile(os.path.join(sample_folder_dir, name)) and name.endswith(".png")
465
+ ]
466
+ existing_set = set(existing_files)
467
+ expected_files = {f"{i:06d}.png" for i in range(args.num_fid_samples)}
468
+ if expected_files.issubset(existing_set):
469
+ logger.info(
470
+ "Found %d PNG samples and a broken NPZ. Rebuilding NPZ from existing images and skipping sampling.",
471
+ len(existing_files),
472
+ )
473
+ create_npz_from_sample_folder(sample_folder_dir, args.num_fid_samples)
474
+ skip_sampling = True
475
+ else:
476
+ logger.info(
477
+ "Existing PNG samples are incomplete (%d found, need at least %d). Proceeding to resample.",
478
+ len(existing_files),
479
+ args.num_fid_samples,
480
+ )
481
+ except Exception as e2:
482
+ logger.exception("Error while checking/rebuilding NPZ from PNGs: %s", e2)
483
+ skip_tensor = torch.tensor(1 if skip_sampling else 0, device=device)
484
+ dist.broadcast(skip_tensor, 0)
485
+ skip_sampling = skip_tensor.item() == 1
486
+ logger.info("Skip sampling (existing images/NPZ): %s", skip_sampling)
487
+ n = args.per_proc_batch_size
488
+ global_batch_size = n * world_size
489
+ existing = [name for name in os.listdir(sample_folder_dir) if (os.path.isfile(os.path.join(sample_folder_dir, name)) and name.endswith(".png"))]
490
+ num_samples = len(existing)
491
+ total_samples = int(math.ceil(args.num_fid_samples / global_batch_size) * global_batch_size)
492
+ if rank == 0:
493
+ logger.info("Total number of images that will be sampled: %d", total_samples)
494
+ if total_samples % world_size != 0:
495
+ raise ValueError("Total samples must be divisible by world size.")
496
+ samples_needed_this_gpu = total_samples // world_size
497
+ if samples_needed_this_gpu % n != 0:
498
+ raise ValueError("Per-rank sample count must be divisible by the per-GPU batch size.")
499
+ iterations = samples_needed_this_gpu // n
500
+ # Resume: align to global_batch_size boundary to preserve label consistency
501
+ total = (num_samples // global_batch_size) * global_batch_size
502
+ start_step = total // global_batch_size
503
+ remaining_iterations = iterations - start_step
504
+ if rank == 0 and start_step > 0:
505
+ logger.info("Resuming from step %d (%d images exist, aligned to %d), %d steps remaining.",
506
+ start_step, num_samples, total, remaining_iterations)
507
+ pbar = tqdm(range(remaining_iterations)) if rank == 0 else range(remaining_iterations)
508
+
509
+ eval_data = args.eval_data
510
+ if eval_data is None and cfg is not None:
511
+ eval_section = cfg.get("eval", None)
512
+ if eval_section is not None:
513
+ eval_data = eval_section.get("data_path", None)
514
+ label_sampler = build_label_sampler(
515
+ args.label_sampling,
516
+ num_classes,
517
+ args.num_fid_samples,
518
+ total_samples,
519
+ samples_needed_this_gpu,
520
+ n,
521
+ device,
522
+ rank,
523
+ iterations,
524
+ args.global_seed,
525
+ eval_data=eval_data if args.label_sampling == "dataset" else None,
526
+ image_size=args.image_size,
527
+ )
528
+
529
+ # Decide whether to reuse precomputed DiT latents (skip DiT forward) or run DiT.
530
+ use_precomputed_latents = False
531
+ # if precomputed_latents_dir is None:
532
+ # # If a dedicated precomputed directory is not provided, reuse latents_dir if present.
533
+ # precomputed_latents_dir = latents_dir
534
+ if (
535
+ not skip_sampling
536
+ and precomputed_latents_dir is not None
537
+ and os.path.isdir(precomputed_latents_dir)
538
+ ):
539
+ if rank == 0:
540
+ logger.info("Using precomputed DiT latents from %s.", precomputed_latents_dir)
541
+ use_precomputed_latents = True
542
+ use_precomputed_tensor = torch.tensor(1 if use_precomputed_latents else 0, device=device)
543
+ dist.broadcast(use_precomputed_tensor, 0)
544
+ use_precomputed_latents = use_precomputed_tensor.item() == 1
545
+ if rank == 0:
546
+ logger.info("Use precomputed DiT latents: %s", use_precomputed_latents)
547
+
548
+ using_cfg = guidance_scale > 1.0
549
+ if not skip_sampling:
550
+ for step_idx in pbar:
551
+ actual_step = start_step + step_idx
552
+ with autocast(**autocast_kwargs):
553
+ if use_precomputed_latents:
554
+ # Load precomputed DiT latents and only run the decoder.
555
+ latents = []
556
+ for local_idx in range(n):
557
+ index = local_idx * world_size + rank + total
558
+ latent_path = os.path.join(precomputed_latents_dir, f"{index:06d}.pt")
559
+ if not os.path.isfile(latent_path):
560
+ raise FileNotFoundError(
561
+ f"Expected latent file {latent_path} but it does not exist. "
562
+ "Please ensure precomputed latents cover all required indices."
563
+ )
564
+ latent = torch.load(latent_path, map_location=device)
565
+ latents.append(latent)
566
+ latents = torch.stack(latents, dim=0).to(device=device)
567
+ else:
568
+ # Run DiT to generate latents.
569
+ z = torch.randn(n, *latent_size, device=device)
570
+ y = label_sampler(actual_step)
571
+
572
+ model_kwargs = dict(y=y)
573
+ model_fn = model.forward
574
+
575
+ if using_cfg:
576
+ z = torch.cat([z, z], dim=0)
577
+ y_null = torch.full((n,), null_label, device=device)
578
+ y = torch.cat([y, y_null], dim=0)
579
+ model_kwargs = dict(
580
+ y=y,
581
+ cfg_scale=guidance_scale,
582
+ cfg_interval=(t_min, t_max),
583
+ )
584
+ if guidance_method == "autoguidance":
585
+ if guid_model_forward is None:
586
+ raise RuntimeError("Guidance model forward is not initialized.")
587
+ model_kwargs["additional_model_forward"] = guid_model_forward
588
+ model_fn = model.forward_with_autoguidance
589
+ else:
590
+ model_fn = model.forward_with_cfg
591
+ latents = sample_fn(z, model_fn, **model_kwargs)[-1]
592
+ if using_cfg:
593
+ latents, _ = latents.chunk(2, dim=0)
594
+
595
+ # Optionally cache DiT latents for future runs.
596
+ if save_latents and latents_dir is not None:
597
+ for local_idx, latent in enumerate(latents):
598
+ index = local_idx * world_size + rank + total
599
+ latent_path = os.path.join(latents_dir, f"{index:06d}.pt")
600
+ torch.save(latent.to("cpu"), latent_path)
601
+
602
+ # Decode latents into images.
603
+ if use_external_vae:
604
+ # Reverse projection if used
605
+ if proj_down_weight is not None:
606
+ if proj_norm_mode == "per_dim" and proj_norm_mean is not None:
607
+ latents = latents * proj_norm_std + proj_norm_mean
608
+ latents = torch.nn.functional.conv2d(latents, proj_down_weight[:, :, None, None])
609
+ decoded = external_vae.decode(latents / vae_scaling_factor).sample
610
+ decoded = ((decoded + 1) / 2).clamp(0, 1)
611
+ else:
612
+ decoded = pr.decode(latents)
613
+ mid_x_rec = getattr(decoded, 'mid_x_rec', None)
614
+ if hasattr(decoded, 'x_rec'):
615
+ decoded = decoded.x_rec
616
+ decoded = decoded.clamp(0, 1)
617
+ if mid_x_rec is not None:
618
+ mid_decoded = mid_x_rec.clamp(0, 1)
619
+ mid_decoded = (
620
+ mid_decoded.mul(255)
621
+ .permute(0, 2, 3, 1)
622
+ .to("cpu", dtype=torch.uint8)
623
+ .numpy()
624
+ )
625
+ else:
626
+ mid_decoded = None
627
+ decoded = (
628
+ decoded.mul(255)
629
+ .permute(0, 2, 3, 1)
630
+ .to("cpu", dtype=torch.uint8)
631
+ .numpy()
632
+ )
633
+
634
+ for local_idx, sample in enumerate(decoded):
635
+ index = local_idx * world_size + rank + total
636
+ Image.fromarray(sample).save(f"{sample_folder_dir}/{index:06d}.png")
637
+ if mid_decoded is not None:
638
+ mid_dir = sample_folder_dir + "_mid"
639
+ os.makedirs(mid_dir, exist_ok=True)
640
+ Image.fromarray(mid_decoded[local_idx]).save(f"{mid_dir}/{index:06d}.png")
641
+
642
+ total += global_batch_size
643
+ dist.barrier()
644
+
645
+ dist.barrier()
646
+ if rank == 0:
647
+ if not skip_sampling:
648
+ npz_path = create_npz_from_sample_folder(sample_folder_dir, args.num_fid_samples)
649
+ # else: npz_path already set and file exists
650
+
651
+ reference_npz = args.reference_npz
652
+ if reference_npz is None and cfg is not None:
653
+ eval_section = cfg.get("eval", None)
654
+ if eval_section is not None:
655
+ reference_npz = eval_section.get("reference_npz_path", None)
656
+
657
+ if reference_npz is not None and not args.skip_fid:
658
+ logger.info("Computing FID against reference...")
659
+ fid_value = compute_fid_from_samples(
660
+ npz_path,
661
+ reference_npz,
662
+ device,
663
+ batch_size=args.fid_batch_size,
664
+ )
665
+ logger.info("FID: %.6f", fid_value)
666
+
667
+ result_path = result_parent_dir / f"{folder_base}_fid_result.json"
668
+ with open(result_path, "w", encoding="utf-8") as f:
669
+ json.dump(
670
+ {
671
+ "fid": fid_value,
672
+ "reference_npz": reference_npz,
673
+ "num_samples": args.num_fid_samples,
674
+ "sample_folder": sample_folder_dir,
675
+ },
676
+ f,
677
+ indent=2,
678
+ )
679
+ logger.info("FID result saved to %s", result_path)
680
+
681
+ logger.info("Done.")
682
+ dist.barrier()
683
+ dist.destroy_process_group()
684
+
685
+
686
+ if __name__ == "__main__":
687
+ parser = argparse.ArgumentParser()
688
+ parser.add_argument("--config", type=str, required=True, help="Path to the config file.")
689
+ parser.add_argument("--sample-dir", type=str, default="samples")
690
+ parser.add_argument("--per-proc-batch-size", type=int, default=125)
691
+ parser.add_argument("--num-fid-samples", type=int, default=50_000)
692
+ parser.add_argument("--global-seed", type=int, default=0)
693
+ parser.add_argument("--precision", type=str, choices=["fp32", "bf16"], default="fp32")
694
+ parser.add_argument("--tf32", action=argparse.BooleanOptionalAction, default=True,
695
+ help="Enable TF32 matmuls (Ampere+). Disable if deterministic results are required.")
696
+ parser.add_argument(
697
+ "--label-sampling",
698
+ type=str,
699
+ choices=["random", "equal", "dataset"],
700
+ default="equal",
701
+ help="Choose how to sample class labels. Use 'dataset' to match training eval (val order).",
702
+ )
703
+ parser.add_argument(
704
+ "--eval-data",
705
+ type=str,
706
+ default=None,
707
+ help="Path to eval ImageFolder (val dir). Required for --label-sampling dataset. Overrides config eval.data_path.",
708
+ )
709
+ parser.add_argument(
710
+ "--image-size",
711
+ type=int,
712
+ default=256,
713
+ help="Image size for center crop when using dataset mode.",
714
+ )
715
+ parser.add_argument(
716
+ "--reference-npz",
717
+ type=str,
718
+ default=None,
719
+ help="Path to reference NPZ for FID computation. Can be precomputed stats (mu/sigma) or raw images (arr_0). "
720
+ "Overrides config eval.reference_npz_path if set.",
721
+ )
722
+ parser.add_argument(
723
+ "--fid-batch-size",
724
+ type=int,
725
+ default=128,
726
+ help="Batch size for FID computation.",
727
+ )
728
+ parser.add_argument(
729
+ "--skip-fid",
730
+ action="store_true",
731
+ help="Generate PNG/NPZ artifacts without computing FID in this process.",
732
+ )
733
+ parser.add_argument(
734
+ "--ckpt",
735
+ type=str,
736
+ default=None,
737
+ help="Override stage_2.ckpt in config with this checkpoint path.",
738
+ )
739
+ parser.add_argument(
740
+ "--cfg-scale",
741
+ type=float,
742
+ default=None,
743
+ help="Override guidance.scale in config.",
744
+ )
745
+ parser.add_argument(
746
+ "--precomputed-latents-dir",
747
+ type=str,
748
+ default=None,
749
+ help="Override misc.precomputed_latents_dir in config. Pass 'none' to disable precomputed latents.",
750
+ )
751
+
752
+ args = parser.parse_args()
753
+ main(args)
patch-reparameterization/src/stage1/encoders/__init__.py CHANGED
@@ -34,7 +34,12 @@ def register_encoder(cls: Optional[Type] = None, *, name: Optional[str] = None)
34
  # Only encoders used by released configs are included and imported.
35
  from . import siglip2
36
  from . import siglip2_unified
 
 
 
 
37
  try:
38
  from . import qwen3_unified
 
39
  except ImportError:
40
  pass
 
34
  # Only encoders used by released configs are included and imported.
35
  from . import siglip2
36
  from . import siglip2_unified
37
+ try:
38
+ from . import dinov2_unified
39
+ except ImportError:
40
+ pass
41
  try:
42
  from . import qwen3_unified
43
+ from . import qwen3_frozen
44
  except ImportError:
45
  pass
patch-reparameterization/src/stage1/encoders/dinov2_unified.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import logging
3
+
4
+ import torch
5
+ from torch import nn
6
+ from transformers import Dinov2WithRegistersModel
7
+
8
+ from . import register_encoder
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @register_encoder()
14
+ class DINOv2Unified(nn.Module):
15
+ """Single-backbone DINOv2-with-registers encoder with two independent embeddings.
16
+
17
+ Architecture:
18
+ image -> sem_embeddings -> [B, 1+R+N, C] --|
19
+ |--> cat [2B, ...] -> encoder -> split
20
+ image -> recon_embeddings -> [B, 1+R+N, C] --|
21
+
22
+ sem_out (patch tokens only) -> sem_ln -> [B, N, C]
23
+ recon_out (patch tokens only) -> recon_proj -> recon_ln -> [B, N, recon_proj_dim]
24
+
25
+ output: cat([sem, recon], dim=-1) [B, N, C + recon_proj_dim]
26
+
27
+ R = num_register_tokens (default 4), N = num_patches (default 256).
28
+ CLS and register tokens are discarded after the encoder.
29
+ """
30
+
31
+ def __init__(self, model_name: str, recon_proj_dim: int = 256, **kwargs):
32
+ super().__init__()
33
+
34
+ backbone = Dinov2WithRegistersModel.from_pretrained(model_name)
35
+ self.hidden_size = backbone.config.hidden_size
36
+ self.patch_size = backbone.config.patch_size
37
+ self.num_register_tokens = backbone.config.num_register_tokens
38
+ self.recon_proj_dim = recon_proj_dim
39
+
40
+ # Two independent embeddings (patch_embeddings + cls_token + register_tokens + position_embeddings)
41
+ self.sem_embeddings = copy.deepcopy(backbone.embeddings)
42
+ self.recon_embeddings = copy.deepcopy(backbone.embeddings)
43
+ # mask_token is only used in MAE pre-training; remove to avoid DDP unused-parameter error
44
+ del self.recon_embeddings.mask_token
45
+ self.recon_embeddings.mask_token = None
46
+
47
+ # Shared encoder + layernorm
48
+ self.encoder = backbone.encoder
49
+ self.layernorm = backbone.layernorm
50
+
51
+ # recon projection + LN
52
+ self.recon_proj = nn.Linear(self.hidden_size, recon_proj_dim, bias=True)
53
+ nn.init.xavier_uniform_(self.recon_proj.weight)
54
+ nn.init.zeros_(self.recon_proj.bias)
55
+
56
+ self.sem_ln = nn.LayerNorm(self.hidden_size, elementwise_affine=False)
57
+ self.recon_ln = nn.LayerNorm(recon_proj_dim, elementwise_affine=False)
58
+
59
+ self.output_dim = self.hidden_size + recon_proj_dim
60
+ self.recon_hidden_size = self.output_dim
61
+
62
+ # skip tokens: 1 CLS + num_register_tokens
63
+ self._skip = 1 + self.num_register_tokens
64
+
65
+ logger.info(
66
+ f"DINOv2Unified: hidden_size={self.hidden_size}, "
67
+ f"recon_proj_dim={recon_proj_dim}, output_dim={self.output_dim}, "
68
+ f"skip_tokens={self._skip}"
69
+ )
70
+
71
+ def forward(self, images: torch.Tensor, return_aux: bool = False, **kwargs):
72
+ B = images.shape[0]
73
+
74
+ sem_embeds = self.sem_embeddings(images) # [B, 1+R+N, C]
75
+ recon_embeds = self.recon_embeddings(images) # [B, 1+R+N, C]
76
+
77
+ combined = torch.cat([sem_embeds, recon_embeds], dim=0) # [2B, 1+R+N, C]
78
+ hidden = self.encoder(combined).last_hidden_state # [2B, 1+R+N, C]
79
+ hidden = self.layernorm(hidden)
80
+
81
+ # discard CLS + register tokens
82
+ sem_out = hidden[:B, self._skip:] # [B, N, C]
83
+ recon_out = hidden[B:, self._skip:] # [B, N, C]
84
+
85
+ sem_tokens = self.sem_ln(sem_out)
86
+ recon_tokens = self.recon_ln(self.recon_proj(recon_out))
87
+
88
+ merged = torch.cat([sem_tokens, recon_tokens], dim=-1)
89
+
90
+ if not return_aux:
91
+ return merged
92
+
93
+ return merged, {
94
+ "sementic_tokens": sem_tokens,
95
+ "recon_tokens": recon_tokens,
96
+ "merged_tokens": merged,
97
+ }
98
+
99
+ def set_training_mode(self, mode: str = "full", **kwargs):
100
+ self.requires_grad_(False)
101
+ if mode == "full":
102
+ self.requires_grad_(True)
103
+ elif mode == "merger_recon_patchemb":
104
+ for p in self.recon_embeddings.parameters():
105
+ p.requires_grad = True
106
+ for p in self.recon_proj.parameters():
107
+ p.requires_grad = True
108
+ elif mode == "frozen":
109
+ pass
110
+ else:
111
+ raise ValueError(f"DINOv2Unified: unknown training mode '{mode}'")
112
+
113
+ trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
114
+ total = sum(p.numel() for p in self.parameters())
115
+ logger.info(
116
+ f"DINOv2Unified set_training_mode='{mode}': "
117
+ f"{trainable/1e6:.2f}M / {total/1e6:.2f}M trainable"
118
+ )
119
+
120
+ def get_last_layer(self) -> nn.Parameter:
121
+ return self.recon_proj.weight
122
+
123
+ def get_recon_last_layer(self) -> nn.Parameter:
124
+ return self.recon_proj.weight
patch-reparameterization/src/stage1/encoders/qwen3_frozen.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Qwen3Frozen: 完全冻结的 Qwen3-VL vision backbone,只做 semantic encoding。
3
+ 继承 Qwen3Unified 的所有辅助方法(RoPE、pos_embed、block_order),
4
+ 去掉 recon 分支,输出 [total_tokens, 1152] 直接送给 decoder。
5
+ 只训 decoder,encoder 完全不更新。
6
+ """
7
+
8
+ import copy
9
+ import logging
10
+
11
+ import torch
12
+ from torch import nn
13
+
14
+ from . import register_encoder
15
+ from .qwen3_unified import Qwen3Unified
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @register_encoder()
21
+ class Qwen3Frozen(Qwen3Unified):
22
+ """
23
+ 完全冻结的单路 Qwen3-VIT encoder(无 recon 分支)。
24
+ 继承 Qwen3Unified 的所有辅助方法,去掉双路设计。
25
+ 输出:[total_tokens, hidden_size=1152]
26
+ """
27
+
28
+ def __init__(self, model_name: str, **kwargs):
29
+ # 不调用 Qwen3Unified.__init__,自己初始化
30
+ nn.Module.__init__(self)
31
+
32
+ from transformers.models.qwen3_vl.modeling_qwen3_vl import (
33
+ Qwen3VLForConditionalGeneration,
34
+ )
35
+ from transformers import AutoConfig
36
+
37
+ full_cfg = AutoConfig.from_pretrained(model_name)
38
+ vis_cfg = full_cfg.vision_config
39
+
40
+ full_model = Qwen3VLForConditionalGeneration.from_pretrained(
41
+ model_name,
42
+ torch_dtype=torch.float32,
43
+ device_map="cpu",
44
+ )
45
+ vis_model = full_model.visual
46
+ del full_model.model
47
+
48
+ self.hidden_size = vis_cfg.hidden_size
49
+ self.patch_size = vis_cfg.patch_size
50
+ self.temporal_patch_size = vis_cfg.temporal_patch_size
51
+ self.spatial_merge_size = vis_cfg.spatial_merge_size
52
+ self.recon_proj_dim = 0 # no recon branch
53
+
54
+ # single patch embed + pos embed (no deepcopy needed, frozen anyway)
55
+ self.sem_patch_embed = vis_model.patch_embed
56
+ self.sem_pos_embed = vis_model.pos_embed
57
+ self.num_grid_per_side = vis_model.num_grid_per_side
58
+ self.rotary_pos_emb = vis_model.rotary_pos_emb
59
+ self.blocks = vis_model.blocks
60
+
61
+ self.output_dim = self.hidden_size
62
+ self.recon_hidden_size = self.hidden_size # decoder latent_dim
63
+
64
+ # PatchReparam reads these to skip internal resize/normalize
65
+ self.handles_preprocessing = True
66
+ self.encoder_mean = torch.tensor([0.5, 0.5, 0.5]).view(1, 3, 1, 1)
67
+ self.encoder_std = torch.tensor([0.5, 0.5, 0.5]).view(1, 3, 1, 1)
68
+ self.encoder_input_size = 256
69
+
70
+ # caches (inherited methods use these)
71
+ self._pos_embed_cache: dict = {}
72
+ self._rot_pos_emb_cache: dict = {}
73
+ self._block_idx_cache: dict = {}
74
+
75
+ # 完全冻结
76
+ self.requires_grad_(False)
77
+
78
+ del vis_model
79
+
80
+ logger.info(f"Qwen3Frozen: hidden_size={self.hidden_size}, fully frozen")
81
+
82
+ def forward(
83
+ self,
84
+ pixel_values: torch.Tensor,
85
+ grid_thw: torch.Tensor,
86
+ return_aux: bool = False,
87
+ **kwargs,
88
+ ):
89
+ # patch embed + pos embed (single path)
90
+ x = self.sem_patch_embed(pixel_values)
91
+ pos = self._abs_pos_embed(self.sem_pos_embed, grid_thw)
92
+
93
+ # reorder to 2x2 block order
94
+ perm = self._block_order_indices(grid_thw, device=x.device)
95
+ inv_perm = torch.empty_like(perm)
96
+ inv_perm[perm] = torch.arange(perm.numel(), device=perm.device)
97
+ x = (x + pos)[perm]
98
+
99
+ # RoPE
100
+ rotary = self._rot_pos_emb(grid_thw)
101
+ emb = torch.cat((rotary, rotary), dim=-1)
102
+ position_embeddings = (emb.cos(), emb.sin())
103
+
104
+ # cu_seqlens
105
+ cu_seqlens = torch.repeat_interleave(
106
+ grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]
107
+ ).cumsum(0, dtype=torch.int32)
108
+ cu_seqlens = torch.nn.functional.pad(cu_seqlens, (1, 0), value=0)
109
+
110
+ # blocks forward
111
+ for blk in self.blocks:
112
+ x = blk(x, cu_seqlens=cu_seqlens, position_embeddings=position_embeddings)
113
+
114
+ # restore row-major
115
+ x = x[inv_perm]
116
+
117
+ if not return_aux:
118
+ return x
119
+
120
+ return x, {"sementic_tokens": x, "merged_tokens": x}
121
+
122
+ def set_training_mode(self, mode: str = "frozen", **kwargs):
123
+ self.requires_grad_(False)
124
+ logger.info(f"Qwen3Frozen: always frozen, ignoring mode='{mode}'")
125
+
126
+ def get_last_layer(self):
127
+ return None
128
+
129
+ def get_recon_last_layer(self):
130
+ return None
patch-reparameterization/src/stage1/encoders/qwen3_unified.py CHANGED
@@ -67,16 +67,22 @@ class Qwen3Unified(nn.Module):
67
  ):
68
  super().__init__()
69
 
70
- # Load vision backbone config only (no LLM weights download needed —
71
- # the pr-qwen-vit-tokenizer checkpoint provides all trained weights)
72
  from transformers import AutoConfig
73
- from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLVisionModel
74
 
75
  full_cfg = AutoConfig.from_pretrained(model_name)
76
  vis_cfg: Qwen3VLVisionConfig = full_cfg.vision_config
77
 
78
- # Instantiate vision model from config only (random init — weights come from checkpoint)
79
- vis_model = Qwen3VLVisionModel(vis_cfg).to(torch.float32)
 
 
 
 
 
 
 
80
 
81
  self.hidden_size = vis_cfg.hidden_size # 1152
82
  self.patch_size = vis_cfg.patch_size # 16
 
67
  ):
68
  super().__init__()
69
 
70
+ # Load full Qwen3VL model to extract the vision backbone, then discard the LLM
 
71
  from transformers import AutoConfig
72
+ from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLForConditionalGeneration
73
 
74
  full_cfg = AutoConfig.from_pretrained(model_name)
75
  vis_cfg: Qwen3VLVisionConfig = full_cfg.vision_config
76
 
77
+ full_model = Qwen3VLForConditionalGeneration.from_pretrained(
78
+ model_name,
79
+ torch_dtype=torch.float32,
80
+ device_map=f"cuda:{torch.cuda.current_device()}" if torch.cuda.is_available() else "cpu",
81
+ )
82
+ vis_model = full_model.visual
83
+ full_model.model = None # free LLM weights
84
+ del full_model
85
+ torch.cuda.empty_cache()
86
 
87
  self.hidden_size = vis_cfg.hidden_size # 1152
88
  self.patch_size = vis_cfg.patch_size # 16
patch-reparameterization/src/stage2/__init__.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .models.lightningDiT import LightningDiT
2
+ from .models.DDT import DiTwDDTHead
3
+ from typing import Callable, Dict, Optional, Type, Union
4
+ from typing import Protocol, Any, runtime_checkable
5
+
6
+ @runtime_checkable
7
+ class Stage2ModelProtocol(Protocol):
8
+ def forward(self, *args: Any, **kwargs: Any) -> Any: ...
9
+ def forward_with_cfg(self, *args: Any, **kwargs: Any) -> Any: ...
10
+ def forward_with_autoguidance(self, *args: Any, **kwargs: Any) -> Any: ...
11
+
12
+
13
+
14
+ # STAGE2_ARCHS: Dict[str, Callable] = {}
15
+ # __all__ = ["STAGE2_ARCHS", "register_stage2"]
16
+
17
+
18
+ # def _add_to_registry(name: str, func: Callable) -> Callable:
19
+ # if name in STAGE2_ARCHS and STAGE2_ARCHS[name] is not func:
20
+ # raise ValueError(f"Stage2 func: '{name}' is already registered.")
21
+ # STAGE2_ARCHS[name] = func
22
+ # return func
23
+
24
+
25
+ # def register_stage2(func: Optional[Callable] = None, *, name: Optional[str] = None) -> Union[Callable[[Callable], Callable], Callable]:
26
+ # """Register a function in ``STAGE2_ARCHS``.
27
+
28
+ # Can be used either as ``@register_stage2()`` (optionally passing ``name``) or
29
+ # via ``register_stage2(my_function)`` after the function definition.
30
+ # """
31
+
32
+ # def decorator(inner_func: Callable) -> Callable:
33
+ # func_name = name or inner_func.__name__
34
+ # return _add_to_registry(func_name, inner_func)
35
+
36
+ # if func is None:
37
+ # return decorator
38
+
39
+ # return decorator(func)
40
+
41
+ # @register_stage2()
42
+ # def DiTXL(token_dim: int, input_size: int) -> LightningDiT:
43
+ # model = LightningDiT(
44
+ # input_size=input_size,
45
+ # patch_size=1,
46
+ # in_channels=token_dim,
47
+ # hidden_size=1152,
48
+ # depth=28,
49
+ # num_heads=16,
50
+ # mlp_ratio=4.0,
51
+ # class_dropout_prob=0.1,
52
+ # num_classes=1000,
53
+ # )
54
+ # return model
55
+
56
+ # @register_stage2()
57
+ # def DDTXL(token_dim: int, input_size: int) -> DiTwDDTHead:
58
+ # model = DiTwDDTHead(
59
+ # input_size=input_size,
60
+ # patch_size=1,
61
+ # in_channels=token_dim,
62
+ # hidden_size=[1152, 2048],
63
+ # depth=[28, 2],
64
+ # num_heads=[16, 16],
65
+ # mlp_ratio=4.0,
66
+ # class_dropout_prob=0.1,
67
+ # num_classes=1000,
68
+ # )
69
+ # return model
70
+
71
+ # @register_stage2()
72
+ # def DDTS(token_dim: int, input_size: int) -> DiTwDDTHead:
73
+ # model = DiTwDDTHead(
74
+ # input_size=input_size,
75
+ # patch_size=1,
76
+ # in_channels=token_dim,
77
+ # hidden_size=[384, 2048],
78
+ # depth=[12, 2],
79
+ # num_heads=[6, 16],
80
+ # mlp_ratio=4.0,
81
+ # class_dropout_prob=0.1,
82
+ # num_classes=1000,
83
+ # )
84
+ # return model
85
+
86
+
87
+
88
+
patch-reparameterization/src/stage2/models/DDT.py ADDED
@@ -0,0 +1,919 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from math import sqrt
3
+ from re import L
4
+ from regex import B
5
+ import torch
6
+ import torch.nn as nn
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ from transformers import SwinModel
11
+ import torch
12
+ from torch import nn
13
+ from .lightningDiT import PatchEmbed, Mlp, NormAttention
14
+ from timm.models.vision_transformer import PatchEmbed, Mlp
15
+ from .model_utils import VisionRotaryEmbeddingFast, RMSNorm, SwiGLUFFN, GaussianFourierEmbedding, LabelEmbedder, NormAttention, get_2d_sincos_pos_embed
16
+ import torch.nn.functional as F
17
+ from typing import *
18
+
19
+
20
+ def DDTModulate(x: torch.Tensor, shift: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
21
+ """
22
+ Applies per-segment modulation to x.
23
+
24
+ Args:
25
+ x: Tensor of shape (B, L_x, D)
26
+ shift: Tensor of shape (B, L, D)
27
+ scale: Tensor of shape (B, L, D)
28
+ Returns:
29
+ Tensor of shape (B, L_x, D): x * (1 + scale) + shift,
30
+ with shift and scale repeated to match L_x if necessary.
31
+ """
32
+ B, Lx, D = x.shape
33
+ _, L, _ = shift.shape
34
+ if Lx % L != 0:
35
+ raise ValueError(f"L_x ({Lx}) must be divisible by L ({L})")
36
+ repeat = Lx // L
37
+ if repeat != 1:
38
+ # repeat each of the L segments 'repeat' times along the length dim
39
+ shift = shift.repeat_interleave(repeat, dim=1)
40
+ scale = scale.repeat_interleave(repeat, dim=1)
41
+ # apply modulation
42
+ return x * (1 + scale) + shift
43
+
44
+
45
+ def DDTGate(x: torch.Tensor, gate: torch.Tensor) -> torch.Tensor:
46
+ """
47
+ Applies per-segment modulation to x.
48
+
49
+ Args:
50
+ x: Tensor of shape (B, L_x, D)
51
+ gate: Tensor of shape (B, L, D)
52
+ Returns:
53
+ Tensor of shape (B, L_x, D): x * gate,
54
+ with gate repeated to match L_x if necessary.
55
+ """
56
+ B, Lx, D = x.shape
57
+ _, L, _ = gate.shape
58
+ if Lx % L != 0:
59
+ raise ValueError(f"L_x ({Lx}) must be divisible by L ({L})")
60
+ repeat = Lx // L
61
+ if repeat != 1:
62
+ # repeat each of the L segments 'repeat' times along the length dim
63
+ # print(f"gate shape: {gate.shape}, x shape: {x.shape}")
64
+ gate = gate.repeat_interleave(repeat, dim=1)
65
+ # apply modulation
66
+ return x * gate
67
+
68
+
69
+ class LightningDDTBlock(nn.Module):
70
+ """
71
+ Lightning DiT Block. We add features including:
72
+ - ROPE
73
+ - QKNorm
74
+ - RMSNorm
75
+ - SwiGLU
76
+ - No shift AdaLN.
77
+ Not all of them are used in the final model, please refer to the paper for more details.
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ hidden_size,
83
+ num_heads,
84
+ mlp_ratio=4.0,
85
+ use_qknorm=False,
86
+ use_swiglu=True,
87
+ use_rmsnorm=True,
88
+ wo_shift=False,
89
+ **block_kwargs
90
+ ):
91
+ super().__init__()
92
+
93
+ # Initialize normalization layers
94
+ if not use_rmsnorm:
95
+ self.norm1 = nn.LayerNorm(
96
+ hidden_size, elementwise_affine=False, eps=1e-6)
97
+ self.norm2 = nn.LayerNorm(
98
+ hidden_size, elementwise_affine=False, eps=1e-6)
99
+ else:
100
+ self.norm1 = RMSNorm(hidden_size)
101
+ self.norm2 = RMSNorm(hidden_size)
102
+
103
+ # Initialize attention layer
104
+ self.attn = NormAttention(
105
+ hidden_size,
106
+ num_heads=num_heads,
107
+ qkv_bias=True,
108
+ qk_norm=use_qknorm,
109
+ use_rmsnorm=use_rmsnorm,
110
+ **block_kwargs
111
+ )
112
+
113
+ # Initialize MLP layer
114
+ mlp_hidden_dim = int(hidden_size * mlp_ratio)
115
+ def approx_gelu(): return nn.GELU(approximate="tanh")
116
+ if use_swiglu:
117
+ # here we did not use SwiGLU from xformers because it is not compatible with torch.compile for now.
118
+ self.mlp = SwiGLUFFN(hidden_size, int(2/3 * mlp_hidden_dim))
119
+ else:
120
+ self.mlp = Mlp(
121
+ in_features=hidden_size,
122
+ hidden_features=mlp_hidden_dim,
123
+ act_layer=approx_gelu,
124
+ drop=0
125
+ )
126
+
127
+ # Initialize AdaLN modulation
128
+ if wo_shift:
129
+ self.adaLN_modulation = nn.Sequential(
130
+ nn.SiLU(),
131
+ nn.Linear(hidden_size, 4 * hidden_size, bias=True)
132
+ )
133
+ else:
134
+ self.adaLN_modulation = nn.Sequential(
135
+ nn.SiLU(),
136
+ nn.Linear(hidden_size, 6 * hidden_size, bias=True)
137
+ )
138
+ self.wo_shift = wo_shift
139
+
140
+ def forward(self, x, c, feat_rope=None):
141
+ if len(c.shape) < len(x.shape):
142
+ c = c.unsqueeze(1) # (B, 1, C)
143
+ if self.wo_shift:
144
+ scale_msa, gate_msa, scale_mlp, gate_mlp = self.adaLN_modulation(
145
+ c).chunk(4, dim=-1)
146
+ shift_msa = None
147
+ shift_mlp = None
148
+ else:
149
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.adaLN_modulation(
150
+ c).chunk(6, dim=-1)
151
+ x = x + DDTGate(self.attn(DDTModulate(self.norm1(x),
152
+ shift_msa, scale_msa), rope=feat_rope), gate_msa)
153
+ x = x + DDTGate(self.mlp(DDTModulate(self.norm2(x),
154
+ shift_mlp, scale_mlp)), gate_mlp)
155
+ return x
156
+
157
+
158
+ class DDTFinalLayer(nn.Module):
159
+ """
160
+ The final layer of DDT.
161
+ """
162
+
163
+ def __init__(self, hidden_size, patch_size, out_channels, use_rmsnorm=False):
164
+ super().__init__()
165
+ if not use_rmsnorm:
166
+ self.norm_final = nn.LayerNorm(
167
+ hidden_size, elementwise_affine=False, eps=1e-6)
168
+ else:
169
+ self.norm_final = RMSNorm(hidden_size)
170
+ self.linear = nn.Linear(
171
+ hidden_size, patch_size * patch_size * out_channels, bias=True)
172
+ self.adaLN_modulation = nn.Sequential(
173
+ nn.SiLU(),
174
+ nn.Linear(hidden_size, 2 * hidden_size, bias=True)
175
+ )
176
+
177
+ def forward(self, x, c):
178
+ if len(c.shape) < len(x.shape):
179
+ c = c.unsqueeze(1)
180
+ shift, scale = self.adaLN_modulation(c).chunk(2, dim=-1)
181
+ x = DDTModulate(self.norm_final(x), shift, scale) # no gate
182
+ x = self.linear(x)
183
+ return x
184
+
185
+
186
+ class DiTwDDTHead(nn.Module):
187
+ def __init__(
188
+ self,
189
+ input_size: int = 1,
190
+ patch_size: Union[list, int] = 1,
191
+ in_channels: int = 768,
192
+ hidden_size=[1152, 2048],
193
+ depth=[28, 2],
194
+ num_heads: Union[list[int], int] = [16, 16],
195
+ mlp_ratio=4.0,
196
+ class_dropout_prob=0.1,
197
+ num_classes=1000,
198
+ use_qknorm=False,
199
+ use_swiglu=True,
200
+ use_rope=True,
201
+ use_rmsnorm=True,
202
+ wo_shift=False,
203
+ use_pos_embed: bool = True,
204
+ ):
205
+ super().__init__()
206
+ self.in_channels = in_channels
207
+ self.out_channels = in_channels
208
+
209
+ self.encoder_hidden_size = hidden_size[0]
210
+ self.decoder_hidden_size = hidden_size[1]
211
+ self.num_heads = [num_heads, num_heads] if isinstance(
212
+ num_heads, int) else list(num_heads)
213
+ self.num_decoder_blocks = depth[1]
214
+ self.num_encoder_blocks = depth[0]
215
+ self.num_blocks = depth[0] + depth[1]
216
+ self.use_rope = use_rope
217
+ # analyze patch size
218
+ if isinstance(patch_size, int) or isinstance(patch_size, float):
219
+ patch_size = [patch_size, patch_size] # patch size for s , x embed
220
+ assert len(
221
+ patch_size) == 2, f"patch size should be a list of two numbers, but got {patch_size}"
222
+ self.patch_size = patch_size
223
+ self.s_patch_size = patch_size[0]
224
+ self.x_patch_size = patch_size[1]
225
+ s_channel_per_token = in_channels * self.s_patch_size * self.s_patch_size
226
+ s_input_size = input_size
227
+ s_patch_size = self.s_patch_size
228
+ x_input_size = input_size
229
+ x_patch_size = self.x_patch_size
230
+ x_channel_per_token = in_channels * self.x_patch_size * self.x_patch_size
231
+ self.x_embedder = PatchEmbed(
232
+ x_input_size, x_patch_size, x_channel_per_token, self.decoder_hidden_size, bias=True)
233
+ self.s_embedder = PatchEmbed(
234
+ s_input_size, s_patch_size, s_channel_per_token, self.encoder_hidden_size, bias=True)
235
+ self.s_channel_per_token = s_channel_per_token
236
+ self.x_channel_per_token = x_channel_per_token
237
+ self.s_projector = nn.Linear(
238
+ self.encoder_hidden_size, self.decoder_hidden_size) if self.encoder_hidden_size != self.decoder_hidden_size else nn.Identity()
239
+ self.t_embedder = GaussianFourierEmbedding(self.encoder_hidden_size)
240
+ self.y_embedder = LabelEmbedder(
241
+ num_classes, self.encoder_hidden_size, class_dropout_prob)
242
+ # print(f"x_channel_per_token: {x_channel_per_token}, s_channel_per_token: {s_channel_per_token}")
243
+ self.final_layer = DDTFinalLayer(
244
+ self.decoder_hidden_size, 1, x_channel_per_token, use_rmsnorm=use_rmsnorm)
245
+ # Will use fixed sin-cos embedding:
246
+ if use_pos_embed:
247
+ num_patches = self.s_embedder.num_patches
248
+ self.pos_embed = nn.Parameter(torch.zeros(
249
+ 1, num_patches, self.encoder_hidden_size), requires_grad=False)
250
+ self.x_pos_embed = None
251
+ self.use_pos_embed = use_pos_embed
252
+ enc_num_heads = self.num_heads[0]
253
+ dec_num_heads = self.num_heads[1]
254
+ # use rotary position encoding, borrow from EVA
255
+ if self.use_rope:
256
+ enc_half_head_dim = self.encoder_hidden_size // enc_num_heads // 2
257
+ hw_seq_len = int(sqrt(self.s_embedder.num_patches))
258
+ # print(f"enc_half_head_dim: {enc_half_head_dim}, hw_seq_len: {hw_seq_len}")
259
+ self.enc_feat_rope = VisionRotaryEmbeddingFast(
260
+ dim=enc_half_head_dim,
261
+ pt_seq_len=hw_seq_len,
262
+ )
263
+ dec_half_head_dim = self.decoder_hidden_size // dec_num_heads // 2
264
+ hw_seq_len = int(sqrt(self.x_embedder.num_patches))
265
+ # print(f"dec_half_head_dim: {dec_half_head_dim}, hw_seq_len: {hw_seq_len}")
266
+ self.dec_feat_rope = VisionRotaryEmbeddingFast(
267
+ dim=dec_half_head_dim,
268
+ pt_seq_len=hw_seq_len,
269
+ )
270
+ else:
271
+ self.feat_rope = None
272
+ self.blocks = nn.ModuleList([
273
+ LightningDDTBlock(self.encoder_hidden_size if i < self.num_encoder_blocks else self.decoder_hidden_size,
274
+ enc_num_heads if i < self.num_encoder_blocks else dec_num_heads,
275
+ mlp_ratio=mlp_ratio,
276
+ use_qknorm=use_qknorm,
277
+ use_rmsnorm=use_rmsnorm,
278
+ use_swiglu=use_swiglu,
279
+ wo_shift=wo_shift,
280
+ ) for i in range(self.num_blocks)
281
+ ])
282
+ self.initialize_weights()
283
+
284
+ def initialize_weights(self, xavier_uniform_init: bool = False):
285
+ if xavier_uniform_init:
286
+ def _basic_init(module):
287
+ if isinstance(module, nn.Linear):
288
+ torch.nn.init.xavier_uniform_(module.weight)
289
+ if module.bias is not None:
290
+ nn.init.constant_(module.bias, 0)
291
+ self.apply(_basic_init)
292
+ # Initialize patch_embed like nn.Linear (instead of nn.Conv2d):
293
+ w = self.x_embedder.proj.weight.data
294
+ nn.init.xavier_uniform_(w.view([w.shape[0], -1]))
295
+ nn.init.constant_(self.x_embedder.proj.bias, 0)
296
+
297
+ # Initialize patch_embed like nn.Linear (instead of nn.Conv2d):
298
+ w = self.s_embedder.proj.weight.data
299
+ nn.init.xavier_uniform_(w.view([w.shape[0], -1]))
300
+ nn.init.constant_(self.s_embedder.proj.bias, 0)
301
+
302
+ # Initialize label embedding table:
303
+ nn.init.normal_(self.y_embedder.embedding_table.weight, std=0.02)
304
+ if self.use_pos_embed:
305
+ # Initialize (and freeze) pos_embed by sin-cos embedding:
306
+ pos_embed = get_2d_sincos_pos_embed(
307
+ self.pos_embed.shape[-1], int(self.s_embedder.num_patches ** 0.5))
308
+ self.pos_embed.data.copy_(
309
+ torch.from_numpy(pos_embed).float().unsqueeze(0))
310
+
311
+ # Zero-out adaLN modulation layers in LightningDiT blocks:
312
+ for block in self.blocks:
313
+ nn.init.constant_(block.adaLN_modulation[-1].weight, 0)
314
+ nn.init.constant_(block.adaLN_modulation[-1].bias, 0)
315
+
316
+ # Initialize timestep embedding MLP:
317
+ nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02)
318
+ nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02)
319
+
320
+ # Zero-out output layers:
321
+ nn.init.constant_(self.final_layer.adaLN_modulation[-1].weight, 0)
322
+ nn.init.constant_(self.final_layer.adaLN_modulation[-1].bias, 0)
323
+ nn.init.constant_(self.final_layer.linear.weight, 0)
324
+ nn.init.constant_(self.final_layer.linear.bias, 0)
325
+
326
+ def unpatchify(self, x):
327
+ """
328
+ x: (N, T, patch_size**2 * C)
329
+ imgs: (N, H, W, C)
330
+ """
331
+ # c = self.out_channels
332
+ c = self.x_channel_per_token
333
+ p = self.x_embedder.patch_size[0]
334
+ h = w = int(x.shape[1] ** 0.5)
335
+ assert h * w == x.shape[1]
336
+
337
+ x = x.reshape(shape=(x.shape[0], h, w, p, p, c))
338
+ x = torch.einsum('nhwpqc->nchpwq', x)
339
+ imgs = x.reshape(shape=(x.shape[0], c, h * p, h * p))
340
+ return imgs
341
+
342
+ def encoder_forward(self, x, t, y):
343
+ """Encoder part — safe under bf16 autocast, can be torch.compiled."""
344
+ t = self.t_embedder(t)
345
+ y = self.y_embedder(y, self.training)
346
+ c = nn.functional.silu(t + y)
347
+ s = self.s_embedder(x)
348
+ if self.use_pos_embed:
349
+ s = s + self.pos_embed
350
+ for i in range(self.num_encoder_blocks):
351
+ s = self.blocks[i](s, c, feat_rope=self.enc_feat_rope)
352
+ t = t.unsqueeze(1).repeat(1, s.shape[1], 1)
353
+ s = nn.functional.silu(t + s)
354
+ return s
355
+
356
+ def decoder_forward(self, x, s):
357
+ """Decoder part — runs in fp32 to prevent bf16 accumulation errors in DDT modulation."""
358
+ with torch.cuda.amp.autocast(enabled=False):
359
+ s = self.s_projector(s.float())
360
+ x = self.x_embedder(x.float())
361
+ if self.use_pos_embed and self.x_pos_embed is not None:
362
+ x = x + self.x_pos_embed
363
+ for i in range(self.num_encoder_blocks, self.num_blocks):
364
+ x = self.blocks[i](x, s, feat_rope=self.dec_feat_rope)
365
+ x = self.final_layer(x, s)
366
+ x = self.unpatchify(x)
367
+ return x
368
+
369
+ def forward(self, x, t, y, s=None, mask=None):
370
+ if s is None:
371
+ s = self.encoder_forward(x, t, y)
372
+ return self.decoder_forward(x, s)
373
+
374
+ def forward_with_cfg(self, x, t, y, cfg_scale, cfg_interval=(0, 1)):
375
+ """
376
+ Forward pass of LightningDiT, but also batches the unconditional forward pass for classifier-free guidance.
377
+ """
378
+ # https://github.com/openai/glide-text2im/blob/main/notebooks/text2im.ipynb
379
+ half = x[: len(x) // 2]
380
+ combined = torch.cat([half, half], dim=0)
381
+ model_out = self.forward(combined, t, y)
382
+ # For exact reproducibility reasons, we apply classifier-free guidance on only
383
+ # three channels by default. The standard approach to cfg applies it to all channels.
384
+ # This can be done by uncommenting the following line and commenting-out the line following that.
385
+ # eps, rest = model_out[:, :self.in_channels], model_out[:, self.in_channels:]
386
+ eps, rest = model_out[:,
387
+ :self.in_channels], model_out[:, self.in_channels:]
388
+ # eps, rest = model_out[:, :3], model_out[:, 3:]
389
+ cond_eps, uncond_eps = torch.split(eps, len(eps) // 2, dim=0)
390
+ guid_t_min, guid_t_max = cfg_interval
391
+ assert guid_t_min < guid_t_max, "cfg_interval should be (min, max) with min < max"
392
+ t = t[: len(t) // 2] # get t for the conditional half
393
+ half_eps = torch.where(
394
+ ((t >= guid_t_min) & (t <= guid_t_max)
395
+ ).view(-1, *[1] * (len(cond_eps.shape) - 1)),
396
+ uncond_eps + cfg_scale * (cond_eps - uncond_eps), cond_eps
397
+ )
398
+ eps = torch.cat([half_eps, half_eps], dim=0)
399
+ return torch.cat([eps, rest], dim=1)
400
+
401
+ def forward_with_autoguidance(self, x, t, y, cfg_scale, additional_model_forward, cfg_interval=(0, 1)):
402
+ """
403
+ Forward pass of LightningDiT, but also contain the forward pass for the additional model
404
+ """
405
+ model_out = self.forward(x, t, y)
406
+ ag_model_out = additional_model_forward(x, t, y)
407
+ eps = model_out[:, :self.in_channels]
408
+ ag_eps = ag_model_out[:, :self.in_channels]
409
+
410
+ guid_t_min, guid_t_max = cfg_interval
411
+ assert guid_t_min < guid_t_max, "cfg_interval should be (min, max) with min < max"
412
+ eps = torch.where(
413
+ ((t >= guid_t_min) & (t <= guid_t_max)
414
+ ).view(-1, *[1] * (len(eps.shape) - 1)),
415
+ ag_eps + cfg_scale * (eps - ag_eps), eps
416
+ )
417
+
418
+ return eps
419
+
420
+
421
+ class DiTwDDTHeadDualHead(DiTwDDTHead):
422
+ """DDT with dual decoder heads: one for semantic, one for recon.
423
+
424
+ Shared encoder (28 blocks) → two independent decoder branches:
425
+ - sem branch: s_projector_sem → x_embedder_sem → decoder_sem_blocks → final_layer_sem → sem_channels
426
+ - recon branch: s_projector_recon → x_embedder_recon → decoder_recon_blocks → final_layer_recon → recon_channels
427
+ Outputs are concatenated back to in_channels.
428
+ """
429
+
430
+ def __init__(
431
+ self,
432
+ sem_channels: int = 768,
433
+ input_size: int = 1,
434
+ patch_size: Union[list, int] = 1,
435
+ in_channels: int = 768,
436
+ hidden_size=[1152, 2048],
437
+ depth=[28, 2],
438
+ num_heads: Union[list[int], int] = [16, 16],
439
+ mlp_ratio=4.0,
440
+ class_dropout_prob=0.1,
441
+ num_classes=1000,
442
+ use_qknorm=False,
443
+ use_swiglu=True,
444
+ use_rope=True,
445
+ use_rmsnorm=True,
446
+ wo_shift=False,
447
+ use_pos_embed: bool = True,
448
+ ):
449
+ # Initialize parent (builds shared encoder + original decoder)
450
+ super().__init__(
451
+ input_size=input_size, patch_size=patch_size, in_channels=in_channels,
452
+ hidden_size=hidden_size, depth=depth, num_heads=num_heads,
453
+ mlp_ratio=mlp_ratio, class_dropout_prob=class_dropout_prob,
454
+ num_classes=num_classes, use_qknorm=use_qknorm, use_swiglu=use_swiglu,
455
+ use_rope=use_rope, use_rmsnorm=use_rmsnorm, wo_shift=wo_shift,
456
+ use_pos_embed=use_pos_embed,
457
+ )
458
+ recon_channels = in_channels - sem_channels
459
+ assert recon_channels > 0, f"sem_channels={sem_channels} >= in_channels={in_channels}"
460
+ self.sem_channels = sem_channels
461
+ self.recon_channels = recon_channels
462
+
463
+ p = self.x_patch_size
464
+ dec_num_heads = self.num_heads[1]
465
+ _mlp_ratio = mlp_ratio
466
+
467
+ # Input size for x_embedder (spatial dim before patchify)
468
+ _x_input_size = int(sqrt(self.x_embedder.num_patches)) * self.x_patch_size
469
+
470
+ # --- Semantic decoder branch ---
471
+ sem_ch_per_token = sem_channels * p * p
472
+ self.x_embedder_sem = PatchEmbed(
473
+ _x_input_size, self.x_patch_size, sem_ch_per_token, self.decoder_hidden_size, bias=True)
474
+ self.s_projector_sem = nn.Linear(
475
+ self.encoder_hidden_size, self.decoder_hidden_size) if self.encoder_hidden_size != self.decoder_hidden_size else nn.Identity()
476
+ self.decoder_blocks_sem = nn.ModuleList([
477
+ LightningDDTBlock(self.decoder_hidden_size, dec_num_heads,
478
+ mlp_ratio=_mlp_ratio,
479
+ use_qknorm=use_qknorm, use_rmsnorm=use_rmsnorm,
480
+ use_swiglu=use_swiglu, wo_shift=wo_shift)
481
+ for _ in range(self.num_decoder_blocks)
482
+ ])
483
+ self.final_layer_sem = DDTFinalLayer(
484
+ self.decoder_hidden_size, 1, sem_ch_per_token, use_rmsnorm=use_rmsnorm)
485
+
486
+ # --- Recon decoder branch ---
487
+ recon_ch_per_token = recon_channels * p * p
488
+ self.x_embedder_recon = PatchEmbed(
489
+ _x_input_size, self.x_patch_size, recon_ch_per_token, self.decoder_hidden_size, bias=True)
490
+ self.s_projector_recon = nn.Linear(
491
+ self.encoder_hidden_size, self.decoder_hidden_size) if self.encoder_hidden_size != self.decoder_hidden_size else nn.Identity()
492
+ self.decoder_blocks_recon = nn.ModuleList([
493
+ LightningDDTBlock(self.decoder_hidden_size, dec_num_heads,
494
+ mlp_ratio=_mlp_ratio,
495
+ use_qknorm=use_qknorm, use_rmsnorm=use_rmsnorm,
496
+ use_swiglu=use_swiglu, wo_shift=wo_shift)
497
+ for _ in range(self.num_decoder_blocks)
498
+ ])
499
+ self.final_layer_recon = DDTFinalLayer(
500
+ self.decoder_hidden_size, 1, recon_ch_per_token, use_rmsnorm=use_rmsnorm)
501
+
502
+ # RoPE for new decoder branches (shared same config as original dec_feat_rope)
503
+ if self.use_rope:
504
+ self.dec_feat_rope_sem = self.dec_feat_rope
505
+ self.dec_feat_rope_recon = self.dec_feat_rope
506
+
507
+ # Remove parent's original decoder modules (unused, would cause DDP errors)
508
+ del self.x_embedder
509
+ del self.s_projector
510
+ del self.final_layer
511
+ # Remove parent's original decoder blocks (blocks[num_encoder_blocks:])
512
+ original_blocks = list(self.blocks)
513
+ self.blocks = nn.ModuleList(original_blocks[:self.num_encoder_blocks])
514
+
515
+ # Initialize new modules
516
+ self._init_dual_head_weights()
517
+
518
+ logger.info(f"DiTwDDTHeadDualHead: sem_channels={sem_channels}, recon_channels={recon_channels}, "
519
+ f"decoder_blocks={self.num_decoder_blocks} x2, decoder_hidden={self.decoder_hidden_size}")
520
+
521
+ def _init_dual_head_weights(self):
522
+ """Initialize weights for the dual decoder branches."""
523
+ for embedder in [self.x_embedder_sem, self.x_embedder_recon]:
524
+ w = embedder.proj.weight.data
525
+ nn.init.xavier_uniform_(w.view([w.shape[0], -1]))
526
+ nn.init.constant_(embedder.proj.bias, 0)
527
+
528
+ for blocks in [self.decoder_blocks_sem, self.decoder_blocks_recon]:
529
+ for block in blocks:
530
+ nn.init.constant_(block.adaLN_modulation[-1].weight, 0)
531
+ nn.init.constant_(block.adaLN_modulation[-1].bias, 0)
532
+
533
+ for fl in [self.final_layer_sem, self.final_layer_recon]:
534
+ nn.init.constant_(fl.adaLN_modulation[-1].weight, 0)
535
+ nn.init.constant_(fl.adaLN_modulation[-1].bias, 0)
536
+ nn.init.constant_(fl.linear.weight, 0)
537
+ nn.init.constant_(fl.linear.bias, 0)
538
+
539
+ def _unpatchify_branch(self, x, channels):
540
+ """Unpatchify for a branch with specific number of channels."""
541
+ c = channels * self.x_patch_size * self.x_patch_size
542
+ p = self.x_patch_size
543
+ h = w = int(x.shape[1] ** 0.5)
544
+ assert h * w == x.shape[1]
545
+ x = x.reshape(shape=(x.shape[0], h, w, p, p, c))
546
+ x = torch.einsum('nhwpqc->nchpwq', x)
547
+ imgs = x.reshape(shape=(x.shape[0], c, h * p, h * p))
548
+ return imgs
549
+
550
+ def decoder_forward(self, x, s):
551
+ """Dual decoder: sem and recon branches run independently, outputs concatenated."""
552
+ with torch.cuda.amp.autocast(enabled=False):
553
+ # Split input x along channel dim: [B, C, H, W] → sem [B, sem_ch, H, W] + recon [B, recon_ch, H, W]
554
+ x_float = x.float()
555
+ x_sem_input = x_float[:, :self.sem_channels, :, :]
556
+ x_recon_input = x_float[:, self.sem_channels:, :, :]
557
+
558
+ s_float = s.float()
559
+
560
+ # --- Semantic branch ---
561
+ s_sem = self.s_projector_sem(s_float)
562
+ h_sem = self.x_embedder_sem(x_sem_input)
563
+ rope_sem = self.dec_feat_rope_sem if self.use_rope else None
564
+ for block in self.decoder_blocks_sem:
565
+ h_sem = block(h_sem, s_sem, feat_rope=rope_sem)
566
+ h_sem = self.final_layer_sem(h_sem, s_sem)
567
+ out_sem = self._unpatchify_branch(h_sem, self.sem_channels) # [B, sem_ch, H, W]
568
+
569
+ # --- Recon branch ---
570
+ s_recon = self.s_projector_recon(s_float)
571
+ h_recon = self.x_embedder_recon(x_recon_input)
572
+ rope_recon = self.dec_feat_rope_recon if self.use_rope else None
573
+ for block in self.decoder_blocks_recon:
574
+ h_recon = block(h_recon, s_recon, feat_rope=rope_recon)
575
+ h_recon = self.final_layer_recon(h_recon, s_recon)
576
+ out_recon = self._unpatchify_branch(h_recon, self.recon_channels) # [B, recon_ch, H, W]
577
+
578
+ # Concatenate: [B, sem_ch + recon_ch, H, W] = [B, in_channels, H, W]
579
+ out = torch.cat([out_sem, out_recon], dim=1)
580
+ return out
581
+
582
+
583
+ class PartialRoPE(nn.Module):
584
+ """
585
+ A RoPE wrapper that only applies rotation to the last `num_spatial` tokens
586
+ in the sequence, leaving the leading `num_globaltoken` tokens unchanged.
587
+
588
+ The underlying `feat_rope` (VisionRotaryEmbeddingFast) expects input of shape
589
+ [B, num_heads, num_spatial, head_dim] and returns the same shape with RoPE applied.
590
+ This wrapper slices q/k, applies RoPE to the spatial slice, then stitches back.
591
+
592
+ Args:
593
+ feat_rope: a VisionRotaryEmbeddingFast instance sized for num_spatial tokens.
594
+ num_globaltoken: number of leading global tokens to skip.
595
+ """
596
+ def __init__(self, feat_rope, num_globaltoken: int):
597
+ super().__init__()
598
+ self.feat_rope = feat_rope
599
+ self.num_globaltoken = num_globaltoken
600
+
601
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
602
+ """
603
+ Args:
604
+ x: [B, num_heads, N, head_dim] where N = num_globaltoken + num_spatial
605
+ Returns:
606
+ x: same shape, with RoPE applied only to x[:, :, num_globaltoken:, :]
607
+ """
608
+ g = self.num_globaltoken
609
+ x_global = x[:, :, :g, :] # (B, H, num_globaltoken, D) — untouched
610
+ x_spatial = x[:, :, g:, :] # (B, H, num_spatial, D) — apply RoPE
611
+ x_spatial = self.feat_rope(x_spatial)
612
+ return torch.cat([x_global, x_spatial], dim=2)
613
+
614
+
615
+ class DiTwDDTHeadTokenInput(nn.Module):
616
+ """
617
+ A variant of DiTwDDTHead that accepts pre-tokenized input x of shape [B, N, D],
618
+ where N = num_globaltoken + H * W (global tokens come first).
619
+
620
+ Key differences from DiTwDDTHead:
621
+ - Input x is already tokenized: no PatchEmbed needed; a linear projector maps D -> hidden_size.
622
+ - Position encoding:
623
+ * spatial tokens (H*W) use fixed 2D sin-cos positional embedding.
624
+ * global tokens use learnable positional embedding.
625
+ - Encoder (DDT-style): all N tokens pass through encoder blocks together with AdaLN condition c.
626
+ After encoding, timestep t is broadcast and fused into the token sequence as the condition s.
627
+ - Decoder: all N tokens are denoised conditioned on s from the encoder via AdaLN.
628
+ - Output: [B, N, D], same shape as input (both global and spatial tokens are denoised).
629
+ """
630
+
631
+ def __init__(
632
+ self,
633
+ in_channels: int = 768, # D: input/output token dimension
634
+ hw_size: int = 16, # spatial grid side length, num spatial tokens = hw_size^2
635
+ num_globaltoken: int = 8, # number of global tokens (prepended before spatial tokens)
636
+ hidden_size: Union[list, int] = [1152, 2048], # [encoder_hidden, decoder_hidden]
637
+ depth: list = [28, 2], # [num_encoder_blocks, num_decoder_blocks]
638
+ num_heads: Union[list, int] = [16, 16],
639
+ mlp_ratio: float = 4.0,
640
+ class_dropout_prob: float = 0.1,
641
+ num_classes: int = 1000,
642
+ use_qknorm: bool = False,
643
+ use_swiglu: bool = True,
644
+ use_rope: bool = False, # RoPE is disabled by default for mixed token sequences
645
+ use_rmsnorm: bool = True,
646
+ wo_shift: bool = False,
647
+ ):
648
+ super().__init__()
649
+ self.in_channels = in_channels
650
+ self.out_channels = in_channels
651
+ self.hw_size = hw_size
652
+ self.num_spatial = hw_size * hw_size
653
+ self.num_globaltoken = num_globaltoken
654
+ self.num_tokens = num_globaltoken + self.num_spatial # total N
655
+
656
+ # hidden sizes
657
+ if isinstance(hidden_size, int):
658
+ hidden_size = [hidden_size, hidden_size]
659
+ self.encoder_hidden_size = hidden_size[0]
660
+ self.decoder_hidden_size = hidden_size[1]
661
+
662
+ # num heads
663
+ if isinstance(num_heads, int):
664
+ num_heads = [num_heads, num_heads]
665
+ self.num_heads = list(num_heads)
666
+ enc_num_heads = self.num_heads[0]
667
+ dec_num_heads = self.num_heads[1]
668
+
669
+ # block depths
670
+ self.num_encoder_blocks = depth[0]
671
+ self.num_decoder_blocks = depth[1]
672
+ self.num_blocks = depth[0] + depth[1]
673
+ self.use_rope = use_rope
674
+
675
+ # ---------- input projectors ----------
676
+ # project D -> encoder_hidden_size for the encoder stage
677
+ self.enc_input_proj = nn.Linear(in_channels, self.encoder_hidden_size, bias=True)
678
+ # project D -> decoder_hidden_size for the decoder stage
679
+ self.dec_input_proj = nn.Linear(in_channels, self.decoder_hidden_size, bias=True)
680
+
681
+ # ---------- position embeddings ----------
682
+ # spatial tokens: fixed 2D sin-cos, shape (1, num_spatial, encoder_hidden_size)
683
+ self.enc_spatial_pos_embed = nn.Parameter(
684
+ torch.zeros(1, self.num_spatial, self.encoder_hidden_size), requires_grad=False)
685
+ self.dec_spatial_pos_embed = nn.Parameter(
686
+ torch.zeros(1, self.num_spatial, self.decoder_hidden_size), requires_grad=False)
687
+
688
+ # global tokens: learnable, shape (1, num_globaltoken, hidden_size)
689
+ self.enc_global_pos_embed = nn.Parameter(
690
+ torch.zeros(1, num_globaltoken, self.encoder_hidden_size), requires_grad=True)
691
+ self.dec_global_pos_embed = nn.Parameter(
692
+ torch.zeros(1, num_globaltoken, self.decoder_hidden_size), requires_grad=True)
693
+
694
+ # ---------- condition embedders ----------
695
+ self.t_embedder = GaussianFourierEmbedding(self.encoder_hidden_size)
696
+ self.y_embedder = LabelEmbedder(num_classes, self.encoder_hidden_size, class_dropout_prob)
697
+
698
+ # project encoder hidden -> decoder hidden for condition s
699
+ self.s_projector = (
700
+ nn.Linear(self.encoder_hidden_size, self.decoder_hidden_size)
701
+ if self.encoder_hidden_size != self.decoder_hidden_size
702
+ else nn.Identity()
703
+ )
704
+
705
+ # ---------- RoPE (optional, only applied to spatial tokens) ----------
706
+ # global tokens are skipped; PartialRoPE wraps VisionRotaryEmbeddingFast
707
+ # so that only the trailing num_spatial positions are rotated.
708
+ if self.use_rope:
709
+ enc_half_head_dim = self.encoder_hidden_size // enc_num_heads // 2
710
+ self.enc_feat_rope = PartialRoPE(
711
+ feat_rope=VisionRotaryEmbeddingFast(
712
+ dim=enc_half_head_dim,
713
+ pt_seq_len=hw_size,
714
+ ),
715
+ num_globaltoken=num_globaltoken,
716
+ )
717
+ dec_half_head_dim = self.decoder_hidden_size // dec_num_heads // 2
718
+ self.dec_feat_rope = PartialRoPE(
719
+ feat_rope=VisionRotaryEmbeddingFast(
720
+ dim=dec_half_head_dim,
721
+ pt_seq_len=hw_size,
722
+ ),
723
+ num_globaltoken=num_globaltoken,
724
+ )
725
+ else:
726
+ self.enc_feat_rope = None
727
+ self.dec_feat_rope = None
728
+
729
+ # ---------- transformer blocks ----------
730
+ self.blocks = nn.ModuleList([
731
+ LightningDDTBlock(
732
+ self.encoder_hidden_size if i < self.num_encoder_blocks else self.decoder_hidden_size,
733
+ enc_num_heads if i < self.num_encoder_blocks else dec_num_heads,
734
+ mlp_ratio=mlp_ratio,
735
+ use_qknorm=use_qknorm,
736
+ use_rmsnorm=use_rmsnorm,
737
+ use_swiglu=use_swiglu,
738
+ wo_shift=wo_shift,
739
+ ) for i in range(self.num_blocks)
740
+ ])
741
+
742
+ # ---------- final layer ----------
743
+ # outputs in_channels per token (no patch expansion)
744
+ self.final_layer = DDTFinalLayer(
745
+ self.decoder_hidden_size, 1, in_channels, use_rmsnorm=use_rmsnorm)
746
+
747
+ # ---------- output projector ----------
748
+ # map decoder_hidden_size back to in_channels
749
+ # (DDTFinalLayer already does linear(hidden -> 1*1*in_channels), so this is identity-like)
750
+ # We keep it explicit for clarity; DDTFinalLayer.linear outputs in_channels directly.
751
+
752
+ self.initialize_weights()
753
+
754
+ # ------------------------------------------------------------------
755
+ def initialize_weights(self):
756
+ # sin-cos 2D pos embed for spatial tokens (encoder)
757
+ enc_pos = get_2d_sincos_pos_embed(self.encoder_hidden_size, self.hw_size)
758
+ self.enc_spatial_pos_embed.data.copy_(
759
+ torch.from_numpy(enc_pos).float().unsqueeze(0))
760
+
761
+ # sin-cos 2D pos embed for spatial tokens (decoder)
762
+ dec_pos = get_2d_sincos_pos_embed(self.decoder_hidden_size, self.hw_size)
763
+ self.dec_spatial_pos_embed.data.copy_(
764
+ torch.from_numpy(dec_pos).float().unsqueeze(0))
765
+
766
+ # learnable global pos embed: small normal init
767
+ nn.init.normal_(self.enc_global_pos_embed, std=0.02)
768
+ nn.init.normal_(self.dec_global_pos_embed, std=0.02)
769
+
770
+ # input projectors
771
+ nn.init.xavier_uniform_(self.enc_input_proj.weight)
772
+ nn.init.constant_(self.enc_input_proj.bias, 0)
773
+ nn.init.xavier_uniform_(self.dec_input_proj.weight)
774
+ nn.init.constant_(self.dec_input_proj.bias, 0)
775
+
776
+ # label embedding
777
+ nn.init.normal_(self.y_embedder.embedding_table.weight, std=0.02)
778
+
779
+ # timestep embedding MLP
780
+ nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02)
781
+ nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02)
782
+
783
+ # zero-out adaLN modulation layers
784
+ for block in self.blocks:
785
+ nn.init.constant_(block.adaLN_modulation[-1].weight, 0)
786
+ nn.init.constant_(block.adaLN_modulation[-1].bias, 0)
787
+
788
+ # zero-out final layer
789
+ nn.init.constant_(self.final_layer.adaLN_modulation[-1].weight, 0)
790
+ nn.init.constant_(self.final_layer.adaLN_modulation[-1].bias, 0)
791
+ nn.init.constant_(self.final_layer.linear.weight, 0)
792
+ nn.init.constant_(self.final_layer.linear.bias, 0)
793
+
794
+ # ------------------------------------------------------------------
795
+ def _build_pos_embed(self, hidden_size, global_pos_embed, spatial_pos_embed):
796
+ """
797
+ Concatenate [global_pos_embed, spatial_pos_embed] along dim=1.
798
+ global_pos_embed: (1, num_globaltoken, hidden_size) — learnable
799
+ spatial_pos_embed: (1, num_spatial, hidden_size) — fixed sin-cos
800
+ Returns: (1, num_tokens, hidden_size)
801
+ """
802
+ return torch.cat([global_pos_embed, spatial_pos_embed], dim=1)
803
+
804
+ # ------------------------------------------------------------------
805
+ def forward(self, x, t, y, s=None):
806
+ """
807
+ Args:
808
+ x: [B, N, D] noisy token sequence, N = num_globaltoken + H*W
809
+ t: [B] diffusion timestep
810
+ y: [B] class label (int)
811
+ s: optional pre-computed encoder condition [B, N, encoder_hidden_size].
812
+ If provided, the encoder stage is skipped.
813
+ Returns:
814
+ out: [B, N, D] denoised token sequence (same shape as input)
815
+ """
816
+ B, N, D = x.shape
817
+ assert N == self.num_tokens, (
818
+ f"Expected N={self.num_tokens} (num_globaltoken={self.num_globaltoken} + "
819
+ f"num_spatial={self.num_spatial}), got {N}"
820
+ )
821
+
822
+ # ---- condition embeddings ----
823
+ t_emb = self.t_embedder(t) # (B, encoder_hidden_size)
824
+ y_emb = self.y_embedder(y, self.training) # (B, encoder_hidden_size)
825
+ c = nn.functional.silu(t_emb + y_emb) # (B, encoder_hidden_size)
826
+
827
+ # ---- encoder stage ----
828
+ if s is None:
829
+ # project input to encoder hidden size
830
+ xe = self.enc_input_proj(x) # (B, N, encoder_hidden_size)
831
+
832
+ # add position embeddings:
833
+ # global tokens (front): learnable enc_global_pos_embed
834
+ # spatial tokens (back): fixed sin-cos enc_spatial_pos_embed
835
+ pos = self._build_pos_embed(
836
+ self.encoder_hidden_size,
837
+ self.enc_global_pos_embed, # (1, num_globaltoken, enc_h)
838
+ self.enc_spatial_pos_embed, # (1, num_spatial, enc_h)
839
+ ) # (1, N, encoder_hidden_size)
840
+ xe = xe + pos
841
+
842
+ # pass through encoder blocks (self-attention conditioned on c)
843
+ # PartialRoPE ensures only spatial tokens (trailing hw_size^2) are rotated
844
+ for i in range(self.num_encoder_blocks):
845
+ xe = self.blocks[i](xe, c, feat_rope=self.enc_feat_rope)
846
+
847
+ # fuse timestep into encoder output to form condition s
848
+ t_broadcast = t_emb.unsqueeze(1).expand(-1, N, -1) # (B, N, enc_h)
849
+ s = nn.functional.silu(t_broadcast + xe) # (B, N, encoder_hidden_size)
850
+
851
+ # project condition s to decoder hidden size
852
+ s = self.s_projector(s) # (B, N, decoder_hidden_size)
853
+
854
+ # ---- decoder stage ----
855
+ # project input to decoder hidden size
856
+ xd = self.dec_input_proj(x) # (B, N, decoder_hidden_size)
857
+
858
+ # add decoder position embeddings
859
+ dec_pos = self._build_pos_embed(
860
+ self.decoder_hidden_size,
861
+ self.dec_global_pos_embed, # (1, num_globaltoken, dec_h)
862
+ self.dec_spatial_pos_embed, # (1, num_spatial, dec_h)
863
+ ) # (1, N, decoder_hidden_size)
864
+ xd = xd + dec_pos
865
+
866
+ # pass through decoder blocks (conditioned on s)
867
+ # PartialRoPE ensures only spatial tokens (trailing hw_size^2) are rotated
868
+ for i in range(self.num_encoder_blocks, self.num_blocks):
869
+ xd = self.blocks[i](xd, s, feat_rope=self.dec_feat_rope)
870
+
871
+ # final layer: outputs (B, N, in_channels)
872
+ out = self.final_layer(xd, s) # (B, N, in_channels)
873
+ return out
874
+
875
+ # ------------------------------------------------------------------
876
+ def forward_with_cfg(self, x, t, y, cfg_scale, cfg_interval=(0, 1)):
877
+ """
878
+ Classifier-free guidance forward pass.
879
+ Assumes x is already doubled: first half conditional, second half unconditional.
880
+ """
881
+ half = x[: len(x) // 2]
882
+ combined = torch.cat([half, half], dim=0)
883
+ model_out = self.forward(combined, t, y) # (2B, N, D)
884
+
885
+ eps, rest = model_out[:, :, :self.in_channels], model_out[:, :, self.in_channels:]
886
+ cond_eps, uncond_eps = torch.split(eps, len(eps) // 2, dim=0)
887
+
888
+ guid_t_min, guid_t_max = cfg_interval
889
+ assert guid_t_min < guid_t_max
890
+ t_half = t[: len(t) // 2]
891
+ half_eps = torch.where(
892
+ ((t_half >= guid_t_min) & (t_half <= guid_t_max)
893
+ ).view(-1, *[1] * (len(cond_eps.shape) - 1)),
894
+ uncond_eps + cfg_scale * (cond_eps - uncond_eps),
895
+ cond_eps,
896
+ )
897
+ eps = torch.cat([half_eps, half_eps], dim=0)
898
+ return torch.cat([eps, rest], dim=-1)
899
+
900
+ # ------------------------------------------------------------------
901
+ def forward_with_autoguidance(self, x, t, y, cfg_scale, additional_model_forward, cfg_interval=(0, 1)):
902
+ """
903
+ Auto-guidance forward pass using an additional (weaker) model.
904
+ """
905
+ model_out = self.forward(x, t, y)
906
+ ag_model_out = additional_model_forward(x, t, y)
907
+
908
+ eps = model_out[:, :, :self.in_channels]
909
+ ag_eps = ag_model_out[:, :, :self.in_channels]
910
+
911
+ guid_t_min, guid_t_max = cfg_interval
912
+ assert guid_t_min < guid_t_max
913
+ eps = torch.where(
914
+ ((t >= guid_t_min) & (t <= guid_t_max)
915
+ ).view(-1, *[1] * (len(eps.shape) - 1)),
916
+ ag_eps + cfg_scale * (eps - ag_eps),
917
+ eps,
918
+ )
919
+ return eps
patch-reparameterization/src/stage2/models/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Encoder registry for stage 2 models."""
2
+
3
+
4
+
5
+ # Import modules that perform registration on import.
6
+ from typing import Callable, Dict, Optional, Type, Union
7
+ from typing import Protocol, Any, runtime_checkable
8
+
9
+ @runtime_checkable
10
+ class Stage2ModelProtocol(Protocol):
11
+ def forward(self, *args: Any, **kwargs: Any) -> Any: ...
12
+ def forward_with_cfg(self, *args: Any, **kwargs: Any) -> Any: ...
13
+ def forward_with_autoguidance(self, *args: Any, **kwargs: Any) -> Any: ...
14
+ def train(self, mode: bool = True) -> Any: ...
15
+ def eval(self) -> Any: ...
patch-reparameterization/src/stage2/models/lightningDiT.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from timm.models.vision_transformer import PatchEmbed, Mlp
4
+ import torch
5
+ from torch import nn
6
+
7
+ from .model_utils import VisionRotaryEmbeddingFast, SwiGLUFFN, RMSNorm, NormAttention, LabelEmbedder, get_2d_sincos_pos_embed, GaussianFourierEmbedding, modulate
8
+
9
+
10
+
11
+
12
+ class LightningDiTBlock(nn.Module):
13
+ """
14
+ Lightning DiT Block. We add features including:
15
+ - ROPE
16
+ - QKNorm
17
+ - RMSNorm
18
+ - SwiGLU
19
+ - No shift AdaLN.
20
+ Not all of them are used in the final model, please refer to the paper for more details.
21
+ """
22
+ def __init__(
23
+ self,
24
+ hidden_size,
25
+ num_heads,
26
+ mlp_ratio=4.0,
27
+ use_qknorm=False,
28
+ use_swiglu=True,
29
+ use_rmsnorm=True,
30
+ wo_shift=False,
31
+ **block_kwargs
32
+ ):
33
+ super().__init__()
34
+
35
+ # Initialize normalization layers
36
+ if not use_rmsnorm:
37
+ self.norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
38
+ self.norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
39
+ else:
40
+ self.norm1 = RMSNorm(hidden_size)
41
+ self.norm2 = RMSNorm(hidden_size)
42
+
43
+ # Initialize attention layer
44
+ self.attn = NormAttention(
45
+ hidden_size,
46
+ num_heads=num_heads,
47
+ qkv_bias=True,
48
+ qk_norm=use_qknorm,
49
+ use_rmsnorm=use_rmsnorm,
50
+ **block_kwargs
51
+ )
52
+
53
+ # Initialize MLP layer
54
+ mlp_hidden_dim = int(hidden_size * mlp_ratio)
55
+ approx_gelu = lambda: nn.GELU(approximate="tanh")
56
+ if use_swiglu:
57
+ # here we did not use SwiGLU from xformers because it is not compatible with torch.compile for now.
58
+ self.mlp = SwiGLUFFN(hidden_size, int(2/3 * mlp_hidden_dim))
59
+ else:
60
+ self.mlp = Mlp(
61
+ in_features=hidden_size,
62
+ hidden_features=mlp_hidden_dim,
63
+ act_layer=approx_gelu,
64
+ drop=0
65
+ )
66
+
67
+ # Initialize AdaLN modulation
68
+ if wo_shift:
69
+ self.adaLN_modulation = nn.Sequential(
70
+ nn.SiLU(),
71
+ nn.Linear(hidden_size, 4 * hidden_size, bias=True)
72
+ )
73
+ else:
74
+ self.adaLN_modulation = nn.Sequential(
75
+ nn.SiLU(),
76
+ nn.Linear(hidden_size, 6 * hidden_size, bias=True)
77
+ )
78
+ self.wo_shift = wo_shift
79
+
80
+ def forward(self, x, c, feat_rope=None):
81
+ if self.wo_shift:
82
+ scale_msa, gate_msa, scale_mlp, gate_mlp = self.adaLN_modulation(c).chunk(4, dim=1)
83
+ shift_msa = None
84
+ shift_mlp = None
85
+ else:
86
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.adaLN_modulation(c).chunk(6, dim=1)
87
+ x = x + gate_msa.unsqueeze(1) * self.attn(modulate(self.norm1(x), shift_msa, scale_msa), rope=feat_rope)
88
+ x = x + gate_mlp.unsqueeze(1) * self.mlp(modulate(self.norm2(x), shift_mlp, scale_mlp))
89
+ return x
90
+ class LightningFinalLayer(nn.Module):
91
+ """
92
+ The final layer of LightningDiT.
93
+ """
94
+ def __init__(self, hidden_size, patch_size, out_channels, use_rmsnorm=False):
95
+ super().__init__()
96
+ if not use_rmsnorm:
97
+ self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
98
+ else:
99
+ self.norm_final = RMSNorm(hidden_size)
100
+ self.linear = nn.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True)
101
+ self.adaLN_modulation = nn.Sequential(
102
+ nn.SiLU(),
103
+ nn.Linear(hidden_size, 2 * hidden_size, bias=True)
104
+ )
105
+ def forward(self, x, c):
106
+ shift, scale = self.adaLN_modulation(c).chunk(2, dim=1)
107
+ x = modulate(self.norm_final(x), shift, scale)
108
+ x = self.linear(x)
109
+ return x
110
+
111
+ class LightningDiT(nn.Module):
112
+ """
113
+ Diffusion model with a Transformer backbone.
114
+ """
115
+ def __init__(
116
+ self,
117
+ input_size=16,
118
+ patch_size=1,
119
+ in_channels=768,
120
+ hidden_size=1152,
121
+ depth=28,
122
+ num_heads=16,
123
+ mlp_ratio=4.0,
124
+ class_dropout_prob=0.1,
125
+ num_classes=1000,
126
+ learn_sigma=False,
127
+ use_qknorm=False,
128
+ use_swiglu=True,
129
+ use_rope=True,
130
+ use_rmsnorm=True,
131
+ wo_shift=False,
132
+ use_gembed: bool = True,
133
+ ):
134
+ super().__init__()
135
+ self.learn_sigma = learn_sigma
136
+ self.in_channels = in_channels
137
+ self.out_channels = in_channels if not learn_sigma else in_channels * 2
138
+ self.patch_size = patch_size
139
+ self.num_heads = num_heads
140
+ self.use_rope = use_rope
141
+ self.use_rmsnorm = use_rmsnorm
142
+ self.depth = depth
143
+ self.hidden_size = hidden_size
144
+ self.use_gembed = use_gembed
145
+ self.x_embedder = PatchEmbed(input_size, patch_size, in_channels, hidden_size, bias=True)
146
+ self.t_embedder = GaussianFourierEmbedding(hidden_size)
147
+ self.y_embedder = LabelEmbedder(num_classes, hidden_size, class_dropout_prob)
148
+ self.ssl_supervise = False
149
+ num_patches = self.x_embedder.num_patches
150
+ # Will use fixed sin-cos embedding:
151
+ self.pos_embed = nn.Parameter(torch.zeros(1, num_patches, hidden_size), requires_grad=False)
152
+
153
+ # use rotary position encoding, borrow from EVA
154
+ if self.use_rope:
155
+ half_head_dim = hidden_size // num_heads // 2
156
+ hw_seq_len = input_size // patch_size
157
+ self.feat_rope = VisionRotaryEmbeddingFast(
158
+ dim=half_head_dim,
159
+ pt_seq_len=hw_seq_len,
160
+ )
161
+ else:
162
+ self.feat_rope = None
163
+
164
+ self.blocks = nn.ModuleList([
165
+ LightningDiTBlock(hidden_size,
166
+ num_heads,
167
+ mlp_ratio=mlp_ratio,
168
+ use_qknorm=use_qknorm,
169
+ use_swiglu=use_swiglu,
170
+ use_rmsnorm=use_rmsnorm,
171
+ wo_shift=wo_shift,
172
+ ) for _ in range(depth)
173
+ ])
174
+ self.final_layer = LightningFinalLayer(hidden_size, patch_size, self.out_channels, use_rmsnorm=use_rmsnorm)
175
+ self.initialize_weights()
176
+
177
+ def initialize_weights(self):
178
+ # Initialize transformer layers:
179
+ def _basic_init(module):
180
+ if isinstance(module, nn.Linear):
181
+ torch.nn.init.xavier_uniform_(module.weight)
182
+ if module.bias is not None:
183
+ nn.init.constant_(module.bias, 0)
184
+ self.apply(_basic_init)
185
+
186
+ # Initialize (and freeze) pos_embed by sin-cos embedding:
187
+ pos_embed = get_2d_sincos_pos_embed(self.pos_embed.shape[-1], int(self.x_embedder.num_patches ** 0.5))
188
+ self.pos_embed.data.copy_(torch.from_numpy(pos_embed).float().unsqueeze(0))
189
+
190
+ # Initialize patch_embed like nn.Linear (instead of nn.Conv2d):
191
+ w = self.x_embedder.proj.weight.data
192
+ nn.init.xavier_uniform_(w.view([w.shape[0], -1]))
193
+ nn.init.constant_(self.x_embedder.proj.bias, 0)
194
+
195
+ # Initialize label embedding table:
196
+ nn.init.normal_(self.y_embedder.embedding_table.weight, std=0.02)
197
+
198
+ # Initialize timestep embedding MLP:
199
+ nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02)
200
+ nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02)
201
+
202
+ # Zero-out adaLN modulation layers in LightningDiT blocks:
203
+ for block in self.blocks:
204
+ nn.init.constant_(block.adaLN_modulation[-1].weight, 0)
205
+ nn.init.constant_(block.adaLN_modulation[-1].bias, 0)
206
+
207
+ # Zero-out output layers:
208
+ nn.init.constant_(self.final_layer.adaLN_modulation[-1].weight, 0)
209
+ nn.init.constant_(self.final_layer.adaLN_modulation[-1].bias, 0)
210
+ nn.init.constant_(self.final_layer.linear.weight, 0)
211
+ nn.init.constant_(self.final_layer.linear.bias, 0)
212
+
213
+ def unpatchify(self, x):
214
+ """
215
+ x: (N, T, patch_size**2 * C)
216
+ imgs: (N, H, W, C)
217
+ """
218
+ c = self.out_channels
219
+ p = self.x_embedder.patch_size[0]
220
+ h = w = int(x.shape[1] ** 0.5)
221
+ assert h * w == x.shape[1]
222
+
223
+ x = x.reshape(shape=(x.shape[0], h, w, p, p, c))
224
+ x = torch.einsum('nhwpqc->nchpwq', x)
225
+ imgs = x.reshape(shape=(x.shape[0], c, h * p, h * p))
226
+ return imgs
227
+
228
+ def forward(self, x, t=None, y=None):
229
+ """
230
+ Forward pass of LightningDiT.
231
+ x: (N, C, H, W) tensor of spatial inputs (images or latent representations of images)
232
+ t: (N,) tensor of diffusion timesteps
233
+ y: (N,) tensor of class labels
234
+ use_checkpoint: boolean to toggle checkpointing
235
+ """
236
+ x = self.x_embedder(x) + self.pos_embed # (N, T, D), where T = H * W / patch_size ** 2
237
+ t = self.t_embedder(t) # (N, D)
238
+ y = self.y_embedder(y, self.training) # (N, D)
239
+ c = t + y # (N, D)
240
+
241
+ for block in self.blocks:
242
+ x = block(x, c, feat_rope=self.feat_rope)
243
+
244
+ x = self.final_layer(x, c) # (N, T, patch_size ** 2 * out_channels)
245
+ x = self.unpatchify(x) # (N, out_channels, H, W)
246
+
247
+ if self.learn_sigma:
248
+ x, _ = x.chunk(2, dim=1)
249
+ return x
250
+
251
+ def forward_with_cfg(self, x, t, y, cfg_scale, cfg_interval=(-1e4, -1e4), interval_cfg: float = 0.0):
252
+ """
253
+ Forward pass of LightningDiT, but also batches the unconditional forward pass for classifier-free guidance.
254
+ """
255
+ # https://github.com/openai/glide-text2im/blob/main/notebooks/text2im.ipynb
256
+ half = x[: len(x) // 2]
257
+ combined = torch.cat([half, half], dim=0)
258
+ model_out = self.forward(combined, t, y)
259
+ if self.ssl_supervise:
260
+ model_out = model_out[0] # take the output only
261
+ # For exact reproducibility reasons, we apply classifier-free guidance on only
262
+ # three channels by default. The standard approach to cfg applies it to all channels.
263
+ # This can be done by uncommenting the following line and commenting-out the line following that.
264
+ # eps, rest = model_out[:, :self.in_channels], model_out[:, self.in_channels:]
265
+ eps, rest = model_out[:, :self.in_channels], model_out[:, self.in_channels:]
266
+ #eps, rest = model_out[:, :3], model_out[:, 3:]
267
+ cond_eps, uncond_eps = torch.split(eps, len(eps) // 2, dim=0)
268
+ t = t[0] # check if t < cfg_interval
269
+ in_interval = False
270
+ for i in range(len(cfg_interval)):
271
+ if t >= cfg_interval[i][0] and t < cfg_interval[i][1]:
272
+ #print(f't:{t}, cfg_interval: {cfg_interval[i]}')
273
+ if interval_cfg > 1.0:
274
+ half_eps = uncond_eps + interval_cfg * (cond_eps - uncond_eps)
275
+ else:
276
+ half_eps = cond_eps # only use conditional generation
277
+ in_interval = True
278
+ break
279
+ if not in_interval:
280
+ #print(f't:{t} not in cfg_interval')
281
+ half_eps = uncond_eps + cfg_scale * (cond_eps - uncond_eps)
282
+ eps = torch.cat([half_eps, half_eps], dim=0)
283
+ return torch.cat([eps, rest], dim=1)
284
+ def forward_with_autoguidance(self, x, t, y, cfg_scale, additional_model_forward, cfg_interval=(-1e4, -1e4), interval_cfg: float = 0.0):
285
+ """
286
+ Forward pass of LightningDiT, but also contain the forward pass for the additional model
287
+ """
288
+ half = x[: len(x) // 2] # cut the x by half, autoguidance does not need repeated input
289
+ t = t[: len(t) // 2]
290
+ y = y[: len(y) // 2]
291
+ model_out = self.forward(half, t, y)
292
+ ag_model_out = additional_model_forward(half, t, y)
293
+ eps = model_out[:, :self.in_channels]
294
+ ag_eps = ag_model_out[:, :self.in_channels]
295
+ t = t[0]
296
+ in_interval = False
297
+ for i in range(len(cfg_interval)):
298
+ if t >= cfg_interval[i][0] and t < cfg_interval[i][1]:
299
+ if interval_cfg > 1.0:
300
+ eps = ag_eps + interval_cfg * (eps - ag_eps)
301
+ in_interval = True
302
+ break
303
+ if not in_interval:
304
+ eps = ag_eps + cfg_scale * (eps - ag_eps)
305
+ return torch.cat([eps, eps], dim=0)
patch-reparameterization/src/stage2/models/model_utils.py ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy.random as random
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+
7
+ from einops import rearrange, repeat
8
+ from typing import *
9
+ from math import pi
10
+ from timm.models.vision_transformer import Attention
11
+ from functools import partial
12
+ from einops import rearrange, repeat
13
+ from collections.abc import Callable
14
+ import numpy as np
15
+
16
+
17
+ def modulate(x, shift, scale):
18
+ return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
19
+
20
+ def broadcat(tensors, dim=-1):
21
+ num_tensors = len(tensors)
22
+ shape_lens = set(list(map(lambda t: len(t.shape), tensors)))
23
+ assert len(shape_lens) == 1, 'tensors must all have the same number of dimensions'
24
+ shape_len = list(shape_lens)[0]
25
+ dim = (dim + shape_len) if dim < 0 else dim
26
+ dims = list(zip(*map(lambda t: list(t.shape), tensors)))
27
+ expandable_dims = [(i, val) for i, val in enumerate(dims) if i != dim]
28
+ assert all([*map(lambda t: len(set(t[1])) <= 2, expandable_dims)]
29
+ ), 'invalid dimensions for broadcastable concatentation'
30
+ max_dims = list(map(lambda t: (t[0], max(t[1])), expandable_dims))
31
+ expanded_dims = list(map(lambda t: (t[0], (t[1],) * num_tensors), max_dims))
32
+ expanded_dims.insert(dim, (dim, dims[dim]))
33
+ expandable_shapes = list(zip(*map(lambda t: t[1], expanded_dims)))
34
+ tensors = list(map(lambda t: t[0].expand(*t[1]), zip(tensors, expandable_shapes)))
35
+ return torch.cat(tensors, dim=dim)
36
+
37
+
38
+ def rotate_half(x):
39
+ x = rearrange(x, '... (d r) -> ... d r', r=2)
40
+ x1, x2 = x.unbind(dim=-1)
41
+ x = torch.stack((-x2, x1), dim=-1)
42
+ return rearrange(x, '... d r -> ... (d r)')
43
+ def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
44
+ """
45
+ embed_dim: output dimension for each position
46
+ pos: a list of positions to be encoded: size (M,)
47
+ out: (M, D)
48
+ """
49
+ assert embed_dim % 2 == 0
50
+ omega = np.arange(embed_dim // 2, dtype=np.float64)
51
+ omega /= embed_dim / 2.
52
+ omega = 1. / 10000**omega # (D/2,)
53
+
54
+ pos = pos.reshape(-1) # (M,)
55
+ out = np.einsum('m,d->md', pos, omega) # (M, D/2), outer product
56
+
57
+ emb_sin = np.sin(out) # (M, D/2)
58
+ emb_cos = np.cos(out) # (M, D/2)
59
+
60
+ emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D)
61
+ return emb
62
+ def get_2d_sincos_pos_embed_from_grid(embed_dim, grid):
63
+ assert embed_dim % 2 == 0
64
+
65
+ # use half of dimensions to encode grid_h
66
+ emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # (H*W, D/2)
67
+ emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # (H*W, D/2)
68
+
69
+ emb = np.concatenate([emb_h, emb_w], axis=1) # (H*W, D)
70
+ return emb
71
+
72
+ def get_2d_sincos_pos_embed(embed_dim, grid_size, cls_token=False, extra_tokens=0):
73
+ """
74
+ grid_size: int of the grid height and width
75
+ return:
76
+ pos_embed: [grid_size*grid_size, embed_dim] or [1+grid_size*grid_size, embed_dim] (w/ or w/o cls_token)
77
+ """
78
+ grid_h = np.arange(grid_size, dtype=np.float32)
79
+ grid_w = np.arange(grid_size, dtype=np.float32)
80
+ grid = np.meshgrid(grid_w, grid_h) # here w goes first
81
+ grid = np.stack(grid, axis=0)
82
+
83
+ grid = grid.reshape([2, 1, grid_size, grid_size])
84
+ pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid)
85
+ if cls_token and extra_tokens > 0:
86
+ pos_embed = np.concatenate([np.zeros([extra_tokens, embed_dim]), pos_embed], axis=0)
87
+ return pos_embed
88
+
89
+ class VisionRotaryEmbedding(nn.Module):
90
+ def __init__(
91
+ self,
92
+ dim,
93
+ pt_seq_len,
94
+ ft_seq_len=None,
95
+ custom_freqs=None,
96
+ freqs_for='lang',
97
+ theta=10000,
98
+ max_freq=10,
99
+ num_freqs=1,
100
+ ):
101
+ super().__init__()
102
+ if custom_freqs:
103
+ freqs = custom_freqs
104
+ elif freqs_for == 'lang':
105
+ freqs = 1. / (theta ** (torch.arange(0, dim, 2)[:(dim // 2)].float() / dim))
106
+ elif freqs_for == 'pixel':
107
+ freqs = torch.linspace(1., max_freq / 2, dim // 2) * pi
108
+ elif freqs_for == 'constant':
109
+ freqs = torch.ones(num_freqs).float()
110
+ else:
111
+ raise ValueError(f'unknown modality {freqs_for}')
112
+
113
+ if ft_seq_len is None:
114
+ ft_seq_len = pt_seq_len
115
+ t = torch.arange(ft_seq_len) / ft_seq_len * pt_seq_len
116
+
117
+ freqs_h = torch.einsum('..., f -> ... f', t, freqs)
118
+ freqs_h = repeat(freqs_h, '... n -> ... (n r)', r=2)
119
+
120
+ freqs_w = torch.einsum('..., f -> ... f', t, freqs)
121
+ freqs_w = repeat(freqs_w, '... n -> ... (n r)', r=2)
122
+
123
+ freqs = broadcat((freqs_h[:, None, :], freqs_w[None, :, :]), dim=-1)
124
+
125
+ self.register_buffer("freqs_cos", freqs.cos())
126
+ self.register_buffer("freqs_sin", freqs.sin())
127
+
128
+ # print('======== shape of rope freq', self.freqs_cos.shape, '========')
129
+
130
+ def forward(self, t, start_index=0):
131
+ rot_dim = self.freqs_cos.shape[-1]
132
+ end_index = start_index + rot_dim
133
+ assert rot_dim <= t.shape[-1], f'feature dimension {t.shape[-1]} is not of sufficient size to rotate in all the positions {rot_dim}'
134
+ t_left, t, t_right = t[..., :start_index], t[..., start_index:end_index], t[..., end_index:]
135
+ t = (t * self.freqs_cos) + (rotate_half(t) * self.freqs_sin)
136
+ return torch.cat((t_left, t, t_right), dim=-1)
137
+
138
+
139
+ class VisionRotaryEmbeddingFast(nn.Module):
140
+ def __init__(
141
+ self,
142
+ dim,
143
+ pt_seq_len=16,
144
+ ft_seq_len=None,
145
+ custom_freqs=None,
146
+ freqs_for='lang',
147
+ theta=10000,
148
+ max_freq=10,
149
+ num_freqs=1,
150
+ ):
151
+ super().__init__()
152
+ if custom_freqs:
153
+ freqs = custom_freqs
154
+ elif freqs_for == 'lang':
155
+ freqs = 1. / (theta ** (torch.arange(0, dim, 2)[:(dim // 2)].float() / dim))
156
+ elif freqs_for == 'pixel':
157
+ freqs = torch.linspace(1., max_freq / 2, dim // 2) * pi
158
+ elif freqs_for == 'constant':
159
+ freqs = torch.ones(num_freqs).float()
160
+ else:
161
+ raise ValueError(f'unknown modality {freqs_for}')
162
+
163
+ if ft_seq_len is None:
164
+ ft_seq_len = pt_seq_len
165
+ t = torch.arange(ft_seq_len) / ft_seq_len * pt_seq_len
166
+
167
+ freqs = torch.einsum('..., f -> ... f', t, freqs)
168
+ freqs = repeat(freqs, '... n -> ... (n r)', r=2)
169
+ freqs = broadcat((freqs[:, None, :], freqs[None, :, :]), dim=-1)
170
+
171
+ freqs_cos = freqs.cos().view(-1, freqs.shape[-1])
172
+ freqs_sin = freqs.sin().view(-1, freqs.shape[-1])
173
+
174
+ self.register_buffer("freqs_cos", freqs_cos)
175
+ self.register_buffer("freqs_sin", freqs_sin)
176
+
177
+ # print('======== shape of rope freq', self.freqs_cos.shape,freqs_sin.shape, '========')
178
+
179
+ def forward(self, t):
180
+ # print('======== shape of t', t.shape, '========')
181
+ _, _, Lt, _ = t.shape # B, num_heads, L, dim
182
+ L, _ = self.freqs_cos.shape # L, dim
183
+ repeat = Lt // L
184
+ freqs_cos, freqs_sin = self.freqs_cos, self.freqs_sin
185
+ if repeat != 1:
186
+ freqs_cos = freqs_cos.repeat_interleave(repeat, dim=0)
187
+ freqs_sin = freqs_sin.repeat_interleave(repeat, dim=0)
188
+ # print('======== shape of rope freq', freqs_cos.shape,freqs_sin.shape, '========')
189
+ # print(f'======== repeat {repeat} times ========')
190
+ # print(f'======== shape of t {t.shape} ========')
191
+ # # assert the repeat is twice
192
+ # #assert repeat == 2, f'repeat should be 2, but got {repeat}'
193
+ # # check the content of the repeated freqs
194
+ # # the content at odd index should be the same as the content at even index
195
+ # assert torch.allclose(freqs_cos[::2], freqs_cos[1::2]), 'repeated freqs_cos are not the same'
196
+ # assert torch.allclose(freqs_sin[::2], freqs_sin[1::2]), 'repeated freqs_sin are not the same'
197
+ # apply repeated freqs
198
+ return t * freqs_cos + rotate_half(t) * freqs_sin
199
+
200
+
201
+ class RelativePositionBias2D(nn.Module):
202
+ """
203
+ 2D relative positional bias for full self-attention.
204
+ Creates a learnable bias table of size (2*H-1) (2*W-1) per head,
205
+ and a fixed index map to look up bias for any pair of token positions.
206
+ """
207
+
208
+ def __init__(self, height: int, width: int, num_heads: int):
209
+ super().__init__()
210
+ self.height = height
211
+ self.width = width
212
+ self.num_heads = num_heads
213
+
214
+ # Create a bias table: one bias for every possible relative offset
215
+ # in y ∈ [-(H-1)..(H-1)] and x ∈ [-(W-1)..(W-1)]
216
+ self.relative_bias_table = nn.Parameter(
217
+ torch.zeros((2 * height - 1) * (2 * width - 1), num_heads)
218
+ )
219
+ # Precompute a (H*W)×(H*W) index matrix of which bias entry each pair (i,j) uses
220
+ coords_h = torch.arange(height)
221
+ coords_w = torch.arange(width)
222
+ # meshgrid of absolute coords, shape (H*W, 2)
223
+ coords = torch.stack(torch.meshgrid(coords_h, coords_w, indexing='ij'), dim=-1).view(-1, 2)
224
+
225
+ # Compute all pairwise relative coords
226
+ relative_coords = coords[:, None, :] - coords[None, :, :] # shape (HW, HW, 2)
227
+ # shift to positive
228
+ relative_coords[..., 0] += height - 1 # y
229
+ relative_coords[..., 1] += width - 1 # x
230
+
231
+ # flatten 2D index into single index: idx = y*(2W-1) + x
232
+ relative_index = relative_coords[..., 0] * (2 * width - 1) + relative_coords[..., 1]
233
+ # register as buffer so it’s on the right device / dtype
234
+ self.register_buffer("relative_index", relative_index.long())
235
+
236
+ def forward(self):
237
+ """
238
+ Returns:
239
+ bias: Tensor of shape (1, num_heads, HW, HW)
240
+ to be added to the raw attention logits before softmax.
241
+ """
242
+ # Lookup and reshape to (HW, HW, num_heads)
243
+ bias = self.relative_bias_table[self.relative_index.view(-1)] # (HW*HW, num_heads)
244
+ bias = bias.view(self.height * self.width,
245
+ self.height * self.width,
246
+ self.num_heads) # (HW, HW, heads)
247
+ # permute to (heads, HW, HW) and add batch-dim
248
+ bias = bias.permute(2, 0, 1).unsqueeze(0) # (1, heads, HW, HW)
249
+ return bias
250
+
251
+
252
+ class SwiGLUFFN(nn.Module):
253
+ def __init__(
254
+ self,
255
+ in_features: int,
256
+ hidden_features: Optional[int] = None,
257
+ out_features: Optional[int] = None,
258
+ act_layer: Callable[..., nn.Module] = None,
259
+ drop: float = 0.0,
260
+ bias: bool = True,
261
+ ) -> None:
262
+ super().__init__()
263
+ out_features = out_features or in_features
264
+ hidden_features = hidden_features or in_features
265
+ self.w12 = nn.Linear(in_features, 2 * hidden_features, bias=bias)
266
+ self.w3 = nn.Linear(hidden_features, out_features, bias=bias)
267
+
268
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
269
+ x12 = self.w12(x)
270
+ x1, x2 = x12.chunk(2, dim=-1)
271
+ hidden = F.silu(x1) * x2
272
+ return self.w3(hidden)
273
+
274
+
275
+ class RMSNorm(torch.nn.Module):
276
+ def __init__(self, dim: int, eps: float = 1e-6):
277
+ """
278
+ Initialize the RMSNorm normalization layer.
279
+
280
+ Args:
281
+ dim (int): The dimension of the input tensor.
282
+ eps (float, optional): A small value added to the denominator for numerical stability. Default is 1e-6.
283
+
284
+ Attributes:
285
+ eps (float): A small value added to the denominator for numerical stability.
286
+ weight (nn.Parameter): Learnable scaling parameter.
287
+
288
+ """
289
+ super().__init__()
290
+ self.eps = eps
291
+ self.weight = nn.Parameter(torch.ones(dim))
292
+
293
+ def _norm(self, x):
294
+ """
295
+ Apply the RMSNorm normalization to the input tensor.
296
+
297
+ Args:
298
+ x (torch.Tensor): The input tensor.
299
+
300
+ Returns:
301
+ torch.Tensor: The normalized tensor.
302
+
303
+ """
304
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
305
+
306
+ def forward(self, x):
307
+ """
308
+ Forward pass through the RMSNorm layer.
309
+
310
+ Args:
311
+ x (torch.Tensor): The input tensor.
312
+
313
+ Returns:
314
+ torch.Tensor: The output tensor after applying RMSNorm.
315
+
316
+ """
317
+ output = self._norm(x.float()).type_as(x)
318
+ return output * self.weight
319
+
320
+
321
+ class NormAttention(nn.Module):
322
+ """
323
+ Attention module of LightningDiT.
324
+ """
325
+ def __init__(
326
+ self,
327
+ dim: int,
328
+ num_heads: int = 8,
329
+ qkv_bias: bool = False,
330
+ qk_norm: bool = False,
331
+ attn_drop: float = 0.,
332
+ proj_drop: float = 0.,
333
+ norm_layer: nn.Module = nn.LayerNorm,
334
+ fused_attn: bool = True,
335
+ use_rmsnorm: bool = False,
336
+ ) -> None:
337
+ super().__init__()
338
+ assert dim % num_heads == 0, 'dim should be divisible by num_heads'
339
+
340
+ self.num_heads = num_heads
341
+ self.head_dim = dim // num_heads
342
+ self.scale = self.head_dim ** -0.5
343
+ self.fused_attn = fused_attn
344
+
345
+ if use_rmsnorm:
346
+ norm_layer = RMSNorm
347
+
348
+ self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
349
+ self.q_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
350
+ self.k_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
351
+ self.attn_drop = nn.Dropout(attn_drop)
352
+ self.proj = nn.Linear(dim, dim)
353
+ self.proj_drop = nn.Dropout(proj_drop)
354
+
355
+ def forward(self, x: torch.Tensor, rope=None) -> torch.Tensor:
356
+ B, N, C = x.shape
357
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
358
+ q, k, v = qkv.unbind(0)
359
+ q, k = self.q_norm(q), self.k_norm(k)
360
+
361
+ if rope is not None:
362
+ q = rope(q)
363
+ k = rope(k)
364
+
365
+ if self.fused_attn:
366
+ q = q.to(v.dtype)
367
+ k = k.to(v.dtype) # rope may change the q,k's dtype
368
+ x = F.scaled_dot_product_attention(
369
+ q, k, v,
370
+ dropout_p=self.attn_drop.p if self.training else 0.,
371
+ )
372
+ else:
373
+ q = q * self.scale
374
+ attn = q @ k.transpose(-2, -1)
375
+ attn = attn.softmax(dim=-1)
376
+ attn = self.attn_drop(attn)
377
+ x = attn @ v
378
+
379
+ x = x.transpose(1, 2).reshape(B, N, C)
380
+ x = self.proj(x)
381
+ x = self.proj_drop(x)
382
+ return x
383
+
384
+
385
+ class GaussianFourierEmbedding(nn.Module):
386
+ """
387
+ Gaussian Fourier Embedding for timesteps.
388
+ """
389
+ embedding_size: int = 256
390
+ scale: float = 1.0
391
+ def __init__(self, hidden_size: int, embedding_size: int = 256, scale: float = 1.0):
392
+ super().__init__()
393
+ self.embedding_size = embedding_size
394
+ self.scale = scale
395
+ self.register_buffer('W', torch.normal(0, self.scale, (embedding_size,)))
396
+ self.mlp = nn.Sequential(
397
+ nn.Linear(embedding_size * 2, hidden_size, bias=True),
398
+ nn.SiLU(),
399
+ nn.Linear(hidden_size, hidden_size, bias=True),
400
+ )
401
+ def forward(self, t):
402
+ with torch.no_grad():
403
+ W = self.W # stop gradient manually
404
+ t = t[:, None] * W[None, :] * 2 * torch.pi
405
+ # Concatenate sine and cosine transformations
406
+ t_embed = torch.cat([torch.sin(t), torch.cos(t)], dim=-1)
407
+ t_embed = self.mlp(t_embed)
408
+ return t_embed
409
+
410
+
411
+ class LabelEmbedder(nn.Module):
412
+ """
413
+ Embeds class labels into vector representations. Also handles label dropout for classifier-free guidance.
414
+ """
415
+ def __init__(self, num_classes, hidden_size, dropout_prob):
416
+ super().__init__()
417
+ use_cfg_embedding = dropout_prob > 0
418
+ self.embedding_table = nn.Embedding(num_classes + use_cfg_embedding, hidden_size)
419
+ self.num_classes = num_classes
420
+ self.dropout_prob = dropout_prob
421
+
422
+ def token_drop(self, labels, force_drop_ids=None):
423
+ """
424
+ Drops labels to enable classifier-free guidance.
425
+ """
426
+ if force_drop_ids is None:
427
+ drop_ids = torch.rand(labels.shape[0], device=labels.device) < self.dropout_prob
428
+ else:
429
+ drop_ids = force_drop_ids == 1
430
+ labels = torch.where(drop_ids, self.num_classes, labels)
431
+ return labels
432
+
433
+ def forward(self, labels, train, force_drop_ids=None):
434
+ use_dropout = self.dropout_prob > 0
435
+ if (train and use_dropout) or (force_drop_ids is not None):
436
+ labels = self.token_drop(labels, force_drop_ids)
437
+ embeddings = self.embedding_table(labels)
438
+ return embeddings
patch-reparameterization/src/stage2/transport/__init__.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .transport import Transport, ModelType, WeightType, PathType, Sampler
2
+
3
+ def create_transport(
4
+ path_type='Linear',
5
+ prediction="velocity",
6
+ loss_weight=None,
7
+ train_eps=None,
8
+ sample_eps=None,
9
+ time_dist_type="uniform",
10
+ time_dist_shift=1.0,
11
+ sem_dim=None,
12
+ recon_loss_weight=1.0,
13
+ normalize_channel_weight=False,
14
+ **kwargs
15
+ ):
16
+ """function for creating Transport object
17
+ **Note**: model prediction defaults to velocity
18
+ Args:
19
+ - path_type: type of path to use; default to linear
20
+ - learn_score: set model prediction to score
21
+ - learn_noise: set model prediction to noise
22
+ - velocity_weighted: weight loss by velocity weight
23
+ - likelihood_weighted: weight loss by likelihood weight
24
+ - train_eps: small epsilon for avoiding instability during training
25
+ - sample_eps: small epsilon for avoiding instability during sampling
26
+ - time_dist_type: type of time distribution to use; default to uniform
27
+ - time_dist_shift: shift for time distribution; default to 1.0
28
+ """
29
+
30
+ if prediction == "noise":
31
+ model_type = ModelType.NOISE
32
+ elif prediction == "score":
33
+ model_type = ModelType.SCORE
34
+ else:
35
+ model_type = ModelType.VELOCITY
36
+
37
+ if loss_weight == "velocity":
38
+ loss_type = WeightType.VELOCITY
39
+ elif loss_weight == "likelihood":
40
+ loss_type = WeightType.LIKELIHOOD
41
+ else:
42
+ loss_type = WeightType.NONE
43
+
44
+ path_choice = {
45
+ "Linear": PathType.LINEAR,
46
+ "GVP": PathType.GVP,
47
+ "VP": PathType.VP,
48
+ "SemanticRecon": PathType.SEMANTIC_RECON,
49
+ }
50
+
51
+ path_type = path_choice[path_type]
52
+
53
+ if (path_type in [PathType.VP]):
54
+ train_eps = 1e-5 if train_eps is None else train_eps
55
+ sample_eps = 1e-3 if train_eps is None else sample_eps
56
+ elif (path_type in [PathType.GVP, PathType.LINEAR, PathType.SEMANTIC_RECON] and model_type != ModelType.VELOCITY):
57
+ train_eps = 1e-3 if train_eps is None else train_eps
58
+ sample_eps = 1e-3 if train_eps is None else sample_eps
59
+ else: # velocity & [GVP, LINEAR, SEMANTIC_RECON] is stable everywhere
60
+ train_eps = 0
61
+ sample_eps = 0
62
+
63
+ use_time_shift = kwargs.get("use_time_shift", True)
64
+ # create flow state
65
+ state = Transport(
66
+ model_type=model_type,
67
+ path_type=path_type,
68
+ loss_type=loss_type,
69
+ time_dist_type=time_dist_type,
70
+ time_dist_shift=time_dist_shift,
71
+ train_eps=train_eps,
72
+ sample_eps=sample_eps,
73
+ use_time_shift=use_time_shift,
74
+ sem_dim=sem_dim,
75
+ recon_loss_weight=recon_loss_weight,
76
+ normalize_channel_weight=normalize_channel_weight,
77
+ )
78
+
79
+ return state
patch-reparameterization/src/stage2/transport/integrators.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch as th
3
+ import torch.nn as nn
4
+ from torchdiffeq import odeint
5
+ from functools import partial
6
+ from tqdm import tqdm
7
+ import logging
8
+ logger = logging.getLogger(__name__)
9
+ class sde:
10
+ """SDE solver class"""
11
+ def __init__(
12
+ self,
13
+ drift,
14
+ diffusion,
15
+ *,
16
+ t0,
17
+ t1,
18
+ num_steps,
19
+ sampler_type,
20
+ time_dist_shift,
21
+ ):
22
+ assert t0 < t1, "SDE sampler has to be in forward time"
23
+
24
+ self.num_timesteps = num_steps
25
+ self.t = 1 - th.linspace(t0, t1, num_steps)
26
+ self.t = time_dist_shift * self.t / (1 + (time_dist_shift - 1) * self.t)
27
+ self.drift = drift
28
+ self.diffusion = diffusion
29
+ self.sampler_type = sampler_type
30
+ self.time_dist_shift = time_dist_shift
31
+
32
+ def __Euler_Maruyama_step(self, x, mean_x, t_curr, t_next, model, **model_kwargs):
33
+ w_cur = th.randn(x.size()).to(x)
34
+ t = th.ones(x.size(0)).to(x) * t_curr
35
+ dw = w_cur * th.sqrt(t_curr - t_next)
36
+ drift = self.drift(x, t, model, **model_kwargs)
37
+ diffusion = self.diffusion(x, t)
38
+ mean_x = x - drift * (t_curr - t_next)
39
+ x = mean_x + th.sqrt(2 * diffusion) * dw
40
+ return x, mean_x
41
+
42
+ def __Heun_step(self, x, _, t_curr, t_next, model, **model_kwargs):
43
+ w_cur = th.randn(x.size()).to(x)
44
+ dw = w_cur * th.sqrt(t_curr - t_next)
45
+ diffusion = self.diffusion(x, th.ones(x.size(0)).to(x) * t_curr)
46
+ xhat = x + th.sqrt(2 * diffusion) * dw
47
+ K1 = self.drift(
48
+ xhat, th.ones(x.size(0)).to(x) * t_curr, model, **model_kwargs
49
+ )
50
+ xp = xhat - (t_curr - t_next) * K1
51
+ K2 = self.drift(
52
+ xp, th.ones(x.size(0)).to(x) * t_next, model, **model_kwargs
53
+ )
54
+ return xhat - 0.5 * (t_curr - t_next) * (K1 + K2), xhat # at last time point we do not perform the heun step
55
+
56
+ def __forward_fn(self):
57
+ """TODO: generalize here by adding all private functions ending with steps to it"""
58
+ sampler_dict = {
59
+ "euler": self.__Euler_Maruyama_step,
60
+ "heun": self.__Heun_step,
61
+ }
62
+
63
+ try:
64
+ sampler = sampler_dict[self.sampler_type]
65
+ except:
66
+ raise NotImplementedError("Smapler type not implemented.")
67
+
68
+ return sampler
69
+
70
+ def sample(self, init, model, **model_kwargs) -> tuple[th.Tensor]:
71
+ """forward loop of sde"""
72
+ x = init
73
+ mean_x = init
74
+ samples = []
75
+ sampler = self.__forward_fn()
76
+ for t_curr, t_next in zip(self.t[:-1], self.t[1:]):
77
+ with th.no_grad():
78
+ x, mean_x = sampler(x, mean_x, t_curr, t_next, model, **model_kwargs)
79
+ samples.append(x)
80
+
81
+ return samples
82
+
83
+ class ode:
84
+ """ODE solver class"""
85
+ def __init__(
86
+ self,
87
+ drift,
88
+ *,
89
+ t0,
90
+ t1,
91
+ sampler_type,
92
+ num_steps,
93
+ atol,
94
+ rtol,
95
+ time_dist_shift,
96
+ use_time_shift=True
97
+ ):
98
+ # Allow t0 > t1 for reverse (data -> noise) integration
99
+ assert t0 != t1, "ODE sampler needs distinct endpoints"
100
+ if t0 > t1:
101
+ t0, t1 = t1, t0 # linspace(t0, t1) must go low -> high
102
+ self.drift = drift
103
+ # num_steps=1 means 1 euler step, need 2 time points (start + end)
104
+ num_t_points = max(num_steps, 2)
105
+ self.t = 1 - th.linspace(t0, t1, num_t_points)
106
+ if use_time_shift:
107
+ self.t = time_dist_shift * self.t / (1 + (time_dist_shift - 1) * self.t)
108
+ else:
109
+ logger.warning("Sampling Time shift is disabled.")
110
+
111
+ self.atol = atol
112
+ self.rtol = rtol
113
+ self.sampler_type = sampler_type
114
+
115
+ def sample(self, x, model, **model_kwargs) -> tuple[th.Tensor]:
116
+
117
+ device = x[0].device if isinstance(x, tuple) else x.device
118
+ def _fn(t, x):
119
+ t = th.ones(x[0].size(0)).to(device) * t if isinstance(x, tuple) else th.ones(x.size(0)).to(device) * t
120
+ model_output = self.drift(x, t, model, **model_kwargs)
121
+ return model_output
122
+
123
+ t = self.t.to(device)
124
+ atol = [self.atol] * len(x) if isinstance(x, tuple) else [self.atol]
125
+ rtol = [self.rtol] * len(x) if isinstance(x, tuple) else [self.rtol]
126
+ samples = odeint(
127
+ _fn,
128
+ x,
129
+ t,
130
+ method=self.sampler_type,
131
+ atol=atol,
132
+ rtol=rtol
133
+ )
134
+ return samples
patch-reparameterization/src/stage2/transport/path.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch as th
2
+ import numpy as np
3
+ from functools import partial
4
+ import logging
5
+ logger = logging.getLogger(__name__)
6
+ def expand_t_like_x(t, x):
7
+ """Function to reshape time t to broadcastable dimension of x
8
+ Args:
9
+ t: [batch_dim,], time vector
10
+ x: [batch_dim,...], data point
11
+ """
12
+ dims = [1] * (len(x.size()) - 1)
13
+ t = t.view(t.size(0), *dims)
14
+ return t
15
+
16
+
17
+ #################### Coupling Plans ####################
18
+
19
+ class ICPlan:
20
+ """Linear Coupling Plan"""
21
+ def __init__(self, sigma=0.0):
22
+ self.sigma = sigma
23
+
24
+ def compute_alpha_t(self, t):
25
+ """Compute the data coefficient along the path"""
26
+ return 1 - t, -1
27
+
28
+ def compute_sigma_t(self, t):
29
+ """Compute the noise coefficient along the path"""
30
+ return t, 1
31
+
32
+ def compute_d_alpha_alpha_ratio_t(self, t):
33
+ """Compute the ratio between d_alpha and alpha"""
34
+ return -1 / (1 - t)
35
+
36
+ def compute_drift(self, x, t):
37
+ """We always output sde according to score parametrization; """
38
+ t = expand_t_like_x(t, x)
39
+ alpha_ratio = self.compute_d_alpha_alpha_ratio_t(t)
40
+ sigma_t, d_sigma_t = self.compute_sigma_t(t)
41
+ drift = alpha_ratio * x
42
+ diffusion = alpha_ratio * (sigma_t ** 2) - sigma_t * d_sigma_t
43
+
44
+ return -drift, diffusion
45
+
46
+ def compute_diffusion(self, x, t, form="constant", norm=1.0):
47
+ """Compute the diffusion term of the SDE
48
+ Args:
49
+ x: [batch_dim, ...], data point
50
+ t: [batch_dim,], time vector
51
+ form: str, form of the diffusion term
52
+ norm: float, norm of the diffusion term
53
+ """
54
+ t = expand_t_like_x(t, x)
55
+ choices = {
56
+ "constant": norm,
57
+ "SBDM": norm * self.compute_drift(x, t)[1],
58
+ "sigma": norm * self.compute_sigma_t(t)[0],
59
+ # "linear": norm * (1 - t),
60
+ "linear": norm * t,
61
+ "decreasing": 0.25 * (norm * th.cos(np.pi * (1 - t)) + 1) ** 2,
62
+ "inccreasing-decreasing": norm * th.sin(np.pi * (1 - t)) ** 2,
63
+ }
64
+
65
+ try:
66
+ diffusion = choices[form]
67
+ except KeyError:
68
+ raise NotImplementedError(f"Diffusion form {form} not implemented")
69
+
70
+ return diffusion
71
+
72
+ def get_score_from_velocity(self, velocity, x, t):
73
+ """Wrapper function: transfrom velocity prediction model to score
74
+ Args:
75
+ velocity: [batch_dim, ...] shaped tensor; velocity model output
76
+ x: [batch_dim, ...] shaped tensor; x_t data point
77
+ t: [batch_dim,] time tensor
78
+ """
79
+ t = expand_t_like_x(t, x)
80
+ alpha_t, d_alpha_t = self.compute_alpha_t(t)
81
+ sigma_t, d_sigma_t = self.compute_sigma_t(t)
82
+ mean = x
83
+ reverse_alpha_ratio = alpha_t / d_alpha_t
84
+ var = sigma_t**2 - reverse_alpha_ratio * d_sigma_t * sigma_t
85
+ score = (reverse_alpha_ratio * velocity - mean) / var
86
+ return score
87
+
88
+ def get_noise_from_velocity(self, velocity, x, t):
89
+ """Wrapper function: transfrom velocity prediction model to denoiser
90
+ Args:
91
+ velocity: [batch_dim, ...] shaped tensor; velocity model output
92
+ x: [batch_dim, ...] shaped tensor; x_t data point
93
+ t: [batch_dim,] time tensor
94
+ """
95
+ t = expand_t_like_x(t, x)
96
+ alpha_t, d_alpha_t = self.compute_alpha_t(t)
97
+ sigma_t, d_sigma_t = self.compute_sigma_t(t)
98
+ mean = x
99
+ reverse_alpha_ratio = alpha_t / d_alpha_t
100
+ var = reverse_alpha_ratio * d_sigma_t - sigma_t
101
+ noise = (reverse_alpha_ratio * velocity - mean) / var
102
+ return noise
103
+
104
+ def get_velocity_from_score(self, score, x, t):
105
+ """Wrapper function: transfrom score prediction model to velocity
106
+ Args:
107
+ score: [batch_dim, ...] shaped tensor; score model output
108
+ x: [batch_dim, ...] shaped tensor; x_t data point
109
+ t: [batch_dim,] time tensor
110
+ """
111
+ t = expand_t_like_x(t, x)
112
+ drift, var = self.compute_drift(x, t)
113
+ velocity = var * score - drift
114
+ return velocity
115
+
116
+ def compute_mu_t(self, t, x0, x1):
117
+ """Compute the mean of time-dependent density p_t"""
118
+ t = expand_t_like_x(t, x1)
119
+ alpha_t, _ = self.compute_alpha_t(t)
120
+ sigma_t, _ = self.compute_sigma_t(t)
121
+ return alpha_t * x1 + sigma_t * x0
122
+
123
+ def compute_xt(self, t, x0, x1):
124
+ """Sample xt from time-dependent density p_t; rng is required"""
125
+ xt = self.compute_mu_t(t, x0, x1)
126
+ return xt
127
+
128
+ def compute_ut(self, t, x0, x1, xt):
129
+ """Compute the vector field corresponding to p_t"""
130
+ t = expand_t_like_x(t, x1)
131
+ _, d_alpha_t = self.compute_alpha_t(t)
132
+ _, d_sigma_t = self.compute_sigma_t(t)
133
+ return d_alpha_t * x1 + d_sigma_t * x0
134
+
135
+ def plan(self, t, x0, x1, **kwargs):
136
+ """Compute (t, xt, ut). Extra kwargs are accepted but ignored so that
137
+ callers can unconditionally pass semantic=/recon= without branching."""
138
+ xt = self.compute_xt(t, x0, x1)
139
+ ut = self.compute_ut(t, x0, x1, xt)
140
+ return t, xt, ut
141
+
142
+
143
+ class VPCPlan(ICPlan):
144
+ """class for VP path flow matching"""
145
+
146
+ def __init__(self, sigma_min=0.1, sigma_max=20.0):
147
+ self.sigma_min = sigma_min
148
+ self.sigma_max = sigma_max
149
+ self.log_mean_coeff = lambda t: -0.25 * ((1 - t) ** 2) * (self.sigma_max - self.sigma_min) - 0.5 * (1 - t) * self.sigma_min
150
+ self.d_log_mean_coeff = lambda t: 0.5 * (1 - t) * (self.sigma_max - self.sigma_min) + 0.5 * self.sigma_min
151
+
152
+ def compute_alpha_t(self, t):
153
+ """Compute coefficient of x0"""
154
+ p_alpha_t = 2 * self.log_mean_coeff(t)
155
+ alpha_t = th.sqrt(1 - th.exp(p_alpha_t))
156
+ d_alpha_t = th.exp(p_alpha_t) * (2 * self.d_log_mean_coeff(t)) / (-2 * alpha_t)
157
+ return alpha_t, d_alpha_t
158
+
159
+ def compute_sigma_t(self, t):
160
+ """Compute coefficient of x1"""
161
+ sigma_t = self.log_mean_coeff(t)
162
+ sigma_t = th.exp(sigma_t)
163
+ d_sigma_t = sigma_t * self.d_log_mean_coeff(t)
164
+ return sigma_t, d_sigma_t
165
+
166
+ def compute_d_alpha_alpha_ratio_t(self, t):
167
+ """Special purposed function for computing numerical stabled d_alpha_t / alpha_t"""
168
+ alpha_t, d_alpha_t = self.compute_alpha_t(t)
169
+ return d_alpha_t / alpha_t
170
+
171
+ def compute_drift(self, x, t):
172
+ """Compute the drift term of the SDE"""
173
+ t = expand_t_like_x(t, x)
174
+ beta_t = self.sigma_min + (1 - t) * (self.sigma_max - self.sigma_min)
175
+ return -0.5 * beta_t * x, beta_t / 2
176
+
177
+
178
+ class GVPCPlan(ICPlan):
179
+ def __init__(self, sigma=0.0):
180
+ super().__init__(sigma)
181
+
182
+ def compute_alpha_t(self, t):
183
+ """Compute coefficient of x1"""
184
+ alpha_t = th.cos(t * np.pi / 2)
185
+ d_alpha_t = -np.pi / 2 * th.sin(t * np.pi / 2)
186
+ return alpha_t, d_alpha_t
187
+
188
+ def compute_sigma_t(self, t):
189
+ """Compute coefficient of x0"""
190
+ sigma_t = th.sin(t * np.pi / 2)
191
+ d_sigma_t = np.pi / 2 * th.cos(t * np.pi / 2)
192
+ return sigma_t, d_sigma_t
193
+
194
+ def compute_d_alpha_alpha_ratio_t(self, t):
195
+ """Special purposed function for computing numerical stabled d_alpha_t / alpha_t"""
196
+ return -np.pi / 2 * th.tan(t * np.pi / 2)
197
+
198
+ class SemanticReconPlan(ICPlan):
199
+ """Flow Matching plan with a time-dependent endpoint.
200
+
201
+ Instead of a fixed x1, the target endpoint varies with time:
202
+ x1(t) = semantic + (1 - t) * recon
203
+
204
+ where ``semantic`` (s) and ``recon`` (z) are the normalised semantic and
205
+ reconstruction components of the latent, satisfying s + z = x1_standard.
206
+
207
+ Derived interpolant and velocity field:
208
+ xt = (1-t) * x1(t) + t * x0
209
+ = (1-t) * [s + (1-t)*z] + t * x0
210
+
211
+ ut = d(xt)/dt
212
+ = x0 - s - 2*(1-t)*z
213
+
214
+ When ``semantic`` or ``recon`` is None (e.g. at inference time or when the
215
+ feature is unavailable), the plan falls back to the standard ICPlan
216
+ behaviour so that existing code paths are not broken.
217
+ """
218
+
219
+ def plan(self, t, x0, x1, semantic=None, recon=None):
220
+ """Compute (t, xt, ut) for training.
221
+
222
+ Args:
223
+ t: [B] time samples in [0, 1]
224
+ x0: [B, ...] noise samples ~ N(0, I)
225
+ x1: [B, ...] data samples (= semantic + recon, normalised)
226
+ semantic: [B, ...] normalised semantic component s (optional)
227
+ recon: [B, ...] normalised recon component z (optional)
228
+
229
+ Returns:
230
+ t, xt, ut – all [B, ...]
231
+ """
232
+ # logger.debug(f"sementic: {semantic}")
233
+ # logger.debug(f"recon: {recon}")
234
+ if semantic is None or recon is None:
235
+ # Graceful fallback: standard linear interpolant
236
+ return super().plan(t, x0, x1)
237
+
238
+ t_exp = expand_t_like_x(t, semantic) # [B, 1, 1, ...] broadcastable
239
+ x1_t = semantic + (1 - t_exp) * recon # time-dependent endpoint
240
+ xt = (1 - t_exp) * x1_t + t_exp * x0 # interpolant
241
+ ut = x0 - semantic - 2 * (1 - t_exp) * recon # analytic velocity field
242
+ return t, xt, ut
patch-reparameterization/src/stage2/transport/transport.py ADDED
@@ -0,0 +1,614 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch as th
2
+ import numpy as np
3
+ import logging
4
+
5
+ import enum
6
+
7
+ from . import path
8
+ from .utils import EasyDict, log_state, mean_flat
9
+ from .integrators import ode, sde
10
+ import logging
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class ModelType(enum.Enum):
14
+ """
15
+ Which type of output the model predicts.
16
+ """
17
+
18
+ NOISE = enum.auto() # the model predicts epsilon
19
+ SCORE = enum.auto() # the model predicts \nabla \log p(x)
20
+ VELOCITY = enum.auto() # the model predicts v(x)
21
+
22
+ class PathType(enum.Enum):
23
+ """
24
+ Which type of path to use.
25
+ """
26
+
27
+ LINEAR = enum.auto()
28
+ GVP = enum.auto()
29
+ VP = enum.auto()
30
+ SEMANTIC_RECON = enum.auto() # time-dependent endpoint: x1(t) = s + (1-t)*z
31
+
32
+ class WeightType(enum.Enum):
33
+ """
34
+ Which type of weighting to use.
35
+ """
36
+
37
+ NONE = enum.auto()
38
+ VELOCITY = enum.auto()
39
+ LIKELIHOOD = enum.auto()
40
+
41
+
42
+ def truncated_logitnormal_sample(
43
+ shape, mu, sigma, low=0.0, high=1.0
44
+ ):
45
+ """
46
+ Samples X in (0,1) with Z = logit(X) ~ Normal(mu, sigma^2), truncated so X in [low, high].
47
+ Works for scalars or tensors mu/sigma/low/high with broadcasting.
48
+
49
+ Args:
50
+ shape: output batch shape (e.g., (N,) or (N,M)). Leave () to broadcast to mu.shape.
51
+ mu, sigma: tensors or floats (sigma > 0).
52
+ low, high: truncation bounds in [0,1]. (low can be 0, high can be 1).
53
+ device, dtype: optional overrides.
54
+
55
+ Returns:
56
+ Tensor of samples with shape = broadcast(shape, mu.shape, ...)
57
+ """
58
+ mu = th.as_tensor(mu)
59
+ sigma= th.as_tensor(sigma)
60
+ low = th.as_tensor(low)
61
+ high = th.as_tensor(high)
62
+
63
+ # Map truncation bounds to logit space; handles 0/1 → ±inf automatically.
64
+ z_low = th.logit(low) # = -inf if low==0
65
+ z_high = th.logit(high) # = +inf if high==1
66
+
67
+ # Standardize bounds for the base Normal(0,1)
68
+ base = th.distributions.Normal(th.zeros_like(mu), th.ones_like(sigma))
69
+ alpha = (z_low - mu) / sigma
70
+ beta = (z_high - mu) / sigma
71
+
72
+ # Truncated-normal inverse CDF sampling:
73
+ # U ~ Uniform(Φ(alpha), Φ(beta)); Z = mu + sigma * Φ^{-1}(U); X = sigmoid(Z)
74
+ cdf_alpha = base.cdf(alpha)
75
+ cdf_beta = base.cdf(beta)
76
+
77
+ # Draw uniforms on the truncated interval
78
+ out_shape = th.broadcast_shapes(shape, mu.shape, sigma.shape, low.shape, high.shape)
79
+ U = th.rand(out_shape, device=mu.device, dtype=mu.dtype)
80
+ U = cdf_alpha + (cdf_beta - cdf_alpha) * U.clamp_(0, 1)
81
+
82
+ Z = mu + sigma * base.icdf(U)
83
+ X = th.sigmoid(Z)
84
+
85
+ # Numerical safety when low/high are extremely close; clamp back into [low, high].
86
+ return X.clamp(low, high)
87
+
88
+
89
+ class Transport:
90
+
91
+ def __init__(
92
+ self,
93
+ *,
94
+ model_type,
95
+ path_type,
96
+ loss_type,
97
+ time_dist_type,
98
+ time_dist_shift,
99
+ train_eps,
100
+ sample_eps,
101
+ use_time_shift,
102
+ sem_dim=None,
103
+ recon_loss_weight=1.0,
104
+ normalize_channel_weight=False,
105
+ ):
106
+ path_options = {
107
+ PathType.LINEAR: path.ICPlan,
108
+ PathType.GVP: path.GVPCPlan,
109
+ PathType.VP: path.VPCPlan,
110
+ PathType.SEMANTIC_RECON: path.SemanticReconPlan,
111
+ }
112
+
113
+ self.loss_type = loss_type
114
+ self.model_type = model_type
115
+ self.time_dist_type = time_dist_type
116
+ self.time_dist_shift = time_dist_shift
117
+ assert self.time_dist_shift >= 1.0, "time distribution shift must be >= 1.0."
118
+ self.path_sampler = path_options[path_type]()
119
+ self.train_eps = train_eps
120
+ self.sample_eps = sample_eps
121
+ self.use_time_shift = use_time_shift
122
+ self.sem_dim = sem_dim
123
+ self.recon_loss_weight = float(recon_loss_weight)
124
+ self.normalize_channel_weight = normalize_channel_weight
125
+ if self.sem_dim is not None:
126
+ logger.info(f"Channel-wise loss: sem_dim={self.sem_dim}, recon_loss_weight={self.recon_loss_weight}, normalize={self.normalize_channel_weight}")
127
+
128
+ def prior_logp(self, z):
129
+ '''
130
+ Standard multivariate normal prior
131
+ Assume z is batched
132
+ '''
133
+ shape = th.tensor(z.size())
134
+ N = th.prod(shape[1:])
135
+ _fn = lambda x: -N / 2. * np.log(2 * np.pi) - th.sum(x ** 2) / 2.
136
+ return th.vmap(_fn)(z)
137
+
138
+
139
+ def check_interval(
140
+ self,
141
+ train_eps,
142
+ sample_eps,
143
+ *,
144
+ diffusion_form="SBDM",
145
+ sde=False,
146
+ reverse=False,
147
+ eval=False,
148
+ last_step_size=0.0,
149
+ ):
150
+ t0 = 0
151
+ t1 = 1 - 1 / 1000
152
+ eps = train_eps if not eval else sample_eps
153
+ if (type(self.path_sampler) in [path.VPCPlan]):
154
+
155
+ t1 = 1 - eps if (not sde or last_step_size == 0) else 1 - last_step_size
156
+
157
+ elif (type(self.path_sampler) in [path.ICPlan, path.GVPCPlan, path.SemanticReconPlan]) \
158
+ and (self.model_type != ModelType.VELOCITY or sde): # avoid numerical issue by taking a first semi-implicit step
159
+
160
+ t0 = eps if (diffusion_form == "SBDM" and sde) or self.model_type != ModelType.VELOCITY else 0
161
+ t1 = 1 - eps if (not sde or last_step_size == 0) else 1 - last_step_size
162
+
163
+ if reverse:
164
+ t0, t1 = 1 - t0, 1 - t1
165
+
166
+ return t0, t1
167
+
168
+
169
+ def sample(self, x1):
170
+ """Sampling x0 & t based on shape of x1 (if needed)
171
+ Args:
172
+ x1 - data point; [batch, *dim]
173
+ """
174
+
175
+ x0 = th.randn_like(x1)
176
+ dist_options = self.time_dist_type.split("_")
177
+ t0, t1 = self.check_interval(self.train_eps, self.sample_eps)
178
+ if dist_options[0] == "uniform":
179
+ t = th.rand((x1.shape[0],)) * (t1 - t0) + t0
180
+ # print('UNIFORM IS CALLED')
181
+ elif dist_options[0] == "logit-normal":
182
+ assert len(dist_options) == 3, "Logit-normal distribution must specify the mean and variance."
183
+ mu, sigma = float(dist_options[1]), float(dist_options[2])
184
+ assert sigma > 0, "Logit-normal distribution must have positive variance."
185
+ t = truncated_logitnormal_sample(
186
+ (x1.shape[0],), mu=mu, sigma=sigma, low=t0, high=t1
187
+ )
188
+ # print('LOGITNORMAL IS CALLED')
189
+ elif dist_options[0] == "fixed":
190
+ # Fixed t for 1-step training (e.g., "fixed_1.0")
191
+ fixed_t = float(dist_options[1]) if len(dist_options) > 1 else 1.0
192
+ t = th.full((x1.shape[0],), fixed_t)
193
+ else:
194
+ raise NotImplementedError(f"Unknown time distribution type {self.time_dist_type}")
195
+
196
+ t = t.to(x1)
197
+ # logger.debug(f"t before shift: {t}")
198
+ #sqrt_size_ratio = 1 / self.time_dist_shift # already sqrted
199
+ if self.use_time_shift:
200
+ t = self.time_dist_shift * t / (1 + (self.time_dist_shift - 1) * t)
201
+ # logger.debug(f"t after shift: {t}")
202
+ return t, x0, x1
203
+
204
+
205
+ def training_losses(
206
+ self,
207
+ model,
208
+ x1,
209
+ model_kwargs=None
210
+ ):
211
+ """Loss for training the score model
212
+ Args:
213
+ - model: backbone model; could be score, noise, or velocity
214
+ - x1: datapoint
215
+ - model_kwargs: additional arguments for the model
216
+ """
217
+ if model_kwargs == None:
218
+ model_kwargs = {}
219
+
220
+ # Extract auxiliary loss parameters so they aren't passed to the model's forward pass
221
+ aux_losses_cfg = model_kwargs.pop('aux_losses_cfg', {})
222
+ latent_var = model_kwargs.pop('latent_var', None)
223
+
224
+ # Pre-emptively pop all potential projection modules to avoid passing them to the model
225
+ proj_modules = {}
226
+ keys_to_pop = [k for k in model_kwargs.keys() if k.endswith('_proj')]
227
+ for k in keys_to_pop:
228
+ proj_name = k.replace('_proj', '')
229
+ proj_modules[proj_name] = model_kwargs.pop(k)
230
+
231
+ # Handle legacy 'semetic_proj' key if present
232
+ if 'semetic_proj' in model_kwargs:
233
+ proj_modules['semantic'] = model_kwargs.pop('semetic_proj')
234
+
235
+ # Pop semantic/recon components for SemanticReconPlan path construction.
236
+ # These must NOT be forwarded to the model's forward().
237
+ path_semantic = model_kwargs.pop('path_semantic', None)
238
+ path_recon = model_kwargs.pop('path_recon', None)
239
+
240
+ logger.debug(f"x1 shape{x1.shape}")
241
+ t, x0, x1 = self.sample(x1)
242
+ t, xt, ut = self.path_sampler.plan(t, x0, x1, semantic=path_semantic, recon=path_recon)
243
+ logger.debug(f"xt shape{xt.shape}")
244
+ model_output = model(xt, t, **model_kwargs)
245
+ logger.debug(f"model output shape{model_output.shape}")
246
+ B, *_, C = xt.shape
247
+ assert model_output.size() == (B, *xt.size()[1:-1], C)
248
+
249
+ terms = {}
250
+ terms['pred'] = model_output
251
+ terms['xt'] = xt
252
+ terms['t'] = t
253
+ terms['x0'] = x0
254
+ if self.model_type == ModelType.VELOCITY:
255
+ mse = (model_output - ut) ** 2
256
+ if self.sem_dim is not None and self.sem_dim > 0:
257
+ # Channel dim is dim=1 for [B,C,H,W] or dim=-1 for [B,N,C]
258
+ if mse.dim() == 4: # [B, C, H, W]
259
+ terms['loss_sem'] = mean_flat(mse[:, :self.sem_dim])
260
+ terms['loss_recon'] = mean_flat(mse[:, self.sem_dim:])
261
+ if self.recon_loss_weight != 1.0:
262
+ C = mse.shape[1]
263
+ recon_dim = C - self.sem_dim
264
+ sem_w = 1.0
265
+ recon_w = self.recon_loss_weight
266
+ if self.normalize_channel_weight:
267
+ norm_factor = C / (self.sem_dim * sem_w + recon_dim * recon_w)
268
+ sem_w *= norm_factor
269
+ recon_w *= norm_factor
270
+ weight = th.ones(1, C, 1, 1, device=mse.device)
271
+ weight[:, :self.sem_dim] = sem_w
272
+ weight[:, self.sem_dim:] = recon_w
273
+ mse = mse * weight
274
+ else: # [B, N, C]
275
+ terms['loss_sem'] = mean_flat(mse[..., :self.sem_dim])
276
+ terms['loss_recon'] = mean_flat(mse[..., self.sem_dim:])
277
+ if self.recon_loss_weight != 1.0:
278
+ C = mse.shape[-1]
279
+ recon_dim = C - self.sem_dim
280
+ sem_w = 1.0
281
+ recon_w = self.recon_loss_weight
282
+ if self.normalize_channel_weight:
283
+ norm_factor = C / (self.sem_dim * sem_w + recon_dim * recon_w)
284
+ sem_w *= norm_factor
285
+ recon_w *= norm_factor
286
+ weight = th.ones(1, 1, C, device=mse.device)
287
+ weight[..., :self.sem_dim] = sem_w
288
+ weight[..., self.sem_dim:] = recon_w
289
+ mse = mse * weight
290
+ terms['loss'] = mean_flat(mse)
291
+ else:
292
+ _, drift_var = self.path_sampler.compute_drift(xt, t)
293
+ sigma_t, _ = self.path_sampler.compute_sigma_t(path.expand_t_like_x(t, xt))
294
+ if self.loss_type in [WeightType.VELOCITY]:
295
+ weight = (drift_var / sigma_t) ** 2
296
+ elif self.loss_type in [WeightType.LIKELIHOOD]:
297
+ weight = drift_var / (sigma_t ** 2)
298
+ elif self.loss_type in [WeightType.NONE]:
299
+ weight = 1
300
+ else:
301
+ raise NotImplementedError()
302
+
303
+ if self.model_type == ModelType.NOISE:
304
+ terms['loss'] = mean_flat(weight * ((model_output - x0) ** 2))
305
+ else:
306
+ terms['loss'] = mean_flat(weight * ((model_output * sigma_t + x0) ** 2))
307
+
308
+ # --- Compute Auxiliary Losses (e.g., Semantic Loss) ---
309
+ if self.model_type == ModelType.VELOCITY and aux_losses_cfg:
310
+ # Apply base weight to the original loss
311
+ base_weight = aux_losses_cfg.get('base_weight', 1.0)
312
+ logger.debug(f"flow loss: {terms['loss'].detach().mean().item()}")
313
+ terms['loss'] = terms['loss'] * base_weight
314
+
315
+ # Prepare unnormalized velocity if any projection loss is enabled
316
+ # We check if any proj loss is enabled to avoid unnecessary computation
317
+ proj_losses_cfg = aux_losses_cfg.get('proj_losses', {})
318
+ need_unnorm = any(
319
+ isinstance(cfg, dict) and cfg.get('enabled', False)
320
+ for cfg in proj_losses_cfg.values()
321
+ )
322
+
323
+ v_pred_unnorm, v_gt_unnorm = None, None
324
+ if need_unnorm and latent_var is not None:
325
+ logger.debug(f"model_output shape: {model_output.shape}")
326
+ logger.debug(f"latent_var shape: {latent_var.shape}")
327
+
328
+ # 1. Convert model_output and ut to [B, N, C] if they are [B, C, H, W]
329
+ if model_output.dim() == 4:
330
+ B, C, H, W = model_output.shape
331
+ N = H * W
332
+ # [B, C, H, W] -> [B, C, N] -> [B, N, C]
333
+ v_pred_flat = model_output.view(B, C, N).transpose(1, 2)
334
+ v_gt_flat = ut.view(B, C, N).transpose(1, 2)
335
+ else:
336
+ v_pred_flat = model_output
337
+ v_gt_flat = ut
338
+ B, N, C = v_pred_flat.shape
339
+
340
+ # 2. Reshape latent_var to [1, N, C] or [1, 1, C]
341
+ if latent_var.dim() == 1:
342
+ # [C] -> [1, 1, C]
343
+ scale = th.sqrt(latent_var + 1e-5).view(1, 1, -1)
344
+ elif latent_var.dim() == 2:
345
+ # [N, C] -> [1, N, C]
346
+ scale = th.sqrt(latent_var + 1e-5).unsqueeze(0)
347
+ elif latent_var.dim() == 3:
348
+ # [C, H, W] -> [C, N] -> [N, C] -> [1, N, C]
349
+ scale = th.sqrt(latent_var + 1e-5).view(latent_var.shape[0], -1).transpose(0, 1).unsqueeze(0)
350
+ else:
351
+ scale = th.sqrt(latent_var + 1e-5)
352
+
353
+ # 3. Unnormalize
354
+ v_pred_unnorm = v_pred_flat * scale
355
+ v_gt_unnorm = v_gt_flat * scale
356
+
357
+ # --- Loop over projection losses ---
358
+ # This makes it easy to add more projection losses in the future
359
+ for proj_name, proj_cfg in proj_losses_cfg.items():
360
+ if isinstance(proj_cfg, dict) and proj_cfg.get('enabled', False):
361
+ # Get the projection module from our pre-popped dict
362
+ proj_module = proj_modules.get(proj_name)
363
+
364
+ if proj_module is not None:
365
+ if v_pred_unnorm is None:
366
+ logger.warning(f"{proj_name} loss enabled but latent_var is missing.")
367
+ continue
368
+
369
+ # Map to projection space
370
+ proj_v_pred = proj_module(v_pred_unnorm)
371
+ proj_v_gt = proj_module(v_gt_unnorm)
372
+
373
+ # Compute MSE
374
+ proj_loss = mean_flat(((proj_v_pred - proj_v_gt) ** 2))
375
+
376
+ logger.debug(f"{proj_name}_loss: {proj_loss.detach().mean().item()}")
377
+ weight = proj_cfg.get('weight', 1.0)
378
+
379
+ # Accumulate to total loss
380
+ terms['loss'] = terms['loss'] + weight * proj_loss
381
+ terms[f'{proj_name}_loss'] = proj_loss.detach()
382
+ else:
383
+ logger.warning(f"{proj_name} loss enabled but {proj_name}_proj not found in model_kwargs")
384
+ # logger.debug(terms)
385
+ return terms
386
+
387
+
388
+ def get_drift(
389
+ self
390
+ ):
391
+ """member function for obtaining the drift of the probability flow ODE"""
392
+ def score_ode(x, t, model, **model_kwargs):
393
+ drift_mean, drift_var = self.path_sampler.compute_drift(x, t)
394
+ model_output = model(x, t, **model_kwargs)
395
+ return (-drift_mean + drift_var * model_output) # by change of variable
396
+
397
+ def noise_ode(x, t, model, **model_kwargs):
398
+ drift_mean, drift_var = self.path_sampler.compute_drift(x, t)
399
+ sigma_t, _ = self.path_sampler.compute_sigma_t(path.expand_t_like_x(t, x))
400
+ model_output = model(x, t, **model_kwargs)
401
+ score = model_output / -sigma_t
402
+ return (-drift_mean + drift_var * score)
403
+
404
+ def velocity_ode(x, t, model, **model_kwargs):
405
+ model_output = model(x, t, **model_kwargs)
406
+ return model_output
407
+
408
+ if self.model_type == ModelType.NOISE:
409
+ drift_fn = noise_ode
410
+ elif self.model_type == ModelType.SCORE:
411
+ drift_fn = score_ode
412
+ else:
413
+ drift_fn = velocity_ode
414
+
415
+ def body_fn(x, t, model, **model_kwargs):
416
+ model_output = drift_fn(x, t, model, **model_kwargs)
417
+ assert model_output.shape == x.shape, "Output shape from ODE solver must match input shape"
418
+ return model_output
419
+
420
+ return body_fn
421
+
422
+
423
+ def get_score(
424
+ self,
425
+ ):
426
+ """member function for obtaining score of
427
+ x_t = alpha_t * x + sigma_t * eps"""
428
+ if self.model_type == ModelType.NOISE:
429
+ score_fn = lambda x, t, model, **kwargs: model(x, t, **kwargs) / -self.path_sampler.compute_sigma_t(path.expand_t_like_x(t, x))[0]
430
+ elif self.model_type == ModelType.SCORE:
431
+ score_fn = lambda x, t, model, **kwagrs: model(x, t, **kwagrs)
432
+ elif self.model_type == ModelType.VELOCITY:
433
+ score_fn = lambda x, t, model, **kwargs: self.path_sampler.get_score_from_velocity(model(x, t, **kwargs), x, t)
434
+ else:
435
+ raise NotImplementedError()
436
+
437
+ return score_fn
438
+
439
+
440
+ class Sampler:
441
+ """Sampler class for the transport model"""
442
+ def __init__(
443
+ self,
444
+ transport,
445
+ ):
446
+ """Constructor for a general sampler; supporting different sampling methods
447
+ Args:
448
+ - transport: an tranport object specify model prediction & interpolant type
449
+ """
450
+
451
+ self.transport = transport
452
+ self.drift = self.transport.get_drift()
453
+ self.score = self.transport.get_score()
454
+
455
+ def __get_sde_diffusion_and_drift(
456
+ self,
457
+ *,
458
+ diffusion_form="SBDM",
459
+ diffusion_norm=1.0,
460
+ ):
461
+
462
+ def sde_diffusion_fn(x, t):
463
+ diffusion = self.transport.path_sampler.compute_diffusion(x, t, form=diffusion_form, norm=diffusion_norm)
464
+ return diffusion
465
+
466
+ def sde_drift_fn(x, t, model, **kwargs):
467
+ drift_mean = self.drift(x, t, model, **kwargs) - sde_diffusion_fn(x, t) * self.score(x, t, model, **kwargs)
468
+ return drift_mean
469
+
470
+
471
+ return sde_drift_fn, sde_diffusion_fn
472
+
473
+ def __get_last_step(
474
+ self,
475
+ sde_drift,
476
+ *,
477
+ last_step,
478
+ last_step_size,
479
+ ):
480
+ """Get the last step function of the SDE solver"""
481
+
482
+ if last_step is None:
483
+ last_step_fn = \
484
+ lambda x, t, model, **model_kwargs: \
485
+ x
486
+ elif last_step == "Mean":
487
+ last_step_fn = \
488
+ lambda x, t, model, **model_kwargs: \
489
+ x - sde_drift(x, t, model, **model_kwargs) * last_step_size
490
+ elif last_step == "Tweedie":
491
+ alpha = self.transport.path_sampler.compute_alpha_t # simple aliasing; the original name was too long
492
+ sigma = self.transport.path_sampler.compute_sigma_t
493
+ last_step_fn = \
494
+ lambda x, t, model, **model_kwargs: \
495
+ x / alpha(t)[0][0] + (sigma(t)[0][0] ** 2) / alpha(t)[0][0] * self.score(x, t, model, **model_kwargs)
496
+ elif last_step == "Euler":
497
+ last_step_fn = \
498
+ lambda x, t, model, **model_kwargs: \
499
+ x - self.drift(x, t, model, **model_kwargs) * last_step_size
500
+ else:
501
+ raise NotImplementedError()
502
+
503
+ return last_step_fn
504
+
505
+ def sample_sde(
506
+ self,
507
+ *,
508
+ sampling_method="Euler",
509
+ diffusion_form="SBDM",
510
+ diffusion_norm=1.0,
511
+ last_step="Mean",
512
+ last_step_size=0.04,
513
+ num_steps=250,
514
+ ):
515
+ """returns a sampling function with given SDE settings
516
+ Args:
517
+ - sampling_method: type of sampler used in solving the SDE; default to be Euler-Maruyama
518
+ - diffusion_form: function form of diffusion coefficient; default to be matching SBDM
519
+ - diffusion_norm: function magnitude of diffusion coefficient; default to 1
520
+ - last_step: type of the last step; default to identity
521
+ - last_step_size: size of the last step; default to match the stride of 250 steps over [0,1]
522
+ - num_steps: total integration step of SDE
523
+ """
524
+
525
+ if last_step is None:
526
+ last_step_size = 0.0
527
+
528
+ sde_drift, sde_diffusion = self.__get_sde_diffusion_and_drift(
529
+ diffusion_form=diffusion_form,
530
+ diffusion_norm=diffusion_norm,
531
+ )
532
+
533
+ t0, t1 = self.transport.check_interval(
534
+ self.transport.train_eps,
535
+ self.transport.sample_eps,
536
+ diffusion_form=diffusion_form,
537
+ sde=True,
538
+ eval=True,
539
+ reverse=False,
540
+ last_step_size=last_step_size,
541
+ )
542
+
543
+ _sde = sde(
544
+ sde_drift,
545
+ sde_diffusion,
546
+ t0=t0,
547
+ t1=t1,
548
+ num_steps=num_steps,
549
+ sampler_type=sampling_method,
550
+ time_dist_shift=self.transport.time_dist_shift,
551
+ )
552
+
553
+ last_step_fn = self.__get_last_step(sde_drift, last_step=last_step, last_step_size=last_step_size)
554
+
555
+
556
+ def _sample(init, model, **model_kwargs):
557
+ xs = _sde.sample(init, model, **model_kwargs)
558
+ ts = th.ones(init.size(0), device=init.device) * (1 - t1)
559
+ x = last_step_fn(xs[-1], ts, model, **model_kwargs)
560
+ xs.append(x)
561
+
562
+ assert len(xs) == num_steps, "Samples does not match the number of steps"
563
+
564
+ return xs
565
+
566
+ return _sample
567
+
568
+ def sample_ode(
569
+ self,
570
+ *,
571
+ sampling_method="dopri5",
572
+ num_steps=50,
573
+ atol=1e-6,
574
+ rtol=1e-3,
575
+ reverse=False,
576
+ **kwargs
577
+ ):
578
+ """returns a sampling function with given ODE settings
579
+ Args:
580
+ - sampling_method: type of sampler used in solving the ODE; default to be Dopri5
581
+ - num_steps:
582
+ - fixed solver (Euler, Heun): the actual number of integration steps performed
583
+ - adaptive solver (Dopri5): the number of datapoints saved during integration; produced by interpolation
584
+ - atol: absolute error tolerance for the solver
585
+ - rtol: relative error tolerance for the solver
586
+ - reverse: whether solving the ODE in reverse (data to noise); default to False
587
+ """
588
+ if reverse:
589
+ drift = lambda x, t, model, **kwargs: self.drift(x, th.ones_like(t) * (1 - t), model, **kwargs)
590
+ else:
591
+ drift = self.drift
592
+
593
+ t0, t1 = self.transport.check_interval(
594
+ self.transport.train_eps,
595
+ self.transport.sample_eps,
596
+ sde=False,
597
+ eval=True,
598
+ reverse=reverse,
599
+ last_step_size=0.0,
600
+ )
601
+ # print(t0, t1)
602
+ _ode = ode(
603
+ drift=drift,
604
+ t0=t0,
605
+ t1=t1,
606
+ sampler_type=sampling_method,
607
+ num_steps=num_steps,
608
+ atol=atol,
609
+ rtol=rtol,
610
+ time_dist_shift=self.transport.time_dist_shift,
611
+ use_time_shift=kwargs.get("use_time_shift", True),
612
+ )
613
+
614
+ return _ode.sample
patch-reparameterization/src/stage2/transport/utils.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch as th
2
+
3
+ class EasyDict:
4
+
5
+ def __init__(self, sub_dict):
6
+ for k, v in sub_dict.items():
7
+ setattr(self, k, v)
8
+
9
+ def __getitem__(self, key):
10
+ return getattr(self, key)
11
+
12
+ def mean_flat(x):
13
+ """
14
+ Take the mean over all non-batch dimensions.
15
+ """
16
+ return th.mean(x, dim=list(range(1, len(x.size()))))
17
+
18
+ def log_state(state):
19
+ result = []
20
+
21
+ sorted_state = dict(sorted(state.items()))
22
+ for key, value in sorted_state.items():
23
+ # Check if the value is an instance of a class
24
+ if "<object" in str(value) or "object at" in str(value):
25
+ result.append(f"{key}: [{value.__class__.__name__}]")
26
+ else:
27
+ result.append(f"{key}: {value}")
28
+
29
+ return '\n'.join(result)
patch-reparameterization/src/stage2/transport/xpred.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+
4
+ import logging
5
+
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def make_xpred_sampler(num_steps: int, sampling_method: str = "euler", t_eps: float = 1e-3):
11
+ """
12
+ Create a JiT-style x-pred sampler operating in latent space.
13
+
14
+ The underlying model is assumed to output x_pred given (z_t, t, **kwargs),
15
+ and we convert it to a velocity field via:
16
+
17
+ v_pred = (x_pred - z_t) / (1 - t)
18
+
19
+ The returned object has the same callable interface as the existing
20
+ Transport Sampler ODE/SDE samplers:
21
+
22
+ xs = sampler(z_init, model_fn, **model_kwargs)
23
+ final_z = xs[-1]
24
+ """
25
+ method = sampling_method.lower()
26
+ if method not in ("euler", "heun"):
27
+ raise NotImplementedError(f"x-pred sampler only supports euler/heun, got {sampling_method}")
28
+
29
+ def _forward_velocity(z: torch.Tensor, t: torch.Tensor, model_fn, model_kwargs):
30
+ """
31
+ Map x-pred model output to velocity:
32
+
33
+ v_pred = (x_pred - z) / (1 - t)
34
+ """
35
+ bsz = z.size(0)
36
+ if t.ndim == 0:
37
+ t = t.expand(bsz)
38
+ t = t.to(z.device)
39
+ t_view = t.view(bsz, *([1] * (z.ndim - 1)))
40
+ x_pred = model_fn(z, t, **model_kwargs)
41
+ v_pred = (x_pred - z) / (1.0 - t_view).clamp_min(t_eps)
42
+ return v_pred
43
+
44
+ def _sample(init: torch.Tensor, model_fn, **model_kwargs):
45
+ """
46
+ Fixed-step ODE sampler (Euler / Heun) for JiT-style x-pred velocity.
47
+
48
+ Args:
49
+ init: initial latent z_0
50
+ model_fn: callable taking (z_t, t, **model_kwargs) and returning x_pred
51
+ model_kwargs: passed through to model_fn
52
+ """
53
+ z = init
54
+ xs = []
55
+ bsz = z.size(0)
56
+ device = z.device
57
+ ts = torch.linspace(0.0, 1.0, num_steps + 1, device=device)
58
+
59
+ for i in range(num_steps):
60
+ t = ts[i].expand(bsz)
61
+ dt = ts[i + 1] - ts[i]
62
+
63
+ if method == "euler":
64
+ v_pred = _forward_velocity(z, t, model_fn, model_kwargs)
65
+ z = z + dt * v_pred
66
+ else: # heun
67
+ v_t = _forward_velocity(z, t, model_fn, model_kwargs)
68
+ z_euler = z + dt * v_t
69
+ t_next = ts[i + 1].expand(bsz)
70
+ v_next = _forward_velocity(z_euler, t_next, model_fn, model_kwargs)
71
+ v_pred = 0.5 * (v_t + v_next)
72
+ z = z + dt * v_pred
73
+
74
+ xs.append(z)
75
+
76
+ return xs
77
+
78
+ return _sample
79
+
80
+
81
+ def compute_xpred_loss(
82
+ model: nn.Module,
83
+ x: torch.Tensor,
84
+ labels: torch.Tensor,
85
+ *,
86
+ transport=None,
87
+ t_eps: float = 1e-3,
88
+ sample_t_method: str = "transport",
89
+ ) -> torch.Tensor:
90
+ """
91
+ JiT-style x-pred loss in latent space.
92
+
93
+ Given latent "clean" target x, we construct:
94
+
95
+ z_t = t * x + (1 - t) * e
96
+ v = (x - z_t) / (1 - t)
97
+ x̂ = model(z_t, t, y=labels)
98
+ v̂ = (x_hat - z_t) / (1 - t)
99
+ L = E[ ||v - v̂||^2 ]
100
+ """
101
+ device = x.device
102
+ bsz = x.size(0)
103
+
104
+ # If a Transport object is provided, reuse its time sampler so that
105
+ # x-pred shares the same time distribution (uniform/logit-normal,
106
+ # eps truncation, time_dist_shift, etc.) as the underlying FM setup.
107
+ # Linear plan is parameterized data->noise; JiT is noise->data,
108
+ # so we flip t -> 1 - t to keep the *geometric* end with higher
109
+ # sampling density consistent.
110
+ if sample_t_method == "transport" and transport is not None:
111
+ # transport.sample(x1) -> (t, x0, x1)
112
+ t, _, _ = transport.sample(x)
113
+ t = 1.0 - t
114
+ elif sample_t_method == "normal_sigmoid":
115
+ t = torch.randn(bsz, device=device)*0.8 - 0.8
116
+ t = torch.sigmoid(t)
117
+ else:
118
+ raise NotImplementedError(f"Invalid sample_t_method: {sample_t_method}")
119
+ logger.debug(f"t: {t}")
120
+ t_view = t.view(bsz, *([1] * (x.ndim - 1)))
121
+ e = torch.randn_like(x)
122
+ z_t = t_view * x + (1.0 - t_view) * e
123
+
124
+ v = (x - z_t) / (1.0 - t_view).clamp_min(t_eps)
125
+ x_pred = model(z_t, t, y=labels)
126
+ v_pred = (x_pred - z_t) / (1.0 - t_view).clamp_min(t_eps)
127
+
128
+ loss = (v - v_pred) ** 2
129
+ logger.debug(f"xpred loss: {loss}")
130
+ return loss.mean()
131
+
requirements.txt CHANGED
@@ -1,18 +1,15 @@
1
- # UniSpace inference dependencies
2
- # Do NOT list gradio, spaces, huggingface_hub — preinstalled and platform-managed
3
- torch
4
- torchvision
5
- transformers
6
- accelerate>=1.12
7
- safetensors>=0.7
8
- omegaconf
9
- timm
10
- einops
11
- numpy>=1.26,<3
12
- scipy
13
- scikit-image
14
- pillow
15
  sentencepiece
16
- PyYAML>=6.0
17
  tqdm
18
- opencv-python
 
 
1
+ # Matches the verified UniSpace environment (unispace/requirements.txt):
2
+ # the repo vendors custom Qwen3 / Qwen3-VL modeling code that tracks
3
+ # transformers 4.57.x internals, so both torch and transformers are pinned.
4
+ torch==2.8.0
5
+ torchvision==0.23.0
6
+ transformers==4.57.3
7
+ accelerate==1.12.0
8
+ omegaconf==2.3.0
9
+ safetensors>=0.4.3
10
+ einops>=0.8.1
11
+ timm>=1.0.13
 
 
 
12
  sentencepiece
 
13
  tqdm
14
+ numpy>=1.26,<3
15
+ pillow
unispace/LICENSE-APACHE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2025 Bytedance Ltd. and/or its affiliates
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
200
+ implied. See the License for the specific language governing
201
+ permissions and limitations under the License.
unispace/NOTICE ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ unispace (unified multimodal model)
2
+ ====================================
3
+
4
+ This subdirectory contains code originally extracted and refactored from
5
+ the ByteDance Bagel project (https://github.com/ByteDance-Seed/Bagel).
6
+
7
+ Copyright 2025 Bytedance Ltd. and/or its affiliates.
8
+
9
+ Licensed under the Apache License, Version 2.0 (the "License");
10
+ you may not use this file except in compliance with the License.
11
+ You may obtain a copy of the License at
12
+
13
+ http://www.apache.org/licenses/LICENSE-2.0
14
+
15
+ Unless required by applicable law or agreed to in writing, software
16
+ distributed under the License is distributed on an "AS IS" BASIS,
17
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ See the License for the specific language governing permissions and
19
+ limitations under the License.
20
+
21
+ The original Apache-2.0 license and copyright notices in the source
22
+ files (header lines `Copyright 2025 Bytedance Ltd. and/or its affiliates`
23
+ / `SPDX-License-Identifier: Apache-2.0`) are retained as required by the
24
+ license.
25
+
26
+ Modifications and additions made in this subdirectory, as well as all
27
+ other components of this repository (including the `patch-reparameterization/`
28
+ subdirectory), are licensed under the MIT License — see the repository
29
+ root LICENSE file.
unispace/modeling/__init__.py CHANGED
@@ -1 +1,4 @@
1
- # UniSpace modeling package — no auto-imports to avoid circular dependencies
 
 
 
 
1
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from . import unimm, siglip, autoencoder
unispace/modeling/autoencoder.py ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2024 Black Forest Labs.
2
+ # Copyright (c) 2025 Bytedance Ltd. and/or its affiliates.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+ # This file has been modified by ByteDance Ltd. and/or its affiliates. on 2025-05-20.
6
+ #
7
+ # Original file was released under Apache-2.0, with the full license text
8
+ # available at https://github.com/black-forest-labs/flux/blob/main/LICENSE.
9
+ #
10
+ # This modified file is released under the same license.
11
+
12
+ from dataclasses import dataclass
13
+
14
+ import torch
15
+ from einops import rearrange
16
+ from torch import Tensor, nn
17
+ from safetensors.torch import load_file as load_sft
18
+
19
+
20
+ @dataclass
21
+ class AutoEncoderParams:
22
+ resolution: int
23
+ in_channels: int
24
+ downsample: int
25
+ ch: int
26
+ out_ch: int
27
+ ch_mult: list[int]
28
+ num_res_blocks: int
29
+ z_channels: int
30
+ scale_factor: float
31
+ shift_factor: float
32
+
33
+
34
+ def swish(x: Tensor) -> Tensor:
35
+ return x * torch.sigmoid(x)
36
+
37
+
38
+ class AttnBlock(nn.Module):
39
+ def __init__(self, in_channels: int):
40
+ super().__init__()
41
+ self.in_channels = in_channels
42
+
43
+ self.norm = nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True)
44
+
45
+ self.q = nn.Conv2d(in_channels, in_channels, kernel_size=1)
46
+ self.k = nn.Conv2d(in_channels, in_channels, kernel_size=1)
47
+ self.v = nn.Conv2d(in_channels, in_channels, kernel_size=1)
48
+ self.proj_out = nn.Conv2d(in_channels, in_channels, kernel_size=1)
49
+
50
+ def attention(self, h_: Tensor) -> Tensor:
51
+ h_ = self.norm(h_)
52
+ q = self.q(h_)
53
+ k = self.k(h_)
54
+ v = self.v(h_)
55
+
56
+ b, c, h, w = q.shape
57
+ q = rearrange(q, "b c h w -> b 1 (h w) c").contiguous()
58
+ k = rearrange(k, "b c h w -> b 1 (h w) c").contiguous()
59
+ v = rearrange(v, "b c h w -> b 1 (h w) c").contiguous()
60
+ h_ = nn.functional.scaled_dot_product_attention(q, k, v)
61
+
62
+ return rearrange(h_, "b 1 (h w) c -> b c h w", h=h, w=w, c=c, b=b)
63
+
64
+ def forward(self, x: Tensor) -> Tensor:
65
+ return x + self.proj_out(self.attention(x))
66
+
67
+
68
+ class ResnetBlock(nn.Module):
69
+ def __init__(self, in_channels: int, out_channels: int):
70
+ super().__init__()
71
+ self.in_channels = in_channels
72
+ out_channels = in_channels if out_channels is None else out_channels
73
+ self.out_channels = out_channels
74
+
75
+ self.norm1 = nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True)
76
+ self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1)
77
+ self.norm2 = nn.GroupNorm(num_groups=32, num_channels=out_channels, eps=1e-6, affine=True)
78
+ self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
79
+ if self.in_channels != self.out_channels:
80
+ self.nin_shortcut = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0)
81
+
82
+ def forward(self, x):
83
+ h = x
84
+ h = self.norm1(h)
85
+ h = swish(h)
86
+ h = self.conv1(h)
87
+
88
+ h = self.norm2(h)
89
+ h = swish(h)
90
+ h = self.conv2(h)
91
+
92
+ if self.in_channels != self.out_channels:
93
+ x = self.nin_shortcut(x)
94
+
95
+ return x + h
96
+
97
+
98
+ class Downsample(nn.Module):
99
+ def __init__(self, in_channels: int):
100
+ super().__init__()
101
+ # no asymmetric padding in torch conv, must do it ourselves
102
+ self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=2, padding=0)
103
+
104
+ def forward(self, x: Tensor):
105
+ pad = (0, 1, 0, 1)
106
+ x = nn.functional.pad(x, pad, mode="constant", value=0)
107
+ x = self.conv(x)
108
+ return x
109
+
110
+
111
+ class Upsample(nn.Module):
112
+ def __init__(self, in_channels: int):
113
+ super().__init__()
114
+ self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=1, padding=1)
115
+
116
+ def forward(self, x: Tensor):
117
+ x = nn.functional.interpolate(x, scale_factor=2.0, mode="nearest")
118
+ x = self.conv(x)
119
+ return x
120
+
121
+
122
+ class Encoder(nn.Module):
123
+ def __init__(
124
+ self,
125
+ resolution: int,
126
+ in_channels: int,
127
+ ch: int,
128
+ ch_mult: list[int],
129
+ num_res_blocks: int,
130
+ z_channels: int,
131
+ ):
132
+ super().__init__()
133
+ self.ch = ch
134
+ self.num_resolutions = len(ch_mult)
135
+ self.num_res_blocks = num_res_blocks
136
+ self.resolution = resolution
137
+ self.in_channels = in_channels
138
+ # downsampling
139
+ self.conv_in = nn.Conv2d(in_channels, self.ch, kernel_size=3, stride=1, padding=1)
140
+
141
+ curr_res = resolution
142
+ in_ch_mult = (1,) + tuple(ch_mult)
143
+ self.in_ch_mult = in_ch_mult
144
+ self.down = nn.ModuleList()
145
+ block_in = self.ch
146
+ for i_level in range(self.num_resolutions):
147
+ block = nn.ModuleList()
148
+ attn = nn.ModuleList()
149
+ block_in = ch * in_ch_mult[i_level]
150
+ block_out = ch * ch_mult[i_level]
151
+ for _ in range(self.num_res_blocks):
152
+ block.append(ResnetBlock(in_channels=block_in, out_channels=block_out))
153
+ block_in = block_out
154
+ down = nn.Module()
155
+ down.block = block
156
+ down.attn = attn
157
+ if i_level != self.num_resolutions - 1:
158
+ down.downsample = Downsample(block_in)
159
+ curr_res = curr_res // 2
160
+ self.down.append(down)
161
+
162
+ # middle
163
+ self.mid = nn.Module()
164
+ self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in)
165
+ self.mid.attn_1 = AttnBlock(block_in)
166
+ self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in)
167
+
168
+ # end
169
+ self.norm_out = nn.GroupNorm(num_groups=32, num_channels=block_in, eps=1e-6, affine=True)
170
+ self.conv_out = nn.Conv2d(block_in, 2 * z_channels, kernel_size=3, stride=1, padding=1)
171
+
172
+ def forward(self, x: Tensor) -> Tensor:
173
+ # downsampling
174
+ hs = [self.conv_in(x)]
175
+ for i_level in range(self.num_resolutions):
176
+ for i_block in range(self.num_res_blocks):
177
+ h = self.down[i_level].block[i_block](hs[-1])
178
+ if len(self.down[i_level].attn) > 0:
179
+ h = self.down[i_level].attn[i_block](h)
180
+ hs.append(h)
181
+ if i_level != self.num_resolutions - 1:
182
+ hs.append(self.down[i_level].downsample(hs[-1]))
183
+
184
+ # middle
185
+ h = hs[-1]
186
+ h = self.mid.block_1(h)
187
+ h = self.mid.attn_1(h)
188
+ h = self.mid.block_2(h)
189
+ # end
190
+ h = self.norm_out(h)
191
+ h = swish(h)
192
+ h = self.conv_out(h)
193
+ return h
194
+
195
+
196
+ class Decoder(nn.Module):
197
+ def __init__(
198
+ self,
199
+ ch: int,
200
+ out_ch: int,
201
+ ch_mult: list[int],
202
+ num_res_blocks: int,
203
+ in_channels: int,
204
+ resolution: int,
205
+ z_channels: int,
206
+ ):
207
+ super().__init__()
208
+ self.ch = ch
209
+ self.num_resolutions = len(ch_mult)
210
+ self.num_res_blocks = num_res_blocks
211
+ self.resolution = resolution
212
+ self.in_channels = in_channels
213
+ self.ffactor = 2 ** (self.num_resolutions - 1)
214
+
215
+ # compute in_ch_mult, block_in and curr_res at lowest res
216
+ block_in = ch * ch_mult[self.num_resolutions - 1]
217
+ curr_res = resolution // 2 ** (self.num_resolutions - 1)
218
+ self.z_shape = (1, z_channels, curr_res, curr_res)
219
+
220
+ # z to block_in
221
+ self.conv_in = nn.Conv2d(z_channels, block_in, kernel_size=3, stride=1, padding=1)
222
+
223
+ # middle
224
+ self.mid = nn.Module()
225
+ self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in)
226
+ self.mid.attn_1 = AttnBlock(block_in)
227
+ self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in)
228
+
229
+ # upsampling
230
+ self.up = nn.ModuleList()
231
+ for i_level in reversed(range(self.num_resolutions)):
232
+ block = nn.ModuleList()
233
+ attn = nn.ModuleList()
234
+ block_out = ch * ch_mult[i_level]
235
+ for _ in range(self.num_res_blocks + 1):
236
+ block.append(ResnetBlock(in_channels=block_in, out_channels=block_out))
237
+ block_in = block_out
238
+ up = nn.Module()
239
+ up.block = block
240
+ up.attn = attn
241
+ if i_level != 0:
242
+ up.upsample = Upsample(block_in)
243
+ curr_res = curr_res * 2
244
+ self.up.insert(0, up) # prepend to get consistent order
245
+
246
+ # end
247
+ self.norm_out = nn.GroupNorm(num_groups=32, num_channels=block_in, eps=1e-6, affine=True)
248
+ self.conv_out = nn.Conv2d(block_in, out_ch, kernel_size=3, stride=1, padding=1)
249
+
250
+ def forward(self, z: Tensor) -> Tensor:
251
+ # z to block_in
252
+ h = self.conv_in(z)
253
+
254
+ # middle
255
+ h = self.mid.block_1(h)
256
+ h = self.mid.attn_1(h)
257
+ h = self.mid.block_2(h)
258
+
259
+ # upsampling
260
+ for i_level in reversed(range(self.num_resolutions)):
261
+ for i_block in range(self.num_res_blocks + 1):
262
+ h = self.up[i_level].block[i_block](h)
263
+ if len(self.up[i_level].attn) > 0:
264
+ h = self.up[i_level].attn[i_block](h)
265
+ if i_level != 0:
266
+ h = self.up[i_level].upsample(h)
267
+
268
+ # end
269
+ h = self.norm_out(h)
270
+ h = swish(h)
271
+ h = self.conv_out(h)
272
+ return h
273
+
274
+
275
+ class DiagonalGaussian(nn.Module):
276
+ def __init__(self, sample: bool = True, chunk_dim: int = 1):
277
+ super().__init__()
278
+ self.sample = sample
279
+ self.chunk_dim = chunk_dim
280
+
281
+ def forward(self, z: Tensor) -> Tensor:
282
+ mean, logvar = torch.chunk(z, 2, dim=self.chunk_dim)
283
+ if self.sample:
284
+ std = torch.exp(0.5 * logvar)
285
+ return mean + std * torch.randn_like(mean)
286
+ else:
287
+ return mean
288
+
289
+
290
+ class AutoEncoder(nn.Module):
291
+ def __init__(self, params: AutoEncoderParams):
292
+ super().__init__()
293
+ self.encoder = Encoder(
294
+ resolution=params.resolution,
295
+ in_channels=params.in_channels,
296
+ ch=params.ch,
297
+ ch_mult=params.ch_mult,
298
+ num_res_blocks=params.num_res_blocks,
299
+ z_channels=params.z_channels,
300
+ )
301
+ self.decoder = Decoder(
302
+ resolution=params.resolution,
303
+ in_channels=params.in_channels,
304
+ ch=params.ch,
305
+ out_ch=params.out_ch,
306
+ ch_mult=params.ch_mult,
307
+ num_res_blocks=params.num_res_blocks,
308
+ z_channels=params.z_channels,
309
+ )
310
+ self.reg = DiagonalGaussian()
311
+
312
+ self.scale_factor = params.scale_factor
313
+ self.shift_factor = params.shift_factor
314
+
315
+ def encode(self, x: Tensor) -> Tensor:
316
+ z = self.reg(self.encoder(x))
317
+ z = self.scale_factor * (z - self.shift_factor)
318
+ return z
319
+
320
+ def decode(self, z: Tensor) -> Tensor:
321
+ z = z / self.scale_factor + self.shift_factor
322
+ return self.decoder(z)
323
+
324
+ def forward(self, x: Tensor) -> Tensor:
325
+ return self.decode(self.encode(x))
326
+
327
+
328
+ def print_load_warning(missing: list[str], unexpected: list[str]) -> None:
329
+ if len(missing) > 0 and len(unexpected) > 0:
330
+ print(f"Got {len(missing)} missing keys:\n\t" + "\n\t".join(missing))
331
+ print("\n" + "-" * 79 + "\n")
332
+ print(f"Got {len(unexpected)} unexpected keys:\n\t" + "\n\t".join(unexpected))
333
+ elif len(missing) > 0:
334
+ print(f"Got {len(missing)} missing keys:\n\t" + "\n\t".join(missing))
335
+ elif len(unexpected) > 0:
336
+ print(f"Got {len(unexpected)} unexpected keys:\n\t" + "\n\t".join(unexpected))
337
+
338
+
339
+ def load_ae(local_path: str, vae_type: str = "flux1-vae", rae_config_path: str = None, **kwargs):
340
+ """
341
+ Load VAE model.
342
+
343
+ Args:
344
+ local_path: Path to the VAE checkpoint file or directory.
345
+ vae_type: 'flux1-vae' (z=16), 'flux2-vae' (z=32), 'rae' (z=768),
346
+ or 'qwen3_unified' (Qwen3Unified encoder, z=1280).
347
+ rae_config_path: Path to RAE config yaml (used when vae_type='rae' or 'qwen3_unified').
348
+
349
+ Returns:
350
+ (model, config) tuple. Config has .z_channels and .downsample attributes.
351
+ """
352
+ if vae_type == "flux2-vae":
353
+ from modeling.autoencoder_flux2 import load_flux2_ae
354
+ return load_flux2_ae(local_path)
355
+
356
+ if vae_type in ("rae", "qwen3_unified"):
357
+ from modeling.autoencoder_rae import load_rae
358
+ return load_rae(local_path, rae_config_path=rae_config_path,
359
+ rae_accelerate_root=kwargs.get('rae_accelerate_root', None),
360
+ encoder_only=kwargs.get('encoder_only', False))
361
+
362
+ ae_params = AutoEncoderParams(
363
+ resolution=256,
364
+ in_channels=3,
365
+ downsample=8,
366
+ ch=128,
367
+ out_ch=3,
368
+ ch_mult=[1, 2, 4, 4],
369
+ num_res_blocks=2,
370
+ z_channels=16,
371
+ scale_factor=0.3611,
372
+ shift_factor=0.1159,
373
+ )
374
+
375
+ # Loading the autoencoder
376
+ ae = AutoEncoder(ae_params)
377
+
378
+ if local_path is not None:
379
+ sd = load_sft(local_path)
380
+ missing, unexpected = ae.load_state_dict(sd, strict=False, assign=True)
381
+ print_load_warning(missing, unexpected)
382
+ return ae, ae_params
unispace/modeling/autoencoder_flux2.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 The HuggingFace Team. All rights reserved.
2
+ # Adapted from diffusers 0.36 AutoencoderKLFlux2, standalone implementation.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+ # Self-contained Flux2 VAE implementation that loads official BFL weights
6
+ # (diffusion_pytorch_model.safetensors) without requiring diffusers >= 0.36.
7
+ # Key names match diffusers convention exactly.
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+ from dataclasses import dataclass
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ import torch.nn.functional as F
17
+ from safetensors.torch import load_file as load_sft
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Config dataclass (mirrors AutoEncoderParams from autoencoder.py for Flux1)
22
+ # ---------------------------------------------------------------------------
23
+ @dataclass
24
+ class Flux2VAEConfig:
25
+ in_channels: int = 3
26
+ out_channels: int = 3
27
+ block_out_channels: tuple = (128, 256, 512, 512)
28
+ layers_per_block: int = 2
29
+ latent_channels: int = 32
30
+ norm_num_groups: int = 32
31
+ downsample: int = 8 # 2^(num_down_blocks - 1) = 2^3 = 8
32
+ z_channels: int = 32 # alias for latent_channels, compat with Flux1 config
33
+ batch_norm_eps: float = 1e-4
34
+ batch_norm_momentum: float = 0.1
35
+ patch_size: tuple = (2, 2)
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Building blocks
40
+ # ---------------------------------------------------------------------------
41
+ class ResnetBlock2D(nn.Module):
42
+ """Standard ResNet block: GroupNorm -> SiLU -> Conv -> GroupNorm -> SiLU -> Conv + shortcut."""
43
+
44
+ def __init__(self, in_channels: int, out_channels: int, groups: int = 32):
45
+ super().__init__()
46
+ self.norm1 = nn.GroupNorm(groups, in_channels, eps=1e-6)
47
+ self.conv1 = nn.Conv2d(in_channels, out_channels, 3, padding=1)
48
+ self.norm2 = nn.GroupNorm(groups, out_channels, eps=1e-6)
49
+ self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1)
50
+ self.nonlinearity = nn.SiLU()
51
+ if in_channels != out_channels:
52
+ self.conv_shortcut = nn.Conv2d(in_channels, out_channels, 1)
53
+ else:
54
+ self.conv_shortcut = None
55
+
56
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
57
+ h = self.norm1(x)
58
+ h = self.nonlinearity(h)
59
+ h = self.conv1(h)
60
+ h = self.norm2(h)
61
+ h = self.nonlinearity(h)
62
+ h = self.conv2(h)
63
+ if self.conv_shortcut is not None:
64
+ x = self.conv_shortcut(x)
65
+ return x + h
66
+
67
+
68
+ class AttnBlock(nn.Module):
69
+ """Self-attention block matching diffusers Attention with _from_deprecated_attn_block=True."""
70
+
71
+ def __init__(self, channels: int, num_groups: int = 32):
72
+ super().__init__()
73
+ self.group_norm = nn.GroupNorm(num_groups, channels, eps=1e-6)
74
+ self.to_q = nn.Linear(channels, channels)
75
+ self.to_k = nn.Linear(channels, channels)
76
+ self.to_v = nn.Linear(channels, channels)
77
+ self.to_out = nn.ModuleList([nn.Linear(channels, channels)])
78
+
79
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
80
+ residual = x
81
+ b, c, h, w = x.shape
82
+ x = self.group_norm(x)
83
+ x = x.reshape(b, c, h * w).permute(0, 2, 1) # (B, HW, C)
84
+
85
+ q = self.to_q(x)
86
+ k = self.to_k(x)
87
+ v = self.to_v(x)
88
+
89
+ # scaled_dot_product_attention
90
+ x = F.scaled_dot_product_attention(q, k, v)
91
+ x = self.to_out[0](x)
92
+
93
+ x = x.permute(0, 2, 1).reshape(b, c, h, w)
94
+ return x + residual
95
+
96
+
97
+ class Downsample2D(nn.Module):
98
+ """Spatial downsample with padding + stride-2 conv."""
99
+
100
+ def __init__(self, channels: int):
101
+ super().__init__()
102
+ self.conv = nn.Conv2d(channels, channels, 3, stride=2, padding=0)
103
+
104
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
105
+ x = F.pad(x, (0, 1, 0, 1), mode="constant", value=0)
106
+ return self.conv(x)
107
+
108
+
109
+ class Upsample2D(nn.Module):
110
+ """Spatial upsample with nearest interpolation + conv."""
111
+
112
+ def __init__(self, channels: int):
113
+ super().__init__()
114
+ self.conv = nn.Conv2d(channels, channels, 3, padding=1)
115
+
116
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
117
+ x = F.interpolate(x, scale_factor=2.0, mode="nearest")
118
+ return self.conv(x)
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Encoder / Decoder blocks (key names: down_blocks.{i}.resnets.{j} / downsamplers.0)
123
+ # ---------------------------------------------------------------------------
124
+ class DownEncoderBlock2D(nn.Module):
125
+ def __init__(self, in_ch: int, out_ch: int, num_layers: int = 2,
126
+ add_downsample: bool = True, groups: int = 32):
127
+ super().__init__()
128
+ self.resnets = nn.ModuleList()
129
+ for i in range(num_layers):
130
+ self.resnets.append(ResnetBlock2D(in_ch if i == 0 else out_ch, out_ch, groups))
131
+ self.downsamplers = nn.ModuleList()
132
+ if add_downsample:
133
+ self.downsamplers.append(Downsample2D(out_ch))
134
+
135
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
136
+ for resnet in self.resnets:
137
+ x = resnet(x)
138
+ for ds in self.downsamplers:
139
+ x = ds(x)
140
+ return x
141
+
142
+
143
+ class UpDecoderBlock2D(nn.Module):
144
+ def __init__(self, in_ch: int, out_ch: int, num_layers: int = 3,
145
+ add_upsample: bool = True, groups: int = 32):
146
+ super().__init__()
147
+ self.resnets = nn.ModuleList()
148
+ for i in range(num_layers):
149
+ self.resnets.append(ResnetBlock2D(in_ch if i == 0 else out_ch, out_ch, groups))
150
+ self.upsamplers = nn.ModuleList()
151
+ if add_upsample:
152
+ self.upsamplers.append(Upsample2D(out_ch))
153
+
154
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
155
+ for resnet in self.resnets:
156
+ x = resnet(x)
157
+ for us in self.upsamplers:
158
+ x = us(x)
159
+ return x
160
+
161
+
162
+ class MidBlock(nn.Module):
163
+ """Mid block: resnet -> attention -> resnet."""
164
+
165
+ def __init__(self, channels: int, groups: int = 32):
166
+ super().__init__()
167
+ self.resnets = nn.ModuleList([
168
+ ResnetBlock2D(channels, channels, groups),
169
+ ResnetBlock2D(channels, channels, groups),
170
+ ])
171
+ self.attentions = nn.ModuleList([AttnBlock(channels, groups)])
172
+
173
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
174
+ x = self.resnets[0](x)
175
+ x = self.attentions[0](x)
176
+ x = self.resnets[1](x)
177
+ return x
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # Encoder & Decoder
182
+ # ---------------------------------------------------------------------------
183
+ class Flux2Encoder(nn.Module):
184
+ def __init__(self, cfg: Flux2VAEConfig):
185
+ super().__init__()
186
+ ch = cfg.block_out_channels
187
+ self.conv_in = nn.Conv2d(cfg.in_channels, ch[0], 3, padding=1)
188
+
189
+ self.down_blocks = nn.ModuleList()
190
+ in_c = ch[0]
191
+ for i, out_c in enumerate(ch):
192
+ is_last = (i == len(ch) - 1)
193
+ self.down_blocks.append(
194
+ DownEncoderBlock2D(in_c, out_c, cfg.layers_per_block,
195
+ add_downsample=not is_last, groups=cfg.norm_num_groups)
196
+ )
197
+ in_c = out_c
198
+
199
+ self.mid_block = MidBlock(ch[-1], cfg.norm_num_groups)
200
+
201
+ self.conv_norm_out = nn.GroupNorm(cfg.norm_num_groups, ch[-1], eps=1e-6)
202
+ self.conv_act = nn.SiLU()
203
+ self.conv_out = nn.Conv2d(ch[-1], 2 * cfg.latent_channels, 3, padding=1)
204
+
205
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
206
+ x = self.conv_in(x)
207
+ for block in self.down_blocks:
208
+ x = block(x)
209
+ x = self.mid_block(x)
210
+ x = self.conv_norm_out(x)
211
+ x = self.conv_act(x)
212
+ x = self.conv_out(x)
213
+ return x
214
+
215
+
216
+ class Flux2Decoder(nn.Module):
217
+ def __init__(self, cfg: Flux2VAEConfig):
218
+ super().__init__()
219
+ ch = cfg.block_out_channels
220
+ rev_ch = list(reversed(ch))
221
+
222
+ self.conv_in = nn.Conv2d(cfg.latent_channels, ch[-1], 3, padding=1)
223
+ self.mid_block = MidBlock(ch[-1], cfg.norm_num_groups)
224
+
225
+ self.up_blocks = nn.ModuleList()
226
+ in_c = rev_ch[0]
227
+ for i, out_c in enumerate(rev_ch):
228
+ is_last = (i == len(rev_ch) - 1)
229
+ # Decoder has layers_per_block + 1 resnets per block
230
+ self.up_blocks.append(
231
+ UpDecoderBlock2D(in_c, out_c, cfg.layers_per_block + 1,
232
+ add_upsample=not is_last, groups=cfg.norm_num_groups)
233
+ )
234
+ in_c = out_c
235
+
236
+ self.conv_norm_out = nn.GroupNorm(cfg.norm_num_groups, ch[0], eps=1e-6)
237
+ self.conv_act = nn.SiLU()
238
+ self.conv_out = nn.Conv2d(ch[0], cfg.out_channels, 3, padding=1)
239
+
240
+ def forward(self, z: torch.Tensor) -> torch.Tensor:
241
+ z = self.conv_in(z)
242
+ z = self.mid_block(z)
243
+ for block in self.up_blocks:
244
+ z = block(z)
245
+ z = self.conv_norm_out(z)
246
+ z = self.conv_act(z)
247
+ z = self.conv_out(z)
248
+ return z
249
+
250
+
251
+ # ---------------------------------------------------------------------------
252
+ # DiagonalGaussian (sample or mode)
253
+ # ---------------------------------------------------------------------------
254
+ class DiagonalGaussian(nn.Module):
255
+ def __init__(self, sample: bool = True):
256
+ super().__init__()
257
+ self.sample = sample
258
+
259
+ def forward(self, z: torch.Tensor) -> torch.Tensor:
260
+ mean, logvar = torch.chunk(z, 2, dim=1)
261
+ logvar = torch.clamp(logvar, -30.0, 20.0)
262
+ if self.sample:
263
+ std = torch.exp(0.5 * logvar)
264
+ return mean + std * torch.randn_like(mean)
265
+ return mean
266
+
267
+
268
+ # ---------------------------------------------------------------------------
269
+ # Main model: AutoEncoderFlux2
270
+ # ---------------------------------------------------------------------------
271
+ class AutoEncoderFlux2(nn.Module):
272
+ """
273
+ Standalone Flux2 VAE (AutoencoderKLFlux2) implementation.
274
+
275
+ Key differences from Flux1 VAE (autoencoder.py):
276
+ - latent_channels = 32 (vs 16)
277
+ - Has quant_conv / post_quant_conv layers
278
+ - Uses BatchNorm-based normalization instead of scale_factor/shift_factor
279
+ - Architecture follows diffusers standard (DownEncoderBlock2D / UpDecoderBlock2D)
280
+ """
281
+
282
+ def __init__(self, config: Flux2VAEConfig | None = None):
283
+ super().__init__()
284
+ if config is None:
285
+ config = Flux2VAEConfig()
286
+ self.config = config
287
+
288
+ self.encoder = Flux2Encoder(config)
289
+ self.decoder = Flux2Decoder(config)
290
+ self.quant_conv = nn.Conv2d(2 * config.latent_channels, 2 * config.latent_channels, 1)
291
+ self.post_quant_conv = nn.Conv2d(config.latent_channels, config.latent_channels, 1)
292
+ self.reg = DiagonalGaussian(sample=False) # default: mode (deterministic)
293
+
294
+ # BatchNorm for latent normalization (patchified space)
295
+ bn_channels = math.prod(config.patch_size) * config.latent_channels # 2*2*32=128
296
+ self.bn = nn.BatchNorm2d(
297
+ bn_channels,
298
+ eps=config.batch_norm_eps,
299
+ momentum=config.batch_norm_momentum,
300
+ affine=False,
301
+ track_running_stats=True,
302
+ )
303
+
304
+ def encode(self, x: torch.Tensor) -> torch.Tensor:
305
+ """Encode image to normalized latent (BN in patchified space, then un-patchify)."""
306
+ h = self.encoder(x)
307
+ h = self.quant_conv(h)
308
+ z = self.reg(h) # (B, C=32, H, W)
309
+
310
+ # Patchify → BN normalize → un-patchify to keep API compatible with Flux1
311
+ B, C, H, W = z.shape
312
+ pH, pW = self.config.patch_size # (2, 2)
313
+ # (B, C, H, W) → (B, C*pH*pW, H//pH, W//pW)
314
+ z_p = z.reshape(B, C, H // pH, pH, W // pW, pW)
315
+ z_p = z_p.permute(0, 1, 3, 5, 2, 4).reshape(B, C * pH * pW, H // pH, W // pW)
316
+ # BN normalize
317
+ bn_mean = self.bn.running_mean.view(1, -1, 1, 1).to(z_p.device, z_p.dtype)
318
+ bn_std = torch.sqrt(
319
+ self.bn.running_var.view(1, -1, 1, 1).to(z_p.device, z_p.dtype)
320
+ + self.config.batch_norm_eps
321
+ )
322
+ z_p = (z_p - bn_mean) / bn_std
323
+ # Un-patchify: (B, C*pH*pW, H', W') → (B, C, H, W)
324
+ z = z_p.reshape(B, C, pH, pW, H // pH, W // pW)
325
+ z = z.permute(0, 1, 4, 2, 5, 3).reshape(B, C, H, W)
326
+ return z
327
+
328
+ def decode(self, z: torch.Tensor) -> torch.Tensor:
329
+ """Decode normalized latent to image (symmetric with encode).
330
+
331
+ Pipeline: patchify → BN denormalize → unpatchify → post_quant_conv → decoder.
332
+ This mirrors the encode path which does: encoder → patchify → BN normalize → unpatchify.
333
+ """
334
+ B, C, H, W = z.shape
335
+ pH, pW = self.config.patch_size # (2, 2)
336
+
337
+ # Patchify: (B, C, H, W) → (B, C*pH*pW, H//pH, W//pW)
338
+ z_p = z.reshape(B, C, H // pH, pH, W // pW, pW)
339
+ z_p = z_p.permute(0, 1, 3, 5, 2, 4).reshape(B, C * pH * pW, H // pH, W // pW)
340
+
341
+ # BN denormalize
342
+ bn_mean = self.bn.running_mean.view(1, -1, 1, 1).to(z_p.device, z_p.dtype)
343
+ bn_std = torch.sqrt(
344
+ self.bn.running_var.view(1, -1, 1, 1).to(z_p.device, z_p.dtype)
345
+ + self.config.batch_norm_eps
346
+ )
347
+ z_p = z_p * bn_std + bn_mean
348
+
349
+ # Un-patchify: (B, C*pH*pW, H', W') → (B, C, H, W)
350
+ z = z_p.reshape(B, C, pH, pW, H // pH, W // pW)
351
+ z = z.permute(0, 1, 4, 2, 5, 3).reshape(B, C, H, W)
352
+
353
+ z = self.post_quant_conv(z)
354
+ return self.decoder(z)
355
+
356
+ def normalize_latents(self, latents: torch.Tensor) -> torch.Tensor:
357
+ """Apply BN normalization to patchified latents (as in Flux2Pipeline)."""
358
+ bn_mean = self.bn.running_mean.view(1, -1, 1, 1).to(latents.device, latents.dtype)
359
+ bn_std = torch.sqrt(
360
+ self.bn.running_var.view(1, -1, 1, 1).to(latents.device, latents.dtype)
361
+ + self.config.batch_norm_eps
362
+ )
363
+ return (latents - bn_mean) / bn_std
364
+
365
+ def denormalize_latents(self, latents: torch.Tensor) -> torch.Tensor:
366
+ """Reverse BN normalization for decoding."""
367
+ bn_mean = self.bn.running_mean.view(1, -1, 1, 1).to(latents.device, latents.dtype)
368
+ bn_std = torch.sqrt(
369
+ self.bn.running_var.view(1, -1, 1, 1).to(latents.device, latents.dtype)
370
+ + self.config.batch_norm_eps
371
+ )
372
+ return latents * bn_std + bn_mean
373
+
374
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
375
+ return self.decode(self.encode(x))
376
+
377
+
378
+ # ---------------------------------------------------------------------------
379
+ # Loading helper
380
+ # ---------------------------------------------------------------------------
381
+ def _print_load_warning(missing: list[str], unexpected: list[str]) -> None:
382
+ if missing:
383
+ print(f"[Flux2-VAE] {len(missing)} missing keys:\n\t" + "\n\t".join(missing[:20]))
384
+ if unexpected:
385
+ print(f"[Flux2-VAE] {len(unexpected)} unexpected keys:\n\t" + "\n\t".join(unexpected[:20]))
386
+
387
+
388
+ def load_flux2_ae(local_path: str) -> tuple[AutoEncoderFlux2, Flux2VAEConfig]:
389
+ """
390
+ Load Flux2 VAE from a safetensors checkpoint directory or file.
391
+
392
+ Args:
393
+ local_path: Path to the safetensors file, or directory containing
394
+ diffusion_pytorch_model.safetensors.
395
+
396
+ Returns:
397
+ (model, config) tuple.
398
+ """
399
+ import os
400
+ if os.path.isdir(local_path):
401
+ sf_path = os.path.join(local_path, "diffusion_pytorch_model.safetensors")
402
+ else:
403
+ sf_path = local_path
404
+
405
+ config = Flux2VAEConfig()
406
+ model = AutoEncoderFlux2(config)
407
+
408
+ sd = load_sft(sf_path)
409
+ missing, unexpected = model.load_state_dict(sd, strict=False, assign=True)
410
+ _print_load_warning(missing, unexpected)
411
+
412
+ return model, config
unispace/modeling/siglip/__init__.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The HuggingFace Inc. team.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ from transformers.utils import (
7
+ OptionalDependencyNotAvailable,
8
+ _LazyModule,
9
+ is_sentencepiece_available,
10
+ is_torch_available,
11
+ is_vision_available,
12
+ )
13
+
14
+
15
+ _import_structure = {
16
+ "configuration_siglip": [
17
+ "SiglipConfig",
18
+ "SiglipTextConfig",
19
+ "SiglipVisionConfig",
20
+ ],
21
+ "processing_siglip": ["SiglipProcessor"],
22
+ }
23
+
24
+ try:
25
+ if not is_sentencepiece_available():
26
+ raise OptionalDependencyNotAvailable()
27
+ except OptionalDependencyNotAvailable:
28
+ pass
29
+ else:
30
+ _import_structure["tokenization_siglip"] = ["SiglipTokenizer"]
31
+
32
+
33
+ try:
34
+ if not is_vision_available():
35
+ raise OptionalDependencyNotAvailable()
36
+ except OptionalDependencyNotAvailable:
37
+ pass
38
+ else:
39
+ _import_structure["image_processing_siglip"] = ["SiglipImageProcessor"]
40
+
41
+ try:
42
+ if not is_torch_available():
43
+ raise OptionalDependencyNotAvailable()
44
+ except OptionalDependencyNotAvailable:
45
+ pass
46
+ else:
47
+ _import_structure["modeling_siglip"] = [
48
+ "SiglipModel",
49
+ "SiglipPreTrainedModel",
50
+ "SiglipTextModel",
51
+ "SiglipVisionModel",
52
+ "SiglipForImageClassification",
53
+ ]
54
+
55
+
56
+ if TYPE_CHECKING:
57
+ from .configuration_siglip import (
58
+ SiglipConfig,
59
+ SiglipTextConfig,
60
+ SiglipVisionConfig,
61
+ )
62
+ from .processing_siglip import SiglipProcessor
63
+
64
+ try:
65
+ if not is_sentencepiece_available():
66
+ raise OptionalDependencyNotAvailable()
67
+ except OptionalDependencyNotAvailable:
68
+ pass
69
+ else:
70
+ from .tokenization_siglip import SiglipTokenizer
71
+
72
+ try:
73
+ if not is_vision_available():
74
+ raise OptionalDependencyNotAvailable()
75
+ except OptionalDependencyNotAvailable:
76
+ pass
77
+ else:
78
+ from .image_processing_siglip import SiglipImageProcessor
79
+
80
+ try:
81
+ if not is_torch_available():
82
+ raise OptionalDependencyNotAvailable()
83
+ except OptionalDependencyNotAvailable:
84
+ pass
85
+ else:
86
+ from .modeling_siglip import (
87
+ SiglipForImageClassification,
88
+ SiglipModel,
89
+ SiglipPreTrainedModel,
90
+ SiglipTextModel,
91
+ SiglipVisionModel,
92
+ )
93
+
94
+
95
+ else:
96
+ import sys
97
+
98
+ sys.modules[__name__] = _LazyModule(__name__, globals()["__file__"], _import_structure, module_spec=__spec__)
unispace/modeling/siglip/configuration_siglip.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The HuggingFace Inc. team.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Siglip model configuration"""
5
+
6
+ import os
7
+ from typing import Union
8
+
9
+ from transformers.configuration_utils import PretrainedConfig
10
+ from transformers.utils import logging
11
+
12
+
13
+ logger = logging.get_logger(__name__)
14
+
15
+
16
+ class SiglipTextConfig(PretrainedConfig):
17
+ r"""
18
+ This is the configuration class to store the configuration of a [`SiglipTextModel`]. It is used to instantiate a
19
+ Siglip text encoder according to the specified arguments, defining the model architecture. Instantiating a
20
+ configuration with the defaults will yield a similar configuration to that of the text encoder of the Siglip
21
+ [google/siglip-base-patch16-224](https://huggingface.co/google/siglip-base-patch16-224) architecture.
22
+
23
+ Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
24
+ documentation from [`PretrainedConfig`] for more information.
25
+
26
+ Args:
27
+ vocab_size (`int`, *optional*, defaults to 32000):
28
+ Vocabulary size of the Siglip text model. Defines the number of different tokens that can be represented by
29
+ the `inputs_ids` passed when calling [`SiglipModel`].
30
+ hidden_size (`int`, *optional*, defaults to 768):
31
+ Dimensionality of the encoder layers and the pooler layer.
32
+ intermediate_size (`int`, *optional*, defaults to 3072):
33
+ Dimensionality of the "intermediate" (i.e., feed-forward) layer in the Transformer encoder.
34
+ num_hidden_layers (`int`, *optional*, defaults to 12):
35
+ Number of hidden layers in the Transformer encoder.
36
+ num_attention_heads (`int`, *optional*, defaults to 12):
37
+ Number of attention heads for each attention layer in the Transformer encoder.
38
+ max_position_embeddings (`int`, *optional*, defaults to 64):
39
+ The maximum sequence length that this model might ever be used with. Typically set this to something large
40
+ just in case (e.g., 512 or 1024 or 2048).
41
+ hidden_act (`str` or `function`, *optional*, defaults to `"gelu_pytorch_tanh"`):
42
+ The non-linear activation function (function or string) in the encoder and pooler. If string, `"gelu"`,
43
+ `"relu"`, `"selu"` and `"gelu_new"` `"quick_gelu"` are supported.
44
+ layer_norm_eps (`float`, *optional*, defaults to 1e-06):
45
+ The epsilon used by the layer normalization layers.
46
+ attention_dropout (`float`, *optional*, defaults to 0.0):
47
+ The dropout ratio for the attention probabilities.
48
+ pad_token_id (`int`, *optional*, defaults to 1):
49
+ The id of the padding token in the vocabulary.
50
+ bos_token_id (`int`, *optional*, defaults to 49406):
51
+ The id of the beginning-of-sequence token in the vocabulary.
52
+ eos_token_id (`int`, *optional*, defaults to 49407):
53
+ The id of the end-of-sequence token in the vocabulary.
54
+
55
+ Example:
56
+
57
+ ```python
58
+ >>> from transformers import SiglipTextConfig, SiglipTextModel
59
+
60
+ >>> # Initializing a SiglipTextConfig with google/siglip-base-patch16-224 style configuration
61
+ >>> configuration = SiglipTextConfig()
62
+
63
+ >>> # Initializing a SiglipTextModel (with random weights) from the google/siglip-base-patch16-224 style configuration
64
+ >>> model = SiglipTextModel(configuration)
65
+
66
+ >>> # Accessing the model configuration
67
+ >>> configuration = model.config
68
+ ```"""
69
+
70
+ model_type = "siglip_text_model"
71
+
72
+ def __init__(
73
+ self,
74
+ vocab_size=32000,
75
+ hidden_size=768,
76
+ intermediate_size=3072,
77
+ num_hidden_layers=12,
78
+ num_attention_heads=12,
79
+ max_position_embeddings=64,
80
+ hidden_act="gelu_pytorch_tanh",
81
+ layer_norm_eps=1e-6,
82
+ attention_dropout=0.0,
83
+ # This differs from `CLIPTokenizer`'s default and from openai/siglip
84
+ # See https://github.com/huggingface/transformers/pull/24773#issuecomment-1632287538
85
+ pad_token_id=1,
86
+ bos_token_id=49406,
87
+ eos_token_id=49407,
88
+ **kwargs,
89
+ ):
90
+ super().__init__(pad_token_id=pad_token_id, bos_token_id=bos_token_id, eos_token_id=eos_token_id, **kwargs)
91
+
92
+ self.vocab_size = vocab_size
93
+ self.hidden_size = hidden_size
94
+ self.intermediate_size = intermediate_size
95
+ self.num_hidden_layers = num_hidden_layers
96
+ self.num_attention_heads = num_attention_heads
97
+ self.max_position_embeddings = max_position_embeddings
98
+ self.layer_norm_eps = layer_norm_eps
99
+ self.hidden_act = hidden_act
100
+ self.attention_dropout = attention_dropout
101
+
102
+ @classmethod
103
+ def from_pretrained(cls, pretrained_model_name_or_path: Union[str, os.PathLike], **kwargs) -> "PretrainedConfig":
104
+ cls._set_token_in_kwargs(kwargs)
105
+
106
+ config_dict, kwargs = cls.get_config_dict(pretrained_model_name_or_path, **kwargs)
107
+
108
+ # get the text config dict if we are loading from SiglipConfig
109
+ if config_dict.get("model_type") == "siglip":
110
+ config_dict = config_dict["text_config"]
111
+
112
+ if "model_type" in config_dict and hasattr(cls, "model_type") and config_dict["model_type"] != cls.model_type:
113
+ logger.warning(
114
+ f"You are using a model of type {config_dict['model_type']} to instantiate a model of type "
115
+ f"{cls.model_type}. This is not supported for all configurations of models and can yield errors."
116
+ )
117
+
118
+ return cls.from_dict(config_dict, **kwargs)
119
+
120
+
121
+ class SiglipVisionConfig(PretrainedConfig):
122
+ r"""
123
+ This is the configuration class to store the configuration of a [`SiglipVisionModel`]. It is used to instantiate a
124
+ Siglip vision encoder according to the specified arguments, defining the model architecture. Instantiating a
125
+ configuration with the defaults will yield a similar configuration to that of the vision encoder of the Siglip
126
+ [google/siglip-base-patch16-224](https://huggingface.co/google/siglip-base-patch16-224) architecture.
127
+
128
+ Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
129
+ documentation from [`PretrainedConfig`] for more information.
130
+
131
+ Args:
132
+ hidden_size (`int`, *optional*, defaults to 768):
133
+ Dimensionality of the encoder layers and the pooler layer.
134
+ intermediate_size (`int`, *optional*, defaults to 3072):
135
+ Dimensionality of the "intermediate" (i.e., feed-forward) layer in the Transformer encoder.
136
+ num_hidden_layers (`int`, *optional*, defaults to 12):
137
+ Number of hidden layers in the Transformer encoder.
138
+ num_attention_heads (`int`, *optional*, defaults to 12):
139
+ Number of attention heads for each attention layer in the Transformer encoder.
140
+ num_channels (`int`, *optional*, defaults to 3):
141
+ Number of channels in the input images.
142
+ image_size (`int`, *optional*, defaults to 224):
143
+ The size (resolution) of each image.
144
+ patch_size (`int`, *optional*, defaults to 16):
145
+ The size (resolution) of each patch.
146
+ hidden_act (`str` or `function`, *optional*, defaults to `"gelu_pytorch_tanh"`):
147
+ The non-linear activation function (function or string) in the encoder and pooler. If string, `"gelu"`,
148
+ `"relu"`, `"selu"` and `"gelu_new"` `"quick_gelu"` are supported.
149
+ layer_norm_eps (`float`, *optional*, defaults to 1e-06):
150
+ The epsilon used by the layer normalization layers.
151
+ attention_dropout (`float`, *optional*, defaults to 0.0):
152
+ The dropout ratio for the attention probabilities.
153
+
154
+ Example:
155
+
156
+ ```python
157
+ >>> from transformers import SiglipVisionConfig, SiglipVisionModel
158
+
159
+ >>> # Initializing a SiglipVisionConfig with google/siglip-base-patch16-224 style configuration
160
+ >>> configuration = SiglipVisionConfig()
161
+
162
+ >>> # Initializing a SiglipVisionModel (with random weights) from the google/siglip-base-patch16-224 style configuration
163
+ >>> model = SiglipVisionModel(configuration)
164
+
165
+ >>> # Accessing the model configuration
166
+ >>> configuration = model.config
167
+ ```"""
168
+
169
+ model_type = "siglip_vision_model"
170
+
171
+ def __init__(
172
+ self,
173
+ hidden_size=768,
174
+ intermediate_size=3072,
175
+ num_hidden_layers=12,
176
+ num_attention_heads=12,
177
+ num_channels=3,
178
+ image_size=224,
179
+ patch_size=16,
180
+ hidden_act="gelu_pytorch_tanh",
181
+ layer_norm_eps=1e-6,
182
+ attention_dropout=0.0,
183
+ **kwargs,
184
+ ):
185
+ super().__init__(**kwargs)
186
+
187
+ self.hidden_size = hidden_size
188
+ self.intermediate_size = intermediate_size
189
+ self.num_hidden_layers = num_hidden_layers
190
+ self.num_attention_heads = num_attention_heads
191
+ self.num_channels = num_channels
192
+ self.patch_size = patch_size
193
+ self.image_size = image_size
194
+ self.attention_dropout = attention_dropout
195
+ self.layer_norm_eps = layer_norm_eps
196
+ self.hidden_act = hidden_act
197
+
198
+ @classmethod
199
+ def from_pretrained(cls, pretrained_model_name_or_path: Union[str, os.PathLike], **kwargs) -> "PretrainedConfig":
200
+ cls._set_token_in_kwargs(kwargs)
201
+
202
+ config_dict, kwargs = cls.get_config_dict(pretrained_model_name_or_path, **kwargs)
203
+
204
+ # get the vision config dict if we are loading from SiglipConfig
205
+ if config_dict.get("model_type") == "siglip":
206
+ config_dict = config_dict["vision_config"]
207
+
208
+ if "model_type" in config_dict and hasattr(cls, "model_type") and config_dict["model_type"] != cls.model_type:
209
+ logger.warning(
210
+ f"You are using a model of type {config_dict['model_type']} to instantiate a model of type "
211
+ f"{cls.model_type}. This is not supported for all configurations of models and can yield errors."
212
+ )
213
+
214
+ return cls.from_dict(config_dict, **kwargs)
215
+
216
+
217
+ class SiglipConfig(PretrainedConfig):
218
+ r"""
219
+ [`SiglipConfig`] is the configuration class to store the configuration of a [`SiglipModel`]. It is used to
220
+ instantiate a Siglip model according to the specified arguments, defining the text model and vision model configs.
221
+ Instantiating a configuration with the defaults will yield a similar configuration to that of the Siglip
222
+ [google/siglip-base-patch16-224](https://huggingface.co/google/siglip-base-patch16-224) architecture.
223
+
224
+ Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
225
+ documentation from [`PretrainedConfig`] for more information.
226
+
227
+ Args:
228
+ text_config (`dict`, *optional*):
229
+ Dictionary of configuration options used to initialize [`SiglipTextConfig`].
230
+ vision_config (`dict`, *optional*):
231
+ Dictionary of configuration options used to initialize [`SiglipVisionConfig`].
232
+ kwargs (*optional*):
233
+ Dictionary of keyword arguments.
234
+
235
+ Example:
236
+
237
+ ```python
238
+ >>> from transformers import SiglipConfig, SiglipModel
239
+
240
+ >>> # Initializing a SiglipConfig with google/siglip-base-patch16-224 style configuration
241
+ >>> configuration = SiglipConfig()
242
+
243
+ >>> # Initializing a SiglipModel (with random weights) from the google/siglip-base-patch16-224 style configuration
244
+ >>> model = SiglipModel(configuration)
245
+
246
+ >>> # Accessing the model configuration
247
+ >>> configuration = model.config
248
+
249
+ >>> # We can also initialize a SiglipConfig from a SiglipTextConfig and a SiglipVisionConfig
250
+ >>> from transformers import SiglipTextConfig, SiglipVisionConfig
251
+
252
+ >>> # Initializing a SiglipText and SiglipVision configuration
253
+ >>> config_text = SiglipTextConfig()
254
+ >>> config_vision = SiglipVisionConfig()
255
+
256
+ >>> config = SiglipConfig.from_text_vision_configs(config_text, config_vision)
257
+ ```"""
258
+
259
+ model_type = "siglip"
260
+
261
+ def __init__(self, text_config=None, vision_config=None, **kwargs):
262
+ super().__init__(**kwargs)
263
+
264
+ if text_config is None:
265
+ text_config = {}
266
+ logger.info("`text_config` is `None`. Initializing the `SiglipTextConfig` with default values.")
267
+
268
+ if vision_config is None:
269
+ vision_config = {}
270
+ logger.info("`vision_config` is `None`. initializing the `SiglipVisionConfig` with default values.")
271
+
272
+ self.text_config = SiglipTextConfig(**text_config)
273
+ self.vision_config = SiglipVisionConfig(**vision_config)
274
+
275
+ self.initializer_factor = 1.0
276
+
277
+ @classmethod
278
+ def from_text_vision_configs(cls, text_config: SiglipTextConfig, vision_config: SiglipVisionConfig, **kwargs):
279
+ r"""
280
+ Instantiate a [`SiglipConfig`] (or a derived class) from siglip text model configuration and siglip vision
281
+ model configuration.
282
+
283
+ Returns:
284
+ [`SiglipConfig`]: An instance of a configuration object
285
+ """
286
+
287
+ return cls(text_config=text_config.to_dict(), vision_config=vision_config.to_dict(), **kwargs)
unispace/modeling/siglip/convert_siglip_to_hf.py ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The HuggingFace Inc. team.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Convert SigLIP checkpoints from the original repository.
5
+
6
+ URL: https://github.com/google-research/big_vision/tree/main
7
+ """
8
+
9
+ import argparse
10
+ import collections
11
+ from pathlib import Path
12
+
13
+ import numpy as np
14
+ import requests
15
+ import torch
16
+ from huggingface_hub import hf_hub_download
17
+ from numpy import load
18
+ from PIL import Image
19
+
20
+ from transformers import SiglipConfig, SiglipImageProcessor, SiglipModel, SiglipProcessor, SiglipTokenizer
21
+ from transformers.utils import logging
22
+
23
+
24
+ logging.set_verbosity_info()
25
+ logger = logging.get_logger(__name__)
26
+
27
+
28
+ model_name_to_checkpoint = {
29
+ # base checkpoints
30
+ "siglip-base-patch16-224": "/Users/nielsrogge/Documents/SigLIP/webli_en_b16_224_63724782.npz",
31
+ "siglip-base-patch16-256": "/Users/nielsrogge/Documents/SigLIP/webli_en_b16_256_60500360.npz",
32
+ "siglip-base-patch16-384": "/Users/nielsrogge/Documents/SigLIP/webli_en_b16_384_68578854.npz",
33
+ "siglip-base-patch16-512": "/Users/nielsrogge/Documents/SigLIP/webli_en_b16_512_68580893.npz",
34
+ # large checkpoints
35
+ "siglip-large-patch16-256": "/Users/nielsrogge/Documents/SigLIP/webli_en_l16_256_60552751.npz",
36
+ "siglip-large-patch16-384": "/Users/nielsrogge/Documents/SigLIP/webli_en_l16_384_63634585.npz",
37
+ # multilingual checkpoint
38
+ "siglip-base-patch16-256-i18n": "/Users/nielsrogge/Documents/SigLIP/webli_i18n_b16_256_66117334.npz",
39
+ # so400m checkpoints
40
+ "siglip-so400m-patch14-384": "/Users/nielsrogge/Documents/SigLIP/webli_en_so400m_384_58765454.npz",
41
+ }
42
+
43
+ model_name_to_image_size = {
44
+ "siglip-base-patch16-224": 224,
45
+ "siglip-base-patch16-256": 256,
46
+ "siglip-base-patch16-384": 384,
47
+ "siglip-base-patch16-512": 512,
48
+ "siglip-large-patch16-256": 256,
49
+ "siglip-large-patch16-384": 384,
50
+ "siglip-base-patch16-256-i18n": 256,
51
+ "siglip-so400m-patch14-384": 384,
52
+ }
53
+
54
+
55
+ def get_siglip_config(model_name):
56
+ config = SiglipConfig()
57
+
58
+ vocab_size = 250000 if "i18n" in model_name else 32000
59
+ image_size = model_name_to_image_size[model_name]
60
+ patch_size = 16 if "patch16" in model_name else 14
61
+
62
+ # size of the architecture
63
+ config.vision_config.image_size = image_size
64
+ config.vision_config.patch_size = patch_size
65
+ config.text_config.vocab_size = vocab_size
66
+
67
+ if "base" in model_name:
68
+ pass
69
+ elif "large" in model_name:
70
+ config.text_config.hidden_size = 1024
71
+ config.text_config.intermediate_size = 4096
72
+ config.text_config.num_hidden_layers = 24
73
+ config.text_config.num_attention_heads = 16
74
+ config.vision_config.hidden_size = 1024
75
+ config.vision_config.intermediate_size = 4096
76
+ config.vision_config.num_hidden_layers = 24
77
+ config.vision_config.num_attention_heads = 16
78
+ elif "so400m" in model_name:
79
+ config.text_config.hidden_size = 1152
80
+ config.text_config.intermediate_size = 4304
81
+ config.text_config.num_hidden_layers = 27
82
+ config.text_config.num_attention_heads = 16
83
+ config.vision_config.hidden_size = 1152
84
+ config.vision_config.intermediate_size = 4304
85
+ config.vision_config.num_hidden_layers = 27
86
+ config.vision_config.num_attention_heads = 16
87
+ else:
88
+ raise ValueError("Model not supported")
89
+
90
+ return config
91
+
92
+
93
+ def create_rename_keys(config):
94
+ rename_keys = []
95
+ # fmt: off
96
+
97
+ # vision encoder
98
+
99
+ rename_keys.append(("params/img/embedding/kernel", "vision_model.embeddings.patch_embedding.weight"))
100
+ rename_keys.append(("params/img/embedding/bias", "vision_model.embeddings.patch_embedding.bias"))
101
+ rename_keys.append(("params/img/pos_embedding", "vision_model.embeddings.position_embedding.weight"))
102
+
103
+ for i in range(config.vision_config.num_hidden_layers):
104
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/LayerNorm_0/scale", f"vision_model.encoder.layers.{i}.layer_norm1.weight"))
105
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/LayerNorm_0/bias", f"vision_model.encoder.layers.{i}.layer_norm1.bias"))
106
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/LayerNorm_1/scale", f"vision_model.encoder.layers.{i}.layer_norm2.weight"))
107
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/LayerNorm_1/bias", f"vision_model.encoder.layers.{i}.layer_norm2.bias"))
108
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/MlpBlock_0/Dense_0/kernel", f"vision_model.encoder.layers.{i}.mlp.fc1.weight"))
109
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/MlpBlock_0/Dense_0/bias", f"vision_model.encoder.layers.{i}.mlp.fc1.bias"))
110
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/MlpBlock_0/Dense_1/kernel", f"vision_model.encoder.layers.{i}.mlp.fc2.weight"))
111
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/MlpBlock_0/Dense_1/bias", f"vision_model.encoder.layers.{i}.mlp.fc2.bias"))
112
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/MultiHeadDotProductAttention_0/key/kernel", f"vision_model.encoder.layers.{i}.self_attn.k_proj.weight"))
113
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/MultiHeadDotProductAttention_0/key/bias", f"vision_model.encoder.layers.{i}.self_attn.k_proj.bias"))
114
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/MultiHeadDotProductAttention_0/value/kernel", f"vision_model.encoder.layers.{i}.self_attn.v_proj.weight"))
115
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/MultiHeadDotProductAttention_0/value/bias", f"vision_model.encoder.layers.{i}.self_attn.v_proj.bias"))
116
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/MultiHeadDotProductAttention_0/query/kernel", f"vision_model.encoder.layers.{i}.self_attn.q_proj.weight"))
117
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/MultiHeadDotProductAttention_0/query/bias", f"vision_model.encoder.layers.{i}.self_attn.q_proj.bias"))
118
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/MultiHeadDotProductAttention_0/out/kernel", f"vision_model.encoder.layers.{i}.self_attn.out_proj.weight"))
119
+ rename_keys.append((f"params/img/Transformer/encoderblock_{i}/MultiHeadDotProductAttention_0/out/bias", f"vision_model.encoder.layers.{i}.self_attn.out_proj.bias"))
120
+
121
+ rename_keys.append(("params/img/Transformer/encoder_norm/scale", "vision_model.post_layernorm.weight"))
122
+ rename_keys.append(("params/img/Transformer/encoder_norm/bias", "vision_model.post_layernorm.bias"))
123
+
124
+ rename_keys.append(("params/img/MAPHead_0/probe", "vision_model.head.probe"))
125
+ rename_keys.append(("params/img/MAPHead_0/LayerNorm_0/scale", "vision_model.head.layernorm.weight"))
126
+ rename_keys.append(("params/img/MAPHead_0/LayerNorm_0/bias", "vision_model.head.layernorm.bias"))
127
+ rename_keys.append(("params/img/MAPHead_0/MlpBlock_0/Dense_0/kernel", "vision_model.head.mlp.fc1.weight"))
128
+ rename_keys.append(("params/img/MAPHead_0/MlpBlock_0/Dense_0/bias", "vision_model.head.mlp.fc1.bias"))
129
+ rename_keys.append(("params/img/MAPHead_0/MlpBlock_0/Dense_1/kernel", "vision_model.head.mlp.fc2.weight"))
130
+ rename_keys.append(("params/img/MAPHead_0/MlpBlock_0/Dense_1/bias", "vision_model.head.mlp.fc2.bias"))
131
+ rename_keys.append(("params/img/MAPHead_0/MultiHeadDotProductAttention_0/out/kernel", "vision_model.head.attention.out_proj.weight"))
132
+ rename_keys.append(("params/img/MAPHead_0/MultiHeadDotProductAttention_0/out/bias", "vision_model.head.attention.out_proj.bias"))
133
+
134
+ # text encoder
135
+
136
+ rename_keys.append(("params/txt/Embed_0/embedding", "text_model.embeddings.token_embedding.weight"))
137
+ rename_keys.append(("params/txt/pos_embedding", "text_model.embeddings.position_embedding.weight"))
138
+
139
+ for i in range(config.text_config.num_hidden_layers):
140
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/LayerNorm_0/scale", f"text_model.encoder.layers.{i}.layer_norm1.weight"))
141
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/LayerNorm_0/bias", f"text_model.encoder.layers.{i}.layer_norm1.bias"))
142
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/LayerNorm_1/scale", f"text_model.encoder.layers.{i}.layer_norm2.weight"))
143
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/LayerNorm_1/bias", f"text_model.encoder.layers.{i}.layer_norm2.bias"))
144
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/MlpBlock_0/Dense_0/kernel", f"text_model.encoder.layers.{i}.mlp.fc1.weight"))
145
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/MlpBlock_0/Dense_0/bias", f"text_model.encoder.layers.{i}.mlp.fc1.bias"))
146
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/MlpBlock_0/Dense_1/kernel", f"text_model.encoder.layers.{i}.mlp.fc2.weight"))
147
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/MlpBlock_0/Dense_1/bias", f"text_model.encoder.layers.{i}.mlp.fc2.bias"))
148
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/MultiHeadDotProductAttention_0/key/kernel", f"text_model.encoder.layers.{i}.self_attn.k_proj.weight"))
149
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/MultiHeadDotProductAttention_0/key/bias", f"text_model.encoder.layers.{i}.self_attn.k_proj.bias"))
150
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/MultiHeadDotProductAttention_0/value/kernel", f"text_model.encoder.layers.{i}.self_attn.v_proj.weight"))
151
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/MultiHeadDotProductAttention_0/value/bias", f"text_model.encoder.layers.{i}.self_attn.v_proj.bias"))
152
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/MultiHeadDotProductAttention_0/query/kernel", f"text_model.encoder.layers.{i}.self_attn.q_proj.weight"))
153
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/MultiHeadDotProductAttention_0/query/bias", f"text_model.encoder.layers.{i}.self_attn.q_proj.bias"))
154
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/MultiHeadDotProductAttention_0/out/kernel", f"text_model.encoder.layers.{i}.self_attn.out_proj.weight"))
155
+ rename_keys.append((f"params/txt/Encoder_0/encoderblock_{i}/MultiHeadDotProductAttention_0/out/bias", f"text_model.encoder.layers.{i}.self_attn.out_proj.bias"))
156
+
157
+ rename_keys.append(("params/txt/Encoder_0/encoder_norm/scale", "text_model.final_layer_norm.weight"))
158
+ rename_keys.append(("params/txt/Encoder_0/encoder_norm/bias", "text_model.final_layer_norm.bias"))
159
+ rename_keys.append(("params/txt/head/kernel", "text_model.head.weight"))
160
+ rename_keys.append(("params/txt/head/bias", "text_model.head.bias"))
161
+
162
+ # learned temperature and bias
163
+ rename_keys.append(("params/t", "logit_scale"))
164
+ rename_keys.append(("params/b", "logit_bias"))
165
+
166
+ # fmt: on
167
+ return rename_keys
168
+
169
+
170
+ def rename_key(dct, old, new, config):
171
+ val = dct.pop(old)
172
+
173
+ if ("out_proj" in new or "v_proj" in new or "k_proj" in new or "q_proj" in new) and "vision" in new:
174
+ val = val.reshape(-1, config.vision_config.hidden_size)
175
+ if ("out_proj" in new or "v_proj" in new or "k_proj" in new or "q_proj" in new) and "text" in new:
176
+ val = val.reshape(-1, config.text_config.hidden_size)
177
+
178
+ if "patch_embedding.weight" in new:
179
+ val = val.transpose(3, 2, 0, 1)
180
+ elif new.endswith("weight") and "position_embedding" not in new and "token_embedding" not in new:
181
+ val = val.T
182
+
183
+ if "position_embedding" in new and "vision" in new:
184
+ val = val.reshape(-1, config.vision_config.hidden_size)
185
+ if "position_embedding" in new and "text" in new:
186
+ val = val.reshape(-1, config.text_config.hidden_size)
187
+
188
+ if new.endswith("bias"):
189
+ val = val.reshape(-1)
190
+
191
+ dct[new] = torch.from_numpy(val)
192
+
193
+
194
+ def read_in_q_k_v_head(state_dict, config):
195
+ # read in individual input projection layers
196
+ key_proj_weight = (
197
+ state_dict.pop("params/img/MAPHead_0/MultiHeadDotProductAttention_0/key/kernel")
198
+ .reshape(-1, config.vision_config.hidden_size)
199
+ .T
200
+ )
201
+ key_proj_bias = state_dict.pop("params/img/MAPHead_0/MultiHeadDotProductAttention_0/key/bias").reshape(-1)
202
+ value_proj_weight = (
203
+ state_dict.pop("params/img/MAPHead_0/MultiHeadDotProductAttention_0/value/kernel")
204
+ .reshape(-1, config.vision_config.hidden_size)
205
+ .T
206
+ )
207
+ value_proj_bias = state_dict.pop("params/img/MAPHead_0/MultiHeadDotProductAttention_0/value/bias").reshape(-1)
208
+ query_proj_weight = (
209
+ state_dict.pop("params/img/MAPHead_0/MultiHeadDotProductAttention_0/query/kernel")
210
+ .reshape(-1, config.vision_config.hidden_size)
211
+ .T
212
+ )
213
+ query_proj_bias = state_dict.pop("params/img/MAPHead_0/MultiHeadDotProductAttention_0/query/bias").reshape(-1)
214
+
215
+ # next, add them to the state dict as a single matrix + vector
216
+ state_dict["vision_model.head.attention.in_proj_weight"] = torch.from_numpy(
217
+ np.concatenate([query_proj_weight, key_proj_weight, value_proj_weight], axis=0)
218
+ )
219
+ state_dict["vision_model.head.attention.in_proj_bias"] = torch.from_numpy(
220
+ np.concatenate([query_proj_bias, key_proj_bias, value_proj_bias], axis=0)
221
+ )
222
+
223
+
224
+ # We will verify our results on an image of cute cats
225
+ def prepare_img():
226
+ url = "http://images.cocodataset.org/val2017/000000039769.jpg"
227
+ image = Image.open(requests.get(url, stream=True).raw)
228
+ return image
229
+
230
+
231
+ def flatten_nested_dict(params, parent_key="", sep="/"):
232
+ items = []
233
+
234
+ for k, v in params.items():
235
+ new_key = parent_key + sep + k if parent_key else k
236
+
237
+ if isinstance(v, collections.abc.MutableMapping):
238
+ items.extend(flatten_nested_dict(v, new_key, sep=sep).items())
239
+ else:
240
+ items.append((new_key, v))
241
+ return dict(items)
242
+
243
+
244
+ @torch.no_grad()
245
+ def convert_siglip_checkpoint(model_name, pytorch_dump_folder_path, verify_logits=True, push_to_hub=False):
246
+ """
247
+ Copy/paste/tweak model's weights to our SigLIP structure.
248
+ """
249
+
250
+ # define default SigLIP configuration
251
+ config = get_siglip_config(model_name)
252
+
253
+ # get checkpoint
254
+ checkpoint = model_name_to_checkpoint[model_name]
255
+
256
+ # get vocab file
257
+ if "i18n" in model_name:
258
+ vocab_file = "/Users/nielsrogge/Documents/SigLIP/multilingual_vocab/sentencepiece.model"
259
+ else:
260
+ vocab_file = "/Users/nielsrogge/Documents/SigLIP/english_vocab/sentencepiece.model"
261
+
262
+ # load original state dict
263
+ data = load(checkpoint)
264
+ state_dict = flatten_nested_dict(data)
265
+
266
+ # remove and rename some keys
267
+ rename_keys = create_rename_keys(config)
268
+ for src, dest in rename_keys:
269
+ rename_key(state_dict, src, dest, config)
270
+
271
+ # qkv matrices of attention pooling head need special treatment
272
+ read_in_q_k_v_head(state_dict, config)
273
+
274
+ # load HuggingFace model
275
+ model = SiglipModel(config).eval()
276
+ model.load_state_dict(state_dict)
277
+
278
+ # create processor
279
+ # important: make tokenizer not return attention_mask since original one doesn't require it
280
+ image_size = config.vision_config.image_size
281
+ size = {"height": image_size, "width": image_size}
282
+ image_processor = SiglipImageProcessor(size=size)
283
+ tokenizer = SiglipTokenizer(vocab_file=vocab_file, model_input_names=["input_ids"])
284
+ processor = SiglipProcessor(image_processor=image_processor, tokenizer=tokenizer)
285
+
286
+ # verify on dummy images and texts
287
+ url_1 = "https://cdn.openai.com/multimodal-neurons/assets/apple/apple-ipod.jpg"
288
+ image_1 = Image.open(requests.get(url_1, stream=True).raw).convert("RGB")
289
+ url_2 = "https://cdn.openai.com/multimodal-neurons/assets/apple/apple-blank.jpg"
290
+ image_2 = Image.open(requests.get(url_2, stream=True).raw).convert("RGB")
291
+ texts = ["an apple", "a picture of an apple"]
292
+
293
+ inputs = processor(images=[image_1, image_2], text=texts, return_tensors="pt", padding="max_length")
294
+
295
+ # verify input_ids against original ones
296
+ if image_size == 224:
297
+ filename = "siglip_pixel_values.pt"
298
+ elif image_size == 256:
299
+ filename = "siglip_pixel_values_256.pt"
300
+ elif image_size == 384:
301
+ filename = "siglip_pixel_values_384.pt"
302
+ elif image_size == 512:
303
+ filename = "siglip_pixel_values_512.pt"
304
+ else:
305
+ raise ValueError("Image size not supported")
306
+
307
+ filepath = hf_hub_download(repo_id="nielsr/test-image", filename=filename, repo_type="dataset")
308
+ original_pixel_values = torch.load(filepath)
309
+ filepath = hf_hub_download(repo_id="nielsr/test-image", filename="siglip_input_ids.pt", repo_type="dataset")
310
+ original_input_ids = torch.load(filepath)
311
+
312
+ if "i18n" not in model_name:
313
+ assert inputs.input_ids.tolist() == original_input_ids.tolist()
314
+
315
+ print("Mean of original pixel values:", original_pixel_values.mean())
316
+ print("Mean of new pixel values:", inputs.pixel_values.mean())
317
+
318
+ # note: we're testing with original pixel values here since we don't have exact pixel values
319
+ with torch.no_grad():
320
+ outputs = model(input_ids=inputs.input_ids, pixel_values=original_pixel_values)
321
+
322
+ # with torch.no_grad():
323
+ # outputs = model(input_ids=inputs.input_ids, pixel_values=inputs.pixel_values)
324
+
325
+ print(outputs.logits_per_image[:3, :3])
326
+
327
+ probs = torch.sigmoid(outputs.logits_per_image) # these are the probabilities
328
+ print(f"{probs[0][0]:.1%} that image 0 is '{texts[0]}'")
329
+ print(f"{probs[0][1]:.1%} that image 0 is '{texts[1]}'")
330
+
331
+ if verify_logits:
332
+ if model_name == "siglip-base-patch16-224":
333
+ expected_slice = torch.tensor(
334
+ [[-2.9621, -2.1672], [-0.2713, 0.2910]],
335
+ )
336
+ elif model_name == "siglip-base-patch16-256":
337
+ expected_slice = torch.tensor(
338
+ [[-3.1146, -1.9894], [-0.7312, 0.6387]],
339
+ )
340
+ elif model_name == "siglip-base-patch16-384":
341
+ expected_slice = torch.tensor(
342
+ [[-2.8098, -2.1891], [-0.4242, 0.4102]],
343
+ )
344
+ elif model_name == "siglip-base-patch16-512":
345
+ expected_slice = torch.tensor(
346
+ [[-2.7899, -2.2668], [-0.4295, -0.0735]],
347
+ )
348
+ elif model_name == "siglip-large-patch16-256":
349
+ expected_slice = torch.tensor(
350
+ [[-1.5827, -0.5801], [-0.9153, 0.1363]],
351
+ )
352
+ elif model_name == "siglip-large-patch16-384":
353
+ expected_slice = torch.tensor(
354
+ [[-2.1523, -0.2899], [-0.2959, 0.7884]],
355
+ )
356
+ elif model_name == "siglip-so400m-patch14-384":
357
+ expected_slice = torch.tensor([[-1.2441, -0.6649], [-0.7060, 0.7374]])
358
+ elif model_name == "siglip-base-patch16-256-i18n":
359
+ expected_slice = torch.tensor(
360
+ [[-0.9064, 0.1073], [-0.0299, 0.5304]],
361
+ )
362
+
363
+ assert torch.allclose(outputs.logits_per_image[:3, :3], expected_slice, atol=1e-4)
364
+ print("Looks ok!")
365
+
366
+ if pytorch_dump_folder_path is not None:
367
+ Path(pytorch_dump_folder_path).mkdir(exist_ok=True)
368
+ print(f"Saving model {model_name} to {pytorch_dump_folder_path}")
369
+ model.save_pretrained(pytorch_dump_folder_path)
370
+ print(f"Saving processor to {pytorch_dump_folder_path}")
371
+ processor.save_pretrained(pytorch_dump_folder_path)
372
+
373
+ if push_to_hub:
374
+ model.push_to_hub(f"nielsr/{model_name}")
375
+ processor.push_to_hub(f"nielsr/{model_name}")
376
+
377
+
378
+ if __name__ == "__main__":
379
+ parser = argparse.ArgumentParser()
380
+ # Required parameters
381
+ parser.add_argument(
382
+ "--model_name",
383
+ default="siglip-base-patch16-224",
384
+ type=str,
385
+ choices=model_name_to_checkpoint.keys(),
386
+ help="Name of the model you'd like to convert.",
387
+ )
388
+ parser.add_argument(
389
+ "--pytorch_dump_folder_path", default=None, type=str, help="Path to the output PyTorch model directory."
390
+ )
391
+ parser.add_argument(
392
+ "--verify_logits",
393
+ action="store_false",
394
+ help="Whether to verify logits against the original implementation.",
395
+ )
396
+ parser.add_argument(
397
+ "--push_to_hub", action="store_true", help="Whether or not to push the converted model to the 🤗 hub."
398
+ )
399
+
400
+ args = parser.parse_args()
401
+ convert_siglip_checkpoint(args.model_name, args.pytorch_dump_folder_path, args.verify_logits, args.push_to_hub)
unispace/modeling/siglip/image_processing_siglip.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The HuggingFace Inc. team.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Image processor class for SigLIP."""
5
+
6
+ from typing import Dict, List, Optional, Union
7
+
8
+ from transformers.image_processing_utils import BaseImageProcessor, BatchFeature, get_size_dict
9
+ from transformers.image_transforms import (
10
+ convert_to_rgb,
11
+ resize,
12
+ to_channel_dimension_format,
13
+ )
14
+ from transformers.image_utils import (
15
+ IMAGENET_STANDARD_MEAN,
16
+ IMAGENET_STANDARD_STD,
17
+ ChannelDimension,
18
+ ImageInput,
19
+ PILImageResampling,
20
+ infer_channel_dimension_format,
21
+ is_scaled_image,
22
+ make_list_of_images,
23
+ to_numpy_array,
24
+ valid_images,
25
+ validate_preprocess_arguments,
26
+ )
27
+ from transformers.utils import TensorType, filter_out_non_signature_kwargs, is_vision_available, logging
28
+
29
+
30
+ logger = logging.get_logger(__name__)
31
+
32
+
33
+ if is_vision_available():
34
+ import PIL
35
+
36
+
37
+ class SiglipImageProcessor(BaseImageProcessor):
38
+ r"""
39
+ Constructs a SigLIP image processor.
40
+
41
+ Args:
42
+ do_resize (`bool`, *optional*, defaults to `True`):
43
+ Whether to resize the image's (height, width) dimensions to the specified `size`. Can be overridden by
44
+ `do_resize` in the `preprocess` method.
45
+ size (`Dict[str, int]` *optional*, defaults to `{"height": 224, "width": 224}`):
46
+ Size of the image after resizing. Can be overridden by `size` in the `preprocess` method.
47
+ resample (`PILImageResampling`, *optional*, defaults to `Resampling.BICUBIC`):
48
+ Resampling filter to use if resizing the image. Can be overridden by `resample` in the `preprocess` method.
49
+ do_rescale (`bool`, *optional*, defaults to `True`):
50
+ Whether to rescale the image by the specified scale `rescale_factor`. Can be overridden by `do_rescale` in
51
+ the `preprocess` method.
52
+ rescale_factor (`int` or `float`, *optional*, defaults to `1/255`):
53
+ Scale factor to use if rescaling the image. Can be overridden by `rescale_factor` in the `preprocess`
54
+ method.
55
+ do_normalize (`bool`, *optional*, defaults to `True`):
56
+ Whether to normalize the image by the specified mean and standard deviation. Can be overridden by
57
+ `do_normalize` in the `preprocess` method.
58
+ image_mean (`float` or `List[float]`, *optional*, defaults to `[0.5, 0.5, 0.5]`):
59
+ Mean to use if normalizing the image. This is a float or list of floats the length of the number of
60
+ channels in the image. Can be overridden by the `image_mean` parameter in the `preprocess` method.
61
+ image_std (`float` or `List[float]`, *optional*, defaults to `[0.5, 0.5, 0.5]`):
62
+ Standard deviation to use if normalizing the image. This is a float or list of floats the length of the
63
+ number of channels in the image. Can be overridden by the `image_std` parameter in the `preprocess` method.
64
+ Can be overridden by the `image_std` parameter in the `preprocess` method.
65
+ do_convert_rgb (`bool`, *optional*, defaults to `True`):
66
+ Whether to convert the image to RGB.
67
+ """
68
+
69
+ model_input_names = ["pixel_values"]
70
+
71
+ def __init__(
72
+ self,
73
+ do_resize: bool = True,
74
+ size: Dict[str, int] = None,
75
+ resample: PILImageResampling = PILImageResampling.BICUBIC,
76
+ do_rescale: bool = True,
77
+ rescale_factor: Union[int, float] = 1 / 255,
78
+ do_normalize: bool = True,
79
+ image_mean: Optional[Union[float, List[float]]] = None,
80
+ image_std: Optional[Union[float, List[float]]] = None,
81
+ do_convert_rgb: bool = None,
82
+ **kwargs,
83
+ ) -> None:
84
+ super().__init__(**kwargs)
85
+ size = size if size is not None else {"height": 224, "width": 224}
86
+ image_mean = image_mean if image_mean is not None else IMAGENET_STANDARD_MEAN
87
+ image_std = image_std if image_std is not None else IMAGENET_STANDARD_STD
88
+
89
+ self.do_resize = do_resize
90
+ self.size = size
91
+ self.resample = resample
92
+ self.do_rescale = do_rescale
93
+ self.rescale_factor = rescale_factor
94
+ self.do_normalize = do_normalize
95
+ self.image_mean = image_mean
96
+ self.image_std = image_std
97
+ self.do_convert_rgb = do_convert_rgb
98
+
99
+ @filter_out_non_signature_kwargs()
100
+ def preprocess(
101
+ self,
102
+ images: ImageInput,
103
+ do_resize: bool = None,
104
+ size: Dict[str, int] = None,
105
+ resample: PILImageResampling = None,
106
+ do_rescale: bool = None,
107
+ rescale_factor: float = None,
108
+ do_normalize: bool = None,
109
+ image_mean: Optional[Union[float, List[float]]] = None,
110
+ image_std: Optional[Union[float, List[float]]] = None,
111
+ return_tensors: Optional[Union[str, TensorType]] = None,
112
+ data_format: Optional[ChannelDimension] = ChannelDimension.FIRST,
113
+ input_data_format: Optional[Union[str, ChannelDimension]] = None,
114
+ do_convert_rgb: bool = None,
115
+ ) -> PIL.Image.Image:
116
+ """
117
+ Preprocess an image or batch of images.
118
+
119
+ Args:
120
+ images (`ImageInput`):
121
+ Image to preprocess. Expects a single or batch of images with pixel values ranging from 0 to 255. If
122
+ passing in images with pixel values between 0 and 1, set `do_rescale=False`.
123
+ do_resize (`bool`, *optional*, defaults to `self.do_resize`):
124
+ Whether to resize the image.
125
+ size (`Dict[str, int]`, *optional*, defaults to `self.size`):
126
+ Size of the image after resizing.
127
+ resample (`int`, *optional*, defaults to `self.resample`):
128
+ Resampling filter to use if resizing the image. This can be one of the enum `PILImageResampling`. Only
129
+ has an effect if `do_resize` is set to `True`.
130
+ do_rescale (`bool`, *optional*, defaults to `self.do_rescale`):
131
+ Whether to rescale the image.
132
+ rescale_factor (`float`, *optional*, defaults to `self.rescale_factor`):
133
+ Rescale factor to rescale the image by if `do_rescale` is set to `True`.
134
+ do_normalize (`bool`, *optional*, defaults to `self.do_normalize`):
135
+ Whether to normalize the image.
136
+ image_mean (`float` or `List[float]`, *optional*, defaults to `self.image_mean`):
137
+ Image mean to use for normalization. Only has an effect if `do_normalize` is set to `True`.
138
+ image_std (`float` or `List[float]`, *optional*, defaults to `self.image_std`):
139
+ Image standard deviation to use for normalization. Only has an effect if `do_normalize` is set to
140
+ `True`.
141
+ return_tensors (`str` or `TensorType`, *optional*):
142
+ The type of tensors to return. Can be one of:
143
+ - Unset: Return a list of `np.ndarray`.
144
+ - `TensorType.TENSORFLOW` or `'tf'`: Return a batch of type `tf.Tensor`.
145
+ - `TensorType.PYTORCH` or `'pt'`: Return a batch of type `torch.Tensor`.
146
+ - `TensorType.NUMPY` or `'np'`: Return a batch of type `np.ndarray`.
147
+ - `TensorType.JAX` or `'jax'`: Return a batch of type `jax.numpy.ndarray`.
148
+ data_format (`ChannelDimension` or `str`, *optional*, defaults to `ChannelDimension.FIRST`):
149
+ The channel dimension format for the output image. Can be one of:
150
+ - `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
151
+ - `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
152
+ - Unset: Use the channel dimension format of the input image.
153
+ input_data_format (`ChannelDimension` or `str`, *optional*):
154
+ The channel dimension format for the input image. If unset, the channel dimension format is inferred
155
+ from the input image. Can be one of:
156
+ - `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
157
+ - `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
158
+ - `"none"` or `ChannelDimension.NONE`: image in (height, width) format.
159
+ do_convert_rgb (`bool`, *optional*, defaults to `self.do_convert_rgb`):
160
+ Whether to convert the image to RGB.
161
+ """
162
+ do_resize = do_resize if do_resize is not None else self.do_resize
163
+ size = size if size is not None else self.size
164
+ size = get_size_dict(size, param_name="size", default_to_square=False)
165
+ resample = resample if resample is not None else self.resample
166
+ do_rescale = do_rescale if do_rescale is not None else self.do_rescale
167
+ rescale_factor = rescale_factor if rescale_factor is not None else self.rescale_factor
168
+ do_normalize = do_normalize if do_normalize is not None else self.do_normalize
169
+ image_mean = image_mean if image_mean is not None else self.image_mean
170
+ image_std = image_std if image_std is not None else self.image_std
171
+ do_convert_rgb = do_convert_rgb if do_convert_rgb is not None else self.do_convert_rgb
172
+
173
+ images = make_list_of_images(images)
174
+
175
+ if not valid_images(images):
176
+ raise ValueError(
177
+ "Invalid image type. Must be of type PIL.Image.Image, numpy.ndarray, "
178
+ "torch.Tensor, tf.Tensor or jax.ndarray."
179
+ )
180
+ validate_preprocess_arguments(
181
+ do_rescale=do_rescale,
182
+ rescale_factor=rescale_factor,
183
+ do_normalize=do_normalize,
184
+ image_mean=image_mean,
185
+ image_std=image_std,
186
+ do_resize=do_resize,
187
+ size=size,
188
+ resample=resample,
189
+ )
190
+ # All transformations expect numpy arrays.
191
+ images = [to_numpy_array(image) for image in images]
192
+
193
+ if do_convert_rgb:
194
+ images = [convert_to_rgb(image) for image in images]
195
+
196
+ if is_scaled_image(images[0]) and do_rescale:
197
+ logger.warning_once(
198
+ "It looks like you are trying to rescale already rescaled images. If the input"
199
+ " images have pixel values between 0 and 1, set `do_rescale=False` to avoid rescaling them again."
200
+ )
201
+
202
+ if input_data_format is None:
203
+ # We assume that all images have the same channel dimension format.
204
+ input_data_format = infer_channel_dimension_format(images[0])
205
+
206
+ if do_resize:
207
+ height, width = size["height"], size["width"]
208
+ images = [
209
+ resize(image=image, size=(height, width), resample=resample, input_data_format=input_data_format)
210
+ for image in images
211
+ ]
212
+
213
+ if do_rescale:
214
+ images = [
215
+ self.rescale(image=image, scale=rescale_factor, input_data_format=input_data_format)
216
+ for image in images
217
+ ]
218
+
219
+ if do_normalize:
220
+ images = [
221
+ self.normalize(image=image, mean=image_mean, std=image_std, input_data_format=input_data_format)
222
+ for image in images
223
+ ]
224
+
225
+ images = [
226
+ to_channel_dimension_format(image, data_format, input_channel_dim=input_data_format) for image in images
227
+ ]
228
+
229
+ data = {"pixel_values": images}
230
+ return BatchFeature(data=data, tensor_type=return_tensors)