vLLM vs SGLang, plus Ollama and llama.cpp compared

vLLM vs SGLang compared on prefix caching, hardware, and constrained decoding, plus Ollama and llama.cpp. Every default checked November 8, 2026.

Published
Updated
Reading
12 min
Level
Intermediate
By
AI for Devs
TL;DR
  • For vLLM vs SGLang, start with vLLM when you need its documented 200+ model architectures, and with SGLang when most requests share long prefixes. Both run on NVIDIA and AMD GPUs with prefix caching on by default as of November 8, 2026.
  • vLLM v0.33.0 reuses prefixes in full 16-token blocks, while SGLang v0.5.23 matches at one-token pages. On a 4,000-token shared prompt, that gap is at most 15 tokens.
  • Ollama runs one request per model by default. Going from one to four llama-server slots raised throughput 3.2 to 3.8 times on four concurrent requests in this article's test.
  • llama-server now listens on port 9931 by default, so older guides that assume 8080 will send your client to the wrong port.
  • Tested here: llama.cpp build b12202, compiled from source on an Apple M2. vLLM v0.33.0, SGLang v0.5.23, and Ollama v0.40.15 were not run, and their commands come from current docs.

vLLM and SGLang are open-source inference engines that run open-weight models on GPU servers behind OpenAI-compatible APIs. The vLLM vs SGLang decision usually turns on hardware, cache behavior, and which engine supports your model on the day you need it. Ollama and llama.cpp solve a different problem: one machine, few users, and quantized weights. This comparison checks each engine's defaults against its own docs and release notes as of November 8, 2026.

#vLLM vs SGLang at a glance

Start with vLLM if you want the safest default for multi-user GPU serving, because its README documents 200+ model architectures and a long list of hardware plugins. Start with SGLang if agent loops and multi-turn chat dominate your traffic, because its radix cache matches prefixes at token granularity. Both are Apache-2.0.

vLLM v0.33.0SGLang v0.5.23llama-server b12202Ollama v0.40.15
Built forMulti-user GPU servingMulti-user GPU servingOne binary on most hardwareA local daemon with a model library
Default port800030000993111434
WeightsHugging Face; GGUF through a pluginHugging Face; a gguf load formatGGUFGGUF and Safetensors import
Prefix cacheOn, 16-token blocksOn, one-token pagesOn by default (cache_prompt)Not described in the FAQ
Concurrent requestsContinuous batchingBatch scheduler, FCFS by defaultSlots, auto (4 here)1 per model, 512 queued
LicenseApache-2.0Apache-2.0MITMIT

All four expose OpenAI-compatible routes, so moving between them is usually a base URL change. Extras such as sampling options and structured output fields differ, and the inference platforms directory lists hosted options if you would rather not run any of them.

#SGLang vs vLLM on prefix caching

Both engines keep KV cache entries from earlier requests and skip prefill for any prefix they have already seen. vLLM finds those entries with a chain of block hashes, and SGLang finds them by walking a radix tree of token IDs. The structures differ in match granularity, eviction choices, and what you can tune.

vLLM reuses only full 16-token blocks, so two requests sharing 28 tokens reuse 16. SGLang's radix tree at page size 1 reuses all 28.vLLM: hash chain over full blocks, 16 tokens each in v0.33.0Req Ablock 0, cachedh0 = hash(tokens 0..15)block 1, cachedh1 = hash(h0, 16..31)8 leftA partial blockis never cachedhash matches h0Req Bblock 0, reused16 tokensblock 1, prefilleddiffers at token 28Reuses 16 of28 shared tokensSGLang: radix tree over token IDs, page size 1 by defaultrootshared edgetokens 0 to 27, reused by A and BReq A branchtokens 28 to 39, cachedReq B branchnew tokens only, prefilledReuses all 28 shared tokensreused KVcached branchprefilled nownot cached
Fig. 1vLLM only reuses complete blocks, so two requests sharing 28 tokens reuse 16 of them. SGLang's tree stores one token per page by default and reuses all 28.

#vLLM: hashed blocks

vLLM's design doc hashes each full block together with its parent block's hash, so a block's identity depends on everything before it. It caches full blocks only. Eviction takes the least recently used block from a free queue.

The block size is 16 tokens in v0.33.0's CacheConfig, where enable_prefix_caching defaults to true. Pull request 63231, merged on November 5, 2026, raises the default on main to 64 and lists coarser prefix hits as a trade-off. Check the block size again when you upgrade.

Per-request cache_salt values feed the first block's hash, which isolates tenants that should never share cached prefixes.

#SGLang: a radix tree

SGLang's RadixAttention keeps KV cache for prompts and generations in a radix tree, mapping token sequences to cache tensors on the GPU while the tree lives on the CPU. The current server arguments set --page-size to 1 and leave --disable-radix-cache off, so token-level reuse is on by default.

The scheduler is less cache-driven by default than older write-ups suggest. --schedule-policy defaults to fcfs, and the longest-prefix-match policy lpm is something you opt into. Eviction defaults to LRU over leaf nodes, with lfu, slru, priority, and tlru policies for skewed traffic.

#Granularity matters most for short prefixes

The gap between the two designs is smaller than a diagram suggests. A vLLM block of 16 tokens forfeits at most 15 tokens per request, which is 0.4% of a 4,000-token shared prompt. At the new 64-token default, the worst case is 63 tokens, or 1.6%.

Granularity matters when shared prefixes are short, or when many branches diverge just past a block boundary. Measure it instead of guessing. vLLM exposes vllm:prefix_cache_queries and vllm:prefix_cache_hits in its metrics, and SGLang's --enable-cache-report adds cached token counts to each response. To raise hit rates on either engine, put stable content first.

#Concurrent requests: batches, slots, and a queue

The engines differ most in what happens when four requests arrive at once. vLLM and SGLang batch them into shared GPU steps. llama-server assigns each one to a slot in a single process. Ollama runs one request per model and queues the rest, unless you change its default parallelism setting.

vLLM and SGLang batch four requests together, llama-server runs them in slots, and Ollama runs one request per model by default while the rest waitvLLMport 8000R1R2R3R4Schedulercontinuous batchingrunrunrunrunRuns a shared batch.Paged KV blocks, freedas requests finish.SGLangport 30000R1R2R3R4Schedulerradix tree lookup firstrunrunrunrunRuns a shared batch.FCFS by default; lpmpolicy is opt-in.llama-serverport 9931R1R2R3R4Slot pickerbest prompt matchrunrunrunrunAuto gave 4 slots here,one KV pool. Set thecount with -np.Ollamaport 11434R1R2R3R4Queueup to 512 waitingrunwaitwaitwaitOne request per modelby default. Raise itwith NUM_PARALLEL.decoding nowqueued
Fig. 2The four engines at their defaults. Only Ollama holds the second, third, and fourth requests back, and one environment variable changes that.

vLLM's PagedAttention paper reports 2 to 4 times the throughput of FasterTransformer and Orca at the same latency, a figure from the vLLM authors on 2023 models. Continuous batching is why short requests do not wait for long ones to finish.

llama-server has continuous batching on by default too. Posts that describe llama.cpp as strictly sequential are out of date; the limit is slot count and memory. On the test machine, the automatic -np -1 setting created 4 slots sharing one unified KV buffer.

#Hardware and model format support

vLLM and SGLang now cover far more than NVIDIA and AMD, and llama.cpp lists more consumer backends than the others. Check the platform you own against this table first, because a missing backend ends the comparison. Entries come from each project's own README and install docs on November 8, 2026.

HardwarevLLMSGLangllama.cppOllama
NVIDIACUDA on Linux, compute capability 7.5+B200, H100, A100, RTX 30 to 50CUDA backendCapability 5.0+, driver 550+
AMDROCm 6.3+Instinct MI300X to MI355XHIP, VulkanROCm v7, Vulkan
IntelIntel XPUArc B-Series GPUs, Xeon CPUsSYCLNot named; Vulkan adds other GPUs
Apple siliconvLLM-Metal plugin; CPU build experimentalMetal and MLX, from sourceMetal, on by defaultMetal, plus MLX engine
CPUx86, Arm, and PowerPCXeonCore targetSupported
OtherPlugins for TPU, Gaudi, Ascend, and moreTPU v6e and v7, Ascend, Moore ThreadsCANN, Hexagon, MUSA, WebGPU, and moreNone listed

The docs and release notes disagree on CUDA. The v0.33.0 release notes say release wheels and images are CUDA 13.0 only, while the GPU install page still shows CUDA 12.9 on November 8, 2026. Trust the release notes, and match your drivers to them.

#Model formats

vLLM and SGLang load Hugging Face weights. vLLM's GGUF support is "highly experimental" by its own docs and requires the separate vllm-gguf-plugin, and SGLang lists gguf as a --load-format option.

llama.cpp reads GGUF files and pulls them from Hugging Face with -hf. Its README lists 1.5-bit through 8-bit integer quantization for lower memory use. Ollama imports GGUF files and Safetensors directories through a Modelfile, and it runs GGUF through llama.cpp alongside its MLX engine on Apple silicon.

#Constrained decoding across the four engines

All four engines can force output to match a JSON schema, but only vLLM, SGLang, and llama.cpp document grammars beyond JSON. If you need regex or EBNF, Ollama is out, and on vLLM and SGLang the backend you choose sets the accepted syntax. This matters for structured outputs at scale.

EngineDefault backendConstraints documented
vLLMauto, choosing among XGrammar, Guidance, and OutlinesJSON schema, regex, choice, EBNF grammar, structural tags
SGLangXGrammar, with Outlines and llguidance selectableJSON schema, regex, EBNF, structural tags
llama.cppBuilt-in GBNF grammarsJSON schema, GBNF
Ollamaformat fieldJSON or a JSON schema

Two details cause migrations to fail. vLLM deprecated lm-format-enforcer in v0.33.0 and plans to remove it in v0.35.0, and SGLang accepts only one of json_schema, regex, or ebnf per request.

Constrained decoding also works on small local models. The llama-server run in the Ollama vs llama.cpp section below enforces a schema through the OpenAI-style response_format field on a 0.8B model.

#vLLM vs Ollama: a shared GPU server or a local daemon

Use Ollama when one developer runs models on one machine, and use vLLM when several people or services share a GPU. Ollama optimizes for pulling and running a model with one command. vLLM optimizes for batching concurrent traffic on Linux GPUs, and it asks for more setup in return.

Ollama's FAQ spells out its defaults: OLLAMA_NUM_PARALLEL is 1 per model, OLLAMA_MAX_QUEUE is 512, models stay loaded for 5 minutes, and the context window is 4,096 tokens. Required RAM scales with parallelism times context length, so check OLLAMA_CONTEXT_LENGTH before you send a RAG prompt longer than 4,096 tokens.

vLLM's route list covers OpenAI chat, completions, responses, and embeddings, plus Anthropic /v1/messages, rerank, and score endpoints. Those routes help once coding agents and internal tools hit the same server. vLLM also needs Python 3.11 to 3.14 and Linux for GPU serving.

Move to vLLM when concurrency stops fitting within OLLAMA_NUM_PARALLEL, or when the model you want has no GGUF build. Ollama can stay on developer laptops while production points at vLLM through a different base URL.

#Ollama vs llama.cpp: convenience or control

Ollama is a daemon and model library that uses llama.cpp for GGUF models, while llama.cpp is the inference library plus a server you configure with flags. Pick Ollama when you want pull-and-run. Pick llama-server when you need to set slots, grammars, cache behavior, or a specific build.

The most visible default difference is parallelism. The test below measures it on llama-server by comparing one slot against the automatic setting. Ollama itself was not run for this article, so -np 1 shows the effect of serving one request at a time, and it says nothing about Ollama's speed.

Tested on an Apple M2 with 16 GB of memory, llama.cpp build b12202 compiled from source with Metal, and ggml-org/gemma-3-270m-it-GGUF at Q8_0 (292 MB). The machine was running other jobs, so read the ratios, not the absolute numbers.

build and run
Shell
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
  -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF \
  -DLLAMA_BUILD_UI=OFF -DLLAMA_USE_PREBUILT_UI=OFF
cmake --build build --target llama-server -j 7
 
LLAMA_CACHE=./models ./build/bin/llama-server -hf ggml-org/gemma-3-270m-it-GGUF
load_test.py
Python
"""Send the same chat request to any OpenAI-compatible server at two concurrency levels."""
import json, sys, time, urllib.request
from concurrent.futures import ThreadPoolExecutor
 
BASE = sys.argv[1]  # for example http://localhost:9931
MODEL = sys.argv[2]
REQUESTS, MAX_TOKENS = 8, 128
 
 
def chat(i):
    body = {
        "model": MODEL,
        "messages": [{"role": "user", "content": f"Request {i}: list facts about caches."}],
        "max_tokens": MAX_TOKENS,
        "temperature": 0,
        "ignore_eos": True,  # llama-server extra: every request decodes MAX_TOKENS
    }
    req = urllib.request.Request(
        f"{BASE}/v1/chat/completions",
        data=json.dumps(body).encode(),
        headers={"Content-Type": "application/json"},
    )
    start = time.perf_counter()
    with urllib.request.urlopen(req, timeout=600) as r:
        usage = json.load(r)["usage"]
    return usage["completion_tokens"], time.perf_counter() - start
 
 
for workers in (1, 4):
    start = time.perf_counter()
    with ThreadPoolExecutor(workers) as pool:
        results = list(pool.map(chat, range(REQUESTS)))
    wall = time.perf_counter() - start
    tokens = sum(t for t, _ in results)
    mean = sum(s for _, s in results) / len(results)
    print(f"concurrency={workers} requests={REQUESTS} tokens={tokens} "
          f"wall={wall:.2f}s throughput={tokens / wall:.1f} tok/s mean_latency={mean:.2f}s")

Real output, default slots and then -np 1:

Text
# default: total_slots 4
concurrency=1 requests=8 tokens=1024 wall=23.06s throughput=44.4 tok/s mean_latency=2.88s
concurrency=4 requests=8 tokens=1024 wall=6.05s throughput=169.3 tok/s mean_latency=3.02s
# -np 1: total_slots 1
concurrency=1 requests=8 tokens=1024 wall=22.63s throughput=45.2 tok/s mean_latency=2.83s
concurrency=4 requests=8 tokens=1024 wall=22.72s throughput=45.1 tok/s mean_latency=9.21s

Four slots delivered 3.8 times the throughput of one slot here, and 3.2 times in an earlier round on the same machine (145.4 against 45.2 tokens per second). With one slot, mean latency for four concurrent requests was 9.2 seconds instead of 3.0.

Prefix reuse and JSON-schema decoding both work on llama-server, and reuse depends on the model. This script ran against ggml-org/Qwen3.5-0.8B-GGUF at Q8_0, started with --reasoning off:

prefix_and_schema.py
Python
"""Show llama-server's prefix reuse and JSON-schema constrained decoding."""
import json, urllib.request
 
URL = "http://localhost:9931/v1/chat/completions"
SYSTEM = "You answer questions about inference servers. " * 60
 
 
def post(**body):
    req = urllib.request.Request(URL, data=json.dumps(body).encode(),
                                 headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=300) as r:
        return json.load(r)
 
 
for question in ("What is a slot?", "What is a grammar?"):
    out = post(model="gemma", max_tokens=8, temperature=0, messages=[
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": question},
    ])
    t = out["timings"]
    print(f"{question:20} cached={t['cache_n']:4} evaluated={t['prompt_n']:4} "
          f"prefill={t['prompt_ms']:.0f} ms")
 
schema = {
    "type": "object",
    "properties": {
        "engine": {"type": "string", "enum": ["vllm", "sglang", "llama.cpp", "ollama"]},
        "default_port": {"type": "integer"},
    },
    "required": ["engine", "default_port"],
    "additionalProperties": False,
}
out = post(model="gemma", max_tokens=64, temperature=0,
           messages=[{"role": "user", "content": "Name one inference engine and a port."}],
           response_format={"type": "json_schema",
                            "json_schema": {"name": "engine", "schema": schema}})
print(out["choices"][0]["message"]["content"])
print("parsed:", json.loads(out["choices"][0]["message"]["content"]))
Text
What is a slot?      cached=   0 evaluated= 442 prefill=1170 ms
What is a grammar?   cached= 425 evaluated=  17 prefill=151 ms
{
  "engine": "vllm",
  "default_port": 8000
}
parsed: {'engine': 'vllm', 'default_port': 8000}

The second request reused 425 cached prompt tokens, and prefill fell from 1,170 ms to 151 ms. The JSON output parsed against the schema on the first try. The same script on Gemma 3 270M reused 1 token, and the verbose server log shows context checkpoints being invalidated on that model (n_swa = 512). Test prefix reuse on the model you plan to ship.

#vLLM vs llama.cpp: batching or portability

Choose vLLM when many requests share a GPU and throughput per dollar decides the bill. Choose llama.cpp when you need one binary on a laptop, an edge device, or a machine with no CUDA. The overlap is small, because vLLM treats GGUF as experimental and llama.cpp loads GGUF files.

A useful test is where your weights already live. If your team ships Hugging Face checkpoints at FP8 or NVFP4, stay with vLLM or SGLang. If you distribute a single quantized GGUF file to users' machines, llama.cpp's -hf flag and Metal, Vulkan, and CPU backends do the job with no Python environment.

llama.cpp's install docs list Homebrew, conda-forge, Winget, MacPorts, and Nix, and the README adds an install script and a Docker image. vLLM's route is a Python 3.11 to 3.14 environment and uv pip install vllm --torch-backend=auto, then vllm download-kernels.

#Start an OpenAI-compatible server with each engine

Each of these commands starts a server, and the same chat request then works against all four once you change the port and model name. Only the llama-server command ran for this article. The other three come from the vLLM and SGLang quickstarts and Ollama's quickstart and Linux install pages on November 8, 2026, and they need the hardware they name.

commands, not run except llama-server
Shell
# NOT RUN HERE. vLLM quickstart, Linux GPU host, port 8000.
uv venv --python 3.12 --seed && source .venv/bin/activate
uv pip install vllm --torch-backend=auto
vllm download-kernels
vllm serve Qwen/Qwen2.5-1.5B-Instruct
 
# NOT RUN HERE. SGLang quickstart, GPU host, port 30000.
uv venv --python 3.12 && source .venv/bin/activate
uv pip install --prerelease=allow sglang
sglang serve Qwen/Qwen3-0.6B --host 0.0.0.0 --port 30000
 
# NOT RUN HERE. Ollama Linux install and quickstart, port 11434.
curl -fsSL https://ollama.com/install.sh | sh
ollama run gemma4:e2b
 
# RUN HERE on an Apple M2. llama-server, port 9931.
llama-server -hf ggml-org/gemma-3-270m-it-GGUF
same request, change port and model
Shell
curl http://localhost:9931/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model": "gemma", "messages": [{"role": "user", "content": "Explain the KV cache in one sentence."}], "max_tokens": 60, "temperature": 0}'

Ollama's OpenAI routes sit under http://localhost:11434/v1/. vLLM and SGLang use /v1 on their own ports. Running them on a shared host means binding to 0.0.0.0 on purpose, because SGLang, llama-server, and Ollama all default to 127.0.0.1.

#Published benchmarks are project-run and dated

Most public vLLM vs SGLang numbers were published by one of the two projects, on specific models, at versions that are now old. Treat them as evidence that a design works on one workload, and do not use them to predict your own latency. Newer per-feature numbers appear in release notes.

Published byClaimSetup and date
SGLang teamUp to 3.1x vLLM's throughput on Llama 70BvLLM 0.5.2, A100 and H100, July 25, 2024
SGLang authorsUp to 6.4x higher throughput than other systemsPaper, multiple structured tasks, 2024
vLLM teamv0.6.0 at 2.7x the throughput of v0.5.3 on Llama 3 8BOne H100, September 5, 2024
vLLM v0.33.0 notes1,373 to 3,390 output tok/s for one drafter changeDeepSeek-V4.1-Flash plus DSpark on GB300, the PR's benchmark
SGLang v0.5.23 notesUp to 44% faster single-user decodeDeepSeek-V4.1-Flash on GB300, opt-in kernels

SemiAnalysis's InferenceX is the outside check to read next. It publishes continuous benchmark runs covering both engines, with code and data in the open. This article did not extract a head-to-head number from it, so filter by your GPU and model yourself.

Both projects keep adding speculative decoding methods and hardware-specific kernels, as the release notes above show, so rerun any comparison when you upgrade.

#Choose your inference engine with one replay test

Run one replay test before you commit to any inference engine, and let your own cache hit rate and concurrency pick the winner. A sample of real prompts answers the vLLM vs SGLang question in an afternoon, and it tells you whether a local daemon was ever going to be enough.

  1. Capture 1,000 real requests, with prompts and output lengths, from production or a staging copy.
  2. Compute the share of prompt tokens that already appeared earlier in the sample. That approximates your prefix hit rate.
  3. On one GPU, serve the same model at the same dtype on vLLM v0.33.0 and SGLang v0.5.23, and replay at your target concurrency.
  4. Record time to first token, time per output token, and tokens per second at your p95 target, and read each engine's cache metrics.

As a starting rule, not a measured threshold: when more than half of your prompt tokens repeat, try SGLang first. Otherwise start with vLLM. For one machine and one user, use llama-server if you need slots and grammars, and Ollama if you do not.

Vector stores make the same trade between convenience and control, which the vector database guide works through. New engine releases and benchmark changes land in the news feed and on the inference topic page.

#Questions

Is SGLang faster than vLLM?

SGLang's own 2024 benchmark reports up to 3.1 times vLLM's throughput on Llama 70B, against vLLM 0.5.2. Those versions are more than two years old, so the number says little about v0.33.0 and v0.5.23. Replay your own traffic on both, and check SemiAnalysis's InferenceX for current runs by an outside party.

Is vLLM better than Ollama?

vLLM is better for a shared GPU server, and Ollama is better for one developer on one machine. vLLM batches concurrent requests on Linux GPUs. Ollama pulls and runs models with one command, handles one request per model by default, and queues up to 512 more, so it degrades under concurrent load unless you raise OLLAMA_NUM_PARALLEL.

Does Ollama use llama.cpp?

Yes, for GGUF models. Ollama's June 5, 2026 post says version 0.30 improved GGUF support through llama.cpp, and that this adds to its MLX engine on Apple silicon. Ollama adds a model library, a daemon, and automatic loading and unloading on top. If you need slot counts or grammars, run llama-server directly.

Can vLLM run GGUF models?

vLLM can load GGUF, but its docs call that support highly experimental and under-optimized, and it now needs the separate vllm-gguf-plugin package. Use GGUF on vLLM to reduce memory in a pinch, and pass the base model's tokenizer as the docs recommend. For normal vLLM serving, use Hugging Face weights instead.

Does vLLM run on a Mac?

Only in limited ways. vLLM's GPU install page says Linux is the only supported OS and points Apple silicon users to the vLLM-Metal plugin, which uses MLX and Metal. Its CPU page calls native macOS support experimental and source-only, with no prebuilt Apple silicon wheels. For local work on a Mac, llama.cpp and Ollama are simpler.

What port does llama-server use by default?

llama-server listens on 127.0.0.1 port 9931 by default, per its server README and the b12202 build tested for this article. Older guides and Docker examples that pass --port 8080 are setting that value explicitly. If your client gets connection refused, check the port first, or start the server with your own --port value.