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 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 backbone,
83
- and VAE. Mirrors the model-loading steps in unison/pipelines/infer.py's main()."""
 
 
 
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, DEVICE, omni_last_layer_idx=omni_last_layer_idx,
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), DEVICE, model_config)
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
- audio_vae.to(DEVICE).eval()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- scheduler = FlowMatchEulerDiscreteScheduler()
120
- print(f"Variant ready: {name}")
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 publish it
126
- as the active variant, so the Space doesn't block its HTTP server on the load."""
 
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, building and
145
- switching to `name` first if a different variant is currently active.
146
-
147
- Caller must hold _gpu_lock that's what makes the unlocked reads/writes of
148
- _active_name/_active_entry here safe."""
149
- global _active_name, _active_entry
150
- if name == _active_name:
151
- return _active_entry
152
-
153
- print(f"Switching variant: {_active_name!r} -> {name!r}")
154
- entry = _build_variant(name)
155
- _active_name, _active_entry = name, entry
156
- gc.collect()
157
- torch.cuda.empty_cache()
158
- return entry
 
 
 
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
- print(f"Moving text backbone to {device}...")
112
- self.text_backbone = self.text_backbone.to(device)
113
- torch.cuda.empty_cache()
 
 
 
 
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.