BoxOfColors commited on
Commit
3bede42
Β·
1 Parent(s): 3f11a0e

Prevent GPU OOM from crashing the whole Space container

Browse files

Root cause of "every error triggers a full rebuild": none of the six
@spaces.GPU functions caught CUDA OOM, so an out-of-memory error could
escape as an unrecoverable worker fault instead of a clean exception β€”
forcing HF to restart the whole container (wiping /tmp, re-downloading
~30GB of checkpoints) instead of just failing one request.

- _catch_oom wraps all 6 @spaces.GPU functions: catches
torch.cuda.OutOfMemoryError, clears the cache, and re-raises as
gr.Error so it renders as a clean UI error instead of a fatal fault.
- Enable HunyuanVideo-Foley's existing (but previously unused)
enable_offload=True path, which swaps SigLIP2/CLAP/Synchformer/DAC-VAE
in and out instead of holding ~14GB of static weights resident for
the whole call β€” the swap logic was already fully wired in the
vendored model_utils.py/feature_utils.py, just never turned on.
- Add del + empty_cache for MMAudio's net/feature_utils (~9.5GB) at the
end of _mmaudio_gpu_infer and _regen_mmaudio_gpu, which had no
cleanup at all (TARO already does this for its own components).

Files changed (1) hide show
  1. app.py +49 -1
app.py CHANGED
@@ -8,6 +8,8 @@ Supported models
8
  HunyuanFoley – text-guided foley via SigLIP2 + Synchformer + CLAP (48 kHz, up to 15 s)
9
  """
10
 
 
 
11
  import html as _html
12
  import math
13
  import os
@@ -514,7 +516,7 @@ def _load_hunyuan_model(device, model_size):
514
  hunyuan_weights_dir = str(HUNYUAN_MODEL_DIR / "HunyuanVideo-Foley")
515
  print(f"[HunyuanFoley] Loading {model_size.upper()} model from {hunyuan_weights_dir}")
516
  return load_model(hunyuan_weights_dir, config_path, device,
517
- enable_offload=False, model_size=model_size)
518
 
519
 
520
  def mux_video_audio(silent_video: str, audio_path: str, output_path: str,
@@ -673,6 +675,29 @@ HUNYUAN_MAX_DUR = MODEL_CONFIGS["hunyuan"]["window_s"]
673
  HUNYUAN_SECS_PER_STEP = MODEL_CONFIGS["hunyuan"]["secs_per_step"]
674
 
675
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
676
  def _clamp_duration(secs: float, label: str) -> int:
677
  """Clamp a raw GPU-seconds estimate to [120, GPU_DURATION_CAP] and log it.
678
  ZeroGPU Pro users get up to 300 s per call; 120 s floor covers cold-disk
@@ -1004,6 +1029,7 @@ def _cpu_preprocess(video_file: str, model_dur: float,
1004
 
1005
 
1006
  @spaces.GPU(duration=_taro_duration)
 
1007
  def _taro_gpu_infer(video_file, seed_val, cfg_scale, num_steps, mode,
1008
  crossfade_s, crossfade_db, num_samples):
1009
  """GPU-only TARO inference β€” model loading + feature extraction + diffusion.
@@ -1176,6 +1202,7 @@ def _mmaudio_duration(video_file, prompt, negative_prompt, seed_val,
1176
 
1177
 
1178
  @spaces.GPU(duration=_mmaudio_duration)
 
1179
  def _mmaudio_gpu_infer(video_file, prompt, negative_prompt, seed_val,
1180
  cfg_strength, num_steps, crossfade_s, crossfade_db, num_samples,
1181
  silent_video, segments_json,
@@ -1273,6 +1300,15 @@ def _mmaudio_gpu_infer(video_file, prompt, negative_prompt, seed_val,
1273
  if torch.cuda.is_available():
1274
  torch.cuda.empty_cache()
1275
 
 
 
 
 
 
 
 
 
 
1276
  return results
1277
 
1278
 
@@ -1343,6 +1379,7 @@ def _hunyuan_duration(video_file, prompt, negative_prompt, seed_val,
1343
 
1344
 
1345
  @spaces.GPU(duration=_hunyuan_duration)
 
1346
  def _hunyuan_gpu_infer(video_file, prompt, negative_prompt, seed_val,
1347
  guidance_scale, num_steps, model_size, crossfade_s, crossfade_db,
1348
  num_samples, silent_video, segments_json, total_dur_s,
@@ -1558,6 +1595,7 @@ def _taro_regen_duration(video_file, seg_idx, seg_meta_json,
1558
 
1559
 
1560
  @spaces.GPU(duration=_taro_regen_duration)
 
1561
  def _regen_taro_gpu(video_file, seg_idx, seg_meta_json,
1562
  seed_val, cfg_scale, num_steps, mode,
1563
  crossfade_s, crossfade_db, slot_id=None):
@@ -1634,6 +1672,7 @@ def _mmaudio_regen_duration(video_file, seg_idx, seg_meta_json,
1634
 
1635
 
1636
  @spaces.GPU(duration=_mmaudio_regen_duration)
 
1637
  def _regen_mmaudio_gpu(video_file, seg_idx, seg_meta_json,
1638
  prompt, negative_prompt, seed_val,
1639
  cfg_strength, num_steps, crossfade_s, crossfade_db,
@@ -1679,6 +1718,14 @@ def _regen_mmaudio_gpu(video_file, seg_idx, seg_meta_json,
1679
  cfg_strength=float(cfg_strength),
1680
  )
1681
  new_wav = audios.float().cpu()[0].numpy() # full window β€” _stitch_wavs trims
 
 
 
 
 
 
 
 
1682
  return new_wav, sr
1683
 
1684
 
@@ -1719,6 +1766,7 @@ def _hunyuan_regen_duration(video_file, seg_idx, seg_meta_json,
1719
 
1720
 
1721
  @spaces.GPU(duration=_hunyuan_regen_duration)
 
1722
  def _regen_hunyuan_gpu(video_file, seg_idx, seg_meta_json,
1723
  prompt, negative_prompt, seed_val,
1724
  guidance_scale, num_steps, model_size,
 
8
  HunyuanFoley – text-guided foley via SigLIP2 + Synchformer + CLAP (48 kHz, up to 15 s)
9
  """
10
 
11
+ import functools
12
+ import gc
13
  import html as _html
14
  import math
15
  import os
 
516
  hunyuan_weights_dir = str(HUNYUAN_MODEL_DIR / "HunyuanVideo-Foley")
517
  print(f"[HunyuanFoley] Loading {model_size.upper()} model from {hunyuan_weights_dir}")
518
  return load_model(hunyuan_weights_dir, config_path, device,
519
+ enable_offload=True, model_size=model_size)
520
 
521
 
522
  def mux_video_audio(silent_video: str, audio_path: str, output_path: str,
 
675
  HUNYUAN_SECS_PER_STEP = MODEL_CONFIGS["hunyuan"]["secs_per_step"]
676
 
677
 
678
+ def _catch_oom(fn):
679
+ """Wrap a @spaces.GPU function so a CUDA OOM can't escape and crash the
680
+ ZeroGPU worker. An escaped OOM doesn't just fail that one request β€” it can
681
+ take down the whole Space container, which restarts from scratch (every
682
+ checkpoint re-downloaded, ~2 minutes) instead of just showing an error.
683
+ Applied as the innermost decorator (below @spaces.GPU) so spaces.GPU's
684
+ duration estimator still sees the original call signature via
685
+ functools.wraps."""
686
+ @functools.wraps(fn)
687
+ def _wrapped(*args, **kwargs):
688
+ try:
689
+ return fn(*args, **kwargs)
690
+ except torch.cuda.OutOfMemoryError as e:
691
+ if torch.cuda.is_available():
692
+ torch.cuda.empty_cache()
693
+ gc.collect()
694
+ raise gr.Error(
695
+ f"Out of GPU memory during generation ({fn.__name__}). "
696
+ "Try fewer samples, a shorter clip, or fewer steps."
697
+ ) from e
698
+ return _wrapped
699
+
700
+
701
  def _clamp_duration(secs: float, label: str) -> int:
702
  """Clamp a raw GPU-seconds estimate to [120, GPU_DURATION_CAP] and log it.
703
  ZeroGPU Pro users get up to 300 s per call; 120 s floor covers cold-disk
 
1029
 
1030
 
1031
  @spaces.GPU(duration=_taro_duration)
1032
+ @_catch_oom
1033
  def _taro_gpu_infer(video_file, seed_val, cfg_scale, num_steps, mode,
1034
  crossfade_s, crossfade_db, num_samples):
1035
  """GPU-only TARO inference β€” model loading + feature extraction + diffusion.
 
1202
 
1203
 
1204
  @spaces.GPU(duration=_mmaudio_duration)
1205
+ @_catch_oom
1206
  def _mmaudio_gpu_infer(video_file, prompt, negative_prompt, seed_val,
1207
  cfg_strength, num_steps, crossfade_s, crossfade_db, num_samples,
1208
  silent_video, segments_json,
 
1300
  if torch.cuda.is_available():
1301
  torch.cuda.empty_cache()
1302
 
1303
+ # net + feature_utils (CLIP + Synchformer + VAE) are ~9.5GB combined and,
1304
+ # unlike TARO/HunyuanFoley, were never explicitly freed here β€” do it before
1305
+ # returning so the ZeroGPU worker isn't left holding peak memory longer
1306
+ # than necessary.
1307
+ del net, feature_utils
1308
+ gc.collect()
1309
+ if torch.cuda.is_available():
1310
+ torch.cuda.empty_cache()
1311
+
1312
  return results
1313
 
1314
 
 
1379
 
1380
 
1381
  @spaces.GPU(duration=_hunyuan_duration)
1382
+ @_catch_oom
1383
  def _hunyuan_gpu_infer(video_file, prompt, negative_prompt, seed_val,
1384
  guidance_scale, num_steps, model_size, crossfade_s, crossfade_db,
1385
  num_samples, silent_video, segments_json, total_dur_s,
 
1595
 
1596
 
1597
  @spaces.GPU(duration=_taro_regen_duration)
1598
+ @_catch_oom
1599
  def _regen_taro_gpu(video_file, seg_idx, seg_meta_json,
1600
  seed_val, cfg_scale, num_steps, mode,
1601
  crossfade_s, crossfade_db, slot_id=None):
 
1672
 
1673
 
1674
  @spaces.GPU(duration=_mmaudio_regen_duration)
1675
+ @_catch_oom
1676
  def _regen_mmaudio_gpu(video_file, seg_idx, seg_meta_json,
1677
  prompt, negative_prompt, seed_val,
1678
  cfg_strength, num_steps, crossfade_s, crossfade_db,
 
1718
  cfg_strength=float(cfg_strength),
1719
  )
1720
  new_wav = audios.float().cpu()[0].numpy() # full window β€” _stitch_wavs trims
1721
+
1722
+ # See _mmaudio_gpu_infer β€” net + feature_utils (~9.5GB) had no explicit
1723
+ # cleanup here before returning to the ZeroGPU worker.
1724
+ del net, feature_utils
1725
+ gc.collect()
1726
+ if torch.cuda.is_available():
1727
+ torch.cuda.empty_cache()
1728
+
1729
  return new_wav, sr
1730
 
1731
 
 
1766
 
1767
 
1768
  @spaces.GPU(duration=_hunyuan_regen_duration)
1769
+ @_catch_oom
1770
  def _regen_hunyuan_gpu(video_file, seg_idx, seg_meta_json,
1771
  prompt, negative_prompt, seed_val,
1772
  guidance_scale, num_steps, model_size,