someone-in-the-world Claude Sonnet 4.6 commited on
Commit
961feed
Β·
1 Parent(s): 2837b03

Add torch.cuda.Event section timing to infer()

Browse files

Replaces time.time() with perf_counter() and torch.cuda.Event-based
timing so GPU-timeline durations (preprocess, inference, vae_decode)
are measured accurately via elapsed_time() rather than wall-clock.
Prints a per-section table after each run.

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

Files changed (1) hide show
  1. app.py +69 -17
app.py CHANGED
@@ -161,29 +161,73 @@ def update_dimensions_on_upload(image):
161
  @spaces.GPU
162
  def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps, progress=gr.Progress(track_tqdm=True)):
163
  import time, traceback
164
- t0 = time.time()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  def _t():
167
- return f"t={time.time()-t0:.1f}s"
168
 
169
  def _mem(sync=False):
170
- if not torch.cuda.is_available():
171
  return "CUDA not available"
172
  if sync:
173
  try:
174
  torch.cuda.synchronize()
175
  except Exception as se:
176
  return f"CUDA sync failed: {se}"
177
- alloc = torch.cuda.memory_allocated() / 1024**3
178
- reserved= torch.cuda.memory_reserved() / 1024**3
179
- peak = torch.cuda.max_memory_allocated()/ 1024**3
180
  return f"alloc={alloc:.2f}GB reserved={reserved:.2f}GB peak={peak:.2f}GB"
181
 
182
  print(f"[infer] ===== START =====")
183
  print(f"[infer] steps={steps}, guidance={guidance_scale}, seed={seed}, randomize={randomize_seed}")
184
  print(f"[infer] prompt={repr(prompt[:120])}")
185
 
186
- if torch.cuda.is_available():
187
  p = torch.cuda.get_device_properties(0)
188
  print(f"[infer] GPU: {p.name}, total={p.total_memory/1024**3:.1f}GB, cap={p.major}.{p.minor}")
189
  torch.cuda.reset_peak_memory_stats()
@@ -194,8 +238,10 @@ def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps,
194
  torch.cuda.empty_cache()
195
  print(f"[infer] cache cleared β€” {_mem()}")
196
 
 
197
  pil_images = b64_to_pil_list(images_b64_json)
198
- print(f"[infer] decoded {len(pil_images)} image(s)")
 
199
  if not pil_images:
200
  raise gr.Error("Please upload at least one image to edit.")
201
  if not prompt or prompt.strip() == "":
@@ -207,20 +253,23 @@ def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps,
207
  width, height = update_dimensions_on_upload(pil_images[0])
208
  print(f"[infer] input={pil_images[0].size}, output={width}x{height}, seed={seed}")
209
 
210
- # Per-step callback: logs wall-clock time per denoising step after a GPU sync.
211
- # Step 1 time includes any torch.compile Triton kernel compilation β€” if it is
212
- # much longer than later steps, compilation overhead is the bottleneck.
213
  _step_t = []
214
  def _step_cb(pipeline, step_idx, timestep, cb_kwargs):
215
  torch.cuda.synchronize()
216
- now = time.time()
217
  _step_t.append(now)
218
- delta = now - (_step_t[-2] if len(_step_t) > 1 else t0)
 
 
 
219
  tag = " ← includes compile" if step_idx == 0 else ""
220
- print(f"[infer] step {step_idx+1}/{steps} done β€” {delta:.1f}s{tag} | {_mem()} | {_t()}")
221
  return cb_kwargs
222
 
223
- t_pipe_start = time.time()
224
  print(f"[infer] calling pipe... {_t()}")
225
  try:
226
  result_image = pipe(
@@ -230,8 +279,10 @@ def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps,
230
  callback_on_step_end=_step_cb,
231
  callback_on_step_end_tensor_inputs=["latents"],
232
  ).images[0]
 
233
  print(f"[infer] VAE decode + postprocess done β€” {_mem(sync=True)} | {_t()}")
234
- duration = time.time() - t_pipe_start
 
235
  threading.Thread(
236
  target=log_inference,
237
  args=(pil_images, result_image, prompt, seed, steps, guidance_scale,
@@ -246,7 +297,8 @@ def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps,
246
  torch.cuda.synchronize()
247
  except Exception as cuda_err:
248
  print(f"[infer] CUDA synchronize after error: {cuda_err}")
249
- duration = time.time() - t_pipe_start
 
250
  threading.Thread(
251
  target=log_inference,
252
  args=(pil_images, None, prompt, seed, steps, guidance_scale,
 
161
  @spaces.GPU
162
  def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps, progress=gr.Progress(track_tqdm=True)):
163
  import time, traceback
164
+ _cuda_ok = torch.cuda.is_available()
165
+ _marks: dict = {} # name -> (cuda_event | None, perf_counter_float)
166
+
167
+ def _mark(name: str) -> None:
168
+ ev = None
169
+ if _cuda_ok:
170
+ ev = torch.cuda.Event(enable_timing=True)
171
+ ev.record()
172
+ _marks[name] = (ev, time.perf_counter())
173
+
174
+ def _elapsed_ms(a: str, b: str) -> float:
175
+ ev_a, t_a = _marks[a]
176
+ ev_b, t_b = _marks[b]
177
+ if ev_a and ev_b:
178
+ return ev_a.elapsed_time(ev_b) # true GPU-timeline ms
179
+ return (t_b - t_a) * 1000.0
180
+
181
+ def _print_timings() -> None:
182
+ if _cuda_ok:
183
+ try:
184
+ torch.cuda.synchronize()
185
+ except Exception:
186
+ pass
187
+ rows = [
188
+ ("image_load", "load_start", "load_end"),
189
+ ("preprocess", "pipe_start", "first_step"),
190
+ ("inference", "first_step", "last_step"),
191
+ ("vae_decode", "last_step", "pipe_end"),
192
+ ]
193
+ total_ms = 0.0
194
+ lines = []
195
+ for label, a, b in rows:
196
+ if a in _marks and b in _marks:
197
+ ms = _elapsed_ms(a, b)
198
+ total_ms += ms
199
+ lines.append(f"[timing] {label:<14} {ms:8.1f} ms")
200
+ if "load_start" in _marks and "pipe_end" in _marks:
201
+ overall_ms = _elapsed_ms("load_start", "pipe_end")
202
+ lines.append(f"[timing] {'overhead':<14} {overall_ms - total_ms:8.1f} ms")
203
+ lines.append(f"[timing] {'── total ──':<14} {overall_ms:8.1f} ms")
204
+ print("[timing] ─────────────────────────────────────")
205
+ print("\n".join(lines))
206
+ print("[timing] ─────────────────────────────────────")
207
+
208
+ t0 = time.perf_counter()
209
 
210
  def _t():
211
+ return f"t={time.perf_counter()-t0:.1f}s"
212
 
213
  def _mem(sync=False):
214
+ if not _cuda_ok:
215
  return "CUDA not available"
216
  if sync:
217
  try:
218
  torch.cuda.synchronize()
219
  except Exception as se:
220
  return f"CUDA sync failed: {se}"
221
+ alloc = torch.cuda.memory_allocated() / 1024**3
222
+ reserved = torch.cuda.memory_reserved() / 1024**3
223
+ peak = torch.cuda.max_memory_allocated()/ 1024**3
224
  return f"alloc={alloc:.2f}GB reserved={reserved:.2f}GB peak={peak:.2f}GB"
225
 
226
  print(f"[infer] ===== START =====")
227
  print(f"[infer] steps={steps}, guidance={guidance_scale}, seed={seed}, randomize={randomize_seed}")
228
  print(f"[infer] prompt={repr(prompt[:120])}")
229
 
230
+ if _cuda_ok:
231
  p = torch.cuda.get_device_properties(0)
232
  print(f"[infer] GPU: {p.name}, total={p.total_memory/1024**3:.1f}GB, cap={p.major}.{p.minor}")
233
  torch.cuda.reset_peak_memory_stats()
 
238
  torch.cuda.empty_cache()
239
  print(f"[infer] cache cleared β€” {_mem()}")
240
 
241
+ _mark("load_start")
242
  pil_images = b64_to_pil_list(images_b64_json)
243
+ _mark("load_end")
244
+ print(f"[infer] decoded {len(pil_images)} image(s) β€” {_elapsed_ms('load_start', 'load_end'):.0f}ms")
245
  if not pil_images:
246
  raise gr.Error("Please upload at least one image to edit.")
247
  if not prompt or prompt.strip() == "":
 
253
  width, height = update_dimensions_on_upload(pil_images[0])
254
  print(f"[infer] input={pil_images[0].size}, output={width}x{height}, seed={seed}")
255
 
256
+ # Per-step callback: syncs the GPU then records a CUDA event so elapsed_time()
257
+ # gives true GPU-timeline durations for preprocess / inference / vae_decode.
258
+ # Step 1 time includes any torch.compile Triton kernel compilation.
259
  _step_t = []
260
  def _step_cb(pipeline, step_idx, timestep, cb_kwargs):
261
  torch.cuda.synchronize()
262
+ now = time.perf_counter()
263
  _step_t.append(now)
264
+ if step_idx == 0:
265
+ _mark("first_step")
266
+ _mark("last_step") # overwritten each iteration; final value = end of last step
267
+ delta_ms = (now - (_step_t[-2] if len(_step_t) > 1 else t0)) * 1000
268
  tag = " ← includes compile" if step_idx == 0 else ""
269
+ print(f"[infer] step {step_idx+1}/{steps} done β€” {delta_ms:.0f}ms{tag} | {_mem()} | {_t()}")
270
  return cb_kwargs
271
 
272
+ _mark("pipe_start")
273
  print(f"[infer] calling pipe... {_t()}")
274
  try:
275
  result_image = pipe(
 
279
  callback_on_step_end=_step_cb,
280
  callback_on_step_end_tensor_inputs=["latents"],
281
  ).images[0]
282
+ _mark("pipe_end")
283
  print(f"[infer] VAE decode + postprocess done β€” {_mem(sync=True)} | {_t()}")
284
+ _print_timings()
285
+ duration = _elapsed_ms("pipe_start", "pipe_end") / 1000.0
286
  threading.Thread(
287
  target=log_inference,
288
  args=(pil_images, result_image, prompt, seed, steps, guidance_scale,
 
297
  torch.cuda.synchronize()
298
  except Exception as cuda_err:
299
  print(f"[infer] CUDA synchronize after error: {cuda_err}")
300
+ _print_timings()
301
+ duration = time.perf_counter() - _marks["pipe_start"][1]
302
  threading.Thread(
303
  target=log_inference,
304
  args=(pil_images, None, prompt, seed, steps, guidance_scale,