Script for lora-merge

#3
by QrusherZA - opened

Hi is it possible to share your script you used to merge the DMD lora onto the diff model. I am trying to write one but it's not working out.

this is what i have so far, and it's breaking silently, want to pull my hair out 😞

import torch
from safetensors import safe_open
from safetensors.torch import load_file, save_file
from tqdm import tqdm
import time

BASE_MODEL = r"C:\ComfyUI\ComfyUI_windows_portable\ComfyUI\models\checkpoints\sulphur_dev_bf16.safetensors"
LORA = r"C:\ComfyUI\ComfyUI_windows_portable\ComfyUI\models\loras\LTX2.3_DMD_reshaped_r256.safetensors"
OUTPUT = r"C:\ComfyUI\ComfyUI_windows_portable\ComfyUI\models\checkpoints\sulphur_dev_bf16_DMD.safetensors"
STRENGTH = 1.0

# Preserve base metadata (LTX-2.3 carries audio VAE config here β€” dropping it
# breaks audio on the merged checkpoint).
print("Reading base metadata...")
with safe_open(BASE_MODEL, framework="pt", device="cpu") as f:
    meta = f.metadata() or {}

print("Loading base model...")
base = load_file(BASE_MODEL)

print("Loading LoRA...")
lora = load_file(LORA)

merged = dict(base)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

print(f"Using device: {device}")

# Find every lora_down tensor
down_keys = [
    k
    for k in lora.keys()
    if (k.endswith(".lora_A.weight") or k.endswith(".lora_down.weight"))
]

applied = 0
skipped_missing = 0
skipped_shape = 0
skipped_nd = 0

with torch.no_grad():
    print("Building lookup ...")
    lookup = {}

    for k in merged:
        lookup[k.split("model.")[-1]] = k

    for down_key in tqdm(down_keys):
        try:
            up_key = down_key.replace(".lora_A.weight", ".lora_B.weight")
            alpha_key = down_key.replace(".lora_A.weight", ".alpha")

            suffix = down_key.replace(".lora_A.weight", ".weight")

            suffix = suffix.split("diffusion_model.")[-1]

            base_key = lookup.get(suffix)

            if base_key not in merged:
                skipped_missing += 1
                continue

            if up_key not in lora:
                print(f"Missing lora_up for {down_key}")
                skipped_missing += 1
                continue

            W = merged[base_key]

            # Only 2D Linear weights are LoRA-targetable. Anything else (modulation
            # tables, norms) must be left alone.
            if W.dim() != 2:
                print(f"Non-2D, skipping {base_key} {tuple(W.shape)}")
                skipped_nd += 1
                continue

            A = lora[down_key]
            B = lora[up_key]
            rank = A.shape[0]

            if alpha_key in lora:
                alpha = float(lora[alpha_key].item())
            else:
                alpha = rank

            scale = (alpha / rank) * STRENGTH

            # Verify matrix multiplication dimensions BEFORE allocating delta
            if B.shape[1] != A.shape[0]:
                print(
                    f"Rank mismatch: {base_key} " f"A{tuple(A.shape)} B{tuple(B.shape)}"
                )
                skipped_shape += 1
                continue

            # Verify resulting matrix will match the checkpoint weight
            expected = (B.shape[0], A.shape[1])

            if expected != W.shape:
                print(
                    f"Shape mismatch: {base_key} "
                    f"W{tuple(W.shape)} Expected{expected}"
                )
                skipped_shape += 1
                continue

            # Safe to compute
            t = time.perf_counter()

            delta = torch.matmul(B.float(), A.float())

            elapsed = time.perf_counter() - t

            delta.mul_(scale)

            Wf = W.float()

            Wf.add_(delta)

            merged[base_key] = Wf.to(W.dtype)

            del Wf

            applied += 1

            del A, B, delta

            if applied % 50 == 0:
                print(
                    f"{applied}/{len(down_keys)} "
                    f"{base_key} "
                    f"{time.perf_counter()-t:.3f}s",
                    flush=True,
                )

            if applied % 100 == 0:
                print(
                    f"{applied}/{len(down_keys)} " f"elapsed={elapsed:.3f}s", flush=True
                )
        except Exception as e:
            print("\nFAILED")
            print("Layer:", down_key)
            print("Base :", base_key if "base_key" in locals() else "<unknown>")
            print("Error:", repr(e))
            raise


print(f"\nApplied:          {applied}")
print(f"Skipped (no base):{skipped_missing}")
print(f"Skipped (shape):  {skipped_shape}")
print(f"Skipped (non-2D): {skipped_nd}")

if applied == 0:
    print(
        "\nWARNING: 0 keys applied β€” nothing was merged. Check that the LoRA "
        "key names match the base (naming convention mismatch?)."
    )

print("\nSaving...")
save_file(merged, OUTPUT, metadata=meta)
print("Done!")

Edit: Actually I shouldn't comment on code while I'm too sleepy to read it properly haha
I'm away from home working remotely for a while, I'll try and find what I used tomorrow if that helps?

Hey man, get to it whenever you have time, I'm not in a rush for this one :)

Couldn't find my original script but I saw the convo with 10Strip on discord and his snippet looked pretty close to what I used. Praise be to Claude for helping me with my terrible python.

import torch
from safetensors import safe_open
from safetensors.torch import load_file, save_file
from tqdm import tqdm

BASE_MODEL = "base.safetensors"
LORA = "lora.safetensors"
OUTPUT = "output.safetensors"
STRENGTH = 1.0

# Preserve base metadata (LTX-2.3 carries audio VAE config here β€” dropping it
# breaks audio on the merged checkpoint).
print("Reading base metadata...")
with safe_open(BASE_MODEL, framework="pt", device="cpu") as f:
    meta = f.metadata() or {}

print("Loading base model...")
base = load_file(BASE_MODEL)

print("Loading LoRA...")
lora = load_file(LORA)

# NOTE: operate on `base` directly rather than merged = dict(base).
# A shallow dict copy keeps every original tensor alive via `base`'s
# own references even after we overwrite the entry in `merged`, roughly
# doubling peak RAM across every touched layer. Aliasing instead lets
# each replaced tensor's old bf16 copy get garbage-collected immediately
# once merged[base_key] is reassigned.
merged = base

# Find every lora_A tensor (this LoRA uses PEFT/diffusers-style naming:
# lora_A/lora_B, not lora_down/lora_up)
down_keys = [k for k in lora.keys() if "lora_A.weight" in k]

applied = 0
skipped_missing = 0
skipped_shape = 0
skipped_nd = 0

for down_key in tqdm(down_keys):
    up_key = down_key.replace("lora_A", "lora_B")
    alpha_key = down_key.replace(".lora_A.weight", ".alpha")
    # Base model keys carry an extra "model." prefix that the LoRA doesn't
    # have (LoRA: "diffusion_model...." / base: "model.diffusion_model....")
    base_key = "model." + down_key.replace(".lora_A.weight", ".weight")

    if base_key not in merged:
        skipped_missing += 1
        continue

    if up_key not in lora:
        print(f"Missing lora_up for {down_key}")
        skipped_missing += 1
        continue

    W = merged[base_key]

    # Only 2D Linear weights are LoRA-targetable. Anything else (modulation
    # tables, norms) must be left alone.
    if W.dim() != 2:
        print(f"Non-2D, skipping {base_key} {tuple(W.shape)}")
        skipped_nd += 1
        continue

    A = lora[down_key]
    B = lora[up_key]
    rank = A.shape[0]

    if alpha_key in lora:
        alpha = float(lora[alpha_key].item())
    else:
        alpha = rank

    scale = alpha / rank

    delta = torch.matmul(B.float(), A.float())
    delta *= scale
    delta *= STRENGTH

    # CRITICAL: without this, a mismatched delta silently BROADCASTS and
    # corrupts the weight with no error. LTX-2.3 has architectural tensors
    # (e.g. scale_shift_table 9-vs-6 slot differences) that hit this.
    if delta.shape != W.shape:
        print(f"Shape mismatch, skipping {base_key}: "
              f"W{tuple(W.shape)} vs delta{tuple(delta.shape)}")
        skipped_shape += 1
        continue

    merged[base_key] = W + delta.to(W.dtype)
    applied += 1

    del A, B, delta

print(f"\nApplied:          {applied}")
print(f"Skipped (no base):{skipped_missing}")
print(f"Skipped (shape):  {skipped_shape}")
print(f"Skipped (non-2D): {skipped_nd}")

if applied == 0:
    print("\nWARNING: 0 keys applied β€” nothing was merged. Check that the LoRA "
          "key names match the base (naming convention mismatch?).")

print("\nSaving...")
save_file(merged, OUTPUT, metadata=meta)
print("Done!")

aah yes, that was me asking TenStrip for help on a script i wrote, i gave up because it couldn't find the weights, unfortunately i don't have any vibe coding skills :( . I'm too old school for that lol, Thank you for the above brother, will test it and make modifications where needed <3

Sign up or log in to comment