multi-agent-lab / docs /adr /0032-llama-cpp-local-backend.md
agharsallah
feat: Replace llama.cpp backend with in-process transformers backend for local GPU inference
7d636f8
|
Raw
History Blame Contribute Delete
4.72 kB
# ADR-0032: A Third Inference Backend — Local llama.cpp (GGUF)
## Status
Superseded by ADR-0033. llama.cpp's persistent `llama-server` cannot hold a GPU under
ZeroGPU's per-call grant model; replaced by an in-process `transformers` backend that
works on any HF Space hardware (ADR-0024 *second inference backend / unified registry*,
ADR-0015 *LiteLLM gateway*, ADR-0022 *per-agent explicit model binding* remain in
force; this ADR is retained as a historical record only).
## Context
The engine had two live backends: vLLM endpoints we deploy on Modal (ADR-0015/0019)
and Hugging Face's serverless router (ADR-0024). Both run *somewhere else* — Modal needs
warmed GPUs, HF needs a token and a provider that serves the model. There was no way to
run a cast **entirely on the operator's own machine**, with no account, no token, and no
network after the first download.
llama.cpp's `llama-server` is exactly that: it loads a quantized **GGUF** model, runs it
on whatever hardware is present (Apple Metal, NVIDIA CUDA, or CPU), and exposes an
**OpenAI-compatible** API on `/v1`. Because it speaks the same REST surface as Modal/HF,
it slots into the existing LiteLLM gateway with no new transport code — the same seam
ADR-0024 was designed to leave open ("adding a third backend later touches only
`inference._BACKENDS`").
This also stacks hackathon lanes from one local server: the **Llama Champion** badge
(a real llama.cpp runtime in the cast), the **NVIDIA Nemotron Quest** (Nemotron 3 Nano
4B), and the **OpenBMB** track (MiniCPM 4.1 8B) — plus a JetBrains Mellum 2 thinking
model on the balanced tier. Every model stays within the ≤32B "small minds" rule, and the
4B Nemotron honours the ≤4B Tiny-Titan band.
## Decision
**A third backend = one more catalogue + a registry entry.** Add
`src/models/llamacpp_catalogue.py`, stdlib-only and offline-safe like its siblings, listing
the GGUF models with both engine-facing fields (`key` / `profile` / `params_b` /
`served_id`) and serving fields (`hf_repo` / `quant` / `ctx_size` / sampling /
`flash_attn` / `reasoning`). Its `binding_for()` yields the LiteLLM custom-endpoint form
`model = openai/<served_id>`, `base_url = $LLAMACPP_BASE_URL` (default
`http://127.0.0.1:8080/v1`), `api_key = $LLAMACPP_API_KEY` (a placeholder — llama-server
ignores it). Register it in `inference._BACKENDS` under the prefix `llamacpp`; qualified
keys are `llamacpp:<slug>` (e.g. `llamacpp:nemotron-3-nano-4b`). Nothing above the registry
changes — the router, the config loader's `endpoint:` expansion, the live/offline gate, and
the Lab picker all derive from the façade.
**The serving side is a separate, pure-where-it-matters launcher.** Add
`src/models/llamacpp_server.py`. `detect_accelerator(platform, probe)` returns
`metal` on macOS, `cuda` when `nvidia-smi` reports a GPU, else `cpu`;
`build_command(model, accelerator, …)` assembles the `llama-server` argv — pulling the
model by its `-hf` spec (downloaded on first run), serving it under `--alias <key>` so the
running server reports the stable id the engine binds to, and offloading **every layer to
the GPU** (`-ngl 999`) when one is present, omitting the flag on CPU. Both take their
environment as arguments so the GPU/CPU branches are testable with no GPU and no binary.
The `__main__` CLI launches a model by key and prints the matching `LLAMACPP_BASE_URL`
export.
**Opt-in by base URL, not a token.** A local server needs no auth, so the live/offline
gate (`has_credentials`) keys on `LLAMACPP_BASE_URL` being *set* — the launcher sets it,
or you export it to point at an already-running or remote server. With it unset the
backend never claims to be live, so the deterministic stub still owns the no-config demo.
## Consequences
- A cast can run fully local: `uv run python -m src.models.llamacpp_server
nemotron-3-nano-4b`, export the printed URL, and the engine routes to it through the
unchanged LiteLLM transport — no account, no token, GPU used automatically when present.
- Llama Champion + Nemotron + OpenBMB lanes are reachable from one server; the catalogue
is plain data, so adding a GGUF or retuning a tier is a one-line `LlamaCppModel(...)` edit.
- Backward compatible by construction: bare keys still mean Modal, and the `llamacpp`
prefix is new — existing config, manifests, and the green test baseline are unaffected.
- The engine still never names a vendor on a hot path: routing is by qualified key through
one façade; the GGUF/quant churn is hidden behind `--alias <key>`.
- llama-server and the GGUF download are operator-side, never a Python dependency — the
offline stub remains the default with no binary, no network, and extras uninstalled.