Spaces:
Running on Zero
Running on Zero
Update app.py
Browse files
app.py
CHANGED
|
@@ -67,10 +67,14 @@ DEFAULT_LORA_D_STRENGTH = 0.0
|
|
| 67 |
DEFAULT_LORA_E_STRENGTH = 0.0
|
| 68 |
DEFAULT_LORA_F_STRENGTH = 0.0
|
| 69 |
DEFAULT_LORA_G_STRENGTH = 0.0
|
| 70 |
-
#
|
| 71 |
-
#
|
| 72 |
-
#
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
# Must stay identical to the conditioner's table: the *label* goes over the wire, so a canvas that half does not
|
| 76 |
# know is rejected there and surfaces as a failure here. This is the workflow's "Target Dimension" node.
|
|
@@ -155,39 +159,71 @@ def lower_duration_floor(seconds: float = MIN_UI_DURATION) -> None:
|
|
| 155 |
|
| 156 |
MiniMaxH3ModularPipeline.min_duration = property(lambda self: float(seconds))
|
| 157 |
|
| 158 |
-
def _convert_diffusion_model_lora(raw: dict, base_shapes: dict) -> dict:
|
| 159 |
"""Rename a `diffusion_model.blocks.*` (original-checkpoint) LoRA state dict onto
|
| 160 |
`MiniMaxH3Transformer3DModel`'s (`transformer_blocks.*`) naming, so `load_lora_adapter` can attach it.
|
| 161 |
`raw` maps original key -> tensor. `base_shapes` maps the *unwrapped* base model's parameter names to their
|
| 162 |
shapes — captured once before any adapter is attached, since `load_lora_adapter` wraps each target Linear in
|
| 163 |
a PEFT layer and renames its weight to `<name>.base_layer.weight`, so a live `transformer.state_dict()` call
|
| 164 |
after the first adapter attaches would no longer have `to_q.weight` etc. under their original names.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
"""
|
| 166 |
import re
|
| 167 |
|
| 168 |
-
out = {}
|
|
|
|
| 169 |
|
| 170 |
-
# Family A:
|
| 171 |
-
#
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
| 176 |
# Enhancer. `lora_down`/`lora_up` are the same A/B convention under a different name.
|
| 177 |
-
|
| 178 |
r"^lora_unet_blocks_(\d+)_(attn_out_proj|attn_qkv_proj|mlp_fc1|mlp_fc2)\.(lora_down|lora_up)\.weight$"
|
| 179 |
)
|
|
|
|
| 180 |
kohya_targets = {
|
| 181 |
"attn_out_proj": ("attn", "out_proj"),
|
| 182 |
"attn_qkv_proj": ("attn", "qkv_proj"),
|
| 183 |
"mlp_fc1": ("mlp", "fc1"),
|
| 184 |
"mlp_fc2": ("mlp", "fc2"),
|
| 185 |
}
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
def
|
| 189 |
-
|
| 190 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
if ab == "lora_A":
|
| 192 |
# Shared low-rank input side — identical for q, k, v.
|
| 193 |
out[f"{prefix}attn.to_q.{ab}.weight"] = tensor
|
|
@@ -198,57 +234,70 @@ def _convert_diffusion_model_lora(raw: dict, base_shapes: dict) -> dict:
|
|
| 198 |
k_out = base_shapes[f"{prefix}attn.to_k.weight"][0]
|
| 199 |
v_out = base_shapes[f"{prefix}attn.to_v.weight"][0]
|
| 200 |
assert tensor.shape[0] == q_out + k_out + v_out, (
|
| 201 |
-
f"
|
| 202 |
f"(q{q_out}+k{k_out}+v{v_out}), got {tensor.shape[0]}"
|
| 203 |
)
|
| 204 |
out[f"{prefix}attn.to_q.{ab}.weight"] = tensor[:q_out].clone()
|
| 205 |
out[f"{prefix}attn.to_k.{ab}.weight"] = tensor[q_out:q_out + k_out].clone()
|
| 206 |
out[f"{prefix}attn.to_v.{ab}.weight"] = tensor[q_out + k_out:].clone()
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
|
|
|
| 220 |
|
| 221 |
for key, raw_tensor in raw.items():
|
| 222 |
# Some files (fp16-labeled ones especially) don't match the bf16 transformer's dtype; PEFT expects the
|
| 223 |
# adapter's dtype to match the wrapped base layer's.
|
| 224 |
tensor = raw_tensor.to(torch.bfloat16)
|
| 225 |
|
| 226 |
-
match =
|
| 227 |
if match:
|
| 228 |
-
|
| 229 |
-
emit(
|
| 230 |
continue
|
| 231 |
|
| 232 |
-
match =
|
| 233 |
if match:
|
| 234 |
block, target, direction = match.groups()
|
| 235 |
kind, leaf = kohya_targets[target]
|
| 236 |
-
emit(block
|
| 237 |
continue
|
| 238 |
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
| 247 |
continue
|
| 248 |
|
| 249 |
print(f"[lora-convert] skipping unrecognized key: {key}", flush=True)
|
| 250 |
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
PIPE = None
|
| 254 |
FILM = None
|
|
@@ -354,8 +403,12 @@ def load_models() -> str | None:
|
|
| 354 |
path = hf_hub_download(LORA_REPO, filename)
|
| 355 |
with safe_open(path, framework="pt") as handle:
|
| 356 |
raw = {k: handle.get_tensor(k) for k in handle.keys()}
|
| 357 |
-
converted = _convert_diffusion_model_lora(
|
| 358 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
# `load_lora_adapter` warns-and-continues on a zero-key match instead of raising, so count
|
| 360 |
# matched layers ourselves and fail loudly if a file attached nothing.
|
| 361 |
matched = sum(
|
|
|
|
| 67 |
DEFAULT_LORA_E_STRENGTH = 0.0
|
| 68 |
DEFAULT_LORA_F_STRENGTH = 0.0
|
| 69 |
DEFAULT_LORA_G_STRENGTH = 0.0
|
| 70 |
+
# Per-LoRA, not global: different training pipelines can store SwiGLU's fc1 gate/value halves in either order,
|
| 71 |
+
# and one flag can only be right for however many of the 8 files happen to agree. `lora1` (the Distilled/Turbo
|
| 72 |
+
# LoRA) is confirmed needing the swap by InstantX's official conversion of the same lineage
|
| 73 |
+
# (MiniMax-H3-Turbo-Lora-Diffusers/convert.py: "SwiGLU fc1 halves are swapped to match Diffusers' [value; gate]
|
| 74 |
+
# layout"); the rest default off until tested individually — set H3_LORA_SWAP_FC1_NAMES to a comma-separated
|
| 75 |
+
# list of LORA_FILES keys (e.g. "lora1,lorac") to override. Replaces H3_LORA_SWAP_FC1, which no longer does
|
| 76 |
+
# anything.
|
| 77 |
+
SWAP_FC1_NAMES = {name for name in os.environ.get("H3_LORA_SWAP_FC1_NAMES", "lora1").split(",") if name}
|
| 78 |
|
| 79 |
# Must stay identical to the conditioner's table: the *label* goes over the wire, so a canvas that half does not
|
| 80 |
# know is rejected there and surfaces as a failure here. This is the workflow's "Target Dimension" node.
|
|
|
|
| 159 |
|
| 160 |
MiniMaxH3ModularPipeline.min_duration = property(lambda self: float(seconds))
|
| 161 |
|
| 162 |
+
def _convert_diffusion_model_lora(raw: dict, base_shapes: dict, swap_fc1: bool) -> tuple[dict, dict]:
|
| 163 |
"""Rename a `diffusion_model.blocks.*` (original-checkpoint) LoRA state dict onto
|
| 164 |
`MiniMaxH3Transformer3DModel`'s (`transformer_blocks.*`) naming, so `load_lora_adapter` can attach it.
|
| 165 |
`raw` maps original key -> tensor. `base_shapes` maps the *unwrapped* base model's parameter names to their
|
| 166 |
shapes — captured once before any adapter is attached, since `load_lora_adapter` wraps each target Linear in
|
| 167 |
a PEFT layer and renames its weight to `<name>.base_layer.weight`, so a live `transformer.state_dict()` call
|
| 168 |
after the first adapter attaches would no longer have `to_q.weight` etc. under their original names.
|
| 169 |
+
|
| 170 |
+
Returns `(converted_weights, network_alphas)` — `network_alphas` is `load_lora_adapter`'s per-module `alpha`
|
| 171 |
+
map. Built for every converted module, not just ones whose raw file carries an explicit `.alpha` key: PEFT's
|
| 172 |
+
default scaling isn't guaranteed to land on `alpha == rank` when a LoRA mixes ranks across target types —
|
| 173 |
+
InstantX's own Turbo-LoRA conversion needs `network_alphas` for exactly this reason (attn/mlp modules rank
|
| 174 |
+
64, AdaLN modules rank 16), even though that file carries no `.alpha` keys at all. So `alpha = rank` is
|
| 175 |
+
synthesized for every module first, then overridden wherever the raw file specifies something else.
|
| 176 |
"""
|
| 177 |
import re
|
| 178 |
|
| 179 |
+
out: dict = {}
|
| 180 |
+
raw_alphas: dict[str, float] = {} # raw ComfyUI base name -> alpha, from real `.alpha` keys only
|
| 181 |
|
| 182 |
+
# Family A: standard (non-Kohya) naming — covers Mylo, VBVR, AIO_V2, moawxx, Anthro Realism, and (once
|
| 183 |
+
# `diffusion_model.` is stripped) the Distilled/Turbo LoRA. Matched by module base name rather than one
|
| 184 |
+
# fixed pattern, and renamed by substitution — this is what lets `token_refiner.blocks.*` and the top-level
|
| 185 |
+
# `final_layer.adaln_proj` resolve onto real targets instead of falling through unmatched. Ported from
|
| 186 |
+
# InstantX's official `MiniMax-H3-Turbo-Lora-Diffusers/convert.py`, written for this exact LoRA family.
|
| 187 |
+
standard_ab = re.compile(r"^(?:diffusion_model\.)?(.+)\.(lora_[AB])\.weight$")
|
| 188 |
+
standard_alpha = re.compile(r"^(?:diffusion_model\.)?(.+)\.alpha$")
|
| 189 |
+
|
| 190 |
+
# Family B (Kohya-style): `lora_unet_blocks_N_TARGET.(lora_down|lora_up|alpha)` — covers SB and Fluid
|
| 191 |
# Enhancer. `lora_down`/`lora_up` are the same A/B convention under a different name.
|
| 192 |
+
kohya_ab = re.compile(
|
| 193 |
r"^lora_unet_blocks_(\d+)_(attn_out_proj|attn_qkv_proj|mlp_fc1|mlp_fc2)\.(lora_down|lora_up)\.weight$"
|
| 194 |
)
|
| 195 |
+
kohya_alpha = re.compile(r"^lora_unet_blocks_(\d+)_(attn_out_proj|attn_qkv_proj|mlp_fc1|mlp_fc2)\.alpha$")
|
| 196 |
kohya_targets = {
|
| 197 |
"attn_out_proj": ("attn", "out_proj"),
|
| 198 |
"attn_qkv_proj": ("attn", "qkv_proj"),
|
| 199 |
"mlp_fc1": ("mlp", "fc1"),
|
| 200 |
"mlp_fc2": ("mlp", "fc2"),
|
| 201 |
}
|
| 202 |
+
kohya_ab_name = {"lora_down": "lora_A", "lora_up": "lora_B"}
|
| 203 |
+
|
| 204 |
+
def rename_base(name: str) -> str:
|
| 205 |
+
"""ComfyUI module path (before `.lora_*`/`.alpha`) -> Diffusers module path."""
|
| 206 |
+
if name.startswith("token_refiner.blocks."):
|
| 207 |
+
name = "token_refiner.refiner_blocks." + name[len("token_refiner.blocks."):]
|
| 208 |
+
elif name.startswith("blocks."):
|
| 209 |
+
name = "transformer_blocks." + name[len("blocks."):]
|
| 210 |
+
name = name.replace("final_layer.adaln_proj.linear", "norm_out.linear")
|
| 211 |
+
name = name.replace(".attn.out_proj", ".attn.to_out.0")
|
| 212 |
+
name = name.replace(".mlp.fc2", ".ff.net.2")
|
| 213 |
+
name = name.replace(".mlp.fc1", ".ff.net.0.proj")
|
| 214 |
+
return name
|
| 215 |
+
|
| 216 |
+
def target_bases(raw_base: str) -> list[str]:
|
| 217 |
+
"""Diffusers-side base name(s) for one pre-rename module path — one, except `attn.qkv_proj`, which fans
|
| 218 |
+
out to `to_q`/`to_k`/`to_v` (same rank, so the same alpha applies to all three)."""
|
| 219 |
+
if raw_base.endswith(".attn.qkv_proj"):
|
| 220 |
+
prefix = rename_base(raw_base[: -len("attn.qkv_proj")])
|
| 221 |
+
return [f"{prefix}attn.to_q", f"{prefix}attn.to_k", f"{prefix}attn.to_v"]
|
| 222 |
+
return [rename_base(raw_base)]
|
| 223 |
+
|
| 224 |
+
def emit(raw_base: str, ab: str, tensor) -> None:
|
| 225 |
+
if raw_base.endswith(".attn.qkv_proj"):
|
| 226 |
+
prefix = rename_base(raw_base[: -len("attn.qkv_proj")])
|
| 227 |
if ab == "lora_A":
|
| 228 |
# Shared low-rank input side — identical for q, k, v.
|
| 229 |
out[f"{prefix}attn.to_q.{ab}.weight"] = tensor
|
|
|
|
| 234 |
k_out = base_shapes[f"{prefix}attn.to_k.weight"][0]
|
| 235 |
v_out = base_shapes[f"{prefix}attn.to_v.weight"][0]
|
| 236 |
assert tensor.shape[0] == q_out + k_out + v_out, (
|
| 237 |
+
f"{raw_base}.{ab}: expected {q_out + k_out + v_out} rows "
|
| 238 |
f"(q{q_out}+k{k_out}+v{v_out}), got {tensor.shape[0]}"
|
| 239 |
)
|
| 240 |
out[f"{prefix}attn.to_q.{ab}.weight"] = tensor[:q_out].clone()
|
| 241 |
out[f"{prefix}attn.to_k.{ab}.weight"] = tensor[q_out:q_out + k_out].clone()
|
| 242 |
out[f"{prefix}attn.to_v.{ab}.weight"] = tensor[q_out + k_out:].clone()
|
| 243 |
+
return
|
| 244 |
+
|
| 245 |
+
if raw_base.endswith(".mlp.fc1") and ab == "lora_B" and swap_fc1:
|
| 246 |
+
half = tensor.shape[0] // 2
|
| 247 |
+
tensor = torch.cat([tensor[half:], tensor[:half]], dim=0)
|
| 248 |
+
|
| 249 |
+
key_base = rename_base(raw_base)
|
| 250 |
+
if f"{key_base}.weight" not in base_shapes:
|
| 251 |
+
# The more permissive substitution-based rename can produce a name that isn't an actual target on
|
| 252 |
+
# the live model — validated here rather than trusting the rename blindly, since it no longer
|
| 253 |
+
# checks against a fixed whitelist of known `kind`s the way the old anchored regex did.
|
| 254 |
+
print(f"[lora-convert] '{raw_base}' renamed to '{key_base}', which isn't a real target — skipping", flush=True)
|
| 255 |
+
return
|
| 256 |
+
out[f"{key_base}.{ab}.weight"] = tensor
|
| 257 |
|
| 258 |
for key, raw_tensor in raw.items():
|
| 259 |
# Some files (fp16-labeled ones especially) don't match the bf16 transformer's dtype; PEFT expects the
|
| 260 |
# adapter's dtype to match the wrapped base layer's.
|
| 261 |
tensor = raw_tensor.to(torch.bfloat16)
|
| 262 |
|
| 263 |
+
match = standard_ab.match(key)
|
| 264 |
if match:
|
| 265 |
+
raw_base, ab = match.groups()
|
| 266 |
+
emit(raw_base, ab, tensor)
|
| 267 |
continue
|
| 268 |
|
| 269 |
+
match = kohya_ab.match(key)
|
| 270 |
if match:
|
| 271 |
block, target, direction = match.groups()
|
| 272 |
kind, leaf = kohya_targets[target]
|
| 273 |
+
emit(f"blocks.{block}.{kind}.{leaf}", kohya_ab_name[direction], tensor)
|
| 274 |
continue
|
| 275 |
|
| 276 |
+
match = standard_alpha.match(key)
|
| 277 |
+
if match:
|
| 278 |
+
(raw_base,) = match.groups()
|
| 279 |
+
raw_alphas[raw_base] = float(raw_tensor)
|
| 280 |
+
continue
|
| 281 |
+
|
| 282 |
+
match = kohya_alpha.match(key)
|
| 283 |
+
if match:
|
| 284 |
+
block, target = match.groups()
|
| 285 |
+
kind, leaf = kohya_targets[target]
|
| 286 |
+
raw_alphas[f"blocks.{block}.{kind}.{leaf}"] = float(raw_tensor)
|
| 287 |
continue
|
| 288 |
|
| 289 |
print(f"[lora-convert] skipping unrecognized key: {key}", flush=True)
|
| 290 |
|
| 291 |
+
network_alphas: dict[str, float] = {}
|
| 292 |
+
for out_key, out_tensor in out.items():
|
| 293 |
+
if out_key.endswith(".lora_B.weight"):
|
| 294 |
+
base = out_key[: -len(".lora_B.weight")]
|
| 295 |
+
network_alphas[f"{base}.alpha"] = float(out_tensor.shape[1])
|
| 296 |
+
for raw_base, alpha in raw_alphas.items():
|
| 297 |
+
for base in target_bases(raw_base):
|
| 298 |
+
network_alphas[f"{base}.alpha"] = alpha
|
| 299 |
+
|
| 300 |
+
return out, network_alphas
|
| 301 |
|
| 302 |
PIPE = None
|
| 303 |
FILM = None
|
|
|
|
| 403 |
path = hf_hub_download(LORA_REPO, filename)
|
| 404 |
with safe_open(path, framework="pt") as handle:
|
| 405 |
raw = {k: handle.get_tensor(k) for k in handle.keys()}
|
| 406 |
+
converted, network_alphas = _convert_diffusion_model_lora(
|
| 407 |
+
raw, base_shapes, swap_fc1=name in SWAP_FC1_NAMES
|
| 408 |
+
)
|
| 409 |
+
pipe.transformer.load_lora_adapter(
|
| 410 |
+
converted, adapter_name=name, prefix=None, network_alphas=network_alphas
|
| 411 |
+
)
|
| 412 |
# `load_lora_adapter` warns-and-continues on a zero-key match instead of raising, so count
|
| 413 |
# matched layers ourselves and fail loudly if a file attached nothing.
|
| 414 |
matched = sum(
|