Spaces:
Running on Zero
Running on Zero
Commit ·
d9d1b60
1
Parent(s): be0f671
Refactor _infer_gpu into single-responsibility helper functions
Browse filesSplit the monolithic function into _log_infer_start, _log_gpu_properties,
_place_pipe_on_device, _instrument_first_touch, _make_step_callback, and
_log_infer_error. _infer_gpu itself is now just the orchestration of these
steps, with no behavior change.
app.py
CHANGED
|
@@ -387,72 +387,111 @@ def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps,
|
|
| 387 |
raise
|
| 388 |
|
| 389 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
@spaces.GPU(duration=lambda *a, **kw: int(a[8]) if len(a) > 8 else 60)
|
| 391 |
def _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, mode, gpu_duration=20):
|
| 392 |
_cuda_ok = torch.cuda.is_available()
|
| 393 |
timer = _InferTimer(_cuda_ok)
|
| 394 |
t0 = time.perf_counter()
|
| 395 |
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
print(f"[infer] prompt={repr(prompt[:120])}")
|
| 399 |
|
| 400 |
-
|
| 401 |
-
p = torch.cuda.get_device_properties(0)
|
| 402 |
-
print(f"[infer] GPU: {p.name}, total={p.total_memory/1024**3:.1f}GB, cap={p.major}.{p.minor}")
|
| 403 |
-
torch.cuda.reset_peak_memory_stats()
|
| 404 |
-
|
| 405 |
-
# Each ZeroGPU call runs in a fresh worker (hooks are always unset here),
|
| 406 |
-
# so cpu_offload buys no cross-call reuse — it only trades one bulk
|
| 407 |
-
# to(device) transfer for several slower hook-managed ones. On a
|
| 408 |
-
# generously-sized allocation, move the full ~37GB pipeline to GPU in one
|
| 409 |
-
# shot (fast); but the 47GB 2g.48gb MIG slice this Space actually gets
|
| 410 |
-
# measured a peak of 46.82GB and still OOM'd on that path, wasting ~40s
|
| 411 |
-
# before falling back — so only attempt it when there's real headroom
|
| 412 |
-
# above that, otherwise go straight to enable_model_cpu_offload.
|
| 413 |
-
_FAST_PATH_MIN_GB = 60
|
| 414 |
-
if getattr(pipe.transformer, "_hf_hook", None) is None:
|
| 415 |
-
if _cuda_ok and p.total_memory / 1024**3 >= _FAST_PATH_MIN_GB:
|
| 416 |
-
try:
|
| 417 |
-
pipe.to(device)
|
| 418 |
-
print(f"[infer] moved full pipe to {device} — t={time.perf_counter()-t0:.1f}s")
|
| 419 |
-
except torch.cuda.OutOfMemoryError:
|
| 420 |
-
print(f"[infer] OOM moving full pipe to {device}, falling back to cpu offload")
|
| 421 |
-
pipe.to("cpu")
|
| 422 |
-
torch.cuda.empty_cache()
|
| 423 |
-
pipe.enable_model_cpu_offload(device=device)
|
| 424 |
-
print(f"[infer] enabled cpu offload on {device} (fallback)")
|
| 425 |
-
else:
|
| 426 |
-
pipe.enable_model_cpu_offload(device=device)
|
| 427 |
-
print(f"[infer] enabled cpu offload on {device} (slice too small for fast path)")
|
| 428 |
print(f"[infer] {_gpu_mem_str(_cuda_ok)} — t={time.perf_counter()-t0:.1f}s")
|
| 429 |
|
| 430 |
if _cuda_ok:
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
handle_box["h"].remove()
|
| 436 |
-
handle_box["h"] = module.register_forward_pre_hook(_hook)
|
| 437 |
-
_instrument_first_call(pipe.text_encoder, "text_encoder")
|
| 438 |
-
_instrument_first_call(pipe.transformer, "transformer")
|
| 439 |
-
_instrument_first_call(pipe.vae, "vae")
|
| 440 |
|
| 441 |
print(f"[infer] {len(pil_images)} image(s) pre-decoded, output={width}x{height}, seed={seed}")
|
| 442 |
|
| 443 |
generator = torch.Generator(device=device).manual_seed(seed)
|
| 444 |
-
|
| 445 |
-
_step_t = []
|
| 446 |
-
def _step_cb(pipeline, step_idx, timestep, cb_kwargs):
|
| 447 |
-
now = time.perf_counter()
|
| 448 |
-
_step_t.append(now)
|
| 449 |
-
if step_idx == 0:
|
| 450 |
-
timer.mark("first_step")
|
| 451 |
-
timer.mark("last_step") # overwritten each step; final value = end of last step
|
| 452 |
-
delta_ms = (now - (_step_t[-2] if len(_step_t) > 1 else t0)) * 1000
|
| 453 |
-
tag = " ← includes cold-start (offload hook install + first weight transfer)" if step_idx == 0 else ""
|
| 454 |
-
print(f"[infer] step {step_idx+1}/{steps} done — {delta_ms:.0f}ms{tag} | t={now-t0:.1f}s")
|
| 455 |
-
return cb_kwargs
|
| 456 |
|
| 457 |
timer.mark("pipe_start")
|
| 458 |
print(f"[infer] calling pipe... t={time.perf_counter()-t0:.1f}s")
|
|
@@ -461,7 +500,7 @@ def _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, m
|
|
| 461 |
image=pil_images, prompt=prompt, negative_prompt=negative_prompt,
|
| 462 |
height=height, width=width, num_inference_steps=steps,
|
| 463 |
generator=generator, true_cfg_scale=guidance_scale,
|
| 464 |
-
callback_on_step_end=
|
| 465 |
callback_on_step_end_tensor_inputs=["latents"],
|
| 466 |
).images[0]
|
| 467 |
timer.mark("pipe_end")
|
|
@@ -470,13 +509,7 @@ def _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, m
|
|
| 470 |
duration = timer.elapsed_ms("pipe_start", "pipe_end") / 1000.0
|
| 471 |
return result_image, seed, duration
|
| 472 |
except Exception as e:
|
| 473 |
-
|
| 474 |
-
print(traceback.format_exc())
|
| 475 |
-
try:
|
| 476 |
-
torch.cuda.synchronize()
|
| 477 |
-
except Exception as cuda_err:
|
| 478 |
-
print(f"[infer] CUDA synchronize after error: {cuda_err}")
|
| 479 |
-
timer.print_timings()
|
| 480 |
raise
|
| 481 |
finally:
|
| 482 |
# No manual pipe.to("cpu"): in the offload-fallback case that fights the
|
|
|
|
| 387 |
raise
|
| 388 |
|
| 389 |
|
| 390 |
+
def _log_infer_start(prompt, steps, guidance_scale, seed, gpu_duration, mode):
|
| 391 |
+
print(f"[infer] ===== START =====")
|
| 392 |
+
print(f"[infer] steps={steps}, guidance={guidance_scale}, seed={seed}, gpu_duration={gpu_duration}s, mode={mode}")
|
| 393 |
+
print(f"[infer] prompt={repr(prompt[:120])}")
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
def _log_gpu_properties(cuda_ok):
|
| 397 |
+
if not cuda_ok:
|
| 398 |
+
return None
|
| 399 |
+
p = torch.cuda.get_device_properties(0)
|
| 400 |
+
print(f"[infer] GPU: {p.name}, total={p.total_memory/1024**3:.1f}GB, cap={p.major}.{p.minor}")
|
| 401 |
+
torch.cuda.reset_peak_memory_stats()
|
| 402 |
+
return p
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
# Each ZeroGPU call runs in a fresh worker (hooks are always unset here), so
|
| 406 |
+
# cpu_offload buys no cross-call reuse — it only trades one bulk to(device)
|
| 407 |
+
# transfer for several slower hook-managed ones. On a generously-sized
|
| 408 |
+
# allocation, move the full ~37GB pipeline to GPU in one shot (fast); but the
|
| 409 |
+
# 47GB 2g.48gb MIG slice this Space actually gets measured a peak of 46.82GB
|
| 410 |
+
# and still OOM'd on that path, wasting ~40s before falling back — so only
|
| 411 |
+
# attempt it when there's real headroom above that, otherwise go straight to
|
| 412 |
+
# enable_model_cpu_offload.
|
| 413 |
+
_FAST_PATH_MIN_GB = 60
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def _place_pipe_on_device(cuda_ok, gpu_props, t0):
|
| 417 |
+
if getattr(pipe.transformer, "_hf_hook", None) is not None:
|
| 418 |
+
return # already placed by an earlier call sharing this worker
|
| 419 |
+
if cuda_ok and gpu_props.total_memory / 1024**3 >= _FAST_PATH_MIN_GB:
|
| 420 |
+
try:
|
| 421 |
+
pipe.to(device)
|
| 422 |
+
print(f"[infer] moved full pipe to {device} — t={time.perf_counter()-t0:.1f}s")
|
| 423 |
+
return
|
| 424 |
+
except torch.cuda.OutOfMemoryError:
|
| 425 |
+
print(f"[infer] OOM moving full pipe to {device}, falling back to cpu offload")
|
| 426 |
+
pipe.to("cpu")
|
| 427 |
+
torch.cuda.empty_cache()
|
| 428 |
+
pipe.enable_model_cpu_offload(device=device)
|
| 429 |
+
print(f"[infer] enabled cpu offload on {device} (fallback)")
|
| 430 |
+
return
|
| 431 |
+
pipe.enable_model_cpu_offload(device=device)
|
| 432 |
+
print(f"[infer] enabled cpu offload on {device} (slice too small for fast path)")
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def _instrument_first_touch(modules_with_names, t0):
|
| 436 |
+
"""Install self-removing forward-pre-hooks that log the moment each module is first entered."""
|
| 437 |
+
def _make_hook(name, handle_box):
|
| 438 |
+
def _hook(mod, inputs):
|
| 439 |
+
print(f"[infer] first call into {name} — {_gpu_mem_str(True, sync=True)} | t={time.perf_counter()-t0:.1f}s")
|
| 440 |
+
handle_box["h"].remove()
|
| 441 |
+
return _hook
|
| 442 |
+
for module, name in modules_with_names:
|
| 443 |
+
handle_box = {}
|
| 444 |
+
handle_box["h"] = module.register_forward_pre_hook(_make_hook(name, handle_box))
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
def _make_step_callback(steps, timer, t0):
|
| 448 |
+
"""Build the diffusers step callback that logs per-step timing and marks timer checkpoints."""
|
| 449 |
+
step_times = []
|
| 450 |
+
def _step_cb(pipeline, step_idx, timestep, cb_kwargs):
|
| 451 |
+
now = time.perf_counter()
|
| 452 |
+
step_times.append(now)
|
| 453 |
+
if step_idx == 0:
|
| 454 |
+
timer.mark("first_step")
|
| 455 |
+
timer.mark("last_step") # overwritten each step; final value = end of last step
|
| 456 |
+
delta_ms = (now - (step_times[-2] if len(step_times) > 1 else t0)) * 1000
|
| 457 |
+
tag = " ← includes cold-start (offload hook install + first weight transfer)" if step_idx == 0 else ""
|
| 458 |
+
print(f"[infer] step {step_idx+1}/{steps} done — {delta_ms:.0f}ms{tag} | t={now-t0:.1f}s")
|
| 459 |
+
return cb_kwargs
|
| 460 |
+
return _step_cb
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
def _log_infer_error(e, t0, timer):
|
| 464 |
+
print(f"[infer] ERROR: {type(e).__name__}: {e} | t={time.perf_counter()-t0:.1f}s")
|
| 465 |
+
print(traceback.format_exc())
|
| 466 |
+
try:
|
| 467 |
+
torch.cuda.synchronize()
|
| 468 |
+
except Exception as cuda_err:
|
| 469 |
+
print(f"[infer] CUDA synchronize after error: {cuda_err}")
|
| 470 |
+
timer.print_timings()
|
| 471 |
+
|
| 472 |
+
|
| 473 |
@spaces.GPU(duration=lambda *a, **kw: int(a[8]) if len(a) > 8 else 60)
|
| 474 |
def _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, mode, gpu_duration=20):
|
| 475 |
_cuda_ok = torch.cuda.is_available()
|
| 476 |
timer = _InferTimer(_cuda_ok)
|
| 477 |
t0 = time.perf_counter()
|
| 478 |
|
| 479 |
+
_log_infer_start(prompt, steps, guidance_scale, seed, gpu_duration, mode)
|
| 480 |
+
gpu_props = _log_gpu_properties(_cuda_ok)
|
|
|
|
| 481 |
|
| 482 |
+
_place_pipe_on_device(_cuda_ok, gpu_props, t0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
print(f"[infer] {_gpu_mem_str(_cuda_ok)} — t={time.perf_counter()-t0:.1f}s")
|
| 484 |
|
| 485 |
if _cuda_ok:
|
| 486 |
+
_instrument_first_touch(
|
| 487 |
+
[(pipe.text_encoder, "text_encoder"), (pipe.transformer, "transformer"), (pipe.vae, "vae")],
|
| 488 |
+
t0,
|
| 489 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
|
| 491 |
print(f"[infer] {len(pil_images)} image(s) pre-decoded, output={width}x{height}, seed={seed}")
|
| 492 |
|
| 493 |
generator = torch.Generator(device=device).manual_seed(seed)
|
| 494 |
+
step_cb = _make_step_callback(steps, timer, t0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
|
| 496 |
timer.mark("pipe_start")
|
| 497 |
print(f"[infer] calling pipe... t={time.perf_counter()-t0:.1f}s")
|
|
|
|
| 500 |
image=pil_images, prompt=prompt, negative_prompt=negative_prompt,
|
| 501 |
height=height, width=width, num_inference_steps=steps,
|
| 502 |
generator=generator, true_cfg_scale=guidance_scale,
|
| 503 |
+
callback_on_step_end=step_cb,
|
| 504 |
callback_on_step_end_tensor_inputs=["latents"],
|
| 505 |
).images[0]
|
| 506 |
timer.mark("pipe_end")
|
|
|
|
| 509 |
duration = timer.elapsed_ms("pipe_start", "pipe_end") / 1000.0
|
| 510 |
return result_image, seed, duration
|
| 511 |
except Exception as e:
|
| 512 |
+
_log_infer_error(e, t0, timer)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
raise
|
| 514 |
finally:
|
| 515 |
# No manual pipe.to("cpu"): in the offload-fallback case that fights the
|