Nekochu commited on
Commit
8ea0c8b
·
1 Parent(s): 0b6961f

GPU postprocessing pipeline + TF32 + conditional torch.compile

Browse files
Files changed (1) hide show
  1. app.py +112 -44
app.py CHANGED
@@ -36,6 +36,17 @@ try:
36
  except ImportError:
37
  HAS_SPACES = False
38
 
 
 
 
 
 
 
 
 
 
 
 
39
  # Workaround: Gradio cache_examples bug with None outputs.
40
  _original_read_from_flag = gr.components.Component.read_from_flag
41
  def _patched_read_from_flag(self, payload):
@@ -116,7 +127,7 @@ def clean_matte(alpha_np, area_threshold=300, dilation=15, blur_size=5):
116
  is_3d = alpha_np.ndim == 3
117
  if is_3d:
118
  alpha_np = alpha_np[:, :, 0]
119
- mask_8u = (alpha_np > 0.5).astype(np.uint8) * 255
120
  num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask_8u, connectivity=8)
121
  valid = np.zeros(num_labels, dtype=bool)
122
  valid[1:] = stats[1:, cv2.CC_STAT_AREA] >= area_threshold
@@ -138,6 +149,60 @@ def create_checkerboard(w, h, checker_size=64, color1=0.15, color2=0.55):
138
  def premultiply(fg, alpha):
139
  return fg * alpha
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  # ---------------------------------------------------------------------------
142
  # Fast classical green-screen mask
143
  # ---------------------------------------------------------------------------
@@ -288,8 +353,14 @@ def _load_greenformer(img_size):
288
  torch.backends.cuda.mem_efficient_sdp_enabled(),
289
  torch.backends.cuda.math_sdp_enabled())
290
 
291
- # Skip torch.compile on ZeroGPU — the 37s warmup eats too much of the 120s budget.
292
- if not HAS_SPACES and sys.platform in ("linux", "win32"):
 
 
 
 
 
 
293
  try:
294
  compiled = torch.compile(model)
295
  dummy = torch.zeros(1, 4, img_size, img_size, dtype=torch.float16, device="cuda")
@@ -301,7 +372,7 @@ def _load_greenformer(img_size):
301
  logger.warning("torch.compile() failed, using eager mode: %s", e)
302
  torch.cuda.empty_cache()
303
  else:
304
- logger.info("Skipping torch.compile() (ZeroGPU: saving GPU time for inference)")
305
 
306
  logger.info("GreenFormer loaded on CUDA (img_size=%d)", img_size)
307
  return model
@@ -359,61 +430,58 @@ def corridorkey_frame_onnx(session, image_f32, mask_f32, img_size,
359
  # ---------------------------------------------------------------------------
360
  def corridorkey_batch_pytorch(model, images_f32, masks_f32, img_size,
361
  despill_strength=0.5, auto_despeckle=True, despeckle_size=400):
362
- """PyTorch batched inference for multiple frames on GPU.
363
-
364
- Args:
365
- model: GreenFormer model on CUDA
366
- images_f32: list of [H, W, 3] float32 numpy arrays (0-1, sRGB)
367
- masks_f32: list of [H, W] float32 numpy arrays (0-1)
368
- img_size: model input resolution (1024 or 2048)
369
-
370
- Returns:
371
- list of dicts with 'alpha' [H,W,1] and 'fg' [H,W,3]
372
- """
373
  import torch
 
374
 
375
  batch_size = len(images_f32)
376
  if batch_size == 0:
377
  return []
378
 
379
- # Store original sizes per frame
380
- orig_sizes = [(img.shape[1], img.shape[0]) for img in images_f32] # (w, h)
 
 
 
 
 
 
 
381
 
382
- # Preprocess: resize, normalize, concatenate into batch tensor
383
- batch_inputs = []
384
- for img, mask in zip(images_f32, masks_f32):
385
- img_r = cv2.resize(img, (img_size, img_size))
386
- mask_r = cv2.resize(mask, (img_size, img_size))[:, :, np.newaxis]
387
- inp = np.concatenate([(img_r - IMAGENET_MEAN) / IMAGENET_STD, mask_r], axis=-1)
388
- batch_inputs.append(inp.transpose(2, 0, 1)) # [4, H, W]
389
 
390
- batch_np = np.stack(batch_inputs, axis=0).astype(np.float32) # [B, 4, H, W]
391
- batch_tensor = torch.from_numpy(batch_np).cuda().half() # FP16 input
392
 
393
- # Forward pass — model is FP16, input is FP16, no autocast needed
394
  with torch.inference_mode():
395
- out = model(batch_tensor)
 
 
 
 
 
396
 
397
- # Extract results
398
- alphas_gpu = out["alpha"].float().cpu().numpy() # [B, 1, H, W]
399
- fgs_gpu = out["fg"].float().cpu().numpy() # [B, 3, H, W]
400
 
401
- del batch_tensor
402
- # Don't empty cache per batch - too expensive. Let PyTorch manage.
 
 
 
 
 
 
403
 
404
- # Postprocess each frame
405
  results = []
406
  for i in range(batch_size):
407
- w, h = orig_sizes[i]
408
- alpha = cv2.resize(alphas_gpu[i].transpose(1, 2, 0), (w, h), interpolation=cv2.INTER_LANCZOS4)
409
- fg = cv2.resize(fgs_gpu[i].transpose(1, 2, 0), (w, h), interpolation=cv2.INTER_LANCZOS4)
410
- if alpha.ndim == 2:
411
- alpha = alpha[:, :, np.newaxis]
412
- if auto_despeckle:
413
- alpha = clean_matte(alpha, area_threshold=despeckle_size, dilation=25, blur_size=5)
414
- fg = despill(fg, green_limit_mode="average", strength=despill_strength)
415
- results.append({"alpha": alpha, "fg": fg})
416
-
417
  return results
418
 
419
 
 
36
  except ImportError:
37
  HAS_SPACES = False
38
 
39
+ # GPU perf: TF32 tensor cores for FP32 postprocessing ops
40
+ try:
41
+ import torch as _torch
42
+ _torch.set_float32_matmul_precision('high')
43
+ del _torch
44
+ except ImportError:
45
+ pass
46
+ # Persist torch.compile inductor cache across ZeroGPU sessions
47
+ _inductor_cache = os.path.join(os.path.expanduser("~"), ".cache", "corridorkey", "inductor")
48
+ os.environ.setdefault("TORCHINDUCTOR_CACHE_DIR", _inductor_cache)
49
+
50
  # Workaround: Gradio cache_examples bug with None outputs.
51
  _original_read_from_flag = gr.components.Component.read_from_flag
52
  def _patched_read_from_flag(self, payload):
 
127
  is_3d = alpha_np.ndim == 3
128
  if is_3d:
129
  alpha_np = alpha_np[:, :, 0]
130
+ mask_8u = (alpha_np > 0.25).astype(np.uint8) * 255
131
  num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask_8u, connectivity=8)
132
  valid = np.zeros(num_labels, dtype=bool)
133
  valid[1:] = stats[1:, cv2.CC_STAT_AREA] >= area_threshold
 
149
  def premultiply(fg, alpha):
150
  return fg * alpha
151
 
152
+ # ---------------------------------------------------------------------------
153
+ # GPU postprocessing utilities (keep data on device, avoid CPU↔GPU transfers)
154
+ # ---------------------------------------------------------------------------
155
+ def despill_torch(image, strength, screen_channel=1):
156
+ """GPU despill on [B, 3, H, W] tensor."""
157
+ import torch
158
+ if strength <= 0.0:
159
+ return image
160
+ other_a, other_b = [i for i in (0, 1, 2) if i != screen_channel]
161
+ screen, a, b = image[:, screen_channel], image[:, other_a], image[:, other_b]
162
+ spill = torch.clamp(screen - (a + b) / 2.0, min=0.0)
163
+ out = [None, None, None]
164
+ out[screen_channel] = screen - spill
165
+ out[other_a] = a + spill * 0.5
166
+ out[other_b] = b + spill * 0.5
167
+ despilled = torch.stack(out, dim=1)
168
+ return image * (1.0 - strength) + despilled * strength if strength < 1.0 else despilled
169
+
170
+
171
+ def _connected_components_gpu(mask, min_dist=4, max_iter=100):
172
+ """GPU flood-fill connected components on [B, 1, H, W] binary mask."""
173
+ import torch
174
+ import torch.nn.functional as F
175
+ bs, _, H, W = mask.shape
176
+ comp = (torch.randperm(bs * W * H, device=mask.device, dtype=torch.float32) + 1.1).view(mask.shape)
177
+ comp[mask != 1] = 0
178
+ k = 2 * min_dist + 1
179
+ for _ in range(max_iter):
180
+ comp[mask == 1] = F.max_pool2d(comp, k, stride=1, padding=min_dist)[mask == 1]
181
+ _, comp = torch.unique(comp, return_inverse=True)
182
+ return comp.view(mask.shape)
183
+
184
+
185
+ def clean_matte_torch(alpha, area_threshold, dilation=25, blur_size=5):
186
+ """GPU clean matte on [B, 1, H, W] tensor. Removes small disconnected blobs."""
187
+ import torch
188
+ import torch.nn.functional as F
189
+ import torchvision.transforms.functional as TF
190
+ mask = (alpha > 0.25).float()
191
+ comp = _connected_components_gpu(mask, max_iter=max(area_threshold // 20, 5))
192
+ sizes = torch.bincount(comp.flatten())
193
+ big = torch.nonzero(sizes >= area_threshold).squeeze(-1)
194
+ big = big[big > 0]
195
+ cleaned = torch.zeros_like(mask)
196
+ if big.numel() > 0:
197
+ cleaned[torch.isin(comp, big)] = 1.0
198
+ if dilation > 0:
199
+ for _ in range(dilation // 2):
200
+ cleaned = F.max_pool2d(cleaned, 5, stride=1, padding=2)
201
+ if blur_size > 0:
202
+ cleaned = TF.gaussian_blur(cleaned, [blur_size * 2 + 1, blur_size * 2 + 1])
203
+ return alpha * cleaned
204
+
205
+
206
  # ---------------------------------------------------------------------------
207
  # Fast classical green-screen mask
208
  # ---------------------------------------------------------------------------
 
353
  torch.backends.cuda.mem_efficient_sdp_enabled(),
354
  torch.backends.cuda.math_sdp_enabled())
355
 
356
+ should_compile = False
357
+ if sys.platform in ("linux", "win32"):
358
+ if not HAS_SPACES:
359
+ should_compile = True
360
+ elif os.path.isdir(_inductor_cache) and os.listdir(_inductor_cache):
361
+ should_compile = True
362
+ logger.info("Warm ZeroGPU: using cached inductor kernels")
363
+ if should_compile:
364
  try:
365
  compiled = torch.compile(model)
366
  dummy = torch.zeros(1, 4, img_size, img_size, dtype=torch.float16, device="cuda")
 
372
  logger.warning("torch.compile() failed, using eager mode: %s", e)
373
  torch.cuda.empty_cache()
374
  else:
375
+ logger.info("Skipping torch.compile() (cold start, no cached kernels)")
376
 
377
  logger.info("GreenFormer loaded on CUDA (img_size=%d)", img_size)
378
  return model
 
430
  # ---------------------------------------------------------------------------
431
  def corridorkey_batch_pytorch(model, images_f32, masks_f32, img_size,
432
  despill_strength=0.5, auto_despeckle=True, despeckle_size=400):
433
+ """PyTorch batched inference with full GPU pipeline (preprocess + inference + postprocess on device)."""
 
 
 
 
 
 
 
 
 
 
434
  import torch
435
+ import torchvision.transforms.functional as TF
436
 
437
  batch_size = len(images_f32)
438
  if batch_size == 0:
439
  return []
440
 
441
+ w, h = images_f32[0].shape[1], images_f32[0].shape[0]
442
+
443
+ # --- GPU Preprocessing in FP32 (avoids FP16 precision loss in normalize/resize) ---
444
+ batch_imgs = torch.stack([
445
+ torch.from_numpy(img.transpose(2, 0, 1)) for img in images_f32
446
+ ]).cuda()
447
+ batch_masks = torch.stack([
448
+ torch.from_numpy(m if m.ndim == 2 else m[:, :, 0]).unsqueeze(0) for m in masks_f32
449
+ ]).cuda()
450
 
451
+ batch_imgs = TF.resize(batch_imgs, [img_size, img_size], antialias=False)
452
+ batch_masks = TF.resize(batch_masks, [img_size, img_size], antialias=False)
453
+ batch_imgs = TF.normalize(batch_imgs, [0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
 
 
 
 
454
 
455
+ inp = torch.cat([batch_imgs, batch_masks], dim=1).half()
456
+ del batch_imgs, batch_masks
457
 
458
+ # --- Forward Pass ---
459
  with torch.inference_mode():
460
+ out = model(inp)
461
+ del inp
462
+
463
+ # --- GPU Postprocessing (despill + clean_matte + resize stay on device) ---
464
+ alpha = out["alpha"].float()
465
+ fg = out["fg"].float()
466
 
467
+ alpha = TF.resize(alpha, [h, w])
468
+ fg = TF.resize(fg, [h, w])
 
469
 
470
+ if auto_despeckle:
471
+ alpha = clean_matte_torch(alpha, area_threshold=int(despeckle_size), dilation=25, blur_size=5)
472
+ fg = despill_torch(fg, despill_strength)
473
+
474
+ # --- Single CPU transfer at the end ---
475
+ alpha_np = alpha.cpu().numpy()
476
+ fg_np = fg.cpu().numpy()
477
+ del alpha, fg
478
 
 
479
  results = []
480
  for i in range(batch_size):
481
+ results.append({
482
+ "alpha": alpha_np[i].transpose(1, 2, 0),
483
+ "fg": fg_np[i].transpose(1, 2, 0),
484
+ })
 
 
 
 
 
 
485
  return results
486
 
487