goumsss Claude Sonnet 4.6 commited on
Commit
66c36b4
·
1 Parent(s): 47fb1ee

Fix ZeroGPU initialisation: spaces before torch, two-phase pipeline load, guidance_scale

Browse files

Three correctness fixes informed by the BFL reference starter app:

1. Import `spaces` before `torch` — ZeroGPU patches CUDA init at import time;
loading torch first can cause silent GPU mis-behaviour on HF Spaces.
Also unifies the public API to a single @GPU-decorated function using the
same no-op shim pattern as the reference.

2. Two-phase pipeline loading — `_load_pipeline_cpu()` runs at module scope
(outside the @GPU budget) so weights are in CPU RAM before the first
ZeroGPU call. `get_pipeline()` just does `.to(device)` inside @GPU , which
takes ~1s and removes the cold-start timeout risk on first generation.

3. Fix `guidance_scale=1.0` everywhere — the previous `0.0` on local runs
was a copy-paste from schnell; klein-4B distilled uses 1.0 in all envs.

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

Files changed (1) hide show
  1. image_generator.py +77 -37
image_generator.py CHANGED
@@ -1,5 +1,30 @@
1
  import random
2
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import torch
4
 
5
  # Patch for torch < 2.4 which lacks torch.xpu (required by diffusers >= 0.30)
@@ -21,10 +46,7 @@ if not hasattr(torch, "xpu"):
21
  IS_HF_SPACE = os.environ.get("SPACE_ID") is not None
22
 
23
  if IS_HF_SPACE:
24
- import spaces
25
- import sys, threading, asyncio
26
-
27
- # Suppress Python 3.10 asyncio GC bug (Invalid file descriptor: -1)
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):
@@ -197,41 +219,54 @@ def build_prompt(animals: list[str], places: list[str]) -> str:
197
  return f"{build_subject(animals, places)}, {NUMZOO_STYLE}"
198
 
199
  # ---------------------------------------------------------------------------
200
- # Pipeline loader (cached globally survives between ZeroGPU calls)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  # ---------------------------------------------------------------------------
202
 
203
  _pipe = None
 
 
204
 
205
- def get_pipeline():
 
 
206
  global _pipe
207
  if _pipe is not None:
208
- return _pipe
209
-
210
  from diffusers import Flux2KleinPipeline
211
-
212
  hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
213
- # float16 on MPS (bfloat16 not fully supported), bfloat16 everywhere else
214
- use_mps = (not IS_HF_SPACE) and (not torch.cuda.is_available()) and torch.backends.mps.is_available()
215
- dtype = torch.float16 if use_mps else torch.bfloat16
216
-
217
- print(f"Loading FLUX.2-klein-4B pipeline… (token={'set' if hf_token else 'NOT SET'}, dtype={dtype})")
218
  _pipe = Flux2KleinPipeline.from_pretrained(
219
- "black-forest-labs/FLUX.2-klein-4B",
220
- torch_dtype=dtype,
221
  token=hf_token,
222
  )
 
223
 
224
- if IS_HF_SPACE:
225
- _pipe = _pipe.to("cuda")
226
- elif torch.cuda.is_available():
227
- _pipe = _pipe.to("cuda")
228
- elif use_mps:
229
- _pipe = _pipe.to("mps")
230
- else:
231
- _pipe = _pipe.to("cpu")
232
 
233
- print("Pipeline ready.")
234
- return _pipe
 
 
 
 
 
 
 
 
235
 
236
  # ---------------------------------------------------------------------------
237
  # Core generation (always wrapped in try/except)
@@ -244,12 +279,11 @@ def _generate(animals: list[str], places: list[str]):
244
  prompt = build_prompt(animals, places)
245
  print(f"Generating | prompt: {prompt}")
246
 
247
- guidance = 1.0 if IS_HF_SPACE else 0.0 # klein=1.0, schnell=0.0
248
  t0 = time.time()
249
  result = pipe(
250
  prompt=prompt,
251
  num_inference_steps=4,
252
- guidance_scale=guidance,
253
  height=512,
254
  width=512,
255
  )
@@ -264,15 +298,21 @@ def _generate(animals: list[str], places: list[str]):
264
 
265
 
266
  # ---------------------------------------------------------------------------
267
- # Public API two versions depending on environment
 
 
 
268
  # ---------------------------------------------------------------------------
269
 
270
  if IS_HF_SPACE:
271
- @spaces.GPU(duration=60)
272
- def generate_reward_image(animals: list[str], places: list[str]):
273
- """Generate reward image on HF Spaces ZeroGPU."""
274
- return _generate(animals, places)
275
- else:
276
- def generate_reward_image(animals: list[str], places: list[str]):
277
- """Generate reward image locally (MPS/CPU)."""
278
- return _generate(animals, places)
 
 
 
 
1
  import random
2
  import os
3
+ import sys
4
+ import threading
5
+ import asyncio
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # ZeroGPU shim — spaces MUST be imported BEFORE torch.
9
+ # On HF ZeroGPU, spaces patches CUDA initialisation; that patch must land
10
+ # before torch is imported or GPU calls can silently mis-behave.
11
+ # Locally spaces isn't installed, so we fall back to a no-op @GPU decorator
12
+ # that makes the same code run unchanged on MPS / CPU.
13
+ # ---------------------------------------------------------------------------
14
+
15
+ try:
16
+ import spaces # type: ignore
17
+ GPU = spaces.GPU
18
+ ON_ZEROGPU = True
19
+ except Exception:
20
+ def GPU(*dargs, **dkwargs): # noqa: N802 — mirror the spaces.GPU API
21
+ def wrap(fn):
22
+ return fn
23
+ if len(dargs) == 1 and callable(dargs[0]) and not dkwargs:
24
+ return dargs[0]
25
+ return wrap
26
+ ON_ZEROGPU = False
27
+
28
  import torch
29
 
30
  # Patch for torch < 2.4 which lacks torch.xpu (required by diffusers >= 0.30)
 
46
  IS_HF_SPACE = os.environ.get("SPACE_ID") is not None
47
 
48
  if IS_HF_SPACE:
49
+ # Suppress Python 3.13 asyncio GC bug (Invalid file descriptor: -1)
 
 
 
50
  _orig_unraisable = sys.unraisablehook
51
  def _unraisable_hook(args):
52
  if args.exc_type is ValueError and "Invalid file descriptor" in str(args.exc_value):
 
219
  return f"{build_subject(animals, places)}, {NUMZOO_STYLE}"
220
 
221
  # ---------------------------------------------------------------------------
222
+ # Pipeline loader two-phase, following the reference ZeroGPU pattern:
223
+ #
224
+ # Phase 1 · _load_pipeline_cpu() — from_pretrained to CPU RAM.
225
+ # Called at module scope (outside any @GPU function) so the weights are
226
+ # already resident when the first ZeroGPU call arrives. This keeps the
227
+ # model-download cost out of the 60 s GPU-runtime budget and prevents
228
+ # cold-start timeouts on the very first generation.
229
+ #
230
+ # Phase 2 · get_pipeline() — .to(device).
231
+ # Must be called INSIDE a @GPU-decorated function (where a GPU slice is
232
+ # guaranteed). Moving already-loaded CPU tensors to CUDA is fast (~1 s)
233
+ # and comfortably within the budget.
234
+ #
235
+ # Locally (MPS / CPU) both phases happen inside generate_reward_image because
236
+ # the no-op @GPU decorator doesn't impose any budget constraint.
237
  # ---------------------------------------------------------------------------
238
 
239
  _pipe = None
240
+ _use_mps = (not IS_HF_SPACE) and (not torch.cuda.is_available()) and torch.backends.mps.is_available()
241
+ _dtype = torch.float16 if _use_mps else torch.bfloat16 # float16 on MPS (bfloat16 unsupported)
242
 
243
+
244
+ def _load_pipeline_cpu() -> None:
245
+ """Phase 1: load model weights into CPU RAM. Safe to call at module scope."""
246
  global _pipe
247
  if _pipe is not None:
248
+ return
 
249
  from diffusers import Flux2KleinPipeline
 
250
  hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
251
+ print(f"Loading FLUX.2-klein-4B on CPU… (dtype={_dtype}, token={'set' if hf_token else 'NOT SET'})")
 
 
 
 
252
  _pipe = Flux2KleinPipeline.from_pretrained(
253
+ _MODEL_ID,
254
+ torch_dtype=_dtype,
255
  token=hf_token,
256
  )
257
+ print("Pipeline loaded on CPU — ready for device placement.")
258
 
 
 
 
 
 
 
 
 
259
 
260
+ def get_pipeline():
261
+ """Phase 2: move pipeline to the target device. Call inside @GPU on HF Spaces."""
262
+ global _pipe
263
+ if _pipe is None:
264
+ _load_pipeline_cpu() # fallback for local / first-call safety
265
+ if torch.cuda.is_available():
266
+ return _pipe.to("cuda")
267
+ if _use_mps:
268
+ return _pipe.to("mps")
269
+ return _pipe.to("cpu")
270
 
271
  # ---------------------------------------------------------------------------
272
  # Core generation (always wrapped in try/except)
 
279
  prompt = build_prompt(animals, places)
280
  print(f"Generating | prompt: {prompt}")
281
 
 
282
  t0 = time.time()
283
  result = pipe(
284
  prompt=prompt,
285
  num_inference_steps=4,
286
+ guidance_scale=1.0, # klein-4B distilled: always 1.0 (not schnell's 0.0)
287
  height=512,
288
  width=512,
289
  )
 
298
 
299
 
300
  # ---------------------------------------------------------------------------
301
+ # Module-scope CPU pre-load (HF Spaces only).
302
+ # Runs after the persistent cache is set up and the snapshot is downloaded,
303
+ # so from_pretrained finds the weights locally and completes quickly.
304
+ # Locally this is skipped — the pipeline loads lazily on first generate call.
305
  # ---------------------------------------------------------------------------
306
 
307
  if IS_HF_SPACE:
308
+ _load_pipeline_cpu()
309
+
310
+ # ---------------------------------------------------------------------------
311
+ # Public API — single function, @GPU decorator is a no-op locally.
312
+ # ---------------------------------------------------------------------------
313
+
314
+ @GPU(duration=60)
315
+ def generate_reward_image(animals: list[str], places: list[str]):
316
+ """Generate a reward image. On HF Spaces runs inside a ZeroGPU slice;
317
+ locally the @GPU decorator is a no-op and MPS/CPU is used instead."""
318
+ return _generate(animals, places)