Measured, Makefile-driven local LLM serving. Seven managed runtimes -- vLLM (CUDA), Ollama, vLLM-Metal, Splash (Apple Silicon), LM Studio, ds4 and Kapitsa -- with one portable config per model, host-adaptive launchers, isolated remote-host operation, and the retained benchmark and capability results for every host in the fleet. The workload everything is tuned for is deep-context security-audit calls; AGENTS.md states the mission and the invariants.
Each host keeps the best provider per model: one runtime, chosen by Capability Check v2 and then the AuditBench session rate. LM Studio is kept beside it as the entry-level option, since it installs cleanly on every platform.
Serving performance is measured with AuditBench v2, and API compatibility
and audit fitness are gated with Capability Check v2. Both
live in the sibling llm-audit-bench
repository (docs/PROTOCOL.md and docs/CAPABILITY.md are the
canonical protocol documents); this repository holds the runtime
adapters, model configs, and results.
New to local serving? docs/INTRODUCTION.md explains the concepts these documents assume.
- For bench, capability and development targets, clone
llm-audit-bench beside
this repository, or point
AUDIT_BENCH_DIRat an existing checkout. The harness is standard-library Python; there is nothing to install. Serving does not need it. - Copy
.envrc.exampleto.envrc, fill in your own values (the ssh alias of your remote GPU host), anddirenv allow-- or export the same variables from your shell. Committed files carry no host-specific values. - Install the runtime you intend to serve with. Host tools are yours
to provide:
uv,python3,curlandtareverywhere, plus what a runtime names; every target checks the tools it uses and names any missing. Prerequisites and host-tuning rationale are per runtime in vllm/README.md, ollama/README.md, vllm-metal/README.md, splash/README.md, lmstudio/README.md, ds4/README.md, and kapitsa/README.md.
Development needs shellcheck, markdownlint-cli2, and Python 3.10 or
newer for make lint and make test. CI installs the linters at the
versions pinned in uv.lock and package-lock.json.
Everything runs from the repository root through make; make help
prints the full list. The lifecycle verbs are uniform across runtimes:
| Verb | Does |
|---|---|
make <runtime>-start MODEL=<name> |
Start detached, wait until serving |
make <runtime>-stop |
Stop by pidfile, verify the GPU or process group was released |
make <runtime>-status |
Process, memory split, context allocation, KV sizing |
make <runtime>-logs |
Follow the server log |
make <runtime>-bench MODEL=<name> |
Run AuditBench v2 |
make <runtime>-capability MODEL=<name> |
Run Capability Check v2 |
where <runtime> is vllm, ollama, vllm-metal, splash, lmstudio,
ds4 or kapitsa (plus ollama-install, vllm-metal-install,
vllm-metal-pull, splash-install, splash-pull, lmstudio-install,
lmstudio-pull, ds4-install, ds4-pull, kapitsa-install, and
kapitsa-pull for runtime and model installation).
MODEL names a config file in that runtime's models/ directory, never a
served model ID or registry tag; there is deliberately no default model.
make status shows whether each local runtime is running, and with which model.
Every bench and capability run retains its JSON under
~/.local/state/auditbench/ or ~/.local/state/capability-check/ on the
host that ran it. In the llm-audit-bench checkout, uv run audit-bench --list and uv run capability-check --list index them, and --show FILE
re-prints one.
The vLLM and Ollama runtimes also run on a remote GPU host over ssh:
make vllm-remote-{bootstrap,start,stop,status,logs,bench,capability}
and the matching ollama-remote-* set, with HOST=<ssh-host> supplied
by .envrc or the command line. Remote Ollama is fully isolated from
the host's own Ollama (own binary, model cache, state, and port), and
the guards are causal: a target refuses when the live server state
contradicts it, from start on a busy GPU or claimed port to bootstrap
under a running server. Servers bind loopback only; reach a remote one
through make vllm-remote-tunnel (localhost:8001) or
make ollama-remote-tunnel (localhost:11436).
Each provider, an access path to one runtime, has a ready opencode config
at opencode/<provider>/opencode.json. Start a server here, then run opencode
in your own project with that file layered over your settings; opencode
merges configs:
make splash-start MODEL=qwen3.8-27b # in this repository
export OPENCODE_CONFIG=/path/to/llm-local-servers/opencode/splash/opencode.json
opencode models splash # the model ids it offers
opencode # in your project
opencode run -m splash/mlx-community/Qwen3.8-27B-4bit "Summarise the auth flow."MODELnames a config in this repository; the opencode model id is its served name. Only the model the server was started with answers.- To give one project a provider permanently, copy the file to that
project's root as
opencode.json. - Entries with reasoning-effort variants take
opencode run --variant <effort>; no variant leaves the model's default. - The remote providers need their tunnel running (
make vllm-remote-tunnelormake ollama-remote-tunnel). - Do not point
OPENCODE_CONFIG_DIRat these directories: opencode writes its plugin files into that directory.
| Provider | Base URL | Config | Reasoning field |
|---|---|---|---|
vllm |
http://127.0.0.1:8000/v1 |
vllm/opencode.json | reasoning |
vllm-remote |
http://127.0.0.1:8001/v1, make vllm-remote-tunnel |
vllm-remote/opencode.json | reasoning |
vllm-metal |
http://127.0.0.1:8021/v1 |
vllm-metal/opencode.json | reasoning |
splash |
http://127.0.0.1:8022/v1 |
splash/opencode.json | reasoning |
lmstudio |
http://127.0.0.1:1234/v1 |
lmstudio/opencode.json | reasoning_content |
ollama |
http://127.0.0.1:11434/v1 |
ollama/opencode.json | not declared |
ollama-mlx |
http://127.0.0.1:11434/v1 |
ollama-mlx/opencode.json | not declared |
ollama-remote |
http://127.0.0.1:11436/v1, make ollama-remote-tunnel |
ollama-remote/opencode.json | not declared |
ds4 |
http://127.0.0.1:8023/v1 |
ds4/opencode.json | reasoning_content |
kapitsa |
http://127.0.0.1:8024/v1 |
kapitsa/opencode.json | reasoning_content |
make opencode-config generates them from the model configs, and
make test fails when they are stale, so never edit them by hand.
- Every config of a runtime is a model of each of its providers, except
that Ollama splits by artifact format: safetensors tags run on the MLX
runner, which the fleet serves on Apple Silicon only, so they are
ollama-mlxmodels and stay offollamaandollama-remote, which list the GGUF configs. CAPABILITIES.md lists which configs run on which host. - The id is
VLLM_SERVED_NAME, theOLLAMA_MODELtag, theSPLASH_MODEL_REPOrepository,DS4_SERVED_NAME,KAPITSA_SERVED_NAME, or for LM Studio the config name. Configs that share a served id become one model, with the smaller context. - The display name is derived from the config name.
limit.contextis the config's launch context, andlimit.outputis a quarter of it.reasoningis declared only where the model reasons with no request control, since opencode sends no thinking toggle.tool_callfollows the config's tool expectation.- Ollama entries declare no reasoning, because the capability check verifies Ollama reasoning only on its native API.
<PREFIX>_REASONING_EFFORTSin a config, such asVLLM_REASONING_EFFORTS, becomesreasoningEffortvariants on an entry that declares reasoning.
Every host is measured with AuditBench v2. Canonical medians copied from vllm/BENCHMARKS.md, ollama/BENCHMARKS.md, vllm-metal/BENCHMARKS.md, splash/BENCHMARKS.md, lmstudio/BENCHMARKS.md, ds4/BENCHMARKS.md and kapitsa/BENCHMARKS.md, which hold the evidence, ranges, caveats, replay results, and history; nothing here is derived. Capability pass/fail is in CAPABILITIES.md.
The mission workload: a growing tool-loop conversation resent every turn,
report or verdict at depth, 16 tool steps at 262,144 configured context
(fewer at shorter contexts), at the declared sampling. Ranked rows pass
Capability Check v2 (schema enforcement is advisory) and publish their
optimal operating point: the fastest measured stream count whose sessions
all completed, where one stream qualifies as much as N. @Nx names the
measured concurrency; an @Nx rate with an incomplete session is shown
for reference only. Rows are ranked by the optimal rate within a host
only, and a MoE or hybrid row and a dense row are not comparable.
Remote host: sm_80, 81,920 MiB VRAM; vLLM 0.31.0 with Transformers 5.15.1, isolated Ollama 0.40.13 (its rows). Sessions per hour by stream count: the vLLM rows ran 1 stream and the largest of 2/4/8 the pool admits, the Ollama rows the 2.5.0 stream ladder; context, slots, KV pool, completion, the pp512/tg128 reference and the retained JSON names are in vllm/BENCHMARKS.md and ollama/BENCHMARKS.md.
| Rank | Runtime and profile | Tier | Sessions/h 1x | Sessions/h @Nx | Optimal | Report decode 1x |
|---|---|---|---|---|---|---|
| 1 | vLLM qwen3.6-35b-a3b-awq |
MoE | 38.49 | 58.67 @8x | 8x | 111.4 |
| 2 | vLLM gemma4-26b-a4b-awq |
MoE | 37.20 | 45.99 @8x | 8x | 63.6 |
| 3 | Ollama ornith1.5-35b-a3b GGUF |
MoE | 34.00 | -- | 1x | 58.1 |
| 4 | vLLM qwen3.8-27b (W4A16) |
dense | 11.83 | 21.48 @4x | 4x | 49.8 |
| 5 | vLLM qwen3.8-27b-512k |
dense | 14.49 | 19.55 @4x | 4x | 36.7 |
| 6 | Ollama nemotron-3.5-bolt-74b-a9b-iq2 (74B-A9B) |
hybrid MoE | 17.62 | -- | 1x | 117.4 |
| 7 | vLLM qwen3.8-27b-dflash2 |
dense | 8.40 | 13.04 @2x | 2x | 22.8 |
| 8 | Ollama qwen3.8-flash-next-gguf-iq4xs (125B-A6B) |
MoE | 8.55 | 11.21 @2x | 2x | 27.8 |
| 9 | vLLM nanbeige4.3-33b-w4a16 |
dense | 7.19 | -- | 1x | 27.7 |
Unranked: vLLM mimo-v2.6-9b-awq (58.97 / 93.99 @8x) and Ollama
flint-40b-a2b-nvfp4 (8.51 at 1x) fail Capability Check v2 audit fitness.
DFlash 2 loses to plain qwen3.8 at the declared sampling (8.40 / 13.04 @2x); see
vllm/BENCHMARKS.md.
Local host: MacBook Pro Mac17,6, 40 GPU cores, macOS 27.0. vLLM-Metal
0.31.0, Splash 1.5.0, LM Studio 0.4.25+1, Ollama 0.40.13 (MiMo, Nanbeige and
Lightning rows on 0.40.8), ds4, Kapitsa 0.2.24. Context, slots, KV pool,
completion, the pp512/tg128 reference and the retained JSON names are in
vllm-metal/BENCHMARKS.md,
splash/BENCHMARKS.md,
lmstudio/BENCHMARKS.md,
ollama/BENCHMARKS.md,
ds4/BENCHMARKS.md and
kapitsa/BENCHMARKS.md.
| Rank | Runtime and profile | Tier | Context | Sessions/h 1x | Sessions/h @Nx | Optimal | Report decode 1x |
|---|---|---|---|---|---|---|---|
| 1 | Splash qwen3.8-flash-next-q2 (125B-A6B, GSQ-RCO Q2_0) |
MoE | 262,144 | 31.65 | 32.27 @2x | 2x | 128.7 |
| 2 | Splash qwen3.8-flash-next-iq3s (125B-A6B, GSQ-RCO IQ3_S) |
MoE | 262,144 | 27.17 | 28.73 @2x | 2x | 118.3 |
| 3 | Kapitsa clover-35b-a3b (block diffusion, 35B-A3B) |
MoE | 262,144 | 25.36 | 25.01 @4x | 1x | 86.9 |
| 4 | Splash qwen3.6-35b-a3b |
MoE | 262,144 | 20.90 | 20.18 @4x, incomplete | 1x | 211.5 |
| 5 | Ollama muse-glimmer-30b-mlx |
dense | 131,072 | 20.79 | -- | 1x | 65.3 |
| 6 | Splash flint-gguf (40B-A2.5B) |
MoE | 262,144 | 17.19 | 19.49 @2x | 2x | 152.9 |
| 7 | Ollama nemotron-3.5-super-mlx (120B-A12B) |
hybrid MoE | 524,288 | 11.88 | -- | 1x | 59.8 |
| 8 | Splash ornith1.5-122b (122B-A10B) |
MoE | 262,144 | 11.87 | 11.80 @2x | 1x | 73.5 |
| 9 | vLLM-Metal ornith1.5-35b-a3b-4bit |
MoE | 262,144 | 9.15 | 10.52 @4x, incomplete | 1x | 33.9 |
| 10 | Splash qwen3.8-27b |
dense | 262,144 | 8.57 | 8.49 @4x | 1x | 63.0 |
| 11 | Splash bonsai2-27b-pq2 (ternary) |
dense | 262,144 | 8.10 | 8.48 @2x | 2x | 58.3 |
| 12 | LM Studio qwen3.8-27b-splash |
dense | 262,144 | 8.12 | -- | 1x | 53.5 |
| 13 | Ollama nemotron-3.5-bolt-74b-a9b-q3kxl (74B-A9B) |
hybrid MoE | 262,144 | 7.73 | -- | 1x | 31.7 |
| 14 | Ollama mimo-v2.6-flash-q2ks (309B-A15B) |
MoE | 262,144 | 5.34 | -- | 1x | 22.2 |
| 15 | vLLM-Metal granite4.2-30b-4bit |
hybrid MoE | 131,072 | 2.14 | 3.18 @2x | 2x | 10.5 |
| 16 | vLLM-Metal gemma4-31b-4bit |
dense | 262,144 | 1.80 | 1.89 @2x | 2x | 2.8 |
| 17 | Ollama nanbeige4.3-33b-q4km |
dense | 262,144 | 1.44 | -- | 1x | 6.0 |
Unranked: the vLLM-Metal MiMo profile, LM Studio qwen3.6-35b-a3b-splash
and Ollama nemotron-3.5-lightning-30b-mlx fail Capability Check v2 audit
fitness; their rates (Lightning 29.41/h at 262k) cannot promote them. The
LM Studio MLX-engine profiles pass but their runs are noncanonical by
design (off-by-one token allowance). Details are in each runtime's
BENCHMARKS.md.
These profiles serve interactive agentic sessions and are not ranked for audits.
ds4 serves large MoE models, one slot. qwen3.8-flash-next-q4 passes
Capability Check v2 and runs 10.00 sessions/h; Splash serves the same model for
audits 3.6x faster, so ds4 keeps it for interactive use. Speed is from the
262,144 runs, one stream, in tok/s. At 524,288 a 50-step session to about 445k
tokens took Qwen 14 min (report 57.0 tok/s) and V4 Flash 2.2 h, its
checkpoints evicted at depth. ds4 has no response_format, which fails only
the advisory schema check. Details are in
ds4/BENCHMARKS.md.
| Model | Context | Audit fitness | Tool calls | Turn decode, mid session | Report decode, ~160k deep | Session prefill | Interactive use |
|---|---|---|---|---|---|---|---|
qwen3.8-flash-next-q4 |
524,288 (YaRN) | pass, 12/12 | pass | 69.3 | 60.9 | 1,277 | yes, the pick |
deepseek-v4-flash-q2 |
524,288 | 11/12 at 262,144, 10/12 at 524,288 | pass | 33.4 | 27.2 | 420 | yes, slower |
glm5.3-flash-q2 |
262,144 | pass, 11/12 | fail: string-typed arguments | 54.2 | 26.9 | 364 | no |
deepseek-v4.1-flash-q2 |
262,144 | pass, 11/12 | pass | 14.7 | 14.2 | 629 | yes, slow: SSD streaming |
Local host: sm_120, 12,227 MiB VRAM, 62 GiB RAM; Ollama 0.40.6 (the 0.40.13 default is not measured there). Details are in ollama/BENCHMARKS.md.
| Rank | Runtime and profile | Tier | Context | Sessions/h 1x | Sessions/h @Nx | Optimal | Report decode 1x |
|---|---|---|---|---|---|---|---|
| 1 | Ollama qwen3.6-35b-a3b-reap48 (expert-pruned Qwen3.6-35B-A3B, all on GPU) |
MoE | 262,144 | 6.74 | -- | 1x | 19.3 |
| 2 | Ollama saluki-27b (2-bit Qwen3.8-27B, all on GPU) |
dense | 163,840 | 4.56 | -- | 1x | 11.4 |
Unranked: Ollama lfm2.5-24b-a2b (7.49/h at 131,072, all on GPU) fails
Capability Check v2 audit fitness and its 1x run is incomplete; it is the
RTX's interactive profile.