Spaces:
Running on Zero
Running on Zero
Vansh Chugh commited on
Commit ·
0410be1
1
Parent(s): f4fd752
Defer GPU move to inside process_fn (ZeroGPU can't touch CUDA from a background thread)
Browse files- app.py +48 -28
- unison/models/text_encoders/omni_encoder.py +7 -3
app.py
CHANGED
|
@@ -68,7 +68,7 @@ VARIANTS = {
|
|
| 68 |
# which LLM layers it extracts at construction time). Switching variants evicts the
|
| 69 |
# previous one and rebuilds from the local HF cache (no re-download).
|
| 70 |
_active_name = None
|
| 71 |
-
_active_entry = None
|
| 72 |
_loading = True
|
| 73 |
_load_error = None
|
| 74 |
|
|
@@ -76,11 +76,15 @@ _load_error = None
|
|
| 76 |
# so two requests running together would corrupt each other's results. This lock
|
| 77 |
# also covers _active_name/_active_entry below, so no separate lock is needed.
|
| 78 |
_gpu_lock = threading.Lock()
|
|
|
|
| 79 |
|
| 80 |
|
| 81 |
def _build_variant(name):
|
| 82 |
-
"""Download and construct one variant's full stack: text encoder, DiT
|
| 83 |
-
and VAE.
|
|
|
|
|
|
|
|
|
|
| 84 |
spec = VARIANTS[name]
|
| 85 |
print(f"Building variant: {name} ...")
|
| 86 |
|
|
@@ -90,12 +94,12 @@ def _build_variant(name):
|
|
| 90 |
omni_last_layer_idx = model_config.get("omni_last_layer_idx", -1)
|
| 91 |
|
| 92 |
extractor = init_text_hidden_extractor(
|
| 93 |
-
"omni", OMNI_MODEL_ID, None, dit_depth,
|
| 94 |
)
|
| 95 |
sync_omni_dim_with_text_encoder(model_config, extractor)
|
| 96 |
|
| 97 |
ckpt_path = hf_hub_download(repo_id=UNISON_REPO, filename=spec["ckpt_file"])
|
| 98 |
-
model = _load_model(types.SimpleNamespace(model_ckpt=ckpt_path),
|
| 99 |
|
| 100 |
vae_ckpt_path = hf_hub_download(repo_id=MMAUDIO_REPO, filename=spec["vae_ckpt_file"])
|
| 101 |
vocoder_ckpt_path = (
|
|
@@ -106,24 +110,37 @@ def _build_variant(name):
|
|
| 106 |
tod_vae_ckpt=vae_ckpt_path,
|
| 107 |
bigvgan_vocoder_ckpt=vocoder_ckpt_path,
|
| 108 |
mode=spec["vae_mode"],
|
| 109 |
-
)
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
# probe latent length for a MAX_AUDIO_DURATION-long clip (needed for generation mode).
|
| 113 |
-
sample_rate = spec["sample_rate"]
|
| 114 |
dummy_len = int(MAX_AUDIO_DURATION * sample_rate)
|
| 115 |
with torch.no_grad():
|
| 116 |
dummy_lat = audio_vae.wrapped_encode(torch.zeros(1, dummy_len, device=DEVICE))
|
| 117 |
gen_target_frames = int(dummy_lat.shape[-1])
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
return (model, extractor, audio_vae, 0.5, sample_rate, gen_target_frames, scheduler)
|
| 122 |
|
| 123 |
|
| 124 |
def load_default_variant():
|
| 125 |
-
"""Background-thread target: build DEFAULT_VARIANT at startup and
|
| 126 |
-
as the active variant, so the Space doesn't block its HTTP server
|
|
|
|
| 127 |
global _active_name, _active_entry, _loading, _load_error
|
| 128 |
try:
|
| 129 |
entry = _build_variant(DEFAULT_VARIANT)
|
|
@@ -141,21 +158,24 @@ threading.Thread(target=load_default_variant, daemon=True).start()
|
|
| 141 |
|
| 142 |
|
| 143 |
def get_variant(name: str):
|
| 144 |
-
"""Return the active variant's (model, extractor, vae, ...) tuple,
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
Caller must hold _gpu_lock
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
|
| 161 |
model_card = ModelCard(
|
|
|
|
| 68 |
# which LLM layers it extracts at construction time). Switching variants evicts the
|
| 69 |
# previous one and rebuilds from the local HF cache (no re-download).
|
| 70 |
_active_name = None
|
| 71 |
+
_active_entry = None # built on CPU at first; moved to GPU on first real use
|
| 72 |
_loading = True
|
| 73 |
_load_error = None
|
| 74 |
|
|
|
|
| 76 |
# so two requests running together would corrupt each other's results. This lock
|
| 77 |
# also covers _active_name/_active_entry below, so no separate lock is needed.
|
| 78 |
_gpu_lock = threading.Lock()
|
| 79 |
+
_active_ready = False # has _active_entry been moved onto the GPU yet?
|
| 80 |
|
| 81 |
|
| 82 |
def _build_variant(name):
|
| 83 |
+
"""Download and construct one variant's full stack on CPU: text encoder, DiT
|
| 84 |
+
backbone, and VAE. Safe to call from the background thread — nothing here
|
| 85 |
+
touches CUDA, since ZeroGPU only intercepts CUDA calls made from inside an
|
| 86 |
+
@spaces.GPU-decorated call. Moving onto the GPU happens later, in
|
| 87 |
+
_activate_variant()."""
|
| 88 |
spec = VARIANTS[name]
|
| 89 |
print(f"Building variant: {name} ...")
|
| 90 |
|
|
|
|
| 94 |
omni_last_layer_idx = model_config.get("omni_last_layer_idx", -1)
|
| 95 |
|
| 96 |
extractor = init_text_hidden_extractor(
|
| 97 |
+
"omni", OMNI_MODEL_ID, None, dit_depth, "cpu", omni_last_layer_idx=omni_last_layer_idx,
|
| 98 |
)
|
| 99 |
sync_omni_dim_with_text_encoder(model_config, extractor)
|
| 100 |
|
| 101 |
ckpt_path = hf_hub_download(repo_id=UNISON_REPO, filename=spec["ckpt_file"])
|
| 102 |
+
model = _load_model(types.SimpleNamespace(model_ckpt=ckpt_path), "cpu", model_config)
|
| 103 |
|
| 104 |
vae_ckpt_path = hf_hub_download(repo_id=MMAUDIO_REPO, filename=spec["vae_ckpt_file"])
|
| 105 |
vocoder_ckpt_path = (
|
|
|
|
| 110 |
tod_vae_ckpt=vae_ckpt_path,
|
| 111 |
bigvgan_vocoder_ckpt=vocoder_ckpt_path,
|
| 112 |
mode=spec["vae_mode"],
|
| 113 |
+
).eval()
|
| 114 |
+
|
| 115 |
+
scheduler = FlowMatchEulerDiscreteScheduler()
|
| 116 |
+
print(f"Variant built (CPU): {name}")
|
| 117 |
+
# gen_target_frames is None until _activate_variant() probes it on the GPU.
|
| 118 |
+
return (model, extractor, audio_vae, 0.5, spec["sample_rate"], None, scheduler)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _activate_variant(entry):
|
| 122 |
+
"""Move a CPU-built variant onto the GPU and probe its latent frame count.
|
| 123 |
+
Must only be called from inside process_fn (i.e. inside @spaces.GPU) — this
|
| 124 |
+
is where it's actually safe to touch CUDA."""
|
| 125 |
+
model, extractor, audio_vae, vae_scale_factor, sample_rate, _, scheduler = entry
|
| 126 |
+
extractor.text_backbone = extractor.text_backbone.to(DEVICE)
|
| 127 |
+
model = model.to(device=DEVICE, dtype=torch.bfloat16).eval()
|
| 128 |
+
audio_vae = audio_vae.to(DEVICE).eval()
|
| 129 |
|
| 130 |
# probe latent length for a MAX_AUDIO_DURATION-long clip (needed for generation mode).
|
|
|
|
| 131 |
dummy_len = int(MAX_AUDIO_DURATION * sample_rate)
|
| 132 |
with torch.no_grad():
|
| 133 |
dummy_lat = audio_vae.wrapped_encode(torch.zeros(1, dummy_len, device=DEVICE))
|
| 134 |
gen_target_frames = int(dummy_lat.shape[-1])
|
| 135 |
|
| 136 |
+
print("Variant activated (GPU)")
|
| 137 |
+
return (model, extractor, audio_vae, vae_scale_factor, sample_rate, gen_target_frames, scheduler)
|
|
|
|
| 138 |
|
| 139 |
|
| 140 |
def load_default_variant():
|
| 141 |
+
"""Background-thread target: build DEFAULT_VARIANT (CPU only) at startup and
|
| 142 |
+
publish it as the active variant, so the Space doesn't block its HTTP server
|
| 143 |
+
on the download. Actually moving it to the GPU happens on the first request."""
|
| 144 |
global _active_name, _active_entry, _loading, _load_error
|
| 145 |
try:
|
| 146 |
entry = _build_variant(DEFAULT_VARIANT)
|
|
|
|
| 158 |
|
| 159 |
|
| 160 |
def get_variant(name: str):
|
| 161 |
+
"""Return the active variant's (model, extractor, vae, ...) tuple, ready to
|
| 162 |
+
use on the GPU — building and/or activating it first if needed.
|
| 163 |
+
|
| 164 |
+
Caller must hold _gpu_lock and be inside an @spaces.GPU call — that's both
|
| 165 |
+
what makes the unlocked _active_* globals safe, and what makes it safe to
|
| 166 |
+
touch CUDA in _activate_variant()."""
|
| 167 |
+
global _active_name, _active_entry, _active_ready
|
| 168 |
+
if name != _active_name:
|
| 169 |
+
print(f"Switching variant: {_active_name!r} -> {name!r}")
|
| 170 |
+
_active_name, _active_entry, _active_ready = name, _build_variant(name), False
|
| 171 |
+
gc.collect()
|
| 172 |
+
torch.cuda.empty_cache()
|
| 173 |
+
|
| 174 |
+
if not _active_ready:
|
| 175 |
+
_active_entry = _activate_variant(_active_entry)
|
| 176 |
+
_active_ready = True
|
| 177 |
+
|
| 178 |
+
return _active_entry
|
| 179 |
|
| 180 |
|
| 181 |
model_card = ModelCard(
|
unison/models/text_encoders/omni_encoder.py
CHANGED
|
@@ -108,9 +108,13 @@ class QwenOmniThinkerExtractor(nn.Module):
|
|
| 108 |
|
| 109 |
gc.collect()
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
# Load processor; force right-side padding so the system prompt is always
|
| 116 |
# at index 0, making the slice offset constant.
|
|
|
|
| 108 |
|
| 109 |
gc.collect()
|
| 110 |
|
| 111 |
+
# Skip when device is cpu — ZeroGPU only intercepts CUDA calls made inside
|
| 112 |
+
# an @spaces.GPU-decorated call, so touching CUDA here would crash if this
|
| 113 |
+
# constructor is ever run from a background thread (see app.py).
|
| 114 |
+
if str(device) != "cpu":
|
| 115 |
+
print(f"Moving text backbone to {device}...")
|
| 116 |
+
self.text_backbone = self.text_backbone.to(device)
|
| 117 |
+
torch.cuda.empty_cache()
|
| 118 |
|
| 119 |
# Load processor; force right-side padding so the system prompt is always
|
| 120 |
# at index 0, making the slice offset constant.
|