Spaces:
Running on Zero
Running on Zero
Upload h3_eros.py
Browse files- h3_eros.py +139 -0
h3_eros.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""10Eros-Max (TenStrip) beta_5 support: replace the diffusers transformer's weights with the Eros checkpoint.
|
| 2 |
+
|
| 3 |
+
`TenStrip/10Eros-Max` ships full bf16 checkpoints in the *reference* (ComfyUI) module tree, not LoRAs, so the update is
|
| 4 |
+
a straight weight replacement rather than a fold. Each tensor is renamed and row-transformed exactly like the
|
| 5 |
+
`larryvrh` LoRA deltas in `h3_lora` (fused-QKV thirds onto `attn.to_q/k/v`, the `SwiGLU` gate/value swap onto
|
| 6 |
+
`ff.net.0.proj`, `blocks.` -> `transformer_blocks.`, ...). The mapping was checked against the base weights: block 0
|
| 7 |
+
q/k/v/out_proj/fc1 all sit within ~1% cosine of MiniMax-H3 after that transform, and fc1 does not match without the swap.
|
| 8 |
+
|
| 9 |
+
AdaLN: the reference file stores the projections rank-8 compressed over the 1025-step timestep grid — `adaln_t_table[i] =
|
| 10 |
+
adaln_basis @ (silu(temb(i / 1024)) - adaln_mean)` and `*.adaln_proj.linear` of shape `[out, 8]`. Diffusers computes the
|
| 11 |
+
full `linear(silu(temb))`, so each projection is rebuilt as `W = W_ref @ basis`, `b = b_ref - W @ mean`. Checked against
|
| 12 |
+
the base model (time embedder from MiniMax-H3, which the file does not carry): the table matches at cos 0.9998 and the
|
| 13 |
+
rebuilt output matches the reference form at cos 1.0000 (0.9993-0.9999 vs. base weights). Exact on the sampler's
|
| 14 |
+
timestep grid, a rank-8 approximation off it; the time embedder itself stays base. `rope.inv_freq` is skipped.
|
| 15 |
+
|
| 16 |
+
Tensors are streamed one at a time with `safe_open`, so peak CPU memory is one tensor rather than the 40 GB file.
|
| 17 |
+
|
| 18 |
+
`H3_EROS=off` skips it, `H3_EROS_REPO` / `H3_EROS_FILE` pick the checkpoint. A `TURBO` file has the 4-step delta baked
|
| 19 |
+
in, which is why `app.py` then defaults the LoRA selector to `off` — stacking larry/lightx on top double-applies it.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import os
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
|
| 28 |
+
EROS_REPO = os.environ.get("H3_EROS_REPO", "TenStrip/10Eros-Max")
|
| 29 |
+
EROS_FILE = os.environ.get("H3_EROS_FILE", "10Eros_Max_h3_TURBO-hybrid_beta5.safetensors")
|
| 30 |
+
|
| 31 |
+
_SKIP_EXACT = {"rope.inv_freq", "adaln_basis", "adaln_mean", "adaln_t_table"}
|
| 32 |
+
_DIRECT = {
|
| 33 |
+
"video_patch_proj": "proj_in",
|
| 34 |
+
"audio_patch_proj": "audio_proj_in",
|
| 35 |
+
"condition_proj": "context_embedder",
|
| 36 |
+
"final_layer.video_out": "proj_out",
|
| 37 |
+
"final_layer.audio_out": "audio_proj_out",
|
| 38 |
+
"final_layer.norm": "norm_out.norm",
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def enabled() -> bool:
|
| 43 |
+
return EROS_FILE.lower() not in ("", "off", "none") and os.environ.get("H3_EROS", "on").lower() not in ("", "off", "none")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def turbo_baked() -> bool:
|
| 47 |
+
return enabled() and "turbo" in EROS_FILE.lower()
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def adaln_target(name: str) -> str | None:
|
| 51 |
+
"""Diffusers module prefix for a compressed reference AdaLN projection tensor, else `None`."""
|
| 52 |
+
if name.startswith("final_layer.adaln_proj.linear."):
|
| 53 |
+
return "norm_out.linear"
|
| 54 |
+
if name.startswith("blocks.") and ".adaln_proj.linear." in name:
|
| 55 |
+
return name.replace("blocks.", "transformer_blocks.", 1).rsplit(".", 1)[0]
|
| 56 |
+
return None
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def rebuild_adaln(w_ref: torch.Tensor, b_ref: torch.Tensor, basis: torch.Tensor, mean: torch.Tensor):
|
| 60 |
+
"""Rank-8 reference projection -> full `[out, time_embed_dim]` weight and bias (float32)."""
|
| 61 |
+
w = w_ref.float() @ basis.float()
|
| 62 |
+
return w, b_ref.float() - w @ mean.float()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def map_tensor(name: str, tensor: torch.Tensor, inner_dim: int) -> list[tuple[str, torch.Tensor]]:
|
| 66 |
+
"""Reference-tree tensor -> [(diffusers key, tensor)]. Returns [] for tensors with no diffusers counterpart."""
|
| 67 |
+
if name in _SKIP_EXACT or adaln_target(name) is not None:
|
| 68 |
+
return []
|
| 69 |
+
stem, _, leaf = name.rpartition(".") # leaf is `weight` / `bias`
|
| 70 |
+
if stem in _DIRECT:
|
| 71 |
+
return [(f"{_DIRECT[stem]}.{leaf}", tensor)]
|
| 72 |
+
|
| 73 |
+
if name.startswith("token_refiner.blocks."):
|
| 74 |
+
target = name.replace("token_refiner.blocks.", "token_refiner.refiner_blocks.", 1)
|
| 75 |
+
elif name.startswith("blocks."):
|
| 76 |
+
target = name.replace("blocks.", "transformer_blocks.", 1)
|
| 77 |
+
else:
|
| 78 |
+
target = name # token_refiner.final_norm.weight
|
| 79 |
+
|
| 80 |
+
if target.endswith(".attn.qkv_proj.weight"):
|
| 81 |
+
prefix = target.removesuffix("qkv_proj.weight")
|
| 82 |
+
return [
|
| 83 |
+
(f"{prefix}to_{kind}.weight", part.contiguous())
|
| 84 |
+
for kind, part in zip(("q", "k", "v"), tensor.split(inner_dim, dim=0))
|
| 85 |
+
]
|
| 86 |
+
if target.endswith(".mlp.fc1.weight"):
|
| 87 |
+
gate, value = tensor.chunk(2, dim=0)
|
| 88 |
+
return [(target.replace(".mlp.fc1.weight", ".ff.net.0.proj.weight"), torch.cat([value, gate]).contiguous())]
|
| 89 |
+
if target.endswith(".mlp.fc2.weight"):
|
| 90 |
+
return [(target.replace(".mlp.fc2.weight", ".ff.net.2.weight"), tensor)]
|
| 91 |
+
if target.endswith(".attn.out_proj.weight"):
|
| 92 |
+
return [(target.replace(".attn.out_proj.weight", ".attn.to_out.0.weight"), tensor)]
|
| 93 |
+
if target.endswith(".attn.q_norm.weight"):
|
| 94 |
+
return [(target.replace(".attn.q_norm.weight", ".attn.norm_q.weight"), tensor)]
|
| 95 |
+
if target.endswith(".attn.k_norm.weight"):
|
| 96 |
+
return [(target.replace(".attn.k_norm.weight", ".attn.norm_k.weight"), tensor)]
|
| 97 |
+
return [(target, tensor)] # norm1 / norm2 / token_refiner.final_norm
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def apply_eros(transformer) -> str | None:
|
| 101 |
+
"""Replace the transformer's weights in place with the Eros checkpoint. Returns a status line, or `None` if off."""
|
| 102 |
+
if not enabled():
|
| 103 |
+
return None
|
| 104 |
+
from huggingface_hub import hf_hub_download
|
| 105 |
+
from safetensors import safe_open
|
| 106 |
+
|
| 107 |
+
inner_dim = transformer.config.num_attention_heads * transformer.config.attention_head_dim
|
| 108 |
+
params = dict(transformer.named_parameters())
|
| 109 |
+
path = hf_hub_download(EROS_REPO, EROS_FILE)
|
| 110 |
+
|
| 111 |
+
replaced, skipped = 0, [] # skipped: reference-only tensors (basis / mean / table / rope)
|
| 112 |
+
with safe_open(path, framework="pt", device="cpu") as f:
|
| 113 |
+
basis, mean = f.get_tensor("adaln_basis"), f.get_tensor("adaln_mean")
|
| 114 |
+
for name in f.keys():
|
| 115 |
+
if name.endswith("adaln_proj.linear.weight") and (prefix := adaln_target(name)):
|
| 116 |
+
w, b = rebuild_adaln(f.get_tensor(name), f.get_tensor(name.replace(".weight", ".bias")), basis, mean)
|
| 117 |
+
for key, tensor in ((f"{prefix}.weight", w), (f"{prefix}.bias", b)):
|
| 118 |
+
if key not in params or tuple(params[key].shape) != tuple(tensor.shape):
|
| 119 |
+
raise ValueError(f"AdaLN `{key}`: transformer vs Eros {tuple(tensor.shape)} mismatch")
|
| 120 |
+
params[key].data.copy_(tensor.to(params[key].dtype))
|
| 121 |
+
replaced += 1
|
| 122 |
+
continue
|
| 123 |
+
targets = map_tensor(name, f.get_tensor(name), inner_dim)
|
| 124 |
+
if not targets:
|
| 125 |
+
skipped.append(name)
|
| 126 |
+
continue
|
| 127 |
+
for key, tensor in targets:
|
| 128 |
+
param = params.get(key)
|
| 129 |
+
if param is None:
|
| 130 |
+
raise KeyError(f"Eros tensor `{name}` -> `{key}` not found in the transformer")
|
| 131 |
+
if tuple(param.shape) != tuple(tensor.shape):
|
| 132 |
+
raise ValueError(f"`{key}`: transformer {tuple(param.shape)} vs Eros {tuple(tensor.shape)}")
|
| 133 |
+
param.data.copy_(tensor.to(param.dtype))
|
| 134 |
+
replaced += 1
|
| 135 |
+
|
| 136 |
+
return (
|
| 137 |
+
f"Eros `{EROS_REPO}/{EROS_FILE}`: {replaced} weights replaced, {len(skipped)} reference-only tensors skipped "
|
| 138 |
+
f"(AdaLN rebuilt from rank-8){' · turbo baked in' if turbo_baked() else ''}"
|
| 139 |
+
)
|