Dedupe reward generation: reuse pre-gen in the on-demand fallback
Browse filesWhen 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>
- app.py +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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
| 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)
|