Commit ·
ea4ada4
1
Parent(s): 80ce38f
Break down upscale's non-model overhead into named profiling spans (#61)
Browse filesUpscaleProfiler 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 +36 -0
- postprocess/upscale/upscale.py +28 -22
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 |
-
|
| 134 |
-
for
|
| 135 |
-
for
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
(
|
| 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 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
| 162 |
return output
|
| 163 |
|
| 164 |
|
|
@@ -207,14 +209,18 @@ def upscale_frames(
|
|
| 207 |
batch.append(frames[i])
|
| 208 |
i += 1
|
| 209 |
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
| 212 |
upscaled = _tile_process(model, padded, profiler)
|
| 213 |
# crop the pre-pad border (scaled) back off
|
| 214 |
upscaled = upscaled[:, :, : h * SCALE, : w * SCALE]
|
| 215 |
-
|
|
|
|
| 216 |
for result in results:
|
| 217 |
-
|
|
|
|
| 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 |
|