Prevent GPU OOM from crashing the whole Space container
Browse filesRoot 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).
|
@@ -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=
|
| 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,
|