"""In-process model layer for the HTML Toy Maker — runs Nemotron on HF ZeroGPU. The Gradio app calls `generate(messages)`; that function is wrapped in `@spaces.GPU`, so the GPU is only attached while it runs (ZeroGPU). The big GGUF DOWNLOAD does not need the GPU, so we kick it off in a background thread at import time (overlapping Space boot). The model is then constructed INSIDE the GPU function the first time and cached in a module global — so the first request only pays for model load + generation, not the download. Constraints: NVIDIA Nemotron (RTX 5080 prize) on the llama.cpp runtime (Llama Champion), Q8_0 quant, n_ctx = 65536. Set a HF_TOKEN Space secret for faster, rate-limited-free pulls. ⚠️ If `Llama(...)` raises an "unknown architecture" / GGML assert on load, the prebuilt llama-cpp-python wheel is older than Nemotron-3-Nano's hybrid Mamba-2 support — bump the wheel in requirements.txt or build from source. """ import os # Must be set BEFORE huggingface_hub is imported, so the fast Rust downloader is used. os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1") import glob import threading import spaces from llama_cpp import Llama # Nemotron-3-Nano-30B-A3B, Q8_0 (~33.6 GB). Q8_0 may be sharded into *-00001-of-0000N.gguf; # we download all matching shards and point llama.cpp at the first (it auto-loads the rest). MODEL_REPO = "unsloth/Nemotron-3-Nano-30B-A3B-GGUF" QUANT_GLOB = "*Q8_0*.gguf" # Full trained context (n_ctx_train = 1,048,576). Nemotron-3-Nano is hybrid Mamba-2: only its # few attention layers grow KV with context (Mamba layers are constant-size), so long context # is cheap here. Q8_0 (~34 GB) leaves ~36 GB free on ZeroGPU's ~70 GB H200 for the KV cache. # (llama.cpp allocates KV up front from this — if it ever OOMs at load, dial this back.) N_CTX = 1048576 # Unsloth's recommended Nemotron sampling. DEFAULTS = dict(temperature=0.6, top_p=0.95, min_p=0.01) _llm: Llama | None = None _model_path: str | None = None _download_error: Exception | None = None def _download() -> None: """Fetch the GGUF shards (CPU only). Runs in a background thread started at import.""" global _model_path, _download_error try: from huggingface_hub import snapshot_download local_dir = snapshot_download(MODEL_REPO, allow_patterns=[QUANT_GLOB]) shards = sorted(glob.glob(os.path.join(local_dir, "**", QUANT_GLOB), recursive=True)) if not shards: raise RuntimeError(f"No {QUANT_GLOB} files found in {MODEL_REPO}") _model_path = shards[0] # first shard; llama.cpp loads the rest of a split GGUF itself except Exception as exc: # surfaced when _load() is first called _download_error = exc # Start downloading immediately at import (overlaps Space startup; does NOT block it, so the # port binds fast and ZeroGPU registers the GPU function right away). _download_thread = threading.Thread(target=_download, name="gguf-download", daemon=True) _download_thread.start() def _load() -> Llama: """Construct the llama.cpp model (GPU attached), cached for reuse across requests.""" global _llm if _llm is not None: return _llm _download_thread.join() # wait for the background download (instant if already done) if _model_path is None: raise RuntimeError(f"GGUF download failed: {_download_error}") _llm = Llama( model_path=_model_path, n_gpu_layers=-1, # offload everything to the GPU n_ctx=N_CTX, flash_attn=True, verbose=False, ) return _llm @spaces.GPU(duration=300) def generate(messages: list[dict], max_tokens: int = 8192, **sampling) -> dict: """Run one chat completion. Returns {"content", "reasoning"}. Nemotron emits its chain-of-thought via / SPECIAL tokens (ids 12/13), which are dropped on detokenize — so reasoning usually arrives mixed into `content`. Newer llama.cpp may surface it separately as `reasoning_content`; we pass both back and let app.py split them for display. """ llm = _load() params = {**DEFAULTS, **sampling} out = llm.create_chat_completion(messages=messages, max_tokens=max_tokens, **params) msg = out["choices"][0]["message"] return { "content": (msg.get("content") or "").strip(), "reasoning": (msg.get("reasoning_content") or "").strip(), }