eloigil6 Claude Opus 4.8 commited on
Commit
3ae6607
Β·
1 Parent(s): d0f696e

Add ZeroGPU support for GPU-accelerated MusicGen on HF Spaces

Browse files

Detect the ZeroGPU runtime via SPACES_ZERO_GPU and shim the spaces package to a no-op locally, so the same code runs on mps/cpu unchanged. On ZeroGPU the model loads on cuda at module level (the documented pattern) and generation runs inside an @spaces.GPU call with a dynamic duration budget; generate_song routes to the GPU path only on the Space, leaving local mps/cpu/stub paths and progress untouched. Bump the Space to python 3.12.12 (ZeroGPU requirement) and add spaces to requirements.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (3) hide show
  1. README.md +1 -1
  2. app.py +67 -6
  3. requirements.txt +4 -1
README.md CHANGED
@@ -5,7 +5,7 @@ colorFrom: yellow
5
  colorTo: green
6
  sdk: gradio
7
  sdk_version: 6.17.3
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
  license: mit
 
5
  colorTo: green
6
  sdk: gradio
7
  sdk_version: 6.17.3
8
+ python_version: '3.12.12'
9
  app_file: app.py
10
  pinned: false
11
  license: mit
app.py CHANGED
@@ -11,7 +11,7 @@ MusicGen ignores texture words in prompts, hence the separate bed.
11
  Env knobs:
12
  LOFINITY_ENGINE musicgen (default) | stub
13
  LOFINITY_DURATION clip length in seconds (default 30, the single-shot max)
14
- LOFINITY_DEVICE mps | cpu (default: mps if available)
15
  OLLAMA_URL default http://localhost:11434
16
  OLLAMA_MODEL default llama3.2:3b
17
  """
@@ -32,6 +32,24 @@ from gradio.server import Server
32
  ROOT = Path(__file__).parent
33
  FRONTEND = ROOT / "frontend"
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  ENGINE = os.getenv("LOFINITY_ENGINE", "musicgen")
36
  # 30s is musicgen-small's single-shot max (1500 tokens). Longer tapes are
37
  # stitched from 30s chunks: each one re-seeds the model with the last OVERLAP_S
@@ -139,7 +157,14 @@ def load_musicgen():
139
  from transformers import AutoProcessor, MusicgenForConditionalGeneration
140
 
141
  requested = os.getenv("LOFINITY_DEVICE")
142
- device = requested or ("mps" if torch.backends.mps.is_available() else "cpu")
 
 
 
 
 
 
 
143
  print(f"[lofinity] loading musicgen-small on {device}…")
144
  processor = AutoProcessor.from_pretrained("facebook/musicgen-small")
145
  model = MusicgenForConditionalGeneration.from_pretrained(
@@ -152,6 +177,14 @@ def load_musicgen():
152
  return _musicgen
153
 
154
 
 
 
 
 
 
 
 
 
155
  def encode_wav(samples, rate: int) -> str:
156
  """Encode mono float samples as a base64 WAV data URI, entirely in memory.
157
 
@@ -259,6 +292,24 @@ def musicgen_engine(music_prompt: str, seconds: int = CHUNK_S, progress_cb=None)
259
  return samples, rate
260
 
261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  def stub_engine(_music_prompt: str, seconds: int = CHUNK_S, progress_cb=None) -> tuple:
263
  """A short audible tone β€” handy when developing without the heavy model.
264
  Honors `seconds` and fakes per-chunk timing so the length slider and the
@@ -295,10 +346,20 @@ def generate_song(prompt: str, seconds: int = DEFAULT_SECONDS) -> dict:
295
  _PROGRESS.update(done=0, total=chunks)
296
  music_prompt, title, bed = enrich_prompt(prompt)
297
  print(f"[lofinity] brewing {title!r} ({seconds}s) :: {music_prompt} [+ {bed}]")
298
- engine = stub_engine if ENGINE == "stub" else musicgen_engine
299
- samples, rate = engine(
300
- music_prompt, seconds, progress_cb=lambda d, t: _PROGRESS.update(done=d, total=t)
301
- )
 
 
 
 
 
 
 
 
 
 
302
  _PROGRESS.update(done=chunks, total=chunks)
303
  try:
304
  samples = ambience.mix(samples, rate, bed)
 
11
  Env knobs:
12
  LOFINITY_ENGINE musicgen (default) | stub
13
  LOFINITY_DURATION clip length in seconds (default 30, the single-shot max)
14
+ LOFINITY_DEVICE cuda | mps | cpu (default: cuda on ZeroGPU, else mps if available)
15
  OLLAMA_URL default http://localhost:11434
16
  OLLAMA_MODEL default llama3.2:3b
17
  """
 
32
  ROOT = Path(__file__).parent
33
  FRONTEND = ROOT / "frontend"
34
 
35
+ # ZeroGPU: on a Hugging Face ZeroGPU Space a GPU is attached only for the
36
+ # duration of a function wrapped in @spaces.GPU, then released. The `spaces`
37
+ # package exists only in that runtime (which sets SPACES_ZERO_GPU=true); locally
38
+ # we shim @spaces.GPU to a no-op so the exact same code runs on mps/cpu untouched.
39
+ IS_ZEROGPU = os.environ.get("SPACES_ZERO_GPU") == "true"
40
+ try:
41
+ import spaces # provided by the ZeroGPU Space runtime
42
+ except ImportError: # local dev / non-ZeroGPU β€” make the decorator harmless
43
+ class _SpacesShim:
44
+ @staticmethod
45
+ def GPU(*args, **kwargs):
46
+ # handle both bare @spaces.GPU and @spaces.GPU(duration=...)
47
+ if args and callable(args[0]):
48
+ return args[0]
49
+ return lambda fn: fn
50
+
51
+ spaces = _SpacesShim()
52
+
53
  ENGINE = os.getenv("LOFINITY_ENGINE", "musicgen")
54
  # 30s is musicgen-small's single-shot max (1500 tokens). Longer tapes are
55
  # stitched from 30s chunks: each one re-seeds the model with the last OVERLAP_S
 
157
  from transformers import AutoProcessor, MusicgenForConditionalGeneration
158
 
159
  requested = os.getenv("LOFINITY_DEVICE")
160
+ if requested:
161
+ device = requested
162
+ elif IS_ZEROGPU:
163
+ device = "cuda"
164
+ elif torch.backends.mps.is_available():
165
+ device = "mps"
166
+ else:
167
+ device = "cpu"
168
  print(f"[lofinity] loading musicgen-small on {device}…")
169
  processor = AutoProcessor.from_pretrained("facebook/musicgen-small")
170
  model = MusicgenForConditionalGeneration.from_pretrained(
 
177
  return _musicgen
178
 
179
 
180
+ # ZeroGPU wants models resident on cuda at startup (module import time), not
181
+ # lazily inside the @spaces.GPU call β€” outside the decorated function a CUDA
182
+ # emulation layer lets this .to('cuda') succeed without a real GPU attached, and
183
+ # placements done at startup are far more efficient than per-call transfers.
184
+ if IS_ZEROGPU and ENGINE != "stub":
185
+ load_musicgen()
186
+
187
+
188
  def encode_wav(samples, rate: int) -> str:
189
  """Encode mono float samples as a base64 WAV data URI, entirely in memory.
190
 
 
292
  return samples, rate
293
 
294
 
295
+ def _gpu_budget(music_prompt: str, seconds: int = CHUNK_S) -> int:
296
+ """GPU seconds to request from ZeroGPU for a brew of this length: per-chunk
297
+ render time plus headroom. Tighter budgets earn better queue priority, and
298
+ the signature must mirror gpu_musicgen so ZeroGPU can pass it the same args."""
299
+ chunks = max(1, round(int(seconds) / CHUNK_S))
300
+ return 20 + 25 * chunks # 30s->45, 60s->70, 90s->95
301
+
302
+
303
+ @spaces.GPU(duration=_gpu_budget)
304
+ def gpu_musicgen(music_prompt: str, seconds: int = CHUNK_S) -> tuple:
305
+ """ZeroGPU entry point β€” runs MusicGen on the real GPU and returns the audio
306
+ to the web process. No progress_cb on purpose: this body executes in a
307
+ separate GPU worker, so _PROGRESS updates can't reach /api/progress yet (the
308
+ brewing garden is wired up in a later stage); on the Space the brew jumps to
309
+ done. Locally @spaces.GPU is a no-op, but this path is only taken on Spaces."""
310
+ return musicgen_engine(music_prompt, seconds)
311
+
312
+
313
  def stub_engine(_music_prompt: str, seconds: int = CHUNK_S, progress_cb=None) -> tuple:
314
  """A short audible tone β€” handy when developing without the heavy model.
315
  Honors `seconds` and fakes per-chunk timing so the length slider and the
 
346
  _PROGRESS.update(done=0, total=chunks)
347
  music_prompt, title, bed = enrich_prompt(prompt)
348
  print(f"[lofinity] brewing {title!r} ({seconds}s) :: {music_prompt} [+ {bed}]")
349
+ if ENGINE == "stub":
350
+ samples, rate = stub_engine(
351
+ music_prompt, seconds,
352
+ progress_cb=lambda d, t: _PROGRESS.update(done=d, total=t),
353
+ )
354
+ elif IS_ZEROGPU:
355
+ # the GPU body runs in a separate worker process, so progress can't
356
+ # stream back here yet (Stage 3); the brewing bar jumps 0->100% on Space
357
+ samples, rate = gpu_musicgen(music_prompt, seconds)
358
+ else:
359
+ samples, rate = musicgen_engine(
360
+ music_prompt, seconds,
361
+ progress_cb=lambda d, t: _PROGRESS.update(done=d, total=t),
362
+ )
363
  _PROGRESS.update(done=chunks, total=chunks)
364
  try:
365
  samples = ambience.mix(samples, rate, bed)
requirements.txt CHANGED
@@ -1,5 +1,8 @@
1
  # gradio itself is provided by the Space SDK (sdk_version in README.md).
2
- torch
 
 
 
3
  transformers
4
  # Ollama must be running locally with the model below pulled:
5
  # ollama pull llama3.2:3b
 
1
  # gradio itself is provided by the Space SDK (sdk_version in README.md).
2
+ # `spaces` provides the @spaces.GPU decorator used on ZeroGPU. app.py shims it to
3
+ # a no-op when it's absent, so it's only actually required on the Space.
4
+ spaces
5
+ torch # ZeroGPU requires torch >=2.8; the Space runtime supplies the CUDA build
6
  transformers
7
  # Ollama must be running locally with the model below pulled:
8
  # ollama pull llama3.2:3b