Split fetch/apply; drop Xet CAS store after loading PinkCherry
Browse files- pinkcherry_loader.py +87 -88
pinkcherry_loader.py
CHANGED
|
@@ -4,21 +4,25 @@ The PinkCherry checkpoint is a ComfyUI-layout state dict: fused `qkv_proj` atten
|
|
| 4 |
swiglu, `blocks.N` / `token_refiner.blocks.N` naming. The diffusers `MiniMaxH3Transformer3DModel`
|
| 5 |
expects SD3-style names (`to_q/to_k/to_v`, `ff.net.0.proj` / `ff.net.2`, `transformer_blocks.N`,
|
| 6 |
`refiner_blocks`). This module remaps one to the other and verifies full coverage.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
from __future__ import annotations
|
| 10 |
|
| 11 |
import os
|
| 12 |
import re
|
| 13 |
-
import
|
| 14 |
-
import
|
| 15 |
|
| 16 |
import safetensors.torch
|
| 17 |
import torch
|
|
|
|
| 18 |
|
| 19 |
PINKCHERRY_REPO = "SexGod1979/PinkCherry_MiniMax-H3"
|
| 20 |
PINKCHERRY_FILE = "beta-0.6-fl2va/PinkCherry_fl2va_MiniMax_H3_bf16_beta-0.6.safetensors"
|
| 21 |
-
PINKCHERRY_URL = f"https://huggingface.co/{PINKCHERRY_REPO}/resolve/main/{PINKCHERRY_FILE}"
|
| 22 |
|
| 23 |
# ComfyUI -> diffusers, per attention/block module. `qkv_proj` is fused Q,K,V in that order and is
|
| 24 |
# handled separately by the splitter.
|
|
@@ -82,89 +86,84 @@ def _remap_key(key: str) -> str | None:
|
|
| 82 |
return None
|
| 83 |
|
| 84 |
|
| 85 |
-
def
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
try:
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
expected = set(transformer.state_dict().keys())
|
| 147 |
-
missing = expected - set(remapped.keys())
|
| 148 |
-
unexpected = set(remapped.keys()) - expected
|
| 149 |
-
if missing:
|
| 150 |
-
raise KeyError(f"remap left {len(missing)} model keys uncovered: {sorted(missing)[:10]} ...")
|
| 151 |
-
if unexpected:
|
| 152 |
-
raise KeyError(f"remap produced {len(unexpected)} keys the model does not have: {sorted(unexpected)[:10]} ...")
|
| 153 |
-
for key in expected:
|
| 154 |
-
if remapped[key].shape != transformer.state_dict()[key].shape:
|
| 155 |
-
raise ValueError(
|
| 156 |
-
f"shape mismatch on {key}: PinkCherry {tuple(remapped[key].shape)} vs model {tuple(transformer.state_dict()[key].shape)}"
|
| 157 |
-
)
|
| 158 |
-
|
| 159 |
-
missing, unexpected = transformer.load_state_dict(remapped, strict=False)
|
| 160 |
-
if missing:
|
| 161 |
-
raise KeyError(f"load left {len(missing)} keys missing: {sorted(missing)[:10]} ...")
|
| 162 |
-
if unexpected:
|
| 163 |
-
raise KeyError(f"load got {len(unexpected)} unexpected keys: {sorted(unexpected)[:10]} ...")
|
| 164 |
-
print(f"[pinkcherry] loaded {len(remapped)} tensors into the transformer", flush=True)
|
| 165 |
-
finally:
|
| 166 |
-
try:
|
| 167 |
-
os.unlink(path)
|
| 168 |
-
print(f"[pinkcherry] removed temp file {path}", flush=True)
|
| 169 |
-
except OSError:
|
| 170 |
-
pass
|
|
|
|
| 4 |
swiglu, `blocks.N` / `token_refiner.blocks.N` naming. The diffusers `MiniMaxH3Transformer3DModel`
|
| 5 |
expects SD3-style names (`to_q/to_k/to_v`, `ff.net.0.proj` / `ff.net.2`, `transformer_blocks.N`,
|
| 6 |
`refiner_blocks`). This module remaps one to the other and verifies full coverage.
|
| 7 |
+
|
| 8 |
+
Storage strategy: the checkpoint is downloaded first (Xet, fast), loaded into RAM, and its on-disk
|
| 9 |
+
files — the materialized safetensors plus the Xet CAS blob — are deleted before the base model is
|
| 10 |
+
fetched, so peak disk usage never approaches the 150 GB ZeroGPU limit.
|
| 11 |
"""
|
| 12 |
|
| 13 |
from __future__ import annotations
|
| 14 |
|
| 15 |
import os
|
| 16 |
import re
|
| 17 |
+
import shutil
|
| 18 |
+
from pathlib import Path
|
| 19 |
|
| 20 |
import safetensors.torch
|
| 21 |
import torch
|
| 22 |
+
from huggingface_hub import hf_hub_download
|
| 23 |
|
| 24 |
PINKCHERRY_REPO = "SexGod1979/PinkCherry_MiniMax-H3"
|
| 25 |
PINKCHERRY_FILE = "beta-0.6-fl2va/PinkCherry_fl2va_MiniMax_H3_bf16_beta-0.6.safetensors"
|
|
|
|
| 26 |
|
| 27 |
# ComfyUI -> diffusers, per attention/block module. `qkv_proj` is fused Q,K,V in that order and is
|
| 28 |
# handled separately by the splitter.
|
|
|
|
| 86 |
return None
|
| 87 |
|
| 88 |
|
| 89 |
+
def _remap(state: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
|
| 90 |
+
remapped: dict[str, torch.Tensor] = {}
|
| 91 |
+
for key, tensor in state.items():
|
| 92 |
+
if key == "__metadata__":
|
| 93 |
+
continue
|
| 94 |
+
if key == "rope.inv_freq":
|
| 95 |
+
# ComfyUI stores the rotary table; the diffusers model computes it internally.
|
| 96 |
+
continue
|
| 97 |
+
if key.endswith("attn.qkv_proj.weight"):
|
| 98 |
+
inner = tensor.shape[0] // 3
|
| 99 |
+
q, k, v = tensor[:inner], tensor[inner : 2 * inner], tensor[2 * inner :]
|
| 100 |
+
base = key[: -len("attn.qkv_proj.weight")]
|
| 101 |
+
base = _BLOCK_RE.sub(r"transformer_blocks.\1.", base)
|
| 102 |
+
base = _REFINER_RE.sub(r"token_refiner.refiner_blocks.\1.", base)
|
| 103 |
+
remapped[base + "attn.to_q.weight"] = q
|
| 104 |
+
remapped[base + "attn.to_k.weight"] = k
|
| 105 |
+
remapped[base + "attn.to_v.weight"] = v
|
| 106 |
+
continue
|
| 107 |
+
if key.endswith("mlp.fc1.weight"):
|
| 108 |
+
# ComfyUI's swiglu stores `[gate; value]`; diffusers' SwiGLU reads `[value; gate]`,
|
| 109 |
+
# so the two halves swap places (same transform as the official conversion script).
|
| 110 |
+
gate, value = tensor.chunk(2, dim=0)
|
| 111 |
+
tensor = torch.cat([value, gate], dim=0).contiguous()
|
| 112 |
+
new = _remap_key(key)
|
| 113 |
+
if new is None:
|
| 114 |
+
raise KeyError(f"no mapping for PinkCherry key {key!r}")
|
| 115 |
+
remapped[new] = tensor
|
| 116 |
+
return remapped
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _drop_xet_cas() -> None:
|
| 120 |
+
"""Remove the Xet content-addressed blob store so its compressed copy stops counting against storage."""
|
| 121 |
+
from huggingface_hub.constants import HF_HUB_CACHE
|
| 122 |
+
|
| 123 |
+
for candidate in (Path(os.environ.get("HF_XET_CACHE", "")), Path(HF_HUB_CACHE) / "xet"):
|
| 124 |
+
if candidate.is_dir():
|
| 125 |
+
try:
|
| 126 |
+
shutil.rmtree(candidate)
|
| 127 |
+
print(f"[pinkcherry] removed Xet CAS store {candidate}", flush=True)
|
| 128 |
+
except OSError as error:
|
| 129 |
+
print(f"[pinkcherry] could not remove {candidate}: {error}", flush=True)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def fetch_pinkcherry_state_dict() -> dict[str, torch.Tensor]:
|
| 133 |
+
"""Download the checkpoint (Xet, fast), load it into RAM, free its disk footprint, return the remapped dict."""
|
| 134 |
+
path = hf_hub_download(PINKCHERRY_REPO, PINKCHERRY_FILE)
|
| 135 |
+
print(f"[pinkcherry] loading {path}", flush=True)
|
| 136 |
+
state = safetensors.torch.load_file(path)
|
| 137 |
+
remapped = _remap(state)
|
| 138 |
+
del state
|
| 139 |
try:
|
| 140 |
+
Path(path).unlink()
|
| 141 |
+
print(f"[pinkcherry] removed materialized checkpoint {path}", flush=True)
|
| 142 |
+
except OSError as error:
|
| 143 |
+
print(f"[pinkcherry] could not unlink {path}: {error}", flush=True)
|
| 144 |
+
_drop_xet_cas()
|
| 145 |
+
print(f"[pinkcherry] remapped {len(remapped)} tensors, disk footprint freed", flush=True)
|
| 146 |
+
return remapped
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def apply_pinkcherry_state_dict(transformer, remapped: dict[str, torch.Tensor]) -> None:
|
| 150 |
+
"""Validate and load the remapped PinkCherry weights into the (base-loaded) transformer."""
|
| 151 |
+
expected = set(transformer.state_dict().keys())
|
| 152 |
+
missing = expected - set(remapped.keys())
|
| 153 |
+
unexpected = set(remapped.keys()) - expected
|
| 154 |
+
if missing:
|
| 155 |
+
raise KeyError(f"remap left {len(missing)} model keys uncovered: {sorted(missing)[:10]} ...")
|
| 156 |
+
if unexpected:
|
| 157 |
+
raise KeyError(f"remap produced {len(unexpected)} keys the model does not have: {sorted(unexpected)[:10]} ...")
|
| 158 |
+
for key in expected:
|
| 159 |
+
if remapped[key].shape != transformer.state_dict()[key].shape:
|
| 160 |
+
raise ValueError(
|
| 161 |
+
f"shape mismatch on {key}: PinkCherry {tuple(remapped[key].shape)} vs model {tuple(transformer.state_dict()[key].shape)}"
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
missing, unexpected = transformer.load_state_dict(remapped, strict=False)
|
| 165 |
+
if missing:
|
| 166 |
+
raise KeyError(f"load left {len(missing)} keys missing: {sorted(missing)[:10]} ...")
|
| 167 |
+
if unexpected:
|
| 168 |
+
raise KeyError(f"load got {len(unexpected)} unexpected keys: {sorted(unexpected)[:10]} ...")
|
| 169 |
+
print(f"[pinkcherry] loaded {len(remapped)} tensors into the transformer", flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|