MiniMax-H3-Turbo-Lora / h3_lora.py
akhaliq's picture
akhaliq HF Staff
Fold the 4-step Turbo LoRA into the transformer, default to 4 steps
d8b48ff
Raw
History Blame Contribute Delete
4.55 kB
"""Fold the MiniMax-H3 Turbo LoRA (`larryvrh/MiniMax-H3-Turbo-Lora`) into the diffusers transformer.
The LoRA ships against the *reference* (ComfyUI) module tree — `blocks.N.attn.qkv_proj`, `blocks.N.mlp.fc1`,
`token_refiner.blocks.N`, `final_layer.adaln_proj.linear` — with `alpha == rank`, so the update is exactly
`W + lora_B @ lora_A`. The diffusers checkpoint is the same weights under different names and two layout transforms
(see `scripts/convert_minimax_h3_to_diffusers.py` in huggingface/diffusers#14371), so each delta gets the same
transform the base weight got:
* fused `attn.qkv_proj` rows are `[q_all; k_all; v_all]` in both in-memory layouts -> split into contiguous thirds
onto `attn.to_q` / `to_k` / `to_v`;
* fused `mlp.fc1` is `[gate; value]` while diffusers' `SwiGLU` fuses `[value; gate]` -> swap the halves onto
`ff.net.0.proj`;
* `mlp.fc2` -> `ff.net.2`, `attn.out_proj` -> `attn.to_out.0`, `blocks.` -> `transformer_blocks.`,
`token_refiner.blocks.` -> `token_refiner.refiner_blocks.`, `final_layer.adaln_proj.linear` -> `norm_out.linear`;
* the `adaln_proj.linear` modulation tables share the `[timestep][modality][param]` row layout in both trees, so
they map name-for-name with no reordering.
The delta is folded into the bf16 weights rather than applied as a runtime wrapper for one reason: the AoTI block
package (`h3_aoti`) reads each block's live weights, and a wrapper module would be invisible to it. The fold computes
`lora_B @ lora_A` in float32 and rounds once on the way back into bf16.
"""
from __future__ import annotations
import os
import torch
LORA_REPO = os.environ.get("H3_LORA_REPO", "larryvrh/MiniMax-H3-Turbo-Lora")
# The trained weights; the `_ema` variant is an immature time-averaged snapshot at this checkpoint. `off` disables.
LORA_FILE = os.environ.get("H3_LORA", "minimax_h3_turbo_4step.safetensors")
def _delta_targets(name: str, delta: torch.Tensor, inner_dim: int) -> list[tuple[str, torch.Tensor]]:
"""Map one reference-tree LoRA base name and its `lora_B @ lora_A` delta onto diffusers parameter key(s)."""
if name.startswith("token_refiner.blocks."):
target = name.replace("token_refiner.blocks.", "token_refiner.refiner_blocks.", 1)
elif name.startswith("blocks."):
target = name.replace("blocks.", "transformer_blocks.", 1)
else:
target = name
target = target.replace("final_layer.adaln_proj.linear", "norm_out.linear")
if target.endswith(".attn.qkv_proj"):
prefix = target.removesuffix("qkv_proj")
return [
(f"{prefix}to_{kind}.weight", part.contiguous())
for kind, part in zip(("q", "k", "v"), delta.split(inner_dim, dim=0))
]
if target.endswith(".mlp.fc1"):
gate, value = delta.chunk(2, dim=0)
return [(target.replace(".mlp.fc1", ".ff.net.0.proj") + ".weight", torch.cat([value, gate]).contiguous())]
if target.endswith(".mlp.fc2"):
return [(target.replace(".mlp.fc2", ".ff.net.2") + ".weight", delta)]
if target.endswith(".attn.out_proj"):
return [(target.replace(".attn.out_proj", ".attn.to_out.0") + ".weight", delta)]
# `adaln_proj.linear` (block-level and the final `norm_out.linear`): identical row layout on both sides.
return [(target + ".weight", delta)]
def apply_lora(transformer) -> str | None:
"""Fold the configured Turbo LoRA into `transformer` in place. Returns a status line, or `None` when disabled."""
if LORA_FILE.lower() in ("", "off", "none"):
return None
from huggingface_hub import hf_hub_download
from safetensors.torch import load_file
path = hf_hub_download(LORA_REPO, LORA_FILE)
lora = load_file(path)
bases = sorted({key.rsplit(".lora_", 1)[0] for key in lora})
config = transformer.config
inner_dim = config.num_attention_heads * config.attention_head_dim
params = dict(transformer.named_parameters())
folded = 0
for name in bases:
a = lora[f"{name}.lora_A.weight"].float()
b = lora[f"{name}.lora_B.weight"].float()
delta = b @ a # alpha == rank, so the scale is 1
for key, converted in _delta_targets(name, delta, inner_dim):
param = params.get(key)
if param is None:
raise KeyError(f"LoRA target `{key}` (from `{name}`) not found in the transformer")
param.data = (param.data.float() + converted).to(param.dtype)
folded += 1
return f"LoRA `{LORA_REPO}/{LORA_FILE}` folded into {folded} weights ({len(bases)} modules)"