Local LLM server — wm (GTX 1080 Ti), setup & usage
llama.cpp in router mode on the Wingman work desktop. This is the NVIDIA counterpart of
fl (fl/.config/llamacpp/, RX 7600 XT 16 GB) and lw (lw/…, CPU laptop). Same recipe as fl —
MoE experts in RAM, quantized KV, MTP — on a smaller, older card.
- Box: GTX 1080 Ti 11 GB (Pascal, compute 6.1: int8 MMQ path, no tensor cores) on the
legacy
nvidia-580xxdriver · i7-8700 (6 cores / 12 threads) · 31 GiB DDR4 (~35 GB/s = the ceiling for experts kept in RAM). - Backend: upstream prebuilt CUDA 12.8 tarball (
~/.local/opt/llama.cpp-cuda/current, build b11227) through the~/.local/bin/llama-server-cudawrapper. CUDA 13.x cannot target Pascal, so the Archggml-cuda/llama.cpp-cuda-gitpackages are useless on this box; the prebuilt carries Pascal as PTX and the driver JITs it (first start of a new build ≈ 50 s, then cached). A Vulkan prebuilt sits beside it (llama-server-vulkan) as the no-CUDA fallback — same build, ~half the prompt speed, same decode. - Endpoint:
http://127.0.0.1:11343/v1on the box,http://192.168.50.24:11343/v1from the LAN (wired IP, DHCP — may change). Bound to0.0.0.0and keyless: anyone on the office network (and ZeroTier) can use it. IPv4 only — use127.0.0.1, notlocalhost(which also resolves to::1, where nothing listens). The chat endpoint is POST-only: a browser GET gets 404. - Files here (
~/.config/llamacpp/, per-file stow links from the repo'swm/.config/llamacpp/):config.ini(presets — section names ARE the API model ids),llama.service(user unit),bench.py(nvidia-smi benchmark),bench-results.md/bench-history.md(ledgers, generated next to the script, not tracked). The GGUFs live in~/Documents/models/(llama-server -hfdrops downloads into~/.cache/huggingface/hub/— move them out).
How it runs
A user systemd unit, llama.service (this dir; linked into ~/.config/systemd/user/ and enabled,
starts with the login session — loginctl enable-linger coja makes it start at boot without a login).
Its ExecStart is the command the old abbr ran:
nice -n 19 llama-server-cuda --host 0.0.0.0 --port 11343 --cors-origins localhost \
--models-max 1 --models-preset ~/.config/llamacpp/config.ini
The fish abbr llamaserver now means systemctl --user restart llama; and journalctl --user -fu llama.
Install once after stowing (a symlinked unit can't be linked directly, hence readlink -f):
systemctl --user link (readlink -f ~/.config/llamacpp/llama.service); systemctl --user enable --now llama
- Router mode: each requested model loads on demand in a child process;
/v1/modelslists the presets. --models-max 1: one model resident; a new request LRU-evicts the previous. Two presets never fit together in the ~6.5–7.5 GB the desktop leaves free.sleep-idle-secondsper preset frees idle models either way.nice -n 19: the MoE presets run their expert matmuls on all 6 cores — niceness keeps the desktop responsive; the GPU part is unaffected.--cors-origins localhost: same-machine profile (llama.cpp PR 25655) — blocks browser pages from calling the keyless API. Non-browser clients unaffected.- Presets are read once at startup: edit
config.ini→systemctl --user restart llama→./bench.py -m <ids>. - Logs:
journalctl --user -u llama -f(each model child logs under the router). - "couldn't bind HTTP server socket … 11343" = a router is already running (the service, another
shell, or a bench run) —
systemctl --user status llama,pgrep -af llama-server.
VRAM (the golden rule)
11,264 MiB total, but Hyprland + Xorg + electron apps hold 3.5–4.5 GB, so a preset gets
~6.5–7.5 GB and must be sized for the low end. Unlike fl's amdgpu, an NVIDIA overcommit does
not freeze the PC — cudaMalloc fails and the load errors out cleanly. So the guard is
"loads reliably on a busy desktop": keep every benched VRAM free ≥ ~1 GB (bench.py flags
< 0.8). If a preset is tight: raise n-cpu-moe (MoE), lower ctx-size, or use a smaller quant.
Dense models are the trap here: anything that doesn't fit entirely (> ~6 GB file) streams the
leftover layers from DDR4 and collapses (14B Q3 = 7.6 t/s, 8B Q8 = 3.4, 24B IQ3 = 1.8 — see the
tombstones in config.ini). MoE with the experts in RAM is 2–5× faster at the same size.
Model roster (router benches 2026-09-28 → 10-19, bench.py through the service, desktop baseline 3.3–4.6 GB, gen 256)
Three conditions appear below: quiet = ~3.3 GB desktop, nothing else on the CPU · busy = 4.2–4.6 GB desktop · sim = the 2026-10-19 sweep with the AirSim/PX4 simulation stack running (details after the table).
| model id | decode t/s | ctx | VRAM free | extras | role |
|---|---|---|---|---|---|
gemma-4-E4B-it-UD-Q4_K_XL |
85–101 (MTP 64–84%; 91 sim) | 32k | 3.6–4.8 GB | vision (projector on CPU), MTP draft, thinking OFF | ⭐ fastest + instant tier — quick generalist, images, long documents; pi's default local model |
Mellum2.1-12B-A2.5B-Thinking-IQ4_XS |
63 @moe4 / 87 @moe0 quiet · 42 sim | 16k | 1.6–2.3 GB | code MoE 2.5B active, thinking ON, prefill 1.1–1.5k t/s | ⭐ fast reasoning coder (added 10-12) |
Qwen3.5-9B-MTP |
51–61 (MTP 90%) | 8k | 0.4–1.8 GB | vision, MTP | ⚠ quiet-desktop preferred (loaded at 1.0 GB free on a ~4.5 GB desktop 10-19) — the daily driver at +75% |
Qwen3.5-9B-Thinking |
34–38 | 16k | 1.0–2.5 GB | vision, reasoning budget 8k | hard problems (TTFT ≈ think block) |
Qwen3.5-9B-Q4_K_M |
31–33 (one unexplained 24 on 10-12) | 16k | 1.0–2.8 GB | vision, thinking OFF | ⭐ daily driver — best quality that always fits |
gpt-oss-20b / -low |
21 quiet · 17 busy · 10–12 sim | 32k | 2.1–3.7 GB | MoE @moe18, reasoning + tools | reasoning and tool use; -low cuts the 8.8 s think TTFT to 1.8 s |
Ornith-1.5-35B-A3B-instant |
14–31 (MTP 59–67%; 15 sim) | 16k | 1.4–3.5 GB | MoE @moe36, MTP, thinking OFF | ⭐ agentic coder for agent loops; 31.5 and 15.1 t/s on two quiet-desktop runs 16 min apart (prefill 63 → 9) — CPU contention, the server runs at nice 19 |
Ornith-1.5-35B-A3B-IQ4_XS |
10–26 (MTP 47–61%; 15 sim) | 16k | 1.8–3.3 GB | same file, thinking ON | coder with reasoning; TTFT ~20 s is the think block |
Ornith-1.5-35B-A3B-long |
10 busy · 16.9 @35k prompt (MTP 59–65%) | 128k | 1.8 GB | same file, @moe40, thinking OFF, prefill ~250 t/s | long-context tier (added 10-28) — 35k prompt = 2.4 min to prefill, 100k ≈ 7 min |
Read across: everything that fits entirely on the GPU runs 31–101 t/s; everything
that keeps experts in RAM runs 10–31 and tracks the CPU's idle share. The bench guard (--min-free-gb 0.8) skipped four presets
in the first sweep; they were re-tuned (coder moe 28→34, gpt-oss moe 14→18, 9B draft split into
its own alias) and re-benched green. Expected spread: MTP rows swing with draft acceptance
(content-dependent); every MoE row dips when the desktop is busy (the experts share the 6 cores).
The sim condition (2026-10-19 12:55 sweep). Unreal Blocks (≈ 3 cores at nice 5, 0.56 GB VRAM, GPU 41%
busy) plus the PX4/planner nodes at realtime priority had been running since 12:08; load average ~9 on the
6-core box. Every CPU-expert preset sat at its floor (Ornith 15, gpt-oss 10–12, Mellum 42) and the GPU-only
presets lost 5–15% to the shared GPU. Nothing in the presets caused it and nothing in them fixes it: the
service runs at nice 19 and yields by design. Judge local-model speed with the sim stopped. Two things
the run did establish: all nine presets loaded with ≥ 1.0 GB free at a ~4–4.5 GB desktop (the
busy-desktop sizing holds), and the run's "baseline 7.5 GB" header was pi's resident gemma, not the desktop
— bench.py now drains before it records the baseline.
Which model, when
- Quick questions, images, long one-shot documents:
gemma-4-E4B— 85 t/s, 32k, vision. - Best always-available quality:
Qwen3.5-9B-Q4_K_M(33 t/s). On a quiet desktop pickQwen3.5-9B-MTPinstead — same model, 57 t/s; if it fails to load, the desktop is holding too much VRAM (electron apps), fall back to the plain preset. - Autocomplete-class / cheapest agent steps:
gemma-4-E4Bagain — LFM2.5 was faster (119 t/s) but its output was unusable in practice and it was retired 2026-10-12. - Reasoning coder at speed:
Mellum2.1-12B-A2.5B-Thinking— 63 t/s (87 on a quiet desktop) with a think block and >1k t/s prefill; the first stop for a bug that needs thought. - Agent coding loops (opencode/pi edit-test cycles):
Ornith-1.5-35B-A3B-instant— 14–31 t/s depending on what else is using the CPU (nice 19), no think preamble. Switch to…-IQ4_XS(thinking) for the bug that needs reasoning. - Reasoning + tool use at long context:
gpt-oss-20b(32k);-lowfor interactive use. - Really long context (to 128k):
Ornith-1.5-35B-A3B-long— the 35B with every expert in RAM so the KV fits; prefill ~250 t/s (100k ≈ 7 min). fl's Qwen3-Coder-Next 80B needs ≥ 28 GB of RAM for its experts and does not fit next to this desktop. - No separate fast coder: the 2-bit Qwen3-Coder was deleted 2026-10-12 (dominated by Ornith-instant);
a K-quant Qwen3-Coder stays listed in
config.inicandidates if a second coder is ever wanted. - Anything bigger/denser (14B+ dense, 27B, 80B-A3B) does not belong on this card — see the
candidates list in
config.inifor what does.
Client wiring
These clients run on this box (other machines use http://192.168.50.24:11343/v1). Rule: client model id = config.ini section
name, client context ≤ server ctx-size.
| tool | overlay file | provider / client id |
|---|---|---|
| aichat | wm/.config/aichat/config.yaml |
wm: (e.g. wm:Ornith-1.5-35B-A3B-instant) — default model still duskadiy:; aichat -m wm:… or flip model: |
| opencode | wm/.config/opencode/opencode.json |
wmcpp/ (e.g. wmcpp/Ornith-1.5-35B-A3B-instant); defaults + agent models point at duskadiy/ (the common file's llama.cpp/ LAN provider is unreachable here) |
| pi | wm/.pi/agent/models.json + settings.json |
wmcpp; default model wmcpp/gemma-4-E4B-it-UD-Q4_K_XL since 10-12 (was duskadiy/Ornith…) |
Two ways a model "doesn't respond" in pi (both seen 2026-10-12):
- The router was started before the preset existed — it reads
config.inionce, so pi's request fails as an unknown model.systemctl --user restart llamaafter every preset change. - A small coder (Mellum 2.1, and AesCode-8B before its removal) run with pi's full tool set from
~keeps calling read/bash to explore the home directory and never finishes (router log: the same conversation re-served with a growing context, 2.3k → 5.4k tokens). Run pi inside a project directory, orpi --no-toolsfor plain questions; the same prompt from an empty directory finishes in four rounds.
When adding/removing a preset, update all three wm overlays (aichat, opencode, pi). They are full-file
copies of the common configs plus the wm/wmcpp provider — when a common client config changes,
mirror the change here (same rule as lw).
Tuning cheat-sheet (Pascal + CPU experts)
n-cpu-moe N(MoE only): experts of the first N layers stay in RAM. The VRAM/speed dial; each 4-bit expert layer on the GPU ≈ 0.4 GB (35B-A3B). Measured: gpt-oss @moe12 = 29.8 vs @moe24 = 18.6 t/s; Coder-30B @moe24 = 29.9 vs @moe48 = 20.2. Tune in steps of 2–4, re-bench. Except Ornith: moe30 vs moe36 on the same quiet desktop = 32.1 vs 31.5 t/s (10-12) — its experts on the GPU buy nothing; give the VRAM to the desktop instead.- Quant type for CPU experts: I-quants (IQ4_XS/IQ3_XXS) dequantize slowly on CPU and 4-bit experts stream 2× the bytes of 2-bit → Ornith IQ4_XS with 80% of experts in RAM = 11–15 t/s base. Prefer K-quants (Q4_K_M / UD-Q4_K_XL / UD-Q3_K_XL) and the smallest quant the quality allows when the experts live in RAM.
- MTP / spec decode (
spec-type = draft-mtp) works here: Qwen3.5-9B 32.6 → 57.3 t/s at 90% acceptance; Ornith 11–15 → 14–21 at 60%; gemma-4-E4B 84.7 with its draft. Cost: a flat ~0.75–1.1 GB VRAM per model — which is why the 9B draft lives in its own quiet-desktop alias. Thinking mode drafts worse (Ornith 47–53%, 10 t/s). Qwen3.5/3.6 heads live in the-MTPrepo files; gemma-4 needs a separatemtp-*.ggufdraft; gpt-oss and Qwen3-Coder-30B have no head. threads = 6= physical cores. Never 12: decode collapses 14 → 1.3 t/s (HT contention).- KV cache:
q8_0K and V everywhere except gpt-oss (attention sinks — defaults).flash-attnon everywhere (required for quantized V). Dense Qwens pay ~0.6–0.8 GB per 16k; MoE 48-layer Qwen3-Coder ~1.6 GB per 16k (why it stays at 16k). - First value after a download:
llama-fit-params -m <gguf> -c <ctx> -fa onprints the split that fits the VRAM free right now; take it, then add margin (moe +2, or ngl −4). - CPU governor is
powersave(intel_pstate still boosts);performancemight buy a little on the CPU-expert presets — unmeasured. - Vision: keep the projector on the CPU (
no-mmproj-offload = true) — ~0.9 GB VRAM back.
Benchmarking
Run on this box against the running llamaserver (it evicts whatever is resident — close chat clients first):
./bench.py # full sweep of everything the router lists
./bench.py -m id1,id2 # just the changed presets
./bench.py -n 512 --ctx 12000 # longer gen + a long-context decode column
Safety-first like fl's: force-unloads resident models via the router API, waits for the card
to drain (--drain-gb 5.5 = the desktop's own usage plus slack — raise it if your desktop
alone sits higher), records a ⚠ row instead of generating when a model leaves < 0.8 GB free.
The MTP acceptance % lands in the spec column. Ledgers: bench-results.md (latest) +
bench-history.md (append-only) next to the script.
Reading the columns (changed 2026-10-19):
- baseline used is measured after draining anything of ours; the header adds "(was X GB with a resident model, unloaded first)" when it had to. A high baseline now really is the desktop.
- TTFT = time to the first answer token — for a thinking model that is the think block.
all-think= the whole gen budget (256 tokens) was reasoning and no answer arrived; decode t/s on that row is still valid (server timings count every token).-n 512or the-low/-instantalias gets an answer. - prefill t/s at the default ~30-token prompt is fixed-overhead noise (Mellum showed 15 and 100 on two
runs; a 1.7k-token probe gave 1,160). Use
--ctxfor a real prefill number. - fl's and lw's
bench.pystill carry the content-only TTFT (0.00 on an all-think row); port the two changes when their ledgers next matter.
Watch list
- Free VRAM is the whole game: the desktop's 4.2–4.6 GB (Hyprland ~0.4, Xorg, Ferdium/electron ~0.6+) costs the 9B its MTP and every MoE two or three expert layers. Closing electron apps before a session is worth more than any preset knob.
- Qwen3.5-9B ctx 32768 for the plain preset (KV is cheap on the hybrid arch) — bench first.
- Qwen3.6-35B-A3B-MTP UD-IQ3_XXS (14 GB) — the one "bigger" model worth a test: 28% fewer bytes per token than Ornith's file and a draft that accepts well on non-thinking Qwen; likely ≥ Ornith's 31 t/s quiet.
- 2026-10-12 candidate round: AesCode-8B and Mellum 2.1 kept; FrogNano-4B (87.9 t/s with draft, vision) retired — gemma-4-E4B and LFM2.5 cover that slot faster.
- 2026-10-19: AesCode-8B removed (preset and the aichat/opencode/pi entries); tombstone in
config.ini. - gemma
sleep-idle-seconds = 30vs pi (journal 2026-10-19 12:44–12:56): a pi session slept and woke its default model 3× in 12 min, each wake a full reload ≈ 6 s before the first token (cold load 17 s). Resident, gemma holds 3.7 GB and still leaves ~3 GB beside a 4.5 GB desktop; 120–300 s (fl uses 300 on its coders and 30 on this same gemma) would remove the stall. Not changed — the VRAM-vs-latency trade is the user's call. - The sim stack is the busy floor (10-19): with Blocks/PX4 running, CPU-expert presets drop 40–55% and
GPU-only ones 5–15%. Unmeasured:
threads 3–4on the CPU-expert presets while the sim holds 3 cores — 6 threads then share physical cores with it, and HT sharing is exactly what collapsed decode at 12. - gemma logs
Gemma4Assistant requires ctx_other … (this warning is normal during memory fitting)andfailed to measure the memory of the extra modelat every load, including each wake: the auto-fitter probing the MTP draft. Harmless (ngl 99, everything on the GPU); noted in the preset so nobody chases it. - Dead files were deleted 2026-10-12 (66 GB): ~/Documents/models now holds exactly the nine presets' files (46 GB).