goumsss Claude Opus 4.8 commited on
Commit
ec4a4b3
Β·
1 Parent(s): 01c4239

Dedupe reward generation: reuse pre-gen in the on-demand fallback

Browse files

When the player finished a level before the background pre-generation
completed, the on-demand fallback generated the SAME prompt again β€” two
generations for one reward (visible in the logs; wasted a ZeroGPU call).

- image_generator: add a single-slot cache keyed by reward_id (level).
generate_reward_image(..., reward_id=level); inside the gen lock, a
matching reward_id returns the cached image instead of regenerating.
A different reward_id always regenerates, so rewards stay varied.
- app.py: pass reward_id=level from both pregenerate_image and
generate_on_demand.

Verified: same reward_id β†’ reused (0.0s, identical image); new id β†’ fresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +2 -2
  2. image_generator.py +19 -5
app.py CHANGED
@@ -198,7 +198,7 @@ def pregenerate_image(state: dict):
198
  places = state.get("selected_places", [random.choice(PLACE_EMOJIS)])
199
 
200
  print(f"[pregenerate] level={level} | animals={animals} | places={places}")
201
- result, prompt = generate_reward_image(animals, places)
202
  print(f"[pregenerate] level={level} done | prompt={prompt!r}")
203
 
204
  if result is not None:
@@ -346,7 +346,7 @@ def generate_on_demand(state: dict, coll_items: list):
346
  places = state.get("selected_places", [random.choice(PLACE_EMOJIS)])
347
 
348
  print(f"[on_demand] level={level}")
349
- result, prompt = generate_reward_image(animals, places)
350
  print(f"[on_demand] level={level} done")
351
 
352
  if result is not None:
 
198
  places = state.get("selected_places", [random.choice(PLACE_EMOJIS)])
199
 
200
  print(f"[pregenerate] level={level} | animals={animals} | places={places}")
201
+ result, prompt = generate_reward_image(animals, places, reward_id=level)
202
  print(f"[pregenerate] level={level} done | prompt={prompt!r}")
203
 
204
  if result is not None:
 
346
  places = state.get("selected_places", [random.choice(PLACE_EMOJIS)])
347
 
348
  print(f"[on_demand] level={level}")
349
+ result, prompt = generate_reward_image(animals, places, reward_id=level)
350
  print(f"[on_demand] level={level} done")
351
 
352
  if result is not None:
image_generator.py CHANGED
@@ -272,6 +272,10 @@ def build_prompt(animals: list[str], places: list[str]) -> str:
272
  _pipe = None
273
  _pipe_lock = threading.Lock() # serialize loading β€” pregenerate + on-demand can race
274
  _gen_lock = threading.Lock() # serialize inference β€” one device can't run two forwards at once
 
 
 
 
275
  _use_mps = (not IS_HF_SPACE) and (not torch.cuda.is_available()) and torch.backends.mps.is_available()
276
  _dtype = torch.float16 if _use_mps else torch.bfloat16 # float16 on MPS (bfloat16 unsupported)
277
 
@@ -327,7 +331,7 @@ def get_pipeline():
327
  # Core generation (always wrapped in try/except)
328
  # ---------------------------------------------------------------------------
329
 
330
- def _generate(animals: list[str], places: list[str]):
331
  try:
332
  import time
333
  prompt = build_prompt(animals, places)
@@ -335,6 +339,12 @@ def _generate(animals: list[str], places: list[str]):
335
  # can fire concurrently, but one device (MPS/GPU slice) can't run two forward
336
  # passes at once. ZeroGPU serializes @GPU calls for us; locally we must.
337
  with _gen_lock:
 
 
 
 
 
 
338
  pipe = get_pipeline()
339
  print(f"Generating | prompt: {prompt}")
340
  t0 = time.time()
@@ -346,7 +356,10 @@ def _generate(animals: list[str], places: list[str]):
346
  width=512,
347
  )
348
  print(f"βœ… Generated in {time.time() - t0:.1f}s")
349
- return result.images[0], prompt
 
 
 
350
 
351
  except Exception as e:
352
  import traceback
@@ -370,7 +383,8 @@ if IS_HF_SPACE:
370
  # ---------------------------------------------------------------------------
371
 
372
  @GPU(duration=60)
373
- def generate_reward_image(animals: list[str], places: list[str]):
374
  """Generate a reward image. On HF Spaces runs inside a ZeroGPU slice;
375
- locally the @GPU decorator is a no-op and MPS/CPU is used instead."""
376
- return _generate(animals, places)
 
 
272
  _pipe = None
273
  _pipe_lock = threading.Lock() # serialize loading β€” pregenerate + on-demand can race
274
  _gen_lock = threading.Lock() # serialize inference β€” one device can't run two forwards at once
275
+ # Single-slot cache so the on-demand fallback reuses the in-flight pre-generation
276
+ # for the SAME reward instead of generating it twice. Keyed by reward_id (level);
277
+ # a different reward_id always regenerates, so rewards stay varied across levels.
278
+ _recent_reward = {"id": None, "image": None}
279
  _use_mps = (not IS_HF_SPACE) and (not torch.cuda.is_available()) and torch.backends.mps.is_available()
280
  _dtype = torch.float16 if _use_mps else torch.bfloat16 # float16 on MPS (bfloat16 unsupported)
281
 
 
331
  # Core generation (always wrapped in try/except)
332
  # ---------------------------------------------------------------------------
333
 
334
+ def _generate(animals: list[str], places: list[str], reward_id=None):
335
  try:
336
  import time
337
  prompt = build_prompt(animals, places)
 
339
  # can fire concurrently, but one device (MPS/GPU slice) can't run two forward
340
  # passes at once. ZeroGPU serializes @GPU calls for us; locally we must.
341
  with _gen_lock:
342
+ # Reuse the pre-generated image for this reward instead of generating twice.
343
+ if reward_id is not None and _recent_reward["id"] == reward_id \
344
+ and _recent_reward["image"] is not None:
345
+ print(f"♻️ Reusing pre-generated image for reward {reward_id}")
346
+ return _recent_reward["image"], prompt
347
+
348
  pipe = get_pipeline()
349
  print(f"Generating | prompt: {prompt}")
350
  t0 = time.time()
 
356
  width=512,
357
  )
358
  print(f"βœ… Generated in {time.time() - t0:.1f}s")
359
+ image = result.images[0]
360
+ if reward_id is not None:
361
+ _recent_reward["id"], _recent_reward["image"] = reward_id, image
362
+ return image, prompt
363
 
364
  except Exception as e:
365
  import traceback
 
383
  # ---------------------------------------------------------------------------
384
 
385
  @GPU(duration=60)
386
+ def generate_reward_image(animals: list[str], places: list[str], reward_id=None):
387
  """Generate a reward image. On HF Spaces runs inside a ZeroGPU slice;
388
+ locally the @GPU decorator is a no-op and MPS/CPU is used instead.
389
+ Pass reward_id so the pre-generate + on-demand paths dedupe the same reward."""
390
+ return _generate(animals, places, reward_id=reward_id)