someone-in-the-world Claude Sonnet 5 commited on
Commit
ea4ada4
·
1 Parent(s): 80ce38f

Break down upscale's non-model overhead into named profiling spans (#61)

Browse files

UpscaleProfiler previously only measured model() calls, leaving the ~74%
of upscale wall time spent outside them as one opaque "non_model" number.
Adds a span() context manager and wraps job_grouping, tile_merge,
frames_to_tensor, pre_pad, host_transfer, and pil_convert so the next
UPSCALE_PROFILE=1 run on the dev Space localizes where that time actually
goes.

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

postprocess/upscale/profiling.py CHANGED
@@ -6,6 +6,11 @@ only marks a dim dynamic after seeing more than one value for it, so the very fi
6
  given (shape, batch_size) pair is the one that risks eating a recompile, not just the first call
7
  overall. Also tracks peak CUDA memory for the run. See issue #48.
8
 
 
 
 
 
 
9
  Off by default — the `synchronize()` calls this needs for accurate per-call timing would
10
  otherwise skew real request latency by killing async kernel overlap.
11
  """
@@ -28,6 +33,8 @@ class UpscaleProfiler:
28
  self._first_call_time = 0.0
29
  self._steady_call_count = 0
30
  self._steady_call_time = 0.0
 
 
31
  self._wall_t0 = _time.perf_counter()
32
  if self.enabled and device.type == "cuda":
33
  torch.cuda.reset_peak_memory_stats(device)
@@ -55,6 +62,23 @@ class UpscaleProfiler:
55
  self._first_call_count += 1
56
  self._first_call_time += elapsed
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  def report(self, frame_count: int, tile_size: int, frame_batch_size: int, max_tile_batch: int) -> None:
59
  if not self.enabled:
60
  return
@@ -70,6 +94,18 @@ class UpscaleProfiler:
70
  f"| steady calls={self._steady_call_count} total={self._steady_call_time:.2f}s avg={steady_avg:.3f}s/call",
71
  flush=True,
72
  )
 
 
 
 
 
 
 
 
 
 
 
 
73
  if self._device.type == "cuda":
74
  peak_allocated = torch.cuda.max_memory_allocated(self._device) / 1024**3
75
  peak_reserved = torch.cuda.max_memory_reserved(self._device) / 1024**3
 
6
  given (shape, batch_size) pair is the one that risks eating a recompile, not just the first call
7
  overall. Also tracks peak CUDA memory for the run. See issue #48.
8
 
9
+ Also buckets the non-model spans identified in issue #61 (frame/tensor prep, tile-shape grouping,
10
+ tile merge, the post-tile host transfer, and per-frame PIL conversion) via `span()`, so a dev-Space
11
+ run localizes exactly where the ~74%-of-wall-time "non_model" cost goes instead of leaving it as
12
+ one opaque number.
13
+
14
  Off by default — the `synchronize()` calls this needs for accurate per-call timing would
15
  otherwise skew real request latency by killing async kernel overlap.
16
  """
 
33
  self._first_call_time = 0.0
34
  self._steady_call_count = 0
35
  self._steady_call_time = 0.0
36
+ self._span_times: dict[str, float] = {}
37
+ self._span_counts: dict[str, int] = {}
38
  self._wall_t0 = _time.perf_counter()
39
  if self.enabled and device.type == "cuda":
40
  torch.cuda.reset_peak_memory_stats(device)
 
62
  self._first_call_count += 1
63
  self._first_call_time += elapsed
64
 
65
+ @contextmanager
66
+ def span(self, name: str) -> Iterator[None]:
67
+ """Wrap an arbitrary non-model span (e.g. tile merge, host transfer, PIL conversion),
68
+ accumulating elapsed time under `name` for the non_model breakdown in `report()`."""
69
+ if not self.enabled:
70
+ yield
71
+ return
72
+ if self._device.type == "cuda":
73
+ torch.cuda.synchronize()
74
+ t0 = _time.perf_counter()
75
+ yield
76
+ if self._device.type == "cuda":
77
+ torch.cuda.synchronize()
78
+ elapsed = _time.perf_counter() - t0
79
+ self._span_times[name] = self._span_times.get(name, 0.0) + elapsed
80
+ self._span_counts[name] = self._span_counts.get(name, 0) + 1
81
+
82
  def report(self, frame_count: int, tile_size: int, frame_batch_size: int, max_tile_batch: int) -> None:
83
  if not self.enabled:
84
  return
 
94
  f"| steady calls={self._steady_call_count} total={self._steady_call_time:.2f}s avg={steady_avg:.3f}s/call",
95
  flush=True,
96
  )
97
+ if self._span_times:
98
+ non_model = wall_elapsed - model_time
99
+ span_total = sum(self._span_times.values())
100
+ breakdown = " ".join(
101
+ f"{name}={t:.2f}s(n={self._span_counts[name]})"
102
+ for name, t in sorted(self._span_times.items(), key=lambda kv: -kv[1])
103
+ )
104
+ print(
105
+ f"[upscale] profile: non_model breakdown: {breakdown} | "
106
+ f"tracked_total={span_total:.2f}s unaccounted={non_model - span_total:.2f}s",
107
+ flush=True,
108
+ )
109
  if self._device.type == "cuda":
110
  peak_allocated = torch.cuda.max_memory_allocated(self._device) / 1024**3
111
  peak_reserved = torch.cuda.max_memory_reserved(self._device) / 1024**3
postprocess/upscale/upscale.py CHANGED
@@ -130,17 +130,18 @@ def _tile_process(model: torch.nn.Module, img: torch.Tensor, profiler: UpscalePr
130
  # patch shape so each model() call is a single batched forward pass over same-shaped patches
131
  # (chunked to MAX_TILE_BATCH to bound memory) instead of one tile at a time.
132
  jobs_by_shape: dict[tuple[int, int], list[tuple[int, int, int, int, int, int, int, int, int]]] = {}
133
- for b in range(batch):
134
- for y in range(tiles_y):
135
- for x in range(tiles_x):
136
- in_x0, in_x1 = x * TILE_SIZE, min((x + 1) * TILE_SIZE, width)
137
- in_y0, in_y1 = y * TILE_SIZE, min((y + 1) * TILE_SIZE, height)
138
- pad_x0, pad_x1 = max(in_x0 - TILE_PAD, 0), min(in_x1 + TILE_PAD, width)
139
- pad_y0, pad_y1 = max(in_y0 - TILE_PAD, 0), min(in_y1 + TILE_PAD, height)
140
- shape = (pad_y1 - pad_y0, pad_x1 - pad_x0)
141
- jobs_by_shape.setdefault(shape, []).append(
142
- (b, in_x0, in_y0, in_x1, in_y1, pad_x0, pad_y0, pad_x1, pad_y1)
143
- )
 
144
 
145
  for shape, jobs in jobs_by_shape.items():
146
  for chunk_start in range(0, len(jobs), MAX_TILE_BATCH):
@@ -152,13 +153,14 @@ def _tile_process(model: torch.nn.Module, img: torch.Tensor, profiler: UpscalePr
152
  with profiler.timed(shape, patch.shape[0]):
153
  tile_out = model(patch)
154
 
155
- for i, (b, in_x0, in_y0, in_x1, in_y1, pad_x0, pad_y0, pad_x1, pad_y1) in enumerate(chunk):
156
- trim_x0, trim_y0 = (in_x0 - pad_x0) * SCALE, (in_y0 - pad_y0) * SCALE
157
- trim_x1 = trim_x0 + (in_x1 - in_x0) * SCALE
158
- trim_y1 = trim_y0 + (in_y1 - in_y0) * SCALE
159
- output[b:b + 1, :, in_y0 * SCALE:in_y1 * SCALE, in_x0 * SCALE:in_x1 * SCALE] = (
160
- tile_out[i:i + 1, :, trim_y0:trim_y1, trim_x0:trim_x1]
161
- )
 
162
  return output
163
 
164
 
@@ -207,14 +209,18 @@ def upscale_frames(
207
  batch.append(frames[i])
208
  i += 1
209
 
210
- tensor = _frames_to_tensor(batch)
211
- padded = _pre_pad(tensor, TILE_PAD)
 
 
212
  upscaled = _tile_process(model, padded, profiler)
213
  # crop the pre-pad border (scaled) back off
214
  upscaled = upscaled[:, :, : h * SCALE, : w * SCALE]
215
- results = upscaled.clamp(0, 1).permute(0, 2, 3, 1).float().cpu().numpy()
 
216
  for result in results:
217
- out_frames.append(Image.fromarray((result * 255.0).round().astype(np.uint8)))
 
218
  if progress_callback is not None:
219
  progress_callback(len(out_frames), len(frames))
220
 
 
130
  # patch shape so each model() call is a single batched forward pass over same-shaped patches
131
  # (chunked to MAX_TILE_BATCH to bound memory) instead of one tile at a time.
132
  jobs_by_shape: dict[tuple[int, int], list[tuple[int, int, int, int, int, int, int, int, int]]] = {}
133
+ with profiler.span("job_grouping"):
134
+ for b in range(batch):
135
+ for y in range(tiles_y):
136
+ for x in range(tiles_x):
137
+ in_x0, in_x1 = x * TILE_SIZE, min((x + 1) * TILE_SIZE, width)
138
+ in_y0, in_y1 = y * TILE_SIZE, min((y + 1) * TILE_SIZE, height)
139
+ pad_x0, pad_x1 = max(in_x0 - TILE_PAD, 0), min(in_x1 + TILE_PAD, width)
140
+ pad_y0, pad_y1 = max(in_y0 - TILE_PAD, 0), min(in_y1 + TILE_PAD, height)
141
+ shape = (pad_y1 - pad_y0, pad_x1 - pad_x0)
142
+ jobs_by_shape.setdefault(shape, []).append(
143
+ (b, in_x0, in_y0, in_x1, in_y1, pad_x0, pad_y0, pad_x1, pad_y1)
144
+ )
145
 
146
  for shape, jobs in jobs_by_shape.items():
147
  for chunk_start in range(0, len(jobs), MAX_TILE_BATCH):
 
153
  with profiler.timed(shape, patch.shape[0]):
154
  tile_out = model(patch)
155
 
156
+ with profiler.span("tile_merge"):
157
+ for i, (b, in_x0, in_y0, in_x1, in_y1, pad_x0, pad_y0, pad_x1, pad_y1) in enumerate(chunk):
158
+ trim_x0, trim_y0 = (in_x0 - pad_x0) * SCALE, (in_y0 - pad_y0) * SCALE
159
+ trim_x1 = trim_x0 + (in_x1 - in_x0) * SCALE
160
+ trim_y1 = trim_y0 + (in_y1 - in_y0) * SCALE
161
+ output[b:b + 1, :, in_y0 * SCALE:in_y1 * SCALE, in_x0 * SCALE:in_x1 * SCALE] = (
162
+ tile_out[i:i + 1, :, trim_y0:trim_y1, trim_x0:trim_x1]
163
+ )
164
  return output
165
 
166
 
 
209
  batch.append(frames[i])
210
  i += 1
211
 
212
+ with profiler.span("frames_to_tensor"):
213
+ tensor = _frames_to_tensor(batch)
214
+ with profiler.span("pre_pad"):
215
+ padded = _pre_pad(tensor, TILE_PAD)
216
  upscaled = _tile_process(model, padded, profiler)
217
  # crop the pre-pad border (scaled) back off
218
  upscaled = upscaled[:, :, : h * SCALE, : w * SCALE]
219
+ with profiler.span("host_transfer"):
220
+ results = upscaled.clamp(0, 1).permute(0, 2, 3, 1).float().cpu().numpy()
221
  for result in results:
222
+ with profiler.span("pil_convert"):
223
+ out_frames.append(Image.fromarray((result * 255.0).round().astype(np.uint8)))
224
  if progress_callback is not None:
225
  progress_callback(len(out_frames), len(frames))
226