From b96820a0d2e0a2a3cfab5ef9c0fd46d59ff72f12 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 5 Jul 2026 05:30:14 +0000 Subject: [PATCH] Give the 720p HunyuanVideo repack its own family and free offload hooks on cancel The trusted-repo allowlist admitted the 720p t2v repack while the only Hunyuan family entry carried 480p presets, so a 720p load silently defaulted to 832x480. It now resolves a dedicated entry whose repo-id alias outranks the generic token (same guider config, verified: both repos ship guidance 6.0). A scheduler-wrapped cancel unwinds pipe call by exception and skips the pipeline's end-of-call maybe_free_model_hooks, leaving onloaded offload modules on the GPU until the next request; generate() now frees them before surfacing the cancelled sentinel. --- studio/backend/core/inference/video.py | 11 +++++++++ .../backend/core/inference/video_families.py | 24 +++++++++++++++++++ studio/backend/tests/test_video_backend.py | 23 ++++++++++++++++++ studio/backend/tests/test_video_families.py | 1 + 4 files changed, 59 insertions(+) diff --git a/studio/backend/core/inference/video.py b/studio/backend/core/inference/video.py index 8e833576b0..9958ddc3d9 100644 --- a/studio/backend/core/inference/video.py +++ b/studio/backend/core/inference/video.py @@ -1238,6 +1238,17 @@ class VideoBackend: with torch.inference_mode(), progress_ctx: output = pipe(**kwargs) except _VideoGenerationCancelled: + # This cancel unwinds pipe.__call__ by exception (the scheduler + # wrapper has no cooperative _interrupt), skipping the pipeline's + # end-of-call maybe_free_model_hooks(); under model/group offload + # the currently-onloaded modules would otherwise stay on the GPU + # until the next request touches them. + free_hooks = getattr(pipe, "maybe_free_model_hooks", None) + if callable(free_hooks): + try: + free_hooks() + except Exception: # noqa: BLE001 -- cleanup is best-effort + pass raise RuntimeError(VIDEO_CANCELLED_MSG) from None if cancel.is_set(): raise RuntimeError(VIDEO_CANCELLED_MSG) diff --git a/studio/backend/core/inference/video_families.py b/studio/backend/core/inference/video_families.py index 9278ca1744..91ef0dbc27 100644 --- a/studio/backend/core/inference/video_families.py +++ b/studio/backend/core/inference/video_families.py @@ -237,6 +237,30 @@ _FAMILIES: tuple[VideoFamily, ...] = ( # VAE (4.7 -> 2.4); the Qwen2.5-VL TE is stored bf16 (14.0) plus ByT5 0.8. bf16_components_gb = (16.6, 14.8, 2.4), ), + # The 720p t2v repack: same architecture, pipeline quirks, guider config + # (guidance 6.0) and shard footprint as the 480p entry above; only the + # trained resolution class differs. Kept as its OWN family so a 720p load + # defaults to 720p-class sizes instead of silently rendering at 832x480. + # The repo-id alias is the full path segment, so it out-lengths (and thus + # outranks) the generic "hunyuanvideo-1.5" token for this repo only. + VideoFamily( + name = "hunyuanvideo-1.5-720p", + pipeline_class = "HunyuanVideo15Pipeline", + transformer_class = "HunyuanVideo15Transformer3DModel", + base_repo = "hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v", + aliases = ("hunyuanvideo-1.5-diffusers-720p_t2v", "hv15-720p"), + has_audio = False, + guidance_via_guider = True, + default_steps = 50, + default_guidance = 6.0, + default_num_frames = 121, + default_fps = 24, + frame_step = 4, + resolution_multiple = 16, + # 720p-class presets: landscape, vertical, square (all /16). + resolution_presets = ((1280, 720), (720, 1280), (960, 960)), + bf16_components_gb = (16.6, 14.8, 2.4), + ), ) diff --git a/studio/backend/tests/test_video_backend.py b/studio/backend/tests/test_video_backend.py index 92527dc358..d34f093d3d 100644 --- a/studio/backend/tests/test_video_backend.py +++ b/studio/backend/tests/test_video_backend.py @@ -303,6 +303,10 @@ class _FakeHV15Pipe: self.components = {"transformer": self.transformer, "vae": self.vae} self.moved_to = None self.last_kwargs = None + self.hooks_freed = 0 + + def maybe_free_model_hooks(self): + self.hooks_freed += 1 def to(self, device): self.moved_to = device @@ -718,6 +722,10 @@ def test_hv15_cancel_unwinds_scheduler_loop(fake_runtime): assert pipe.scheduler.calls == 1 # The wrapper must restore scheduler.step even on the exception path. assert pipe.scheduler.step.__func__ is _FakeHV15Scheduler.step + # The exception unwound pipe.__call__ before its own end-of-call cleanup, so + # generate() must have freed the offload hooks itself (VRAM would otherwise + # stay onloaded until the next request). + assert pipe.hooks_freed == 1 def test_singleton(): @@ -1129,3 +1137,18 @@ def test_base_download_files_ltx23_keeps_only_shared_components(): assert not any( n.startswith(("vae/", "connectors/", "latent_upsampler/", "transformer/")) for n in names ) + + +def test_hv15_720p_repo_gets_720p_family_defaults(): + # The 720p repack is trusted, but it must resolve its OWN family entry: the + # generic hunyuanvideo-1.5 entry would default generation to 832x480. + from core.inference.video_families import detect_video_family + + fam = detect_video_family("hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v") + assert fam is not None and fam.name == "hunyuanvideo-1.5-720p" + assert fam.resolution_presets[0] == (1280, 720) + assert fam.base_repo.endswith("720p_t2v") + # The 480p repo keeps the original entry. + fam480 = detect_video_family("hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v") + assert fam480 is not None and fam480.name == "hunyuanvideo-1.5" + assert fam480.resolution_presets[0] == (832, 480) diff --git a/studio/backend/tests/test_video_families.py b/studio/backend/tests/test_video_families.py index 0f816d1a29..364a79ab07 100644 --- a/studio/backend/tests/test_video_families.py +++ b/studio/backend/tests/test_video_families.py @@ -151,6 +151,7 @@ def test_supported_names(): "wan2.2-ti2v-5b", "wan2.2-t2v-a14b", "hunyuanvideo-1.5", + "hunyuanvideo-1.5-720p", )