Ringarc. Book free AI Audit

Labs · Tech blog

Ollama vs llama.cpp vs vLLM on one 16GB desktop card

Vikas Goenka · 7 August 2026

I run open-source models on my desktop, a machine with an RTX 5070 Ti and 16GB of VRAM. Everyone starts with Ollama because it's one command. The enthusiasts swear by llama.cpp. The serious deployment people run vLLM. I knew the engines perform differently, but I wanted real numbers: same model, same GPU, how big is the gap actually?

So I spent a few evenings benchmarking all three. Same prompts, same harness, same machine, one engine on the GPU at a time. I started with two models and realised partway through that the lineup was missing a third case, so I added it later. Glad I did, because it changed the conclusion.

The contenders

And the models are the real trick. Three of them, covering the three situations you can be in with a 16GB card:

One honest detail before the numbers. My desktop session stayed running the whole time, which eats about 1GB of VRAM. That's the box as I actually use it, and that 1GB ends up playing a real part in the story.

Round one: the model that fits, snugly

Everything runs. Nobody crashes. And yet:

gpt-oss-20bTTFTPrefill (long)Decode8 clients
Ollama0.25 s4,160 tok/s166 tok/s152 tok/s
llama.cpp0.07 s7,490 tok/s226 tok/s505 tok/s
vLLM0.25 s2,010 tok/s33.5 tok/s129 tok/s

Same weights, same quantization, three wildly different outcomes. And yes, the datacenter engine really did 33 tokens a second. Seven times behind llama.cpp, on identical weights.

Here's what happened. vLLM couldn't load the model at all at first. Three out-of-memory crashes, because 13.8GB of weights plus its working space didn't fit next to my desktop's 1GB. The only way to get it running was telling it to park 3GB of the weights in system RAM. And the catch is that vLLM still does all its math on the GPU, so those parked gigabytes travel across the PCIe bus for every single token. VRAM moves data at roughly 900GB a second. PCIe moves it at 64. Generating tokens is basically a memory bandwidth game, and vLLM was forced to play it across the slow bus.

Hold that thought, because round three shows what this engine does when nothing is choking it.

llama.cpp beating Ollama by 36%, despite Ollama wrapping the same engine? Two boring reasons: a current build with kernels compiled for this exact GPU generation, and explicitly configured settings instead of shipped defaults. The convenience layer costs about a third of your single-user speed on this model. Now it's a measured number instead of a rumour.

Round two: the model that doesn't fit

An 18.6GB model on a 16GB card means something has to give, and each engine gives differently.

Qwen3-Coder-30BTTFTPrefill (long)Decode8 clients
Ollama0.37 s1,164 tok/s67 tok/s60 tok/s
llama.cpp0.40 s1,100 tok/s71 tok/s139 tok/s
vLLM0.21 s2,580 tok/s59 tok/s179 tok/s

llama.cpp wins the single-user case again, and the way it does it is genuinely clever. Qwen3-Coder is a mixture-of-experts model. Every layer has 128 small expert networks, and each token only wakes up 8 of them. llama.cpp has a flag built for exactly this shape, --n-cpu-moe. Instead of streaming weights to the GPU, it sends the work to the weights: the expert layers live in system RAM and the CPU computes them right there, while the bandwidth-hungry part, attention and the cache, stays on the GPU. Since each token only wakes a few experts, the CPU barely works. An 18GB model doing 71 tokens a second on a 16GB card is, honestly, amazing.

Ollama handled the oversized model without being asked, it auto-splits between GPU and CPU. 67 tokens a second, perfectly usable. vLLM's generic offload managed 59 single-stream but again won time-to-first-token, prefill, and the 8-client column. A hint of things to come.

Round three: the model that fits comfortably, and the story flips

Gemma 4 12B leaves 6 or 7GB of headroom. For the first time, vLLM loaded natively: no offload, no compromises, all its machinery switched on.

Gemma 4 12BTTFTPrefill (long)Decode8 clients
Ollama0.33 s2,203 tok/s87 tok/s78 tok/s
llama.cpp0.13 s2,584 tok/s91 tok/s232 tok/s
vLLM0.05 s3,366 tok/s81 tok/s419 tok/s

Single user, the three are close: 91, 87, 81. You'd struggle to feel the difference in a chat window. But look at the 8-client column. vLLM served 419 tokens a second, nearly double llama.cpp and more than five times Ollama. The same burst of 8 requests took vLLM 4.9 seconds, llama.cpp 8.8, and Ollama 26.3. Sit with that spread for a second: same GPU, same model, and one engine returns the whole batch five times sooner than another.

That's what vLLM is for. Its poor showing in round one wasn't the engine, it was the offload. Give it resident weights and parallel traffic, and you can see exactly why datacenters run it.

The Ollama result deserves its own section

Across all three rounds, one pattern repeated: 8 clients got the same total throughput as 1. Out of the box, Ollama serves requests one at a time. It's not broken, it's a default, and it's fixable with a single environment variable, OLLAMA_NUM_PARALLEL. But I suspect almost nobody sets it, which means almost every multi-user Ollama box out there is quietly serving people from a queue. If your Ollama serves more than one person, or one person who fires parallel requests from scripts, set it today. It's the cheapest performance win in this whole post.

So which engine?

That's the wrong question, and getting the wrong question out of the way was the real result of these evenings. The right question is: does your model fit, and how many people are hitting it?

On my own box, llama.cpp is now the serving engine behind everything single-user, Ollama stays as the quick way to pull and try models, and vLLM earned the job I originally doubted it could do here: batch work. My model eval suite fires eight requests at a time, which is exactly the traffic vLLM eats for breakfast.

Fine print

Same machine throughout: RTX 5070 Ti 16GB, Ryzen 7 9700X, 128GB DDR5, one engine on the GPU at a time, temperature 0, streamed responses, every prompt uniquely tagged so prefix caching couldn't cheat on prefill. Cross-engine model files aren't byte-identical where noted: the Qwen round compares Q4_K_M against AWQ, and vLLM's Gemma checkpoint is Google's official quantized release at 10.3GB against the 7.4GB file the others served, which explains part of its single-stream gap, decode speed follows bytes. vLLM ran handicapped in rounds one and two (eager mode, short context, forced by the memory squeeze) and unhandicapped in round three. And a headless box would hand every engine back my desktop's 1GB, which would likely have turned round one's "can't load" into "loads, barely". If you want the deeper writeup on how GGUF, expert offload, and paged attention actually work under the hood, tell me, that's a post of its own.

Comments

Sign in with GitHub to join the discussion. Comments are stored as GitHub Discussions.

More from the Labs

I benchmark open models against frozen frontier anchors on my Open Model Benchmark, and the next local-inference round lands on this blog. RSS is the reliable way to catch both.

Explore the Labs →