fdgfdbvdb commited on
Commit
f4ebe64
·
verified ·
1 Parent(s): 2f4cdcd

Split fetch/apply; drop Xet CAS store after loading PinkCherry

Browse files
Files changed (1) hide show
  1. 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 tempfile
14
- import urllib.request
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 _download_pinkcherry() -> str:
86
- """Fetch the checkpoint over plain HTTP into a temp file.
87
-
88
- Deliberately bypasses the HF cache: `hf_hub_download` on a Xet-backed repo leaves a second compressed CAS
89
- copy on disk, and base model (77.3 GB) + PinkCherry (61.7 GB) + CAS blob (~35 GB) + venv busts the 150 GB
90
- ZeroGPU storage limit mid-boot. A plain fetch materializes the file exactly once; the caller deletes it after
91
- loading.
92
- """
93
- fd, path = tempfile.mkstemp(suffix=".safetensors", prefix="pinkcherry-")
94
- os.close(fd)
95
- print(f"[pinkcherry] downloading {PINKCHERRY_URL} -> {path}", flush=True)
96
- req = urllib.request.Request(PINKCHERRY_URL, headers={"User-Agent": "pinkcherry-loader/1.0"})
97
- with urllib.request.urlopen(req, timeout=3600) as resp, open(path, "wb") as out:
98
- total = int(resp.headers.get("Content-Length") or 0)
99
- done = 0
100
- while True:
101
- chunk = resp.read(8 * 1024 * 1024)
102
- if not chunk:
103
- break
104
- out.write(chunk)
105
- done += len(chunk)
106
- if total:
107
- print(f"[pinkcherry] {done / 1024**3:.1f} / {total / 1024**3:.1f} GiB", flush=True)
108
- print(f"[pinkcherry] downloaded {done / 1024**3:.1f} GiB", flush=True)
109
- return path
110
-
111
-
112
- def load_pinkcherry_state_dict(transformer) -> None:
113
- """Download, remap, and load the PinkCherry weights into `transformer`; the temp file is removed on exit."""
114
- path = _download_pinkcherry()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  try:
116
- print(f"[pinkcherry] loading {path}", flush=True)
117
- state = safetensors.torch.load_file(path)
118
-
119
- remapped: dict[str, torch.Tensor] = {}
120
- for key, tensor in state.items():
121
- if key == "__metadata__":
122
- continue
123
- if key == "rope.inv_freq":
124
- # ComfyUI stores the rotary table; the diffusers model computes it internally.
125
- continue
126
- if key.endswith("attn.qkv_proj.weight"):
127
- inner = tensor.shape[0] // 3
128
- q, k, v = tensor[:inner], tensor[inner : 2 * inner], tensor[2 * inner :]
129
- base = key[: -len("attn.qkv_proj.weight")]
130
- base = _BLOCK_RE.sub(r"transformer_blocks.\1.", base)
131
- base = _REFINER_RE.sub(r"token_refiner.refiner_blocks.\1.", base)
132
- remapped[base + "attn.to_q.weight"] = q
133
- remapped[base + "attn.to_k.weight"] = k
134
- remapped[base + "attn.to_v.weight"] = v
135
- continue
136
- if key.endswith("mlp.fc1.weight"):
137
- # ComfyUI's swiglu stores `[gate; value]`; diffusers' SwiGLU reads `[value; gate]`,
138
- # so the two halves swap places (same transform as the official conversion script).
139
- gate, value = tensor.chunk(2, dim=0)
140
- tensor = torch.cat([value, gate], dim=0).contiguous()
141
- new = _remap_key(key)
142
- if new is None:
143
- raise KeyError(f"no mapping for PinkCherry key {key!r}")
144
- remapped[new] = tensor
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)