**Decode is llama.cpp's.** Prefill is ours by ~2×, which is the shape one expects:
vLLM's prefill goes through oneDNN/AMX bf16 GEMMs at large batch, where
llama.cpp's advantage in per-step overhead does not apply.
Not a matched-precision comparison in the strict sense: Q4_K_M stores **4.5
bits/weight** over groups of 32 with higher-bit types for some tensors, against
our **4.25** over groups of 128. It buys fidelity with those bits (below).
### Why llama.cpp wins decode
Both engines are far from the hardware limit. A node-local streaming read on the
same 24 cores sustains 222 GB/s:
| | GB/s | of ceiling |
|---|---|---|
| llama.cpp whole step | 86 | 39% |
| this fork, whole step | 73 | 33% |
So the gap is not bandwidth, and it is not primarily kernel quality — it is the
execution model. `ggml_threadpool` starts threads once and they walk the entire
graph together, synchronising at `ggml_barrier` through an atomic spin. vLLM
enters a separate `omp parallel` region inside each kernel, and a region costs
~1.2 µs at 24 threads. See [ROADMAP.md](../../ROADMAP.md) item I10 for the
measurements that size this and the reason it is not the next thing to build.
### Fidelity
Against each build's own bf16 output, 12 greedy prompts, exact reproductions:
| | | bits/weight |
|---|---|---|
| llama.cpp Q4_K_M | 4/12 | 4.5 |
| W4A16 | 2/12 | 4.25 |
| W4A8 | 1/12 | 4.25 |
Some of that gap is simply the extra quarter-bit and the finer grouping. How
much is unknown, which is why closing the budget gap (g64, or fp16 scale+zero —
both config changes) comes before implementing a better quantizer.
This metric also saturates: it discards everything after the first divergent
token. `benchmarks/edge_harness/fidelity_kl.py` replaces it with top-1 agreement
and KL over a corpus, deliberately symmetric with `llama-perplexity
--kl-divergence` so llama.cpp stays comparable on our metric rather than being
judged on a different one.
## Other engines considered
Full analyses in [docs/engines/](../engines/). In brief, for this workload:
| engine | verdict |
|---|---|
| **llama.cpp** | the CPU baseline; best decode, mature quantization, no multimodal pipeline |
| **ExecuTorch** | the phone path — exported artifacts, XNNPACK on CPU, QNN for NPU; not a server engine |
| **MNN** | strong mobile CPU/GPU, own model format, another export toolchain |
| **ncnn** | mobile-first, no LLM serving story of the kind needed here |
| **vLLM / vLLM-Omni** | the only one with a multi-stage multimodal pipeline, continuous batching and an OpenAI-compatible server — which is why the edge work is a fork of it rather than a port away from it |