dagloop5 commited on
Commit
1d9183b
·
verified ·
1 Parent(s): 56e7bf5

Upload h3_eros.py

Browse files
Files changed (1) hide show
  1. 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
+ )