Your chatbot crawls under load, the GPU dashboard shows 60% utilization, and you already bumped the instance size twice. The model is fine — the inference engine is the bottleneck, and you are still running the default setup copied from a two-year-old tutorial.
In this deep-dive I compare vLLM and SGLang with real 2026 H100 numbers, explain RadixAttention vs prefix caching in five minutes, and give you verified serve commands, an OpenAI-compatible client, structured outputs, and a mini-benchmark so you can decide on your workload — not on a headline.
1. The Problem: The Benchmark Said 29%, Your p99 Did Not Move
Every few weeks a new post claims one engine beats the other by 29% — then teams migrate, debug kernel conflicts at midnight, and watch p99 latency sit exactly where it was. Both engines leapfrog each other every release, so a leaderboard delta is noise. What actually decides the winner is your prompt shape: unique prompts behave nothing like RAG loops where every request shares a 4k system prompt.
The Most Common Mistake
Migrating engines because of a headline number. The often-quoted 29% SGLang advantage was measured on Llama 3.1 8B with prefix-heavy traffic — on Llama 3.3 70B the same lab measured a 3–5% gap, and on an RTX 4090 with unique prompts a third lab measured a tie. Benchmark your prompts, not someone else’s.
The goal of this guide: understand the two caching philosophies, serve the same model on both engines with verified commands, hit both with identical code, and walk away with a decision rule you can defend in a design review.
2. Minimal Concepts You Actually Need
Four ideas explain nearly every benchmark you will read this year. Get these and the numbers below read themselves.
PagedAttention (vLLM origin)
KV cache paging · SOSP 2023
vLLM split the KV cache into paged blocks like OS memory, killing fragmentation and enabling continuous batching. Its automatic prefix caching is hash-based: full 16-token blocks with identical hashes are reused across requests — almost free lunch, no output change.
RadixAttention (SGLang origin)
Radix tree reuse · NeurIPS 2024
SGLang keeps finished requests’ KV cache in a radix tree and reuses shared prefixes at token granularity — few-shot examples, chat history, agent templates, RAG context. The longer your shared prefix, the bigger the prefill savings and the lower your time-to-first-token.
TTFT vs ITL vs Throughput
Latency users feel · GPUs bill
TTFT (time to first token) is what chat users feel; ITL (inter-token latency) is streaming smoothness; throughput (tokens/sec) is what you pay per GPU-hour. Prefix caching mostly attacks prefill cost, so it moves TTFT and throughput on shared-prefix workloads — rarely tail ITL.
Disaggregated Prefill/Decode
Experimental · both engines
Split prefill and decode onto separate instances to tune TTFT and ITL independently and control tail latency. The official vLLM docs are explicit: disaggregated prefill DOES NOT improve throughput — it is a latency-shaping tool, not a speedup.
What the 2026 H100 Numbers Actually Say
Techsy’s September 2026 H100 roundup (Spheron’s Llama 3.3 70B FP8 runs + PremAI’s Llama 3.1 8B runs) plus ComputingForGeeks’ RTX 4090 tie tell one consistent story: SGLang pulls ahead when prefixes are shared and models are small; the gap collapses on big models and unique prompts.
Llama 3.3 70B FP8 · H100
2,400 vs 2,460 tok/s @ conc 100 · TTFT p50 740 vs 710 ms
≈3% gap — pick either
Llama 3.1 8B · H100 · prefix-heavy
12,500 vs 16,200 tok/s (SGLang +29%)
SGLang wins shared prefixes
Qwen2.5-7B · RTX 4090 · mixed
5,221 vs 5,155 tok/s — inside run-to-run noise
Honest tie
The One-Paragraph Mental Model
vLLM is the safe default: broadest model coverage, pipeline parallelism for very large models, biggest contributor base. SGLang wins prefix-heavy, structured-generation, and MoE workloads by roughly 10–30% end to end. Both expose OpenAI-compatible endpoints, so your client code barely changes — the server flags are what differ.
3. Tutorial: Serve One Model on Both Engines, Step by Step
We will serve Llama 3.1 8B Instruct on vLLM (:8000) and SGLang (:30000) on the same GPU host, smoke-test both, call both with identical Python, force JSON output on both, and run a 5-minute load comparison. Six steps, official commands only.
Step 1
Serve with vLLM (+ prefix caching on)
vLLM’s serve CLI wraps the OpenAI-compatible server. --enable-prefix-caching turns on the hash-based automatic prefix cache (off by default in some builds); --gpu-memory-utilization 0.9 reserves VRAM for weights plus KV cache the way SGLang’s mem fraction does.
pip install vllm openai vllm serve meta-llama/Llama-3.1-8B-Instruct \ --host 0.0.0.0 --port 8000 \ --enable-prefix-caching \ --gpu-memory-utilization 0.9
Step 2
Serve with SGLang (same model, port 30000)
SGLang’s launcher downloads weights on first run, allocates the KV pool, and prints pool sizing when ready. --mem-fraction-static 0.9 is the VRAM share for weights plus KV cache — the direct analogue of vLLM’s utilization flag above.
pip install sglang openai python3 -m sglang.launch_server \ --model-path meta-llama/Llama-3.1-8B-Instruct \ --host 0.0.0.0 --port 30000 \ --mem-fraction-static 0.9
Step 3
Smoke-test both endpoints
Both servers expose /v1/models and /v1/chat/completions. If these two curls do not list your model, fix the server before touching client code — nine times out of ten it is a wrong port or an OOM during weight load.
curl http://localhost:8000/v1/models curl http://localhost:30000/v1/models
Step 4
One Python client, both engines
This is the verified OpenAI-compatible pattern from the SGLang docs — only base_url changes. Note the shared system prompt: that is exactly the prefix shape where RadixAttention earns its keep, so keep prompts identical or the comparison is meaningless.
from openai import OpenAI
SYSTEM = 'You are a concise release-notes writer for developer tools.'
USER = 'Summarize these changes in 3 bullets: faster KV cache, new tokenizer, fixed streaming.'
def ask(base_url):
client = OpenAI(base_url=base_url, api_key='EMPTY')
r = client.chat.completions.create(
model='meta-llama/Llama-3.1-8B-Instruct',
messages=[{'role': 'system', 'content': SYSTEM},
{'role': 'user', 'content': USER}],
temperature=0, max_tokens=128,
)
return r.choices[0].message.content
print('vLLM: ', ask('http://localhost:8000/v1'))
print('SGLang:', ask('http://localhost:30000/v1'))Step 5
Structured outputs on both
Agents need JSON that parses. vLLM takes guided_json in extra_body (grammar-constrained decoding); SGLang accepts the OpenAI response_format json_object the same way. Same schema, both engines, zero parsing hacks.
schema = {'type': 'object', 'properties': {
'title': {'type': 'string'}, 'bullets': {'type': 'array', 'items': {'type': 'string'}}},
'required': ['title', 'bullets']}
# vLLM: grammar-constrained JSON
vllm = OpenAI(base_url='http://localhost:8000/v1', api_key='EMPTY')
r1 = vllm.chat.completions.create(model='meta-llama/Llama-3.1-8B-Instruct',
messages=[{'role': 'user', 'content': USER}],
extra_body={'guided_json': schema}, max_tokens=256)
# SGLang: OpenAI-style JSON mode
sgl = OpenAI(base_url='http://localhost:30000/v1', api_key='EMPTY')
r2 = sgl.chat.completions.create(model='meta-llama/Llama-3.1-8B-Instruct',
messages=[{'role': 'user', 'content': USER}],
response_format={'type': 'json_object'}, max_tokens=256)Step 6
Mini-benchmark: concurrency sweep that fits in 5 minutes
Same prompts, same concurrency ladder, both ports. Measure total tokens over wall time — that is the throughput number that maps to your GPU bill. Expect SGLang to open a lead as shared-prefix share grows; on unique prompts expect a tie.
import asyncio, time
from openai import AsyncOpenAI
PROMPTS = [USER] * 32 # replace with YOUR production prompts
async def bench(base_url, conc=16):
client = AsyncOpenAI(base_url=base_url, api_key='EMPTY')
sem = asyncio.Semaphore(conc)
async def one(p):
async with sem:
r = await client.chat.completions.create(
model='meta-llama/Llama-3.1-8B-Instruct',
messages=[{'role': 'system', 'content': SYSTEM},
{'role': 'user', 'content': p}],
max_tokens=128)
return len(r.choices[0].message.content.split())
t0 = time.time()
toks = sum(await asyncio.gather(*[one(p) for p in PROMPTS]))
dt = time.time() - t0
return toks / dt
for conc in (1, 8, 32):
v = await bench('http://localhost:8000/v1', conc)
s = await bench('http://localhost:30000/v1', conc)
print(f'conc={conc:3d} vLLM={v:7.1f} tok/s SGLang={s:7.1f} tok/s')How You Know It Worked
Both /v1/models curls list Llama 3.1 8B, the Step 4 script prints two sane summaries, both Step 5 responses pass json.loads on the first try, and your Step 6 sweep shows the gap growing with shared-prefix share — that curve, not any single number, is your migration business case.
4. Common Errors That Invalidate Your Comparison
I have reviewed enough engine shootouts to spot the failure modes by smell. Avoid these five and your numbers will survive contact with production.
Benchmarking with unique prompts, then quoting the prefix-caching gap
Batch evals and data-gen prompts share almost nothing, so RadixAttention has nothing to reuse. If your traffic is unique prompts, expect the RTX 4090 outcome — a tie — and choose on ecosystem, not throughput. Match the benchmark shape to YOUR traffic mix.
Forgetting --enable-prefix-caching on vLLM
vLLM’s automatic prefix cache is a server flag, not a per-request field — and it only caches full blocks. Comparing stock vLLM against SGLang’s always-on radix tree is rigging the test. Set the flag, and remember partial trailing blocks never hit.
OOM at load: VRAM fractions set by copy-paste
Blindly copying --gpu-memory-utilization 0.9 or --mem-fraction-static 0.9 onto a smaller card (or alongside another tenant) dies during CUDA-graph capture. Size from the pool log SGLang prints at startup, leave headroom for long contexts, and re-tune per GPU — 4090 numbers do not transfer to H100s.
Deploying disaggregated serving to “go faster”
Prefill/decode split is experimental and, per the official docs, DOES NOT improve throughput — it trades operational complexity for TTFT/ITL control and calmer tail latency. If your problem is tokens-per-dollar, fix batching and caching first; disaggregate only for strict interactive SLOs.
Assuming the clients are 100% interchangeable
Base chat calls port cleanly, but advanced flags diverge: vLLM’s guided_json / guided_regex / tool-call parsers vs SGLang’s response_format / reasoning parsers / LoRA model:adapter syntax. Audit every extra_body field before swapping servers or your agents break in new and exciting ways.
Golden rule
Start with vLLM for breadth, switch to SGLang when profiling proves shared-prefix or structured-generation throughput is the constraint. Re-run YOUR sweep every quarter — both projects ship every few weeks and today’s 29% is next quarter’s tie.
Conclusion
On 2026 H100s the honest summary fits in one line: SGLang wins prefix-heavy small-model serving by up to ~29%, ties on big models and unique prompts, and both speak OpenAI-compatible APIs so the migration cost is one config change plus a flag audit.
Run the six steps above with your production prompts this week. If the concurrency curve bends toward SGLang, migrate the prefix-heavy services and leave the rest on vLLM — heterogeneous fleets are normal, and the engines coexist behind the same load balancer.
Decision Framework: Summary
Pick SGLang when
- • RAG / chat / agents share long prefixes
- • Heavy structured-output pipelines
- • MoE models (e.g. DeepSeek-class)
Pick vLLM when
- • Prompts are mostly unique (evals, batch)
- • You need pipeline parallelism / exotic archs
- • Widest integrations and docs matter most
Always
- • Enable prefix caching explicitly
- • Benchmark your prompts per quarter
- • Audit extra_body flags before swapping
Sources
- Techsy — vLLM vs SGLang 2026: H100 Benchmarks (Sep 1, 2026)
- ComputingForGeeks — SGLang vs vLLM: Install and Benchmark (RTX 4090, Jul 2026)
- vLLM docs — Automatic Prefix Caching (hash-based design)
- vLLM docs — Disaggregated Prefilling (experimental)
- SGLang docs — OpenAI-compatible Chat Completions API
- VIPS Learn — SGLang vs vLLM comparison (Apr 2026)



