someone-in-the-world commited on
Commit
d9d1b60
·
1 Parent(s): be0f671

Refactor _infer_gpu into single-responsibility helper functions

Browse files

Split 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.

Files changed (1) hide show
  1. app.py +93 -60
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
- print(f"[infer] ===== START =====")
397
- print(f"[infer] steps={steps}, guidance={guidance_scale}, seed={seed}, gpu_duration={gpu_duration}s, mode={mode}")
398
- print(f"[infer] prompt={repr(prompt[:120])}")
399
 
400
- if _cuda_ok:
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
- def _instrument_first_call(module, name):
432
- handle_box = {}
433
- def _hook(mod, inputs):
434
- print(f"[infer] first call into {name} — {_gpu_mem_str(_cuda_ok, sync=True)} | t={time.perf_counter()-t0:.1f}s")
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=_step_cb,
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
- print(f"[infer] ERROR: {type(e).__name__}: {e} | t={time.perf_counter()-t0:.1f}s")
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