Spaces:
Build error
Build error
h3_lora [processing/.]
Browse files- h3_lora.py +292 -0
- h3_split_blocks.py +147 -0
h3_lora.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Turbo and style LoRA support for the diffusers MiniMax-H3 transformer: several checkpoint layouts, one fold
|
| 2 |
+
mechanism.
|
| 3 |
+
|
| 4 |
+
Every LoRA is applied by folding `scale * (lora_B @ lora_A)` into the bf16 weights rather than as runtime
|
| 5 |
+
wrappers, because the AoTI block package (`h3_aoti`) reads each block's live weights and a wrapper module would
|
| 6 |
+
be invisible to it. Deltas are computed in float32 and round once on the way back into bf16. The low-rank factors
|
| 7 |
+
of every loaded LoRA stay resident, so the active one can be switched per request (`set_active`) — unfold the
|
| 8 |
+
old, fold the new, in place, through one bf16 rounding.
|
| 9 |
+
|
| 10 |
+
The supported checkpoints ship in different layouts:
|
| 11 |
+
|
| 12 |
+
* `larry` (`larryvrh/MiniMax-H3-Turbo-Lora`) targets the reference (ComfyUI) module tree —
|
| 13 |
+
`blocks.N.attn.qkv_proj`, `blocks.N.mlp.fc1`, `token_refiner.blocks.N`, `final_layer.adaln_proj.linear` — with
|
| 14 |
+
`alpha == rank` (scale 1). Each delta gets the same transform the base weights got in the diffusers conversion
|
| 15 |
+
(scripts/convert_minimax_h3_to_diffusers.py, huggingface/diffusers#14371): fused-QKV row thirds onto
|
| 16 |
+
`attn.to_q/k/v`, the `SwiGLU` gate/value swap onto `ff.net.0.proj`, `fc2` -> `ff.net.2`, `blocks.` ->
|
| 17 |
+
`transformer_blocks.`, `token_refiner.blocks.` -> `token_refiner.refiner_blocks.`,
|
| 18 |
+
`final_layer.adaln_proj.linear` -> `norm_out.linear`. The row transforms are applied to `lora_B` directly
|
| 19 |
+
(rows of `B @ A` are rows of `B`), so no full delta is ever materialized at load.
|
| 20 |
+
|
| 21 |
+
* `lightx` / `lightx8` (`lightx2v/Minimax-h3-Turbo`) are PEFT checkpoints against the diffusers tree itself —
|
| 22 |
+
`transformer_blocks.N.attn.to_q.lora_A.default.weight` and friends — rank 128, `alpha == 8`, so the fold scale
|
| 23 |
+
is `8 / 128 = 0.0625` (matching `set_adapters(weights=1.0)` in their inference script). Keys map name-for-name.
|
| 24 |
+
`lightx` is the 4-step file, `lightx8` the 8-step v1.0 file.
|
| 25 |
+
|
| 26 |
+
* `realism` (`fal/MiniMax-H3-Realism-People-LoRA`) is a style LoRA, not a turbo one — realistic people, trigger
|
| 27 |
+
word `r34l1sm`. Same reference tree as larry under a `diffusion_model.` prefix, attention only (`qkv_proj` /
|
| 28 |
+
`out_proj` on the 52 blocks), rank 16, `alpha == rank` (the card's scale 1.0), so it goes through the same
|
| 29 |
+
`_larry_targets` mapping. It wants the full step count, not 4–6.
|
| 30 |
+
|
| 31 |
+
* `joyfox` (`joyfox/MiniMax-H3-Turbo`) is another 4-step turbo LoRA, ComfyUI-native like realism but covering
|
| 32 |
+
more of the tree: attention, MLP, the block and final AdaLN projections, and both output heads
|
| 33 |
+
(`final_layer.video_out` -> `proj_out`, `final_layer.audio_out` -> `audio_proj_out`). Ranks are mixed
|
| 34 |
+
(32 attention/MLP, 8 modulation/heads) with a scalar `alpha` per key, so each `lora_B` is prescaled by
|
| 35 |
+
`alpha / rank` at load (here always 1.0). Its `adaln_proj` entries are skipped: the Comfy-Org checkpoint it was
|
| 36 |
+
trained against gives the modulation projection an 8-dimensional input (`[96768, 8]`), while the diffusers
|
| 37 |
+
transformer projects the full 2688-dim time embedding (`[96768, 2688]`), so those deltas have no counterpart
|
| 38 |
+
to fold into.
|
| 39 |
+
|
| 40 |
+
* `H3-Facial-Realism-CloseUp` (`prithivMLmods/MiniMax-H3-Facial-Realism-CloseUp`) is a close-up facial realism
|
| 41 |
+
style LoRA, not a turbo one — it wants the full step count, not 4–6. Its layout is detected at load time:
|
| 42 |
+
PEFT keys against the diffusers tree map name-for-name, everything else is treated as the reference tree
|
| 43 |
+
(with or without the `diffusion_model.` prefix) and goes through `_larry_targets`. Per-key scalar `alpha`
|
| 44 |
+
values are folded into `lora_B` as `alpha / rank` when present, otherwise `alpha == rank` is assumed;
|
| 45 |
+
reference-tree `adaln_proj` entries whose input dim is not the 2688-dim time embedding of the diffusers tree
|
| 46 |
+
are skipped, like joyfox's.
|
| 47 |
+
|
| 48 |
+
`H3_LORA` selects the larry file (`off` skips loading it), `H3_LIGHTX=off` skips lightx, `H3_REALISM=off` skips
|
| 49 |
+
realism, `H3_JOYFOX=off` skips joyfox, `H3_LIGHTX8=off` skips the lightx 8-step file, `H3_FACIAL=off` skips the
|
| 50 |
+
facial realism LoRA, `H3_LORA_DEFAULT` picks which set starts folded, and `H3_LORA_STRENGTH` is the larry card's
|
| 51 |
+
sharpness/artifact dial.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
from __future__ import annotations
|
| 55 |
+
|
| 56 |
+
import os
|
| 57 |
+
|
| 58 |
+
import torch
|
| 59 |
+
|
| 60 |
+
LARRY_REPO = os.environ.get("H3_LORA_REPO", "larryvrh/MiniMax-H3-Turbo-Lora")
|
| 61 |
+
LARRY_FILE = os.environ.get("H3_LORA", "minimax_h3_turbo_v4_step600_ema.safetensors")
|
| 62 |
+
LIGHTX_REPO = os.environ.get("H3_LIGHTX_REPO", "lightx2v/Minimax-h3-Turbo")
|
| 63 |
+
LIGHTX_FILE = os.environ.get("H3_LIGHTX_FILE", "minimax_h3_fl2v_turbo_4step_v0.1.safetensors")
|
| 64 |
+
LIGHTX8_FILE = os.environ.get("H3_LIGHTX8_FILE", "minimax_h3_fl2v_turbo_8step_v1.0_bf16.safetensors")
|
| 65 |
+
LIGHTX_ALPHA = 8
|
| 66 |
+
REALISM_REPO = os.environ.get("H3_REALISM_REPO", "fal/MiniMax-H3-Realism-People-LoRA")
|
| 67 |
+
REALISM_FILE = os.environ.get("H3_REALISM_FILE", "h3-realism-people-t2v-i2v-r2v.safetensors")
|
| 68 |
+
JOYFOX_REPO = os.environ.get("H3_JOYFOX_REPO", "joyfox/MiniMax-H3-Turbo")
|
| 69 |
+
JOYFOX_FILE = os.environ.get("H3_JOYFOX_FILE", "minimax_h3_fl2va_4step_lora.safetensors")
|
| 70 |
+
FACIAL_REPO = os.environ.get("H3_FACIAL_REPO", "prithivMLmods/MiniMax-H3-Facial-Realism-CloseUp")
|
| 71 |
+
FACIAL_FILE = os.environ.get("H3_FACIAL_FILE", "minimax-h3-facial-realism-closeup-cp2000.safetensors")
|
| 72 |
+
FACIAL_NAME = "H3-Facial-Realism-CloseUp"
|
| 73 |
+
LARRY_STRENGTH = float(os.environ.get("H3_LORA_STRENGTH", "1.0"))
|
| 74 |
+
DEFAULT_LORA = os.environ.get("H3_LORA_DEFAULT", "larry")
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _larry_targets(name: str, b: torch.Tensor, inner_dim: int) -> list[tuple[str, torch.Tensor]]:
|
| 78 |
+
"""Map one reference-tree base name and its `lora_B` onto diffusers parameter key + row-transformed B."""
|
| 79 |
+
if name.startswith("token_refiner.blocks."):
|
| 80 |
+
target = name.replace("token_refiner.blocks.", "token_refiner.refiner_blocks.", 1)
|
| 81 |
+
elif name.startswith("blocks."):
|
| 82 |
+
target = name.replace("blocks.", "transformer_blocks.", 1)
|
| 83 |
+
else:
|
| 84 |
+
target = name
|
| 85 |
+
target = target.replace("final_layer.adaln_proj.linear", "norm_out.linear")
|
| 86 |
+
target = target.replace("final_layer.video_out", "proj_out").replace("final_layer.audio_out", "audio_proj_out")
|
| 87 |
+
|
| 88 |
+
if target.endswith(".attn.qkv_proj"):
|
| 89 |
+
prefix = target.removesuffix("qkv_proj")
|
| 90 |
+
return [
|
| 91 |
+
(f"{prefix}to_{kind}.weight", part.contiguous())
|
| 92 |
+
for kind, part in zip(("q", "k", "v"), b.split(inner_dim, dim=0))
|
| 93 |
+
]
|
| 94 |
+
if target.endswith(".mlp.fc1"):
|
| 95 |
+
gate, value = b.chunk(2, dim=0)
|
| 96 |
+
return [(target.replace(".mlp.fc1", ".ff.net.0.proj") + ".weight", torch.cat([value, gate]).contiguous())]
|
| 97 |
+
if target.endswith(".mlp.fc2"):
|
| 98 |
+
return [(target.replace(".mlp.fc2", ".ff.net.2") + ".weight", b)]
|
| 99 |
+
if target.endswith(".attn.out_proj"):
|
| 100 |
+
return [(target.replace(".attn.out_proj", ".attn.to_out.0") + ".weight", b)]
|
| 101 |
+
return [(target + ".weight", b)]
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _load_larry(inner_dim: int) -> dict:
|
| 105 |
+
from huggingface_hub import hf_hub_download
|
| 106 |
+
from safetensors.torch import load_file
|
| 107 |
+
|
| 108 |
+
lora = load_file(hf_hub_download(LARRY_REPO, LARRY_FILE))
|
| 109 |
+
bases = sorted({key.rsplit(".lora_", 1)[0] for key in lora})
|
| 110 |
+
entries = []
|
| 111 |
+
for name in bases:
|
| 112 |
+
a = lora[f"{name}.lora_A.weight"]
|
| 113 |
+
b = lora[f"{name}.lora_B.weight"]
|
| 114 |
+
entries.extend((key, a, b_part) for key, b_part in _larry_targets(name, b, inner_dim))
|
| 115 |
+
return {
|
| 116 |
+
"label": f"{LARRY_REPO}/{LARRY_FILE}",
|
| 117 |
+
"scale": LARRY_STRENGTH,
|
| 118 |
+
"entries": entries,
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _load_lightx(file: str = LIGHTX_FILE) -> dict:
|
| 123 |
+
"""A diffusers-native PEFT checkpoint from `lightx2v/Minimax-h3-Turbo` — keys map name-for-name."""
|
| 124 |
+
from huggingface_hub import hf_hub_download
|
| 125 |
+
from safetensors.torch import load_file
|
| 126 |
+
|
| 127 |
+
lora = load_file(hf_hub_download(LIGHTX_REPO, file))
|
| 128 |
+
suffix_a, suffix_b = ".lora_A.default.weight", ".lora_B.default.weight"
|
| 129 |
+
bases = sorted({key[: -len(suffix_a)] for key in lora if key.endswith(suffix_a)})
|
| 130 |
+
ranks = {lora[f"{name}{suffix_a}"].shape[0] for name in bases}
|
| 131 |
+
if len(ranks) != 1:
|
| 132 |
+
raise ValueError(f"Mixed LoRA ranks in {file}: {sorted(ranks)}")
|
| 133 |
+
entries = [(f"{name}.weight", lora[f"{name}{suffix_a}"], lora[f"{name}{suffix_b}"]) for name in bases]
|
| 134 |
+
return {
|
| 135 |
+
"label": f"{LIGHTX_REPO}/{file}",
|
| 136 |
+
"scale": LIGHTX_ALPHA / ranks.pop(),
|
| 137 |
+
"entries": entries,
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _load_realism(inner_dim: int) -> dict:
|
| 142 |
+
from huggingface_hub import hf_hub_download
|
| 143 |
+
from safetensors.torch import load_file
|
| 144 |
+
|
| 145 |
+
lora = load_file(hf_hub_download(REALISM_REPO, REALISM_FILE))
|
| 146 |
+
bases = sorted({key.rsplit(".lora_", 1)[0].removeprefix("diffusion_model.") for key in lora})
|
| 147 |
+
entries = []
|
| 148 |
+
for name in bases:
|
| 149 |
+
a = lora[f"diffusion_model.{name}.lora_A.weight"]
|
| 150 |
+
b = lora[f"diffusion_model.{name}.lora_B.weight"]
|
| 151 |
+
entries.extend((key, a, b_part) for key, b_part in _larry_targets(name, b, inner_dim))
|
| 152 |
+
return {
|
| 153 |
+
"label": f"{REALISM_REPO}/{REALISM_FILE}",
|
| 154 |
+
"scale": 1.0,
|
| 155 |
+
"entries": entries,
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def _load_joyfox(inner_dim: int) -> dict:
|
| 160 |
+
from huggingface_hub import hf_hub_download
|
| 161 |
+
from safetensors.torch import load_file
|
| 162 |
+
|
| 163 |
+
lora = load_file(hf_hub_download(JOYFOX_REPO, JOYFOX_FILE))
|
| 164 |
+
bases = sorted({key.rsplit(".lora_", 1)[0].removeprefix("diffusion_model.") for key in lora if ".lora_" in key})
|
| 165 |
+
entries = []
|
| 166 |
+
for name in bases:
|
| 167 |
+
if "adaln_proj" in name:
|
| 168 |
+
continue
|
| 169 |
+
prefixed = f"diffusion_model.{name}"
|
| 170 |
+
a = lora[f"{prefixed}.lora_A.weight"]
|
| 171 |
+
b = lora[f"{prefixed}.lora_B.weight"] * (lora[f"{prefixed}.alpha"].item() / a.shape[0])
|
| 172 |
+
entries.extend((key, a, b_part) for key, b_part in _larry_targets(name, b, inner_dim))
|
| 173 |
+
return {
|
| 174 |
+
"label": f"{JOYFOX_REPO}/{JOYFOX_FILE}",
|
| 175 |
+
"scale": 1.0,
|
| 176 |
+
"entries": entries,
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def _load_facial(inner_dim: int) -> dict:
|
| 181 |
+
"""Close-up facial realism style LoRA. The checkpoint layout is detected at load time: PEFT keys against the
|
| 182 |
+
diffusers tree map name-for-name, everything else is treated as the reference tree (with or without a
|
| 183 |
+
`diffusion_model.` prefix) and goes through `_larry_targets`. Per-key scalar `alpha` is folded into `lora_B`
|
| 184 |
+
as `alpha / rank`; reference-tree `adaln_proj` entries whose input dim is not the 2688-dim time embedding of
|
| 185 |
+
the diffusers tree are skipped, like joyfox's."""
|
| 186 |
+
from huggingface_hub import hf_hub_download
|
| 187 |
+
from safetensors.torch import load_file
|
| 188 |
+
|
| 189 |
+
lora = load_file(hf_hub_download(FACIAL_REPO, FACIAL_FILE))
|
| 190 |
+
label = f"{FACIAL_REPO}/{FACIAL_FILE}"
|
| 191 |
+
|
| 192 |
+
suffix_a, suffix_b = ".lora_A.default.weight", ".lora_B.default.weight"
|
| 193 |
+
if any(key.endswith(suffix_a) for key in lora):
|
| 194 |
+
entries = []
|
| 195 |
+
for base in sorted({key[: -len(suffix_a)] for key in lora if key.endswith(suffix_a)}):
|
| 196 |
+
a = lora[f"{base}{suffix_a}"]
|
| 197 |
+
b = lora[f"{base}{suffix_b}"]
|
| 198 |
+
alpha = lora.get(f"{base}.alpha")
|
| 199 |
+
if alpha is not None:
|
| 200 |
+
b = b * (alpha.item() / a.shape[0])
|
| 201 |
+
name = base.removeprefix("transformer.")
|
| 202 |
+
entries.append((f"{name}.weight", a, b))
|
| 203 |
+
return {"label": label, "scale": 1.0, "entries": entries}
|
| 204 |
+
|
| 205 |
+
prefix = "diffusion_model." if any(key.startswith("diffusion_model.") for key in lora) else ""
|
| 206 |
+
entries = []
|
| 207 |
+
for name in sorted({key.rsplit(".lora_", 1)[0].removeprefix(prefix) for key in lora if ".lora_" in key}):
|
| 208 |
+
full = f"{prefix}{name}"
|
| 209 |
+
a = lora[f"{full}.lora_A.weight"]
|
| 210 |
+
if "adaln_proj" in name and a.shape[1] != 2688:
|
| 211 |
+
continue
|
| 212 |
+
b = lora[f"{full}.lora_B.weight"]
|
| 213 |
+
alpha = lora.get(f"{full}.alpha")
|
| 214 |
+
if alpha is not None:
|
| 215 |
+
b = b * (alpha.item() / a.shape[0])
|
| 216 |
+
entries.extend((key, a, b_part) for key, b_part in _larry_targets(name, b, inner_dim))
|
| 217 |
+
return {"label": label, "scale": 1.0, "entries": entries}
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def _apply(entries, params, sign: float) -> None:
|
| 221 |
+
for key, a, b in entries:
|
| 222 |
+
param = params.get(key)
|
| 223 |
+
if param is None:
|
| 224 |
+
raise KeyError(f"LoRA target `{key}` not found in the transformer")
|
| 225 |
+
delta = sign * (b.to(torch.float32) @ a.to(torch.float32))
|
| 226 |
+
param.data = (param.data.float() + delta.to(param.device)).to(param.dtype)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def available() -> list[str]:
|
| 230 |
+
"""The LoRA sets that were loaded at startup, plus `off`."""
|
| 231 |
+
state = getattr(_PIPE_TRANSFORMER, "_lora_state", None) if _PIPE_TRANSFORMER is not None else None
|
| 232 |
+
return sorted(state["sets"]) + ["off"] if state else ["off"]
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
_PIPE_TRANSFORMER = None
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def apply_lora(transformer) -> str | None:
|
| 239 |
+
"""Load every enabled LoRA set, fold the default one into `transformer`, and stash the factors for per-request
|
| 240 |
+
switching. Returns a status line, or `None` when everything is disabled."""
|
| 241 |
+
global _PIPE_TRANSFORMER
|
| 242 |
+
_PIPE_TRANSFORMER = transformer
|
| 243 |
+
|
| 244 |
+
inner_dim = transformer.config.num_attention_heads * transformer.config.attention_head_dim
|
| 245 |
+
sets = {}
|
| 246 |
+
if LARRY_FILE.lower() not in ("", "off", "none"):
|
| 247 |
+
sets["larry"] = _load_larry(inner_dim)
|
| 248 |
+
if os.environ.get("H3_LIGHTX", "on").lower() not in ("", "off", "none"):
|
| 249 |
+
sets["lightx"] = _load_lightx()
|
| 250 |
+
if os.environ.get("H3_LIGHTX8", "on").lower() not in ("", "off", "none"):
|
| 251 |
+
sets["lightx8"] = _load_lightx(LIGHTX8_FILE)
|
| 252 |
+
if os.environ.get("H3_REALISM", "on").lower() not in ("", "off", "none"):
|
| 253 |
+
sets["realism"] = _load_realism(inner_dim)
|
| 254 |
+
if os.environ.get("H3_JOYFOX", "on").lower() not in ("", "off", "none"):
|
| 255 |
+
sets["joyfox"] = _load_joyfox(inner_dim)
|
| 256 |
+
if os.environ.get("H3_FACIAL", "on").lower() not in ("", "off", "none"):
|
| 257 |
+
sets[FACIAL_NAME] = _load_facial(inner_dim)
|
| 258 |
+
if not sets:
|
| 259 |
+
return None
|
| 260 |
+
|
| 261 |
+
active = DEFAULT_LORA if DEFAULT_LORA in sets else sorted(sets)[0]
|
| 262 |
+
params = dict(transformer.named_parameters())
|
| 263 |
+
_apply(sets[active]["entries"], params, sets[active]["scale"])
|
| 264 |
+
transformer._lora_state = {"active": active, "sets": sets}
|
| 265 |
+
return (
|
| 266 |
+
f"LoRAs loaded: "
|
| 267 |
+
+ ", ".join(f"`{name}` ({spec['label']}, {len(spec['entries'])} weights)" for name, spec in sets.items())
|
| 268 |
+
+ f" · active `{active}`"
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def set_active(transformer, name: str) -> str:
|
| 273 |
+
"""Switch the folded LoRA in place. No-op when the state already matches. Returns the active set."""
|
| 274 |
+
state = getattr(transformer, "_lora_state", None)
|
| 275 |
+
if state is None:
|
| 276 |
+
return "off"
|
| 277 |
+
name = name if name in state["sets"] else "off"
|
| 278 |
+
if state["active"] == name:
|
| 279 |
+
return name
|
| 280 |
+
params = dict(transformer.named_parameters())
|
| 281 |
+
if state["active"] != "off":
|
| 282 |
+
old = state["sets"][state["active"]]
|
| 283 |
+
_apply(old["entries"], params, -old["scale"])
|
| 284 |
+
if name != "off":
|
| 285 |
+
_apply(state["sets"][name]["entries"], params, state["sets"][name]["scale"])
|
| 286 |
+
state["active"] = name
|
| 287 |
+
return name
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def set_enabled(transformer, enabled: bool) -> bool:
|
| 291 |
+
"""Backwards-compatible boolean toggle over the default set."""
|
| 292 |
+
return set_active(transformer, DEFAULT_LORA if enabled else "off") != "off"
|
h3_split_blocks.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The halves of a **split** MiniMax-H3 deployment, for both of its checkpoint partitions.
|
| 2 |
+
|
| 3 |
+
MiniMax-H3 is 195.9 GiB in bfloat16 and a ZeroGPU Space is evicted at 150 GB of storage, so `MiniMaxH3Blocks` is cut
|
| 4 |
+
at its `text_encoder` step: the 62.14 GiB Qwen3-VL runs in the conditioner Space, everything else in a generator
|
| 5 |
+
Space, and `prompt_embeds` + `text_token_tags` is the whole wire format between them.
|
| 6 |
+
|
| 7 |
+
`resize` / `setup` run on **both** sides: they own no pretrained component, and each half needs the canvas and the
|
| 8 |
+
prepared keyframes or normalized references. Both conditioner halves also return the resolved `height` / `width` /
|
| 9 |
+
`num_frames`, which the generating half pins rather than re-deriving.
|
| 10 |
+
|
| 11 |
+
Two things the blocks leave to the caller: a keyframe reaches them EXIF-transposed and in RGB, and the `t2va` / `fl2va`
|
| 12 |
+
frame count is aligned to `17 * n + 5` before the call, since that arithmetic lives on the denoising side of the cut.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from diffusers.modular_pipelines.minimax_h3.before_encoder import MiniMaxH3Ref2VASetupStep
|
| 16 |
+
from diffusers.modular_pipelines.minimax_h3.decoders import MiniMaxH3AfterDenoiseStep
|
| 17 |
+
from diffusers.modular_pipelines.minimax_h3.encoders import (
|
| 18 |
+
MiniMaxH3Ref2VAReferenceEncoderStep,
|
| 19 |
+
MiniMaxH3Ref2VATextEncoderStep,
|
| 20 |
+
MiniMaxH3TextEncoderStep,
|
| 21 |
+
)
|
| 22 |
+
from diffusers.modular_pipelines.minimax_h3.modular_blocks_minimax_h3 import (
|
| 23 |
+
MiniMaxH3AutoKeyframeVaeEncoderStep,
|
| 24 |
+
MiniMaxH3AutoResizeStep,
|
| 25 |
+
MiniMaxH3CoreDenoiseStep,
|
| 26 |
+
MiniMaxH3DecodeStep,
|
| 27 |
+
MiniMaxH3Ref2VACoreDenoiseStep,
|
| 28 |
+
_generation_outputs,
|
| 29 |
+
)
|
| 30 |
+
from diffusers.modular_pipelines.modular_pipeline import SequentialPipelineBlocks
|
| 31 |
+
from diffusers.modular_pipelines.modular_pipeline_utils import OutputParam
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _wire_outputs(num_frames: bool = True) -> list[OutputParam]:
|
| 35 |
+
"""The wire format of the split. `num_frames` is declared by the `ref2va` half alone, whose setup resolves one."""
|
| 36 |
+
return [
|
| 37 |
+
OutputParam.template("prompt_embeds"),
|
| 38 |
+
OutputParam("text_token_tags", description="The per-row modality tag of every row of `prompt_embeds`."),
|
| 39 |
+
OutputParam("height", type_hint=int, description="Resolved height of the generated video in pixels."),
|
| 40 |
+
OutputParam("width", type_hint=int, description="Resolved width of the generated video in pixels."),
|
| 41 |
+
*(
|
| 42 |
+
[OutputParam("num_frames", type_hint=int, description="Resolved number of frames, of the form 17 * n + 5.")]
|
| 43 |
+
if num_frames
|
| 44 |
+
else []
|
| 45 |
+
),
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class MiniMaxH3ConditionerBlocks(SequentialPipelineBlocks):
|
| 50 |
+
"""The conditioner half of a split MiniMax-H3: the keyframes on the canvas plus the Qwen3-VL read at layer 50."""
|
| 51 |
+
|
| 52 |
+
model_name = "minimax-h3"
|
| 53 |
+
block_classes = [MiniMaxH3AutoResizeStep, MiniMaxH3TextEncoderStep]
|
| 54 |
+
block_names = ["resize", "text_encoder"]
|
| 55 |
+
|
| 56 |
+
@property
|
| 57 |
+
def description(self):
|
| 58 |
+
return (
|
| 59 |
+
"The conditioner half of a split MiniMax-H3 deployment: puts the keyframes onto the target canvas and "
|
| 60 |
+
"encodes MiniMax-H3's presentation of the request into the `prompt_embeds` / `text_token_tags` pair the "
|
| 61 |
+
"denoising half consumes. The frame count is the caller's to align."
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
@property
|
| 65 |
+
def outputs(self):
|
| 66 |
+
return _wire_outputs(num_frames=False)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class MiniMaxH3GeneratorBlocks(SequentialPipelineBlocks):
|
| 70 |
+
"""The denoising half of a split MiniMax-H3: `MiniMaxH3Blocks` with its `text_encoder` step removed."""
|
| 71 |
+
|
| 72 |
+
model_name = "minimax-h3"
|
| 73 |
+
block_classes = [
|
| 74 |
+
MiniMaxH3AutoResizeStep,
|
| 75 |
+
MiniMaxH3AutoKeyframeVaeEncoderStep,
|
| 76 |
+
MiniMaxH3CoreDenoiseStep,
|
| 77 |
+
MiniMaxH3AfterDenoiseStep,
|
| 78 |
+
MiniMaxH3DecodeStep,
|
| 79 |
+
]
|
| 80 |
+
block_names = ["resize", "vae_encoder", "denoise", "after_denoise", "decode"]
|
| 81 |
+
|
| 82 |
+
@property
|
| 83 |
+
def description(self):
|
| 84 |
+
return (
|
| 85 |
+
"The denoising half of a split MiniMax-H3 deployment: the `t2va` / `fl2va` branch of `MiniMaxH3Blocks` "
|
| 86 |
+
"without its text-encoder step, so `prompt_embeds` and `text_token_tags` come in as inputs and the "
|
| 87 |
+
"62.14 GiB Qwen3-VL conditioner is never loaded here."
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
@property
|
| 91 |
+
def outputs(self):
|
| 92 |
+
return _generation_outputs()
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class MiniMaxH3Ref2VAConditionerBlocks(SequentialPipelineBlocks):
|
| 96 |
+
"""The conditioner half of a split `ref2va`: the resolved plan plus the Qwen3-VL read at its 50th layer.
|
| 97 |
+
|
| 98 |
+
Component for component this is `MiniMaxH3ConditionerBlocks`, so one conditioner Space serves both partitions.
|
| 99 |
+
What differs is the presentation: `ref2va` prepends a label per reference and a vision block per image and per
|
| 100 |
+
merged video frame pair, so the references themselves have to reach this half.
|
| 101 |
+
"""
|
| 102 |
+
|
| 103 |
+
model_name = "minimax-h3"
|
| 104 |
+
block_classes = [MiniMaxH3Ref2VASetupStep, MiniMaxH3Ref2VATextEncoderStep]
|
| 105 |
+
block_names = ["setup", "text_encoder"]
|
| 106 |
+
|
| 107 |
+
@property
|
| 108 |
+
def description(self):
|
| 109 |
+
return (
|
| 110 |
+
"The conditioner half of a split MiniMax-H3 `ref2va` deployment: resolves the request plan (canvas, frame "
|
| 111 |
+
"count, references normalized onto MiniMax-H3's own rates and resolutions) and encodes MiniMax-H3's "
|
| 112 |
+
"presentation of it into the `prompt_embeds` / `text_token_tags` pair the denoising half consumes."
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
@property
|
| 116 |
+
def outputs(self):
|
| 117 |
+
return _wire_outputs()
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class MiniMaxH3Ref2VAGeneratorBlocks(SequentialPipelineBlocks):
|
| 121 |
+
"""The denoising half of a split `ref2va`: the `ref2va` branch with its `text_encoder` step removed.
|
| 122 |
+
|
| 123 |
+
`reference_encoder` stays here, next to the two autoencoders it runs: its output shapes are where every reference
|
| 124 |
+
block's geometry in the packed layout comes from.
|
| 125 |
+
"""
|
| 126 |
+
|
| 127 |
+
model_name = "minimax-h3"
|
| 128 |
+
block_classes = [
|
| 129 |
+
MiniMaxH3Ref2VASetupStep,
|
| 130 |
+
MiniMaxH3Ref2VAReferenceEncoderStep,
|
| 131 |
+
MiniMaxH3Ref2VACoreDenoiseStep,
|
| 132 |
+
MiniMaxH3AfterDenoiseStep,
|
| 133 |
+
MiniMaxH3DecodeStep,
|
| 134 |
+
]
|
| 135 |
+
block_names = ["setup", "reference_encoder", "denoise", "after_denoise", "decode"]
|
| 136 |
+
|
| 137 |
+
@property
|
| 138 |
+
def description(self):
|
| 139 |
+
return (
|
| 140 |
+
"The denoising half of a split MiniMax-H3 `ref2va` deployment: the `ref2va` branch of `MiniMaxH3Blocks` "
|
| 141 |
+
"without its text-encoder step, so `prompt_embeds` and `text_token_tags` come in as inputs and the "
|
| 142 |
+
"62.14 GiB Qwen3-VL conditioner is never loaded here. The transformer is the `transformer_ref` partition."
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
@property
|
| 146 |
+
def outputs(self):
|
| 147 |
+
return _generation_outputs()
|