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
| """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() | |