Spaces:
Running on Zero
Running on Zero
Add ZeroGPU support for GPU-accelerated MusicGen on HF Spaces
Browse filesDetect 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>
- README.md +1 -1
- app.py +67 -6
- 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.
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 299 |
-
|
| 300 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|