townlet / docs /lessons-zerogpu.md
Budlee's picture
Dean-aligned Llama(): 5 kwargs only; hf_hub_download at module scope; poll 25s
233354f verified
|
Raw
History Blame Contribute Delete
5.2 kB
# Lessons learned: llama.cpp + ZeroGPU + a continuous simulation loop
Raw notes for the blog. The things I wish I had known on day one. They came out of a long debug session against the "RuntimeError: No CUDA GPUs are available" error.
## 1. ZeroGPU is designed for synchronous, user-clicks-a-button workloads
`@spaces.GPU` forks a worker process per call. The scheduler hands you a GPU slot if one is available; if the pool is busy, your fork's `worker_init` fails with `No CUDA GPUs are available` *inside `spaces/zero/wrappers.py:worker_init`*, before any of your code runs. This is the allocator backing off β€” not a bug, not anything you can catch from app code.
That's fine if your app makes ten GPU calls per recording session (chat demos, generate-an-image apps). It is not fine if your app makes hundreds (a simulation that polls every five seconds). The scheduler punishes high-frequency callers to keep the shared pool fair, and Townlet was the loudest neighbour on the block.
## 2. The `spaces` package is built around torch, not llama.cpp
`import spaces` monkey-patches torch's CUDA APIs so `model.to("cuda")` at module scope succeeds without a real GPU attached. On the first `@spaces.GPU` call the worker streams weights from disk β†’ pinned memory β†’ VRAM through a double-buffered pipeline β€” that's the cold-start optimisation the docs talk about.
**llama-cpp-python is invisible to that pipeline.** It's a separate C++ runtime; the monkey-patching does nothing for it. Every fork loads the GGUF from scratch every time. None of the warm-worker reuse benefits apply.
Surveying 50+ ZeroGPU Spaces in the same hackathon: nearly every team using LLMs ships transformers + torch on the GPU and keeps llama-cpp as a CPU-only side path. Two Spaces in the org actually ran llama-cpp on ZeroGPU (Dean's Rizz Therapy and 1000-Rooms) and both are click-triggered single-pipeline apps. None of them runs llama-cpp inside a continuous poll loop. Townlet was the only one.
## 3. Keep the `Llama()` constructor minimal
Dean's actual production code (he's the Discord OP whose recipe the whole hackathon copied) calls `Llama()` with **five kwargs**: `model_path`, `n_gpu_layers=-1`, `n_ctx`, `flash_attn=True`, `verbose=False`. That's it. No `type_k`/`type_v` for KV-cache quantisation, no `use_mlock`, no `n_batch` override, no `use_mmap=True`.
I had piled on those kwargs chasing performance. They added latency on every fork because some are version-gated and my `try/except` had to retry on rejection. Stripping back to the minimal five matched Dean's pattern and removed a whole class of failure modes.
## 4. `hf_hub_download` belongs at module scope, not inside `@spaces.GPU`
`Llama.from_pretrained(repo_id=..., filename=...)` is convenient, but it runs the cache-validation roundtrip *inside the fork*, where every second of wall time is precious. Dean's pattern, the 1000-Rooms pattern, and the long-running `gokaygokay/Gemma-2-llamacpp` reference all do `model_path = hf_hub_download(...)` in the parent process at import time, then `Llama(model_path=model_path, ...)` inside the decorated function. Same outcome, no network roundtrip per fork.
## 5. Remaining quota controls queue priority, so credits compound
From the docs: *"Remaining quota directly impacts priority in ZeroGPU queues."* Credits don't just extend daily runtime β€” they raise priority *while you have them*, which lowers the rate at which the allocator denies your forks. $20 of pre-paid credits buys 200 minutes of GPU time *and* moves you up the queue. It's the cheapest debugging tool a hackathon team has.
## 6. The error message doesn't tell you the cause
`No CUDA GPUs are available` is what the worker reports when `torch.Tensor([0]).cuda()` fails inside `worker_init`. The actual upstream causes can be: quota pre-check rejected the request, no slots in the shared pool, the allocator's fairness signal kicking in, or an HTTP-layer failure that the spaces decorator surfaces as a generic exception. We can't tell which from our side. The right escalation path is the spaces-team discussion board, not the app code.
## 7. The fix wasn't an architecture change. It was being less greedy.
Once diagnosed, the fix was small:
- Strip `Llama()` to Dean's five kwargs
- Move `hf_hub_download` to module scope
- Slow the JS poll interval (~25s β€” the world still feels alive because the snapshot tick at 800ms keeps characters animating between drains)
- Pre-paid quota as priority insurance
The simulation kept its continuous-loop architecture. It just stopped hammering the allocator.
## Things I'd do differently next time
- **Use transformers, not llama.cpp, on ZeroGPU.** Llama Champion badge is nice but it cost a week of debug time. Most teams shipped transformers + LoRA, which is what the spaces package is actually built for.
- **Measure quota burn before optimising decisions.** I tuned drain duration and decision count before knowing that polling cadence was the dominant variable.
- **Read the spaces source, not just the docs.** `worker_init` raising before your code runs is something the docs underplay. Five minutes in the source tells you more than the entire ZeroGPU page.