Local LLM inference has converged on three stacks that are constantly confused with each other: llama.cpp, Ollama, and vLLM. They are not competitors at the same layer โ two of them literally wrap or complement each other. This guide pins down what each one is, where the concurrency and memory models diverge, and which to pick per scenario.
What each layer actually is
llama.cpp is the C/C++ inference engine: a GGUF-native runtime with backends for CUDA, Metal, Vulkan, ROCm, and plain CPU, plus llama-server, a built-in HTTP server with OpenAI- and Anthropic-compatible routes, continuous batching, and parallel decoding.1
Ollama is a model distribution and runtime layer, not an engine of its own (mostly). Historically it wrapped a pinned llama.cpp build โ the LLAMA_CPP_VERSION variable in the Ollama repo pins the exact upstream source, patched via llama/compat/.2 On top of that pin Ollama has added its own engines: a multimodal engine that runs vision models (Llama 4, Gemma 3, Qwen 2.5 VL) directly on the GGML tensor library instead of gluing separate text-decoder and vision-encoder processes together,3 and an MLX-based engine for Apple Silicon that Ollama put into preview in March 2026, replacing the llama.cpp Metal path for accelerated models.4
vLLM is a Python/CUDA serving engine built around PagedAttention: KV-cache memory managed OS-style in fixed-size blocks with logical-to-physical block tables, giving near-zero cache waste and 2โ4ร throughput over the systems it was benchmarked against in the original paper.5 Since the V1 re-architecture it enables prefix caching by default (under 1% throughput loss at 0% hit rate) and reaches up to 1.7ร higher throughput than V0.6
| Layer | llama.cpp | Ollama | vLLM |
|---|---|---|---|
| What it is | C/C++ GGUF engine + llama-server HTTP | Runtime + model registry wrapping pinned llama.cpp, own multimodal engine, MLX engine on Apple Silicon | Python/CUDA serving engine, PagedAttention KV manager |
| Model format | GGUF (native) | GGUF (+ MLX-native on Apple Silicon) | HF safetensors (GGUF via experimental plugin) |
| Primary target | Single box, any hardware incl. CPU | Ease of use, ollama run | GPU server throughput |
| Concurrency | Fixed slots (--parallel N) | OLLAMA_NUM_PARALLEL slots | Continuous batching over one paged KV pool |
Concurrency models: fixed slots vs a shared pool
This is the single biggest behavioral difference, and the source of the worst gotcha.
llama-server: --parallel N fixed slots, --ctx-size split across them
llama-server -np N creates N slots. Current master defaults: -np/--parallel default is -1 (auto) and -cb/--cont-batching (continuous batching) is enabled by default; -b/--batch-size defaults to 2048 and -ub/--ubatch-size to 512. --ctx-size (default 0 = loaded from model) is the total context, and the KV budget is divided across slots.1
The gotcha: --ctx-size is the total, not per-slot. With -c 8192 -np 4, every slot gets 8192/4 = 2048 tokens of KV space โ your 8k prompt silently truncates or errors. If you actually want 8k per slot across 4 slots, you must pass -c 32768 -np 4. This has tripped up enough users that upstream added an issue-and-fix cycle and eventually a per-slot control (ctx-per-slot / --kv-unified-per-slot on current master) to set the per-slot context limit directly.7
vLLM: one paged KV pool, continuous batching
vLLM keeps a single shared KV pool and allocates blocks on demand; requests join and leave the batch at every decode step. Raising concurrency costs nothing when the server is idle because no per-request context is pre-reserved: a request's KV cache grows token-by-token in physical blocks, freed immediately when the request finishes.5 --max-num-seqs limits batch size; the true limit on concurrent requests is "how many sequences fit their accumulated KV into the pool."
Ollama: slots like llama.cpp, sized by defaults
Ollama inherits the slot model: OLLAMA_NUM_PARALLEL (default 1) is the number of parallel requests each model processes, and required RAM scales by OLLAMA_NUM_PARALLEL * OLLAMA_CONTEXT_LENGTH.8 With the default of 1, concurrent requests queue; each additional slot multiplies the KV allocation just like --parallel does in llama-server.
GGUF: native home vs experimental guest
GGUF is llama.cpp's native format โ the reference implementation, with the full quant family (Q2_K through Q8_0, i-quants, imatrix calibration).9
vLLM's GGUF support is a different story, in its own docs' words: "GGUF support in vLLM is highly experimental and under-optimized at the moment, it might be incompatible with other features." GGUF loading has migrated out of core into the OOT vllm-gguf-plugin package (uv pip install vllm-gguf-plugin), only single-file GGUFs are supported, and the docs recommend passing the base model's tokenizer explicitly, e.g.10:
vllm serve unsloth/Qwen3-0.6B-GGUF:Q4_K_M --tokenizer Qwen/Qwen3-0.6BPractical rule: on vLLM, GGUF is a memory-footprint reduction, not a performance path. For high-throughput serving use safetensors with a first-class quantization scheme (AWQ, GPTQ, FP8); use GGUF only when that model exists only as GGUF.
Defaults that bite
Ollama context defaults are VRAM-tiered. Current docs: < 24 GiB VRAM โ 4k context; 24โ48 GiB โ 32k; โฅ 48 GiB โ 256k, overridable with OLLAMA_CONTEXT_LENGTH (older releases used a flat 4096 default, and 4096 is still what the FAQ's override examples show).11 If your 24 GB card picks a 32k default you didn't ask for, KV cache grows accordingly โ check ollama ps (its CONTEXT column) and set num_ctx per request or the env var globally.
Ollama KV cache type defaults to f16. OLLAMA_KV_CACHE_TYPE accepts f16 (default), q8_0 (~half the KV memory, tiny quality loss), or q4_0 (~quarter, more noticeable loss) โ and requires flash attention to be enabled to take effect.8
OLLAMA_NUM_PARALLEL defaults to 1 โ simultaneous requests queue rather than run concurrently, and raising it multiplies KV memory by N.8
vLLM gpu_memory_utilization defaults to 0.9 โ vLLM claims up to 90% of the card's total memory at startup (per-instance, not per-process-coordinated), then subtracts weights, activation peak, and CUDA-graph pools; the KV cache is the residual. On a 24 GB card shared with a display or other processes, 0.9 will OOM at init โ lower it (0.80โ0.85) or pin --max-model-len.12
Worked example: 14B Q4_K_M, 4 slots ร 8k, on a 24 GB card
Take Qwen2.5-14B (48 layers, GQA with 8 KV heads, hidden size 5120 across 40 heads โ head dim 128, 14.7B total parameters).13 We compute the budget using the site's KV formula (see the KV cache guide and inference math), plugging in llama.cpp's measured Q4_K_M โ 4.85 bits-per-weight for this weight class.9
Weights at 4.85 bpw:
KV cache per token (f16, 2 bytes/value), using :
Per slot at 8192 tokens, and total for 4 slots:
| Component | Size |
|---|---|
| Weights (Q4_K_M, 4.85 bpw) | 8.30 GiB |
| KV cache, 4 ร 8k slots, f16 | 6.00 GiB |
| Subtotal | 14.30 GiB |
| Headroom on a 24 GB card (24 decimal GB โ the 14.30 GiB subtotal = 15.35 GB) | ~8.65 GB |
It fits โ with room to spare, which is the point: on a 24 GB card a 14B Q4_K_M with 4 concurrent 8k conversations is comfortably inside budget, and the ~8.65 GB headroom โ 24 decimal GB on the sticker minus the 14.30 GiB subtotal (15.35 decimal GB) โ covers compute scratch (dependent on batch size and backend) plus the CUDA context. Two levers if you need more slots or context: -ctk q8_0 -ctv q8_0 halves KV to 3.0 GiB (llama-server flags, f16 default1), the same trade Ollama's OLLAMA_KV_CACHE_TYPE=q8_0 makes. For a full sizing workflow see the VRAM calculator explained and quantization guide; the interactive tool is at LLM inference calculator.
And remember the gotcha from above: to actually get 8k per slot you'd start llama-server -m model-Q4_K_M.gguf -c 32768 -np 4, not -c 8192.
Decision table
| Scenario | Pick | Why |
|---|---|---|
| Laptop / desktop, one user at a time | Ollama | One command to a running model; VRAM-tiered defaults, model registry, auto GPU split via llama.cpp under the hood |
| Mac on Apple Silicon, fastest decode | Ollama (MLX engine) | MLX path built for unified memory; preview requires >32 GB unified memory and currently accelerates specific models4 |
| Max control: KV quant, slot tuning, odd hardware, CPU inference | llama.cpp (llama-server) | Direct access to every flag (-ctk, -np, -c, --override-tensor), GGUF-native, runs on CPU where vLLM GGUF does not |
| Many concurrent users, one GPU or many, latency SLO | vLLM | Paged KV pool + continuous batching scale with load; prefix caching on by default6 |
| Production API with structured output / tool use at high QPS | vLLM | First-class serving features, tensor parallelism across GPUs, FP8/AWQ quantization support |
| Multimodal (vision) locally | Ollama or llama-server | Ollama's multimodal engine makes vision first-class3; llama-server's --mmproj covers GGUF vision models1 |
| Serving an HF-only model as GGUF | llama.cpp or Ollama | vLLM's GGUF path is 'highly experimental', single-file only, plugin-based10 |
Rule of thumb: llama.cpp when you need to touch the knobs or fit weird hardware; Ollama when you want zero-friction local use; vLLM when other people or services are hitting the endpoint and throughput per GPU matters.
Footnotes
-
ggml-org/llama.cpp, tools/server/README.md (master): https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md โ
-np/--paralleldefault -1 (auto),-cb(continuous batching) default enabled,-bdefault 2048,-ubdefault 512,-c/--ctx-sizedefault 0 (loaded from model). โฉ โฉ2 โฉ3 โฉ4 -
ollama/ollama, llama/README.md:
LLAMA_CPP_VERSIONpins Ollama's llama.cpp source: https://github.com/ollama/ollama/blob/main/llama/README.md โฉ -
Ollama blog, "Ollama's new engine for multimodal models" (May 15, 2025): https://ollama.com/blog/multimodal-models โฉ โฉ2
-
Ollama blog, "Ollama is now powered by MLX on Apple Silicon in preview" (March 2026): https://ollama.com/blog/mlx โฉ โฉ2
-
Kwon et al., "Efficient Memory Management for Large Language Model Serving with PagedAttention", SOSP 2023, arXiv:2309.06180: https://arxiv.org/abs/2309.06180 โฉ โฉ2
-
vLLM blog, "vLLM V1: A Major Upgrade to vLLM's Core Architecture" (2025-01-27): up to 1.7ร throughput vs V0; prefix caching on by default with \<1% throughput loss at 0% hit rate: https://blog.vllm.ai/2025/01/27/v1-alpha-release.html โฉ โฉ2
-
llama.cpp issue #11681 ("--ctx-size is divided by --parallel") and PR #24124 (commit 1844325) / release b10662 (
ctx-per-slot,--kv-unified-per-slot): https://github.com/ggml-org/llama.cpp/issues/11681 โฉ -
Ollama FAQ:
OLLAMA_NUM_PARALLELdefault 1, RAM scales withOLLAMA_NUM_PARALLEL * OLLAMA_CONTEXT_LENGTH;OLLAMA_KV_CACHE_TYPEdefault f16 with q8_0/q4_0 options: https://docs.ollama.com/faq โฉ โฉ2 โฉ3 -
ggml-org/llama.cpp, tools/quantize/README.md: Q4_K_M = 4.8944 bpw on Llama-3.1-8B (โ4.85โ4.9 bpw weight class): https://github.com/ggml-org/llama.cpp/blob/master/tools/quantize/README.md โฉ โฉ2
-
vLLM docs, GGUF feature page: "highly experimental and under-optimized", migrated to OOT vllm-gguf-plugin, single-file GGUF only, base-model tokenizer recommended: https://github.com/vllm-project/vllm/blob/main/docs/features/quantization/gguf.md โฉ โฉ2
-
Ollama docs, Context length: <24 GiB VRAM โ 4k, 24โ48 GiB โ 32k, โฅ48 GiB โ 256k,
OLLAMA_CONTEXT_LENGTHoverride: https://docs.ollama.com/context-length ; older flat 4096 default per https://docs.ollama.com/faq โฉ -
vLLM EngineArgs:
gpu_memory_utilizationdefault 0.9, "fraction of GPU memory to be used for the model executor", per-instance: https://docs.vllm.ai/en/latest/api/engine_args.html (vllm/config/cache.py, default=0.9) โฉ -
Qwen2.5-14B-Instruct model card (48 layers, GQA 40 Q / 8 KV heads, 14.7B params) and config.json (hidden_size 5120, num_key_value_heads 8): https://huggingface.co/Qwen/Qwen2.5-14B-Instruct โฉ