# Field Notes — Building Tabras What I learned building a small-model card duel for the Build Small hackathon. ## 1. ZeroGPU is a GPU-sharing mechanism, not a hosted GPU provider This was my biggest wall, and I hit it late. I built Tabras assuming a Hugging Face ZeroGPU Space would behave like a hosted GPU I could just run my models on. It does not. ZeroGPU *time-slices* a shared GPU and only grants it inside `@spaces.GPU` functions — and it ships args/returns between processes by pickling them. That breaks the moment you try to hand it a `trust_remote_code` model (MiniCPM) or a diffusers pipeline: they aren't picklable, and low-level CUDA init isn't allowed in the main process at all. I burned real time learning this through a wall of `PicklingError` and "CUDA init reached" tracebacks. The last-minute fix was to **re-architect**: make the Space a thin HTTP client and move every model onto **Modal** GPU endpoints (MiniCPM for cards, Nemotron for the boss, SDXL-Turbo for art), each autoscaled on its own dedicated GPU. The Space just POSTs prompts and renders the results. Lesson: understand the *execution model* of your compute before you design around it, not after. Crucially, Tabras still runs **fully local / off-grid** — a `MODE` switch flips between LOCAL (in-process Transformers/Diffusers, or a local `llama.cpp` server for MiniCPM) and MODAL (the hosted endpoints). The README has the local instructions. Small and local was always the point, and keeping that path alive mattered to me. ## 2. Small models: surprisingly powerful, with sharp edges - **Image models punch way above their size.** SDXL-Turbo, in ~4 denoising steps, produced genuinely striking card art and theme backgrounds. The visual identity of the whole game is carried by a tiny, fast diffusion model. - **Nemotron was the standout for agentic / tool-calling work.** Giving it the public board state and a constrained JSON action schema, it reliably reads the situation and returns valid, sensible plays. A 4B model running a competent opponent was the part I expected to be flaky and wasn't. - **The text model owns meaning, not structure.** MiniCPM writes evocative names and flavor, but it leans on lazy patterns ("Fire Card"), leaks prompt vocabulary, and truncates its own JSON. The single highest-leverage fix was reordering the requested JSON so `effects` and `name` come *first* — they survive a token cutoff that used to silently collapse a whole card to a fallback. Design around what the model is bad at. ## 3. Perceived latency beats raw latency Small/local inference isn't instant, and the variance (some packs fast, some slow) looked worse than a consistent wait. The fixes that made the demo feel *good* were mostly UX, not compute: - A **minimum loading window** on each draft transition, so every pick shows the same deliberate "forging" beat — that uniform pause hides the slow ones behind the same animation the fast ones use. - **Prefetching every branch** of the draft during idle time (the reveal/rules screens, and while you read the current pack), so whichever card you pick, its next pack is already generating. - **Pre-baking static art** for the fixed backbone cards once and bundling it, so they never spend a live generation or shimmer. A consistent 2s always beats an unpredictable 0–8s. ## 4. Card-game design is hard — and a ton of fun Balancing a generative card game is genuinely difficult. The core principle that made it tractable: **the LLM owns meaning, the engine owns math.** The model picks *which* effects a card has and writes its identity; deterministic Python prices every number against a point budget, so cards are balanced-by-construction no matter what the model invents. The draft is deck-aware — it reads the build you're assembling and shapes packs toward it, sometimes dangling a tempting off-archetype card. Getting that loop to feel fair *and* surprising was the most fun part of the whole project. ## 5. Ambition under a deadline This was an ambitious build for the time window — three small models, a custom UI, a last-minute compute re-architecture, and a real game loop. The thing that saved me was treating the **demo video as the deliverable** and optimizing the local recording surface hard, rather than betting everything on a flawless live Space. I'm proud of how much of it came together, and I had a lot of fun doing it.