goumsss Claude Sonnet 4.6 commited on
Commit
9e11bdb
·
1 Parent(s): c1afda8

Remove startup model pre-caching — let from_pretrained cache in container

Browse files

The persistent storage placeholder-file issue was fundamental: HF Spaces
git-backed storage creates FILE placeholders at every intermediate directory
path (blobs/, refs/, snapshots/, .locks/) on each mount. Removing them
destroyed access to the cached content behind them, forcing a full 57.8 GB
re-download on every cold start — the opposite of the intended behaviour.

New approach: remove snapshot_download at startup entirely. from_pretrained
downloads to the container's local ~/.cache/huggingface/hub/ on first use.
The Python process (and _pipe global) survive between ZeroGPU GPU-slot
releases, so the model loads once per container lifetime. Increased
@spaces.GPU duration to 120s to cover first-run download + load + generate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. image_generator.py +8 -118
image_generator.py CHANGED
@@ -2,72 +2,6 @@ import random
2
  import os
3
  import torch
4
 
5
- # ---------------------------------------------------------------------------
6
- # Persistent storage cache (HF Spaces Pro — /data survives rebuilds)
7
- # ---------------------------------------------------------------------------
8
- _PERSISTENT_CACHE = "/data/hf_cache"
9
-
10
-
11
- def _repair_hf_cache(cache_root: str) -> None:
12
- """
13
- HF Spaces persistent storage mounts placeholder FILES at every path that
14
- should be a directory. This breaks huggingface_hub's cache which needs:
15
- {cache_root}/models--{org}--{model}/
16
- {cache_root}/models--{org}--{model}/blobs/
17
- {cache_root}/models--{org}--{model}/refs/
18
- {cache_root}/models--{org}--{model}/snapshots/
19
- {cache_root}/models--{org}--{model}/snapshots/{40-char-hash}/
20
- {cache_root}/.locks/models--{org}--{model}/ ← lock dir used during download
21
-
22
- Walk all known directory levels and unlink any non-directory obstacles.
23
- Real content files (safetensors, configs, symlinks) are left untouched.
24
- """
25
- def _fix(path: str) -> bool:
26
- """Remove path if it's not a real directory. Return True if it's a dir."""
27
- if os.path.lexists(path) and not os.path.isdir(path):
28
- print(f" fix: removing non-dir placeholder at {path}")
29
- os.unlink(path)
30
- return False
31
- return os.path.isdir(path)
32
-
33
- _fix(cache_root)
34
- if not os.path.isdir(cache_root):
35
- return
36
-
37
- # Fix models--* tree (blobs, refs, snapshots)
38
- for model_name in os.listdir(cache_root):
39
- if not model_name.startswith("models--"):
40
- continue
41
- model_dir = os.path.join(cache_root, model_name)
42
- if not _fix(model_dir):
43
- continue
44
- for sub in ("blobs", "refs", "snapshots"):
45
- sub_path = os.path.join(model_dir, sub)
46
- if not _fix(sub_path):
47
- continue
48
- if sub == "snapshots":
49
- for h in os.listdir(sub_path):
50
- h_path = os.path.join(sub_path, h)
51
- if not os.path.islink(h_path): # symlinks are fine
52
- _fix(h_path)
53
-
54
- # Fix .locks tree — huggingface_hub creates per-blob lock files here
55
- locks_root = os.path.join(cache_root, ".locks")
56
- if _fix(locks_root) and os.path.isdir(locks_root):
57
- for model_name in os.listdir(locks_root):
58
- if model_name.startswith("models--"):
59
- _fix(os.path.join(locks_root, model_name))
60
-
61
-
62
- if os.path.isdir("/data"):
63
- if os.path.lexists(_PERSISTENT_CACHE) and not os.path.isdir(_PERSISTENT_CACHE):
64
- os.unlink(_PERSISTENT_CACHE)
65
- os.makedirs(_PERSISTENT_CACHE, exist_ok=True)
66
- _repair_hf_cache(_PERSISTENT_CACHE)
67
- os.environ["HF_HUB_CACHE"] = _PERSISTENT_CACHE
68
- os.environ["HUGGINGFACE_HUB_CACHE"] = _PERSISTENT_CACHE
69
- print(f"Using persistent cache: {_PERSISTENT_CACHE}")
70
-
71
  # Patch for torch < 2.4 which lacks torch.xpu (required by diffusers >= 0.30)
72
  if not hasattr(torch, "xpu"):
73
  class _MockXPU:
@@ -87,12 +21,10 @@ if not hasattr(torch, "xpu"):
87
  IS_HF_SPACE = os.environ.get("SPACE_ID") is not None
88
  if IS_HF_SPACE:
89
  import spaces
90
- import sys, threading, asyncio
91
 
92
- # ── Suppress Python 3.10 asyncio GC bug ──────────────────────────────
93
- # BaseEventLoop.__del__ crashes with "Invalid file descriptor: -1" when
94
- # an event loop is collected by the GC. sys.unraisablehook is the correct
95
- # Python 3.8+ way to intercept these GC-triggered exceptions.
96
  _orig_unraisable = sys.unraisablehook
97
  def _unraisable_hook(args):
98
  if args.exc_type is ValueError and "Invalid file descriptor" in str(args.exc_value):
@@ -100,51 +32,7 @@ if IS_HF_SPACE:
100
  _orig_unraisable(args)
101
  sys.unraisablehook = _unraisable_hook
102
 
103
- # Download model files to persistent storage at startup.
104
- # /data/hf_cache survives rebuilds → downloads once, loads instantly after.
105
- _hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
106
- _cache_dir = os.environ.get("HF_HUB_CACHE")
107
- _model_cache = os.path.join(_cache_dir, "models--black-forest-labs--FLUX.1-schnell")
108
- _refs_main = os.path.join(_model_cache, "refs", "main")
109
- print(f"Pre-caching FLUX.1-schnell → {_cache_dir} (token={'set' if _hf_token else 'NOT SET'})")
110
- # snapshot_download is idempotent: it checks blob hashes and only fetches
111
- # what's missing, so if the cache is intact it completes in seconds.
112
- print(f" model dir : {'dir' if os.path.isdir(_model_cache) else 'absent/file'}")
113
- print(f" refs/main : {'present' if os.path.isfile(_refs_main) else 'absent'}")
114
- try:
115
-
116
- from huggingface_hub import snapshot_download
117
-
118
- _result = {}
119
- def _download():
120
- # Own an explicit event loop so it can be closed cleanly before the
121
- # thread exits — prevents the GC from seeing an open loop.
122
- loop = asyncio.new_event_loop()
123
- asyncio.set_event_loop(loop)
124
- try:
125
- snapshot_download(
126
- "black-forest-labs/FLUX.1-schnell",
127
- cache_dir=_cache_dir,
128
- token=_hf_token,
129
- ignore_patterns=["*.msgpack", "*.h5", "flax_model*"],
130
- )
131
- _result["ok"] = True
132
- except Exception as e:
133
- _result["error"] = e
134
- finally:
135
- try:
136
- loop.close()
137
- except Exception:
138
- pass
139
-
140
- _t = threading.Thread(target=_download, daemon=True)
141
- _t.start()
142
- _t.join()
143
- if "error" in _result:
144
- raise _result["error"]
145
- print("✅ Model files ready in persistent cache")
146
- except Exception as e:
147
- print(f"⚠️ Pre-cache warning (will retry at generation time): {e}")
148
 
149
  # ---------------------------------------------------------------------------
150
  # Emoji → descriptive text maps (fed into FLUX prompt)
@@ -217,7 +105,7 @@ def build_prompt(streak: int, animals: list[str], places: list[str]) -> str:
217
  return prompt
218
 
219
  # ---------------------------------------------------------------------------
220
- # Pipeline loader (cached)
221
  # ---------------------------------------------------------------------------
222
 
223
  _pipe = None
@@ -282,7 +170,9 @@ def _generate(streak: int, animals: list[str], places: list[str]):
282
  # ---------------------------------------------------------------------------
283
 
284
  if IS_HF_SPACE:
285
- @spaces.GPU(duration=60)
 
 
286
  def generate_reward_image(streak: int, animals: list[str], places: list[str]):
287
  """Generate reward image on HF Spaces ZeroGPU."""
288
  return _generate(streak, animals, places)
 
2
  import os
3
  import torch
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  # Patch for torch < 2.4 which lacks torch.xpu (required by diffusers >= 0.30)
6
  if not hasattr(torch, "xpu"):
7
  class _MockXPU:
 
21
  IS_HF_SPACE = os.environ.get("SPACE_ID") is not None
22
  if IS_HF_SPACE:
23
  import spaces
24
+ import sys
25
 
26
+ # Suppress Python 3.10 asyncio GC bug: BaseEventLoop.__del__ crashes with
27
+ # "Invalid file descriptor: -1" when a loop is collected by the GC.
 
 
28
  _orig_unraisable = sys.unraisablehook
29
  def _unraisable_hook(args):
30
  if args.exc_type is ValueError and "Invalid file descriptor" in str(args.exc_value):
 
32
  _orig_unraisable(args)
33
  sys.unraisablehook = _unraisable_hook
34
 
35
+ print("HF Space detected — model will load on first GPU call and stay cached in memory.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  # ---------------------------------------------------------------------------
38
  # Emoji → descriptive text maps (fed into FLUX prompt)
 
105
  return prompt
106
 
107
  # ---------------------------------------------------------------------------
108
+ # Pipeline loader (cached globally — survives between ZeroGPU calls)
109
  # ---------------------------------------------------------------------------
110
 
111
  _pipe = None
 
170
  # ---------------------------------------------------------------------------
171
 
172
  if IS_HF_SPACE:
173
+ # duration=120: covers first-run model download (~30s) + load (~10s) + generate (~5s).
174
+ # On subsequent calls _pipe is already loaded so only ~5s of GPU time is used.
175
+ @spaces.GPU(duration=120)
176
  def generate_reward_image(streak: int, animals: list[str], places: list[str]):
177
  """Generate reward image on HF Spaces ZeroGPU."""
178
  return _generate(streak, animals, places)