comdoleger commited on
Commit
64e55ce
·
verified ·
1 Parent(s): 31709d8

Upload scripts/convert_diffusers_to_comfy_transformer_only.py with huggingface_hub

Browse files
scripts/convert_diffusers_to_comfy_transformer_only.py ADDED
@@ -0,0 +1,457 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #######################################################
2
+ # Convert Diffusers Flux/Flex to diffusion model ComfyUI safetensors file
3
+ # This will only have the transformer weights, not the TEs and VAE
4
+ # You can save the transformer weights as bf16 or 8-bit with the --do_8_bit flag
5
+ # You can also save with scaled 8-bit using the --do_8bit_scaled flag
6
+ #
7
+ # Call like this for 8-bit transformer weights with stochastic rounding:
8
+ # python convert_diffusers_to_comfy_transformer_only.py /path/to/diffusers/checkpoint /output/path/my_finetune.safetensors --do_8_bit
9
+ #
10
+ # Call like this for 8-bit transformer weights with scaling:
11
+ # python convert_diffusers_to_comfy_transformer_only.py /path/to/diffusers/checkpoint /output/path/my_finetune.safetensors --do_8bit_scaled
12
+ #
13
+ # Call like this for bf16 transformer weights:
14
+ # python convert_diffusers_to_comfy_transformer_only.py /path/to/diffusers/checkpoint /output/path/my_finetune.safetensors
15
+ #
16
+ # Output should go in ComfyUI/models/diffusion_models/
17
+ #
18
+ #######################################################
19
+
20
+
21
+ import argparse
22
+ from datetime import date
23
+ import json
24
+ import os
25
+ from pathlib import Path
26
+ import safetensors
27
+ import safetensors.torch
28
+ import torch
29
+ import tqdm
30
+ from collections import OrderedDict
31
+
32
+
33
+ parser = argparse.ArgumentParser()
34
+
35
+ parser.add_argument("diffusers_path", type=str,
36
+ help="Path to the original Flux diffusers folder.")
37
+ parser.add_argument("flux_path", type=str,
38
+ help="Output path for the Flux safetensors file.")
39
+ parser.add_argument("--do_8_bit", action="store_true",
40
+ help="Use 8-bit weights with stochastic rounding instead of bf16.")
41
+ parser.add_argument("--do_8bit_scaled", action="store_true",
42
+ help="Use scaled 8-bit weights instead of bf16.")
43
+ args = parser.parse_args()
44
+
45
+ flux_path = Path(args.flux_path)
46
+ diffusers_path = Path(args.diffusers_path)
47
+
48
+ if os.path.exists(os.path.join(diffusers_path, "transformer")):
49
+ diffusers_path = Path(os.path.join(diffusers_path, "transformer"))
50
+
51
+ do_8_bit = args.do_8_bit
52
+ do_8bit_scaled = args.do_8bit_scaled
53
+
54
+ # Don't allow both flags to be active simultaneously
55
+ if do_8_bit and do_8bit_scaled:
56
+ print("Error: Cannot use both --do_8_bit and --do_8bit_scaled at the same time.")
57
+ exit()
58
+
59
+ if not os.path.exists(flux_path.parent):
60
+ os.makedirs(flux_path.parent)
61
+
62
+ if not diffusers_path.exists():
63
+ print(f"Error: Missing transformer folder: {diffusers_path}")
64
+ exit()
65
+
66
+ original_json_path = Path.joinpath(
67
+ diffusers_path, "diffusion_pytorch_model.safetensors.index.json")
68
+
69
+ if not original_json_path.exists():
70
+ print(f"Error: Missing transformer index json: {original_json_path}")
71
+ exit()
72
+
73
+ with open(original_json_path, "r", encoding="utf-8") as f:
74
+ original_json = json.load(f)
75
+
76
+ diffusers_map = {
77
+ "time_in.in_layer.weight": [
78
+ "time_text_embed.timestep_embedder.linear_1.weight",
79
+ ],
80
+ "time_in.in_layer.bias": [
81
+ "time_text_embed.timestep_embedder.linear_1.bias",
82
+ ],
83
+ "time_in.out_layer.weight": [
84
+ "time_text_embed.timestep_embedder.linear_2.weight",
85
+ ],
86
+ "time_in.out_layer.bias": [
87
+ "time_text_embed.timestep_embedder.linear_2.bias",
88
+ ],
89
+ "vector_in.in_layer.weight": [
90
+ "time_text_embed.text_embedder.linear_1.weight",
91
+ ],
92
+ "vector_in.in_layer.bias": [
93
+ "time_text_embed.text_embedder.linear_1.bias",
94
+ ],
95
+ "vector_in.out_layer.weight": [
96
+ "time_text_embed.text_embedder.linear_2.weight",
97
+ ],
98
+ "vector_in.out_layer.bias": [
99
+ "time_text_embed.text_embedder.linear_2.bias",
100
+ ],
101
+ "guidance_in.in_layer.weight": [
102
+ "time_text_embed.guidance_embedder.linear_1.weight",
103
+ ],
104
+ "guidance_in.in_layer.bias": [
105
+ "time_text_embed.guidance_embedder.linear_1.bias",
106
+ ],
107
+ "guidance_in.out_layer.weight": [
108
+ "time_text_embed.guidance_embedder.linear_2.weight",
109
+ ],
110
+ "guidance_in.out_layer.bias": [
111
+ "time_text_embed.guidance_embedder.linear_2.bias",
112
+ ],
113
+ "txt_in.weight": [
114
+ "context_embedder.weight",
115
+ ],
116
+ "txt_in.bias": [
117
+ "context_embedder.bias",
118
+ ],
119
+ "img_in.weight": [
120
+ "x_embedder.weight",
121
+ ],
122
+ "img_in.bias": [
123
+ "x_embedder.bias",
124
+ ],
125
+ "double_blocks.().img_mod.lin.weight": [
126
+ "norm1.linear.weight",
127
+ ],
128
+ "double_blocks.().img_mod.lin.bias": [
129
+ "norm1.linear.bias",
130
+ ],
131
+ "double_blocks.().txt_mod.lin.weight": [
132
+ "norm1_context.linear.weight",
133
+ ],
134
+ "double_blocks.().txt_mod.lin.bias": [
135
+ "norm1_context.linear.bias",
136
+ ],
137
+ "double_blocks.().img_attn.qkv.weight": [
138
+ "attn.to_q.weight",
139
+ "attn.to_k.weight",
140
+ "attn.to_v.weight",
141
+ ],
142
+ "double_blocks.().img_attn.qkv.bias": [
143
+ "attn.to_q.bias",
144
+ "attn.to_k.bias",
145
+ "attn.to_v.bias",
146
+ ],
147
+ "double_blocks.().txt_attn.qkv.weight": [
148
+ "attn.add_q_proj.weight",
149
+ "attn.add_k_proj.weight",
150
+ "attn.add_v_proj.weight",
151
+ ],
152
+ "double_blocks.().txt_attn.qkv.bias": [
153
+ "attn.add_q_proj.bias",
154
+ "attn.add_k_proj.bias",
155
+ "attn.add_v_proj.bias",
156
+ ],
157
+ "double_blocks.().img_attn.norm.query_norm.scale": [
158
+ "attn.norm_q.weight",
159
+ ],
160
+ "double_blocks.().img_attn.norm.key_norm.scale": [
161
+ "attn.norm_k.weight",
162
+ ],
163
+ "double_blocks.().txt_attn.norm.query_norm.scale": [
164
+ "attn.norm_added_q.weight",
165
+ ],
166
+ "double_blocks.().txt_attn.norm.key_norm.scale": [
167
+ "attn.norm_added_k.weight",
168
+ ],
169
+ "double_blocks.().img_mlp.0.weight": [
170
+ "ff.net.0.proj.weight",
171
+ ],
172
+ "double_blocks.().img_mlp.0.bias": [
173
+ "ff.net.0.proj.bias",
174
+ ],
175
+ "double_blocks.().img_mlp.2.weight": [
176
+ "ff.net.2.weight",
177
+ ],
178
+ "double_blocks.().img_mlp.2.bias": [
179
+ "ff.net.2.bias",
180
+ ],
181
+ "double_blocks.().txt_mlp.0.weight": [
182
+ "ff_context.net.0.proj.weight",
183
+ ],
184
+ "double_blocks.().txt_mlp.0.bias": [
185
+ "ff_context.net.0.proj.bias",
186
+ ],
187
+ "double_blocks.().txt_mlp.2.weight": [
188
+ "ff_context.net.2.weight",
189
+ ],
190
+ "double_blocks.().txt_mlp.2.bias": [
191
+ "ff_context.net.2.bias",
192
+ ],
193
+ "double_blocks.().img_attn.proj.weight": [
194
+ "attn.to_out.0.weight",
195
+ ],
196
+ "double_blocks.().img_attn.proj.bias": [
197
+ "attn.to_out.0.bias",
198
+ ],
199
+ "double_blocks.().txt_attn.proj.weight": [
200
+ "attn.to_add_out.weight",
201
+ ],
202
+ "double_blocks.().txt_attn.proj.bias": [
203
+ "attn.to_add_out.bias",
204
+ ],
205
+ "single_blocks.().modulation.lin.weight": [
206
+ "norm.linear.weight",
207
+ ],
208
+ "single_blocks.().modulation.lin.bias": [
209
+ "norm.linear.bias",
210
+ ],
211
+ "single_blocks.().linear1.weight": [
212
+ "attn.to_q.weight",
213
+ "attn.to_k.weight",
214
+ "attn.to_v.weight",
215
+ "proj_mlp.weight",
216
+ ],
217
+ "single_blocks.().linear1.bias": [
218
+ "attn.to_q.bias",
219
+ "attn.to_k.bias",
220
+ "attn.to_v.bias",
221
+ "proj_mlp.bias",
222
+ ],
223
+ "single_blocks.().linear2.weight": [
224
+ "proj_out.weight",
225
+ ],
226
+ "single_blocks.().norm.query_norm.scale": [
227
+ "attn.norm_q.weight",
228
+ ],
229
+ "single_blocks.().norm.key_norm.scale": [
230
+ "attn.norm_k.weight",
231
+ ],
232
+ "single_blocks.().linear2.weight": [
233
+ "proj_out.weight",
234
+ ],
235
+ "single_blocks.().linear2.bias": [
236
+ "proj_out.bias",
237
+ ],
238
+ "final_layer.linear.weight": [
239
+ "proj_out.weight",
240
+ ],
241
+ "final_layer.linear.bias": [
242
+ "proj_out.bias",
243
+ ],
244
+ "final_layer.adaLN_modulation.1.weight": [
245
+ "norm_out.linear.weight",
246
+ ],
247
+ "final_layer.adaLN_modulation.1.bias": [
248
+ "norm_out.linear.bias",
249
+ ],
250
+ }
251
+
252
+
253
+ def is_in_diffusers_map(k):
254
+ for values in diffusers_map.values():
255
+ for value in values:
256
+ if k.endswith(value):
257
+ return True
258
+ return False
259
+
260
+
261
+ diffusers = {k: Path.joinpath(diffusers_path, v)
262
+ for k, v in original_json["weight_map"].items() if is_in_diffusers_map(k)}
263
+
264
+ original_safetensors = set(diffusers.values())
265
+
266
+ # determine the number of transformer blocks
267
+ transformer_blocks = 0
268
+ single_transformer_blocks = 0
269
+ for key in diffusers.keys():
270
+ print(key)
271
+ if key.startswith("transformer_blocks."):
272
+ print(key)
273
+ block = int(key.split(".")[1])
274
+ if block >= transformer_blocks:
275
+ transformer_blocks = block + 1
276
+ elif key.startswith("single_transformer_blocks."):
277
+ block = int(key.split(".")[1])
278
+ if block >= single_transformer_blocks:
279
+ single_transformer_blocks = block + 1
280
+
281
+ print(f"Transformer blocks: {transformer_blocks}")
282
+ print(f"Single transformer blocks: {single_transformer_blocks}")
283
+
284
+ for file in original_safetensors:
285
+ if not file.exists():
286
+ print(f"Error: Missing transformer safetensors file: {file}")
287
+ exit()
288
+
289
+ original_safetensors = {f: safetensors.safe_open(
290
+ f, framework="pt", device="cpu") for f in original_safetensors}
291
+
292
+
293
+ def swap_scale_shift(weight):
294
+ shift, scale = weight.chunk(2, dim=0)
295
+ new_weight = torch.cat([scale, shift], dim=0)
296
+ return new_weight
297
+
298
+
299
+ flux_values = {}
300
+
301
+ for b in range(transformer_blocks):
302
+ for key, weights in diffusers_map.items():
303
+ if key.startswith("double_blocks."):
304
+ block_prefix = f"transformer_blocks.{b}."
305
+ found = True
306
+ for weight in weights:
307
+ if not (f"{block_prefix}{weight}" in diffusers):
308
+ found = False
309
+ if found:
310
+ flux_values[key.replace("()", f"{b}")] = [
311
+ f"{block_prefix}{weight}" for weight in weights]
312
+ for b in range(single_transformer_blocks):
313
+ for key, weights in diffusers_map.items():
314
+ if key.startswith("single_blocks."):
315
+ block_prefix = f"single_transformer_blocks.{b}."
316
+ found = True
317
+ for weight in weights:
318
+ if not (f"{block_prefix}{weight}" in diffusers):
319
+ found = False
320
+ if found:
321
+ flux_values[key.replace("()", f"{b}")] = [
322
+ f"{block_prefix}{weight}" for weight in weights]
323
+
324
+ for key, weights in diffusers_map.items():
325
+ if not (key.startswith("double_blocks.") or key.startswith("single_blocks.")):
326
+ found = True
327
+ for weight in weights:
328
+ if not (f"{weight}" in diffusers):
329
+ found = False
330
+ if found:
331
+ flux_values[key] = [f"{weight}" for weight in weights]
332
+
333
+ flux = {}
334
+
335
+ for key, values in tqdm.tqdm(flux_values.items()):
336
+ if len(values) == 1:
337
+ flux[key] = original_safetensors[diffusers[values[0]]
338
+ ].get_tensor(values[0]).to("cpu")
339
+ else:
340
+ flux[key] = torch.cat(
341
+ [
342
+ original_safetensors[diffusers[value]
343
+ ].get_tensor(value).to("cpu")
344
+ for value in values
345
+ ]
346
+ )
347
+
348
+ if "norm_out.linear.weight" in diffusers:
349
+ flux["final_layer.adaLN_modulation.1.weight"] = swap_scale_shift(
350
+ original_safetensors[diffusers["norm_out.linear.weight"]].get_tensor(
351
+ "norm_out.linear.weight").to("cpu")
352
+ )
353
+ if "norm_out.linear.bias" in diffusers:
354
+ flux["final_layer.adaLN_modulation.1.bias"] = swap_scale_shift(
355
+ original_safetensors[diffusers["norm_out.linear.bias"]].get_tensor(
356
+ "norm_out.linear.bias").to("cpu")
357
+ )
358
+
359
+
360
+ def stochastic_round_to(tensor, dtype=torch.float8_e4m3fn):
361
+ # Define the float8 range
362
+ min_val = torch.finfo(dtype).min
363
+ max_val = torch.finfo(dtype).max
364
+
365
+ # Clip values to float8 range
366
+ tensor = torch.clamp(tensor, min_val, max_val)
367
+
368
+ # Convert to float32 for calculations
369
+ tensor = tensor.float()
370
+
371
+ # Get the nearest representable float8 values
372
+ lower = torch.floor(tensor * 256) / 256
373
+ upper = torch.ceil(tensor * 256) / 256
374
+
375
+ # Calculate the probability of rounding up
376
+ prob = (tensor - lower) / (upper - lower)
377
+
378
+ # Generate random values for stochastic rounding
379
+ rand = torch.rand_like(tensor)
380
+
381
+ # Perform stochastic rounding
382
+ rounded = torch.where(rand < prob, upper, lower)
383
+
384
+ # Convert back to float8
385
+ return rounded.to(dtype)
386
+
387
+
388
+ # List of keys that should not be scaled (usually embedding layers and biases)
389
+ blacklist = []
390
+ for key in flux.keys():
391
+ if not key.endswith(".weight") or "embed" in key:
392
+ blacklist.append(key)
393
+
394
+ # Function to scale weights for 8-bit quantization
395
+ def scale_weights_to_8bit(tensor, max_value=416.0, dtype=torch.float8_e4m3fn):
396
+ # Get the limits of the dtype
397
+ min_val = torch.finfo(dtype).min
398
+ max_val = torch.finfo(dtype).max
399
+
400
+ # Only process 2D tensors that are not in the blacklist
401
+ if tensor.dim() == 2:
402
+ # Calculate the scaling factor
403
+ abs_max = torch.max(torch.abs(tensor))
404
+ scale = abs_max / max_value
405
+
406
+ # Scale the tensor and clip to float8 range
407
+ scaled_tensor = (tensor / scale).clip(min=min_val, max=max_val).to(dtype)
408
+
409
+ return scaled_tensor, scale
410
+ else:
411
+ # For tensors that shouldn't be scaled, just convert to float8
412
+ return tensor.clip(min=min_val, max=max_val).to(dtype), None
413
+
414
+
415
+ # set all the keys to appropriate dtype
416
+ if do_8_bit:
417
+ print("Converting to 8-bit with stochastic rounding...")
418
+ for key in flux.keys():
419
+ flux[key] = stochastic_round_to(
420
+ flux[key], torch.float8_e4m3fn).to('cpu')
421
+ elif do_8bit_scaled:
422
+ print("Converting to scaled 8-bit...")
423
+ scales = {}
424
+ for key in tqdm.tqdm(flux.keys()):
425
+ if key.endswith(".weight") and key not in blacklist:
426
+ flux[key], scale = scale_weights_to_8bit(flux[key])
427
+ if scale is not None:
428
+ scale_key = key[:-len(".weight")] + ".scale_weight"
429
+ scales[scale_key] = scale
430
+ else:
431
+ # For non-weight tensors or blacklisted ones, just convert without scaling
432
+ min_val = torch.finfo(torch.float8_e4m3fn).min
433
+ max_val = torch.finfo(torch.float8_e4m3fn).max
434
+ flux[key] = flux[key].clip(min=min_val, max=max_val).to(torch.float8_e4m3fn).to('cpu')
435
+
436
+ # Add all the scales to the flux dictionary
437
+ flux.update(scales)
438
+
439
+ # Add a marker tensor to indicate this is a scaled fp8 model
440
+ flux["scaled_fp8"] = torch.tensor([]).to(torch.float8_e4m3fn)
441
+ else:
442
+ print("Converting to bfloat16...")
443
+ for key in flux.keys():
444
+ flux[key] = flux[key].clone().to('cpu', torch.bfloat16)
445
+
446
+ meta = OrderedDict()
447
+ meta['format'] = 'pt'
448
+ # date format like 2024-08-01 YYYY-MM-DD
449
+ meta['modelspec.date'] = date.today().strftime("%Y-%m-%d")
450
+
451
+ os.makedirs(os.path.dirname(flux_path), exist_ok=True)
452
+
453
+ print(f"Saving to {flux_path}")
454
+
455
+ safetensors.torch.save_file(flux, flux_path, metadata=meta)
456
+
457
+ print("Done.")