multimodalart HF Staff commited on
Commit
2267636
·
verified ·
1 Parent(s): e8fb64b

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -1,13 +1,31 @@
1
  ---
2
- title: Mmdiff
3
- emoji: 📉
4
- colorFrom: purple
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.19.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: MMDiff
3
+ emoji: 🎨
4
+ colorFrom: blue
5
+ colorTo: yellow
6
  sdk: gradio
7
+ sdk_version: "5.0.0"
 
8
  app_file: app.py
9
+ short_description: Multi-modal generation with diffusion transformers
10
+ python_version: "3.10"
11
+ startup_duration_timeout: 600
12
  ---
13
 
14
+ # MMDiff: Extending Diffusion Transformers for Multi-Modal Generation
15
+
16
+ This Space demonstrates MMDiff, a method that extends frozen diffusion transformers (FLUX.1-dev) to generate images alongside dense predictions (saliency maps, segmentation maps, depth maps) in a single forward pass.
17
+
18
+ ## How it works
19
+
20
+ 1. A text prompt is used to generate an image with FLUX.1-dev
21
+ 2. During denoising, intermediate transformer features and concept attention maps are captured
22
+ 3. Lightweight trained decoder heads (DPT, DeepLabV3+) decode these features into dense predictions:
23
+ - **Saliency** (DUTS): Binary foreground/background segmentation
24
+ - **Segmentation** (Pascal VOC): 21-class semantic segmentation
25
+ - **Depth** (NYU Depth V2): Monocular depth estimation
26
+
27
+ ## Model
28
+
29
+ - **Backbone**: [black-forest-labs/FLUX.1-dev](https://huggingface.co/black-forest-labs/FLUX.1-dev) (frozen)
30
+ - **Decoder weights**: [yagmurakarken/mmdiff](https://huggingface.co/yagmurakarken/mmdiff)
31
+ - **Paper**: [MMDiff: Extending Diffusion Transformers for Multi-Modal Generation](https://huggingface.co/papers/2606.16673)
app.py ADDED
@@ -0,0 +1,842 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MMDiff: Multi-Modal Generation with Diffusion Transformers.
2
+
3
+ This demo generates an image from a text prompt using FLUX.1-dev while simultaneously
4
+ producing dense predictions (saliency, segmentation, depth) from the frozen backbone's
5
+ intermediate features via lightweight trained decoder heads.
6
+ """
7
+
8
+ import spaces
9
+ import torch
10
+ import torch.nn.functional as F
11
+ import numpy as np
12
+ import tempfile
13
+ import os
14
+ import sys
15
+ import yaml
16
+ from pathlib import Path
17
+ from PIL import Image
18
+ from torchvision import transforms
19
+
20
+ # Add local modules to path
21
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
22
+
23
+ # Download checkpoints before any model loading
24
+ from download_checkpoints import download_all
25
+ download_all()
26
+
27
+ from core import (
28
+ load_config, load_flux_pipeline, MultiTimestepFeatureCache,
29
+ resolve_c_dino, build_dino_extractor, build_hyperfeature_fusion,
30
+ calculate_distributed_concept_channels, distribute_concepts,
31
+ distribute_concepts_across_layers,
32
+ )
33
+ from flux_concept_attention import (
34
+ FluxWithConceptAttentionPipeline,
35
+ FluxTransformer2DModelWithConceptAttention,
36
+ )
37
+ from models.hyperfeature_fusion import create_hyperfeature_fusion
38
+ from models.dpt_segmentation_decoder import OriginalDPTSegmentationDecoder
39
+ from models.dpt_decoder import DPTHeadSpatial
40
+ from models.dpt_backbone import DPTRefineNetStack
41
+ from models.blocks import FeatureFusionBlock, _make_scratch, ResidualConvUnit
42
+ from models.segmentation_losses import CombinedSegmentationLoss
43
+ import torch.nn as nn
44
+
45
+ # ---- Model builders (mirroring scripts/inference.py + training scripts) ----
46
+
47
+ class ASPP(nn.Module):
48
+ def __init__(self, C_in, C_mid=256, rates=(1, 6, 12, 18), groups=32):
49
+ super().__init__()
50
+ def b(conv):
51
+ return nn.Sequential(conv, nn.GroupNorm(min(groups, C_mid), C_mid), nn.ReLU(True))
52
+ self.branches = nn.ModuleList([
53
+ b(nn.Conv2d(C_in, C_mid, 1, bias=False)),
54
+ b(nn.Conv2d(C_in, C_mid, 3, padding=rates[1], dilation=rates[1], bias=False)),
55
+ b(nn.Conv2d(C_in, C_mid, 3, padding=rates[2], dilation=rates[2], bias=False)),
56
+ b(nn.Conv2d(C_in, C_mid, 3, padding=rates[3], dilation=rates[3], bias=False)),
57
+ ])
58
+ self.img_pool = nn.Sequential(
59
+ nn.AdaptiveAvgPool2d(1),
60
+ nn.Conv2d(C_in, C_mid, 1, bias=False),
61
+ nn.GroupNorm(min(groups, C_mid), C_mid), nn.ReLU(True),
62
+ )
63
+ self.project = nn.Sequential(
64
+ nn.Conv2d(C_mid * 5, C_mid, 1, bias=False),
65
+ nn.GroupNorm(min(groups, C_mid), C_mid), nn.ReLU(True),
66
+ )
67
+
68
+ def forward(self, x):
69
+ H, W = x.shape[-2:]
70
+ feats = [b(x) for b in self.branches]
71
+ img = F.interpolate(self.img_pool(x), size=(H, W), mode='bilinear', align_corners=False)
72
+ return self.project(torch.cat(feats + [img], dim=1))
73
+
74
+
75
+ class DeepLabV3PlusHead(nn.Module):
76
+ def __init__(self, C_in=256, C_mid=256, num_classes=21, groups=32):
77
+ super().__init__()
78
+ self.aspp = ASPP(C_in, C_mid, groups=groups)
79
+ self.decode = nn.Sequential(
80
+ nn.Conv2d(C_mid, C_mid, 3, padding=1, bias=False),
81
+ nn.GroupNorm(min(groups, C_mid), C_mid), nn.ReLU(True),
82
+ nn.Conv2d(C_mid, num_classes, 1),
83
+ )
84
+ nn.init.constant_(self.decode[-1].bias, -0.5)
85
+
86
+ def forward(self, x, target_size):
87
+ y = self.decode(self.aspp(x))
88
+ return F.interpolate(y, size=target_size, mode='bilinear', align_corners=True)
89
+
90
+
91
+ def build_pascal_decoder(config, c_dino=768, dropout=0.0):
92
+ concepts = config['concepts'][config['training']['concept_config']]
93
+ base_channels = 3072
94
+ num_classes = config['data']['num_classes']
95
+ per_feature = calculate_distributed_concept_channels(len(concepts), 4)
96
+ concepts_per_layer = per_feature[0]
97
+ in_channels = base_channels + c_dino + concepts_per_layer
98
+
99
+ def path_block():
100
+ return nn.Sequential(
101
+ nn.Conv2d(in_channels, 256, 3, padding=1, bias=False),
102
+ nn.GroupNorm(32, 256), nn.ReLU(True), nn.Dropout2d(dropout))
103
+
104
+ decoder = nn.ModuleDict({f'path{i}': path_block() for i in range(1, 5)})
105
+ decoder['head'] = DeepLabV3PlusHead(C_in=256, C_mid=256, num_classes=num_classes, groups=32)
106
+ decoder['aux_head'] = nn.Sequential(nn.Dropout2d(dropout), nn.Conv2d(256, num_classes, 1))
107
+ decoder['reduce1024to256'] = nn.Sequential(
108
+ nn.Conv2d(1024, 256, 1, bias=False), nn.GroupNorm(32, 256), nn.ReLU(True), nn.Dropout2d(dropout))
109
+ return decoder
110
+
111
+
112
+ class FluxDinoPascalModel(nn.Module):
113
+ """Pascal VOC segmentation model (cache_only mode)."""
114
+ def __init__(self, decoder, config, cache_dir, num_timesteps=4, hidden_dim=768,
115
+ num_transformer_layers=3, layer_scale_init=1e-6, dino_model="dinov3_vitb16"):
116
+ super().__init__()
117
+ self.decoder = decoder
118
+ self.config = config
119
+ self.num_timesteps = num_timesteps
120
+ self.concepts = config['concepts'][config['training']['concept_config']]
121
+ self.cache = MultiTimestepFeatureCache(cache_dir)
122
+
123
+ self.hyperfeature_fusion = build_hyperfeature_fusion(
124
+ True, num_timesteps, hidden_dim, num_transformer_layers,
125
+ layer_scale_init, fusion_type="transformer", return_alpha=True)
126
+
127
+ self.dino_extractor = build_dino_extractor(dino_model, "full")
128
+ self.c_dino = resolve_c_dino("full", dino_model)
129
+ self.feature_mode = "full"
130
+ self.use_flux, self.use_dino = True, True
131
+
132
+ def forward(self, images, image_name, resolution, timestep_data):
133
+ device = next(self.parameters()).device
134
+ images = images.to(device)
135
+ height, width = resolution
136
+ patch_h, patch_w = height // 16, width // 16
137
+
138
+ dino_features = self.dino_extractor(images)
139
+
140
+ multi_timestep_features = {}
141
+ for timestep in timestep_data['timesteps']:
142
+ single_features = timestep_data['features'][timestep]['single_features']
143
+ multi_timestep_features[timestep] = [
144
+ f.float().permute(0, 2, 1).reshape(1, 3072, patch_h, patch_w) for f in single_features]
145
+
146
+ flux_features, alpha_layers = self.hyperfeature_fusion(multi_timestep_features)
147
+ del multi_timestep_features
148
+
149
+ concatenated_features = []
150
+ for layer_idx in range(4):
151
+ flux_feat = flux_features[layer_idx]
152
+ dino_feat = dino_features[layer_idx]
153
+ if flux_feat.shape[-2:] != dino_feat.shape[-2:]:
154
+ flux_feat = F.interpolate(flux_feat, size=dino_feat.shape[-2:], mode='bilinear', align_corners=False)
155
+ concatenated_features.append(torch.cat([flux_feat, dino_feat], dim=1))
156
+
157
+ last_timestep = timestep_data['timesteps'][-1]
158
+ concept_maps = timestep_data['concept_maps'][last_timestep]
159
+ distributed = distribute_concepts(concept_maps, len(concatenated_features), device)
160
+ del flux_features
161
+
162
+ distributed_resized = []
163
+ for i, dist in enumerate(distributed):
164
+ target_size = concatenated_features[i].shape[-2:]
165
+ if dist.shape[-2:] != target_size:
166
+ dist = F.interpolate(dist, size=target_size, mode='bilinear', align_corners=False)
167
+ distributed_resized.append(dist)
168
+ del distributed
169
+
170
+ fused_with_concepts = [
171
+ torch.cat([feat.to(device), distributed_resized[i].to(device)], dim=1)
172
+ for i, feat in enumerate(concatenated_features)]
173
+ del concatenated_features, distributed_resized
174
+
175
+ return self._decode(fused_with_concepts, height, width)
176
+
177
+ def _decode(self, feats, height, width):
178
+ path1 = self.decoder['path1'](feats[0])
179
+ path2 = self.decoder['path2'](feats[1])
180
+ path3 = self.decoder['path3'](feats[2])
181
+ path4 = self.decoder['path4'](feats[3])
182
+
183
+ target_h, target_w = path1.shape[-2:]
184
+ path2_up = F.interpolate(path2, size=(target_h, target_w), mode='bilinear', align_corners=False)
185
+ path3_up = F.interpolate(path3, size=(target_h, target_w), mode='bilinear', align_corners=False)
186
+ path4_up = F.interpolate(path4, size=(target_h, target_w), mode='bilinear', align_corners=False)
187
+
188
+ fused = self.decoder['reduce1024to256'](torch.cat([path1, path2_up, path3_up, path4_up], dim=1))
189
+ logits = self.decoder['head'](fused, (height, width))
190
+ return logits
191
+
192
+
193
+ def build_duts_decoder(config, c_dino=768):
194
+ concepts = config['concepts'][config['training']['concept_config']]
195
+ base_channels = 3072
196
+ per_feature = calculate_distributed_concept_channels(len(concepts), 4)
197
+ in_channels = [base_channels + c_dino + c for c in per_feature]
198
+ return OriginalDPTSegmentationDecoder(
199
+ in_channels=in_channels,
200
+ num_classes=config['data']['num_classes'],
201
+ features=config['model']['decoder']['features'],
202
+ target_size=None,
203
+ )
204
+
205
+
206
+ class FluxDinoDUTSModel(nn.Module):
207
+ """DUTS saliency model (cache_only mode)."""
208
+ def __init__(self, decoder, config, cache_dir, num_timesteps=4, hidden_dim=768,
209
+ num_transformer_layers=3, layer_scale_init=1e-6, dino_model="dinov3_vitb16"):
210
+ super().__init__()
211
+ self.decoder = decoder
212
+ self.config = config
213
+ self.num_timesteps = num_timesteps
214
+ self.concepts = config['concepts'][config['training']['concept_config']]
215
+ self.cache = MultiTimestepFeatureCache(cache_dir)
216
+
217
+ self.hyperfeature_fusion = build_hyperfeature_fusion(
218
+ True, num_timesteps, hidden_dim, num_transformer_layers,
219
+ layer_scale_init, fusion_type="transformer")
220
+
221
+ self.dino_extractor = build_dino_extractor(dino_model, "full")
222
+ self.c_dino = resolve_c_dino("full", dino_model)
223
+ self.feature_mode = "full"
224
+ self.use_flux, self.use_dino = True, True
225
+
226
+ def forward(self, images, image_name, resolution, timestep_data):
227
+ device = next(self.parameters()).device
228
+ images = images.to(device)
229
+ height, width = resolution
230
+ patch_h, patch_w = height // 16, width // 16
231
+
232
+ for timestep in timestep_data['features']:
233
+ for key in timestep_data['features'][timestep]:
234
+ val = timestep_data['features'][timestep][key]
235
+ if isinstance(val, list):
236
+ timestep_data['features'][timestep][key] = [
237
+ f.float() if isinstance(f, torch.Tensor) else f for f in val]
238
+ elif isinstance(val, torch.Tensor):
239
+ timestep_data['features'][timestep][key] = val.float()
240
+
241
+ dino_features = self.dino_extractor(images)
242
+
243
+ multi_timestep_features = {}
244
+ for timestep in timestep_data['timesteps']:
245
+ single_features = timestep_data['features'][timestep]['single_features']
246
+ multi_timestep_features[timestep] = [
247
+ f.permute(0, 2, 1).reshape(1, 3072, patch_h, patch_w) for f in single_features]
248
+
249
+ flux_features = self.hyperfeature_fusion(multi_timestep_features)
250
+ del multi_timestep_features
251
+
252
+ concatenated_features = []
253
+ for layer_idx in range(4):
254
+ flux_feat = flux_features[layer_idx]
255
+ dino_feat = dino_features[layer_idx]
256
+ if flux_feat.shape[-2:] != dino_feat.shape[-2:]:
257
+ flux_feat = F.interpolate(flux_feat, size=dino_feat.shape[-2:], mode='bilinear', align_corners=False)
258
+ concatenated_features.append(torch.cat([flux_feat, dino_feat], dim=1))
259
+
260
+ last_timestep = timestep_data['timesteps'][-1]
261
+ concept_maps = timestep_data['concept_maps'][last_timestep]
262
+ distributed = distribute_concepts(concept_maps, len(concatenated_features), device)
263
+ del flux_features
264
+
265
+ distributed_resized = []
266
+ for i, dist in enumerate(distributed):
267
+ target_size = concatenated_features[i].shape[-2:]
268
+ if dist.shape[-2:] != target_size:
269
+ dist = F.interpolate(dist, size=target_size, mode='bilinear', align_corners=False)
270
+ distributed_resized.append(dist)
271
+ del distributed
272
+
273
+ fused_with_concepts = [
274
+ torch.cat([feat.to(device), distributed_resized[i].to(device)], dim=1)
275
+ for i, feat in enumerate(concatenated_features)]
276
+ del concatenated_features, distributed_resized
277
+
278
+ logits = self.decoder(fused_with_concepts)
279
+ if logits.shape[-2:] != (height, width):
280
+ logits = F.interpolate(logits, size=(height, width), mode='bilinear', align_corners=False)
281
+ return logits
282
+
283
+
284
+ def build_nyu_decoder(config, c_dino=768, high_res=False):
285
+ concepts = config['concepts'][config['training']['concept_config']]
286
+ num_concepts = len(concepts)
287
+ per_layer = num_concepts // 4
288
+ remainder = num_concepts % 4
289
+ in_channels = []
290
+ for i in range(4):
291
+ count = per_layer + (1 if i < remainder else 0)
292
+ in_channels.append(3072 + c_dino + count)
293
+ return DPTHeadSpatial(in_channels=in_channels, features=256, num_classes=1, use_bn=False)
294
+
295
+
296
+ class FluxNYUDepthModel(nn.Module):
297
+ """NYU Depth model (cache_only mode)."""
298
+ def __init__(self, decoder, config, cache_dir, num_timesteps=4, hidden_dim=768,
299
+ num_transformer_layers=3, layer_scale_init=1e-4, dino_model="dinov3_vitb16",
300
+ high_res_decoder=False):
301
+ super().__init__()
302
+ self.decoder = decoder
303
+ self.config = config
304
+ self.num_timesteps = num_timesteps
305
+ self.concepts = config['concepts'][config['training']['concept_config']]
306
+ self.cache = MultiTimestepFeatureCache(cache_dir)
307
+ self.high_res_decoder = high_res_decoder
308
+
309
+ self.hyperfeature_fusion = build_hyperfeature_fusion(
310
+ True, num_timesteps, hidden_dim, num_transformer_layers,
311
+ layer_scale_init, fusion_type="transformer", return_alpha=False)
312
+
313
+ self.dino_extractor = build_dino_extractor(dino_model, "full")
314
+ self.c_dino = resolve_c_dino("full", dino_model)
315
+ self.feature_mode = "full"
316
+ self.use_flux, self.use_dino = True, True
317
+
318
+ def forward(self, images, image_name, resolution, timestep_data):
319
+ device = next(self.parameters()).device
320
+ images = images.to(device)
321
+
322
+ res = timestep_data.get('resolution')
323
+ if res is not None:
324
+ native_h, native_w = res
325
+ else:
326
+ native_h, native_w = timestep_data.get('native_h', 896), timestep_data.get('native_w', 1152)
327
+ patch_h, patch_w = native_h // 16, native_w // 16
328
+
329
+ dino_features = self.dino_extractor(images)
330
+
331
+ multi_timestep_features = {}
332
+ for timestep in timestep_data['timesteps']:
333
+ single_features = timestep_data['features'][timestep]['single_features']
334
+ multi_timestep_features[timestep] = [
335
+ f.float().permute(0, 2, 1).reshape(1, 3072, patch_h, patch_w) for f in single_features]
336
+
337
+ fused_flux_features = self.hyperfeature_fusion(multi_timestep_features)
338
+
339
+ concept_maps_avg = {}
340
+ for concept in self.concepts:
341
+ concept_stack = [timestep_data['concept_maps'][t][concept]
342
+ for t in timestep_data['timesteps']
343
+ if concept in timestep_data['concept_maps'][t]]
344
+ if concept_stack:
345
+ concept_maps_avg[concept] = torch.stack([
346
+ c.to(device) if hasattr(c, "to") else torch.tensor(c, device=device)
347
+ for c in concept_stack]).mean(dim=0).float()
348
+
349
+ target_size = dino_features[0].shape[-2:] if self.use_dino else fused_flux_features[0].shape[-2:]
350
+ distributed_concepts = distribute_concepts_across_layers(
351
+ concept_maps_avg, num_layers=4, target_size=target_size, device=device)
352
+
353
+ final_features = []
354
+ for layer_idx in range(4):
355
+ flux_feat = fused_flux_features[layer_idx]
356
+ concept_feat = distributed_concepts[layer_idx]
357
+ dino_feat = dino_features[layer_idx]
358
+ layer_size = dino_feat.shape[-2:]
359
+ if flux_feat.shape[-2:] != layer_size:
360
+ flux_feat = F.interpolate(flux_feat, size=layer_size, mode='bilinear', align_corners=False)
361
+ if concept_feat.shape[-2:] != layer_size:
362
+ concept_feat = F.interpolate(concept_feat, size=layer_size, mode='bilinear', align_corners=False)
363
+ final_features.append(torch.cat([flux_feat, dino_feat, concept_feat], dim=1))
364
+
365
+ return self.decoder(final_features)
366
+
367
+
368
+ # ---- Checkpoint loading ----
369
+
370
+ def load_checkpoint(model, checkpoint_path):
371
+ ckpt = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
372
+ state_dict = ckpt.get("state_dict", ckpt)
373
+ missing, unexpected = model.load_state_dict(state_dict, strict=False)
374
+ relevant_missing = [k for k in missing if k.startswith(("hyperfeature_fusion", "decoder"))]
375
+ if relevant_missing:
376
+ print(f"[WARN] {len(relevant_missing)} fusion/decoder keys NOT found in checkpoint")
377
+ print(f"[CKPT] Loaded {checkpoint_path} (missing={len(missing)}, unexpected={len(unexpected)})")
378
+
379
+
380
+ # ---- Generation with feature capture (from generate.py) ----
381
+
382
+ def generate_and_capture(pipeline, prompt, concepts, height, width, steps, guidance,
383
+ seed, device, concept_attention_kwargs, num_timesteps, group_size=7):
384
+ transformer = pipeline.transformer
385
+ target_steps = {steps + (-(i * group_size + 1)) for i in range(num_timesteps)
386
+ if i * group_size < steps}
387
+ captured = {}
388
+
389
+ def _capture(pipe, step_idx, t, callback_kwargs):
390
+ if step_idx in target_steps:
391
+ tv = int(t.item()) if hasattr(t, "item") else int(t)
392
+ _, single_features = transformer.get_features()
393
+ captured[tv] = [f.detach().cpu().clone() for f in single_features]
394
+ return callback_kwargs
395
+
396
+ transformer.stored_features.clear()
397
+ with torch.no_grad():
398
+ result = pipeline(
399
+ prompt=prompt,
400
+ height=height,
401
+ width=width,
402
+ num_inference_steps=steps,
403
+ guidance_scale=guidance,
404
+ generator=torch.Generator(device).manual_seed(seed),
405
+ concept_attention_kwargs=concept_attention_kwargs,
406
+ output_type="pil",
407
+ callback_on_step_end=_capture,
408
+ )
409
+
410
+ raw = result.concept_attention_maps
411
+ if not raw:
412
+ raise RuntimeError("Pipeline returned no concept-attention maps")
413
+ maps_list = raw[0] if (len(raw) == 1 and isinstance(raw[0], list)) else raw
414
+ if len(maps_list) != len(concepts):
415
+ raise ValueError(f"{len(concepts)} concepts vs {len(maps_list)} concept maps")
416
+ concept_maps = {c: maps_list[i] for i, c in enumerate(concepts)}
417
+
418
+ return result.images[0], captured, concept_maps
419
+
420
+
421
+ def build_timestep_data(captured, concept_maps, concepts, height, width, prompt, stem):
422
+ timesteps = sorted(captured.keys(), reverse=True)
423
+ cmaps = {c: (m.cpu() if hasattr(m, "cpu") else m) for c, m in concept_maps.items()}
424
+ data = {
425
+ "timesteps": timesteps,
426
+ "features": {t: {"single_features": captured[t]} for t in timesteps},
427
+ "concept_maps": {t: dict(cmaps) for t in timesteps},
428
+ "image_name": stem,
429
+ "concepts": concepts,
430
+ "resolution": (height, width),
431
+ "prompt": prompt,
432
+ "native_h": height,
433
+ "native_w": width,
434
+ }
435
+ return data
436
+
437
+
438
+ # ---- Visualization helpers ----
439
+
440
+ VOC_PALETTE = None
441
+
442
+ def voc_color_palette():
443
+ global VOC_PALETTE
444
+ if VOC_PALETTE is not None:
445
+ return VOC_PALETTE
446
+ palette = [0] * (256 * 3)
447
+ for i in range(256):
448
+ r = g = b = 0
449
+ c = i
450
+ for j in range(8):
451
+ r |= ((c >> 0) & 1) << (7 - j)
452
+ g |= ((c >> 1) & 1) << (7 - j)
453
+ b |= ((c >> 2) & 1) << (7 - j)
454
+ c >>= 3
455
+ palette[i * 3 + 0] = r
456
+ palette[i * 3 + 1] = g
457
+ palette[i * 3 + 2] = b
458
+ VOC_PALETTE = palette
459
+ return palette
460
+
461
+
462
+ def colorize_depth(depth, cmap="magma"):
463
+ import matplotlib
464
+ d = depth.astype(np.float32)
465
+ lo, hi = np.percentile(d, 2), np.percentile(d, 98)
466
+ d = np.clip((d - lo) / (hi - lo + 1e-8), 0, 1)
467
+ colormap = matplotlib.colormaps[cmap]
468
+ rgb = (colormap(d)[:, :, :3] * 255).astype(np.uint8)
469
+ return rgb
470
+
471
+
472
+ def colorize_saliency(prob):
473
+ prob_norm = (prob - prob.min()) / (prob.max() - prob.min() + 1e-8)
474
+ rgb = (plt_colormap(prob_norm)[:, :, :3] * 255).astype(np.uint8)
475
+ return rgb
476
+
477
+
478
+ def plt_colormap(x, cmap_name="inferno"):
479
+ import matplotlib
480
+ colormap = matplotlib.colormaps[cmap_name]
481
+ return colormap(x)
482
+
483
+
484
+ # ---- Config loading ----
485
+
486
+ def make_config(task):
487
+ """Load the task config with env vars expanded."""
488
+ if task == "pascal":
489
+ config_path = os.path.join(os.path.dirname(__file__), "configs", "pascal_voc_config.yaml")
490
+ elif task == "nyu":
491
+ config_path = os.path.join(os.path.dirname(__file__), "configs", "nyu_depth_config.yaml")
492
+ else:
493
+ config_path = os.path.join(os.path.dirname(__file__), "configs", f"{task}_config.yaml")
494
+ with open(config_path, "r") as f:
495
+ def _expand_env(value):
496
+ if isinstance(value, str):
497
+ return os.path.expanduser(os.path.expandvars(value))
498
+ if isinstance(value, dict):
499
+ return {k: _expand_env(v) for k, v in value.items()}
500
+ if isinstance(value, list):
501
+ return [_expand_env(v) for v in value]
502
+ return value
503
+ config = _expand_env(yaml.safe_load(f))
504
+ # Set dummy paths since we use a temp dir
505
+ config['paths'] = config.get('paths', {})
506
+ config['paths']['permanent_cache_dir'] = '/tmp/mmdiff_cache'
507
+ # Fix data_root if it's an unexpanded env var path
508
+ if 'data' in config and 'data_root' in config['data']:
509
+ if config['data']['data_root'].startswith('$') or '/path/' in config['data']['data_root']:
510
+ config['data']['data_root'] = '/tmp/dummy_data'
511
+ return config
512
+
513
+
514
+ # ---- Global model loading at module scope ----
515
+
516
+ print("[SETUP] Loading configs...")
517
+ configs = {
518
+ 'duts': make_config('duts'),
519
+ 'pascal': make_config('pascal_voc'),
520
+ 'nyu': make_config('nyu_depth'),
521
+ }
522
+
523
+ print("[SETUP] Loading FLUX.1-dev pipeline with concept attention...")
524
+ flux_model = "black-forest-labs/FLUX.1-dev"
525
+ transformer = FluxTransformer2DModelWithConceptAttention.from_pretrained(
526
+ flux_model, subfolder="transformer", torch_dtype=torch.float16
527
+ )
528
+ pipeline = FluxWithConceptAttentionPipeline.from_pretrained(
529
+ flux_model, transformer=transformer, torch_dtype=torch.float16
530
+ ).to("cuda")
531
+ pipeline.set_progress_bar_config(disable=True)
532
+ print("[SETUP] FLUX pipeline loaded.")
533
+
534
+ # Build decoder models
535
+ device = "cuda"
536
+ cache_dir = "/tmp/mmdiff_cache"
537
+ os.makedirs(cache_dir, exist_ok=True)
538
+
539
+ # Common architecture params (from configs)
540
+ num_timesteps = 4
541
+ hidden_dim = 768
542
+ num_transformer_layers = 3
543
+ layer_scale_init = 1e-6
544
+ dino_model_name = "dinov3_vitb16"
545
+ c_dino = resolve_c_dino("full", dino_model_name)
546
+
547
+ # Build DUTS saliency model
548
+ print("[SETUP] Building DUTS saliency model...")
549
+ duts_decoder = build_duts_decoder(configs['duts'], c_dino=c_dino)
550
+ duts_model = FluxDinoDUTSModel(
551
+ duts_decoder, configs['duts'], cache_dir,
552
+ num_timesteps=num_timesteps, hidden_dim=hidden_dim,
553
+ num_transformer_layers=num_transformer_layers, layer_scale_init=layer_scale_init,
554
+ dino_model=dino_model_name)
555
+ duts_ckpt = "/tmp/checkpoints/duts_saliency.ckpt"
556
+ load_checkpoint(duts_model, duts_ckpt)
557
+ duts_model = duts_model.to(device).eval()
558
+
559
+ # Build Pascal VOC model
560
+ print("[SETUP] Building Pascal VOC segmentation model...")
561
+ pascal_layer_scale_init = 1e-6
562
+ pascal_decoder = build_pascal_decoder(configs['pascal'], c_dino=c_dino, dropout=0.0)
563
+ pascal_model = FluxDinoPascalModel(
564
+ pascal_decoder, configs['pascal'], cache_dir,
565
+ num_timesteps=num_timesteps, hidden_dim=hidden_dim,
566
+ num_transformer_layers=num_transformer_layers, layer_scale_init=pascal_layer_scale_init,
567
+ dino_model=dino_model_name)
568
+ pascal_ckpt = "/tmp/checkpoints/pascal_segmentation.ckpt"
569
+ load_checkpoint(pascal_model, pascal_ckpt)
570
+ pascal_model = pascal_model.to(device).eval()
571
+
572
+ # Build NYU Depth model
573
+ print("[SETUP] Building NYU Depth model...")
574
+ nyu_layer_scale_init = 1e-4
575
+ nyu_decoder = build_nyu_decoder(configs['nyu'], c_dino=c_dino, high_res=False)
576
+ nyu_model = FluxNYUDepthModel(
577
+ nyu_decoder, configs['nyu'], cache_dir,
578
+ num_timesteps=num_timesteps, hidden_dim=hidden_dim,
579
+ num_transformer_layers=num_transformer_layers, layer_scale_init=nyu_layer_scale_init,
580
+ dino_model=dino_model_name, high_res_decoder=False)
581
+ nyu_ckpt = "/tmp/checkpoints/nyu_depth.ckpt"
582
+ load_checkpoint(nyu_model, nyu_ckpt)
583
+ nyu_model = nyu_model.to(device).eval()
584
+
585
+ print("[SETUP] All models loaded successfully!")
586
+
587
+
588
+ # ---- Inference function ----
589
+
590
+ @spaces.GPU(duration=120)
591
+ def generate(prompt, task_choice, seed, num_steps, guidance_scale):
592
+ """Generate an image and dense prediction(s) from a text prompt."""
593
+ height, width = 512, 512
594
+
595
+ # Select config and concepts based on task
596
+ if task_choice == "Saliency (DUTS)":
597
+ task = "duts"
598
+ config = configs['duts']
599
+ elif task_choice == "Segmentation (Pascal VOC)":
600
+ task = "pascal"
601
+ config = configs['pascal']
602
+ elif task_choice == "Depth (NYU)":
603
+ task = "nyu"
604
+ config = configs['nyu']
605
+ else: # All
606
+ task = "all"
607
+ config = configs['duts'] # Use DUTS concepts for generation
608
+
609
+ concepts = config['concepts'][config['training']['concept_config']]
610
+ concept_attention_kwargs = {
611
+ "concepts": concepts,
612
+ "timesteps": config["flux"]["concept_timesteps"],
613
+ "layers": config["flux"]["concept_layers"],
614
+ }
615
+
616
+ # Generate image and capture features
617
+ with tempfile.TemporaryDirectory(prefix="mmdiff_demo_") as tmp_cache:
618
+ stem = "demo_image"
619
+
620
+ if task == "all":
621
+ # For "all" mode, we generate once and use each task's own concepts for decoding
622
+ # First generate with DUTS concepts for the image
623
+ pil_image, captured, concept_maps = generate_and_capture(
624
+ pipeline, prompt, concepts, height, width,
625
+ num_steps, guidance_scale, seed, device,
626
+ concept_attention_kwargs, num_timesteps)
627
+
628
+ # Build timestep data for DUTS
629
+ timestep_data = build_timestep_data(
630
+ captured, concept_maps, concepts, height, width, prompt, stem)
631
+
632
+ image_tensor = transforms.ToTensor()(pil_image).unsqueeze(0).to(device)
633
+
634
+ results = {"image": pil_image}
635
+
636
+ # DUTS saliency
637
+ duts_concepts = configs['duts']['concepts'][configs['duts']['training']['concept_config']]
638
+ duts_cakw = {
639
+ "concepts": duts_concepts,
640
+ "timesteps": configs['duts']["flux"]["concept_timesteps"],
641
+ "layers": configs['duts']["flux"]["concept_layers"],
642
+ }
643
+ # Re-capture with DUTS concepts for proper saliency
644
+ _, duts_captured, duts_concept_maps = generate_and_capture(
645
+ pipeline, prompt, duts_concepts, height, width,
646
+ num_steps, guidance_scale, seed, device,
647
+ duts_cakw, num_timesteps)
648
+ duts_td = build_timestep_data(duts_captured, duts_concept_maps, duts_concepts, height, width, prompt, stem)
649
+ with torch.no_grad():
650
+ logits = duts_model(image_tensor, stem, (height, width), duts_td)
651
+ prob = torch.sigmoid(logits.squeeze(1))[0].float().cpu().numpy()
652
+ sal_vis = colorize_saliency(prob)
653
+ results["saliancy"] = Image.fromarray(sal_vis)
654
+
655
+ # Pascal segmentation
656
+ pascal_concepts = configs['pascal']['concepts'][configs['pascal']['training']['concept_config']]
657
+ pascal_cakw = {
658
+ "concepts": pascal_concepts,
659
+ "timesteps": configs['pascal']["flux"]["concept_timesteps"],
660
+ "layers": configs['pascal']["flux"]["concept_layers"],
661
+ }
662
+ _, pascal_captured, pascal_concept_maps = generate_and_capture(
663
+ pipeline, prompt, pascal_concepts, height, width,
664
+ num_steps, guidance_scale, seed, device,
665
+ pascal_cakw, num_timesteps)
666
+ pascal_td = build_timestep_data(pascal_captured, pascal_concept_maps, pascal_concepts, height, width, prompt, stem)
667
+ with torch.no_grad():
668
+ logits = pascal_model(image_tensor, stem, (height, width), pascal_td)
669
+ pred = torch.argmax(logits, dim=1)[0].byte().cpu().numpy()
670
+ seg_img = Image.fromarray(pred, mode="P")
671
+ seg_img.putpalette(voc_color_palette())
672
+ seg_rgb = seg_img.convert("RGB")
673
+ results["segmentation"] = seg_rgb
674
+
675
+ # NYU depth
676
+ nyu_concepts = configs['nyu']['concepts'][configs['nyu']['training']['concept_config']]
677
+ nyu_cakw = {
678
+ "concepts": nyu_concepts,
679
+ "timesteps": configs['nyu']["flux"]["concept_timesteps"],
680
+ "layers": configs['nyu']["flux"]["concept_layers"],
681
+ }
682
+ _, nyu_captured, nyu_concept_maps = generate_and_capture(
683
+ pipeline, prompt, nyu_concepts, height, width,
684
+ num_steps, guidance_scale, seed, device,
685
+ nyu_cakw, num_timesteps)
686
+ nyu_td = build_timestep_data(nyu_captured, nyu_concept_maps, nyu_concepts, height, width, prompt, stem)
687
+ with torch.no_grad():
688
+ depth = nyu_model(image_tensor, stem, (height, width), nyu_td)
689
+ depth = F.softplus(depth).squeeze().float().cpu().numpy()
690
+ depth_vis = colorize_depth(depth)
691
+ results["depth"] = Image.fromarray(depth_vis)
692
+
693
+ return results
694
+ else:
695
+ # Single task
696
+ pil_image, captured, concept_maps = generate_and_capture(
697
+ pipeline, prompt, concepts, height, width,
698
+ num_steps, guidance_scale, seed, device,
699
+ concept_attention_kwargs, num_timesteps)
700
+
701
+ timestep_data = build_timestep_data(
702
+ captured, concept_maps, concepts, height, width, prompt, stem)
703
+
704
+ image_tensor = transforms.ToTensor()(pil_image).unsqueeze(0).to(device)
705
+
706
+ if task == "duts":
707
+ with torch.no_grad():
708
+ logits = duts_model(image_tensor, stem, (height, width), timestep_data)
709
+ prob = torch.sigmoid(logits.squeeze(1))[0].float().cpu().numpy()
710
+ sal_vis = colorize_saliency(prob)
711
+ return {"image": pil_image, "saliancy": Image.fromarray(sal_vis)}
712
+
713
+ elif task == "pascal":
714
+ with torch.no_grad():
715
+ logits = pascal_model(image_tensor, stem, (height, width), timestep_data)
716
+ pred = torch.argmax(logits, dim=1)[0].byte().cpu().numpy()
717
+ seg_img = Image.fromarray(pred, mode="P")
718
+ seg_img.putpalette(voc_color_palette())
719
+ seg_rgb = seg_img.convert("RGB")
720
+ return {"image": pil_image, "segmentation": seg_rgb}
721
+
722
+ elif task == "nyu":
723
+ with torch.no_grad():
724
+ depth = nyu_model(image_tensor, stem, (height, width), timestep_data)
725
+ depth = F.softplus(depth).squeeze().float().cpu().numpy()
726
+ depth_vis = colorize_depth(depth)
727
+ return {"image": pil_image, "depth": Image.fromarray(depth_vis)}
728
+
729
+
730
+ # ---- Gradio UI ----
731
+
732
+ import gradio as gr
733
+
734
+ DESCRIPTION = """# MMDiff: Extending Diffusion Transformers for Multi-Modal Generation
735
+
736
+ Generate an image from a text prompt using FLUX.1-dev while simultaneously producing
737
+ dense predictions (saliency maps, segmentation maps, depth maps) from the frozen
738
+ diffusion transformer's intermediate features via lightweight trained decoder heads.
739
+
740
+ **Paper**: [MMDiff: Extending Diffusion Transformers for Multi-Modal Generation](https://huggingface.co/papers/2606.16673)
741
+ **Model**: [yagmurakarken/mmdiff](https://huggingface.co/yagmurakarken/mmdiff)
742
+ """
743
+
744
+ with gr.Blocks(theme=gr.themes.Citrus()) as demo:
745
+ gr.Markdown(DESCRIPTION)
746
+
747
+ with gr.Row():
748
+ with gr.Column(scale=1):
749
+ prompt_input = gr.Textbox(
750
+ label="Text Prompt",
751
+ placeholder="A cat sitting on a wooden table...",
752
+ value="A cat sitting on a wooden table next to a window",
753
+ lines=2,
754
+ )
755
+ task_select = gr.Radio(
756
+ choices=["Saliency (DUTS)", "Segmentation (Pascal VOC)", "Depth (NYU)", "All (Saliency + Segmentation + Depth)"],
757
+ label="Task",
758
+ value="Saliency (DUTS)",
759
+ )
760
+ generate_btn = gr.Button("Generate", variant="primary", size="lg")
761
+
762
+ with gr.Accordion("Advanced Options", open=False):
763
+ seed_input = gr.Slider(0, 1000, value=0, step=1, label="Seed")
764
+ steps_input = gr.Slider(4, 50, value=28, step=1, label="Inference Steps")
765
+ guidance_input = gr.Slider(1.0, 10.0, value=3.5, step=0.5, label="Guidance Scale")
766
+
767
+ with gr.Column(scale=2):
768
+ # Output gallery - dynamically shown based on task
769
+ with gr.Row():
770
+ image_output = gr.Image(label="Generated Image", type="pil", height=300)
771
+ with gr.Row():
772
+ saliency_output = gr.Image(label="Saliency Map", type="pil", height=300, visible=False)
773
+ segmentation_output = gr.Image(label="Segmentation Map", type="pil", height=300, visible=False)
774
+ depth_output = gr.Image(label="Depth Map", type="pil", height=300, visible=False)
775
+
776
+ # Examples
777
+ gr.Examples(
778
+ examples=[
779
+ ["A cat sitting on a wooden table next to a window", "Saliency (DUTS)", 0, 28, 3.5],
780
+ ["A person riding a bicycle on a city street", "Segmentation (Pascal VOC)", 42, 28, 3.5],
781
+ ["A modern living room with a sofa and coffee table", "Depth (NYU)", 0, 28, 3.5],
782
+ ["A dog playing in a grassy park", "All (Saliency + Segmentation + Depth)", 0, 28, 3.5],
783
+ ],
784
+ inputs=[prompt_input, task_select, seed_input, steps_input, guidance_input],
785
+ outputs=[image_output, saliency_output, segmentation_output, depth_output],
786
+ fn=generate,
787
+ cache_examples=False,
788
+ run_on_click=True,
789
+ )
790
+
791
+ def update_visibility(task_choice):
792
+ """Show/hide output columns based on task."""
793
+ if "All" in task_choice:
794
+ return {
795
+ saliency_output: gr.update(visible=True),
796
+ segmentation_output: gr.update(visible=True),
797
+ depth_output: gr.update(visible=True),
798
+ }
799
+ elif "Saliency" in task_choice:
800
+ return {
801
+ saliency_output: gr.update(visible=True),
802
+ segmentation_output: gr.update(visible=False),
803
+ depth_output: gr.update(visible=False),
804
+ }
805
+ elif "Segmentation" in task_choice:
806
+ return {
807
+ saliency_output: gr.update(visible=False),
808
+ segmentation_output: gr.update(visible=True),
809
+ depth_output: gr.update(visible=False),
810
+ }
811
+ elif "Depth" in task_choice:
812
+ return {
813
+ saliency_output: gr.update(visible=False),
814
+ segmentation_output: gr.update(visible=False),
815
+ depth_output: gr.update(visible=True),
816
+ }
817
+ return {}
818
+
819
+ task_select.change(
820
+ fn=update_visibility,
821
+ inputs=[task_select],
822
+ outputs=[saliency_output, segmentation_output, depth_output],
823
+ )
824
+
825
+ def run_and_route(prompt, task_choice, seed, steps, guidance):
826
+ """Run generation and route outputs to the right components."""
827
+ results = generate(prompt, task_choice, seed, steps, guidance)
828
+ # Return None for hidden outputs
829
+ img = results.get("image")
830
+ sal = results.get("saliancy")
831
+ seg = results.get("segmentation")
832
+ dep = results.get("depth")
833
+ return img, sal, seg, dep
834
+
835
+ generate_btn.click(
836
+ fn=run_and_route,
837
+ inputs=[prompt_input, task_select, seed_input, steps_input, guidance_input],
838
+ outputs=[image_output, saliency_output, segmentation_output, depth_output],
839
+ )
840
+
841
+ if __name__ == "__main__":
842
+ demo.launch()
configs/duts_config.yaml ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DUTS Training Configuration - Native Resolution (Variable Resolution with FluxResizer)
2
+ # Uses FLUX.1-dev and FluxResizer for optimal native resolutions (no padding)
3
+
4
+ # Experiment settings
5
+ experiment:
6
+ name: "duts_native_resolution"
7
+ description: "DUTS with FLUX.1-dev at native resolutions using FluxResizer"
8
+
9
+ # Concept configurations (same as original)
10
+ concepts:
11
+ basic: ["object", "background", "detail", "edges"]
12
+ expanded: ["background", "object", "edges", "salient", "contour"]
13
+ meta: ["living", "vehicle", "furniture", "object", "background"]
14
+ combined: ["object", "background", "living", "vehicle", "furniture", "detail", "edges"]
15
+ combined_expanded: ["object", "background", "living", "vehicle", "furniture", "detail", "edges", "salient", "contour"]
16
+ with_classes: ["object", "background", "detail", "edges", "airplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "table", "dog", "horse", "motorbike", "person", "plant", "sheep", "sofa", "train", "television"]
17
+ all_comprehensive: ["living", "vehicle", "furniture", "object", "background", "detail", "edges", "airplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "table", "dog", "horse", "motorbike", "person", "plant", "sheep", "sofa", "train", "television"]
18
+
19
+ # Training settings
20
+ training:
21
+ concept_config: "expanded" # Which concept configuration to use
22
+ epochs: 100 # Match baseline training duration (was 20)
23
+ learning_rate: 1e-5 # Reduced from 5e-5 for stability (gradient explosion fix)
24
+ batch_size: 1 # REQUIRED: Must be 1 for variable resolution
25
+ accumulate_grad_batches: 4 # Effective batch size = 4
26
+ gradient_clip_val: 0.1 # Very aggressive clipping for gradient explosion (was 0.5)
27
+ precision: "32-true" # FP32 for maximum stability (gradient explosion with FP16)
28
+
29
+ # Data settings
30
+ data:
31
+ dataset: "duts"
32
+ num_classes: 1
33
+ # Root of the DUTS dataset. Expected layout:
34
+ # <data_root>/DUTS-TR/DUTS-TR-Image, <data_root>/DUTS-TR/DUTS-TR-Mask
35
+ # <data_root>/DUTS-TE/DUTS-TE-Image, <data_root>/DUTS-TE/DUTS-TE-Mask
36
+ data_root: "${DUTS_ROOT}" # env var; e.g. export DUTS_ROOT=/datasets/.../DUTS
37
+ # NOTE: No target_size! FluxResizer selects optimal resolution per image
38
+
39
+ # Model settings
40
+ model:
41
+ flux_model: "black-forest-labs/FLUX.1-dev" # Changed from schnell to dev
42
+ dtype: "float32" # Match training precision (was float16)
43
+ dino_model: "dinov3_vitb16" # DINOv3 base model
44
+ feature_locations:
45
+ transformer_blocks: [4, 9, 13, 18]
46
+ single_transformer_blocks: [4, 15, 26, 37]
47
+ decoder:
48
+ features: 256
49
+ hyperfeature_fusion:
50
+ num_timesteps: 4
51
+ fusion_type: "transformer"
52
+ hidden_dim: 768
53
+ num_transformer_layers: 3
54
+ layer_scale_init: 1e-6
55
+
56
+ # FLUX settings
57
+ flux:
58
+ timesteps: 28
59
+ guidance_scale: 3.5
60
+ num_inference_steps: 1
61
+ concept_timesteps: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]
62
+ concept_layers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
63
+
64
+ # FluxResizer settings
65
+ flux_resizer:
66
+ # Optimal resolutions (all divisible by 32 for FLUX/DINO compatibility)
67
+ # FluxResizer will automatically select the closest resolution based on aspect ratio
68
+ optimal_resolutions:
69
+ - [1024, 1024] # 1:1
70
+ - [896, 1152] # ~0.78:1
71
+ - [1152, 896] # ~1.29:1
72
+ - [768, 1344] # ~0.57:1
73
+ - [1344, 768] # ~1.75:1
74
+ - [832, 1216] # ~0.68:1
75
+ - [1216, 832] # ~1.46:1
76
+ - [704, 1408] # 0.5:1
77
+ - [1408, 704] # 2:1
78
+ - [960, 1088] # ~0.88:1
79
+ - [1088, 960] # ~1.13:1
80
+
81
+ # Hardware settings
82
+ hardware:
83
+ devices: 1 # Use 2 GPUs for faster training
84
+ #strategy: "ddp_find_unused_parameters_true" # Multi-GPU strategy
85
+ num_sanity_val_steps: 1
86
+ detect_anomaly: true
87
+ accelerator: "gpu"
88
+
89
+ # Paths (timestamps will be automatically added)
90
+ paths:
91
+ # All outputs live under ${MMDIFF_OUTPUT} (set it to a writable dir, e.g. /work/<user>/mmdiff_out).
92
+ cache_base_dir: "${MMDIFF_OUTPUT}/cache"
93
+ log_base_dir: "${MMDIFF_OUTPUT}/logs"
94
+ checkpoint_base_dir: "${MMDIFF_OUTPUT}/checkpoints"
95
+ use_timestamp: true # Add timestamp to folder names
96
+ permanent_cache_dir: "${MMDIFF_OUTPUT}/cache/duts_flux_cache" # Native resolution feature cache
97
+
98
+ # Logging
99
+ logging:
100
+ log_every_n_steps: 10
101
+ save_top_k: 3
102
+ monitor: "val_loss"
103
+ mode: "min"
104
+
105
+ # HuggingFace
106
+ huggingface:
107
+ token: "" # Leave empty and authenticate via `huggingface-cli login` or the HF_TOKEN env var
configs/nyu_depth_config.yaml ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # NYU Depth Training Configuration - Native Resolution (Variable Resolution with FluxResizer)
2
+ # Uses FLUX.1-dev and FluxResizer for optimal native resolutions (no padding)
3
+
4
+ # Experiment settings
5
+ experiment:
6
+ name: "nyu_depth_native_resolution"
7
+ description: "NYU Depth with FLUX.1-dev at native resolutions using FluxResizer"
8
+
9
+ # Concept configurations (same as original)
10
+ concepts:
11
+ basic: ["depth", "surface", "object", "background"]
12
+ basic_2: ["depth", "surface", "object", "near", "far"]
13
+ indoor: ["wall", "floor", "furniture", "object", "depth"]
14
+ spatial: ["near", "far", "depth", "distance", "surface"]
15
+ detailed: ["wall", "floor", "ceiling", "furniture", "object", "depth", "surface"]
16
+ comprehensive: ["wall", "floor", "ceiling", "furniture", "object", "depth", "near", "far", "surface", "indoor", "outdoor", "structure"]
17
+ semantic: ["bedroom", "kitchen", "bathroom", "living_room", "depth", "furniture", "wall", "floor"]
18
+ geometric: ["depth", "surface", "edge", "plane", "corner", "boundary", "gradient", "distance"]
19
+
20
+ # Training settings
21
+ training:
22
+ concept_config: "basic_2" # Which concept configuration to use
23
+ epochs: 20
24
+ learning_rate: 1e-4
25
+ batch_size: 1 # REQUIRED: Must be 1 for variable resolution
26
+ accumulate_grad_batches: 4 # Effective batch size = 4
27
+ gradient_clip_val: 1.0
28
+ precision: "16-mixed"
29
+
30
+ # Data settings
31
+ data:
32
+ dataset: "nyu_depth_v2"
33
+ # NYU Depth V2 standard train/test split. Point the trainer at it via CLI args
34
+ # (--data_path / --filenames_path); see shell_scripts/train_nyu.sh.
35
+ num_classes: 1 # Depth is regression, but num_classes used for decoder
36
+ min_depth: 0.1 # Minimum valid depth (meters)
37
+ max_depth: 10.0 # Maximum valid depth (meters)
38
+ # NOTE: No target_size! FluxResizer selects optimal resolution per image
39
+
40
+ # Model settings
41
+ model:
42
+ flux_model: "black-forest-labs/FLUX.1-dev" # Changed from schnell to dev
43
+ dtype: "float16"
44
+ dino_model: "dinov3_vitb16" # DINOv3 base model
45
+ feature_locations:
46
+ transformer_blocks: [4, 9, 13, 18]
47
+ single_transformer_blocks: [4, 15, 26, 37]
48
+ decoder:
49
+ features: 256
50
+ hyperfeature_fusion:
51
+ num_timesteps: 4
52
+ fusion_type: "transformer"
53
+ hidden_dim: 768
54
+ num_transformer_layers: 3
55
+ layer_scale_init: 1e-4
56
+
57
+ # FLUX settings
58
+ flux:
59
+ timesteps: 28
60
+ guidance_scale: 3.5
61
+ num_inference_steps: 1
62
+ concept_timesteps: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]
63
+ concept_layers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
64
+
65
+ # Hardware settings
66
+ hardware:
67
+ devices: 1 # Number of GPUs (DDP for multi-GPU)
68
+ #strategy: "ddp_find_unused_parameters_true" # Required for DDP with conditional logic
69
+ num_sanity_val_steps: 1
70
+ detect_anomaly: true
71
+ accelerator: "gpu"
72
+ num_workers: 8
73
+
74
+ # Paths (timestamps will be automatically added)
75
+ paths:
76
+ # All outputs live under ${MMDIFF_OUTPUT} (set it to a writable dir, e.g. /work/<user>/mmdiff_out).
77
+ cache_base_dir: "${MMDIFF_OUTPUT}/cache"
78
+ log_base_dir: "${MMDIFF_OUTPUT}/logs"
79
+ checkpoint_base_dir: "${MMDIFF_OUTPUT}/checkpoints"
80
+ use_timestamp: true
81
+ permanent_cache_dir: "${MMDIFF_OUTPUT}/cache/nyu_native_feature_cache" # Native resolution feature cache
82
+
83
+ # Logging
84
+ logging:
85
+ log_every_n_steps: 10
86
+ save_top_k: 3
87
+ monitor: "val_loss"
88
+ mode: "min"
89
+
90
+ # HuggingFace
91
+ huggingface:
92
+ token: "" # Leave empty and authenticate via `huggingface-cli login` or the HF_TOKEN env var
configs/pascal_voc_config.yaml ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pascal VOC Training Configuration - Native Resolution (Variable Resolution with FluxResizer)
2
+ # Uses FLUX.1-dev and FluxResizer for optimal native resolutions (no padding)
3
+
4
+ # Experiment settings
5
+ experiment:
6
+ name: "pascal_voc_native_resolution"
7
+ description: "Pascal VOC with FLUX.1-dev at native resolutions using FluxResizer"
8
+
9
+ # Concept configurations (same as original)
10
+ concepts:
11
+ basic: ["object", "background", "detail", "edges"]
12
+ meta: ["living", "vehicle", "furniture", "object", "background"]
13
+ meta_expanded: ["living", "vehicle", "furniture", "object", "background", "person", "car", "airplane", "table", "plant", "bird", "bicycle"]
14
+ combined: ["object", "background", "living", "vehicle", "furniture", "detail", "edges"]
15
+ with_classes: ["object", "background", "detail", "edges", "airplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "table", "dog", "horse", "motorbike", "person", "plant", "sheep", "sofa", "train", "television"]
16
+ all_comprehensive: ["living", "vehicle", "furniture", "object", "background", "detail", "edges", "airplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "table", "dog", "horse", "motorbike", "person", "plant", "sheep", "sofa", "train", "television"]
17
+
18
+ # Training settings
19
+ training:
20
+ concept_config: "basic" # Which concept configuration to use
21
+ epochs: 100 # Match baseline (was 20)
22
+ learning_rate: 1e-4 # Match baseline (capped at 3e-5 in optimizer code)
23
+ batch_size: 1 # REQUIRED: Must be 1 for variable resolution
24
+ accumulate_grad_batches: 4 # Effective batch size = 4
25
+ gradient_clip_val: 0.5
26
+ precision: "16-mixed"
27
+
28
+ # Data settings
29
+ data:
30
+ dataset: "pascal_voc"
31
+ data_root: "${VOC_ROOT}" # dir holding JPEGImages/
32
+ train_split_file: "${VOC_SPLITS}/train_aug.txt"
33
+ val_split_file: "${VOC_SPLITS}/val.txt"
34
+ mask_root: "${VOC_MASKS}" # dir holding SegmentationClassAug/ (masks)
35
+ num_classes: 21
36
+ # NOTE: No target_size! FluxResizer selects optimal resolution per image
37
+
38
+ # Model settings
39
+ model:
40
+ flux_model: "black-forest-labs/FLUX.1-dev" # Changed from schnell to dev
41
+ dtype: "float16"
42
+ dino_model: "dinov3_vitb16" # DINOv3 base model
43
+ feature_locations:
44
+ transformer_blocks: [4, 9, 13, 18]
45
+ single_transformer_blocks: [4, 15, 26, 37]
46
+ decoder:
47
+ features: 256
48
+ hyperfeature_fusion:
49
+ num_timesteps: 4
50
+ fusion_type: "transformer"
51
+ hidden_dim: 768
52
+ num_transformer_layers: 3
53
+ layer_scale_init: 1e-6
54
+
55
+ # FLUX settings
56
+ flux:
57
+ timesteps: 28
58
+ guidance_scale: 3.5
59
+ num_inference_steps: 1
60
+ concept_timesteps: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]
61
+ concept_layers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
62
+
63
+ # Hardware settings
64
+ hardware:
65
+ devices: 1 # Use 2 GPUs for faster training
66
+ #strategy: "ddp_find_unused_parameters_true" # Multi-GPU strategy
67
+ num_sanity_val_steps: 1
68
+ detect_anomaly: true
69
+ accelerator: "gpu"
70
+
71
+ # Paths (timestamps will be automatically added)
72
+ paths:
73
+ # All outputs live under ${MMDIFF_OUTPUT} (set it to a writable dir, e.g. /work/<user>/mmdiff_out).
74
+ cache_base_dir: "${MMDIFF_OUTPUT}/cache"
75
+ log_base_dir: "${MMDIFF_OUTPUT}/logs"
76
+ checkpoint_base_dir: "${MMDIFF_OUTPUT}/checkpoints"
77
+ use_timestamp: true
78
+ permanent_cache_dir: "${MMDIFF_OUTPUT}/cache/pascal_feature_cache" # Native resolution feature cache
79
+
80
+ # Logging
81
+ logging:
82
+ log_every_n_steps: 10
83
+ save_top_k: 3
84
+ monitor: "val_loss"
85
+ mode: "min"
86
+
87
+ # HuggingFace
88
+ huggingface:
89
+ token: "" # Leave empty and authenticate via `huggingface-cli login` or the HF_TOKEN env var
core/__init__.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared building blocks for the mmdiff training / extraction / inference scripts."""
2
+
3
+ from .flux_features import (
4
+ load_flux_pipeline,
5
+ MultiTimestepFeatureCache,
6
+ MultiTimestepFeatureExtractor,
7
+ distribute_concepts,
8
+ distribute_concepts_across_layers,
9
+ )
10
+ from .training_utils import (
11
+ EMA,
12
+ load_config,
13
+ setup_paths,
14
+ calculate_distributed_concept_channels,
15
+ feature_mode_flags,
16
+ resolve_c_dino,
17
+ build_dino_extractor,
18
+ build_hyperfeature_fusion,
19
+ add_shared_training_args,
20
+ add_model_args,
21
+ )
22
+
23
+ __all__ = [
24
+ "load_flux_pipeline",
25
+ "MultiTimestepFeatureCache",
26
+ "MultiTimestepFeatureExtractor",
27
+ "distribute_concepts",
28
+ "distribute_concepts_across_layers",
29
+ "EMA",
30
+ "load_config",
31
+ "setup_paths",
32
+ "calculate_distributed_concept_channels",
33
+ "feature_mode_flags",
34
+ "resolve_c_dino",
35
+ "build_dino_extractor",
36
+ "build_hyperfeature_fusion",
37
+ "add_shared_training_args",
38
+ "add_model_args",
39
+ ]
core/flux_features.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared FLUX feature machinery: load the pipeline, extract multi-timestep
2
+ activations + concept-attention maps at native resolution, and cache them."""
3
+
4
+ import numpy as np
5
+ import torch
6
+ import torch.nn.functional as F
7
+ from pathlib import Path
8
+ from torchvision.transforms.functional import to_pil_image
9
+
10
+ from flux_concept_attention import (
11
+ FluxWithConceptAttentionPipeline,
12
+ FluxTransformer2DModelWithConceptAttention,
13
+ )
14
+
15
+
16
+ def load_flux_pipeline(config, device="cuda", flux_model=None):
17
+ """Load the FLUX.1-dev concept-attention pipeline. Returns (pipeline, transformer)."""
18
+ flux_model = flux_model or config["model"]["flux_model"]
19
+ transformer = FluxTransformer2DModelWithConceptAttention.from_pretrained(
20
+ flux_model, subfolder="transformer", torch_dtype=torch.float16
21
+ )
22
+ pipeline = FluxWithConceptAttentionPipeline.from_pretrained(
23
+ flux_model, transformer=transformer, torch_dtype=torch.float16
24
+ ).to(device)
25
+ pipeline.set_progress_bar_config(disable=True)
26
+ return pipeline, transformer
27
+
28
+
29
+ class MultiTimestepFeatureCache:
30
+ """On-disk cache of multi-timestep features + concept maps (one dir per image)."""
31
+
32
+ def __init__(self, cache_dir: str):
33
+ self.cache_dir = Path(cache_dir)
34
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ def _image_dir(self, image_name: str) -> Path:
37
+ return self.cache_dir / image_name
38
+
39
+ def has_multi_timestep_features(self, image_name: str) -> bool:
40
+ return (self._image_dir(image_name) / "multi_timestep_data.pt").exists()
41
+
42
+ def save_multi_timestep_features(self, image_name: str, timestep_data: dict):
43
+ d = self._image_dir(image_name)
44
+ d.mkdir(parents=True, exist_ok=True)
45
+ torch.save(timestep_data, d / "multi_timestep_data.pt")
46
+
47
+ def load_multi_timestep_features(self, image_name: str, device="cuda"):
48
+ d = self._image_dir(image_name)
49
+ if not self.has_multi_timestep_features(image_name):
50
+ return None
51
+
52
+ # weights_only=False is safe here: we only load our own cached tensors.
53
+ try:
54
+ data = torch.load(d / "multi_timestep_data.pt", map_location=device, weights_only=False)
55
+ except (RuntimeError, EOFError) as e:
56
+ print(f"[CACHE ERROR] Corrupted cache for {image_name}: {e}")
57
+ import shutil
58
+ shutil.rmtree(d, ignore_errors=True)
59
+ return None
60
+
61
+ for timestep in data["features"]:
62
+ data["features"][timestep]["single_features"] = [
63
+ f.to(device) for f in data["features"][timestep]["single_features"]
64
+ ]
65
+
66
+ if "concept_maps" not in data:
67
+ data["concept_maps"] = {}
68
+ for timestep in data["concept_maps"]:
69
+ for concept in data["concept_maps"][timestep]:
70
+ cmap = data["concept_maps"][timestep][concept]
71
+ if hasattr(cmap, "to"):
72
+ data["concept_maps"][timestep][concept] = cmap.to(device)
73
+ elif isinstance(cmap, np.ndarray):
74
+ data["concept_maps"][timestep][concept] = torch.from_numpy(cmap).to(device)
75
+
76
+ return data
77
+
78
+
79
+ class MultiTimestepFeatureExtractor:
80
+ """Extract FLUX features at native resolution.
81
+
82
+ ``extract_or_load`` does a resolution-aware cache check then extracts (training/
83
+ inference); ``extract_features`` returns a CPU dict for the extraction scripts to save.
84
+ """
85
+
86
+ def __init__(self, pipeline, config, cache_dir: str, num_timesteps: int = 4, captions: dict = None):
87
+ self.pipeline = pipeline
88
+ self.config = config
89
+ self.cache = MultiTimestepFeatureCache(cache_dir)
90
+ self.num_timesteps = num_timesteps
91
+ self.captions = captions or {}
92
+
93
+ def _calculate_shift(self, image_seq_len, base_seq_len=256, max_seq_len=4096, base_shift=0.5, max_shift=1.15):
94
+ m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
95
+ b = base_shift - m * base_seq_len
96
+ return max(base_shift, min(max_shift, image_seq_len * m + b))
97
+
98
+ def _setup_scheduler_for_resolution(self, height: int, width: int):
99
+ # FLUX uses 16x VAE downsampling; the scheduler shift depends on sequence length.
100
+ image_seq_len = (height // 16) * (width // 16)
101
+ mu = self._calculate_shift(image_seq_len)
102
+ num_inference_steps = self.config.get("flux", {}).get("timesteps", 28)
103
+ self.pipeline.scheduler.set_timesteps(num_inference_steps, mu=mu)
104
+ return mu
105
+
106
+ def _get_evenly_spaced_timesteps(self):
107
+ all_timesteps = self.pipeline.scheduler.timesteps
108
+ group_size = 7
109
+ indices = [-(i * group_size + 1) for i in range(self.num_timesteps) if i * group_size < len(all_timesteps)]
110
+ selected = [int(all_timesteps[i]) for i in indices]
111
+ selected = [t for t in selected if t != 1000][:self.num_timesteps]
112
+ return sorted(selected, reverse=True)
113
+
114
+ def _run_extraction(self, pil_image, image_name, concepts, resolution, prompt):
115
+ """Core extraction loop. Returns a CPU-tensor timestep_data dict."""
116
+ height, width = resolution
117
+ mu = self._setup_scheduler_for_resolution(height, width)
118
+ selected_timesteps = self._get_evenly_spaced_timesteps()
119
+
120
+ timestep_data = {
121
+ "timesteps": selected_timesteps,
122
+ "features": {},
123
+ "concept_maps": {},
124
+ "image_name": image_name,
125
+ "concepts": concepts,
126
+ "resolution": (height, width),
127
+ "prompt": prompt,
128
+ "mu": mu,
129
+ }
130
+ concept_attention_kwargs = {
131
+ "concepts": concepts,
132
+ "timesteps": self.config["flux"]["concept_timesteps"],
133
+ "layers": self.config["flux"]["concept_layers"],
134
+ }
135
+
136
+ for timestep in selected_timesteps:
137
+ with torch.no_grad():
138
+ output = self.pipeline(
139
+ prompt=prompt,
140
+ image=pil_image,
141
+ height=height,
142
+ width=width,
143
+ timesteps=[timestep],
144
+ num_inference_steps=1,
145
+ guidance_scale=self.config["flux"]["guidance_scale"],
146
+ concept_attention_kwargs=concept_attention_kwargs,
147
+ generator=torch.Generator("cuda").manual_seed(42),
148
+ output_type="latent",
149
+ )
150
+
151
+ _, single_features = self.pipeline.transformer.get_features()
152
+ concept_maps_raw = output.concept_attention_maps
153
+ maps_list = (
154
+ concept_maps_raw[0]
155
+ if (len(concept_maps_raw) == 1 and isinstance(concept_maps_raw[0], list))
156
+ else concept_maps_raw
157
+ )
158
+ if len(maps_list) != len(concepts):
159
+ raise ValueError(f"Mismatch: {len(concepts)} concepts vs {len(maps_list)} maps")
160
+ concept_maps = {c: maps_list[i] for i, c in enumerate(concepts)}
161
+
162
+ timestep_data["features"][timestep] = {
163
+ "single_features": [f.cpu() for f in single_features]
164
+ }
165
+ timestep_data["concept_maps"][timestep] = {
166
+ concept: (cmap.cpu() if hasattr(cmap, "cpu") else cmap)
167
+ for concept, cmap in concept_maps.items()
168
+ }
169
+
170
+ return timestep_data
171
+
172
+ def _to_device(self, timestep_data, device):
173
+ for timestep in timestep_data["features"]:
174
+ timestep_data["features"][timestep]["single_features"] = [
175
+ f.to(device) for f in timestep_data["features"][timestep]["single_features"]
176
+ ]
177
+ for timestep in timestep_data["concept_maps"]:
178
+ for concept in timestep_data["concept_maps"][timestep]:
179
+ cmap = timestep_data["concept_maps"][timestep][concept]
180
+ if hasattr(cmap, "to"):
181
+ timestep_data["concept_maps"][timestep][concept] = cmap.to(device)
182
+ return timestep_data
183
+
184
+ def extract_or_load(self, image, image_name, concepts, resolution, prompt=None, force=False):
185
+ """Extract or load resolution-aware cached features; returns tensors on device."""
186
+ device = next(self.pipeline.transformer.parameters()).device
187
+ height, width = resolution
188
+
189
+ if not force and self.cache.has_multi_timestep_features(image_name):
190
+ cached = self.cache.load_multi_timestep_features(image_name, device)
191
+ if cached is not None and cached.get("resolution") == (height, width):
192
+ return cached
193
+
194
+ if prompt is None:
195
+ prompt = self.captions.get(image_name, "")
196
+ pil_image = to_pil_image(image[0].cpu())
197
+ timestep_data = self._run_extraction(pil_image, image_name, concepts, (height, width), prompt)
198
+
199
+ self.cache.save_multi_timestep_features(image_name, timestep_data)
200
+ return self._to_device(timestep_data, device)
201
+
202
+ def extract_features(self, images, prompts, image_names, concepts, height, width):
203
+ """Batch-style extraction (single image per call); returns a CPU dict to save."""
204
+ pil_image = to_pil_image(images[0].cpu())
205
+ return self._run_extraction(pil_image, image_names[0], concepts, (height, width), prompts[0])
206
+
207
+
208
+ def distribute_concepts(concept_maps, num_features, device):
209
+ """Split concept maps roughly evenly across ``num_features`` feature layers."""
210
+ processed = []
211
+ for _, t in concept_maps.items():
212
+ if not hasattr(t, "to"):
213
+ t = torch.from_numpy(t).float().to(device)
214
+ elif t.device != device:
215
+ t = t.to(device)
216
+
217
+ if t.dim() == 2:
218
+ t = t.view(1, 1, t.shape[0], t.shape[1])
219
+ elif t.dim() == 3:
220
+ if t.shape[0] == 1:
221
+ t = t.unsqueeze(1)
222
+ elif t.shape[2] == 1:
223
+ t = t.permute(2, 0, 1).unsqueeze(0)
224
+ else:
225
+ t = t.unsqueeze(1)
226
+ processed.append(t)
227
+
228
+ n = len(processed)
229
+ per = n // num_features
230
+ rem = n % num_features
231
+ out = []
232
+ start = 0
233
+ for i in range(num_features):
234
+ cnt = per + (1 if i < rem else 0)
235
+ end = start + cnt
236
+ if cnt > 0:
237
+ out.append(torch.cat(processed[start:end], dim=1))
238
+ else:
239
+ spatial_h, spatial_w = processed[0].shape[-2:]
240
+ out.append(torch.zeros(1, 1, spatial_h, spatial_w, device=device))
241
+ start = end
242
+ return out
243
+
244
+
245
+ def distribute_concepts_across_layers(concept_maps, num_layers, target_size, device):
246
+ """Depth variant of :func:`distribute_concepts`: resizes every map to
247
+ ``target_size`` first and emits zero-channel tensors for empty layers."""
248
+ processed = []
249
+ for _, v in concept_maps.items():
250
+ if isinstance(v, np.ndarray):
251
+ t = torch.from_numpy(v).float().to(device)
252
+ elif hasattr(v, "to"):
253
+ t = v.float().to(device)
254
+ else:
255
+ t = torch.tensor(v).float().to(device)
256
+
257
+ if t.dim() == 2:
258
+ t = t.view(1, 1, t.shape[0], t.shape[1])
259
+ elif t.dim() == 3:
260
+ if t.shape[0] == 1:
261
+ t = t.unsqueeze(1)
262
+ elif t.shape[2] == 1:
263
+ t = t.permute(2, 0, 1).unsqueeze(0)
264
+ else:
265
+ t = t.unsqueeze(1)
266
+
267
+ if t.shape[-2:] != target_size:
268
+ t = F.interpolate(t, size=target_size, mode="bilinear", align_corners=False)
269
+ processed.append(t)
270
+
271
+ num_concepts = len(processed)
272
+ if num_concepts == 0:
273
+ return [torch.zeros(1, 0, target_size[0], target_size[1], device=device) for _ in range(num_layers)]
274
+
275
+ per_layer = num_concepts // num_layers
276
+ remainder = num_concepts % num_layers
277
+ distributed = []
278
+ start_idx = 0
279
+ for i in range(num_layers):
280
+ count = per_layer + (1 if i < remainder else 0)
281
+ end_idx = start_idx + count
282
+ if count > 0:
283
+ distributed.append(torch.cat(processed[start_idx:end_idx], dim=1))
284
+ else:
285
+ distributed.append(torch.zeros(1, 0, target_size[0], target_size[1], device=device))
286
+ start_idx = end_idx
287
+ return distributed
core/training_utils.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared trainer utilities: EMA, config/paths, DINO/fusion builders and argparse helpers."""
2
+
3
+ import os
4
+ from datetime import datetime
5
+
6
+ import torch
7
+ import yaml
8
+
9
+ from models.hyperfeature_fusion import create_hyperfeature_fusion
10
+ from models.dinov3_hf_extractor import create_dinov3_hf_extractor
11
+
12
+
13
+ DINO_REPO_MAP = {
14
+ "dinov3_vits16": "facebook/dinov3-vits16-pretrain-lvd1689m",
15
+ "dinov3_vitb16": "facebook/dinov3-vitb16-pretrain-lvd1689m",
16
+ "dinov3_vitl16": "facebook/dinov3-vitl16-pretrain-lvd1689m",
17
+ "dinov3_vitg16": "facebook/dinov3-vitg16-pretrain-lvd1689m",
18
+ }
19
+
20
+
21
+ class EMA:
22
+ """Exponential Moving Average of trainable, floating-point model parameters."""
23
+
24
+ def __init__(self, model, beta=0.999):
25
+ self.beta = beta
26
+ self.shadow = {}
27
+ self.backup = {}
28
+ for name, param in model.named_parameters():
29
+ if param.requires_grad and param.dtype.is_floating_point:
30
+ self.shadow[name] = param.data.detach().clone()
31
+
32
+ def update(self, model):
33
+ with torch.no_grad():
34
+ for name, param in model.named_parameters():
35
+ if name in self.shadow:
36
+ if self.shadow[name].device != param.device:
37
+ self.shadow[name] = self.shadow[name].to(param.device)
38
+ self.shadow[name].mul_(self.beta).add_(param.data.detach(), alpha=1.0 - self.beta)
39
+
40
+ def apply_shadow(self, model):
41
+ self.backup = {}
42
+ with torch.no_grad():
43
+ for name, param in model.named_parameters():
44
+ if name in self.shadow:
45
+ if self.shadow[name].device != param.device:
46
+ self.shadow[name] = self.shadow[name].to(param.device)
47
+ self.backup[name] = param.data.detach().clone()
48
+ param.data.copy_(self.shadow[name])
49
+ return self.backup
50
+
51
+ def restore_backup(self, model):
52
+ with torch.no_grad():
53
+ for name, param in model.named_parameters():
54
+ if name in self.backup:
55
+ param.data.copy_(self.backup[name])
56
+ self.backup = {}
57
+
58
+ def restore(self, model, backup=None):
59
+ b = backup if backup is not None else self.backup
60
+ with torch.no_grad():
61
+ for name, param in model.named_parameters():
62
+ if name in b:
63
+ if b[name].device != param.device:
64
+ b[name] = b[name].to(param.device)
65
+ param.data.copy_(b[name])
66
+ self.backup = {}
67
+
68
+
69
+ def _expand_env(value):
70
+ """Recursively expand ${VAR}/$VAR and ~ in all string values of a config."""
71
+ if isinstance(value, str):
72
+ return os.path.expanduser(os.path.expandvars(value))
73
+ if isinstance(value, dict):
74
+ return {k: _expand_env(v) for k, v in value.items()}
75
+ if isinstance(value, list):
76
+ return [_expand_env(v) for v in value]
77
+ return value
78
+
79
+
80
+ def load_config(config_path: str):
81
+ with open(config_path, "r") as f:
82
+ return _expand_env(yaml.safe_load(f))
83
+
84
+
85
+ def setup_paths(config, task: str):
86
+ """Timestamped log/checkpoint dirs plus the (permanent) feature cache dir."""
87
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
88
+ cache_dir = config["paths"].get("permanent_cache_dir") or f"{config['paths']['cache_base_dir']}/{task}_cache"
89
+ return {
90
+ "cache_dir": cache_dir,
91
+ "log_dir": f"{config['paths']['log_base_dir']}/{task}_logs_{timestamp}",
92
+ "checkpoint_dir": f"{config['paths']['checkpoint_base_dir']}/{task}_checkpoints_{timestamp}",
93
+ }
94
+
95
+
96
+ def calculate_distributed_concept_channels(num_concepts, num_features=4):
97
+ """Per-layer concept channel counts when distributing concepts across layers."""
98
+ per = num_concepts // num_features
99
+ rem = num_concepts % num_features
100
+ return [per + (1 if i < rem else 0) for i in range(num_features)]
101
+
102
+
103
+ def feature_mode_flags(feature_mode):
104
+ """Return ``(use_flux, use_dino)`` for an ablation mode."""
105
+ assert feature_mode in ("full", "flux_only", "dino_only"), feature_mode
106
+ return feature_mode in ("full", "flux_only"), feature_mode in ("full", "dino_only")
107
+
108
+
109
+ def resolve_c_dino(feature_mode, dino_model):
110
+ """DINO feature width for the given backbone (0 when DINO is disabled)."""
111
+ if feature_mode == "flux_only":
112
+ return 0
113
+ if "vits" in dino_model:
114
+ return 384
115
+ if "vitl" in dino_model:
116
+ return 1024
117
+ if "vitg" in dino_model:
118
+ return 1536
119
+ return 768 # vitb
120
+
121
+
122
+ def build_dino_extractor(dino_model, feature_mode, layer_indices=(2, 5, 8, 11)):
123
+ """Build the DINOv3 extractor (HF when known, torch.hub fallback otherwise)."""
124
+ trainable = feature_mode == "dino_only"
125
+ repo_id = DINO_REPO_MAP.get(dino_model)
126
+ if repo_id:
127
+ return create_dinov3_hf_extractor(
128
+ repo_id=repo_id, take_indices=list(layer_indices), trainable=trainable
129
+ )
130
+ from models.dino_fusion import create_dino_extractor
131
+ return create_dino_extractor(model_name=dino_model, take_indices=list(layer_indices))
132
+
133
+
134
+ def build_hyperfeature_fusion(use_flux, num_timesteps, hidden_dim, num_transformer_layers,
135
+ layer_scale_init, fusion_type="transformer", return_alpha=False):
136
+ """Build the multi-timestep hyperfeature fusion module, or ``None`` without FLUX."""
137
+ if not use_flux:
138
+ return None
139
+ return create_hyperfeature_fusion(
140
+ num_timesteps=num_timesteps,
141
+ num_layers=4,
142
+ fusion_type=fusion_type,
143
+ hidden_dim=hidden_dim,
144
+ num_transformer_layers=num_transformer_layers,
145
+ layer_scale_init=layer_scale_init,
146
+ return_alpha=return_alpha,
147
+ )
148
+
149
+
150
+ def add_shared_training_args(parser, *, config_default, hidden_dim, num_transformer_layers,
151
+ layer_scale_init, include_cache_args=True):
152
+ """Register the backbone/ablation arguments common to all task trainers."""
153
+ parser.add_argument("--config", type=str, default=config_default)
154
+ parser.add_argument("--num_timesteps", type=int, default=4)
155
+ parser.add_argument("--hidden_dim", type=int, default=hidden_dim)
156
+ parser.add_argument("--num_transformer_layers", type=int, default=num_transformer_layers)
157
+ parser.add_argument("--layer_scale_init", type=float, default=layer_scale_init)
158
+ parser.add_argument("--dino_model", type=str, default="dinov3_vitb16")
159
+ parser.add_argument("--feature_mode", type=str, default="full",
160
+ choices=["full", "flux_only", "dino_only"],
161
+ help="Ablation: 'full' (FLUX+concepts+DINO), 'flux_only' (FLUX+concepts), "
162
+ "'dino_only' (trainable DINO only)")
163
+ parser.add_argument("--fast_dev_run", action="store_true",
164
+ help="Smoke test: run a single train+val batch then exit (verifies the "
165
+ "training pipeline starts end-to-end)")
166
+ if include_cache_args:
167
+ parser.add_argument("--limit_images", type=int, default=None, help="Limit dataset size for testing")
168
+ parser.add_argument("--cache_only", action="store_true",
169
+ help="Load features from cache only (no FLUX pipeline)")
170
+
171
+
172
+ def add_model_args(parser):
173
+ """Architecture flags for inference/generation; must match the checkpoint."""
174
+ parser.add_argument("--dino_model", default="dinov3_vitb16")
175
+ parser.add_argument("--num_timesteps", type=int, default=4)
176
+ parser.add_argument("--hidden_dim", type=int, default=768)
177
+ parser.add_argument("--num_transformer_layers", type=int, default=3)
178
+ parser.add_argument("--layer_scale_init", type=float, default=1e-6)
179
+ parser.add_argument("--fusion_type", default="transformer", choices=["attention", "transformer"])
180
+ parser.add_argument("--high_res_decoder", action="store_true", help="(nyu) high-res depth decoder")
181
+ parser.add_argument("--device", default="cuda")
download_checkpoints.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Download model checkpoints from yagmurakarken/mmdiff at Space startup."""
2
+ import os
3
+ from huggingface_hub import hf_hub_download
4
+
5
+ CHECKPOINT_DIR = "/tmp/checkpoints"
6
+ MODEL_REPO = "yagmurakarken/mmdiff"
7
+ CHECKPOINT_FILES = ["duts_saliency.ckpt", "nyu_depth.ckpt", "pascal_segmentation.ckpt"]
8
+
9
+
10
+ def download_all():
11
+ os.makedirs(CHECKPOINT_DIR, exist_ok=True)
12
+ for fname in CHECKPOINT_FILES:
13
+ path = os.path.join(CHECKPOINT_DIR, fname)
14
+ if not os.path.exists(path):
15
+ print(f"[DOWNLOAD] {fname} ...")
16
+ downloaded = hf_hub_download(
17
+ repo_id=MODEL_REPO,
18
+ filename=fname,
19
+ repo_type="model",
20
+ local_dir=CHECKPOINT_DIR,
21
+ )
22
+ print(f"[DOWNLOAD] {fname} -> {downloaded}")
23
+ else:
24
+ print(f"[DOWNLOAD] {fname} already exists at {path}")
25
+
26
+
27
+ if __name__ == "__main__":
28
+ download_all()
flux_concept_attention/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .flux_with_concept_attention_pipeline import FluxWithConceptAttentionPipeline
2
+ from .flux_dit_with_concept_attention import FluxTransformer2DModelWithConceptAttention
3
+
4
+ __all__ = ["FluxWithConceptAttentionPipeline", "FluxTransformer2DModelWithConceptAttention"]
flux_concept_attention/flux_dit_block_with_concept_attention.py ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn.functional as F
3
+ from typing import Any, Dict, Optional, Tuple
4
+ from torch import nn
5
+ import einops
6
+
7
+ from diffusers.models.transformers.transformer_flux import FluxTransformerBlock
8
+ from diffusers.models.attention import Attention
9
+ from diffusers.models.embeddings import apply_rotary_emb
10
+
11
+
12
+ def _trim_rope(rope, target_len: int):
13
+ """
14
+ Ensure rotary embedding length matches the attention sequence length.
15
+ Works for Diffusers' rope tuples (cos, sin) or a single tensor.
16
+
17
+ Rope format: [seq_len, embed_dim], so dimension 0 is the sequence.
18
+
19
+ Args:
20
+ rope: Either a tuple (cos, sin) or a single tensor, or None
21
+ target_len: Target sequence length
22
+
23
+ Returns:
24
+ Trimmed rope in the same format as input (takes last target_len positions)
25
+ """
26
+ if rope is None:
27
+ return None
28
+
29
+ # Rope is a (cos, sin) tuple (Diffusers format)
30
+ # Each tensor is [seq_len, embed_dim]
31
+ if isinstance(rope, tuple):
32
+ cos, sin = rope
33
+ if cos.shape[0] != target_len:
34
+ # Trim dimension 0 (sequence length)
35
+ cos = cos[-target_len:, ...]
36
+ sin = sin[-target_len:, ...]
37
+ return (cos, sin)
38
+
39
+ # Rope is a single tensor
40
+ if rope.shape[0] != target_len:
41
+ rope = rope[-target_len:, ...]
42
+ return rope
43
+
44
+
45
+ class FluxConceptAttentionProcessor:
46
+ """
47
+ Custom attention processor for FLUX that implements concept attention.
48
+ Exactly matches original FLUX attention pattern while adding concept observation stream.
49
+ """
50
+
51
+ def __init__(self):
52
+ if not hasattr(F, "scaled_dot_product_attention"):
53
+ raise ImportError("FluxConceptAttentionProcessor requires PyTorch 2.0")
54
+
55
+ def __call__(
56
+ self,
57
+ attn: Attention,
58
+ hidden_states: torch.Tensor,
59
+ encoder_hidden_states: torch.Tensor,
60
+ attention_mask: Optional[torch.FloatTensor] = None,
61
+ image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
62
+ concept_hidden_states: Optional[torch.Tensor] = None,
63
+ concept_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
64
+ q_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
65
+ kv_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
66
+ **kwargs
67
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
68
+ # Concept-specific arguments are now explicit parameters (required for inspect.signature)
69
+
70
+ batch_size, _, _ = encoder_hidden_states.shape
71
+
72
+ # *** MAIN GENERATION STREAM *** - Exact FLUX Pattern
73
+
74
+ # 1. `sample` projections (image hidden states)
75
+ query = attn.to_q(hidden_states)
76
+ key = attn.to_k(hidden_states)
77
+ value = attn.to_v(hidden_states)
78
+
79
+ inner_dim = key.shape[-1]
80
+ head_dim = inner_dim // attn.heads
81
+
82
+ image_query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
83
+ image_key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
84
+ image_value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
85
+
86
+ if attn.norm_q is not None:
87
+ image_query = attn.norm_q(image_query)
88
+ if attn.norm_k is not None:
89
+ image_key = attn.norm_k(image_key)
90
+
91
+ # 2. `context` projections (text encoder hidden states) - FLUX specific
92
+ encoder_hidden_states_query_proj = attn.add_q_proj(encoder_hidden_states)
93
+ encoder_hidden_states_key_proj = attn.add_k_proj(encoder_hidden_states)
94
+ encoder_hidden_states_value_proj = attn.add_v_proj(encoder_hidden_states)
95
+
96
+ encoder_hidden_states_query_proj = encoder_hidden_states_query_proj.view(
97
+ batch_size, -1, attn.heads, head_dim
98
+ ).transpose(1, 2)
99
+ encoder_hidden_states_key_proj = encoder_hidden_states_key_proj.view(
100
+ batch_size, -1, attn.heads, head_dim
101
+ ).transpose(1, 2)
102
+ encoder_hidden_states_value_proj = encoder_hidden_states_value_proj.view(
103
+ batch_size, -1, attn.heads, head_dim
104
+ ).transpose(1, 2)
105
+
106
+ if attn.norm_added_q is not None:
107
+ encoder_hidden_states_query_proj = attn.norm_added_q(encoder_hidden_states_query_proj)
108
+ if attn.norm_added_k is not None:
109
+ encoder_hidden_states_key_proj = attn.norm_added_k(encoder_hidden_states_key_proj)
110
+
111
+ # 3. FLUX concatenation order: [encoder, image]
112
+ query = torch.cat([encoder_hidden_states_query_proj, image_query], dim=2)
113
+ key = torch.cat([encoder_hidden_states_key_proj, image_key], dim=2)
114
+ value = torch.cat([encoder_hidden_states_value_proj, image_value], dim=2)
115
+
116
+ # 4. Apply rotary embeddings to FULL concatenated tensors (FLUX way)
117
+ # Use explicit q/kv ropes; fallback to image_rotary_emb
118
+ rope_q = q_rotary_emb if q_rotary_emb is not None else image_rotary_emb
119
+ rope_kv = kv_rotary_emb if kv_rotary_emb is not None else image_rotary_emb
120
+
121
+ if rope_q is not None:
122
+ # query shape after transpose: [B, heads, seq, head_dim]
123
+ q_len = query.shape[2] # sequence dimension is at index 2
124
+ rope_q = _trim_rope(rope_q, q_len)
125
+ query = apply_rotary_emb(query, rope_q, sequence_dim=2)
126
+
127
+ if rope_kv is not None:
128
+ k_len = key.shape[2] # sequence dimension is at index 2
129
+ rope_kv = _trim_rope(rope_kv, k_len)
130
+ key = apply_rotary_emb(key, rope_kv, sequence_dim=2)
131
+
132
+ # 5. Main text+image attention (FLUX standard)
133
+ hidden_states = F.scaled_dot_product_attention(
134
+ query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False
135
+ )
136
+
137
+ hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim)
138
+ hidden_states = hidden_states.to(query.dtype)
139
+
140
+ # *** CONCEPT STREAM *** - Exact same logic as text-image (following reference)
141
+ concept_hidden_states_output = None
142
+ concept_attention_maps = None
143
+
144
+ if concept_hidden_states is not None:
145
+ # Use TEXT projections for concepts (like reference: txt_attn.qkv)
146
+ concept_query = attn.add_q_proj(concept_hidden_states) # Same as text!
147
+ concept_key = attn.add_k_proj(concept_hidden_states) # Same as text!
148
+ concept_value = attn.add_v_proj(concept_hidden_states) # Same as text!
149
+
150
+ concept_query = concept_query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
151
+ concept_key = concept_key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
152
+ concept_value = concept_value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
153
+
154
+ if attn.norm_added_q is not None:
155
+ concept_query = attn.norm_added_q(concept_query) # Text normalization
156
+ if attn.norm_added_k is not None:
157
+ concept_key = attn.norm_added_k(concept_key) # Text normalization
158
+
159
+ concept_image_q = torch.cat([concept_query, image_query], dim=2)
160
+ concept_image_k = torch.cat([concept_key, image_key], dim=2)
161
+ concept_image_v = torch.cat([concept_value, image_value], dim=2)
162
+
163
+ # Apply concept rotary embeddings to FULL concatenated tensor (like reference)
164
+ if concept_rotary_emb is not None:
165
+ # concept_image_q shape after transpose: [B, heads, seq, head_dim]
166
+ cq_len = concept_image_q.shape[2] # sequence dimension is at index 2
167
+ ck_len = concept_image_k.shape[2]
168
+
169
+ rope_cq = _trim_rope(concept_rotary_emb, cq_len)
170
+ rope_ck = _trim_rope(concept_rotary_emb, ck_len)
171
+
172
+ concept_image_q = apply_rotary_emb(concept_image_q, rope_cq, sequence_dim=2)
173
+ concept_image_k = apply_rotary_emb(concept_image_k, rope_ck, sequence_dim=2)
174
+
175
+ # Do the joint attention operation (like reference)
176
+ concept_image_attn = F.scaled_dot_product_attention(
177
+ concept_image_q,
178
+ concept_image_k,
179
+ concept_image_v,
180
+ dropout_p=0.0,
181
+ is_causal=False
182
+ )
183
+
184
+ # Separate the concept attention (like reference: concept_attn = concept_image_attn[:, :, :concepts.shape[1]])
185
+ concept_attn = concept_image_attn[:, :, :concept_hidden_states.size(1)]
186
+ concept_hidden_states_output = concept_attn.transpose(1, 2).reshape(
187
+ batch_size, -1, attn.heads * head_dim
188
+ )
189
+
190
+ # Compute attention maps from concept and image queries (before attention)
191
+ # Save vectors for postprocessing (like reference implementation)
192
+ concept_attention_maps = {
193
+ 'concept_vectors': concept_query, # (batch, heads, concepts, dim)
194
+ 'image_vectors': image_query # (batch, heads, patches, dim)
195
+ }
196
+
197
+ # 6. FLUX output processing
198
+ encoder_hidden_states_out, hidden_states_out = (
199
+ hidden_states[:, : encoder_hidden_states.shape[1]],
200
+ hidden_states[:, encoder_hidden_states.shape[1] :],
201
+ )
202
+
203
+ # linear proj
204
+ hidden_states_out = attn.to_out[0](hidden_states_out)
205
+ # dropout
206
+ hidden_states_out = attn.to_out[1](hidden_states_out)
207
+
208
+ # FLUX specific: separate output projection for encoder
209
+ encoder_hidden_states_out = attn.to_add_out(encoder_hidden_states_out)
210
+
211
+ # Process concept outputs with same projections
212
+ if concept_hidden_states_output is not None:
213
+ concept_hidden_states_output = attn.to_out[0](concept_hidden_states_output)
214
+ concept_hidden_states_output = attn.to_out[1](concept_hidden_states_output)
215
+
216
+ # Return vectors for postprocessing (like reference implementation)
217
+ concept_attention_maps = {
218
+ 'concept_vectors': concept_hidden_states_output, # Final processed concept features
219
+ 'image_vectors': hidden_states_out # Final processed image features
220
+ }
221
+
222
+ return hidden_states_out, encoder_hidden_states_out, concept_hidden_states_output, concept_attention_maps
223
+
224
+
225
+ class FluxTransformerBlockWithConceptAttention(FluxTransformerBlock):
226
+ """
227
+ Simplified FLUX transformer block with concept attention.
228
+ Uses the elegant CogVideoX approach with custom attention processor.
229
+ """
230
+
231
+ def __init__(self, *args, **kwargs):
232
+ super().__init__(*args, **kwargs)
233
+ self.attn.processor = FluxConceptAttentionProcessor()
234
+
235
+ def forward(
236
+ self,
237
+ hidden_states: torch.Tensor,
238
+ encoder_hidden_states: torch.Tensor,
239
+ concept_hidden_states: Optional[torch.Tensor],
240
+ temb: torch.Tensor,
241
+ concept_temb: Optional[torch.Tensor],
242
+ image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
243
+ concept_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
244
+ joint_attention_kwargs: Optional[Dict[str, Any]] = None,
245
+ concept_attention_kwargs: Optional[Dict[str, Any]] = None,
246
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
247
+
248
+ norm_hidden_states, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.norm1(
249
+ hidden_states,
250
+ emb=temb
251
+ )
252
+ norm_encoder_hidden_states, c_gate_msa, c_shift_mlp, c_scale_mlp, c_gate_mlp = self.norm1_context(
253
+ encoder_hidden_states,
254
+ emb=temb
255
+ )
256
+
257
+ # Concept normalization (use same temb as main stream if concept_temb is None)
258
+ norm_concept_hidden_states = None
259
+ concept_gate_msa = concept_shift_mlp = concept_scale_mlp = concept_gate_mlp = None
260
+ concept_gate_ff = None
261
+
262
+ if concept_hidden_states is not None:
263
+ effective_concept_temb = concept_temb if concept_temb is not None else temb
264
+ norm_concept_hidden_states, concept_gate_msa, concept_shift_mlp, concept_scale_mlp, concept_gate_mlp = self.norm1_context(
265
+ concept_hidden_states,
266
+ emb=effective_concept_temb
267
+ )
268
+
269
+ joint_attention_kwargs = joint_attention_kwargs or {}
270
+
271
+ # Pass concept-specific args through joint_attention_kwargs
272
+ # (they're not accepted as direct args by FluxAttention.forward)
273
+ if norm_concept_hidden_states is not None:
274
+ joint_attention_kwargs['concept_hidden_states'] = norm_concept_hidden_states
275
+ joint_attention_kwargs['concept_rotary_emb'] = concept_rotary_emb
276
+
277
+ # Attention with concept attention (using our custom processor)
278
+ attention_outputs = self.attn(
279
+ hidden_states=norm_hidden_states,
280
+ encoder_hidden_states=norm_encoder_hidden_states,
281
+ image_rotary_emb=image_rotary_emb,
282
+ **joint_attention_kwargs,
283
+ )
284
+
285
+ if len(attention_outputs) == 4:
286
+ attn_output, context_attn_output, concept_attn_output, concept_attention_maps = attention_outputs
287
+ ip_attn_output = None
288
+ elif len(attention_outputs) == 5:
289
+ attn_output, context_attn_output, concept_attn_output, concept_attention_maps, ip_attn_output = attention_outputs
290
+ else:
291
+ # Fallback for when no concept attention
292
+ attn_output, context_attn_output = attention_outputs[:2]
293
+ concept_attn_output = None
294
+ concept_attention_maps = None
295
+ ip_attn_output = attention_outputs[2] if len(attention_outputs) > 2 else None
296
+
297
+ ################## Process Concept Features FIRST (like CogVideoX) ##################
298
+ if concept_attn_output is not None and concept_hidden_states is not None:
299
+ # Apply concept attention gate and residual
300
+ concept_attn_output = concept_gate_msa.unsqueeze(1) * concept_attn_output
301
+ concept_hidden_states = concept_hidden_states + concept_attn_output
302
+
303
+ # Concept feedforward processing (norm2_context is regular LayerNorm, not adaptive)
304
+ norm_concept_hidden_states = self.norm2_context(concept_hidden_states)
305
+ norm_concept_hidden_states = norm_concept_hidden_states * (1 + concept_scale_mlp[:, None]) + concept_shift_mlp[:, None]
306
+ concept_ff_output = self.ff_context(norm_concept_hidden_states)
307
+ concept_hidden_states = concept_hidden_states + concept_gate_mlp.unsqueeze(1) * concept_ff_output
308
+
309
+ if concept_hidden_states.dtype == torch.float16:
310
+ concept_hidden_states = concept_hidden_states.clip(-65504, 65504)
311
+
312
+ ################## Now Process Main Generation Stream ##################
313
+ # Standard FLUX processing for image features
314
+ attn_output = gate_msa.unsqueeze(1) * attn_output
315
+ hidden_states = hidden_states + attn_output
316
+
317
+ norm_hidden_states = self.norm2(hidden_states)
318
+ norm_hidden_states = norm_hidden_states * (1 + scale_mlp[:, None]) + shift_mlp[:, None]
319
+
320
+ ff_output = self.ff(norm_hidden_states)
321
+ ff_output = gate_mlp.unsqueeze(1) * ff_output
322
+ hidden_states = hidden_states + ff_output
323
+
324
+ if ip_attn_output is not None:
325
+ hidden_states = hidden_states + ip_attn_output
326
+
327
+ # Standard FLUX processing for text features
328
+ if context_attn_output is not None:
329
+ context_attn_output = c_gate_msa.unsqueeze(1) * context_attn_output
330
+ encoder_hidden_states = encoder_hidden_states + context_attn_output
331
+
332
+ norm_encoder_hidden_states = self.norm2_context(encoder_hidden_states)
333
+ norm_encoder_hidden_states = norm_encoder_hidden_states * (1 + c_scale_mlp[:, None]) + c_shift_mlp[:, None]
334
+
335
+ context_ff_output = self.ff_context(norm_encoder_hidden_states)
336
+ encoder_hidden_states = encoder_hidden_states + c_gate_mlp.unsqueeze(1) * context_ff_output
337
+
338
+ if encoder_hidden_states.dtype == torch.float16:
339
+ encoder_hidden_states = encoder_hidden_states.clip(-65504, 65504)
340
+
341
+ return encoder_hidden_states, hidden_states, concept_hidden_states, concept_attention_maps
342
+
flux_concept_attention/flux_dit_with_concept_attention.py ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict
2
+
3
+ import torch
4
+ import numpy as np
5
+ from typing import Any, Dict, Optional, Tuple, Union
6
+ from torch import nn
7
+ from torch import Tensor
8
+
9
+ from diffusers.models.transformers.transformer_flux import FluxTransformer2DModel
10
+ from diffusers.models.transformers.transformer_flux import FluxSingleTransformerBlock
11
+ from diffusers.models.normalization import AdaLayerNormContinuous
12
+ from diffusers.utils import USE_PEFT_BACKEND, is_torch_version, logging, scale_lora_layers, unscale_lora_layers, BaseOutput
13
+ from diffusers.utils.import_utils import is_torch_npu_available
14
+ from diffusers.utils.torch_utils import maybe_allow_in_graph
15
+ from diffusers.models.embeddings import CombinedTimestepGuidanceTextProjEmbeddings, CombinedTimestepTextProjEmbeddings, FluxPosEmbed
16
+
17
+ from .flux_dit_block_with_concept_attention import FluxTransformerBlockWithConceptAttention
18
+
19
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
20
+
21
+
22
+ class FluxTransformer2DOutputWithConceptAttention(BaseOutput):
23
+ sample: torch.Tensor
24
+ concept_attention_maps: torch.Tensor
25
+
26
+
27
+ class FluxTransformer2DModelWithConceptAttention(FluxTransformer2DModel):
28
+ """
29
+ The Transformer model introduced in Flux with Concept Attention.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ patch_size: int = 1,
35
+ in_channels: int = 64,
36
+ out_channels: Optional[int] = None,
37
+ num_layers: int = 19,
38
+ num_single_layers: int = 38,
39
+ attention_head_dim: int = 128,
40
+ num_attention_heads: int = 24,
41
+ joint_attention_dim: int = 4096,
42
+ pooled_projection_dim: int = 768,
43
+ guidance_embeds: bool = True,
44
+ axes_dims_rope: Tuple[int] = (16, 56, 56),
45
+ feature_locations: Optional[Dict[str, List[int]]] = None,
46
+ ):
47
+ super().__init__(
48
+ patch_size=patch_size,
49
+ in_channels=in_channels,
50
+ out_channels=out_channels,
51
+ num_layers=num_layers,
52
+ num_single_layers=num_single_layers,
53
+ attention_head_dim=attention_head_dim,
54
+ num_attention_heads=num_attention_heads,
55
+ joint_attention_dim=joint_attention_dim,
56
+ pooled_projection_dim=pooled_projection_dim,
57
+ guidance_embeds=guidance_embeds,
58
+ axes_dims_rope=axes_dims_rope,
59
+ )
60
+ self.out_channels = out_channels or in_channels
61
+ self.inner_dim = self.config.num_attention_heads * self.config.attention_head_dim
62
+
63
+ self.pos_embed = FluxPosEmbed(theta=10000, axes_dim=axes_dims_rope)
64
+
65
+ text_time_guidance_cls = (
66
+ CombinedTimestepGuidanceTextProjEmbeddings if self.config.guidance_embeds else CombinedTimestepTextProjEmbeddings
67
+ )
68
+ self.time_text_embed = text_time_guidance_cls(
69
+ embedding_dim=self.inner_dim, pooled_projection_dim=self.config.pooled_projection_dim
70
+ )
71
+
72
+ self.context_embedder = nn.Linear(self.config.joint_attention_dim, self.inner_dim)
73
+ self.x_embedder = nn.Linear(self.config.in_channels, self.inner_dim)
74
+
75
+ self.transformer_blocks = nn.ModuleList(
76
+ [
77
+ FluxTransformerBlockWithConceptAttention(
78
+ dim=self.inner_dim,
79
+ num_attention_heads=self.config.num_attention_heads,
80
+ attention_head_dim=self.config.attention_head_dim,
81
+ )
82
+ for i in range(self.config.num_layers)
83
+ ]
84
+ )
85
+
86
+ self.single_transformer_blocks = nn.ModuleList(
87
+ [
88
+ FluxSingleTransformerBlock(
89
+ dim=self.inner_dim,
90
+ num_attention_heads=self.config.num_attention_heads,
91
+ attention_head_dim=self.config.attention_head_dim,
92
+ )
93
+ for i in range(self.config.num_single_layers)
94
+ ]
95
+ )
96
+
97
+ self.norm_out = AdaLayerNormContinuous(self.inner_dim, self.inner_dim, elementwise_affine=False, eps=1e-6)
98
+ self.proj_out = nn.Linear(self.inner_dim, patch_size * patch_size * self.out_channels, bias=True)
99
+
100
+ self.gradient_checkpointing = False
101
+
102
+ self.stored_features: Dict[str, Tensor] = {}
103
+ self.feature_locations = feature_locations or {
104
+ "transformer_blocks": [4, 9, 13, 18],
105
+ "single_transformer_blocks": [4, 16, 27, 36],
106
+ }
107
+ self._register_feature_hooks()
108
+
109
+ def get_features(self) -> Tuple[List[Tensor], List[Tensor]]:
110
+ """
111
+ Get the stored feature maps as raw tokens for downstream reshaping.
112
+
113
+ For dual stream transformer blocks: Returns the second item in the tuple (image tokens)
114
+ Shape: [B, H*W, C] where H*W is the actual spatial size
115
+
116
+ For single stream transformer blocks: Extracts image tokens from full sequence
117
+ Shape: [B, H*W, C] where H*W is the actual spatial size (excluding text tokens)
118
+
119
+ Returns:
120
+ Tuple containing:
121
+ - List of transformer block features as tokens [B, H*W, C]
122
+ - List of single transformer block features as tokens [B, H*W, C]
123
+ """
124
+ transformer_features = []
125
+ single_transformer_features = []
126
+
127
+ # Debug flag to print shapes on first call
128
+ for name, feature_output in self.stored_features.items():
129
+ if "single_transformer_blocks" in name:
130
+ # Single blocks return (encoder_hidden_states, hidden_states) tuple
131
+ if isinstance(feature_output, tuple) and len(feature_output) >= 2:
132
+ image_feature = feature_output[1] # [B, H*W, C] - image tokens only
133
+ single_transformer_features.append(image_feature)
134
+ else:
135
+ # Fallback if not a tuple (shouldn't happen)
136
+ single_transformer_features.append(feature_output)
137
+ elif "transformer_blocks" in name:
138
+ if isinstance(feature_output, tuple) and len(feature_output) >= 2:
139
+ image_feature = feature_output[1] # [B, H*W, C]
140
+ transformer_features.append(image_feature)
141
+
142
+ return (transformer_features, single_transformer_features)
143
+
144
+ def _get_hook(self, name: str):
145
+ """
146
+ Create a forward hook function for feature extraction.
147
+
148
+ Args:
149
+ name: Identifier for the layer where the hook will be attached
150
+
151
+ Returns:
152
+ Callable hook function that stores the layer's output tensor
153
+ """
154
+
155
+ def hook(
156
+ module: nn.Module, input: Union[Tensor, Tuple[Tensor, ...]], output: Tensor
157
+ ) -> None:
158
+ self.stored_features[name] = output
159
+
160
+ return hook
161
+
162
+ def _register_feature_hooks(self) -> None:
163
+ """
164
+ Register forward hooks on the specified layers to capture their outputs.
165
+
166
+ Attaches hooks based on the feature_locations configuration:
167
+ - transformer_blocks: Main transformer blocks (indexed from 0 to 18)
168
+ - single_transformer_blocks: Single transformer blocks (indexed from 0 to 37)
169
+ """
170
+ for block_type, indices in self.feature_locations.items():
171
+ if block_type == "transformer_blocks":
172
+ for idx in indices:
173
+ if 0 <= idx < len(self.transformer_blocks):
174
+ self.transformer_blocks[idx].register_forward_hook(
175
+ self._get_hook(f"{block_type}_{idx}")
176
+ )
177
+ elif block_type == "single_transformer_blocks":
178
+ for idx in indices:
179
+ if 0 <= idx < len(self.single_transformer_blocks):
180
+ self.single_transformer_blocks[idx].register_forward_hook(
181
+ self._get_hook(f"{block_type}_{idx}")
182
+ )
183
+
184
+ @torch.no_grad()
185
+ def forward(
186
+ self,
187
+ hidden_states: torch.Tensor,
188
+ encoder_hidden_states: torch.Tensor = None,
189
+ concept_hidden_states: torch.Tensor = None,
190
+ pooled_projections: torch.Tensor = None,
191
+ pooled_concept_embeds: torch.Tensor = None,
192
+ timestep: torch.LongTensor = None,
193
+ img_ids: torch.Tensor = None,
194
+ txt_ids: torch.Tensor = None,
195
+ concept_ids: torch.Tensor = None,
196
+ guidance: torch.Tensor = None,
197
+ joint_attention_kwargs: Optional[Dict[str, Any]] = None,
198
+ concept_attention_kwargs: Optional[Dict[str, Any]] = None,
199
+ controlnet_block_samples=None,
200
+ controlnet_single_block_samples=None,
201
+ return_dict: bool = True,
202
+ controlnet_blocks_repeat: bool = False,
203
+ ) -> Union[torch.Tensor, FluxTransformer2DOutputWithConceptAttention]:
204
+ """
205
+ The [`FluxTransformer2DModel`] forward method.
206
+
207
+ Args:
208
+ hidden_states (`torch.Tensor` of shape `(batch_size, image_sequence_length, in_channels)`):
209
+ Input `hidden_states`.
210
+ encoder_hidden_states (`torch.Tensor` of shape `(batch_size, text_sequence_length, joint_attention_dim)`):
211
+ Conditional embeddings (embeddings computed from the input conditions such as prompts) to use.
212
+ pooled_projections (`torch.Tensor` of shape `(batch_size, projection_dim)`): Embeddings projected
213
+ from the embeddings of input conditions.
214
+ timestep ( `torch.LongTensor`):
215
+ Used to indicate denoising step.
216
+ block_controlnet_hidden_states: (`list` of `torch.Tensor`):
217
+ A list of tensors that if specified are added to the residuals of transformer blocks.
218
+ joint_attention_kwargs (`dict`, *optional*):
219
+ A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under
220
+ `self.processor` in
221
+ [diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py).
222
+ concept_attention_kwargs (`dict`, *optional*):
223
+ A kwargs dictionary with parameters for Concept Attention.
224
+ return_dict (`bool`, *optional*, defaults to `True`):
225
+ Whether or not to return a [`~models.transformer_2d.Transformer2DModelOutput`] instead of a plain
226
+ tuple.
227
+
228
+ Returns:
229
+ If `return_dict` is True, an [`~models.transformer_2d.Transformer2DModelOutput`] is returned, otherwise a
230
+ `tuple` where the first element is the sample tensor.
231
+ """
232
+ if joint_attention_kwargs is not None:
233
+ joint_attention_kwargs = joint_attention_kwargs.copy()
234
+ lora_scale = joint_attention_kwargs.pop("scale", 1.0)
235
+ else:
236
+ lora_scale = 1.0
237
+
238
+ if USE_PEFT_BACKEND:
239
+ # weight the lora layers by setting `lora_scale` for each PEFT layer
240
+ scale_lora_layers(self, lora_scale)
241
+ else:
242
+ if joint_attention_kwargs is not None and joint_attention_kwargs.get("scale", None) is not None:
243
+ logger.warning(
244
+ "Passing `scale` via `joint_attention_kwargs` when not using the PEFT backend is ineffective."
245
+ )
246
+
247
+ hidden_states = self.x_embedder(hidden_states)
248
+
249
+ timestep = timestep.to(hidden_states.dtype) * 1000
250
+ if guidance is not None:
251
+ guidance = guidance.to(hidden_states.dtype) * 1000
252
+ else:
253
+ guidance = None
254
+
255
+ temb = (
256
+ self.time_text_embed(timestep, pooled_projections)
257
+ if guidance is None
258
+ else self.time_text_embed(timestep, guidance, pooled_projections)
259
+ )
260
+ encoder_hidden_states = self.context_embedder(encoder_hidden_states)
261
+
262
+ concept_temb = None
263
+ if pooled_concept_embeds is not None:
264
+ if guidance is None:
265
+ concept_temb = self.time_text_embed(timestep, pooled_concept_embeds)
266
+ else:
267
+ concept_temb = self.time_text_embed(timestep, guidance, pooled_concept_embeds)
268
+
269
+ # Apply the context embedder to the concept_hidden_states
270
+ if concept_hidden_states is not None:
271
+ concept_hidden_states = self.context_embedder(concept_hidden_states)
272
+
273
+ if txt_ids.ndim == 3:
274
+ logger.warning(
275
+ "Passing `txt_ids` 3d torch.Tensor is deprecated."
276
+ "Please remove the batch dimension and pass it as a 2d torch Tensor"
277
+ )
278
+ txt_ids = txt_ids[0]
279
+ if img_ids.ndim == 3:
280
+ logger.warning(
281
+ "Passing `img_ids` 3d torch.Tensor is deprecated."
282
+ "Please remove the batch dimension and pass it as a 2d torch Tensor"
283
+ )
284
+ img_ids = img_ids[0]
285
+
286
+ # Build rotary embeddings for different attention patterns:
287
+
288
+ # 1. Image-only rotary (for single blocks)
289
+ image_rotary_emb = self.pos_embed(img_ids)
290
+
291
+ # 2. Joint rotary for text+image (for dual blocks' vanilla attention)
292
+ # Dual blocks concatenate encoder+image queries → need joint rope
293
+ ids_joint = torch.cat((txt_ids, img_ids), dim=0) # [512 + 1024, 2]
294
+ rope_joint = self.pos_embed(ids_joint) # (cos, sin) with len=1536
295
+
296
+ # 3. Concept rotary for concept attention (concept + image sequence)
297
+ concept_image_ids = torch.cat((concept_ids, img_ids), dim=0)
298
+ concept_rotary_emb = self.pos_embed(concept_image_ids)
299
+
300
+ if joint_attention_kwargs is not None and "ip_adapter_image_embeds" in joint_attention_kwargs:
301
+ ip_adapter_image_embeds = joint_attention_kwargs.pop("ip_adapter_image_embeds")
302
+ ip_hidden_states = self.encoder_hid_proj(ip_adapter_image_embeds)
303
+ joint_attention_kwargs.update({"ip_hidden_states": ip_hidden_states})
304
+
305
+ # Initialize concept attention processing (collect raw maps only - no processing!)
306
+ all_concept_attention_maps = []
307
+
308
+ for index_block, block in enumerate(self.transformer_blocks):
309
+ if torch.is_grad_enabled() and self.gradient_checkpointing:
310
+ raise NotImplementedError("Gradient checkpointing is not implemented for concept attention.")
311
+ else:
312
+ # Prepare kwargs for dual block: pass joint rope via kwargs
313
+ # Remove concept_* from joint_attention_kwargs to avoid warnings
314
+ block_joint_kwargs = dict(joint_attention_kwargs or {})
315
+ block_joint_kwargs.pop("concept_hidden_states", None)
316
+ block_joint_kwargs.pop("concept_rotary_emb", None)
317
+
318
+ # Pass joint rope for vanilla attention (encoder+image queries)
319
+ block_joint_kwargs["q_rotary_emb"] = rope_joint
320
+ block_joint_kwargs["kv_rotary_emb"] = rope_joint
321
+
322
+ block_output = block(
323
+ hidden_states=hidden_states,
324
+ encoder_hidden_states=encoder_hidden_states,
325
+ concept_hidden_states=concept_hidden_states,
326
+ temb=temb,
327
+ concept_temb=concept_temb,
328
+ image_rotary_emb=None, # Use q/kv_rotary_emb from kwargs instead
329
+ concept_rotary_emb=concept_rotary_emb,
330
+ joint_attention_kwargs=block_joint_kwargs,
331
+ concept_attention_kwargs=concept_attention_kwargs,
332
+ )
333
+
334
+ encoder_hidden_states, hidden_states, concept_hidden_states, current_concept_attention_maps = block_output
335
+
336
+ # Collect raw attention maps only (no processing here!)
337
+ if (current_concept_attention_maps is not None and
338
+ concept_attention_kwargs is not None and
339
+ index_block in concept_attention_kwargs["layers"]):
340
+ all_concept_attention_maps.append(current_concept_attention_maps)
341
+
342
+ # controlnet residual
343
+ if controlnet_block_samples is not None:
344
+ interval_control = len(self.transformer_blocks) / len(controlnet_block_samples)
345
+ interval_control = int(np.ceil(interval_control))
346
+ # For Xlabs ControlNet.
347
+ if controlnet_blocks_repeat:
348
+ hidden_states = (
349
+ hidden_states + controlnet_block_samples[index_block % len(controlnet_block_samples)]
350
+ )
351
+ else:
352
+ hidden_states = hidden_states + controlnet_block_samples[index_block // interval_control]
353
+
354
+ if concept_hidden_states is not None:
355
+ concept_hidden_states = concept_hidden_states.cpu()
356
+
357
+ # Single-stream blocks: pass encoder separately
358
+ # IMPORTANT: Single blocks also concatenate encoder+hidden internally,
359
+ # so they need the joint rope (1536 = 512 text + 1024 image), not image-only!
360
+
361
+ for index_block, block in enumerate(self.single_transformer_blocks):
362
+ if torch.is_grad_enabled() and self.gradient_checkpointing:
363
+ encoder_hidden_states, hidden_states = self._gradient_checkpointing_func(
364
+ block,
365
+ hidden_states,
366
+ encoder_hidden_states,
367
+ temb,
368
+ rope_joint, # Use joint rope, not image_rotary_emb!
369
+ )
370
+ else:
371
+ # Single blocks return (encoder_hidden_states, hidden_states) tuple
372
+ encoder_hidden_states, hidden_states = block(
373
+ hidden_states=hidden_states,
374
+ encoder_hidden_states=encoder_hidden_states,
375
+ temb=temb,
376
+ image_rotary_emb=rope_joint, # Use joint rope, not image_rotary_emb!
377
+ )
378
+ # controlnet residual (no slicing needed - output is image-only)
379
+ if controlnet_single_block_samples is not None:
380
+ interval_control = len(self.single_transformer_blocks) / len(controlnet_single_block_samples)
381
+ interval_control = int(np.ceil(interval_control))
382
+ hidden_states = hidden_states + controlnet_single_block_samples[index_block // interval_control]
383
+
384
+ hidden_states = self.norm_out(hidden_states, temb)
385
+ output = self.proj_out(hidden_states)
386
+
387
+ # Process collected attention maps (pass through dictionaries to pipeline)
388
+ concept_attention_maps = None
389
+ if all_concept_attention_maps:
390
+ # Return the collected dictionaries as-is for pipeline postprocessing
391
+ concept_attention_maps = all_concept_attention_maps
392
+
393
+ if USE_PEFT_BACKEND:
394
+ # remove `lora_scale` from each PEFT layer
395
+ unscale_lora_layers(self, lora_scale)
396
+
397
+ if not return_dict:
398
+ return (output, concept_attention_maps)
399
+
400
+ return FluxTransformer2DOutputWithConceptAttention(sample=output, concept_attention_maps=concept_attention_maps)
flux_concept_attention/flux_with_concept_attention_pipeline.py ADDED
@@ -0,0 +1,1461 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Here we make various wrapper classes for the FluxPipeline from diffusers
3
+ to add the concept attention functionality.
4
+
5
+ We opt for a wrapper functionality
6
+ """
7
+ import inspect
8
+
9
+ import torch
10
+ import numpy as np
11
+ from typing import List, Union, Optional, Dict, Any, Callable
12
+ import PIL.Image
13
+ import einops
14
+ import matplotlib.pyplot as plt
15
+
16
+ from diffusers import DiffusionPipeline
17
+ from diffusers.image_processor import PipelineImageInput
18
+ from diffusers.pipelines.flux.pipeline_flux import retrieve_timesteps, calculate_shift
19
+ from diffusers.utils import is_torch_xla_available, BaseOutput, logging, USE_PEFT_BACKEND, \
20
+ scale_lora_layers, unscale_lora_layers
21
+
22
+ from diffusers.utils.torch_utils import randn_tensor
23
+
24
+ from diffusers.image_processor import PipelineImageInput, VaeImageProcessor
25
+ from diffusers.loaders import FluxIPAdapterMixin, FluxLoraLoaderMixin, FromSingleFileMixin, TextualInversionLoaderMixin
26
+ from diffusers.models.autoencoders import AutoencoderKL
27
+ from diffusers.models.transformers import FluxTransformer2DModel
28
+ from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
29
+
30
+ from transformers import (
31
+ CLIPImageProcessor,
32
+ CLIPTextModel,
33
+ CLIPTokenizer,
34
+ CLIPVisionModelWithProjection,
35
+ T5EncoderModel,
36
+ T5TokenizerFast,
37
+ )
38
+
39
+ if is_torch_xla_available():
40
+ import torch_xla.core.xla_model as xm
41
+
42
+ XLA_AVAILABLE = True
43
+ else:
44
+ XLA_AVAILABLE = False
45
+
46
+
47
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
48
+
49
+
50
+ def retrieve_timesteps(
51
+ scheduler,
52
+ num_inference_steps: Optional[int] = None,
53
+ device: Optional[Union[str, torch.device]] = None,
54
+ timesteps: Optional[List[int]] = None,
55
+ sigmas: Optional[List[float]] = None,
56
+ **kwargs,
57
+ ):
58
+ """
59
+ Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles
60
+ custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`.
61
+
62
+ Args:
63
+ scheduler (`SchedulerMixin`):
64
+ The scheduler to get timesteps from.
65
+ num_inference_steps (`int`):
66
+ The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps`
67
+ must be `None`.
68
+ device (`str` or `torch.device`, *optional*):
69
+ The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
70
+ timesteps (`List[int]`, *optional*):
71
+ Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed,
72
+ `num_inference_steps` and `sigmas` must be `None`.
73
+ sigmas (`List[float]`, *optional*):
74
+ Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed,
75
+ `num_inference_steps` and `timesteps` must be `None`.
76
+
77
+ Returns:
78
+ `Tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the
79
+ second element is the number of inference steps.
80
+ """
81
+ if timesteps is not None and sigmas is not None:
82
+ raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values")
83
+ if timesteps is not None:
84
+ accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
85
+ if not accepts_timesteps:
86
+ raise ValueError(
87
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
88
+ f" timestep schedules. Please check whether you are using the correct scheduler."
89
+ )
90
+ scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
91
+ timesteps = scheduler.timesteps
92
+ num_inference_steps = len(timesteps)
93
+ elif sigmas is not None:
94
+ accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
95
+ if not accept_sigmas:
96
+ raise ValueError(
97
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
98
+ f" sigmas schedules. Please check whether you are using the correct scheduler."
99
+ )
100
+ scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
101
+ timesteps = scheduler.timesteps
102
+ num_inference_steps = len(timesteps)
103
+ else:
104
+ scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
105
+ timesteps = scheduler.timesteps
106
+ return timesteps, num_inference_steps
107
+
108
+
109
+ def retrieve_latents(
110
+ encoder_output: torch.Tensor, generator: Optional[torch.Generator] = None, sample_mode: str = "sample"
111
+ ):
112
+ if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
113
+ return encoder_output.latent_dist.sample(generator)
114
+ elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
115
+ return encoder_output.latent_dist.mode()
116
+ elif hasattr(encoder_output, "latents"):
117
+ return encoder_output.latents
118
+ else:
119
+ raise AttributeError("Could not access latents of provided encoder_output")
120
+
121
+
122
+ class FluxConceptAttentionOutput(BaseOutput):
123
+ """
124
+ Output class for the FluxPipeline with concept attention functionality.
125
+
126
+ Args:
127
+ images (`List[PIL.Image.Image]` or `np.ndarray`)
128
+ The generated images.
129
+ concept_attention_maps (`List[PIL.Image.Image]` or `np.ndarray`)
130
+ The concept attention maps.
131
+ """
132
+ images: Union[List[PIL.Image.Image], np.ndarray]
133
+ concept_attention_maps: Union[List[PIL.Image.Image], np.ndarray]
134
+
135
+ class FluxWithConceptAttentionPipeline(
136
+ DiffusionPipeline,
137
+ FluxLoraLoaderMixin,
138
+ FromSingleFileMixin,
139
+ TextualInversionLoaderMixin,
140
+ FluxIPAdapterMixin,
141
+ ):
142
+ r"""
143
+ The Flux pipeline for text-to-image generation with added Concept Attention.
144
+
145
+ Reference: https://blackforestlabs.ai/announcing-black-forest-labs/
146
+
147
+ Args:
148
+ transformer ([`FluxTransformer2DModel`]):
149
+ Conditional Transformer (MMDiT) architecture to denoise the encoded image latents.
150
+ scheduler ([`FlowMatchEulerDiscreteScheduler`]):
151
+ A scheduler to be used in combination with `transformer` to denoise the encoded image latents.
152
+ vae ([`AutoencoderKL`]):
153
+ Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations.
154
+ text_encoder ([`CLIPTextModel`]):
155
+ [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically
156
+ the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant.
157
+ text_encoder_2 ([`T5EncoderModel`]):
158
+ [T5](https://huggingface.co/docs/transformers/en/model_doc/t5#transformers.T5EncoderModel), specifically
159
+ the [google/t5-v1_1-xxl](https://huggingface.co/google/t5-v1_1-xxl) variant.
160
+ tokenizer (`CLIPTokenizer`):
161
+ Tokenizer of class
162
+ [CLIPTokenizer](https://huggingface.co/docs/transformers/en/model_doc/clip#transformers.CLIPTokenizer).
163
+ tokenizer_2 (`T5TokenizerFast`):
164
+ Second Tokenizer of class
165
+ [T5TokenizerFast](https://huggingface.co/docs/transformers/en/model_doc/t5#transformers.T5TokenizerFast).
166
+ """
167
+
168
+ model_cpu_offload_seq = "text_encoder->text_encoder_2->image_encoder->transformer->vae"
169
+ _optional_components = ["image_encoder", "feature_extractor"]
170
+ _callback_tensor_inputs = ["latents", "prompt_embeds"]
171
+
172
+ def __init__(
173
+ self,
174
+ scheduler: FlowMatchEulerDiscreteScheduler,
175
+ vae: AutoencoderKL,
176
+ text_encoder: CLIPTextModel,
177
+ tokenizer: CLIPTokenizer,
178
+ text_encoder_2: T5EncoderModel,
179
+ tokenizer_2: T5TokenizerFast,
180
+ transformer: FluxTransformer2DModel,
181
+ image_encoder: CLIPVisionModelWithProjection = None,
182
+ feature_extractor: CLIPImageProcessor = None,
183
+ ):
184
+ super().__init__()
185
+
186
+ self.register_modules(
187
+ vae=vae,
188
+ text_encoder=text_encoder,
189
+ text_encoder_2=text_encoder_2,
190
+ tokenizer=tokenizer,
191
+ tokenizer_2=tokenizer_2,
192
+ transformer=transformer,
193
+ scheduler=scheduler,
194
+ image_encoder=image_encoder,
195
+ feature_extractor=feature_extractor,
196
+ )
197
+ self.vae_scale_factor = (
198
+ 2 ** (len(self.vae.config.block_out_channels)) if hasattr(self, "vae") and self.vae is not None else 16
199
+ )
200
+ self.latent_channels = self.vae.config.latent_channels if getattr(self, "vae", None) else 16
201
+ self.image_processor = VaeImageProcessor(
202
+ vae_scale_factor=self.vae_scale_factor * 2, vae_latent_channels=self.latent_channels
203
+ )
204
+ self.tokenizer_max_length = (
205
+ self.tokenizer.model_max_length if hasattr(self, "tokenizer") and self.tokenizer is not None else 77
206
+ )
207
+ self.default_sample_size = 64
208
+ self.latent_width = self.default_sample_size
209
+ self.latent_height = self.default_sample_size
210
+
211
+ def _get_t5_prompt_embeds(
212
+ self,
213
+ prompt: Union[str, List[str]] = None,
214
+ num_images_per_prompt: int = 1,
215
+ max_sequence_length: int = 512,
216
+ device: Optional[torch.device] = None,
217
+ dtype: Optional[torch.dtype] = None,
218
+ ):
219
+ device = device or self._execution_device
220
+ dtype = dtype or self.text_encoder.dtype
221
+
222
+ prompt = [prompt] if isinstance(prompt, str) else prompt
223
+ batch_size = len(prompt)
224
+
225
+ if isinstance(self, TextualInversionLoaderMixin):
226
+ prompt = self.maybe_convert_prompt(prompt, self.tokenizer_2)
227
+
228
+ text_inputs = self.tokenizer_2(
229
+ prompt,
230
+ padding="max_length",
231
+ max_length=max_sequence_length,
232
+ truncation=True,
233
+ return_length=False,
234
+ return_overflowing_tokens=False,
235
+ return_tensors="pt",
236
+ )
237
+ text_input_ids = text_inputs.input_ids
238
+ untruncated_ids = self.tokenizer_2(prompt, padding="longest", return_tensors="pt").input_ids
239
+
240
+ if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids):
241
+ removed_text = self.tokenizer_2.batch_decode(untruncated_ids[:, self.tokenizer_max_length - 1 : -1])
242
+ logger.warning(
243
+ "The following part of your input was truncated because `max_sequence_length` is set to "
244
+ f" {max_sequence_length} tokens: {removed_text}"
245
+ )
246
+
247
+ prompt_embeds = self.text_encoder_2(text_input_ids.to(device), output_hidden_states=False)[0]
248
+
249
+ dtype = self.text_encoder_2.dtype
250
+ prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
251
+
252
+ _, seq_len, _ = prompt_embeds.shape
253
+
254
+ # duplicate text embeddings and attention mask for each generation per prompt, using mps friendly method
255
+ prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)
256
+ prompt_embeds = prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)
257
+
258
+ return prompt_embeds
259
+
260
+ def _get_clip_prompt_embeds(
261
+ self,
262
+ prompt: Union[str, List[str]],
263
+ num_images_per_prompt: int = 1,
264
+ device: Optional[torch.device] = None,
265
+ ):
266
+ device = device or self._execution_device
267
+
268
+ prompt = [prompt] if isinstance(prompt, str) else prompt
269
+ batch_size = len(prompt)
270
+
271
+ if isinstance(self, TextualInversionLoaderMixin):
272
+ prompt = self.maybe_convert_prompt(prompt, self.tokenizer)
273
+
274
+ text_inputs = self.tokenizer(
275
+ prompt,
276
+ padding="max_length",
277
+ max_length=self.tokenizer_max_length,
278
+ truncation=True,
279
+ return_overflowing_tokens=False,
280
+ return_length=False,
281
+ return_tensors="pt",
282
+ )
283
+
284
+ text_input_ids = text_inputs.input_ids
285
+ untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids
286
+ if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids):
287
+ removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer_max_length - 1 : -1])
288
+ logger.warning(
289
+ "The following part of your input was truncated because CLIP can only handle sequences up to"
290
+ f" {self.tokenizer_max_length} tokens: {removed_text}"
291
+ )
292
+ prompt_embeds = self.text_encoder(text_input_ids.to(device), output_hidden_states=False)
293
+
294
+ # Use pooled output of CLIPTextModel
295
+ prompt_embeds = prompt_embeds.pooler_output
296
+ prompt_embeds = prompt_embeds.to(dtype=self.text_encoder.dtype, device=device)
297
+
298
+ # duplicate text embeddings for each generation per prompt, using mps friendly method
299
+ prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt)
300
+ prompt_embeds = prompt_embeds.view(batch_size * num_images_per_prompt, -1)
301
+
302
+ return prompt_embeds
303
+
304
+ def encode_prompt(
305
+ self,
306
+ prompt: Union[str, List[str]],
307
+ prompt_2: Union[str, List[str]],
308
+ device: Optional[torch.device] = None,
309
+ num_images_per_prompt: int = 1,
310
+ prompt_embeds: Optional[torch.FloatTensor] = None,
311
+ pooled_prompt_embeds: Optional[torch.FloatTensor] = None,
312
+ max_sequence_length: int = 512,
313
+ lora_scale: Optional[float] = None,
314
+ ):
315
+ r"""
316
+
317
+ Args:
318
+ prompt (`str` or `List[str]`, *optional*):
319
+ prompt to be encoded
320
+ prompt_2 (`str` or `List[str]`, *optional*):
321
+ The prompt or prompts to be sent to the `tokenizer_2` and `text_encoder_2`. If not defined, `prompt` is
322
+ used in all text-encoders
323
+ device: (`torch.device`):
324
+ torch device
325
+ num_images_per_prompt (`int`):
326
+ number of images that should be generated per prompt
327
+ prompt_embeds (`torch.FloatTensor`, *optional*):
328
+ Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
329
+ provided, text embeddings will be generated from `prompt` input argument.
330
+ pooled_prompt_embeds (`torch.FloatTensor`, *optional*):
331
+ Pre-generated pooled text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting.
332
+ If not provided, pooled text embeddings will be generated from `prompt` input argument.
333
+ lora_scale (`float`, *optional*):
334
+ A lora scale that will be applied to all LoRA layers of the text encoder if LoRA layers are loaded.
335
+ """
336
+ device = device or self._execution_device
337
+
338
+ # set lora scale so that monkey patched LoRA
339
+ # function of text encoder can correctly access it
340
+ if lora_scale is not None and isinstance(self, FluxLoraLoaderMixin):
341
+ self._lora_scale = lora_scale
342
+
343
+ # dynamically adjust the LoRA scale
344
+ if self.text_encoder is not None and USE_PEFT_BACKEND:
345
+ scale_lora_layers(self.text_encoder, lora_scale)
346
+ if self.text_encoder_2 is not None and USE_PEFT_BACKEND:
347
+ scale_lora_layers(self.text_encoder_2, lora_scale)
348
+
349
+ prompt = [prompt] if isinstance(prompt, str) else prompt
350
+
351
+ if prompt_embeds is None:
352
+ prompt_2 = prompt_2 or prompt
353
+ prompt_2 = [prompt_2] if isinstance(prompt_2, str) else prompt_2
354
+
355
+ # We only use the pooled prompt output from the CLIPTextModel
356
+ pooled_prompt_embeds = self._get_clip_prompt_embeds(
357
+ prompt=prompt,
358
+ device=device,
359
+ num_images_per_prompt=num_images_per_prompt,
360
+ )
361
+ prompt_embeds = self._get_t5_prompt_embeds(
362
+ prompt=prompt_2,
363
+ num_images_per_prompt=num_images_per_prompt,
364
+ max_sequence_length=max_sequence_length,
365
+ device=device,
366
+ )
367
+
368
+ if self.text_encoder is not None:
369
+ if isinstance(self, FluxLoraLoaderMixin) and USE_PEFT_BACKEND:
370
+ # Retrieve the original scale by scaling back the LoRA layers
371
+ unscale_lora_layers(self.text_encoder, lora_scale)
372
+
373
+ if self.text_encoder_2 is not None:
374
+ if isinstance(self, FluxLoraLoaderMixin) and USE_PEFT_BACKEND:
375
+ # Retrieve the original scale by scaling back the LoRA layers
376
+ unscale_lora_layers(self.text_encoder_2, lora_scale)
377
+
378
+ dtype = self.text_encoder.dtype if self.text_encoder is not None else self.transformer.dtype
379
+ text_ids = torch.zeros(prompt_embeds.shape[1], 3).to(device=device, dtype=dtype)
380
+
381
+ return prompt_embeds, pooled_prompt_embeds, text_ids
382
+
383
+ def encode_concepts(self, concepts: List[str], device: Optional[torch.device] = None):
384
+ """
385
+ Encodes our concept vectors using the T5 Encoder.
386
+ """
387
+ """
388
+ # Utils for concept encoding
389
+ def embed_concepts(
390
+ clip,
391
+ t5,
392
+ concepts: list[str],
393
+ batch_size=1
394
+ ):
395
+ # Code pulled from concept_attention.flux/sampling.py: prepare()
396
+ # Embed each concept separately
397
+ concept_embeddings = []
398
+ for concept in concepts:
399
+ concept_embedding = t5(concept)
400
+ # Pull out the first token
401
+ token_embedding = concept_embedding[0, 0, :] # First token of first prompt
402
+ concept_embeddings.append(token_embedding)
403
+ concept_embeddings = torch.stack(concept_embeddings).unsqueeze(0)
404
+ # Add filler tokens of zeros
405
+ concept_ids = torch.zeros(batch_size, concept_embeddings.shape[1], 3)
406
+
407
+ # Embed the concepts to a clip vector
408
+ prompt = " ".join(concepts)
409
+ vec = clip(prompt)
410
+ vec = torch.zeros_like(vec).to(vec.device)
411
+
412
+ return concept_embeddings, concept_ids, vec
413
+ """
414
+
415
+ concept_embeds = self._get_t5_prompt_embeds(
416
+ prompt=concepts,
417
+ num_images_per_prompt=1,
418
+ max_sequence_length=64,
419
+ device=device,
420
+ )
421
+ # Pull out the first token of each embedded concept to get the concept embeddings
422
+ concept_embeds = concept_embeds[:, 0, :]
423
+ concept_embeds = concept_embeds.unsqueeze(0)
424
+ # Make the CLIP vector for the concepts
425
+ clip_vec = self._get_clip_prompt_embeds(
426
+ prompt=" ".join(concepts),
427
+ num_images_per_prompt=1,
428
+ device=device,
429
+ )
430
+ # # Set the vec to zero
431
+ # clip_vec = torch.zeros_like(clip_vec).to(clip_vec.device)
432
+ # # Add filler tokens of zeros
433
+ concept_ids = torch.zeros(concept_embeds.shape[1], 3).to(device=device, dtype=concept_embeds.dtype)
434
+
435
+ return concept_embeds, clip_vec, concept_ids
436
+
437
+ def encode_image(self, image, device, num_images_per_prompt):
438
+ dtype = next(self.image_encoder.parameters()).dtype
439
+
440
+ if not isinstance(image, torch.Tensor):
441
+ image = self.feature_extractor(image, return_tensors="pt").pixel_values
442
+
443
+ image = image.to(device=device, dtype=dtype)
444
+ image_embeds = self.image_encoder(image).image_embeds
445
+ image_embeds = image_embeds.repeat_interleave(num_images_per_prompt, dim=0)
446
+ return image_embeds
447
+
448
+ def prepare_ip_adapter_image_embeds(
449
+ self, ip_adapter_image, ip_adapter_image_embeds, device, num_images_per_prompt
450
+ ):
451
+ image_embeds = []
452
+ if ip_adapter_image_embeds is None:
453
+ if not isinstance(ip_adapter_image, list):
454
+ ip_adapter_image = [ip_adapter_image]
455
+
456
+ if len(ip_adapter_image) != len(self.transformer.encoder_hid_proj.image_projection_layers):
457
+ raise ValueError(
458
+ f"`ip_adapter_image` must have same length as the number of IP Adapters. Got {len(ip_adapter_image)} images and {len(self.transformer.encoder_hid_proj.image_projection_layers)} IP Adapters."
459
+ )
460
+
461
+ for single_ip_adapter_image, image_proj_layer in zip(
462
+ ip_adapter_image, self.transformer.encoder_hid_proj.image_projection_layers
463
+ ):
464
+ single_image_embeds = self.encode_image(single_ip_adapter_image, device, 1)
465
+
466
+ image_embeds.append(single_image_embeds[None, :])
467
+ else:
468
+ for single_image_embeds in ip_adapter_image_embeds:
469
+ image_embeds.append(single_image_embeds)
470
+
471
+ ip_adapter_image_embeds = []
472
+ for i, single_image_embeds in enumerate(image_embeds):
473
+ single_image_embeds = torch.cat([single_image_embeds] * num_images_per_prompt, dim=0)
474
+ single_image_embeds = single_image_embeds.to(device=device)
475
+ ip_adapter_image_embeds.append(single_image_embeds)
476
+
477
+ return ip_adapter_image_embeds
478
+
479
+ def check_inputs(
480
+ self,
481
+ prompt,
482
+ prompt_2,
483
+ height,
484
+ width,
485
+ negative_prompt=None,
486
+ negative_prompt_2=None,
487
+ prompt_embeds=None,
488
+ negative_prompt_embeds=None,
489
+ pooled_prompt_embeds=None,
490
+ negative_pooled_prompt_embeds=None,
491
+ callback_on_step_end_tensor_inputs=None,
492
+ max_sequence_length=None,
493
+ ):
494
+ if height % (self.vae_scale_factor * 2) != 0 or width % (self.vae_scale_factor * 2) != 0:
495
+ logger.warning(
496
+ f"`height` and `width` have to be divisible by {self.vae_scale_factor * 2} but are {height} and {width}. Dimensions will be resized accordingly"
497
+ )
498
+
499
+ if callback_on_step_end_tensor_inputs is not None and not all(
500
+ k in self._callback_tensor_inputs for k in callback_on_step_end_tensor_inputs
501
+ ):
502
+ raise ValueError(
503
+ f"`callback_on_step_end_tensor_inputs` has to be in {self._callback_tensor_inputs}, but found {[k for k in callback_on_step_end_tensor_inputs if k not in self._callback_tensor_inputs]}"
504
+ )
505
+
506
+ if prompt is not None and prompt_embeds is not None:
507
+ raise ValueError(
508
+ f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to"
509
+ " only forward one of the two."
510
+ )
511
+ elif prompt_2 is not None and prompt_embeds is not None:
512
+ raise ValueError(
513
+ f"Cannot forward both `prompt_2`: {prompt_2} and `prompt_embeds`: {prompt_embeds}. Please make sure to"
514
+ " only forward one of the two."
515
+ )
516
+ elif prompt is None and prompt_embeds is None:
517
+ raise ValueError(
518
+ "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined."
519
+ )
520
+ elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
521
+ raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
522
+ elif prompt_2 is not None and (not isinstance(prompt_2, str) and not isinstance(prompt_2, list)):
523
+ raise ValueError(f"`prompt_2` has to be of type `str` or `list` but is {type(prompt_2)}")
524
+
525
+ if negative_prompt is not None and negative_prompt_embeds is not None:
526
+ raise ValueError(
527
+ f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:"
528
+ f" {negative_prompt_embeds}. Please make sure to only forward one of the two."
529
+ )
530
+ elif negative_prompt_2 is not None and negative_prompt_embeds is not None:
531
+ raise ValueError(
532
+ f"Cannot forward both `negative_prompt_2`: {negative_prompt_2} and `negative_prompt_embeds`:"
533
+ f" {negative_prompt_embeds}. Please make sure to only forward one of the two."
534
+ )
535
+
536
+ if prompt_embeds is not None and negative_prompt_embeds is not None:
537
+ if prompt_embeds.shape != negative_prompt_embeds.shape:
538
+ raise ValueError(
539
+ "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but"
540
+ f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`"
541
+ f" {negative_prompt_embeds.shape}."
542
+ )
543
+
544
+ if prompt_embeds is not None and pooled_prompt_embeds is None:
545
+ raise ValueError(
546
+ "If `prompt_embeds` are provided, `pooled_prompt_embeds` also have to be passed. Make sure to generate `pooled_prompt_embeds` from the same text encoder that was used to generate `prompt_embeds`."
547
+ )
548
+ if negative_prompt_embeds is not None and negative_pooled_prompt_embeds is None:
549
+ raise ValueError(
550
+ "If `negative_prompt_embeds` are provided, `negative_pooled_prompt_embeds` also have to be passed. Make sure to generate `negative_pooled_prompt_embeds` from the same text encoder that was used to generate `negative_prompt_embeds`."
551
+ )
552
+
553
+ if max_sequence_length is not None and max_sequence_length > 512:
554
+ raise ValueError(f"`max_sequence_length` cannot be greater than 512 but is {max_sequence_length}")
555
+
556
+ @staticmethod
557
+ def _prepare_latent_image_ids(batch_size, height, width, device, dtype):
558
+ latent_image_ids = torch.zeros(height // 2, width // 2, 3)
559
+ latent_image_ids[..., 1] = latent_image_ids[..., 1] + torch.arange(height // 2)[:, None]
560
+ latent_image_ids[..., 2] = latent_image_ids[..., 2] + torch.arange(width // 2)[None, :]
561
+
562
+ latent_image_id_height, latent_image_id_width, latent_image_id_channels = latent_image_ids.shape
563
+
564
+ latent_image_ids = latent_image_ids.reshape(
565
+ latent_image_id_height * latent_image_id_width, latent_image_id_channels
566
+ )
567
+
568
+ return latent_image_ids.to(device=device, dtype=dtype)
569
+
570
+ @staticmethod
571
+ def _pack_latents(latents, batch_size, num_channels_latents, height, width):
572
+ latents = latents.view(batch_size, num_channels_latents, height // 2, 2, width // 2, 2)
573
+ latents = latents.permute(0, 2, 4, 1, 3, 5)
574
+ latents = latents.reshape(batch_size, (height // 2) * (width // 2), num_channels_latents * 4)
575
+
576
+ return latents
577
+
578
+ @staticmethod
579
+ def _unpack_latents(latents, height, width, vae_scale_factor):
580
+ batch_size, num_patches, channels = latents.shape
581
+
582
+ height = height // vae_scale_factor
583
+ width = width // vae_scale_factor
584
+
585
+ latents = latents.view(batch_size, height, width, channels // 4, 2, 2)
586
+ latents = latents.permute(0, 3, 1, 4, 2, 5)
587
+
588
+ latents = latents.reshape(batch_size, channels // (2 * 2), height * 2, width * 2)
589
+
590
+ return latents
591
+
592
+ def enable_vae_slicing(self):
593
+ r"""
594
+ Enable sliced VAE decoding. When this option is enabled, the VAE will split the input tensor in slices to
595
+ compute decoding in several steps. This is useful to save some memory and allow larger batch sizes.
596
+ """
597
+ self.vae.enable_slicing()
598
+
599
+ def disable_vae_slicing(self):
600
+ r"""
601
+ Disable sliced VAE decoding. If `enable_vae_slicing` was previously enabled, this method will go back to
602
+ computing decoding in one step.
603
+ """
604
+ self.vae.disable_slicing()
605
+
606
+ def enable_vae_tiling(self):
607
+ r"""
608
+ Enable tiled VAE decoding. When this option is enabled, the VAE will split the input tensor into tiles to
609
+ compute decoding and encoding in several steps. This is useful for saving a large amount of memory and to allow
610
+ processing larger images.
611
+ """
612
+ self.vae.enable_tiling()
613
+
614
+ def disable_vae_tiling(self):
615
+ r"""
616
+ Disable tiled VAE decoding. If `enable_vae_tiling` was previously enabled, this method will go back to
617
+ computing decoding in one step.
618
+ """
619
+ self.vae.disable_tiling()
620
+
621
+ def _encode_vae_image(self, image: torch.Tensor, generator: torch.Generator):
622
+ if isinstance(generator, list):
623
+ image_latents = [
624
+ retrieve_latents(self.vae.encode(image[i: i + 1]), generator=generator[i])
625
+ for i in range(image.shape[0])
626
+ ]
627
+ image_latents = torch.cat(image_latents, dim=0)
628
+ else:
629
+ image_latents = retrieve_latents(self.vae.encode(image), generator=generator)
630
+
631
+ image_latents = (image_latents - self.vae.config.shift_factor) * self.vae.config.scaling_factor
632
+
633
+ return image_latents
634
+
635
+ def prepare_latents(
636
+ self,
637
+ image,
638
+ timestep,
639
+ batch_size,
640
+ num_channels_latents,
641
+ height,
642
+ width,
643
+ dtype,
644
+ device,
645
+ generator,
646
+ latents=None,
647
+ ):
648
+ height = 2 * (int(height) // self.vae_scale_factor)
649
+ width = 2 * (int(width) // self.vae_scale_factor)
650
+
651
+ shape = (batch_size, num_channels_latents, height, width)
652
+
653
+ if latents is not None:
654
+ latent_image_ids = self._prepare_latent_image_ids(batch_size, height, width, device, dtype)
655
+ return latents.to(device=device, dtype=dtype), latent_image_ids
656
+
657
+ if isinstance(generator, list) and len(generator) != batch_size:
658
+ raise ValueError(
659
+ f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
660
+ f" size of {batch_size}. Make sure the batch size matches the length of the generators."
661
+ )
662
+
663
+ if image is None:
664
+ latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
665
+ latents = self._pack_latents(latents, batch_size, num_channels_latents, height, width)
666
+
667
+ latent_image_ids = self._prepare_latent_image_ids(batch_size, height, width, device, dtype)
668
+
669
+ return latents, latent_image_ids
670
+
671
+ shape = (batch_size, num_channels_latents, height, width)
672
+ latent_image_ids = self._prepare_latent_image_ids(batch_size, height, width, device, dtype)
673
+ image = image.to(device=device, dtype=dtype)
674
+ if image.shape[1] != self.latent_channels:
675
+ image_latents = self._encode_vae_image(image=image, generator=generator)
676
+ else:
677
+ image_latents = image
678
+ if batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] == 0:
679
+ # expand init_latents for batch_size
680
+ additional_image_per_prompt = batch_size // image_latents.shape[0]
681
+ image_latents = torch.cat([image_latents] * additional_image_per_prompt, dim=0)
682
+ elif batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] != 0:
683
+ raise ValueError(
684
+ f"Cannot duplicate `image` of batch size {image_latents.shape[0]} to {batch_size} text prompts."
685
+ )
686
+ else:
687
+ image_latents = torch.cat([image_latents], dim=0)
688
+
689
+ noise = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
690
+ latents = self.scheduler.scale_noise(image_latents, timestep, noise)
691
+ latents = self._pack_latents(latents, batch_size, num_channels_latents, height, width)
692
+ return latents, latent_image_ids
693
+
694
+ @property
695
+ def guidance_scale(self):
696
+ return self._guidance_scale
697
+
698
+ @property
699
+ def joint_attention_kwargs(self):
700
+ return self._joint_attention_kwargs
701
+
702
+ @property
703
+ def num_timesteps(self):
704
+ return self._num_timesteps
705
+
706
+ @property
707
+ def interrupt(self):
708
+ return self._interrupt
709
+
710
+ @torch.no_grad()
711
+ def __call__(
712
+ self,
713
+ prompt: Union[str, List[str]] = None,
714
+ prompt_2: Optional[Union[str, List[str]]] = None,
715
+ negative_prompt: Union[str, List[str]] = None,
716
+ negative_prompt_2: Optional[Union[str, List[str]]] = None,
717
+ true_cfg_scale: float = 1.0,
718
+ height: Optional[int] = None,
719
+ width: Optional[int] = None,
720
+ image: PipelineImageInput = None,
721
+ timesteps: List[int] = None,
722
+ num_inference_steps: int = 28,
723
+ sigmas: Optional[List[float]] = None,
724
+ guidance_scale: float = 3.5,
725
+ num_images_per_prompt: Optional[int] = 1,
726
+ generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
727
+ latents: Optional[torch.FloatTensor] = None,
728
+ prompt_embeds: Optional[torch.FloatTensor] = None,
729
+ pooled_prompt_embeds: Optional[torch.FloatTensor] = None,
730
+ ip_adapter_image: Optional[PipelineImageInput] = None,
731
+ ip_adapter_image_embeds: Optional[List[torch.Tensor]] = None,
732
+ negative_ip_adapter_image: Optional[PipelineImageInput] = None,
733
+ negative_ip_adapter_image_embeds: Optional[List[torch.Tensor]] = None,
734
+ negative_prompt_embeds: Optional[torch.FloatTensor] = None,
735
+ negative_pooled_prompt_embeds: Optional[torch.FloatTensor] = None,
736
+ output_type: Optional[str] = "pil",
737
+ return_dict: bool = True,
738
+ joint_attention_kwargs: Optional[Dict[str, Any]] = None,
739
+ concept_attention_kwargs: Optional[Dict[str, Any]] = None,
740
+ callback_on_step_end: Optional[Callable[[int, int, Dict], None]] = None,
741
+ callback_on_step_end_tensor_inputs: List[str] = ["latents"],
742
+ max_sequence_length: int = 512,
743
+ ):
744
+ r"""
745
+ Function invoked when calling the pipeline for generation.
746
+
747
+ Args:
748
+ prompt (`str` or `List[str]`, *optional*):
749
+ The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`.
750
+ instead.
751
+ prompt_2 (`str` or `List[str]`, *optional*):
752
+ The prompt or prompts to be sent to `tokenizer_2` and `text_encoder_2`. If not defined, `prompt` is
753
+ will be used instead.
754
+ negative_prompt (`str` or `List[str]`, *optional*):
755
+ The prompt or prompts not to guide the image generation. If not defined, one has to pass
756
+ `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `true_cfg_scale` is
757
+ not greater than `1`).
758
+ negative_prompt_2 (`str` or `List[str]`, *optional*):
759
+ The prompt or prompts not to guide the image generation to be sent to `tokenizer_2` and
760
+ `text_encoder_2`. If not defined, `negative_prompt` is used in all the text-encoders.
761
+ true_cfg_scale (`float`, *optional*, defaults to 1.0):
762
+ When > 1.0 and a provided `negative_prompt`, enables true classifier-free guidance.
763
+ height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor):
764
+ The height in pixels of the generated image. This is set to 1024 by default for the best results.
765
+ width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor):
766
+ The width in pixels of the generated image. This is set to 1024 by default for the best results.
767
+ image (`torch.FloatTensor`, `PIL.Image.Image`, `np.ndarray`, `List[torch.FloatTensor]`, `List[PIL.Image.Image]`, or `List[np.ndarray]`, *optional*):
768
+ Input image for img2img generation. If provided, the pipeline will perform image-to-image generation
769
+ instead of text-to-image generation.
770
+ timesteps (`List[int]`, *optional*):
771
+ Custom timesteps to use for the denoising process with schedulers which support a `timesteps` argument in
772
+ their `set_timesteps` method. If not defined, the default behavior when `num_inference_steps` is passed
773
+ will be used.
774
+ num_inference_steps (`int`, *optional*, defaults to 50):
775
+ The number of denoising steps. More denoising steps usually lead to a higher quality image at the
776
+ expense of slower inference.
777
+ sigmas (`List[float]`, *optional*):
778
+ Custom sigmas to use for the denoising process with schedulers which support a `sigmas` argument in
779
+ their `set_timesteps` method. If not defined, the default behavior when `num_inference_steps` is passed
780
+ will be used.
781
+ guidance_scale (`float`, *optional*, defaults to 7.0):
782
+ Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598).
783
+ `guidance_scale` is defined as `w` of equation 2. of [Imagen
784
+ Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale >
785
+ 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`,
786
+ usually at the expense of lower image quality.
787
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
788
+ The number of images to generate per prompt.
789
+ generator (`torch.Generator` or `List[torch.Generator]`, *optional*):
790
+ One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)
791
+ to make generation deterministic.
792
+ latents (`torch.FloatTensor`, *optional*):
793
+ Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image
794
+ generation. Can be used to tweak the same generation with different prompts. If not provided, a latents
795
+ tensor will ge generated by sampling using the supplied random `generator`.
796
+ prompt_embeds (`torch.FloatTensor`, *optional*):
797
+ Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
798
+ provided, text embeddings will be generated from `prompt` input argument.
799
+ pooled_prompt_embeds (`torch.FloatTensor`, *optional*):
800
+ Pre-generated pooled text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting.
801
+ If not provided, pooled text embeddings will be generated from `prompt` input argument.
802
+ ip_adapter_image: (`PipelineImageInput`, *optional*): Optional image input to work with IP Adapters.
803
+ ip_adapter_image_embeds (`List[torch.Tensor]`, *optional*):
804
+ Pre-generated image embeddings for IP-Adapter. It should be a list of length same as number of
805
+ IP-adapters. Each element should be a tensor of shape `(batch_size, num_images, emb_dim)`. If not
806
+ provided, embeddings are computed from the `ip_adapter_image` input argument.
807
+ negative_ip_adapter_image:
808
+ (`PipelineImageInput`, *optional*): Optional image input to work with IP Adapters.
809
+ negative_ip_adapter_image_embeds (`List[torch.Tensor]`, *optional*):
810
+ Pre-generated image embeddings for IP-Adapter. It should be a list of length same as number of
811
+ IP-adapters. Each element should be a tensor of shape `(batch_size, num_images, emb_dim)`. If not
812
+ provided, embeddings are computed from the `ip_adapter_image` input argument.
813
+ negative_prompt_embeds (`torch.FloatTensor`, *optional*):
814
+ Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt
815
+ weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input
816
+ argument.
817
+ negative_pooled_prompt_embeds (`torch.FloatTensor`, *optional*):
818
+ Pre-generated negative pooled text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt
819
+ weighting. If not provided, pooled negative_prompt_embeds will be generated from `negative_prompt`
820
+ input argument.
821
+ output_type (`str`, *optional*, defaults to `"pil"`):
822
+ The output format of the generate image. Choose between
823
+ [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`.
824
+ return_dict (`bool`, *optional*, defaults to `True`):
825
+ Whether or not to return a [`~pipelines.flux.FluxPipelineOutput`] instead of a plain tuple.
826
+ joint_attention_kwargs (`dict`, *optional*):
827
+ A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under
828
+ `self.processor` in
829
+ [diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py).
830
+ callback_on_step_end (`Callable`, *optional*):
831
+ A function that calls at the end of each denoising steps during the inference. The function is called
832
+ with the following arguments: `callback_on_step_end(self: DiffusionPipeline, step: int, timestep: int,
833
+ callback_kwargs: Dict)`. `callback_kwargs` will include a list of all tensors as specified by
834
+ `callback_on_step_end_tensor_inputs`.
835
+ callback_on_step_end_tensor_inputs (`List`, *optional*):
836
+ The list of tensor inputs for the `callback_on_step_end` function. The tensors specified in the list
837
+ will be passed as `callback_kwargs` argument. You will only be able to include variables listed in the
838
+ `._callback_tensor_inputs` attribute of your pipeline class.
839
+ max_sequence_length (`int` defaults to 512): Maximum sequence length to use with the `prompt`.
840
+
841
+ Examples:
842
+
843
+ Returns:
844
+ [`~pipelines.flux.FluxPipelineOutput`] or `tuple`: [`~pipelines.flux.FluxPipelineOutput`] if `return_dict`
845
+ is True, otherwise a `tuple`. When returning a tuple, the first element is a list with the generated
846
+ images.
847
+ """
848
+ # Verify the concept kwargs inputs
849
+ if concept_attention_kwargs is not None:
850
+ assert "concepts" in concept_attention_kwargs, "Concepts must be passed in the concept_attention_kwargs"
851
+ assert isinstance(concept_attention_kwargs["concepts"], list), "Concepts must be a list of strings"
852
+ assert len(concept_attention_kwargs["concepts"]) > 0, "Concepts must not be an empty list"
853
+ assert "timesteps" in concept_attention_kwargs, "Timesteps must be passed in the concept_attention_kwargs"
854
+ assert isinstance(concept_attention_kwargs["timesteps"], list), "Timesteps must be a list of integers"
855
+ assert len(concept_attention_kwargs["timesteps"]) > 0, "Timesteps must not be an empty list"
856
+ assert "layers" in concept_attention_kwargs, "Layers must be passed in the concept_attention_kwargs"
857
+ assert isinstance(concept_attention_kwargs["layers"], list), "Layers must be a list of integers"
858
+ assert len(concept_attention_kwargs["layers"]) > 0, "Layers must not be an empty list"
859
+
860
+ height = height or self.default_sample_size * self.vae_scale_factor
861
+ width = width or self.default_sample_size * self.vae_scale_factor
862
+
863
+ init_image = None
864
+ if image is not None:
865
+ width, height = image.size
866
+ init_image = self.image_processor.preprocess(image, height=height, width=width)
867
+ init_image = init_image.to(dtype=torch.float32)
868
+
869
+ # 1. Check inputs. Raise error if not correct
870
+ self.check_inputs(
871
+ prompt,
872
+ prompt_2,
873
+ height,
874
+ width,
875
+ negative_prompt=negative_prompt,
876
+ negative_prompt_2=negative_prompt_2,
877
+ prompt_embeds=prompt_embeds,
878
+ negative_prompt_embeds=negative_prompt_embeds,
879
+ pooled_prompt_embeds=pooled_prompt_embeds,
880
+ negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
881
+ callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs,
882
+ max_sequence_length=max_sequence_length,
883
+ )
884
+
885
+ self._guidance_scale = guidance_scale
886
+ self._joint_attention_kwargs = joint_attention_kwargs
887
+ self._current_timestep = None
888
+ self._interrupt = False
889
+
890
+ # 2. Define call parameters
891
+ if prompt is not None and isinstance(prompt, str):
892
+ batch_size = 1
893
+ elif prompt is not None and isinstance(prompt, list):
894
+ batch_size = len(prompt)
895
+ else:
896
+ batch_size = prompt_embeds.shape[0]
897
+
898
+ device = self._execution_device
899
+
900
+ lora_scale = (
901
+ self.joint_attention_kwargs.get("scale", None) if self.joint_attention_kwargs is not None else None
902
+ )
903
+ has_neg_prompt = negative_prompt is not None or (
904
+ negative_prompt_embeds is not None and negative_pooled_prompt_embeds is not None
905
+ )
906
+ do_true_cfg = true_cfg_scale > 1 and has_neg_prompt
907
+ (
908
+ prompt_embeds,
909
+ pooled_prompt_embeds,
910
+ text_ids,
911
+ ) = self.encode_prompt(
912
+ prompt=prompt,
913
+ prompt_2=prompt_2,
914
+ prompt_embeds=prompt_embeds,
915
+ pooled_prompt_embeds=pooled_prompt_embeds,
916
+ device=device,
917
+ num_images_per_prompt=num_images_per_prompt,
918
+ max_sequence_length=max_sequence_length,
919
+ lora_scale=lora_scale,
920
+ )
921
+ if do_true_cfg:
922
+ (
923
+ negative_prompt_embeds,
924
+ negative_pooled_prompt_embeds,
925
+ _,
926
+ ) = self.encode_prompt(
927
+ prompt=negative_prompt,
928
+ prompt_2=negative_prompt_2,
929
+ prompt_embeds=negative_prompt_embeds,
930
+ pooled_prompt_embeds=negative_pooled_prompt_embeds,
931
+ device=device,
932
+ num_images_per_prompt=num_images_per_prompt,
933
+ max_sequence_length=max_sequence_length,
934
+ lora_scale=lora_scale,
935
+ )
936
+
937
+ # Embed concepts
938
+ concept_embeddings, pooled_concept_embeds, concept_ids = self.encode_concepts(
939
+ concept_attention_kwargs["concepts"],
940
+ device=device
941
+ )
942
+ # Add the concept embeddings to the concept_attention_kwargs
943
+ # if concept_attention_kwargs is not None:
944
+ # concept_attention_kwargs["concept_embeddings"] = concept_embeddings
945
+ # concept_attention_kwargs["concept_vec"] = concept_vec
946
+
947
+ # 4. Prepare timesteps
948
+ sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if timesteps is None else None
949
+ image_seq_len = (int(height) // self.vae_scale_factor // 2) * (int(width) // self.vae_scale_factor // 2)
950
+ mu = calculate_shift(
951
+ image_seq_len,
952
+ self.scheduler.config.base_image_seq_len,
953
+ self.scheduler.config.max_image_seq_len,
954
+ self.scheduler.config.base_shift,
955
+ self.scheduler.config.max_shift,
956
+ )
957
+ timesteps, num_inference_steps = retrieve_timesteps(
958
+ self.scheduler,
959
+ num_inference_steps,
960
+ device,
961
+ timesteps,
962
+ sigmas,
963
+ mu=mu,
964
+ )
965
+ num_warmup_steps = max(len(timesteps) - num_inference_steps * self.scheduler.order, 0)
966
+ self._num_timesteps = len(timesteps)
967
+
968
+ # 5. Prepare latent variables
969
+ num_channels_latents = self.transformer.config.in_channels // 4
970
+ latent_timestep = timesteps[:1].repeat(batch_size * num_images_per_prompt)
971
+ latents, latent_image_ids = self.prepare_latents(
972
+ init_image,
973
+ latent_timestep,
974
+ batch_size * num_images_per_prompt,
975
+ num_channels_latents,
976
+ height,
977
+ width,
978
+ prompt_embeds.dtype,
979
+ device,
980
+ generator,
981
+ latents,
982
+ )
983
+
984
+ # handle guidance
985
+ if self.transformer.config.guidance_embeds:
986
+ guidance = torch.full([1], guidance_scale, device=device, dtype=torch.float32)
987
+ guidance = guidance.expand(latents.shape[0])
988
+ else:
989
+ guidance = None
990
+
991
+ if (ip_adapter_image is not None or ip_adapter_image_embeds is not None) and (
992
+ negative_ip_adapter_image is None and negative_ip_adapter_image_embeds is None
993
+ ):
994
+ negative_ip_adapter_image = np.zeros((width, height, 3), dtype=np.uint8)
995
+ elif (ip_adapter_image is None and ip_adapter_image_embeds is None) and (
996
+ negative_ip_adapter_image is not None or negative_ip_adapter_image_embeds is not None
997
+ ):
998
+ ip_adapter_image = np.zeros((width, height, 3), dtype=np.uint8)
999
+
1000
+ if self.joint_attention_kwargs is None:
1001
+ self._joint_attention_kwargs = {}
1002
+
1003
+ image_embeds = None
1004
+ negative_image_embeds = None
1005
+ if ip_adapter_image is not None or ip_adapter_image_embeds is not None:
1006
+ image_embeds = self.prepare_ip_adapter_image_embeds(
1007
+ ip_adapter_image,
1008
+ ip_adapter_image_embeds,
1009
+ device,
1010
+ batch_size * num_images_per_prompt,
1011
+ )
1012
+ if negative_ip_adapter_image is not None or negative_ip_adapter_image_embeds is not None:
1013
+ negative_image_embeds = self.prepare_ip_adapter_image_embeds(
1014
+ negative_ip_adapter_image,
1015
+ negative_ip_adapter_image_embeds,
1016
+ device,
1017
+ batch_size * num_images_per_prompt,
1018
+ )
1019
+
1020
+ # Make concept attention maps
1021
+ all_concept_attention_maps = []
1022
+
1023
+ # 6. Denoising loop
1024
+ with self.progress_bar(total=num_inference_steps) as progress_bar:
1025
+ for i, t in enumerate(timesteps):
1026
+ if self.interrupt:
1027
+ continue
1028
+
1029
+ self._current_timestep = t
1030
+ if image_embeds is not None:
1031
+ self._joint_attention_kwargs["ip_adapter_image_embeds"] = image_embeds
1032
+ # broadcast to batch dimension in a way that's compatible with ONNX/Core ML
1033
+ timestep = t.expand(latents.shape[0]).to(latents.dtype)
1034
+
1035
+ # Don't do concept attention if the timestep is not in the concept_attention_kwargs
1036
+ if concept_attention_kwargs is not None and i not in concept_attention_kwargs["timesteps"]:
1037
+ current_concept_embeddings = None
1038
+ elif concept_attention_kwargs is None:
1039
+ # Use concept embeddings for all timesteps if no specific config
1040
+ current_concept_embeddings = concept_embeddings
1041
+ else:
1042
+ # Use concept embeddings for specified timesteps
1043
+ current_concept_embeddings = concept_embeddings
1044
+
1045
+
1046
+ transformer_output = self.transformer(
1047
+ hidden_states=latents,
1048
+ timestep=timestep / 1000,
1049
+ guidance=guidance,
1050
+ pooled_projections=pooled_prompt_embeds,
1051
+ pooled_concept_embeds=pooled_concept_embeds,
1052
+ encoder_hidden_states=prompt_embeds,
1053
+ concept_hidden_states=current_concept_embeddings,
1054
+ txt_ids=text_ids,
1055
+ img_ids=latent_image_ids,
1056
+ concept_ids=concept_ids,
1057
+ joint_attention_kwargs=self.joint_attention_kwargs,
1058
+ concept_attention_kwargs=concept_attention_kwargs,
1059
+ return_dict=False,
1060
+ )
1061
+ noise_pred, current_concept_attention_maps = transformer_output
1062
+
1063
+ # Process attention maps immediately with softmax (critical!)
1064
+ if concept_attention_kwargs is not None and i in concept_attention_kwargs["timesteps"] and current_concept_attention_maps is not None:
1065
+ if isinstance(current_concept_attention_maps, list):
1066
+ # Transformer now returns list of dictionaries (one per layer)
1067
+ for layer_dict in current_concept_attention_maps:
1068
+ if isinstance(layer_dict, dict):
1069
+ all_concept_attention_maps.append(layer_dict)
1070
+ elif isinstance(current_concept_attention_maps, dict):
1071
+ # Collect vector dictionaries for proper postprocessing (like reference)
1072
+ all_concept_attention_maps.append(current_concept_attention_maps)
1073
+
1074
+ if do_true_cfg:
1075
+ if negative_image_embeds is not None:
1076
+ self._joint_attention_kwargs["ip_adapter_image_embeds"] = negative_image_embeds
1077
+ neg_noise_pred = self.transformer(
1078
+ hidden_states=latents,
1079
+ timestep=timestep / 1000,
1080
+ guidance=guidance,
1081
+ pooled_projections=negative_pooled_prompt_embeds,
1082
+ encoder_hidden_states=negative_prompt_embeds,
1083
+ txt_ids=text_ids,
1084
+ img_ids=latent_image_ids,
1085
+ joint_attention_kwargs=self.joint_attention_kwargs,
1086
+ return_dict=False,
1087
+ )[0]
1088
+ noise_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
1089
+
1090
+ # compute the previous noisy sample x_t -> x_t-1
1091
+ latents_dtype = latents.dtype
1092
+ latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
1093
+
1094
+ if latents.dtype != latents_dtype:
1095
+ if torch.backends.mps.is_available():
1096
+ # some platforms (eg. apple mps) misbehave due to a pytorch bug: https://github.com/pytorch/pytorch/pull/99272
1097
+ latents = latents.to(latents_dtype)
1098
+
1099
+ if callback_on_step_end is not None:
1100
+ callback_kwargs = {}
1101
+ for k in callback_on_step_end_tensor_inputs:
1102
+ callback_kwargs[k] = locals()[k]
1103
+ callback_outputs = callback_on_step_end(self, i, t, callback_kwargs)
1104
+
1105
+ latents = callback_outputs.pop("latents", latents)
1106
+ prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds)
1107
+
1108
+ # call the callback, if provided
1109
+ if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):
1110
+ progress_bar.update()
1111
+
1112
+ if XLA_AVAILABLE:
1113
+ xm.mark_step()
1114
+
1115
+ self._current_timestep = None
1116
+
1117
+ if output_type == "latent":
1118
+ image = latents
1119
+ elif output_type == "visualization":
1120
+ latents = self._unpack_latents(latents, height, width, self.vae_scale_factor)
1121
+ latents = (latents / self.vae.config.scaling_factor) + self.vae.config.shift_factor
1122
+ image = self.vae.decode(latents, return_dict=False)[0]
1123
+ image = image.detach()
1124
+ # Force PIL for visualization
1125
+ image = self.image_processor.postprocess(image, output_type="pil")
1126
+ else:
1127
+ latents = self._unpack_latents(latents, height, width, self.vae_scale_factor)
1128
+ latents = (latents / self.vae.config.scaling_factor) + self.vae.config.shift_factor
1129
+ image = self.vae.decode(latents, return_dict=False)[0]
1130
+ image = image.detach()
1131
+ image = self.image_processor.postprocess(image, output_type=output_type)
1132
+
1133
+
1134
+ # Process final attention maps
1135
+ if all_concept_attention_maps:
1136
+ # Extract and stack vectors across timesteps (like reference)
1137
+ concept_vectors = []
1138
+ image_vectors = []
1139
+
1140
+ for timestep_data in all_concept_attention_maps:
1141
+ concept_vectors.append(timestep_data['concept_vectors'].detach().cpu())
1142
+ image_vectors.append(timestep_data['image_vectors'].detach().cpu())
1143
+
1144
+ # Stack across timesteps: (timesteps, batch, heads, tokens, dim)
1145
+ concept_vectors = torch.stack(concept_vectors, dim=0)
1146
+ image_vectors = torch.stack(image_vectors, dim=0)
1147
+
1148
+ concept_vectors = concept_vectors / (concept_vectors.norm(dim=-1, keepdim=True) + 1e-8)
1149
+
1150
+ concept_attention_maps = einops.einsum(
1151
+ image_vectors,
1152
+ concept_vectors,
1153
+ "time batch patches dim, time batch concepts dim -> time batch concepts patches"
1154
+ )
1155
+
1156
+ concept_attention_maps = torch.softmax(concept_attention_maps, dim=-2)
1157
+
1158
+ concept_attention_maps = concept_attention_maps.mean(dim=0)
1159
+
1160
+ # Convert to numpy for further processing
1161
+ concept_attention_maps = concept_attention_maps.float().numpy()
1162
+
1163
+ # Reshape to spatial format (64x64 for FLUX)
1164
+ concept_attention_maps = einops.rearrange(
1165
+ concept_attention_maps,
1166
+ "batch concepts (h w) -> batch concepts h w",
1167
+ h=height // 16,
1168
+ w=width // 16,
1169
+ )
1170
+
1171
+ # Normalize concept maps - always return raw normalized maps (not colored PIL images)
1172
+ # Global min-max normalization per batch for consistency
1173
+ processed = []
1174
+ for b in range(concept_attention_maps.shape[0]):
1175
+ maps = concept_attention_maps[b] # (concepts, H, W)
1176
+ vmin, vmax = maps.min(), maps.max()
1177
+
1178
+ if vmax > vmin:
1179
+ maps = (maps - vmin) / (vmax - vmin)
1180
+ else:
1181
+ maps = np.zeros_like(maps)
1182
+
1183
+ # If visualization PIL images are needed, create them separately
1184
+ if output_type == "visualization": # New output type for colored visualizations
1185
+ batch_imgs = []
1186
+ for i, m in enumerate(maps):
1187
+ colored = plt.get_cmap("plasma")(m)
1188
+ rgb = (colored[:, :, :3] * 255).astype(np.uint8)
1189
+ batch_imgs.append(PIL.Image.fromarray(rgb))
1190
+ processed.append(batch_imgs)
1191
+ else:
1192
+ # Return raw normalized arrays as list of numpy arrays per concept
1193
+ processed.append([maps[i] for i in range(maps.shape[0])])
1194
+
1195
+ concept_attention_maps = processed
1196
+ else:
1197
+ concept_attention_maps = []
1198
+
1199
+ # Offload all models
1200
+ self.maybe_free_model_hooks()
1201
+
1202
+ if not return_dict:
1203
+ return (image, concept_attention_maps)
1204
+
1205
+ return FluxConceptAttentionOutput(
1206
+ images=image,
1207
+ concept_attention_maps=concept_attention_maps,
1208
+ )
1209
+
1210
+
1211
+ def encode_image(
1212
+ self,
1213
+ image: Union[PIL.Image.Image, torch.Tensor],
1214
+ concepts: List[str],
1215
+ prompt: str = "", # Optional prompt context
1216
+ height: Optional[int] = None,
1217
+ width: Optional[int] = None,
1218
+ num_samples: int = 1,
1219
+ num_inference_steps: int = 28,
1220
+ noise_timestep: int = 15, # How much noise to add (higher = more noise)
1221
+ guidance_scale: float = 3.5,
1222
+ generator: Optional[torch.Generator] = None,
1223
+ layers: List[int] = [15, 16, 17, 18],
1224
+ timesteps_to_analyze: List[int] = [24, 25, 26, 27],
1225
+ device: Optional[torch.device] = None,
1226
+ return_dict: bool = True,
1227
+ max_sequence_length: int = 512,
1228
+ ) -> Union[torch.Tensor, Dict]:
1229
+ """
1230
+ Encode an existing image to analyze concept attention patterns.
1231
+
1232
+ This method adds controlled noise to an input image and runs it through
1233
+ the denoising process to capture how the model attends to different concepts.
1234
+
1235
+ Args:
1236
+ image: Input image to analyze (PIL Image or tensor)
1237
+ concepts: List of concept strings to analyze
1238
+ prompt: Optional text prompt for context
1239
+ height, width: Output dimensions (inferred from image if not provided)
1240
+ num_samples: Number of noise samples to average over
1241
+ num_inference_steps: Total denoising steps
1242
+ noise_timestep: Which timestep to add noise at (higher = more noise)
1243
+ guidance_scale: Guidance scale for generation
1244
+ generator: Random generator for reproducibility
1245
+ layers: Which transformer layers to analyze
1246
+ timesteps_to_analyze: Which denoising timesteps to capture attention from
1247
+ device: Compute device
1248
+ return_dict: Whether to return dict or tuple
1249
+ max_sequence_length: Max sequence length for text encoding
1250
+
1251
+ Returns:
1252
+ Dictionary containing original image and concept attention maps
1253
+ """
1254
+ device = device or self._execution_device
1255
+
1256
+ # Process input image
1257
+ if isinstance(image, PIL.Image.Image):
1258
+ if height is None or width is None:
1259
+ width, height = image.size
1260
+ # Ensure dimensions are compatible
1261
+ height = height - (height % (self.vae_scale_factor * 2))
1262
+ width = width - (width % (self.vae_scale_factor * 2))
1263
+
1264
+ # Preprocess image
1265
+ processed_image = self.image_processor.preprocess(
1266
+ image, height=height, width=width
1267
+ ).to(device=device, dtype=self.vae.dtype)
1268
+ else:
1269
+ processed_image = image.to(device=device, dtype=self.vae.dtype)
1270
+ if height is None or width is None:
1271
+ _, _, height, width = processed_image.shape
1272
+
1273
+ # Encode image to latent space
1274
+ with torch.no_grad():
1275
+ if processed_image.shape[1] != self.latent_channels:
1276
+ image_latents = self._encode_vae_image(processed_image, generator)
1277
+ else:
1278
+ image_latents = processed_image
1279
+
1280
+ # Setup text embeddings
1281
+ prompt_embeds, pooled_prompt_embeds, text_ids = self.encode_prompt(
1282
+ prompt=prompt or "",
1283
+ prompt_2=None,
1284
+ device=device,
1285
+ num_images_per_prompt=1,
1286
+ max_sequence_length=max_sequence_length,
1287
+ )
1288
+
1289
+ # Setup concept embeddings
1290
+ concept_embeddings, pooled_concept_embeds, concept_ids = self.encode_concepts(
1291
+ concepts, device=device
1292
+ )
1293
+
1294
+ # Prepare timesteps
1295
+ image_seq_len = (height // self.vae_scale_factor // 2) * (width // self.vae_scale_factor // 2)
1296
+ mu = calculate_shift(
1297
+ image_seq_len,
1298
+ self.scheduler.config.base_image_seq_len,
1299
+ self.scheduler.config.max_image_seq_len,
1300
+ self.scheduler.config.base_shift,
1301
+ self.scheduler.config.max_shift,
1302
+ )
1303
+
1304
+ # Get full timestep schedule
1305
+ timesteps, _ = retrieve_timesteps(
1306
+ self.scheduler,
1307
+ num_inference_steps,
1308
+ device,
1309
+ timesteps=None,
1310
+ sigmas=None,
1311
+ mu=mu,
1312
+ )
1313
+
1314
+ # Prepare latent image IDs
1315
+ latent_image_ids = self._prepare_latent_image_ids(
1316
+ 1,
1317
+ 2 * (height // self.vae_scale_factor),
1318
+ 2 * (width // self.vae_scale_factor),
1319
+ device,
1320
+ image_latents.dtype
1321
+ )
1322
+
1323
+ # Handle guidance
1324
+ if self.transformer.config.guidance_embeds:
1325
+ guidance = torch.full([1], guidance_scale, device=device, dtype=torch.float32)
1326
+ else:
1327
+ guidance = None
1328
+
1329
+ # Collect attention maps across samples
1330
+ all_concept_attention_maps = []
1331
+
1332
+ for sample_idx in range(num_samples):
1333
+ # Add noise at specified timestep
1334
+ if generator is not None:
1335
+ # Use different seed for each sample
1336
+ sample_generator = torch.Generator(device=device).manual_seed(
1337
+ generator.initial_seed() + sample_idx
1338
+ )
1339
+ noise = torch.randn(
1340
+ image_latents.shape,
1341
+ generator=sample_generator,
1342
+ device=device,
1343
+ dtype=image_latents.dtype
1344
+ )
1345
+ else:
1346
+ noise = torch.randn_like(image_latents)
1347
+
1348
+ # Get the timestep tensor for noise addition
1349
+ t_noise = timesteps[noise_timestep]
1350
+ noisy_latents = self.scheduler.scale_noise(
1351
+ image_latents, t_noise.unsqueeze(0), noise
1352
+ )
1353
+
1354
+ # Pack latents for transformer
1355
+ packed_latents = self._pack_latents(
1356
+ noisy_latents,
1357
+ 1,
1358
+ self.transformer.config.in_channels // 4,
1359
+ 2 * (height // self.vae_scale_factor),
1360
+ 2 * (width // self.vae_scale_factor)
1361
+ )
1362
+
1363
+ # Run single denoising step with concept attention
1364
+ timestep_tensor = t_noise.expand(1).to(packed_latents.dtype) / 1000
1365
+
1366
+ concept_attention_kwargs = {
1367
+ "concepts": concepts,
1368
+ "timesteps": [0], # We're only doing one step
1369
+ "layers": layers
1370
+ }
1371
+
1372
+ with torch.no_grad():
1373
+ transformer_output = self.transformer(
1374
+ hidden_states=packed_latents,
1375
+ timestep=timestep_tensor,
1376
+ guidance=guidance,
1377
+ pooled_projections=pooled_prompt_embeds,
1378
+ pooled_concept_embeds=pooled_concept_embeds,
1379
+ encoder_hidden_states=prompt_embeds,
1380
+ concept_hidden_states=concept_embeddings,
1381
+ txt_ids=text_ids,
1382
+ img_ids=latent_image_ids,
1383
+ concept_ids=concept_ids,
1384
+ joint_attention_kwargs=None,
1385
+ concept_attention_kwargs=concept_attention_kwargs,
1386
+ return_dict=False,
1387
+ )
1388
+
1389
+ _, sample_concept_attention_maps = transformer_output
1390
+
1391
+ if sample_concept_attention_maps:
1392
+ all_concept_attention_maps.extend(sample_concept_attention_maps)
1393
+
1394
+ # Process collected attention maps
1395
+ concept_attention_maps = []
1396
+ if all_concept_attention_maps:
1397
+ # Extract and stack vectors across samples
1398
+ concept_vectors = []
1399
+ image_vectors = []
1400
+
1401
+ for sample_data in all_concept_attention_maps:
1402
+ concept_vectors.append(sample_data['concept_vectors'].detach().cpu())
1403
+ image_vectors.append(sample_data['image_vectors'].detach().cpu())
1404
+
1405
+ if concept_vectors and image_vectors:
1406
+ # Stack and average across samples
1407
+ concept_vectors = torch.stack(concept_vectors, dim=0).mean(dim=0)
1408
+ image_vectors = torch.stack(image_vectors, dim=0).mean(dim=0)
1409
+
1410
+ # Normalize concept vectors
1411
+ concept_vectors = concept_vectors / (concept_vectors.norm(dim=-1, keepdim=True) + 1e-8)
1412
+
1413
+ # Compute attention maps
1414
+ attention_maps = einops.einsum(
1415
+ image_vectors,
1416
+ concept_vectors,
1417
+ "batch patches dim, batch concepts dim -> batch concepts patches"
1418
+ )
1419
+
1420
+ # Apply softmax normalization
1421
+ attention_maps = torch.softmax(attention_maps, dim=-2)
1422
+
1423
+ # Convert to numpy and reshape to spatial format
1424
+ attention_maps = attention_maps.float().numpy()
1425
+ spatial_maps = einops.rearrange(
1426
+ attention_maps,
1427
+ "batch concepts (h w) -> batch concepts h w",
1428
+ h=height // 16,
1429
+ w=width // 16,
1430
+ )
1431
+
1432
+ # Process maps per batch (usually just 1 batch for encode_image)
1433
+ for b in range(spatial_maps.shape[0]):
1434
+ maps = spatial_maps[b] # (concepts, H, W)
1435
+
1436
+ # Normalize maps
1437
+ vmin, vmax = maps.min(), maps.max()
1438
+ if vmax > vmin:
1439
+ maps = (maps - vmin) / (vmax - vmin)
1440
+ else:
1441
+ maps = np.zeros_like(maps)
1442
+
1443
+ # Convert to list of arrays per concept
1444
+ concept_maps = [maps[i] for i in range(maps.shape[0])]
1445
+ concept_attention_maps.append(concept_maps)
1446
+
1447
+ # Prepare output
1448
+ result = {
1449
+ 'image': image, # Original input image
1450
+ 'concept_attention_maps': concept_attention_maps,
1451
+ 'concepts': concepts,
1452
+ 'height': height,
1453
+ 'width': width
1454
+ }
1455
+
1456
+ if not return_dict:
1457
+ return (image, concept_attention_maps)
1458
+
1459
+ return result
1460
+
1461
+
models/blocks.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch.nn as nn
2
+
3
+
4
+ def _make_scratch(in_shape, out_shape, groups=1, expand=False):
5
+ scratch = nn.Module()
6
+
7
+ out_shape1 = out_shape
8
+ out_shape2 = out_shape
9
+ out_shape3 = out_shape
10
+ if len(in_shape) >= 4:
11
+ out_shape4 = out_shape
12
+
13
+ if expand:
14
+ out_shape1 = out_shape
15
+ out_shape2 = out_shape*2
16
+ out_shape3 = out_shape*4
17
+ if len(in_shape) >= 4:
18
+ out_shape4 = out_shape*8
19
+
20
+ scratch.layer1_rn = nn.Conv2d(
21
+ in_shape[0], out_shape1, kernel_size=3, stride=1, padding=1, bias=False, groups=groups
22
+ )
23
+ scratch.layer2_rn = nn.Conv2d(
24
+ in_shape[1], out_shape2, kernel_size=3, stride=1, padding=1, bias=False, groups=groups
25
+ )
26
+ scratch.layer3_rn = nn.Conv2d(
27
+ in_shape[2], out_shape3, kernel_size=3, stride=1, padding=1, bias=False, groups=groups
28
+ )
29
+ if len(in_shape) >= 4:
30
+ scratch.layer4_rn = nn.Conv2d(
31
+ in_shape[3], out_shape4, kernel_size=3, stride=1, padding=1, bias=False, groups=groups
32
+ )
33
+
34
+ return scratch
35
+
36
+
37
+ class ResidualConvUnit(nn.Module):
38
+ """Residual convolution module."""
39
+
40
+ def __init__(self, features, activation, bn):
41
+ super().__init__()
42
+
43
+ self.bn = bn
44
+
45
+ self.groups=1
46
+
47
+ self.conv1 = nn.Conv2d(
48
+ features, features, kernel_size=3, stride=1, padding=1, bias=True, groups=self.groups
49
+ )
50
+
51
+ self.conv2 = nn.Conv2d(
52
+ features, features, kernel_size=3, stride=1, padding=1, bias=True, groups=self.groups
53
+ )
54
+
55
+ if self.bn==True:
56
+ self.bn1 = nn.BatchNorm2d(features)
57
+ self.bn2 = nn.BatchNorm2d(features)
58
+
59
+ self.activation = activation
60
+
61
+ self.skip_add = nn.quantized.FloatFunctional()
62
+
63
+ def forward(self, x):
64
+ out = self.activation(x)
65
+ out = self.conv1(out)
66
+ if self.bn==True:
67
+ out = self.bn1(out)
68
+
69
+ out = self.activation(out)
70
+ out = self.conv2(out)
71
+ if self.bn==True:
72
+ out = self.bn2(out)
73
+
74
+ if self.groups > 1:
75
+ out = self.conv_merge(out)
76
+
77
+ return self.skip_add.add(out, x)
78
+
79
+
80
+ class FeatureFusionBlock(nn.Module):
81
+ """Feature fusion block."""
82
+
83
+ def __init__(self, features, activation, deconv=False, bn=False, expand=False, align_corners=True, size=None):
84
+ super(FeatureFusionBlock, self).__init__()
85
+
86
+ self.deconv = deconv
87
+ self.align_corners = align_corners
88
+
89
+ self.groups=1
90
+
91
+ self.expand = expand
92
+ out_features = features
93
+ if self.expand==True:
94
+ out_features = features//2
95
+
96
+ self.out_conv = nn.Conv2d(features, out_features, kernel_size=1, stride=1, padding=0, bias=True, groups=1)
97
+
98
+ self.resConfUnit1 = ResidualConvUnit(features, activation, bn)
99
+ self.resConfUnit2 = ResidualConvUnit(features, activation, bn)
100
+
101
+ self.skip_add = nn.quantized.FloatFunctional()
102
+
103
+ self.size=size
104
+
105
+ def forward(self, *xs, size=None):
106
+ output = xs[0]
107
+
108
+ if len(xs) == 2:
109
+ res = self.resConfUnit1(xs[1])
110
+ output = self.skip_add.add(output, res)
111
+
112
+ output = self.resConfUnit2(output)
113
+
114
+ if (size is None) and (self.size is None):
115
+ modifier = {"scale_factor": 2}
116
+ elif size is None:
117
+ modifier = {"size": self.size}
118
+ else:
119
+ modifier = {"size": size}
120
+
121
+ output = nn.functional.interpolate(
122
+ output, **modifier, mode="bilinear", align_corners=self.align_corners
123
+ )
124
+
125
+ output = self.out_conv(output)
126
+
127
+ return output
models/dino_fusion.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """torch.hub DINOv3/DINOv2 feature extractor (fallback for the HF extractor).
2
+
3
+ Used by `core.build_dino_extractor` only when a backbone name is not in the
4
+ HuggingFace repo map. Extracts intermediate transformer layers as spatial maps.
5
+ """
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ import torch.nn.functional as F
10
+
11
+
12
+ class DINOv3FeatureExtractor(nn.Module):
13
+ """Extract intermediate ViT layers from a DINOv3/DINOv2 model loaded via torch.hub.
14
+
15
+ ``take_indices`` selects the layers to return (a list of block indices, matching
16
+ the HuggingFace extractor); the backbone is frozen.
17
+ """
18
+ def __init__(self, model_name="dinov3_vitb16", take_indices=(2, 5, 8, 11)):
19
+ super().__init__()
20
+ self.model_name = model_name
21
+ self.take_indices = list(take_indices)
22
+
23
+ if model_name.startswith('facebook/'):
24
+ hf_to_hub = {
25
+ 'facebook/dinov2-base': 'dinov2_vitb14',
26
+ 'facebook/dinov2-small': 'dinov2_vits14',
27
+ 'facebook/dinov2-large': 'dinov2_vitl14',
28
+ 'facebook/dinov2-giant': 'dinov2_vitg14',
29
+ 'facebook/dinov3-vitb16-pretrain-lvd1689m': 'dinov3_vitb16',
30
+ 'facebook/dinov3-vits16-pretrain-lvd1689m': 'dinov3_vits16',
31
+ 'facebook/dinov3-vitl16-pretrain-lvd1689m': 'dinov3_vitl16',
32
+ }
33
+ model_name = hf_to_hub.get(model_name, 'dinov2_vitb14')
34
+ self.model_name = model_name
35
+
36
+ if 'dinov2' in model_name:
37
+ self.dino = torch.hub.load('facebookresearch/dinov2', model_name)
38
+ self.patch_size = 14
39
+ elif 'dinov3' in model_name:
40
+ try:
41
+ self.dino = torch.hub.load('facebookresearch/dinov3', model_name)
42
+ except Exception as e:
43
+ # torch.hub gated/unavailable -> fall back to HuggingFace weights.
44
+ print(f"[DINO] torch.hub failed ({e}); loading from HuggingFace")
45
+ from transformers import AutoModel
46
+ hf_map = {
47
+ 'dinov3_vits16': 'facebook/dinov3-vits16-pretrain-lvd1689m',
48
+ 'dinov3_vitb16': 'facebook/dinov3-vitb16-pretrain-lvd1689m',
49
+ 'dinov3_vitl16': 'facebook/dinov3-vitl16-pretrain-lvd1689m',
50
+ }
51
+ self.dino = AutoModel.from_pretrained(
52
+ hf_map.get(model_name, 'facebook/dinov3-vitb16-pretrain-lvd1689m'),
53
+ trust_remote_code=True)
54
+ self.patch_size = 16
55
+ else:
56
+ raise ValueError(f"Unsupported model name: {model_name}. Use dinov2_* or dinov3_*")
57
+
58
+ self.dino.eval()
59
+ for p in self.dino.parameters():
60
+ p.requires_grad = False
61
+
62
+ @torch.no_grad()
63
+ def forward(self, images):
64
+ """images: [B, 3, H, W] in [0, 1] -> list of feature maps [B, C, H//p, W//p]."""
65
+ h, w = images.shape[-2:]
66
+ if h % self.patch_size != 0 or w % self.patch_size != 0:
67
+ new_h = ((h + self.patch_size - 1) // self.patch_size) * self.patch_size
68
+ new_w = ((w + self.patch_size - 1) // self.patch_size) * self.patch_size
69
+ images = F.interpolate(images, size=(new_h, new_w), mode='bilinear', align_corners=False)
70
+ h, w = new_h, new_w
71
+
72
+ features = self.dino.get_intermediate_layers(images, self.take_indices, return_class_token=False)
73
+
74
+ patch_h, patch_w = h // self.patch_size, w // self.patch_size
75
+ outs = []
76
+ for feat in features:
77
+ B, N, C = feat.shape
78
+ outs.append(feat.permute(0, 2, 1).reshape(B, C, patch_h, patch_w).contiguous())
79
+ return outs
80
+
81
+
82
+ def create_dino_extractor(model_name="dinov3_vitb16", take_indices=(2, 5, 8, 11)):
83
+ return DINOv3FeatureExtractor(model_name=model_name, take_indices=take_indices)
models/dinov3_hf_extractor.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DINOv3 feature extraction via HuggingFace, handling CLS and register tokens."""
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ from transformers import AutoImageProcessor, AutoModel
6
+
7
+
8
+ class DINOv3HFExtractor(nn.Module):
9
+ """
10
+ Extracts intermediate features from DINOv3 via HuggingFace transformers.
11
+
12
+ Properly handles DINOv3 register tokens:
13
+ - Output shape: [B, 1 + P + R, C] where:
14
+ - 1 = CLS token
15
+ - P = spatial patches (1024 for 512x512 with 16x16 patches)
16
+ - R = register tokens (typically 4 for DINOv3)
17
+
18
+ Returns 4 feature maps of shape [B, C_dino, 32, 32] from last 4 layers.
19
+ Input images must be [B, 3, 512, 512] in [0, 1] range.
20
+ """
21
+
22
+ def __init__(self, repo_id="facebook/dinov3-vitb16-pretrain-lvd1689m", take_last=None, take_indices=None, trainable=False):
23
+ super().__init__()
24
+
25
+ self.proc = AutoImageProcessor.from_pretrained(repo_id)
26
+
27
+ # Disable resizing/cropping so native 512x512 maps to 32x32 patches
28
+ for k in ("do_resize", "do_center_crop"):
29
+ if hasattr(self.proc, k):
30
+ setattr(self.proc, k, False)
31
+
32
+ self.model = AutoModel.from_pretrained(repo_id)
33
+ self.model.config.output_hidden_states = True
34
+
35
+ # trainable=True is used by the `dino_only` ablation (fine-tune the backbone);
36
+ # otherwise the backbone is frozen and kept in eval mode.
37
+ self._frozen = not trainable
38
+ if self._frozen:
39
+ self.model.eval()
40
+ for p in self.model.parameters():
41
+ p.requires_grad = False
42
+ else:
43
+ self.model.train()
44
+ for p in self.model.parameters():
45
+ p.requires_grad = True
46
+
47
+ # ImageNet normalization stats: mean [0.485, 0.456, 0.406], std [0.229, 0.224, 0.225]
48
+ mean = torch.tensor(self.proc.image_mean).view(1, 3, 1, 1)
49
+ std = torch.tensor(self.proc.image_std).view(1, 3, 1, 1)
50
+ self.register_buffer("mean", mean, persistent=False)
51
+ self.register_buffer("std", std, persistent=False)
52
+
53
+ if take_indices is not None:
54
+ self.take_indices = take_indices
55
+ self.take_last = None
56
+ else:
57
+ self.take_last = take_last if take_last is not None else 4
58
+ self.take_indices = None
59
+
60
+ self.patch_size = getattr(self.model.config, "patch_size", 16)
61
+ self.num_register_tokens = getattr(self.model.config, "num_register_tokens", 0)
62
+
63
+ hidden_size = getattr(self.model.config, "hidden_size", 768)
64
+
65
+ layers = self.take_indices if self.take_indices is not None else f"last {self.take_last}"
66
+ trainable_str = "trainable" if not self._frozen else "frozen"
67
+ print(f"[DINOv3] {repo_id.split('/')[-1]}: dim={hidden_size}, patch={self.patch_size}, "
68
+ f"layers={layers} ({trainable_str})")
69
+
70
+ def train(self, mode: bool = True):
71
+ """Keep a frozen backbone in eval mode; otherwise follow `mode`."""
72
+ self.training = mode
73
+ if self._frozen:
74
+ self.model.eval()
75
+ else:
76
+ self.model.train(mode)
77
+ return self
78
+
79
+ def forward(self, images_512: torch.Tensor):
80
+ """Extract DINOv3 features from [B, 3, H, W] images in [0, 1]; returns maps [B, C, H//16, W//16]."""
81
+ # Disable grad only when the backbone is frozen; otherwise allow fine-tuning
82
+ with torch.set_grad_enabled(not self._frozen):
83
+ return self._forward(images_512)
84
+
85
+ def _forward(self, images_512: torch.Tensor):
86
+ x = (images_512 - self.mean) / self.std
87
+
88
+ out = self.model(pixel_values=x, output_hidden_states=True)
89
+ hidden_states = out.hidden_states # Tuple: [emb, blk1, ..., blkL]
90
+
91
+ B, _, H, W = images_512.shape
92
+ H_patches = H // self.patch_size
93
+ W_patches = W // self.patch_size
94
+ P = H_patches * W_patches # total spatial patches
95
+ R = self.num_register_tokens
96
+
97
+ # Each hidden state is [B, 1 + P + R, C], laid out as [CLS][P spatial patches][R register tokens]
98
+ maps = []
99
+ if self.take_indices is not None:
100
+ for idx in self.take_indices:
101
+ hidden = hidden_states[idx]
102
+ spatial = hidden[:, 1:1+P, :] # [B, P, C], drop CLS and register tokens
103
+ C = spatial.shape[-1]
104
+ spatial_map = spatial.transpose(1, 2).reshape(B, C, H_patches, W_patches).contiguous()
105
+ maps.append(spatial_map)
106
+ else:
107
+ for hidden in hidden_states[-self.take_last:]:
108
+ spatial = hidden[:, 1:1+P, :] # [B, P, C], drop CLS and register tokens
109
+ C = spatial.shape[-1]
110
+ spatial_map = spatial.transpose(1, 2).reshape(B, C, H_patches, W_patches).contiguous()
111
+ maps.append(spatial_map)
112
+
113
+ return maps
114
+
115
+
116
+ def create_dinov3_hf_extractor(repo_id="facebook/dinov3-vitb16-pretrain-lvd1689m", take_last=None, take_indices=None, trainable=False):
117
+ """
118
+ Factory for DINOv3HFExtractor (frozen in eval mode unless trainable=True).
119
+ repo_id options: vits16 (384-dim), vitb16 (768-dim, recommended), vitl16 (1024-dim).
120
+ """
121
+ return DINOv3HFExtractor(repo_id=repo_id, take_last=take_last, take_indices=take_indices, trainable=trainable)
122
+
123
+
124
+ if __name__ == "__main__":
125
+ extractor = create_dinov3_hf_extractor().cuda()
126
+
127
+ dummy_input = torch.randn(2, 3, 512, 512).cuda()
128
+ print(f"\nInput: {dummy_input.shape}")
129
+
130
+ features = extractor(dummy_input)
131
+
132
+ print(f"\nExtracted {len(features)} feature maps:")
133
+ for i, feat in enumerate(features):
134
+ print(f" Layer {i}: {feat.shape}")
135
+
models/dpt_backbone.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared DPT reassemble + RefineNet cascade.
2
+
3
+ `DPTRefineNetStack` owns the `scratch` reassemble layers and the four
4
+ `FeatureFusionBlock` refinenets used by every DPT decoder in this repo (the depth
5
+ heads in ``dpt_decoder.py`` and the saliency decoder in
6
+ ``dpt_segmentation_decoder.py``). Decoders subclass it so the parameter names stay
7
+ flat (``scratch.*`` / ``refinenet{1..4}.*``) and existing checkpoints keep loading;
8
+ each subclass provides its own input projection and output head.
9
+ """
10
+
11
+ import torch.nn as nn
12
+
13
+ from .blocks import FeatureFusionBlock, _make_scratch
14
+
15
+
16
+ class DPTRefineNetStack(nn.Module):
17
+ def __init__(self, features=256, use_bn=False, out_channels=(256, 512, 1024, 1024)):
18
+ super().__init__()
19
+ self.features = features
20
+ self.scratch = _make_scratch(list(out_channels), features)
21
+ self.refinenet4 = FeatureFusionBlock(features, nn.ReLU(inplace=False), bn=use_bn)
22
+ self.refinenet3 = FeatureFusionBlock(features, nn.ReLU(inplace=False), bn=use_bn)
23
+ self.refinenet2 = FeatureFusionBlock(features, nn.ReLU(inplace=False), bn=use_bn)
24
+ self.refinenet1 = FeatureFusionBlock(features, nn.ReLU(inplace=False), bn=use_bn)
25
+
26
+ def fuse(self, layers, keep_layer1_size=False):
27
+ """Run the coarse-to-fine RefineNet cascade; returns the layer-1 feature map.
28
+
29
+ ``keep_layer1_size=True`` stops the final block from doing its default 2x
30
+ upsample (used by the high-res depth decoder, which upsamples in its head).
31
+ """
32
+ l1, l2, l3, l4 = layers
33
+ l1 = self.scratch.layer1_rn(l1)
34
+ l2 = self.scratch.layer2_rn(l2)
35
+ l3 = self.scratch.layer3_rn(l3)
36
+ l4 = self.scratch.layer4_rn(l4)
37
+ path = self.refinenet4(l4, size=l3.shape[2:])
38
+ path = self.refinenet3(path, l3, size=l2.shape[2:])
39
+ path = self.refinenet2(path, l2, size=l1.shape[2:])
40
+ if keep_layer1_size:
41
+ path = self.refinenet1(path, l1, size=l1.shape[2:])
42
+ else:
43
+ path = self.refinenet1(path, l1)
44
+ return path
models/dpt_decoder.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DPT-style depth decoders that consume spatial feature maps.
2
+
3
+ Both heads take a list of 4 spatial feature maps (FLUX + DINO + concepts) with
4
+ possibly different channel counts, run the shared DPT RefineNet cascade, and
5
+ upsample to a depth map:
6
+ - DPTHeadSpatial : single 2x upsample in the head (bilinear resize to GT after).
7
+ - DPTHeadHighRes : 4x learned 2x upsampling (16x) with optional image skip connections.
8
+ """
9
+
10
+ import torch
11
+ import torch.nn as nn
12
+ import torch.nn.functional as F
13
+
14
+ from .dpt_backbone import DPTRefineNetStack
15
+
16
+
17
+ class DPTHeadSpatial(DPTRefineNetStack):
18
+ def __init__(self, in_channels=[3840, 3840, 3840, 3840], features=256,
19
+ num_classes=1, use_bn=False, head_features=32):
20
+ super().__init__(features=features, use_bn=use_bn)
21
+ self.head_features = head_features
22
+ self.num_classes = num_classes
23
+
24
+ out_channels = [256, 512, 1024, 1024]
25
+ self.projects = nn.ModuleList([nn.Conv2d(in_ch, oc, 1) for in_ch, oc in zip(in_channels, out_channels)])
26
+ self.pre_fuse = nn.ModuleList([
27
+ nn.Sequential(nn.GroupNorm(1, oc), nn.Conv2d(oc, oc, 1), nn.ReLU(inplace=True))
28
+ for oc in out_channels])
29
+
30
+ self.head = nn.Sequential(
31
+ nn.Conv2d(features, features // 2, 3, padding=1),
32
+ nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True),
33
+ nn.Conv2d(features // 2, head_features, 3, padding=1),
34
+ nn.ReLU(inplace=True),
35
+ nn.Conv2d(head_features, num_classes, 1),
36
+ )
37
+
38
+ def forward(self, features):
39
+ resized = [pf(proj(f)) for f, proj, pf in zip(features, self.projects, self.pre_fuse)]
40
+ return self.head(self.fuse(resized))
41
+
42
+
43
+ class DPTHeadHighRes(DPTRefineNetStack):
44
+ """High-resolution variant: progressive 4x (2x) learned upsampling = 16x total,
45
+ with optional skip connections from the original image."""
46
+ def __init__(self, in_channels=[3840, 3840, 3840, 3840], features=256,
47
+ num_classes=1, use_bn=False, head_features=64, use_skip_connections=True):
48
+ super().__init__(features=features, use_bn=use_bn)
49
+ self.head_features = head_features
50
+ self.num_classes = num_classes
51
+ self.use_skip_connections = use_skip_connections
52
+
53
+ out_channels = [256, 512, 1024, 1024]
54
+ self.projects = nn.ModuleList([nn.Conv2d(in_ch, oc, 1) for in_ch, oc in zip(in_channels, out_channels)])
55
+ self.pre_fuse = nn.ModuleList([
56
+ nn.Sequential(nn.GroupNorm(1, oc), nn.Conv2d(oc, oc, 1), nn.ReLU(inplace=True))
57
+ for oc in out_channels])
58
+
59
+ if use_skip_connections:
60
+ def skip_enc(stride, out_ch):
61
+ return nn.Sequential(
62
+ nn.Conv2d(3, out_ch, 3, stride=stride, padding=1), nn.ReLU(inplace=True),
63
+ nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.ReLU(inplace=True))
64
+ self.skip_enc_2x = skip_enc(2, 16)
65
+ self.skip_enc_4x = skip_enc(4, 16)
66
+ self.skip_enc_8x = skip_enc(8, 16)
67
+ self.skip_enc_full = skip_enc(1, 8)
68
+
69
+ def up_block(c_in, c_out):
70
+ return nn.Sequential(
71
+ nn.Conv2d(c_in, c_out, 3, padding=1), nn.ReLU(inplace=True),
72
+ nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True),
73
+ nn.Conv2d(c_out, c_out, 3, padding=1), nn.ReLU(inplace=True))
74
+
75
+ skip = 16 if use_skip_connections else 0
76
+ self.up1 = up_block(features, features // 2)
77
+ self.fuse1 = nn.Conv2d(features // 2 + skip, features // 2, 1) if use_skip_connections else None
78
+ self.up2 = up_block(features // 2, features // 4)
79
+ self.fuse2 = nn.Conv2d(features // 4 + skip, features // 4, 1) if use_skip_connections else None
80
+ self.up3 = up_block(features // 4, head_features)
81
+ self.fuse3 = nn.Conv2d(head_features + skip, head_features, 1) if use_skip_connections else None
82
+ self.up4 = up_block(head_features, head_features // 2)
83
+ self.fuse4 = nn.Conv2d(head_features // 2 + (8 if use_skip_connections else 0), head_features // 2, 1) if use_skip_connections else None
84
+ self.output = nn.Conv2d(head_features // 2, num_classes, 3, padding=1)
85
+
86
+ def _apply_skip(self, x, fuse, skip):
87
+ if not self.use_skip_connections or skip is None:
88
+ return x
89
+ if skip.shape[-2:] != x.shape[-2:]:
90
+ skip = F.interpolate(skip, size=x.shape[-2:], mode='bilinear', align_corners=True)
91
+ return fuse(torch.cat([x, skip], dim=1))
92
+
93
+ def forward(self, features, image=None):
94
+ if self.use_skip_connections and image is not None:
95
+ skip_8x, skip_4x = self.skip_enc_8x(image), self.skip_enc_4x(image)
96
+ skip_2x, skip_full = self.skip_enc_2x(image), self.skip_enc_full(image)
97
+ else:
98
+ skip_8x = skip_4x = skip_2x = skip_full = None
99
+
100
+ resized = [pf(proj(f)) for f, proj, pf in zip(features, self.projects, self.pre_fuse)]
101
+ path_1 = self.fuse(resized, keep_layer1_size=True) # keep patch resolution
102
+
103
+ x = self._apply_skip(self.up1(path_1), self.fuse1, skip_8x)
104
+ x = self._apply_skip(self.up2(x), self.fuse2, skip_4x)
105
+ x = self._apply_skip(self.up3(x), self.fuse3, skip_2x)
106
+ x = self._apply_skip(self.up4(x), self.fuse4, skip_full)
107
+ return self.output(x)
models/dpt_segmentation_decoder.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch.nn as nn
2
+ import torch.nn.functional as F
3
+
4
+ from .dpt_backbone import DPTRefineNetStack
5
+
6
+
7
+ class ProgressiveOutputHead(nn.Module):
8
+ """Progressive channel-reduction head (GroupNorm for AMP/small-batch stability)."""
9
+ def __init__(self, in_features=256, num_classes=21, intermediate_features=32, groups=8):
10
+ super().__init__()
11
+ self.num_classes = num_classes
12
+ self.output_conv1 = nn.Sequential(
13
+ nn.Conv2d(in_features, in_features // 2, 3, padding=1, bias=False),
14
+ nn.GroupNorm(num_groups=groups, num_channels=in_features // 2),
15
+ nn.ReLU(inplace=True),
16
+ )
17
+ self.output_conv2 = nn.Sequential(
18
+ nn.Conv2d(in_features // 2, intermediate_features, 3, padding=1, bias=False),
19
+ nn.GroupNorm(num_groups=max(1, min(groups, intermediate_features)), num_channels=intermediate_features),
20
+ nn.ReLU(inplace=True),
21
+ nn.Conv2d(intermediate_features, num_classes, 1),
22
+ )
23
+ self.final_activation = nn.Identity() # sigmoid/softmax applied in the loss
24
+ self._init_weights()
25
+
26
+ def _init_weights(self):
27
+ for m in self.modules():
28
+ if isinstance(m, nn.Conv2d):
29
+ nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
30
+ # Background-prior bias on the final logits to avoid early sigmoid overflow.
31
+ final_conv = self.output_conv2[-1]
32
+ if isinstance(final_conv, nn.Conv2d) and final_conv.bias is not None:
33
+ nn.init.constant_(final_conv.bias, -2.2 if self.num_classes == 1 else -0.5)
34
+
35
+ def forward(self, x):
36
+ return self.final_activation(self.output_conv2(self.output_conv1(x)))
37
+
38
+
39
+ class OriginalDPTSegmentationDecoder(DPTRefineNetStack):
40
+ def __init__(self, in_channels, num_classes=21, features=256, target_size=(512, 512)):
41
+ super().__init__(features=features, use_bn=False)
42
+ self.target_size = target_size
43
+
44
+ out_channels = [256, 512, 1024, 1024]
45
+ self.projects = nn.ModuleList([nn.Conv2d(in_ch, oc, 1) for in_ch, oc in zip(in_channels, out_channels)])
46
+ self.resize_layers = nn.ModuleList([
47
+ nn.ConvTranspose2d(out_channels[0], out_channels[0], kernel_size=4, stride=4),
48
+ nn.ConvTranspose2d(out_channels[1], out_channels[1], kernel_size=2, stride=2),
49
+ nn.Identity(),
50
+ nn.Conv2d(out_channels[3], out_channels[3], kernel_size=3, stride=2, padding=1),
51
+ ])
52
+ self.output_head = ProgressiveOutputHead(
53
+ in_features=features, num_classes=num_classes, intermediate_features=32, groups=8)
54
+
55
+ def forward(self, features):
56
+ proj = [p(f) for p, f in zip(self.projects, features)]
57
+ resized = [r(f) for r, f in zip(self.resize_layers, proj)]
58
+ logits = self.output_head(self.fuse(resized))
59
+ if self.target_size is not None:
60
+ logits = F.interpolate(logits, size=self.target_size, mode='bilinear', align_corners=True)
61
+ return logits
models/flux_resizer.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Image resizing to FLUX-compatible resolutions (dimensions divisible by 32)."""
2
+
3
+ import cv2
4
+ import numpy as np
5
+ from typing import Tuple
6
+ from PIL import Image
7
+
8
+
9
+ class FluxResizer:
10
+ """
11
+ Resizer that ensures images are compatible with FLUX requirements.
12
+ - FLUX: Dimensions divisible by 32 (due to 2x2 packing on top of 16-stride VAE)
13
+ """
14
+
15
+ # Predefined optimal resolutions (all divisible by 32)
16
+ OPTIMAL_RESOLUTIONS = [
17
+ # Square and near-square
18
+ (1024, 1024), # 1:1 (64×64, 64×64)
19
+ (896, 1152), # ~0.78:1 (56×72)
20
+ (1152, 896), # ~1.29:1 (72×56)
21
+ (768, 1344), # ~0.57:1 (48×84)
22
+ (1344, 768), # ~1.75:1 (84×48)
23
+
24
+ # Additional common ratios
25
+ (832, 1216), # ~0.68:1 (52×76)
26
+ (1216, 832), # ~1.46:1 (76×52)
27
+ (704, 1408), # 0.5:1 (44×88)
28
+ (1408, 704), # 2:1 (88×44)
29
+ (960, 1088), # ~0.88:1 (60×68)
30
+ (1088, 960), # ~1.13:1 (68×60)
31
+ ]
32
+
33
+ def __init__(self):
34
+ self.resolution_aspects = [
35
+ (h, w, w / h) for h, w in self.OPTIMAL_RESOLUTIONS
36
+ ]
37
+
38
+ def select_best_resolution(self, original_h: int, original_w: int) -> Tuple[int, int]:
39
+ """Pick the optimal resolution whose aspect ratio best matches the input."""
40
+ original_aspect = original_w / original_h
41
+
42
+ best_resolution = None
43
+ min_aspect_diff = float('inf')
44
+
45
+ for h, w, aspect in self.resolution_aspects:
46
+ aspect_diff = abs(original_aspect - aspect)
47
+
48
+ if aspect_diff < min_aspect_diff:
49
+ min_aspect_diff = aspect_diff
50
+ best_resolution = (h, w)
51
+
52
+ return best_resolution
53
+
54
+ def resize_image(self, image: np.ndarray) -> Tuple[np.ndarray, Tuple[int, int]]:
55
+ """Resize an (H, W, C) numpy image to the optimal FLUX-compatible resolution."""
56
+ original_h, original_w = image.shape[:2]
57
+ target_h, target_w = self.select_best_resolution(original_h, original_w)
58
+
59
+ resized_image = cv2.resize(image, (target_w, target_h), interpolation=cv2.INTER_LINEAR)
60
+
61
+ return resized_image, (target_h, target_w)
62
+
63
+ def resize_pil_image(self, image: Image.Image) -> Tuple[Image.Image, Tuple[int, int]]:
64
+ """Resize a PIL image to the optimal FLUX-compatible resolution."""
65
+ original_w, original_h = image.size # PIL uses (W, H)
66
+ target_h, target_w = self.select_best_resolution(original_h, original_w)
67
+
68
+ resized_image = image.resize((target_w, target_h), Image.LANCZOS)
69
+
70
+ return resized_image, (target_h, target_w)
71
+
72
+ def resize_mask(self, mask: np.ndarray, target_size: Tuple[int, int]) -> np.ndarray:
73
+ """Resize a mask to target_size=(H, W) using nearest-neighbor interpolation."""
74
+ target_h, target_w = target_size
75
+
76
+ if len(mask.shape) == 3 and mask.shape[2] == 1:
77
+ mask = mask.squeeze(2)
78
+
79
+ resized_mask = cv2.resize(mask, (target_w, target_h), interpolation=cv2.INTER_NEAREST)
80
+
81
+ return resized_mask
82
+
83
+ def get_compatible_resolutions(self) -> list:
84
+ """Return list of all compatible resolutions."""
85
+ return self.OPTIMAL_RESOLUTIONS.copy()
86
+
87
+ @staticmethod
88
+ def verify_compatibility(height: int, width: int) -> bool:
89
+ """True if both dimensions are divisible by 32 (FLUX requirement)."""
90
+ return (height % 32 == 0) and (width % 32 == 0)
models/hyperfeature_fusion.py ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Aggregates features across multiple timesteps and layers using learned attention,
3
+ allowing the model to adaptively combine information from different denoising stages.
4
+ """
5
+
6
+ import torch
7
+ import torch.nn as nn
8
+ import torch.nn.functional as F
9
+ from typing import List, Dict, Tuple
10
+
11
+
12
+ class FeatureNormalizer(nn.Module):
13
+ """
14
+ Normalizes features from different timesteps/layers to comparable scales.
15
+ Uses learnable normalization parameters per timestep.
16
+ """
17
+ def __init__(self, feature_dim: int = 3072, num_timesteps: int = 4):
18
+ super().__init__()
19
+ self.feature_dim = feature_dim
20
+ self.num_timesteps = num_timesteps
21
+
22
+ # Learnable normalization parameters (per timestep)
23
+ self.layer_norms = nn.ModuleList([
24
+ nn.LayerNorm(feature_dim) for _ in range(num_timesteps)
25
+ ])
26
+
27
+ def forward(self, features_per_timestep: List[torch.Tensor]) -> List[torch.Tensor]:
28
+ normalized = []
29
+ for i, feat in enumerate(features_per_timestep):
30
+ # LayerNorm expects channels last: [B, C, H, W] -> [B, H, W, C]
31
+ B, C, H, W = feat.shape
32
+ feat_reshaped = feat.permute(0, 2, 3, 1).contiguous() # [B, H, W, C]
33
+
34
+ normalized_feat = self.layer_norms[i](feat_reshaped)
35
+
36
+ normalized_feat = normalized_feat.permute(0, 3, 1, 2).contiguous() # back to [B, C, H, W]
37
+ normalized.append(normalized_feat)
38
+
39
+ return normalized
40
+
41
+
42
+ class FeatureProjector(nn.Module):
43
+ """
44
+ Projects features from different layers/timesteps to a common dimension.
45
+ Uses 1x1 convolutions for efficient channel reduction.
46
+ """
47
+ def __init__(self, in_channels: int = 3072, out_channels: int = 512, num_timesteps: int = 4):
48
+ super().__init__()
49
+ self.in_channels = in_channels
50
+ self.out_channels = out_channels
51
+
52
+ # Separate linear projection for each timestep (allows timestep-specific transformations)
53
+ # No activation here: the transformer and output_proj provide nonlinearities,
54
+ # and keeping this linear ensures the skip connection can correct in both directions.
55
+ self.projectors = nn.ModuleList([
56
+ nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=True)
57
+ for _ in range(num_timesteps)
58
+ ])
59
+
60
+ def forward(self, features_per_timestep: List[torch.Tensor]) -> List[torch.Tensor]:
61
+ return [self.projectors[i](feat) for i, feat in enumerate(features_per_timestep)]
62
+
63
+
64
+ class CrossTimestepAttention(nn.Module):
65
+ """
66
+ Pixel-wise cross-timestep attention: for each spatial location, attends across
67
+ features from different timesteps to learn per-pixel timestep importance.
68
+ """
69
+ def __init__(self, feature_dim: int = 512, num_timesteps: int = 4, num_heads: int = 4):
70
+ super().__init__()
71
+ self.feature_dim = feature_dim
72
+ self.num_timesteps = num_timesteps
73
+ self.num_heads = num_heads
74
+ self.head_dim = feature_dim // num_heads
75
+
76
+ assert feature_dim % num_heads == 0, "feature_dim must be divisible by num_heads"
77
+
78
+ self.q_proj = nn.Linear(feature_dim, feature_dim)
79
+ self.k_proj = nn.Linear(feature_dim, feature_dim)
80
+ self.v_proj = nn.Linear(feature_dim, feature_dim)
81
+
82
+ self.out_proj = nn.Linear(feature_dim, feature_dim)
83
+
84
+ self.scale = self.head_dim ** -0.5
85
+
86
+ def forward(self, features_per_timestep: List[torch.Tensor]) -> torch.Tensor:
87
+ B, C, H, W = features_per_timestep[0].shape
88
+ T = len(features_per_timestep)
89
+
90
+ stacked = torch.stack(features_per_timestep, dim=1) # [B, T, C, H, W]
91
+
92
+ stacked = stacked.permute(0, 3, 4, 1, 2).contiguous() # [B, H, W, T, C]
93
+ stacked = stacked.view(B * H * W, T, C)
94
+
95
+ # Query from last (most refined) timestep; keys/values from all timesteps
96
+ q = self.q_proj(stacked[:, -1:, :]) # [B*H*W, 1, C]
97
+ k = self.k_proj(stacked) # [B*H*W, T, C]
98
+ v = self.v_proj(stacked) # [B*H*W, T, C]
99
+
100
+ q = q.view(B * H * W, 1, self.num_heads, self.head_dim).transpose(1, 2)
101
+ k = k.view(B * H * W, T, self.num_heads, self.head_dim).transpose(1, 2)
102
+ v = v.view(B * H * W, T, self.num_heads, self.head_dim).transpose(1, 2)
103
+
104
+ attn = torch.matmul(q, k.transpose(-2, -1)) * self.scale # [B*H*W, num_heads, 1, T]
105
+ attn = F.softmax(attn, dim=-1)
106
+
107
+ out = torch.matmul(attn, v) # [B*H*W, num_heads, 1, head_dim]
108
+
109
+ out = out.transpose(1, 2).contiguous().view(B * H * W, 1, C)
110
+
111
+ out = self.out_proj(out)
112
+
113
+ out = out.view(B, H, W, C).permute(0, 3, 1, 2).contiguous() # [B, C, H, W]
114
+
115
+ return out
116
+
117
+
118
+ class LayerScaleTransformerLayer(nn.Module):
119
+ """
120
+ Transformer layer with LayerScale (from CaiT: "Going deeper with Image Transformers").
121
+ LayerScale helps stabilize training of deeper transformers by adding learnable
122
+ diagonal scaling matrices initialized to small values (e.g., 1e-4).
123
+ """
124
+ def __init__(self, feature_dim: int, nhead: int = 8, dropout: float = 0.1,
125
+ layer_scale_init: float = 1e-4):
126
+ super().__init__()
127
+ self.feature_dim = feature_dim
128
+
129
+ self.self_attn = nn.MultiheadAttention(
130
+ feature_dim, nhead, dropout=dropout, batch_first=True
131
+ )
132
+
133
+ self.ffn = nn.Sequential(
134
+ nn.Linear(feature_dim, feature_dim * 2),
135
+ nn.ReLU(),
136
+ nn.Dropout(dropout),
137
+ nn.Linear(feature_dim * 2, feature_dim),
138
+ nn.Dropout(dropout)
139
+ )
140
+
141
+ self.norm1 = nn.LayerNorm(feature_dim)
142
+ self.norm2 = nn.LayerNorm(feature_dim)
143
+
144
+ # LayerScale diagonal scaling, initialized small to stabilize deep transformers
145
+ self.layer_scale_1 = nn.Parameter(
146
+ torch.ones(feature_dim) * layer_scale_init
147
+ )
148
+ self.layer_scale_2 = nn.Parameter(
149
+ torch.ones(feature_dim) * layer_scale_init
150
+ )
151
+
152
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
153
+ # Pre-norm self-attention with LayerScale (CaiT style)
154
+ x_normed = self.norm1(x)
155
+ attn_out, _ = self.self_attn(x_normed, x_normed, x_normed)
156
+ x = x + self.layer_scale_1 * attn_out
157
+
158
+ ffn_out = self.ffn(self.norm2(x))
159
+ x = x + self.layer_scale_2 * ffn_out
160
+
161
+ return x
162
+
163
+
164
+ class TemporalTransformerAggregator(nn.Module):
165
+ """
166
+ Transformer that aggregates features across timesteps with LayerScale.
167
+ Predicts content-adaptive per-pixel fusion weights rather than global weights.
168
+ """
169
+ def __init__(self, feature_dim: int = 512, num_timesteps: int = 4, num_layers: int = 2,
170
+ layer_scale_init: float = 1e-4):
171
+ super().__init__()
172
+ self.feature_dim = feature_dim
173
+ self.num_timesteps = num_timesteps
174
+ self.num_layers = num_layers
175
+
176
+ # Learnable temporal positional encoding, small init to avoid early dominance
177
+ self.temporal_pos_embed = nn.Parameter(torch.randn(1, num_timesteps, feature_dim))
178
+ nn.init.trunc_normal_(self.temporal_pos_embed, std=0.02)
179
+
180
+ self.layers = nn.ModuleList([
181
+ LayerScaleTransformerLayer(
182
+ feature_dim=feature_dim,
183
+ nhead=8,
184
+ dropout=0.1,
185
+ layer_scale_init=layer_scale_init
186
+ ) for _ in range(num_layers)
187
+ ])
188
+
189
+ # Per-pixel timestep weights: [B*H*W, C, T] -> [B*H*W, 1, T]
190
+ self.alpha_head = nn.Conv1d(self.feature_dim, 1, kernel_size=1)
191
+ # Small init so weighting starts nearly uniform across timesteps
192
+ nn.init.normal_(self.alpha_head.weight, mean=0.0, std=0.01)
193
+ nn.init.zeros_(self.alpha_head.bias)
194
+
195
+ def forward(self, features_per_timestep: List[torch.Tensor], return_alpha: bool = False):
196
+ """If return_alpha, also returns the per-pixel timestep weight map [B, T, H, W]."""
197
+ B, C, H, W = features_per_timestep[0].shape
198
+ T = len(features_per_timestep)
199
+
200
+ stacked = torch.stack(features_per_timestep, dim=1) # [B, T, C, H, W]
201
+ stacked = stacked.permute(0, 3, 4, 1, 2).contiguous() # [B, H, W, T, C]
202
+ stacked = stacked.view(B * H * W, T, C)
203
+
204
+ stacked = stacked + self.temporal_pos_embed
205
+
206
+ transformed = stacked
207
+ for layer in self.layers:
208
+ transformed = layer(transformed) # [B*H*W, T, C]
209
+
210
+ transformed_t = transformed.transpose(1, 2) # [B*H*W, C, T]
211
+
212
+ logits_alpha = self.alpha_head(transformed_t) # [B*H*W, 1, T]
213
+ logits_alpha = logits_alpha.squeeze(1) # [B*H*W, T]
214
+
215
+ # Softmax over timesteps per pixel
216
+ alpha = torch.softmax(logits_alpha, dim=-1) # [B*H*W, T]
217
+
218
+ alpha_expanded = alpha.unsqueeze(1) # [B*H*W, 1, T]
219
+ pooled = (alpha_expanded * transformed_t).sum(dim=-1) # [B*H*W, C]
220
+
221
+ out = pooled.view(B, H, W, C).permute(0, 3, 1, 2).contiguous() # [B, C, H, W]
222
+
223
+ if return_alpha:
224
+ alpha_map = alpha.view(B, H, W, T).permute(0, 3, 1, 2).contiguous() # [B, T, H, W]
225
+ return out, alpha_map
226
+
227
+ return out
228
+
229
+
230
+ class CBAM(nn.Module):
231
+ """
232
+ Convolutional Block Attention Module (CBAM, ECCV 2018).
233
+ Sequentially applies channel then spatial attention to refine features.
234
+ """
235
+ def __init__(self, channels: int, reduction: int = 16, kernel_size: int = 7):
236
+ super().__init__()
237
+ self.channels = channels
238
+
239
+ self.channel_mlp = nn.Sequential(
240
+ nn.Linear(channels, channels // reduction),
241
+ nn.ReLU(inplace=True),
242
+ nn.Linear(channels // reduction, channels)
243
+ )
244
+
245
+ self.spatial_conv = nn.Conv2d(2, 1, kernel_size=kernel_size, padding=kernel_size // 2)
246
+
247
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
248
+ B, C, H, W = x.shape
249
+
250
+ avg_pool = x.mean(dim=(2, 3), keepdim=False) # [B, C]
251
+ max_pool = x.amax(dim=(2, 3), keepdim=False) # [B, C]
252
+
253
+ # Shared MLP applied to both pooled descriptors
254
+ channel_weight = torch.sigmoid(
255
+ self.channel_mlp(avg_pool) + self.channel_mlp(max_pool)
256
+ ).view(B, C, 1, 1)
257
+
258
+ x = x * channel_weight
259
+
260
+ avg_spatial = x.mean(dim=1, keepdim=True) # [B, 1, H, W]
261
+ max_spatial = x.amax(dim=1, keepdim=True) # [B, 1, H, W]
262
+
263
+ spatial_weight = torch.sigmoid(
264
+ self.spatial_conv(torch.cat([avg_spatial, max_spatial], dim=1))
265
+ )
266
+
267
+ return x * spatial_weight
268
+
269
+
270
+ class HyperfeatureFusion(nn.Module):
271
+ """
272
+ Complete Hyperfeature Fusion module.
273
+ Implements the full pipeline: normalize -> project -> attend -> fuse -> refine (CBAM).
274
+ """
275
+ def __init__(
276
+ self,
277
+ in_channels: int = 3072,
278
+ out_channels: int = 3072,
279
+ hidden_dim: int = 512,
280
+ num_timesteps: int = 4,
281
+ fusion_type: str = 'attention', # 'attention' or 'transformer'
282
+ num_attention_heads: int = 4,
283
+ num_transformer_layers: int = 2,
284
+ layer_scale_init: float = 1e-4,
285
+ return_alpha: bool = False
286
+ ):
287
+ super().__init__()
288
+ self.in_channels = in_channels
289
+ self.out_channels = out_channels
290
+ self.hidden_dim = hidden_dim
291
+ self.num_timesteps = num_timesteps
292
+ self.fusion_type = fusion_type
293
+ self.return_alpha = return_alpha
294
+
295
+ self.normalizer = FeatureNormalizer(in_channels, num_timesteps)
296
+
297
+ self.projector = FeatureProjector(in_channels, hidden_dim, num_timesteps)
298
+
299
+ if fusion_type == 'attention':
300
+ self.aggregator = CrossTimestepAttention(hidden_dim, num_timesteps, num_attention_heads)
301
+ elif fusion_type == 'transformer':
302
+ self.aggregator = TemporalTransformerAggregator(
303
+ hidden_dim, num_timesteps, num_transformer_layers, layer_scale_init
304
+ )
305
+ else:
306
+ raise ValueError(f"Unknown fusion_type: {fusion_type}")
307
+
308
+ self.output_proj = nn.Sequential(
309
+ nn.Conv2d(hidden_dim, out_channels, kernel_size=3, padding=1),
310
+ nn.ReLU(inplace=True),
311
+ nn.Conv2d(out_channels, out_channels, kernel_size=1)
312
+ )
313
+
314
+ # Align hidden_dim to out_channels for the skip from the latest timestep
315
+ if hidden_dim != out_channels:
316
+ self.skip_align = nn.Conv2d(hidden_dim, out_channels, kernel_size=1, bias=False)
317
+ else:
318
+ self.skip_align = nn.Identity()
319
+
320
+ # Learnable skip gate initialized near zero so the aggregated path dominates
321
+ # early and the skip is gradually introduced during training.
322
+ self.skip_gate = nn.Parameter(torch.tensor(0.1))
323
+
324
+ self.cbam = CBAM(out_channels, reduction=16, kernel_size=7)
325
+
326
+ def forward(self, features_per_timestep: List[torch.Tensor]):
327
+ """features_per_timestep: list of [B, C_in, H, W] tensors ordered early to late."""
328
+ normalized = self.normalizer(features_per_timestep)
329
+
330
+ projected = self.projector(normalized)
331
+
332
+ if self.return_alpha and self.fusion_type == 'transformer':
333
+ aggregated, alpha_map = self.aggregator(projected, return_alpha=True)
334
+ else:
335
+ aggregated = self.aggregator(projected)
336
+ alpha_map = None
337
+
338
+ output = self.output_proj(aggregated)
339
+
340
+ # Gated skip connection from the latest timestep preserves refined features
341
+ latest_skip = self.skip_align(projected[-1])
342
+ output = output + self.skip_gate * latest_skip
343
+
344
+ output = self.cbam(output)
345
+
346
+ if self.return_alpha and alpha_map is not None:
347
+ return output, alpha_map
348
+
349
+ return output
350
+
351
+
352
+ class MultiLayerHyperfeatureFusion(nn.Module):
353
+ """
354
+ Applies Hyperfeature fusion independently for each FLUX feature layer.
355
+ This allows different layers to learn different temporal aggregation strategies.
356
+ """
357
+ def __init__(
358
+ self,
359
+ in_channels: int = 3072,
360
+ out_channels: int = 3072,
361
+ hidden_dim: int = 512,
362
+ num_layers: int = 4,
363
+ num_timesteps: int = 4,
364
+ fusion_type: str = 'attention',
365
+ num_attention_heads: int = 4,
366
+ num_transformer_layers: int = 2,
367
+ layer_scale_init: float = 1e-4,
368
+ return_alpha: bool = False
369
+ ):
370
+ super().__init__()
371
+ self.num_layers = num_layers
372
+ self.num_timesteps = num_timesteps
373
+ self.return_alpha = return_alpha
374
+
375
+ self.layer_fusions = nn.ModuleList([
376
+ HyperfeatureFusion(
377
+ in_channels=in_channels,
378
+ out_channels=out_channels,
379
+ hidden_dim=hidden_dim,
380
+ num_timesteps=num_timesteps,
381
+ fusion_type=fusion_type,
382
+ num_attention_heads=num_attention_heads,
383
+ num_transformer_layers=num_transformer_layers,
384
+ layer_scale_init=layer_scale_init,
385
+ return_alpha=return_alpha
386
+ ) for _ in range(num_layers)
387
+ ])
388
+
389
+ def forward(self, multi_timestep_features: Dict[int, List[torch.Tensor]]):
390
+ """multi_timestep_features: dict mapping timestep -> list of per-layer maps [B, C, H, W]."""
391
+ # Regroup features by layer instead of by timestep
392
+ features_per_layer = []
393
+ timesteps = sorted(multi_timestep_features.keys())
394
+
395
+ for layer_idx in range(self.num_layers):
396
+ layer_features_across_timesteps = [
397
+ multi_timestep_features[t][layer_idx] for t in timesteps
398
+ ]
399
+ features_per_layer.append(layer_features_across_timesteps)
400
+
401
+ fused_layers = []
402
+ alpha_layers = []
403
+ for layer_idx, layer_features in enumerate(features_per_layer):
404
+ result = self.layer_fusions[layer_idx](layer_features)
405
+ if self.return_alpha:
406
+ fused, alpha = result
407
+ fused_layers.append(fused)
408
+ alpha_layers.append(alpha)
409
+ else:
410
+ fused_layers.append(result)
411
+
412
+ if self.return_alpha:
413
+ return fused_layers, alpha_layers
414
+
415
+ return fused_layers
416
+
417
+
418
+ def create_hyperfeature_fusion(
419
+ num_timesteps: int = 4,
420
+ num_layers: int = 4,
421
+ fusion_type: str = 'attention',
422
+ hidden_dim: int = 512,
423
+ num_transformer_layers: int = 2,
424
+ layer_scale_init: float = 1e-4,
425
+ return_alpha: bool = False,
426
+ feature_dim: int = 3072
427
+ ) -> MultiLayerHyperfeatureFusion:
428
+ """Factory for MultiLayerHyperfeatureFusion (feature_dim 3072 for FLUX, 1536 for SD3.5)."""
429
+ return MultiLayerHyperfeatureFusion(
430
+ in_channels=feature_dim,
431
+ out_channels=feature_dim,
432
+ hidden_dim=hidden_dim,
433
+ num_layers=num_layers,
434
+ num_timesteps=num_timesteps,
435
+ fusion_type=fusion_type,
436
+ num_attention_heads=8,
437
+ num_transformer_layers=num_transformer_layers,
438
+ layer_scale_init=layer_scale_init,
439
+ return_alpha=return_alpha
440
+ )
441
+
models/segmentation_losses.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Segmentation losses: Lovasz-Softmax, Focal, Dice, and a combined loss."""
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ from typing import Optional, List
7
+ import numpy as np
8
+
9
+
10
+ def lovasz_grad(gt_sorted):
11
+ """
12
+ Compute gradient of the Lovasz extension w.r.t sorted errors.
13
+ See Algorithm 1 in paper.
14
+ """
15
+ p = len(gt_sorted)
16
+ gts = gt_sorted.sum()
17
+ intersection = gts - gt_sorted.float().cumsum(0)
18
+ union = gts + (1 - gt_sorted).float().cumsum(0)
19
+ jaccard = 1.0 - intersection / union
20
+ if p > 1:
21
+ jaccard[1:p] = jaccard[1:p] - jaccard[0:-1]
22
+ return jaccard
23
+
24
+
25
+ def lovasz_softmax_flat(probas, labels, classes='present', ignore_index=255):
26
+ """Multi-class Lovasz-Softmax loss over flat [P,C] probabilities and [P] labels."""
27
+ if probas.numel() == 0:
28
+ return probas * 0.0
29
+
30
+ C = probas.size(1)
31
+ losses = []
32
+
33
+ class_to_sum = list(range(C)) if classes == 'all' else []
34
+
35
+ for c in range(C):
36
+ fg = (labels == c).float() # foreground for class c
37
+ if classes == 'present' and fg.sum() == 0:
38
+ continue
39
+ if c == ignore_index:
40
+ continue
41
+
42
+ class_to_sum.append(c) if classes != 'all' else None
43
+ errors = (fg - probas[:, c]).abs()
44
+ errors_sorted, perm = torch.sort(errors, 0, descending=True)
45
+ perm = perm.data
46
+ fg_sorted = fg[perm]
47
+ losses.append(torch.dot(errors_sorted, lovasz_grad(fg_sorted)))
48
+
49
+ if len(losses) == 0:
50
+ return probas.sum() * 0.0
51
+
52
+ return torch.stack(losses).mean()
53
+
54
+
55
+ def flatten_probas(probas, labels, ignore_index=255):
56
+ """Flatten [B,C,H,W] preds and [B,H,W] labels to [P,C]/[P], dropping ignored pixels."""
57
+ B, C, H, W = probas.size()
58
+ probas = probas.permute(0, 2, 3, 1).contiguous().view(-1, C) # [B*H*W, C]
59
+ labels = labels.view(-1) # [B*H*W]
60
+
61
+ if ignore_index is not None:
62
+ valid = (labels != ignore_index)
63
+ probas = probas[valid]
64
+ labels = labels[valid]
65
+
66
+ return probas, labels
67
+
68
+
69
+ class LovaszSoftmaxLoss(nn.Module):
70
+ """
71
+ Lovasz-Softmax loss for multi-class semantic segmentation.
72
+ Directly optimizes the mean IoU (Jaccard index).
73
+ """
74
+
75
+ def __init__(self, classes='present', ignore_index=255):
76
+ super().__init__()
77
+ self.classes = classes
78
+ self.ignore_index = ignore_index
79
+
80
+ def forward(self, logits, labels):
81
+ probas = F.softmax(logits, dim=1)
82
+ probas, labels = flatten_probas(probas, labels, self.ignore_index)
83
+ return lovasz_softmax_flat(probas, labels, self.classes, self.ignore_index)
84
+
85
+
86
+ class FocalLoss(nn.Module):
87
+ """
88
+ Focal Loss for multi-class classification.
89
+ FL(p_t) = -alpha_t * (1 - p_t)^gamma * log(p_t)
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ gamma: float = 2.0,
95
+ alpha: Optional[torch.Tensor] = None,
96
+ ignore_index: int = 255,
97
+ reduction: str = 'mean'
98
+ ):
99
+ super().__init__()
100
+ self.gamma = gamma
101
+ self.alpha = alpha
102
+ self.ignore_index = ignore_index
103
+ self.reduction = reduction
104
+
105
+ def forward(self, logits, labels):
106
+ B, C, H, W = logits.shape
107
+
108
+ ce_loss = F.cross_entropy(
109
+ logits, labels,
110
+ weight=self.alpha,
111
+ ignore_index=self.ignore_index,
112
+ reduction='none'
113
+ ) # [B, H, W]
114
+
115
+ logits_flat = logits.permute(0, 2, 3, 1).contiguous().view(-1, C) # [B*H*W, C]
116
+ labels_flat = labels.view(-1) # [B*H*W]
117
+
118
+ valid_mask = (labels_flat != self.ignore_index)
119
+
120
+ probs = F.softmax(logits_flat, dim=1) # [B*H*W, C]
121
+ labels_clamped = labels_flat.clamp(0, C-1) # clamp for safe indexing of ignored pixels
122
+ p_t = probs.gather(1, labels_clamped.unsqueeze(1)).squeeze(1) # [B*H*W]
123
+
124
+ focal_weight = (1 - p_t) ** self.gamma # [B*H*W]
125
+ focal_weight = focal_weight.view(B, H, W) # [B, H, W]
126
+
127
+ focal_loss = focal_weight * ce_loss
128
+
129
+ valid_mask_2d = (labels != self.ignore_index)
130
+
131
+ if self.reduction == 'mean':
132
+ return focal_loss[valid_mask_2d].mean() if valid_mask_2d.sum() > 0 else focal_loss.sum() * 0.0
133
+ elif self.reduction == 'sum':
134
+ return focal_loss[valid_mask_2d].sum()
135
+ else:
136
+ return focal_loss
137
+
138
+
139
+ class DiceLoss(nn.Module):
140
+ """
141
+ Multi-class Dice loss with optional class weighting.
142
+ """
143
+
144
+ def __init__(
145
+ self,
146
+ num_classes: int = 21,
147
+ ignore_index: int = 255,
148
+ smooth: float = 1e-6,
149
+ class_weights: Optional[torch.Tensor] = None,
150
+ reduction: str = 'mean'
151
+ ):
152
+ super().__init__()
153
+ self.num_classes = num_classes
154
+ self.ignore_index = ignore_index
155
+ self.smooth = smooth
156
+ self.class_weights = class_weights
157
+ self.reduction = reduction
158
+
159
+ def forward(self, logits, labels):
160
+ probs = F.softmax(logits, dim=1) # [B, C, H, W]
161
+ valid_mask = (labels != self.ignore_index).float() # [B, H, W]
162
+
163
+ dice_loss = 0.0
164
+ valid_classes = 0
165
+ class_losses = []
166
+
167
+ for c in range(self.num_classes):
168
+ target_c = (labels == c).float() # [B, H, W]
169
+ pred_c = probs[:, c, :, :] # [B, H, W]
170
+
171
+ target_c = target_c * valid_mask
172
+ pred_c = pred_c * valid_mask
173
+
174
+ intersection = (pred_c * target_c).sum()
175
+ union = pred_c.sum() + target_c.sum()
176
+
177
+ if union > 0:
178
+ dice = (2.0 * intersection + self.smooth) / (union + self.smooth)
179
+ class_loss = 1.0 - dice
180
+
181
+ if self.class_weights is not None:
182
+ class_loss = class_loss * self.class_weights[c]
183
+
184
+ class_losses.append(class_loss)
185
+ valid_classes += 1
186
+
187
+ if valid_classes > 0:
188
+ if self.reduction == 'mean':
189
+ return torch.stack(class_losses).mean()
190
+ else:
191
+ return torch.stack(class_losses).sum()
192
+
193
+ return logits.sum() * 0.0
194
+
195
+
196
+ class CombinedSegmentationLoss(nn.Module):
197
+ """Weighted combination of CE/Focal, Dice, and Lovasz-Softmax losses."""
198
+
199
+ def __init__(
200
+ self,
201
+ num_classes: int = 21,
202
+ ignore_index: int = 255,
203
+ weight_ce: float = 1.0,
204
+ weight_dice: float = 1.0,
205
+ weight_lovasz: float = 1.0,
206
+ use_focal: bool = True,
207
+ focal_gamma: float = 2.0,
208
+ class_weights: Optional[torch.Tensor] = None,
209
+ ):
210
+ super().__init__()
211
+
212
+ self.weight_ce = weight_ce
213
+ self.weight_dice = weight_dice
214
+ self.weight_lovasz = weight_lovasz
215
+ self.use_focal = use_focal
216
+
217
+ if use_focal:
218
+ self.ce_loss = FocalLoss(
219
+ gamma=focal_gamma,
220
+ alpha=class_weights,
221
+ ignore_index=ignore_index,
222
+ )
223
+ else:
224
+ self.ce_loss = nn.CrossEntropyLoss(
225
+ weight=class_weights,
226
+ ignore_index=ignore_index,
227
+ )
228
+
229
+ self.dice_loss = DiceLoss(
230
+ num_classes=num_classes,
231
+ ignore_index=ignore_index,
232
+ class_weights=class_weights,
233
+ )
234
+
235
+ self.lovasz_loss = LovaszSoftmaxLoss(
236
+ classes='present',
237
+ ignore_index=ignore_index,
238
+ )
239
+
240
+ def forward(self, logits, labels):
241
+ loss_ce = self.ce_loss(logits, labels)
242
+ loss_dice = self.dice_loss(logits, labels)
243
+ loss_lovasz = self.lovasz_loss(logits, labels)
244
+
245
+ loss_total = (
246
+ self.weight_ce * loss_ce +
247
+ self.weight_dice * loss_dice +
248
+ self.weight_lovasz * loss_lovasz
249
+ )
250
+
251
+ return {
252
+ 'loss_ce': loss_ce,
253
+ 'loss_dice': loss_dice,
254
+ 'loss_lovasz': loss_lovasz,
255
+ 'loss_total': loss_total,
256
+ }
257
+
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ torchvision
3
+ diffusers
4
+ transformers
5
+ accelerate
6
+ einops
7
+ pytorch-lightning
8
+ torchmetrics
9
+ segmentation-models-pytorch
10
+ opencv-python-headless
11
+ matplotlib
12
+ Pillow
13
+ PyYAML
14
+ huggingface-hub
15
+ safetensors