@@ -306,11 +306,63 @@ def convert_to_nunchaku_transformer_block_lowrank_dict( # noqa: C901
306306 logger .debug (" - Using original LoRA" )
307307 lora = orig_lora
308308 else :
309- lora = (
310- torch .cat ([orig_lora [0 ], extra_lora [0 ].to (orig_lora [0 ].dtype )], dim = 0 ), # [r, c]
311- torch .cat ([orig_lora [1 ], extra_lora [1 ].to (orig_lora [1 ].dtype )], dim = 1 ), # [c, r]
312- )
313- logger .debug (f" - Merging original and extra LoRA (rank: { lora [0 ].shape [0 ]} )" )
309+ try :
310+ lora = (
311+ torch .cat ([orig_lora [0 ], extra_lora [0 ].to (orig_lora [0 ].dtype )], dim = 0 ), # [r, c]
312+ torch .cat ([orig_lora [1 ], extra_lora [1 ].to (orig_lora [1 ].dtype )], dim = 1 ), # [c, r]
313+ )
314+ logger .debug (f" - Merging original and extra LoRA (rank: { lora [0 ].shape [0 ]} )" )
315+ except RuntimeError as e :
316+ if "Sizes of tensors must match" in str (e ):
317+ # Handle various dimension mismatch cases for LoRA
318+ logger .debug (
319+ f" - Dimension mismatch detected: orig_lora[1]={ orig_lora [1 ].shape } , extra_lora[1]={ extra_lora [1 ].shape } "
320+ )
321+
322+ # Handle dimension mismatch by using only the properly sized portion of extra_lora
323+ # instead of trying to concatenate mismatched dimensions
324+
325+ # Case 1: single_blocks linear1 [21504] -> mlp_fc1 [12288]
326+ if extra_lora [1 ].shape [1 ] == 21504 and orig_lora [1 ].shape [1 ] == 12288 :
327+ # Use only the first 12288 dimensions from the 21504 extra LoRA
328+ extra_lora_up_split = extra_lora [1 ][:, :12288 ].clone ()
329+ extra_lora_down = extra_lora [0 ].clone ()
330+ # logger.debug(f" - Dimension fix 21504->12288: using split extra LoRA instead of merge")
331+
332+ # Use the split extra LoRA instead of concatenating
333+ lora = (extra_lora_down .to (orig_lora [0 ].dtype ), extra_lora_up_split .to (orig_lora [1 ].dtype ))
334+
335+ # Case 2: transformer_blocks with different MLP dimensions (27648 -> 9216)
336+ elif extra_lora [1 ].shape [1 ] == 27648 and orig_lora [1 ].shape [1 ] == 9216 :
337+ # Use only the first 9216 dimensions from the 27648 extra LoRA
338+ extra_lora_up_split = extra_lora [1 ][:, :9216 ].clone ()
339+ extra_lora_down = extra_lora [0 ].clone ()
340+ # logger.debug(f" - Dimension fix 27648->9216: using split extra LoRA instead of merge")
341+
342+ # Use the split extra LoRA instead of concatenating
343+ lora = (extra_lora_down .to (orig_lora [0 ].dtype ), extra_lora_up_split .to (orig_lora [1 ].dtype ))
344+
345+ # Case 3: Other dimension ratios - try to find a reasonable split
346+ elif extra_lora [1 ].shape [1 ] > orig_lora [1 ].shape [1 ]:
347+ # Use only what we need from extra LoRA
348+ target_dim = orig_lora [1 ].shape [1 ]
349+ extra_lora_up_split = extra_lora [1 ][:, :target_dim ].clone ()
350+ extra_lora_down = extra_lora [0 ].clone ()
351+ # logger.debug(
352+ # f" - Dimension fix {extra_lora[1].shape[1]}->{target_dim}: using truncated extra LoRA"
353+ # )
354+
355+ # Use the truncated extra LoRA instead of concatenating
356+ lora = (extra_lora_down .to (orig_lora [0 ].dtype ), extra_lora_up_split .to (orig_lora [1 ].dtype ))
357+
358+ else :
359+ # For cases where extra LoRA has fewer dimensions, use original LoRA only
360+ # logger.warning(
361+ # f" - Cannot split extra LoRA {extra_lora[1].shape[1]}->{orig_lora[1].shape[1]}, using original only"
362+ # )
363+ lora = orig_lora
364+ else :
365+ raise e
314366 # endregion
315367 if lora is not None :
316368 if convert_map [converted_local_name ] == "adanorm_single" :
@@ -343,6 +395,109 @@ def convert_to_nunchaku_transformer_block_lowrank_dict( # noqa: C901
343395 return converted
344396
345397
398+ def preprocess_single_blocks_lora (
399+ extra_lora_dict : dict [str , torch .Tensor ], candidate_block_name : str
400+ ) -> dict [str , torch .Tensor ]:
401+ """
402+ Preprocess LoRA weights from single_blocks format to match single_transformer_blocks structure.
403+
404+ This function handles the architectural mismatch between old and new models:
405+ - Old single_blocks: linear1 (fused 21504-dim layer) and linear2
406+ - New single_transformer_blocks: mlp_fc1 (12288-dim), qkv_proj (9216-dim), and mlp_fc2
407+
408+ The linear1 layer in the old architecture combines two functions:
409+ 1. MLP projection (first 12288 dimensions)
410+ 2. QKV projection for attention (last 9216 dimensions)
411+
412+ These are split into separate layers in the new architecture.
413+ """
414+ processed_dict = extra_lora_dict .copy ()
415+
416+ # Find all single_transformer_blocks keys that need preprocessing
417+ single_blocks_keys = [k for k in extra_lora_dict .keys () if "single_transformer_blocks" in k and "linear" in k ]
418+
419+ logger .debug (f"Preprocessing LoRA for candidate: { candidate_block_name } " )
420+ logger .debug (f"All keys in extra_lora_dict: { list (extra_lora_dict .keys ())[:10 ]} ..." ) # Show first 10 keys
421+ logger .debug (f"Found single_transformer_blocks keys: { single_blocks_keys [:5 ]} ..." ) # Show first 5 keys
422+
423+ if single_blocks_keys :
424+ logger .debug (f"Found single_transformer_blocks LoRA keys, preprocessing for candidate: { candidate_block_name } " )
425+
426+ # The candidate_block_name is already "single_transformer_blocks.0"
427+ # Look for linear1 and linear2 keys with this exact name
428+ linear1_lora_A_key = f"{ candidate_block_name } .linear1.lora_A.weight"
429+ linear1_lora_B_key = f"{ candidate_block_name } .linear1.lora_B.weight"
430+ linear2_lora_A_key = f"{ candidate_block_name } .linear2.lora_A.weight"
431+ linear2_lora_B_key = f"{ candidate_block_name } .linear2.lora_B.weight"
432+
433+ logger .debug (f"Looking for keys: { linear1_lora_B_key } " )
434+ logger .debug (
435+ f"Available keys matching pattern: { [k for k in extra_lora_dict .keys () if candidate_block_name in k ][:5 ]} ..."
436+ )
437+
438+ if linear1_lora_B_key in extra_lora_dict :
439+ linear1_lora_A = extra_lora_dict [linear1_lora_A_key ]
440+ linear1_lora_B = extra_lora_dict [linear1_lora_B_key ]
441+
442+ # Check if this is the problematic 21504 dimension case
443+ if linear1_lora_B .shape [0 ] == 21504 :
444+ logger .debug (
445+ f"Splitting linear1 LoRA weights: [21504, { linear1_lora_B .shape [1 ]} ] -> "
446+ f"mlp_fc1 [12288, { linear1_lora_B .shape [1 ]} ] + qkv_proj [9216, { linear1_lora_B .shape [1 ]} ]"
447+ )
448+
449+ # Split linear1.lora_B [21504, rank] into two parts:
450+ # 1. First 12288 dimensions -> mlp_fc1
451+ # 2. Last 9216 dimensions (12288:21504) -> qkv_proj
452+ mlp_fc1_lora_B = linear1_lora_B [:12288 , :].clone ()
453+ qkv_proj_lora_B = linear1_lora_B [12288 :21504 , :].clone ()
454+
455+ # The lora_A weight is reused for both new layers
456+ # since it represents the down-projection from the input
457+ mlp_fc1_lora_A = linear1_lora_A .clone ()
458+ qkv_proj_lora_A = linear1_lora_A .clone ()
459+
460+ # Map to new architecture:
461+ # 1. proj_mlp corresponds to mlp_fc1
462+ processed_dict [f"{ candidate_block_name } .proj_mlp.lora_A.weight" ] = mlp_fc1_lora_A
463+ processed_dict [f"{ candidate_block_name } .proj_mlp.lora_B.weight" ] = mlp_fc1_lora_B
464+
465+ # 2. Map the QKV part to the attention layers
466+ # Note: In the new architecture, this maps to attn.to_q, attn.to_k, attn.to_v
467+ # which get fused into qkv_proj during the conversion
468+ processed_dict [f"{ candidate_block_name } .attn.to_q.lora_A.weight" ] = qkv_proj_lora_A
469+ processed_dict [f"{ candidate_block_name } .attn.to_q.lora_B.weight" ] = qkv_proj_lora_B [
470+ :3072 , :
471+ ] # Q projection
472+ processed_dict [f"{ candidate_block_name } .attn.to_k.lora_A.weight" ] = qkv_proj_lora_A
473+ processed_dict [f"{ candidate_block_name } .attn.to_k.lora_B.weight" ] = qkv_proj_lora_B [
474+ 3072 :6144 , :
475+ ] # K projection
476+ processed_dict [f"{ candidate_block_name } .attn.to_v.lora_A.weight" ] = qkv_proj_lora_A
477+ processed_dict [f"{ candidate_block_name } .attn.to_v.lora_B.weight" ] = qkv_proj_lora_B [
478+ 6144 :9216 , :
479+ ] # V projection
480+
481+ # Handle linear2 -> mlp_fc2 mapping
482+ if linear2_lora_B_key in extra_lora_dict :
483+ linear2_lora_A = extra_lora_dict [linear2_lora_A_key ]
484+ linear2_lora_B = extra_lora_dict [linear2_lora_B_key ]
485+
486+ # Map linear2 to proj_out.linears.1 (mlp_fc2)
487+ processed_dict [f"{ candidate_block_name } .proj_out.linears.1.lora_A.weight" ] = linear2_lora_A
488+ processed_dict [f"{ candidate_block_name } .proj_out.linears.1.lora_B.weight" ] = linear2_lora_B
489+
490+ # Remove original keys
491+ processed_dict .pop (linear2_lora_A_key , None )
492+ processed_dict .pop (linear2_lora_B_key , None )
493+
494+ # Remove original linear1 keys
495+ processed_dict .pop (linear1_lora_A_key , None )
496+ processed_dict .pop (linear1_lora_B_key , None )
497+
498+ return processed_dict
499+
500+
346501def convert_to_nunchaku_flux_single_transformer_block_lowrank_dict (
347502 orig_state_dict : dict [str , torch .Tensor ],
348503 extra_lora_dict : dict [str , torch .Tensor ],
@@ -381,6 +536,10 @@ def convert_to_nunchaku_flux_single_transformer_block_lowrank_dict(
381536 - Handles both fused and unfused attention projections (e.g., qkv).
382537 - Applies special packing for W4A16 linear layers (e.g., ``"adanorm_single"`` and ``"adanorm_zero"``).
383538 """
539+
540+ # Preprocess single_blocks LoRA structure if needed
541+ # extra_lora_dict = preprocess_single_blocks_lora(extra_lora_dict, candidate_block_name)
542+
384543 if f"{ candidate_block_name } .proj_out.lora_A.weight" in extra_lora_dict :
385544 assert f"{ converted_block_name } .out_proj.qweight" in orig_state_dict
386545 assert f"{ converted_block_name } .mlp_fc2.qweight" in orig_state_dict
@@ -530,16 +689,58 @@ def convert_to_nunchaku_flux_lowrank_dict(
530689 orig_state_dict = base_model
531690
532691 if isinstance (lora , str ):
533- extra_lora_dict = load_state_dict_in_safetensors (lora , filter_prefix = "transformer." )
692+ # Load the LoRA - check if it has transformer prefix
693+ temp_dict = load_state_dict_in_safetensors (lora )
694+ if any (k .startswith ("transformer." ) for k in temp_dict .keys ()):
695+ # Standard FLUX LoRA with transformer prefix
696+ extra_lora_dict = filter_state_dict (temp_dict , filter_prefix = "transformer." )
697+ # Remove the transformer. prefix after filtering
698+ renamed_dict = {}
699+ for k , v in extra_lora_dict .items ():
700+ new_k = k .replace ("transformer." , "" ) if k .startswith ("transformer." ) else k
701+ renamed_dict [new_k ] = v
702+ extra_lora_dict = renamed_dict
703+ else :
704+ # Kontext LoRA without transformer prefix - use as is
705+ extra_lora_dict = temp_dict
534706 else :
535- extra_lora_dict = filter_state_dict (lora , filter_prefix = "transformer." )
707+ # When called from to_nunchaku, lora is already processed by to_diffusers
708+ # Keys should be in format: single_blocks.0.linear1.lora_A.weight
709+ extra_lora_dict = lora
710+
711+ # Add transformer. prefix and rename blocks to match expectations
712+ renamed_dict = {}
713+ for k , v in extra_lora_dict .items ():
714+ new_k = k
715+ # Add transformer. prefix and rename blocks
716+ if k .startswith ("single_blocks." ):
717+ new_k = "transformer.single_transformer_blocks." + k [14 :]
718+ elif k .startswith ("double_blocks." ):
719+ new_k = "transformer.transformer_blocks." + k [14 :]
720+ elif k .startswith ("proj_out." ):
721+ new_k = "transformer." + k
722+ elif not k .startswith ("transformer." ):
723+ new_k = "transformer." + k
724+ renamed_dict [new_k ] = v
725+ extra_lora_dict = renamed_dict
726+
727+ # Now filter for transformer prefix and remove it for processing
728+ extra_lora_dict = filter_state_dict (extra_lora_dict , filter_prefix = "transformer." )
729+
730+ # Remove the transformer. prefix for internal processing
731+ renamed_dict = {}
732+ for k , v in extra_lora_dict .items ():
733+ new_k = k .replace ("transformer." , "" ) if k .startswith ("transformer." ) else k
734+ renamed_dict [new_k ] = v
735+ extra_lora_dict = renamed_dict
536736
537737 vector_dict , unquantized_lora_dict = {}, {}
538738 for k in list (extra_lora_dict .keys ()):
539739 v = extra_lora_dict [k ]
540740 if v .ndim == 1 :
541741 vector_dict [k .replace (".lora_B.bias" , ".bias" )] = extra_lora_dict .pop (k )
542- elif "transformer_blocks" not in k :
742+ elif "transformer_blocks" not in k and "single_transformer_blocks" not in k :
743+ # Only unquantized parts (like final_layer) go here
543744 unquantized_lora_dict [k ] = extra_lora_dict .pop (k )
544745
545746 # Concatenate qkv_proj biases if present
0 commit comments