Instructions to use InstantX/MiniMax-H3-Turbo-Lora-Diffusers with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use InstantX/MiniMax-H3-Turbo-Lora-Diffusers with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline from diffusers.utils import export_to_video # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("MiniMaxAI/MiniMax-H3", dtype=torch.bfloat16, device_map="cuda") pipe.load_lora_weights("InstantX/MiniMax-H3-Turbo-Lora-Diffusers") prompt = "A man with short gray hair plays a red electric guitar." output = pipe(prompt=prompt).frames[0] export_to_video(output, "output.mp4") - PEFT
How to use InstantX/MiniMax-H3-Turbo-Lora-Diffusers with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- Draw Things
File size: 7,102 Bytes
d290a02 | 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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | """Convert a MiniMax-H3 Turbo LoRA (ComfyUI / generate.py layout) to Diffusers PEFT layout.
The Turbo LoRA ships with ComfyUI module names and fused projections:
* ``blocks.*.attn.qkv_proj`` — fused ``[q_all; k_all; v_all]`` (the in-memory layout after
ComfyUI's load-time QKV reorder), not the raw checkpoint's per-head interleave.
* ``blocks.*.mlp.fc1`` — fused ``[gate; value]``; Diffusers' ``SwiGLU`` wants ``[value; gate]``.
* ``alpha == rank`` — no extra scale (``W_eff = W + B @ A``).
This script renames everything onto ``MiniMaxH3Transformer3DModel``, splits the fused QKV LoRA
into ``to_q`` / ``to_k`` / ``to_v`` (shared ``A``, split ``B``), and swaps the ``fc1`` halves so the
low-rank update matches the converted base weights.
Usage:
```bash
python convert.py \
--input minimax_h3_turbo_4step_ckpt500.safetensors \
--output minimax_h3_turbo_4step_ckpt500_diffusers.safetensors
```
If ``--output`` is omitted, ``_diffusers`` is inserted before the ``.safetensors`` suffix.
"""
from __future__ import annotations
import argparse
from collections import defaultdict
from pathlib import Path
import torch
from safetensors.torch import load_file, save_file
# Must match MiniMaxH3Transformer3DModel / convert_minimax_h3_to_diffusers.py.
NUM_ATTENTION_HEADS = 56
ATTENTION_HEAD_DIM = 128
INNER_DIM = NUM_ATTENTION_HEADS * ATTENTION_HEAD_DIM # 7168
FFN_DIM = 14336
PREFIX = "transformer"
def _module_names(state_dict: dict[str, torch.Tensor]) -> list[str]:
return sorted({key.rsplit(".lora_", 1)[0] for key in state_dict})
def _rename_base(name: str) -> str:
"""Map a ComfyUI module path (without ``.lora_*``) onto the Diffusers module path."""
if name.startswith("token_refiner.blocks."):
name = name.replace("token_refiner.blocks.", "token_refiner.refiner_blocks.", 1)
elif name.startswith("blocks."):
name = name.replace("blocks.", "transformer_blocks.", 1)
name = name.replace("final_layer.adaln_proj.linear", "norm_out.linear")
name = name.replace(".attn.out_proj", ".attn.to_out.0")
name = name.replace(".mlp.fc2", ".ff.net.2")
name = name.replace(".mlp.fc1", ".ff.net.0.proj")
return name
def convert_lora_state_dict(
src: dict[str, torch.Tensor],
) -> tuple[dict[str, torch.Tensor], dict[str, int]]:
"""Convert one Turbo LoRA state dict into Diffusers PEFT keys (with ``transformer.`` prefix)."""
out: dict[str, torch.Tensor] = {}
counts: dict[str, int] = defaultdict(int)
for name in _module_names(src):
a = src[f"{name}.lora_A.weight"]
b = src[f"{name}.lora_B.weight"]
if a.ndim != 2 or b.ndim != 2:
raise ValueError(f"{name}: expected 2-D LoRA matrices, got A{tuple(a.shape)} B{tuple(b.shape)}")
if a.shape[0] != b.shape[1]:
raise ValueError(f"{name}: rank mismatch A{tuple(a.shape)} vs B{tuple(b.shape)}")
# qkv: split fused [q;k;v] B into three LoRAs that share A.
if name.endswith(".attn.qkv_proj"):
if b.shape[0] != 3 * INNER_DIM:
raise ValueError(
f"{name}: fused qkv B has {b.shape[0]} rows, expected {3 * INNER_DIM} "
f"(= 3 * {INNER_DIM})."
)
base = _rename_base(name[: -len(".attn.qkv_proj")])
bq, bk, bv = b.split(INNER_DIM, dim=0)
for suffix, b_part in (("to_q", bq), ("to_k", bk), ("to_v", bv)):
key = f"{PREFIX}.{base}.attn.{suffix}"
# Clone A so to_q/to_k/to_v do not share storage (safetensors forbids that).
out[f"{key}.lora_A.weight"] = a.detach().clone().contiguous()
out[f"{key}.lora_B.weight"] = b_part.detach().clone().contiguous()
counts["qkv_split"] += 1
continue
base = _rename_base(name)
# fc1 / SwiGLU: reference stores [gate; value], Diffusers wants [value; gate].
if name.endswith(".mlp.fc1"):
if b.shape[0] != 2 * FFN_DIM:
raise ValueError(
f"{name}: fc1 B has {b.shape[0]} rows, expected {2 * FFN_DIM} (= 2 * {FFN_DIM})."
)
gate, value = b.chunk(2, dim=0)
b = torch.cat([value, gate], dim=0).contiguous()
counts["fc1_swap"] += 1
else:
counts["rename"] += 1
key = f"{PREFIX}.{base}"
out[f"{key}.lora_A.weight"] = a.detach().clone().contiguous()
out[f"{key}.lora_B.weight"] = b.detach().clone().contiguous()
return out, dict(counts)
def network_alphas_from_state_dict(state_dict: dict[str, torch.Tensor]) -> dict[str, float]:
"""``alpha == rank`` for every module (Turbo LoRA convention)."""
alphas: dict[str, float] = {}
for key, tensor in state_dict.items():
if key.endswith(".lora_B.weight") and tensor.ndim > 1:
base = key[: -len(".lora_B.weight")]
alphas[f"{base}.alpha"] = float(tensor.shape[1])
return alphas
def default_output_path(input_path: str | Path) -> Path:
path = Path(input_path)
stem = path.stem
if stem.endswith("_diffusers"):
return path
return path.with_name(f"{stem}_diffusers{path.suffix}")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
"--input",
required=True,
help="ComfyUI / generate.py Turbo LoRA safetensors "
"(e.g. minimax_h3_turbo_4step_ckpt500.safetensors from larryvrh/MiniMax-H3-Turbo-Lora)",
)
parser.add_argument(
"--output",
default=None,
help="Diffusers PEFT LoRA safetensors (keys prefixed with transformer.). "
"Defaults to <input_stem>_diffusers.safetensors",
)
args = parser.parse_args()
output = Path(args.output) if args.output is not None else default_output_path(args.input)
print(f"loading {args.input}")
src = load_file(args.input, device="cpu")
dst, counts = convert_lora_state_dict(src)
ranks = sorted({int(v.shape[1]) for k, v in dst.items() if k.endswith(".lora_B.weight")})
print(
f"converted {len(src)} -> {len(dst)} tensors; "
f"qkv_split={counts.get('qkv_split', 0)} "
f"fc1_swap={counts.get('fc1_swap', 0)} "
f"rename={counts.get('rename', 0)}; ranks={ranks}"
)
# Keep the original dtype (bf16). Metadata is informational for humans / loaders.
metadata = {
"format": "pt",
"base_model": "MiniMax-H3",
"application": "W_eff = W + lora_B @ lora_A (alpha == rank)",
"sampler_steps": "4",
"converted_from": "larryvrh/MiniMax-H3-Turbo-Lora (ComfyUI layout)",
}
save_file(dst, str(output), metadata=metadata)
print(f"saved {output}")
# Print a tiny alpha hint so callers can wire network_alphas correctly.
alphas = network_alphas_from_state_dict(dst)
alpha_values = sorted(set(alphas.values()))
print(f"network alphas (== rank): {alpha_values} across {len(alphas)} modules")
if __name__ == "__main__":
main()
|