A local-inference stack for one exact machine - Core Ultra 7 255HX, 32 GB DDR5-5600, RTX 5060 Laptop with 8 GB - where every flag was measured instead of guessed, and the guesses that turned out wrong are still in this file with a note saying so.
Two claims worth the click: a 35B mixture-of-experts model at 34 t/s, and a 4B model at 74 t/s with a 131072-token context, both on the same 8 GB card, one at a time.
| model | size | speed | context | tool calls | what it is for |
|---|---|---|---|---|---|
| Gemma 4 E4B QAT UD-Q4_K_XL | 4.2 GB | 74.1 t/s | 131072 | yes | the default: fast, huge context, drives agents |
| Qwen3.6 35B-A3B MoE UD-Q4_K_S | 21.7 GB | 35.3 t/s¹ | 40960 | yes | strongest answers that still fit |
| Qwen3.6 27B-A3B Coder MoE Q4_K_M | 16.1 GB | 37.7 t/s² | 57344 | yes | code, with the best speed-per-capability here |
| flint 40B NVFP4 MoE | 22.9 GB | 29.8 t/s | 49152 | yes | longest context on a big model |
| gpt-oss 20B MXFP4 | 13.8 GB | 30.3 t/s² | 32768 | no | chat only |
| Qwen3 8B Q4_K_M | 5.2 GB | 59.0 t/s | 16384 | yes | small and quick, thinks first |
| Qwen2.5 Coder 7B Q4_K_M | 4.7 GB | 50.3 t/s | 32768 | no | code, not agents |
| DeepSeek-Coder-V2-Lite MAPLE Sub5GB | 4.5 GB | 65.1 t/s² | 18432 | no | 13.5B MoE in 4.5 GB — fastest coder here, but the quant is damaged: 17.7 % on the suite, experiments only |
Every context number is the largest one that completed a real 200-token request on this machine.
/health answering is not proof: one model reports healthy at 24576 and then fails its first
completion. The method, including what had to be thrown away, is in
docs/how-the-numbers-were-made.md.
¹ The two Qwen3.6 windows were widened on 2026-11-13 and their speeds re-measured with
bin\llm-window-ab.ps1, which alternates old and new configuration so thermal drift hits both
arms. On that afternoon the card ran ~30 % below its best-day clocks — the old windows measured
29.1 and 29.2 t/s on the same harness — so treat these two figures as a same-day comparison
(+21 % and +12 % against the narrower window), not as a regression against the rows above.
² Measured on 2026-11-14 with bin\llm-window-ab.ps1, in the same session as the rows footnoted ¹
(bench\window-ab-*.csv). Do not compare columns across footnotes: the absolute numbers move ±30 %
with the card's clocks and the only valid comparison is the one printed in the linked CSV.
git clone https://github.com/zaxsh/local-llm-stack ; cd local-llm-stack
.\get-started.ps1That checks the machine, downloads the pinned llama.cpp build (~715 MB) and whichever models are
missing (~75 GB, resumable), builds Desktop\Local-LLM-Project\llama.cpp, and opens the launcher. It skips whatever is
already done, so it is also how you rebuild after changing a preset.
You supply: Windows 10/11, an NVIDIA GPU (8 GB+ for the two big ones),
PowerShell 7, and pip install -U "huggingface_hub[cli,hf_xet]".
Prefer to drive it by hand?
.\bin\llm-fetch.ps1 -List # what is on disk, what is missing, where each file comes from
.\bin\llm-fetch.ps1 -Missing # download the rest from Hugging Face
.\bin\llm-up.ps1 gemma4 # serve one model on http://127.0.0.1:8083/v1
.\bin\llm-selftest.ps1 # scripts parse, catalogue agrees, presets agree, launcher builds- Tokens per second is bandwidth divided by bytes read per token. GDDR7 moves ~448 GB/s, DDR5 ~47 GB/s. That 9.5x gap decides everything else, and it is why quantisation is not the first lever - memory placement is.
- MoE plus
--n-cpu-moeis the only way to run 20-40B in 8 GB. A sparse model reads a few experts per token, so the experts that do not fit are fine in DDR5:flintstreams roughly 84 % of its parameters from RAM and still does 30 t/s. - f16 KV cache beats q8_0 by 25-35 % here - the opposite of the usual "quantise the cache to save memory" advice, and worth 3 GB of contexts.
- The fast GPU shipped switched off on this laptop. The first pass of this project ran on the Intel iGPU, concluded the machine had no VRAM, and optimised the wrong thing. Section 2 is what correcting that looked like.
| path | what |
|---|---|
get-started.ps1 |
clone to running model, in one command |
GUIDE.md |
for an agent picking this up cold: the method, the current state, the traps, what to do next |
bin\ |
32 scripts: registry, fetcher, bundle builder, launcher builder, benchmarks, the capability battery, self-test, agent connectors |
mcp\ |
rea-mcp.json — the MCP server definition llama-server reads from --mcp-servers-config, written by bin\llm-mcp.ps1 |
models.manifest.json |
the catalogue: repo, file, exact size, sha256 per model |
presets\ |
the tuned flags for all eight models, readable without running anything |
launchers\ |
the Desktop shortcuts |
docs\ |
the results, the method, the project state, the client wiring guide, and docs\START-HERE.md (the one-minute version) |
bench\ |
the CSVs the numbers came from, and bench\capability-suite-v1.json — the graded task battery |
.github\workflows\ci.yml |
parses every script, checks the catalogue, compiles the launcher |
The engine and the weights are not in the repository: get-started.ps1 fetches the build,
models.manifest.json + bin\llm-fetch.ps1 fetch the models. The code here is MIT; the
weights keep their own licences (Gemma terms of use, Apache-2.0 for the Qwen models, gpt-oss' own
terms, flint NVFP4 from RecursiveSI, the DeepSeek model licence for the MAPLE build) and are
downloaded from their authors, never redistributed.
models.manifest.json records where each file came from and what it was when it was measured.
- Machine profile (scanned)
- The finding: the fastest GPU on this laptop is switched off
- Memory budget: what actually fits in 8 GB
- Measured performance
- "Mac-level" — what that actually means here
- What changed in this pass
- Layout and quickstart
- Tuning that matters on this specific machine
- Known quirks, found the hard way
- Next steps, in value order
- Driving these models from an agent
- Connecting any model to any client
- The Desktop bundle
- Context windows: the ceiling, per model
- Getting the models back
- Capability: what each model can actually do
A note on this document: sections 1-15 are the working notebook, kept in the order it was written, including the parts that were later corrected. If you only want the conclusions, the front of this file and the two tables are enough.
| Part | Detail |
|---|---|
| Laptop | MSI barebone MS-17T4, BIOS E17T4IMS.10C (AMI, 2026-06-03) |
| CPU | Intel Core Ultra 7 255HX — Arrow Lake-HX, 20 cores / 20 threads (8 P + 12 E, no SMT) |
| CPU ISA | AVX2 ✅ · AVX-VNNI ✅ · AVX-512 ❌ · AMX ❌ · no NPU on the HX line |
| RAM | 32 GB DDR5-5600, 2 × 16 GB Samsung, dual channel. Measured 47 GB/s copy, ~60 GB/s read (theoretical 89.6) |
| dGPU | NVIDIA GeForce RTX 5060 Laptop — 8 GB GDDR7, ~448 GB/s (28 Gbps on a 128-bit bus) |
| iGPU | Intel Graphics (Xe-LPG, 4 Xe cores), no VRAM — shares DDR5 |
| GPU driver | NVIDIA 621.01 (installed) · Intel 32.0.101.6629 / Vulkan 1.4.303 |
| OS | Windows 11 Pro, build 28000 |
| Disk | Crucial P310 1 TB NVMe, C: 730 GB (227 GB free), D: 200 GB (61 GB free) |
| Power | Balanced plan, MSI User Scenario Mode=1 |
Two memory pools, and the gap between them is the whole story:
RTX 5060 VRAM 448 GB/s | 8 GB <- fast, tiny
DDR5-5600 60 GB/s | 32 GB <- slow, big
Anything you can hold in the first pool runs at roughly 7× the speed of anything in the second. That single sentence drives every recommendation below.
At scan time Windows reported:
[ok] Intel(R) Graphics (driver 32.0.101.6629)
[OFF] NVIDIA GeForce RTX 5060 Laptop GPU - not on the PCI bus
last seen : 8.11.2026 21:03:13 problem: CM_PROB_PHANTOM ; Code 45
Code 45 means "this hardware device is not connected to the computer". The device node is
a ghost: the PCI function is not enumerated at all, and nvidia-smi fails with
"you do not have sufficient permissions" (that message appears when the driver cannot
open a device that is not there). No NVIDIA display adapter exists in Win32_VideoController.
This is a regression, not a machine that never had the card: the device last arrived on 2026-11-08 21:03, the NVIDIA driver 621.01 is installed from October, and there is a 22 GB NVFP4 model in Ollama from 44 hours ago — NVFP4 only runs on Blackwell silicon, so the 5060 was demonstrably working two days ago. Something powered it down since.
On MSI laptops this is almost always one of three things:
- MSI Center → GPU Switch set to Integrated / UMA. In that mode the embedded controller cuts power to the dGPU, the PCI function disappears, and Windows keeps the old node as a phantom. This is by far the most common cause.
- BIOS → Graphics Configuration → iGPU only.
- The card fell off the bus (driver/TDR/resume bug) and did not come back.
Fix ladder — in this order:
| Step | Where | Effect |
|---|---|---|
| 1 | MSI Center → Features → General Settings → GPU Switch → Hybrid Graphics (or Discrete), then reboot | brings the PCI function back |
| 2 | MSI Center → User Scenario → Extreme Performance | raises the shared CPU+GPU power ceiling; a 255HX + 5060 will use ~150 W |
| 3 | BIOS (Del at boot) → Advanced → Graphics Configuration | enable discrete GPU / MUX |
| 4 | Elevated shell: llm-gpu.ps1 -Fix |
PCI rescan, re-enable, power plan, Defender exclusions |
| 5 | Full power-off (unplug, hold power 15 s), not a restart | recovers a card stuck in a bad power state |
After the reboot, verify with:
llm-gpu.ps1 # should print [ok] NVIDIA GeForce RTX 5060 Laptop GPU 8192 MiB ...
nvidia-smi
llm-check # dGPU row should flip from OFF to okNothing in llm\ needs to be reconfigured afterwards: llm-server.ps1 -Backend auto, Ollama
and LM Studio all pick the card up on their own (see section 6).
Usable VRAM is ~7.4 GB once the compositor and desktop have their share, and the KV cache comes out of the same pool (q8_0 ≈ 80 kB/token for a 36-layer GQA model, q4_0 ≈ 42 kB/token — so 32k context is 1.3–2.6 GB on its own).
| Model | Quant | Weights | + KV at 32k | Verdict |
|---|---|---|---|---|
| Qwen3-4B / Llama-3.2-3B | Q4_K_M | 2.6 GB | 3.7 GB | fits easily — ~55–75 t/s |
| Qwen3-8B / Qwen2.5-Coder-7B | Q4_K_M | 4.9 GB | 6.2 GB | fits — the sweet spot: 58–62 t/s measured |
| Qwen3-14B | IQ3_XXS / Q3_K_S | 6.5 GB | 7.8 GB | tight; cut context to 8k or quantize KV to q4_0 |
| Qwen3-14B | Q4_K_M | 9.0 GB | — | spills; ~8–12 t/s |
| gpt-oss-20b (MoE, 3.6B active) | MXFP4 | 12 GB | — | hybrid, but fits fully in page cache: ~35–45 t/s, steadier than flint |
| Qwen3-30B-A3B (MoE, 3B active) | Q4_K_M | 18 GB | — | hybrid only, ~15–20 t/s |
Rule: dense models must fit in VRAM or they crawl. MoE models are the exception — only
a few billion parameters fire per token, so putting the expert tensors in DDR5 and the rest
on the 5060 still runs at 3–5× the CPU-only speed. That is what -CpuMoe is for.
8 GB is not "too small to load anything good". It is too small to load a big dense model, which is a different statement. Pick by shape, not by size:
| Tier | Model shape | Where the bytes live | Bandwidth you actually use | Expect |
|---|---|---|---|---|
| 1 — daily driver | dense, 4–8B Q4_K_M (2.6–4.9 GB) | 100 % VRAM | 448 GB/s | 40–75 t/s — faster than an M4 Pro |
| 2 — capability | MoE, 20–40B total / 3–6B active | attention + hot experts in VRAM, experts in DDR5 | 448 GB/s on the hot path, 60 GB/s for the streamed experts | 30–48 t/s (measured on flint), but bursty |
| 3 — don't | dense, 13B+ Q4 | 7 GB VRAM + the rest DDR5 | you pay 60 GB/s for every byte | 5–12 t/s, worse than tier 1 at everything |
Why tier 3 is a trap, in bytes read per generated token:
| Model | weights | read per token | source | ceiling |
|---|---|---|---|---|
| 8B dense Q4 | 4.9 GB | 4.9 GB | VRAM @ 448 GB/s | ~11 ms → 90 t/s theoretical, ~50 real |
| 14B dense Q4 | 9.0 GB | 9.0 GB | 6.4 GB VRAM + 2.6 GB DDR5 | ~55 ms → 18 t/s theoretical, ~10 real |
| 30B-A3B MoE Q4 | 18 GB | ~1.9 GB (3B active of 30B) | 6 GB VRAM + 12 GB DDR5, only the active experts are read | ~45 ms → 22 t/s theoretical, ~18 real |
The MoE row is the whole trick: 18 GB of weights but only ~1.9 GB of traffic per token, so a 60 GB/s bus is no longer the bottleneck. A dense 18B model is the same size and 10× slower. Measure the split rather than guessing it:
llm-tune.ps1 -Model .\models\gpt-oss-20b.gguf -Mode moe # sweeps --n-cpu-moe, prints the winner
llm-tune.ps1 -Model .\models\Qwen3-8B-Q4_K_M.gguf -Mode denseThis was tried, measured, and lost — it will lose again because of physics, not tuning:
| Apple "unified memory" | This laptop's "unified memory" | |
|---|---|---|
| Memory | on-package LPDDR5X, 273–546 GB/s | the same DDR5-5600 sticks as the CPU, ~60 GB/s |
| GPU | 10–40 cores, designed to stream from that pool | 4 Xe cores, designed as a display adapter |
| Per-token bandwidth | 4.5–9× the DDR5 here | identical to the CPU, minus copy overhead |
Measured on identical weights: 7B Q4 generation 13.0 t/s on the CPU vs 7.9 t/s on the
iGPU vs 8.4 t/s on OpenVINO. There is no configuration of the Intel iGPU that beats this
CPU at generation, because they share one bus and the iGPU only adds a copy. The iGPU's single
win is prompt processing (171 vs 73 t/s), which is compute-bound — and the 5060 does that
better than either. Do not set OLLAMA_IGPU_ENABLE; do not build for Vulkan/iGPU.
Already on disk (ollama list): qwen2.5-coder:7b (4.7 GB, the current CPU daily driver)
and hf.co/RecursiveSI/flint-nvfp4-gguf:NVFP4 — and that second one is more interesting than
its 22 GB suggests.
llama-plan-style header parse of the blob:
arch flint | 49 blocks | 256 experts, top-6 used | emb 2048 | NVFP4 (4.60 bpw) | 21.29 GB
non-expert 1.46 GB | expert 19.83 GB | bytes read per token: 1.92 GB
Measured on the CPU alone, no GPU involved:
| test | result |
|---|---|
| pp32 (prompt) | 45.4 t/s |
| tg8 (generation) | 18.7 t/s |
18.7 t/s from a 40B model on a laptop CPU, and it beats the 7B Qwen2.5-Coder's 13 t/s — purely because it reads 1.92 GB per token instead of 4.9 GB. A dense model of the same 21 GB would read 11× more and land at ~2 t/s. This is the tier-2 strategy working, already, without the GPU.
--n-cpu-moe N keeps N layers' experts in DDR5. Swept with llama-bench (-p 128 -n 128 -r 2 -d 8192) on an 8.15 GB card with 7.04 GB free:
| n-cpu-moe | tg t/s | note |
|---|---|---|
| 34 | OOM | wants 7.39 GiB, only 7.04 free — that is the fit boundary |
| 36 | 29.6 | at the edge, paging costs more than the extra layers win |
| 40 | 43.6 | best — 7 layers' experts on the GPU, 6.3 GB resident |
| 44 | 41.5 | |
| 47 | 40.8 | all experts on CPU, attention on GPU |
Thread count at ncmoe=40: 8 → 44.0, 12 → 44.8, 16 → 46.9, 20 → 44.5. Sixteen threads (8 P + 8 E) beats all 20 — the opposite of the pure-CPU case.
End to end through llm-server.ps1 -Ctx 16384, four consecutive 256-token requests:
38.8 / 48.6 / 28.6 / 44.8 t/s, mean ≈ 40, single-request best 44.3.
The surprise: experts on the GPU barely help. ncmoe 47 and 40 differ by ~3 t/s, which is noise, while the GPU idles at 30–63 % utilisation with memory utilisation at 15 %. The GPU earns its keep on the non-expert tensors (attention, 1.46 GB) — that alone is the 2.2× over CPU-only. The expert GEMVs stay on the CPU either way, because NVFP4 has no fast CPU vector-dot path here: ~36 GB/s effective instead of the ~60 GB/s the bus can deliver. flint is CPU-compute-bound, not VRAM-bound, and no split changes that.
What follows from that:
- The 21.29 GB model does not fit in 29.4 GB of RAM alongside the desktop, so some of every token comes off the SSD. That is the 28–49 t/s spread — it is page cache and temperature, not settings. Closing memory hogs and letting the machine cool narrows it upward.
- Since GPU-resident experts add ~3 t/s, that VRAM is nearly free to spend on context instead: ncmoe 47 frees ~4 GB for a 64k window at the same ~40 t/s.
- An 8B dense Q4 (4.9 GB, fully resident, no CPU experts) is the stable ~50 t/s tier, and a 12 GB MoE such as gpt-oss-20b fits entirely in page cache. flint is the "40B quality on a laptop" tier at 40 ± 10 t/s — that is the trade, and it is a good one.
llm-server.ps1 -Model flint -Ctx 16384 -CpuMoe 40 -Threads 16-Model now accepts an Ollama name (flint, qwen2.5-coder:7b) and resolves it to the
sha256 blob through the manifest, so llama.cpp and Ollama share one 21 GB file instead of
two copies.
llama-bench, Qwen2.5-Coder 7B Q4_K_M, pp512 / tg128:
| Backend | Prompt (pp512) | Generation (tg128) | Verdict |
|---|---|---|---|
CPU (-t 20) |
73 t/s | 13.0 t/s | best generation of the three |
| Vulkan / iGPU | 97 t/s | 7.9 t/s | slower generation |
| OpenVINO / iGPU | 171 t/s | 8.4 t/s | best for long prompts |
1.7B Q8_0: CPU 137 pp / 28.7 tg vs Vulkan 209 pp / 16.4 tg — same pattern.
Hybrid -ngl 12/20/28 on the iGPU: 87–94 t/s prompt but only 7.7–8.8 t/s generation. Splitting
across the iGPU always costs generation speed, which is why "CPU only" was the right default
while the dGPU was off.
All numbers below are real, on this machine, with the card enumerated and 7.04 GB free.
llama-bench for the raw figure, then end to end through llm-server.ps1 (256-token
completions, three repeats).
| Gemma 4 E4B Q4_K_XL | Qwen2.5-Coder 7B dense Q4_K_M | flint 40B MoE NVFP4 | |
|---|---|---|---|
| VRAM resident | 3.9 GB | 5.5 GB (-ngl 99) |
6.45 GB (-CpuMoe 40) |
| generation, bench | 89.6 t/s | 62.3 t/s | 43.6 t/s |
| generation, end to end | ~82 t/s | 58 / 62 / 62 t/s | 41 / 48 / 43 t/s |
| prompt processing | 6,117 t/s | 2,738 t/s | 370 t/s |
| native tool calls | yes (finish_reason: tool_calls) |
no | depends on template |
| variance | ±2 t/s | ±1 t/s | ±10 t/s |
Both dense models are 2.5–4.5× faster than the same work on the CPU (13 t/s generation, 73 t/s prompt). The dense 7B is not faster than flint because it is smaller — it reads 4.7 GB/token against flint's 1.92 GB — it is faster because every byte is in VRAM and none of it is on the CPU, and because prompt processing runs entirely on tensor cores.
The prompt-processing gap is the one that matters in daily use: a 4,800-token file costs 0.8 s on Gemma 4, 2.4 s on the 7B and 13 s on flint. Generation is nearly a tie; ingestion is 16×.
Gemma 4 E4B is the one to use for agent work. 90 t/s generation, 6,117 t/s prompt, 3.9 GB of
VRAM, and it emits native tool_calls — verified with llama.cpp directly, so it is a property of
the model's chat template, not of any proxy. It was already on the disk (via Magnitude's hub
cache) and needed only a hardlink.
Same model, same everything, only -ctk/-ctv changed:
| KV cache | generation (7B dense, VRAM-resident) |
|---|---|
| q8_0 | 44.7 / 49.6 t/s |
| f16 | 57.9 / 62.4 / 62.3 t/s |
Quantising the KV cache costs 25–35 % of generation speed on GPU, because every layer
dequantises the whole cache on every token. llm-server.ps1 therefore defaults to f16 now;
drop to q8_0/q4_0 only when f16 does not fit (the fit warning says so).
Two later measurements sharpen that. On qwen3 at an equal 16384 window the penalty is smaller than
the 25–35 % above — f16 55.5 against q8_0 52.5 t/s (−5.4 %, bench\window-ab-qwen3-kv-only.csv) — so
the number depends on the model and the build. And when the cache genuinely does not fit, halving it
is a win, because speed here is bytes read per token: dsv2 runs 58.0 t/s at 8192/f16 and 65.1 at 18432/q8_0 (§14). Keep f16 when it fits; measure instead of assuming when it does not.
Apple silicon wins on unified memory: one 273 GB/s (M4 Pro) or 546 GB/s (M4 Max) pool that the GPU can address up to 64–128 GB. You cannot copy that; you have two pools instead.
| This laptop | M4 Pro (24–48 GB) | M4 Max (36–128 GB) | |
|---|---|---|---|
| GPU bandwidth | 448 GB/s (8 GB) | 273 GB/s | 546 GB/s |
| CPU bandwidth | 60 GB/s (32 GB) | 273 GB/s | 546 GB/s |
| Model size that runs well | ≤7 GB in VRAM, ~30B MoE hybrid | up to ~30 GB unified | up to ~100 GB unified |
| 8B Q4 generation | est. 40–55 t/s | ~25–35 t/s | ~40–55 t/s |
| 70B-class | no | no | yes, slowly |
Verdict: in VRAM you are already at or above MacBook-Pro-class token rates — 448 GB/s is 1.6× an M4 Pro and 4.5× your own DDR5. What you cannot match is capacity: a 32 GB Mac runs a 20 GB model at one bandwidth, while you have to split it. The honest goal is therefore:
- ≤7 GB models: beat a MacBook Pro. Full VRAM residency, 40–75 t/s.
- 7–30 GB models: reach parity on MoE only. MoE hybrid offload is your equivalent of unified memory — the experts live in DDR5, the hot path stays in VRAM.
- >30 GB dense: not a target. Streaming weights over 60 GB/s DDR5 lands at 2–4 t/s.
That is why the model list in section 3 is shaped the way it is, and why gpt-oss-20b and
Qwen3-30B-A3B matter more here than a 32B dense model would.
Everything below is already applied; nothing here needs a reinstall.
| Change | Where | Why |
|---|---|---|
llm-gpu.ps1 added |
llm\bin |
detects the phantom dGPU, prints the fix ladder, -Fix runs the Windows-side repairs |
llm-server.ps1: new cuda backend + auto default |
llm\bin |
picks cuda → vulkan → cpu automatically based on whether the RTX is enumerated |
llm-server.ps1: -CpuMoe N (MoE hybrid split), -DryRun, VRAM-fit warning |
llm\bin |
the MoE path is the one hybrid split worth using; the warning catches a model that will silently spill |
llm-check.ps1: dGPU row, cuda build row |
llm\bin |
the health check no longer pretends the iGPU is the only GPU |
llm-check.ps1: LM Studio engine read from backend-preferences-v2.json |
llm\bin |
the CLI table's ✓ glyph mangles under a Turkish code page |
llama.cpp\b12255\cuda\ |
llm\llama.cpp |
CUDA 13.5 build + cudart, so -Backend cuda works the moment the GPU is back |
Ollama env: OLLAMA_CONTEXT_LENGTH 65536 → 32768, KEEP_ALIVE 30m, MAX_LOADED_MODELS 1, dropped dead OLLAMA_EXPERIMENT |
HKCU\Environment |
64k context on a 7B + q4_0 KV is ~2.6 GB of an 8 GB card; keep-alive stops a 5 GB reload every 5 minutes |
Per-app High performance GPU for ollama.exe, all three llama-server.exe, llama-cli.exe, llama-bench.exe |
HKCU\...\DirectX\UserGpuPreferences |
on a MUX-less hybrid laptop this is what actually pins the process to the 5060 |
Not applied (needs an elevated shell — run llm-gpu.ps1 -Fix):
- power plan → High performance, core parking off, boost mode aggressive
- Defender exclusions for
llm\models, the HF hub cache and the LM Studio models dir - PCI rescan / device re-enable
And not doable from Windows at all: the MSI Center GPU Switch / BIOS step. That one is on you, and it is worth more than everything else in this file combined.
C:\Users\fih\llm\
├─ get-started.ps1 clone to a running model, one command
├─ LICENSE MIT for the code; the weights keep their own
├─ SECURITY.md what leaves the machine (Hugging Face, only when asked)
├─ llama.cpp\b12255\
│ ├─ cpu\ AVX-VNNI CPU build <- on PATH as llama-server
│ ├─ cuda\ CUDA 13.5 build (RTX 5060) <- used by -Backend auto once the GPU is on
│ ├─ vulkan\ Vulkan build (works on both GPUs, ~10-15 % slower than cuda on the RTX)
│ └─ openvino\ OpenVINO build (Intel iGPU, best prompt throughput)
├─ tools\ cmake, ninja, downloaded zips
├─ venv\ Python 3.12 LLM env
├─ bin\ llm-up · llm-fetch · llm-direct · llm-desktop · llm-claude · llm-server ·
│ llm-plan · llm-tune · llm-gpu · llm-bench · llm-check · llm-ctxgrow ·
│ llm-verify-bundle · llm-selftest · serve-model · connect-* · shim
├─ models.manifest.json the catalogue: repo, file, size, sha256 per model
├─ presets\ generated .cmd per model (the tuned flags, readable)
├─ launchers\ the Desktop .cmd shortcuts, %USERPROFILE%-relative
├─ docs\ this project's reports, copied from the Desktop
├─ models\ .gguf files (hardlinked from the HF blob cache — one copy on disk, two names)
└─ bench\ benchmark CSV output
llm-check # status table, includes the dGPU
llm-up.ps1 # what is in models\, what is running
llm-up.ps1 qwen3 # start the 8B on :8081 (stops whatever holds the card)
llm-up.ps1 flint -Foreground # 40B MoE on :8080
llm-claude.ps1 # Claude Code against whatever is loaded
llm-plan.ps1 -Model flint # does it fit, what split, what speed (header only)
llm-tune.ps1 -Model flint -Mode moe -Ctx 16384 # sweep -CpuMoe, print the winner
llm-server.ps1 -Model flint -Ctx 16384 -CpuMoe 40 -Threads 16 -DryRun # print the command only
llm-fetch.ps1 -List # what is on disk, what is missing, where it comes from
llm-fetch.ps1 -Missing # download whatever is not here yet
llm-direct.ps1 # rebuild the launcher (and llama-direct.exe)Third-party engines, unchanged: Ollama 0.40.15 (CUDA 12/13 + Vulkan runtimes bundled),
LM Studio 0.4.28 + lms (gguf engine currently vulkan-avx2@2.72.0 — switch it to
llama.cpp-win-x86_64-nvidia-cuda12-avx2@2.72.0 once the GPU is back, it is already installed),
Lemonade 2026.41.1 (OpenVINO), GPT4All / AnythingLLM / Jan frontends.
| Setting | Why |
|---|---|
Full GPU offload (-ngl 99) |
448 GB/s vs 60 GB/s. Partial offload is only correct for MoE (-CpuMoe) or a model that does not fit |
-fa on |
flash attention: less KV traffic, matters most at 32k+ context |
-ctk f16 -ctv f16 |
measured: +25–35 % generation vs q8_0 (62.3 vs 44.7 t/s on a 7B). Quantised KV dequantises the whole cache every layer, every token. Only drop to q8_0/q4_0 when f16 does not fit |
--n-cpu-moe N |
MoE only. Sweep it — on flint 40 beat both 36 and 47 |
threads = 16 for MoE, 20 for dense |
measured: flint 16 → 46.9 vs 20 → 44.5 (CPU experts, E-core stragglers cost); the dense 7B needs nothing from the CPU, so 20 is fine |
| Keep the model resident | Ollama OLLAMA_KEEP_ALIVE=30m, LM Studio TTL. A 5 GB reload is 3–5 s |
| Power plan High performance + MSI Extreme | lifts the CPU power ceiling, which is what caps flint |
| Defender exclusions for model dirs | scanning a 21 GB GGUF on every page-in is pure waste, and it shows up as random 25 % slowdowns |
| VBS/HVCI | already off (memory integrity disabled) — good, it costs 5–15 % on llama.cpp |
OpenVINO backend (-Backend openvino) — llm-server.ps1 already works around all of these:
- Startup takes 20–40 s (execution graph compiled per model).
/healthreturns 503{"error":{"message":"Loading model"}}until done — normal, not a hang. - Do not pass
--no-mmap:[GPU] shared USM buffer has smaller size (0) than specified layout (16). - Do not use a quantized KV cache:
pre-allocated tensor (cache_k_l2 (view) (permuted)) ... cannot run the operation (PERMUTE). - Keep
-np 1: the default 4 slots compile 4 graphs (~100 s to ready).
The CPU, Vulkan and CUDA builds have none of these restrictions and start in 1–3 s.
This build's flag names: -lm/--load-mode {auto,none,mmap,mlock,mmap+mlock} replaced the old
--no-mmap/--mmap pair. -ngl 99 means "all layers".
- Switch the dGPU back on (section 2) and reboot. Nothing else in this document is worth
more. Verify with
llm-gpu.ps1then runllm-bench.ps1on a Qwen3-8B Q4_K_M to replace the estimates in section 4 with real numbers. llm-gpu.ps1 -Fixfrom an elevated shell: power plan, core parking, Defender exclusions.- Pull one model per size class and keep them resident:
qwen3:8b(daily),gpt-oss-20b(the MoE hybrid that punches above 8 GB), an embedding model. - Update LM Studio's gguf engine to
nvidia-cuda12-avx2@2.72.0(already installed). - Optional:
winget install LunarG.VulkanSDKonly if you want to compile llama.cpp yourself.
llm-checkGreen across the board = every runtime still wired correctly, and the dGPU row tells you
whether you are on the fast path or still on the CPU.
Two clients, one running llama-server each time. Start the model first — every client fails with a connection error if nothing is listening.
| model | port | id on the wire | start with |
|---|---|---|---|
| Gemma 4 E4B | 8083 | gemma-4-e4b |
llm-up.ps1 gemma4 |
| Qwen3.6-35B-A3B | 8084 | qwen3.6-35b-a3b |
llm-up.ps1 qwen36 |
| flint (40B MoE) | 8080 | flint-nvfp4-40b |
llm-up.ps1 flint |
| Qwen3-8B | 8081 | qwen3-8b |
llm-up.ps1 qwen3 |
| Qwen2.5-Coder-7B | 8081 | qwen2.5-coder-7b |
llm-up.ps1 coder |
| gpt-oss-20b | 8082 | gpt-oss-20b |
llm-up.ps1 gptoss |
The id is --alias, not the filename — that is why it stays stable when a file is re-downloaded.
Qwen3.6-35B-A3B is the strongest model here (42.7 t/s, 2.8 s to first token, native tool
calls), but it runs one layer away from not fitting: -n-cpu-moe 30 at 16k context. 29 fails to
load, and so does 32k context. Measured alternative configurations, including why its MTP
speculative-decoding head is present but unusable on an 8 GB card, are in
Desktop\Local-LLM-Project\docs\how-the-numbers-were-made.md Part 8.
Three routes are registered in ~\.dsh\profiles\dsh-tui\cordis.patch.yml under
llm-pi-ai.providers: llama-gemma, llama-flint, llama-qwen, llama-gptoss. dsh-tui
watches that file, so the routes appear without a restart.
llm-up.ps1 gemma4 # in another terminal
dsh-tui # then /model -> llama-gemma -> gemma-4-e4b
No API key: llama-server is unauthenticated on loopback, and the routes declare no apiKeyEnv.
Claude Code speaks the Anthropic Messages API. llama-server.exe has no /v1/messages
route (verified against the binary), so it cannot be pointed at directly. bin\anthropic-shim.py
sits in between and translates requests, streaming SSE and tool calls:
llm-claude.ps1 # Gemma 4 E4B: starts the model, the shim on :8097, then claude
llm-claude.ps1 -Model flintIt sets ANTHROPIC_BASE_URL=http://127.0.0.1:8097, ANTHROPIC_AUTH_TOKEN=local, and disables
non-essential traffic. Nothing leaves the machine. Standard library only — no pip install on a
4 Mbit/s link.
Two honest caveats, both measured:
- Context. Claude Code's system prompt plus tool schemas is 12–15k tokens before you type
anything. Run the server at
-Ctx 32768(llm-claude.ps1does) or it will truncate mid-task. - Tool calling depends on the model, not the shim. Qwen2.5-Coder-7B returns tool calls as a
```jsonblock in plain text — verified against llama.cpp directly, so it is the model's template, not the translation. An agent loop needs a model with native tool support.
Measured with one tool-shaped prompt ("what is the weather in Istanbul, use get_weather"),
temperature 0.2, against llama-server directly:
| model | native tool_calls |
note |
|---|---|---|
gemma-4-e4b |
✅ | get_weather({"city":"Istanbul"}), 80 t/s |
qwen3-8b |
✅ | correct call, but it reasons first — allow 600+ output tokens |
flint-nvfp4-40b |
✅ | correct call, 1.6 s to first token |
qwen2.5-coder-7b |
❌ | writes the call as a json code block; unusable for tool loops |
gpt-oss-20b |
❌ | emits raw Harmony markup (`< |
deepseek-coder-v2-lite |
❌ | the chat template has no tool block at all — there is nothing to put a call in |
One trap worth knowing before you blame the client: Qwen3 and Gemma 4 are reasoning models.
They stream their chain of thought into a separate reasoning_content field and only then fill
content. With max_tokens of 200–300 the budget is spent thinking and content comes back
empty, which looks exactly like a broken server or a broken shim. Give them 800+ tokens per turn
(Claude Code's default is fine; a bare curl test is not).
gemma-4-e4b is the default for a reason: fastest generation, lowest first-token latency, native
tool calls, and only 3.8 GB of VRAM. llm-scenarios.ps1 re-measures all of the above with six
ordinary user prompts and writes a report to Desktop\Local-LLM-Project\docs\.
The presets for the four models that can call tools start llama-server with
--mcp-servers-config mcp\rea-mcp.json. That file declares
REA — the reverse-engineering toolset also registered in Claude
Code, Codex and dsh-tui on this machine — as an MCP server. llama-server spawns it over stdio,
so the analysis tools arrive without a second daemon and without a second port:
srv start: MCP warmup: 'rea' discovered 39 tools
srv setup: Added 39 MCP tools
The catalog is a context cost, and it is measured. llama-server lists the tools at startup,
stops REA, and respawns it per call — an idle chat is about 20 prompt tokens. But every tool a chat
selects has its schema embedded in the request, and REA's full catalog is enormous: all 255 at
once is 85 098 prompt tokens. That is the failure v1.2.0 shipped — reproduced here on
coder36, whose window is 32 768:
request (85098 tokens) exceeds the available context size (32768 tokens), try increasing it
Measured the same way (one request carrying every tool of the scope, prompt tokens read back from
the server; bin\llm-mcptest.ps1 -Scope <name> repeats a row):
| scope | tools | qwen3 16k |
gemma4 131k |
|---|---|---|---|
session,enhanced — the default |
39 | 12 402 | 8 604 |
direct,enhanced |
56 | 10 607 | 8 191 |
session,direct,enhanced |
80 | — | 14 090 |
| everything REA publishes | 255 | 85 098 | 63 800 |
The default is session,enhanced because it is the smallest scope that can still do the work in a
chat: session carries open_binary/binary_session (without which nothing can be opened at all)
and enhanced carries analyze_function, decompile_all, batch_decompile, binary_overview,
get_call_graph, find_xrefs and the trace_* family. Widen it with
bin\llm-mcp.ps1 -Families session,direct,enhanced, or take everything with -Full — and check the
result against the windows first:
.\bin\llm-ctxfit.ps1 # every tool preset: FIT / TIGHT / OVER
.\bin\llm-ctxfit.ps1 -Scope all # what the old full catalog costsqwen3 (16 384) carries no tools for the same reason: the 39-tool default is 12 402 of its 16 384
tokens, which leaves nothing for the tool result the model would then have to read. coder and
gptoss never had them — neither emits a native tool_calls.
The schemas are charged per request, so the window is not leaking — it is being spent. The rate, measured on this machine and the number to budget with:
| tokens per tool | 39-tool default | |
|---|---|---|
| Qwen tokenizer | 318 | 12 402 |
| Gemma tokenizer | 221 | 8 604 |
Three consequences worth knowing before blaming the model for a short memory:
- The picker is per chat. Only the tools ticked in that chat are sent, and the cost is linear:
the eight that do the actual work —
open_binary,binary_session,analyze_function,decompile_all,batch_decompile,binary_overview,get_call_graph,find_xrefs_to_name— are ~2 500 tokens, a fifth of the default. Select what the task needs, not the whole family. - Most of REA's catalog bytes never reach the model. Its
outputSchemas are 91 % of the 637 KB the 39 tools weigh;llama-serversends name, description and input schema only. Trimming descriptions would therefore buy ~30 %, not 5×. - The progressive catalog is not usable here. REA can advertise 5 tools plus
discover_tools(REA_MCP_TOOL_CATALOG_PROFILE=hot-cold) for 1 678 tokens — measured ongemma4, an 81 % cut. Butllama-serverignoresnotifications/tools/list_changedand respawns REA per call, so a discovered tool never becomes callable: after a realdiscover_toolscall,GET /toolsstill returned 5. It is only useful for chats that need nothing beyond open/session/evidence.
They show up as rea_* on GET /tools and in the Web UI's tool picker, and the model calls them
like any other tool. POST /tools is the endpoint the UI uses to run one; that is also the
quickest way to check the chain by hand:
curl.exe -s -X POST http://127.0.0.1:8083/tools -H 'Content-Type: application/json' `
-d '{"tool":"rea_binary_session","arguments":{"detail":"summary"}}'Measured on this machine, each model started with the real preset and the real config, one
get_weather-shaped tool prompt, then the result fed back:
| model | native tool_calls |
full tool loop | load with MCP |
|---|---|---|---|
gemma-4-e4b |
✅ | ✅ answers from the tool result | 9.2 s |
qwen3.6-35b-a3b |
✅ | ✅ | 29.8 s |
flint-nvfp4-40b |
✅ | ✅ | 32.8 s |
qwen3.6-27b-a3b-coder |
✅ | ✅ | 17.0 s |
qwen3-8b |
✅ | ✅ without tools; the window cannot carry the catalog | 13.9 s |
qwen2.5-coder-7b |
❌ | ❌ writes <json>{"name":"get_weather"...}</json> as text |
12.3 s |
gpt-oss-20b |
❌ | ❌ leaks Harmony markup, then stops on max_tokens |
23.6 s |
The first four carry Mcp = 'rea' in the registry and get the flag; the last three do not, because
a catalog they cannot carry (or cannot call) only clutters the picker. They all still start
normally — the config is harmless there, it is simply not passed.
The first native call is slow on purpose. timeout_ms is 450 s in mcp\rea-mcp.json: a cold
Ghidra start is allowed 330 s before the operation itself. A file/package/JS/.NET/firmware call
returns in about a second; the first rea_open_binary on a real binary shows a spinner for a while
and then decompiles.
It needs Node.js, and Ghidra only for native work. bin\llm-mcp.ps1 resolves the Node.js that
runs REA, the rea-agents entrypoint, a JDK ≥ 21 and the unpacked Ghidra, then writes the absolute
paths into the JSON — the config is generated, never hand-maintained, because a moved path looks
exactly like "the tools vanished":
.\bin\llm-mcp.ps1 -Print # what it resolved, writes nothing
.\bin\llm-mcp.ps1 # rewrite mcp\rea-mcp.json (the bundle gets its own copy)mcp\rea-mcp.json is git-ignored: every path in it belongs to this machine, and a committed copy
would be wrong on any other. mcp\README.md explains each field.
Without the JDK/Ghidra the server still starts and every non-native tool works; rea_binary_session
reports Ghidra as unavailable instead of failing. Verified from inside a spawned server: package
6.9.0, protocol 2024-11-05, client llama.cpp/1.0, 39 tools, Ghidra available — the same tool
catalog digest the dsh-tui registration sees, so the two paths agree.
Section 11 hard-codes one route per model. When you want to serve something new — a download, a file you were handed, an HF repo — use these instead. They are model-agnostic and they read the GGUF header rather than trusting a table.
| script | what it does |
|---|---|
serve-model.ps1 |
start any model (registry key / filename / path / Ollama tag), auto-tuned; prints the base URL, model id and a curl line |
connect-dsh.ps1 |
add or replace a dsh-tui route, validate the YAML, restore the backup if it fails |
connect-claude.ps1 |
start the Anthropic shim against any port and launch Claude Code |
connect-any.ps1 |
print configs for curl, Python/Node SDKs, Continue, Cline, Open WebUI, LangChain |
llm-speccmp.ps1 |
A/B speculative decoding (off vs draft-mtp vs ngram-cache) on your own prompts |
llm-mcp.ps1 |
resolve Node/REA/JDK/Ghidra and write mcp\rea-mcp.json, the tool server llama-server spawns |
cd C:\Users\fih\llm\bin
.\serve-model.ps1 -List # geometry of everything on disk
.\serve-model.ps1 gemma4 # port + alias from the registry
.\serve-model.ps1 qwen3.6:35b-a3b-mtp-q4_K_M -SpecType draft-mtp
.\connect-dsh.ps1 -Port 8083 # -> /model in dsh-tui
.\connect-claude.ps1 -Port 8083 # -> claude, against the same endpointserve-model.ps1 derives -ngl, -n-cpu-moe and thread count from the model's own geometry via
llm-plan.ps1 -Json, so a new quant never needs hand-tuned flags to be usable. It also stops
whatever is already on the card first, because only one model fits in 8 GB.
The long-form version, with the failure modes and a troubleshooting table, is
Desktop\Local-LLM-Project\docs\connecting-local-models.md.
llama.cpp b12255 exposes:
--spec-type none,draft-simple,draft-eagle3,draft-mtp,draft-dflash,draft-dspark,
ngram-simple,ngram-map-k,ngram-map-k4v,ngram-mod,ngram-cache
draft-mtp uses a model's own multi-token-prediction head — no second model to download, but it
does need VRAM. It only exists on GGUFs that ship the head (look for -mtp in the name), and the
ngram-* modes need no draft model at all.
Measured on this machine, neither is worth using — which is why the presets do not enable them:
| mode | model | result |
|---|---|---|
draft-mtp |
Qwen3.6-35B (at -ncmoe 38) |
+18 % (69 % of drafts accepted) — but it needs 8 more CPU layers, which costs ~25 % |
draft-mtp |
Qwen3.6-35B (at -ncmoe 30-32) |
will not load: failed to allocate buffer for rs cache |
ngram-cache |
Qwen3.6-35B | −39 % to −56 %, acceptance 16-21 % |
ngram-cache |
Gemma 4 E4B | −7 % on code, 0 % on prose |
Speculation is only a win when the guess is cheap and correct. On an 8 GB card the memory it
steals is worth more than the tokens it saves. The full sweep is in
Desktop\Local-LLM-Project\docs\how-the-numbers-were-made.md Part 8.
bin\llm-desktop.ps1 builds a self-contained Desktop\Local-LLM-Project\llama.cpp:
cd C:\Users\fih\llm
.\bin\llm-desktop.ps1 # first build (~744 MB of engine, 0 bytes of models)
.\bin\llm-desktop.ps1 -SkipBinaries # refresh only the presets, hard links and fetch\It copies the CUDA build, hard-links every model into its models\ folder (same bytes, no
second copy on disk), and writes one double-clickable preset per model with the flags measured in
this document. It also drops in fetch\ - llm-fetch.ps1 plus models.manifest.json - which
is what the launcher's Download button runs, so the folder keeps working after it is copied
elsewhere. START HERE.cmd is a menu; each preset starts llama-server and opens the built-in
web UI at http://127.0.0.1:<port>.
For the models that can call tools it also writes mcp\rea-mcp.json and passes
--mcp-servers-config mcp\rea-mcp.json in those presets, so the REA tools described in section 11
travel with the folder. The path is relative, and llama-direct.exe starts the engine with the
bundle as its working directory, so moving the folder does not break it. The absolute Node/REA/JDK/
Ghidra paths inside that file are this machine's - regenerate it here after moving any of them:
.\bin\llm-mcp.ps1 -OutFile "$env:USERPROFILE\Desktop\Local-LLM-Project\llama.cpp\mcp\rea-mcp.json"The presets read their ports, aliases, context sizes and -n-cpu-moe values from the registry in
llm-up.ps1, so the bundle cannot drift from the tuned configuration — re-running the script
regenerates them.
bin\llm-direct.ps1 compiles a 26 KB WinForms window (with the .NET Framework csc.exe that
ships with Windows) and drops it in the bundle and in Desktop\Local-LLM-Project\. The model list, the download
sources and the measured flags are compiled into it, so it needs nothing else to run:
.\llm-direct.ps1Pick a model, press Start — the engine loads and the browser opens on the built-in chat UI. Open web UI / Stop / Folder, and it adopts a model that is already serving instead of starting a second one. Non-GUI use:
llama-direct.exe --start qwen36
llama-direct.exe --stop
bin\llm-verify-bundle.ps1 reads the real .cmd presets (not a copy of the flags), starts each
model the way the preset does, sends a normal prompt and a tool-shaped prompt, then checks the
process is gone and the VRAM is back:
.\llm-verify-bundle.ps1 # ~8 minutes, writes bench\bundle-verify.csvGeneration is measured after a warm-up request, because this GPU ramps from ~1.6 GHz to ~2.4 GHz over the first seconds of load — a 120-token run measures the ramp, not the model. Those two mistakes are why an early run reported 24 t/s for a model that actually does 39.
bin\test-direct-gui.ps1 and bin\test-direct-extra.ps1 drive the launcher's real window with
Win32 messages (BM_CLICK on the actual button handles) and read the status label back — UI
Automation cannot see this app's controls, so those scripts use EnumChildWindows instead.
The Ctx field in llm-up.ps1 is now the largest context the model actually loads on this
card, found by trial load — not by arithmetic:
| preset | Ctx | trained for | generation | cost vs the window it had |
|---|---|---|---|---|
gemma4 |
131072 | 131072 | 74.1 t/s | −4 % (was 32k) |
flint |
49152 | 262144 | 29.8 t/s | none |
coder |
32768 | 32768 | 50.3 t/s | −4 % |
dsv2 |
18432 | 163840 | 65.1 t/s | q8_0 KV: f16 dies at 10240, so the window needs the quantised cache |
coder36 |
57344 | 262144 | 37.7 t/s | 49152 → 57344 costs 1.3 % (A/B, 2026-11-14); n-cpu-moe 30, 27 is a CUDA OOM |
gptoss |
32768 | 131072 | 30.3 t/s | 0.0 % against 24576 over 6 loads (A/B, 2026-11-14); n-cpu-moe 15, 14 is a CUDA OOM |
qwen36 |
40960 | 262144 | 35.3 t/s | +21 % against 20480 (A/B, 2026-11-13); n-cpu-moe 31, 32 crashed once |
qwen3 |
16384 | 40960 | 59.0 t/s | — (20480 needs a q8_0 cache: it loads and then runs 5 % slower) |
bin\llm-window-ab.ps1 alternates A/B/A/B, discards a warm-up generation per arm, waits for the card
to drop below 1.5 GB before each load, and reads n_ctx back from /props. It exists because
single-shot sweeps on this laptop disagree with themselves by 30 %. Every window in this table was
confirmed by it: on 2026-11-14 a full pass measured qwen36 40.6 at 40960 and 34.6 at 49152 (keep),
coder36 38.2 at 49152 and 37.7 at 57344 (take the wider window), flint 34.0 at 49152 and 29.3 at
57344 (keep), gptoss 30.3 at both.
The table is a best-day record, so it mixes dates. bin\llm-verify-bundle.ps1 is the one thing that
sees all eight under identical conditions — cold start, 300 tokens, ramp included — and at 02:35 on
2026-11-14 it read coder 56.4, coder36 36.4, dsv2 69.1, flint 30.0, gemma4 75.0, gptoss 33.8,
qwen3 57.7, qwen36 29.7 t/s, every preset loading and releasing the GPU. Most sit below the rows
above; that is the card on that night, not the models.
To move a value: edit Ctx for that row, then
.\llm-desktop.ps1 -SkipBinaries # regenerates presets\*.cmd + START HERE.cmd
.\llm-direct.ps1 # rebuilds llama-direct.exe
.\llm-verify-bundle.ps1 # proves every preset still loads and answersA ceiling is worth nothing if decode speed dies while you use it. Every tokens/s figure in this repo above this point was taken on a short prompt — an empty window. Attention cost grows with the tokens already in the cache, so "44 t/s at 32k" was a claim about an empty 32k window.
bin\llm-stability.ps1 fills the window on purpose and reads timings.predicted_per_second back
from the server, with cache_prompt: false so nothing is reused between runs. The 512-token case
is measured twice — before and after the deep runs — because this laptop throttles (1627 MHz
idle, 2300-2600 MHz under load, 68-77 °C) and a deep run compared against whichever bookend
happened to be colder invents a loss that has nothing to do with context. The denominator below is
the mean of the two bookends; the drift column is how far apart they were.
Measured 2026-11-12, bench\stability-at-depth.csv, regenerate with
.\bin\llm-stability-report.ps1 -Markdown:
| model | window | empty (512) | 25 % of window | 60 % of window | clock drift | verdict |
|---|---|---|---|---|---|---|
| gemma4 | 131072 | 84.8 t/s | 32646 tok -> 68.4 t/s (81 %) | 78291 tok -> 52.7 t/s (62 %) | +8 % | softened/collapsed |
| coder | 32768 | 61.2 t/s | 8205 tok -> 52.4 t/s (86 %) | 19614 tok -> 48.9 t/s (80 %) | -1 % | softened/softened |
| qwen3 | 16384 | 53.2 t/s | 4108 tok -> 48.4 t/s (91 %) | 9813 tok -> 43.6 t/s (82 %) | +1 % | kept/softened |
| coder36 | 32768 | 40.0 t/s | 8186 tok -> 38.1 t/s (95 %) | 19594 tok -> 37.1 t/s (93 %) | +9 % | kept/kept |
| qwen36 | 20480 | 38.4 t/s | 5127 tok -> 38.9 t/s (101 %) | 12259 tok -> 29.7 t/s (77 %) | -16 % | kept/softened |
| flint | 49152 | 34.3 t/s | 12252 tok -> 29.6 t/s (86 %) | 29370 tok -> 28.9 t/s (84 %) | +1 % | softened/softened |
| gptoss | 24576 | 27.4 t/s | 6159 tok -> 27.2 t/s (99 %) | 14718 tok -> 25.5 t/s (93 %) | +3 % | kept/kept |
Verdicts are against the midpoint bookend: kept ≥ 90 %, softened 75-90 %, collapsed < 75 %.
What the table says:
- The MoE models barely notice.
gptosskeeps 99 % / 93 %,coder3695 % / 93 %. A sparse model reads few expert bytes per token and its KV footprint per token is small (gpt-oss 24.6 KB, qwen3.6 42 KB at f16), so filling the window is close to free. gemma4is the one that pays, and only near the top. 81 % at 32k, 62 % at 78k. The 131072 window is real — it loads and answers — but the last third of it costs a third of the speed. Use 32768 interactively, 131072 for one-shot long-document passes.flintandcodersoften by 15-20 % and stay perfectly usable;flintat 29k tokens is still 28.9 t/s with 21 GB of experts paging over PCIe.qwen36's −16 % is the machine, not the model. It ran last on the hottest machine of the sweep; against the post-run bookend it is 85 %, not 77 %, which is the honest number.- These are 5-12 % above the
bench\bundle-verify.csvfigures in the ceiling table because that harness counts an end-to-end HTTP round trip and tokenisation; this one reads the server's own decode timer. Same direction, different quantity.
Prefill is not linear either. gemma4 swallows 78291 tokens at 2084 t/s (38 s), but flint pages
29370 tokens at 219 t/s (134 s) and qwen36 12259 at 325 t/s — on an expert-split model the first
prompt of a long session is the slow part, not the answer.
bin\llm-maxctx.ps1 extrapolates from free VRAM. Do not trust it on this machine. With LM
Studio, Magnitude and Steam resident, free VRAM at 16384 is only 200-300 MB, so the extrapolation
goes negative and proposes contexts smaller than the ones already working (it suggested 14336 for
qwen3 and 11264 for gpt-oss, both of which serve 16384 today). Free VRAM is a residual, not a
budget. It did it again for coder36: it proposed 28672, and walking candidates upward served
32768 at the same -n-cpu-moe 26 — 4096 tokens it left on the table.
Use bin\llm-ctxgrow.ps1 instead — it walks candidates upward and keeps the last one that served a
real completion:
.\llm-ctxgrow.ps1 -Model qwen36 -Candidates 16384,20480,24576,32768 -Out ..\bench\ctxgrow-qwen36.csvNote -Candidates is [string] on purpose: pwsh -File script.ps1 -Candidates 1,2,3 hands the
script a single string "1,2,3", so it splits on [,\s]+ itself. /health answering is not proof
of a working context — qwen36 at 24576 reports healthy and then fails its first completion, which
is why the probe sends a real request.
With the background apps closed there is 6.4 GB free instead of ~4, and the same models can go higher. Measure again after closing them if you want the true ceiling.
dsv2 is AmSach's MAPLE Sub5GB
build of DeepSeek-Coder-V2-Lite-Instruct: 13.5B parameters of which 2.4B fire per token, pruned from
64 to 54 experts per layer (MAPLE) and quantised to 2.87 bits per weight. 4.50 GB on disk, and the
plan says 0.88 GB read per token — a dense model of the same size would read 5.1× more.
Two numbers decide how it is served here.
KV is the whole problem. This architecture stores 276 KB of cache per token, so the model plus an f16 window runs out of 8 GB almost immediately:
| window | KV type | result |
|---|---|---|
| 8192 | f16 | serves, 58.0 t/s (A/B median) |
| 10240 | f16 | loads, then dies on the first completion |
| 18432 | q8_0 | serves, 65.1 t/s (A/B median, +12.2 %) |
| 19456 | q8_0 | reports healthy, then closes the connection mid-completion |
That is the one place where a quantised KV cache is the right answer on this machine: not because it
is faster per token — qwen3 measures 5.4 % slower with q8_0 at the same window — but because
attention reads half the cache and that buys 10240 tokens of window. bench\window-ab-dsv2.csv.
The model itself is damaged. The same 36-item capability suite that scores everything else 68-100 % scores it 17.7 %:
| math | logic | code | instr | json | needle | tools | knowledge |
|---|---|---|---|---|---|---|---|
| 1/6 | 0/5 | 1/4 | 1/6 | 2/3 | 0/3 | 0/3 | 1/6 |
It is not the KV cache. The failures are reproducible with an f16 cache at 8192, and they are not
subtle: it calls Saturn the largest planet (then says it is "larger than only Jupiter"), says plants
absorb oxygen and release carbon dioxide, answers the roses syllogism with (B) instead of (C), and
degrades into loops (Reduced stress levels ×8 in one instruction item). bench\eval-dsv2.json keeps
every reply.
So it ships, because it is the cheapest way to run a 13.5B coder on this card — 65 t/s at 18432 in
4.5 GB, with no --n-cpu-moe at all — and it is labelled everywhere as an experiment, not an agent
backend. If you want code you can trust, coder36 (37.7 t/s, 57344 window, native tools) is the
answer; dsv2 is 1.7× its speed and roughly a third of its score.
The GGUFs are 79 GB and none of them belong in git. models.manifest.json is the catalogue and
bin\llm-fetch.ps1 is the fetcher; the launcher's Download button is a thin wrapper around it.
| key | repo | file | size | provenance |
|---|---|---|---|---|
gemma4 |
unsloth/gemma-4-E4B-it-qat-GGUF |
gemma-4-E4B-it-qat-UD-Q4_K_XL.gguf |
4.22 GB | byte-identical (sha256 match) |
flint |
RecursiveSI/flint-nvfp4-gguf |
flint-nvfp4.gguf |
22.86 GB | byte-identical (sha256 match) |
qwen36 |
unsloth/Qwen3.6-35B-A3B-MTP-GGUF |
Qwen3.6-35B-A3B-UD-Q4_K_S.gguf |
21.39 GB | nearest published size (local: 21.72 GB) |
gptoss |
ggml-org/gpt-oss-20b-GGUF |
gpt-oss-20b-MXFP4.gguf |
12.11 GB | canonical MXFP4 (local: 13.79 GB) |
coder36 |
ManniX-ITA/Qwen3.6-27B-A3B-Coder-MTP-GGUF |
Qwen3.6-27B-A3B-Coder-Q4_K_M.gguf |
16.06 GB | byte-identical — the publisher ships a .sha256 beside every quant |
qwen3 |
Qwen/Qwen3-8B-GGUF |
Qwen3-8B-Q4_K_M.gguf |
5.03 GB | official, same name (local: 5.23 GB) |
coder |
Qwen/Qwen2.5-Coder-7B-Instruct-GGUF |
qwen2.5-coder-7b-instruct-q4_k_m.gguf |
4.68 GB | official, same size (sha differs by 512 bytes) |
Provenance was established by hashing the seven local files and comparing against the LFS sha256
that the Hub API reports for every sibling of a candidate repo — and by reading fsutil hardlink list on each file, which names the blob it was hardlinked from. Three matched exactly — and for
coder36 there was nothing to interpret at all, because the uploader publishes a .sha256 file next
to every quant, so a 15 GB download can be proved rather than assumed. The rest were
downloaded months ago from uploaders that have since re-quantised or vanished; for those the
catalogue points at the closest published equivalent and records the sha256 of the copy that was
actually benchmarked. Anywhere the sizes differ, the downloaded file is smaller, which is the safe
direction: it still fits at the tuned -n-cpu-moe.
bin\llm-fetch.ps1 -List # table: key, file, size, present/missing
bin\llm-fetch.ps1 -Missing # fetch only what is absent
bin\llm-fetch.ps1 -Key gemma4 # one model
bin\llm-fetch.ps1 -All -DryRun # resolve the repos, transfer nothing
bin\llm-fetch.ps1 -Key qwen36 -HighPerformance # more xet ranges, more of the line
bin\llm-fetch.ps1 -Token hf_xxx # this run only, via HF_TOKEN
bin\llm-fetch.ps1 -List -Json # machine-readableEach model is downloaded into %TEMP%\hf-stage-<key> with
hf download <repo> <repoFile> --type model --local-dir <stage>
and only moved into models\ — renamed to the exact filename the registry expects — once the
transfer finished. Two consequences: an interrupted download resumes where it stopped (the staging
folder survives), and models\ never contains a half-written file that the launcher would happily
try to load.
hf_xet is installed, so transfers go through the xet backend: many ranges over many connections
rather than one HTTP stream. Measured on this line, same 94 MB file, back to back:
| transport | time | rate |
|---|---|---|
plain HTTP (HF_HUB_DISABLE_XET=1) |
310 s | 0.30 MB/s |
| xet (default) | 131 s | 0.72 MB/s |
2.4× on a deliberately hostile line. The absolute numbers are the line's fault, not xet's, and both
runs hit a dropped connection and resumed — which is itself the answer to "does the resume work".
-HighPerformance sets HF_XET_HIGH_PERFORMANCE=1 for more parallel ranges; it costs CPU.
Public files download without one, but HF throttles anonymous traffic and refuses gated repos.
HF token... in the launcher opens a masked box and writes what you paste to
%USERPROFILE%\.cache\huggingface\token, which is the file every HF tool reads — so the token works
for hf from the command line too, not just for this app. It is never put on a command line and
never logged. hf auth whoami confirms which account it belongs to.
llama-direct.exe --list # on disk / missing, with the repo each would come from
llama-direct.exe --download gemma4 # fetch one model, blocks, prints live progress
llama-direct.exe --start qwen36 # load a model, return immediately
llama-direct.exe --stop # free the GPU
llama-direct.exe is a /target:winexe, so it has no console of its own; it calls AttachConsole
to borrow the caller's when given a command line, and stays silent when double-clicked. Models
missing from models\ appear greyed out in the window with their size, and the Download button is
disabled for anything already present — a working copy from a different uploader counts as present.
Speed says how fast an answer arrives. It says nothing about whether the answer is right — and on this machine the fastest big model is not the smartest, because speed here is decided by bytes read per token rather than by parameter count.
bin\llm-eval.ps1 runs a 36-item battery over eight categories — maths, logic, code, instruction
following, JSON extraction, long-context retrieval, tool calling, and factual knowledge — and grades
every answer with a program: exact values, regex predicates, JSON schemas, and for the code items by
executing unit tests the model never sees. There is no judge model, and CI proves on every push
that the graders still accept their own gold answers.
.\bin\llm-eval.ps1 # all eight models
.\bin\llm-eval.ps1 -Models coder36 # one model
.\bin\llm-eval.ps1 -Report # re-aggregate what is in bench\, run nothingMeasured here on 2026-11-12 (and dsv2 on 2026-11-14), all eight models, 36 items each:
| model | macro | the short version |
|---|---|---|
| flint | 100.0 | did not miss an item; 29.8 t/s, 49k context |
| gemma4 | 97.9 | 4B, 131k context, 74.5 t/s — the best speed/capability point |
| qwen3 | 97.9 | 8B, thinks, still lands every category but one |
| coder | 95.8 | 4/4 code, 3/3 tools, 50.3 t/s |
| coder36 | 85.4 | 5 items hit the token ceiling, including two code items with an empty reply |
| qwen36 | 78.1 | 8 items hit the ceiling; 2/6 instruction following at these budgets |
| gpt-oss | 67.7 | 0/3 tools and 1/6 instructions — its analysis channel leaks into the content field on this build |
| dsv2 | 17.7 | loads, runs at 65 t/s, and gets Saturn and photosynthesis wrong — see §14 before using it |
Rank correlation between speed and capability across these eight: 0.2. The fastest model is not the
smartest, and the slowest is not either. The full results, the method, and what the battery cannot tell
you are in docs/capability-evaluation.md. Read that before quoting a
number: six items per category means wide intervals, and a model that thinks for long enough hits its
token budget before it writes an answer, which is reported as a budget hit rather than quietly
scored as wrong.
