wanghaofan commited on
Commit
d290a02
·
verified ·
1 Parent(s): 41e8e59

Upload 5 files

Browse files
README.md CHANGED
@@ -1,3 +1,151 @@
1
  ---
2
  license: apache-2.0
 
 
 
 
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
+ base_model: MiniMaxAI/MiniMax-H3
4
+ base_model_relation: adapter
5
+ tags:
6
+ - text-to-video
7
+ - text-to-audio
8
+ - audio-video
9
+ - lora
10
+ - minimax-h3
11
+ - diffusers
12
+ - peft
13
+ pipeline_tag: text-to-video
14
+ library_name: diffusers
15
  ---
16
+
17
+ # MiniMax-H3 Turbo LoRA — Diffusers
18
+
19
+ Diffusers / PEFT conversion of [`larryvrh/MiniMax-H3-Turbo-Lora`](https://huggingface.co/larryvrh/MiniMax-H3-Turbo-Lora): a LoRA that lets [MiniMax-H3](https://huggingface.co/MiniMaxAI/MiniMax-H3) render joint **video + synchronized stereo audio** in about **4 sampling steps** instead of the usual ~20.
20
+
21
+ This repo ships:
22
+
23
+ - converted LoRA weights in Diffusers PEFT layout (`transformer.*.lora_A/B.weight`)
24
+ - `convert.py` to turn the original ComfyUI / `generate.py` safetensors into that layout
25
+
26
+ > ⚠️ **Early prototype.** Same caveat as the upstream release: under-trained preview weights, not production quality. They already beat the base model at 4 steps (sharper detail, cleaner / better-synced audio), but treat this as a work-in-progress taste, not a finished product. Prefer the non-EMA `ckpt500` weights by default.
27
+
28
+ ## Weights
29
+
30
+ Converted from the upstream Turbo LoRA (bf16, `W_eff = W + lora_B @ lora_A`, **alpha = rank** so scale is 1). QKV is split into `to_q` / `to_k` / `to_v`, and SwiGLU `fc1` halves are swapped to match Diffusers' `[value; gate]` layout.
31
+
32
+ | file | source (ComfyUI layout) | notes |
33
+ |---|---|---|
34
+ | `minimax_h3_turbo_4step_ckpt500_diffusers.safetensors` | `minimax_h3_turbo_4step_ckpt500.safetensors` | **recommended default** — newest non-EMA @ ~500 steps, usually sharpest |
35
+ | `minimax_h3_turbo_4step_ema_ckpt500_diffusers.safetensors` | `minimax_h3_turbo_4step_ema_ckpt500.safetensors` | EMA @ ~500 steps — smoother, but early EMA can show **ghosting / motion smear** |
36
+
37
+ Ranks: attention / MLP = 64, AdaLN = 16. Keys are prefixed with `transformer.` for `MiniMaxH3Transformer3DModel.load_lora_adapter`.
38
+
39
+ ## Requirements
40
+
41
+ MiniMax-H3 is not in a released Diffusers build yet. Install Diffusers from `main`, plus PEFT:
42
+
43
+ ```bash
44
+ pip install torch torchvision --index-url https://download.pytorch.org/whl/cu126
45
+ pip install -r requirements.txt
46
+ pip install git+https://github.com/huggingface/diffusers.git
47
+ ```
48
+
49
+ Base weights: Diffusers-format [MiniMaxAI/MiniMax-H3](https://huggingface.co/MiniMaxAI/MiniMax-H3) (or your local conversion). Use a **non-pruned** DiT; pruned time-conditioning layouts are **not** compatible with this LoRA (same restriction as upstream).
50
+
51
+ ## Quick start (Diffusers)
52
+
53
+ ```python
54
+ import torch
55
+ from diffusers import ComponentsManager, ModularPipeline
56
+ from diffusers.utils.export_utils import encode_video
57
+ from huggingface_hub import hf_hub_download
58
+ from safetensors.torch import load_file
59
+
60
+ def network_alphas_alpha_eq_rank(state_dict):
61
+ # Turbo LoRA: alpha == rank. Required when ranks differ (attn/mlp=64, adaln=16).
62
+ alphas = {}
63
+ for key, tensor in state_dict.items():
64
+ if key.endswith(".lora_B.weight") and tensor.ndim > 1:
65
+ base = key[: -len(".lora_B.weight")]
66
+ alphas[f"{base}.alpha"] = float(tensor.shape[1])
67
+ return alphas
68
+
69
+ lora_path = hf_hub_download(
70
+ "InstantX/MiniMax-H3-Turbo-Lora-Diffusers",
71
+ "minimax_h3_turbo_4step_ckpt500_diffusers.safetensors",
72
+ )
73
+
74
+ manager = ComponentsManager()
75
+ pipe = ModularPipeline.from_pretrained("MiniMaxAI/MiniMax-H3", components_manager=manager)
76
+ pipe.load_components(dtype=torch.bfloat16)
77
+
78
+ lora_sd = load_file(lora_path, device="cpu")
79
+ pipe.transformer.load_lora_adapter(
80
+ lora_sd,
81
+ prefix="transformer",
82
+ adapter_name="turbo_4step",
83
+ network_alphas=network_alphas_alpha_eq_rank(lora_sd),
84
+ )
85
+
86
+ # Load LoRA *before* enabling offload so PEFT injects into resident modules.
87
+ manager.enable_auto_cpu_offload(device="cuda", memory_reserve_margin="12GB")
88
+
89
+ # Optional: FlashAttention-3 on Hopper (kernels from the Hub).
90
+ try:
91
+ pipe.transformer.set_attention_backend("_flash_3_hub")
92
+ except Exception:
93
+ pipe.transformer.set_attention_backend("native")
94
+
95
+ # MiniMaxH3Scheduler: num_inference_steps is the sigma grid length *including* terminal 0,
96
+ # so it drives (num_inference_steps - 1) model evals.
97
+ # 5 -> 4 evals (matches upstream generate.py --steps 4)
98
+ # 7–9 -> 6–8 evals (upstream comfort zone for sharpness at this early checkpoint)
99
+ results = pipe(
100
+ prompt="A corgi in a chef hat flipping a pancake, sizzling sounds and a cheerful bark.",
101
+ num_frames=124, # 17*k+5, ~5.17s @ 24fps
102
+ height=768,
103
+ width=1344,
104
+ num_inference_steps=5,
105
+ generator=torch.Generator().manual_seed(42),
106
+ output=["videos", "audio", "sampling_rate"],
107
+ )
108
+
109
+ encode_video(
110
+ results["videos"][0],
111
+ fps=24,
112
+ output_path="out.mp4",
113
+ audio=results["audio"][0],
114
+ audio_sample_rate=results["sampling_rate"],
115
+ )
116
+ ```
117
+
118
+ Diffusers already runs **dual video / audio schedules** (`scheduler` shift 12, `audio_scheduler` shift 3). You do **not** need the ComfyUI Turbo custom sampler node; a wrong single-schedule sampler is what blows up audio at 4 steps in ComfyUI.
119
+
120
+ ## Convert from the original Turbo LoRA
121
+
122
+ Original weights live in [`larryvrh/MiniMax-H3-Turbo-Lora`](https://huggingface.co/larryvrh/MiniMax-H3-Turbo-Lora) (ComfyUI module names, fused `qkv_proj` / `mlp.fc1`).
123
+
124
+ ```bash
125
+ pip install safetensors torch
126
+
127
+ python convert.py \
128
+ --input minimax_h3_turbo_4step_ckpt500.safetensors \
129
+ --output minimax_h3_turbo_4step_ckpt500_diffusers.safetensors
130
+ ```
131
+
132
+ What `convert.py` does:
133
+
134
+ 1. Renames ComfyUI paths onto `MiniMaxH3Transformer3DModel` (`blocks.*` → `transformer_blocks.*`, `mlp.fc*` → `ff.net.*`, `final_layer.adaln_proj` → `norm_out.linear`, …).
135
+ 2. Splits fused `attn.qkv_proj` LoRA into `to_q` / `to_k` / `to_v` (shared `A`, row-split `B` in `[q_all; k_all; v_all]` layout).
136
+ 3. Swaps `mlp.fc1` LoRA halves from `[gate; value]` to Diffusers SwiGLU `[value; gate]`.
137
+ 4. Writes keys with a `transformer.` prefix for `load_lora_adapter`.
138
+
139
+ ## Notes
140
+
141
+ - **Steps**: 4 model evals (`num_inference_steps=5`) works; at this early checkpoint **6–8 evals** (`num_inference_steps=7…9`) are usually sharper. Any count ≥ 4 evals is valid; more steps look better.
142
+ - **Resolution / duration**: `height` / `width` multiples of 32 (short edge typically 768). `num_frames` at 24 fps snaps up to the video VAE’s `17·k+5` grid (124 ≈ 5 s). Validated roughly 5–15 s.
143
+ - **VRAM**: the base DiT is ~33B. An 80–96 GB GPU is comfortable with `ComponentsManager.enable_auto_cpu_offload`; smaller cards need quantization / group offload as in the [MiniMax-H3 Diffusers docs](https://huggingface.co/docs/diffusers/main/en/api/pipelines/minimax_h3).
144
+ - **Audio**: 32 kHz stereo aligned to the video; video and audio ride different flow schedules inside one transformer call.
145
+ - **ComfyUI**: for the original graph / custom Turbo sampler, use the [upstream repo](https://huggingface.co/larryvrh/MiniMax-H3-Turbo-Lora) and [Larryvrh/ComfyUI-MiniMax-H3-Turbo](https://github.com/Larryvrh/ComfyUI-MiniMax-H3-Turbo).
146
+
147
+ ## Credit
148
+
149
+ - Turbo LoRA training & original release: [`larryvrh/MiniMax-H3-Turbo-Lora`](https://huggingface.co/larryvrh/MiniMax-H3-Turbo-Lora)
150
+ - Base model: [`MiniMaxAI/MiniMax-H3`](https://huggingface.co/MiniMaxAI/MiniMax-H3)
151
+ - Diffusers MiniMax-H3 integration: Hugging Face Diffusers (modular pipeline)
convert.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Convert a MiniMax-H3 Turbo LoRA (ComfyUI / generate.py layout) to Diffusers PEFT layout.
2
+
3
+ The Turbo LoRA ships with ComfyUI module names and fused projections:
4
+
5
+ * ``blocks.*.attn.qkv_proj`` — fused ``[q_all; k_all; v_all]`` (the in-memory layout after
6
+ ComfyUI's load-time QKV reorder), not the raw checkpoint's per-head interleave.
7
+ * ``blocks.*.mlp.fc1`` — fused ``[gate; value]``; Diffusers' ``SwiGLU`` wants ``[value; gate]``.
8
+ * ``alpha == rank`` — no extra scale (``W_eff = W + B @ A``).
9
+
10
+ This script renames everything onto ``MiniMaxH3Transformer3DModel``, splits the fused QKV LoRA
11
+ into ``to_q`` / ``to_k`` / ``to_v`` (shared ``A``, split ``B``), and swaps the ``fc1`` halves so the
12
+ low-rank update matches the converted base weights.
13
+
14
+ Usage:
15
+
16
+ ```bash
17
+ python convert.py \
18
+ --input minimax_h3_turbo_4step_ckpt500.safetensors \
19
+ --output minimax_h3_turbo_4step_ckpt500_diffusers.safetensors
20
+ ```
21
+
22
+ If ``--output`` is omitted, ``_diffusers`` is inserted before the ``.safetensors`` suffix.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ from collections import defaultdict
29
+ from pathlib import Path
30
+
31
+ import torch
32
+ from safetensors.torch import load_file, save_file
33
+
34
+ # Must match MiniMaxH3Transformer3DModel / convert_minimax_h3_to_diffusers.py.
35
+ NUM_ATTENTION_HEADS = 56
36
+ ATTENTION_HEAD_DIM = 128
37
+ INNER_DIM = NUM_ATTENTION_HEADS * ATTENTION_HEAD_DIM # 7168
38
+ FFN_DIM = 14336
39
+ PREFIX = "transformer"
40
+
41
+
42
+ def _module_names(state_dict: dict[str, torch.Tensor]) -> list[str]:
43
+ return sorted({key.rsplit(".lora_", 1)[0] for key in state_dict})
44
+
45
+
46
+ def _rename_base(name: str) -> str:
47
+ """Map a ComfyUI module path (without ``.lora_*``) onto the Diffusers module path."""
48
+ if name.startswith("token_refiner.blocks."):
49
+ name = name.replace("token_refiner.blocks.", "token_refiner.refiner_blocks.", 1)
50
+ elif name.startswith("blocks."):
51
+ name = name.replace("blocks.", "transformer_blocks.", 1)
52
+
53
+ name = name.replace("final_layer.adaln_proj.linear", "norm_out.linear")
54
+ name = name.replace(".attn.out_proj", ".attn.to_out.0")
55
+ name = name.replace(".mlp.fc2", ".ff.net.2")
56
+ name = name.replace(".mlp.fc1", ".ff.net.0.proj")
57
+ return name
58
+
59
+
60
+ def convert_lora_state_dict(
61
+ src: dict[str, torch.Tensor],
62
+ ) -> tuple[dict[str, torch.Tensor], dict[str, int]]:
63
+ """Convert one Turbo LoRA state dict into Diffusers PEFT keys (with ``transformer.`` prefix)."""
64
+ out: dict[str, torch.Tensor] = {}
65
+ counts: dict[str, int] = defaultdict(int)
66
+
67
+ for name in _module_names(src):
68
+ a = src[f"{name}.lora_A.weight"]
69
+ b = src[f"{name}.lora_B.weight"]
70
+ if a.ndim != 2 or b.ndim != 2:
71
+ raise ValueError(f"{name}: expected 2-D LoRA matrices, got A{tuple(a.shape)} B{tuple(b.shape)}")
72
+ if a.shape[0] != b.shape[1]:
73
+ raise ValueError(f"{name}: rank mismatch A{tuple(a.shape)} vs B{tuple(b.shape)}")
74
+
75
+ # qkv: split fused [q;k;v] B into three LoRAs that share A.
76
+ if name.endswith(".attn.qkv_proj"):
77
+ if b.shape[0] != 3 * INNER_DIM:
78
+ raise ValueError(
79
+ f"{name}: fused qkv B has {b.shape[0]} rows, expected {3 * INNER_DIM} "
80
+ f"(= 3 * {INNER_DIM})."
81
+ )
82
+ base = _rename_base(name[: -len(".attn.qkv_proj")])
83
+ bq, bk, bv = b.split(INNER_DIM, dim=0)
84
+ for suffix, b_part in (("to_q", bq), ("to_k", bk), ("to_v", bv)):
85
+ key = f"{PREFIX}.{base}.attn.{suffix}"
86
+ # Clone A so to_q/to_k/to_v do not share storage (safetensors forbids that).
87
+ out[f"{key}.lora_A.weight"] = a.detach().clone().contiguous()
88
+ out[f"{key}.lora_B.weight"] = b_part.detach().clone().contiguous()
89
+ counts["qkv_split"] += 1
90
+ continue
91
+
92
+ base = _rename_base(name)
93
+
94
+ # fc1 / SwiGLU: reference stores [gate; value], Diffusers wants [value; gate].
95
+ if name.endswith(".mlp.fc1"):
96
+ if b.shape[0] != 2 * FFN_DIM:
97
+ raise ValueError(
98
+ f"{name}: fc1 B has {b.shape[0]} rows, expected {2 * FFN_DIM} (= 2 * {FFN_DIM})."
99
+ )
100
+ gate, value = b.chunk(2, dim=0)
101
+ b = torch.cat([value, gate], dim=0).contiguous()
102
+ counts["fc1_swap"] += 1
103
+ else:
104
+ counts["rename"] += 1
105
+
106
+ key = f"{PREFIX}.{base}"
107
+ out[f"{key}.lora_A.weight"] = a.detach().clone().contiguous()
108
+ out[f"{key}.lora_B.weight"] = b.detach().clone().contiguous()
109
+
110
+ return out, dict(counts)
111
+
112
+
113
+ def network_alphas_from_state_dict(state_dict: dict[str, torch.Tensor]) -> dict[str, float]:
114
+ """``alpha == rank`` for every module (Turbo LoRA convention)."""
115
+ alphas: dict[str, float] = {}
116
+ for key, tensor in state_dict.items():
117
+ if key.endswith(".lora_B.weight") and tensor.ndim > 1:
118
+ base = key[: -len(".lora_B.weight")]
119
+ alphas[f"{base}.alpha"] = float(tensor.shape[1])
120
+ return alphas
121
+
122
+
123
+ def default_output_path(input_path: str | Path) -> Path:
124
+ path = Path(input_path)
125
+ stem = path.stem
126
+ if stem.endswith("_diffusers"):
127
+ return path
128
+ return path.with_name(f"{stem}_diffusers{path.suffix}")
129
+
130
+
131
+ def main() -> None:
132
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
133
+ parser.add_argument(
134
+ "--input",
135
+ required=True,
136
+ help="ComfyUI / generate.py Turbo LoRA safetensors "
137
+ "(e.g. minimax_h3_turbo_4step_ckpt500.safetensors from larryvrh/MiniMax-H3-Turbo-Lora)",
138
+ )
139
+ parser.add_argument(
140
+ "--output",
141
+ default=None,
142
+ help="Diffusers PEFT LoRA safetensors (keys prefixed with transformer.). "
143
+ "Defaults to <input_stem>_diffusers.safetensors",
144
+ )
145
+ args = parser.parse_args()
146
+ output = Path(args.output) if args.output is not None else default_output_path(args.input)
147
+
148
+ print(f"loading {args.input}")
149
+ src = load_file(args.input, device="cpu")
150
+ dst, counts = convert_lora_state_dict(src)
151
+
152
+ ranks = sorted({int(v.shape[1]) for k, v in dst.items() if k.endswith(".lora_B.weight")})
153
+ print(
154
+ f"converted {len(src)} -> {len(dst)} tensors; "
155
+ f"qkv_split={counts.get('qkv_split', 0)} "
156
+ f"fc1_swap={counts.get('fc1_swap', 0)} "
157
+ f"rename={counts.get('rename', 0)}; ranks={ranks}"
158
+ )
159
+
160
+ # Keep the original dtype (bf16). Metadata is informational for humans / loaders.
161
+ metadata = {
162
+ "format": "pt",
163
+ "base_model": "MiniMax-H3",
164
+ "application": "W_eff = W + lora_B @ lora_A (alpha == rank)",
165
+ "sampler_steps": "4",
166
+ "converted_from": "larryvrh/MiniMax-H3-Turbo-Lora (ComfyUI layout)",
167
+ }
168
+ save_file(dst, str(output), metadata=metadata)
169
+ print(f"saved {output}")
170
+
171
+ # Print a tiny alpha hint so callers can wire network_alphas correctly.
172
+ alphas = network_alphas_from_state_dict(dst)
173
+ alpha_values = sorted(set(alphas.values()))
174
+ print(f"network alphas (== rank): {alpha_values} across {len(alphas)} modules")
175
+
176
+
177
+ if __name__ == "__main__":
178
+ main()
minimax_h3_turbo_4step_ckpt500_diffusers.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ad92b79a1626df5b55bbdd300eec9380ea75c952468d104601c9fdec42bebb9a
3
+ size 851455256
minimax_h3_turbo_4step_ema_ckpt500_diffusers.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fed3db077957dc49385e36a7597d701cc2948ffa5c723d90788c373d54325dea
3
+ size 851455248
requirements.txt ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MiniMax-H3 inference (infer.py / convert.py)
2
+ # Versions pinned to the working docker env (CUDA 12.6).
3
+ #
4
+ # Install torch first from the CUDA wheel index, then the rest:
5
+ # pip install torch==2.9.1 torchvision==0.24.1 --index-url https://download.pytorch.org/whl/cu126
6
+ # pip install -r requirements.txt
7
+ #
8
+ # MiniMax-H3 is not in a released diffusers build yet — install the local checkout
9
+ # (or the PR) instead of the PyPI package:
10
+ # pip install -e ./diffusers
11
+ # # or: pip install git+https://github.com/huggingface/diffusers.git@refs/pull/14355/head
12
+
13
+ # --- PyTorch (install via the CUDA index above; listed here for reference) ---
14
+ # torch==2.9.1+cu126
15
+ # torchvision==0.24.1+cu126
16
+
17
+ torch==2.9.1
18
+ torchvision==0.24.1
19
+
20
+ # --- Hugging Face stack ---
21
+ transformers==5.14.1
22
+ accelerate==1.14.0
23
+ huggingface-hub==1.26.0
24
+ safetensors==0.8.0
25
+ peft==0.20.0
26
+ tokenizers==0.22.2
27
+ sentencepiece==0.2.0
28
+ protobuf==4.24.4
29
+
30
+ # --- Diffusers extras used by infer.py ---
31
+ kernels==0.16.0
32
+
33
+ # --- Numerics / IO ---
34
+ numpy==1.24.4
35
+ scipy==1.12.0
36
+ Pillow==10.2.0
37
+ einops==0.7.0
38
+ ftfy==6.2.0
39
+ opencv-python==4.7.0
40
+ av==17.1.0
41
+ soundfile==0.12.1
42
+ imageio==2.37.4
43
+ imageio-ffmpeg==0.6.0