TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Comparing vLLM and SGLang, and How to Benchmark Properly

Status: Draft (Updated 2026-10-07), with some measured data reflected. This article is written based on descriptions verified in official documentation. Actual measurements taken firsthand are limited to the parts citing the experiments in Section 4 and "Measured: vLLM vs. SGLang on the Same Model, Same GPU" at the end of Section 5 (one small model, one GPU). Comparisons involving large models, multi-GPU setups, and other workloads are unmeasured. Please read "Versions and Dates Verified" and "What Was Not Verified" at the end of the article first. Writing rule: Measured data is the default, and anything not measured is marked as "Unmeasured".

Table of Contents

Choosing between the two engines is not a matter of taste, but of workload shape and measurement. This article outlines how vLLM and SGLang designed their prefix caches differently, how far they have converged by now, what to suspect first for each workload, and how to design and read benchmarks that support those judgments. The author has not operated or measured SGLang firsthand. There are no self-measured comparisons of the two engines' superiority; the content is based solely on official documentation, source code, papers, and experience measured with vLLM in the author's experimental environment.

What You Can Explain in an Interview After Reading This Article

  1. You can distinguish and articulate the differences between block hash chains (vLLM) and radix trees (SGLang), and the fact that their features now overlap significantly.
  2. You can discuss selection criteria by workload shape—such as prefix reuse, structured outputs, and model support—along with their limitations.
  3. You can explain benchmark designs incorporating open-loop loads, Poisson arrivals, prefix cache pitfalls, and percentiles, as well as the procedure for converting results into GPU counts.

1. Design Philosophy

vLLM originated as a general high-throughput engine from the PagedAttention paper (SOSP 2023). In the V1 architecture, the API server process communicates with the engine core process (scheduler and KV manager) via ZMQ, and the engine core manages workers—one per GPU (official architecture documentation). Model Runner V2 has been the default since v0.29.0 (release notes; some ROCm models and others are exceptions).

The SGLang paper (arXiv 2312.07104) started not as an engine, but as a system for efficiently executing LLM programs. Its frontend language provides generate (gen, select) and parallel control (fork, join) primitives, and its runtime features two techniques. RadixAttention keeps the KVs of prompts and generated results in a radix tree using LRU even after requests finish, employing cache-aware scheduling that prioritizes requests with longer matching prefixes. Compressed finite state machines decode multiple tokens determined by constraints in a single step. The paper claims up to a 6.4x increase in throughput and up to a 3.7x reduction in latency. The conditions were mostly A10G 24GB (some A100), Llama-7B/70B, Mixtral-8x7B, etc., under prefix-reuse-heavy workloads like agents, JSON decoding, RAG, and multi-turn conversations, while the compared vLLM version was v0.2.5. A footnote in the paper notes that it used an older version because RadixAttention was only partially integrated into the latest vLLM as an experimental feature at that time. These are not figures compared against current v0.30.0.

vLLM: 블록 해시 사슬 (블록 16 토큰)       SGLang: 라딕스 트리 (page-size 기본 1)
요청 A  [B0]-[B1]-[B2]                    root
요청 B  [B0]-[B1]-[B3]                     └ "시스템 프롬프트 ..." (공통 노드 하나)
해시 = H(부모 해시, 블록 토큰, 추가 키)           ├ "질문 A ..." -> 생성 결과
해시 맵 조회, 빈 블록 큐에서 LRU 방출              └ "질문 B ..."
                                          리프부터 방출(기본 lru), 사용 중 노드는 잠김

SGLang documentation's --page-size default is 1, meaning prefix matching is possible at the token level under document defaults, whereas vLLM only hits in units of full blocks (16 tokens) (Part 04). Recent SGLang release notes state that in v0.5.19, the unified radix tree became the default for all configurations; in v0.5.21, the prefix cache became the Rust core default (falling back to Python for unsupported configurations); and legacy implementations (SWARadixCache, MambaRadixCache, HiRadixCache) were removed. Around the same time, vLLM enabled prefix caching by default and added support for structured outputs (xgrammar, guidance). Therefore, differences in philosophy do not directly mean differences in feature availability.

2. Feature Comparison Table

This simply indicates whether an item exists in the documentation and is not a measure of maturity or performance superiority. The baseline is vLLM v0.30.0 docs and SGLang v0.5.21 docs.

Area vLLM SGLang
Prefix Cache Block hash, LRU, enabled by default. Capacity expansion via OffloadingConnector, LMCache Radix tree, --radix-eviction-policy lru, lfu, slru, priority, tlru. Hierarchical cache HiCache
Request Order --scheduling-policy fcfs (default), priority --schedule-policy fcfs (default), lpm (longest prefix match), dfs-weight, etc.
Speculative Decoding EAGLE, MTP, draft model, n-gram, suffix, etc. EAGLE-2/3, MTP, independent draft model, NGRAM
Quantization AWQ, GPTQ, FP8, INT8, bitsandbytes, ModelOpt, GGUF, KV cache FP8 --quantization awq, fp8, gptq, modelopt_fp4, etc., --kv-cache-dtype fp8_e4m3, fp8_e5m2
LoRA --enable-lora --lora-modules --enable-lora, multiple adapters in a single batch (S-LoRA, Punica techniques)
Multimodal Image, video, audio input Vision API documentation, multimodal models
Structured Outputs xgrammar, guidance (structured_outputs) xgrammar (default), outlines, llguidance. JSON schema, regex, EBNF
MoE --enable-expert-parallel (EP = TP × DP) --ep-size, --attn-dp-size
Distribution TP, PP, DP. PD separation marked as experimental in docs TP, DP, EP. SGLang Model Gateway (router), PD separation documentation
Model Fallback Transformers modeling backend --model-impl transformers

vLLM documentation notes that PD (prefill-decode) separation does not increase throughput, limiting its purpose to separate tuning of TTFT and ITL and tail latency control.

3. Selection Criteria: By Workload Shape

Workload What to Look at First
Single-shot requests with almost no overlapping prefixes There is no benefit from prefix caching, so engine differences come down only to kernel and scheduler overhead. You must measure directly.
Long common system prompts, repeated RAG documents Prefix caching is key. Both engines support it; if capacity is insufficient, look into offloading (vLLM) or HiCache (SGLang). The experiments in Part 04 represent this case.
Agents, multi-turn (history grows per turn) Reusing KV from the immediate previous turn is key. SGLang places this at the center of its design and provides session-aware radix cache and agentic-trace benchmark datasets in its docs. vLLM is also capable via prefix caching; the relative superiority for this workload was not measured.
Mostly structured outputs Since both engines support the same xgrammar, look at schema complexity and backends rather than the engines themselves.
Quickly adopting the latest large MoE models Check if model-specific cookbooks and verified execution recipes exist.
Running on Kubernetes vLLM documentation includes integration items for llm-d, production-stack, KServe, KubeRay, AIBrix, and Dynamo, while SGLang has documentation for Model Gateway and Kubernetes PD deployment.

Model support speed was not quantitatively verified. Instances observed via release notes are as follows: Qwen3.8-Flash-Next and Hy4-preview were added to new models by vLLM v0.29.0 (2026-09-09) before SGLang v0.5.20 (09-18), whereas GLM-5.3-Flash, K2-Horizon, and Nanbeige4.2 saw SGLang v0.5.20 beat vLLM v0.30.0 (09-22). Since these only represent release inclusion timing and not day-0 status, this does not serve as evidence that either side is faster. Both engines have Transformers-based fallbacks, allowing models without native implementations to be spun up anyway.

4. Launching the Server and Calling via OpenAI-Compatible APIs

vllm serve Qwen/Qwen2.5-1.5B-Instruct --max-model-len 8192            # 기본 포트 8000
sglang serve Qwen/Qwen3-0.6B --host 0.0.0.0 --port 30000              # 문서 quickstart (기본 host 127.0.0.1)
curl http://localhost:8000/v1/chat/completions -H "Content-Type: application/json" \
  -d '{"model":"Qwen/Qwen2.5-1.5B-Instruct","messages":[{"role":"user","content":"안녕"}],"max_tokens":32}'

Things to know before operations. vLLM's --api-key only authenticates the /v1, /v2, /inference path and does not authenticate other paths like /invocations , so official documentation warns against relying on this alone for security. vLLM servers apply the model repository's generation_config.json by default, which can cause sampling defaults to vary by model (turn off with--generation-config vllm ). SGLang requires turning on --enable-metrics to emit Prometheus metrics, and you can receive the number of cache-hit tokens in --enable-cache-report of the response via prompt_tokens_details .

5. How to Benchmark Properly

Determine the workload shape first. Input and output length distributions, prefix sharing ratio and popularity distribution, arrival patterns, and SLOs to maintain. Obtain these from actual logs if possible; otherwise, state assumptions and observe sensitivity.

Closed-loop vs. open-loop. Closed-loop fixes the concurrent user count, waits for a response, and then sends the next request (--max-concurrency). If the server slows down, the load decreases accordingly, masking overload conditions. Open-loop sends requests at a designated arrival rate regardless of server state (--request-rate). The model below (an explanatory simulation, not actual measurements) looks at a single server taking 0.25 seconds per request (capacity of 4 req/s).

열린 루프 3 req/s     평균   0.64s  p99   2.26s
열린 루프 4.5 req/s   평균  57.43s  p99 114.55s   <- 용량 초과, 시간이 갈수록 대기열 증가
닫힌 루프 동시 4      평균   1.00s  p99   1.00s    <- 사용자 수만큼의 대기만 보임

Both tools' --request-rate defaults to inf , sending all requests at time 0 (confirmed from source code). While maximum throughput is visible, wait times get mixed into TTFT, making it unusable for latency measurement. Providing a finite value creates arrivals via a Poisson process, and vLLM's --burstiness uses 1.0 for Poisson, with smaller values representing a Gamma distribution where requests bunch up together. You can gradually ramp up the request rate using --ramp-up-strategy .

Length distribution. vLLM's random dataset has a --random-range-ratio default of 0.0, resulting in fixed lengths. Use ShareGPT or your own logs for actual distributions. If a model finishes early due to EOS, output lengths vary, so to fix lengths vLLM uses --ignore-eos (SGLang ignores EOS by default and turns it off via --disable-ignore-eos ). Warm-up. vLLM has a --num-warmups default of 0, while SGLang has a --warmup-requests default of 1. The first request mixes in compilation and CUDA graph costs.

The most common pitfall: Prefix cache. vLLM benchmark documentation warns against sending identical prompts to the same server vllm bench serve warns that repeating this can leave the prompt from the previous run in the prefix cache and inflate throughput, and the same goes for synthetic datasets with fixed seeds. The experiments in Part 04 illustrate this magnitude. In a second pass reading the same document again, the TTFT p50 dropped from 575.5ms to 48.6ms (measured once with LMCache, concurrency 1, 1.5B model). The difference made by repetition can be greater than the difference between engines. Here is how to respond.

Percentiles, not averages. The default outputs are vLLM's --metric-percentiles default 99(including average and median), and SGLang's average, median, and p99. If you need p95, specify it like --metric-percentiles 50,95,99 . Also look at the sample size. With 100 requests, p99 is determined by the top one or two values, and the experiments in Part 04 had 10 requests per pass, making p95 the maximum value. Since vLLM documentation states that the definitions of TTFT, TPOT, and ITL may vary by tool, it is fairer to benchmark both engines using the same client. For SGLang's bench_serving , you can call the vLLM server with --backend vllm . Run the workload generator on a machine separate from the server to prevent it from becoming a bottleneck, and when using --max-concurrency , measure client latency (client_queue_time) separately. The vLLM documentation recommends GuideLLM for production benchmarks.

Tool names have changed. In the SGLang v0.5.21 source, sglang.bench_serving issues a deprecation warning and has been moved to sglang.benchmark.serving (document examples still use the old name). For vLLM, it is vllm bench serve .

Connect to capacity planning. 1) Set an SLO (e.g., p99 TTFT 2s, p99 TPOT 50ms). 2) Measure the p99 in the steady-state region while increasing the request rate. 3) The maximum request rate that maintains the SLO is the capacity of a single instance (based on goodput). 4) Number of instances = Peak request rate ÷ (Capacity × Target utilization) plus a margin for failures. Using illustrative numbers of a peak of 20 req/s, capacity of 4 req/s, and target utilization of 0.7, 20 ÷ 2.8 = 7.1, which rounds to 8 units plus 1 standby unit for a total of 9. The shape of the curve matches the description in the PagedAttention paper: as the request rate increases, latency grows gradually until it exceeds capacity, at which point the queue continuously grows and latency spikes dramatically.

p99 TTFT
   │                              /
SLO ┼ - - - - - - - - - - - - - -/- -    <- 이 교점의 요청률이 용량
   │                       _____/
   │  ____________________/   (이 지점부터 급증)
   └────────────────────────────── 요청률

Actual measurement: vLLM vs SGLang on the same model and GPU

We benchmarked both engines using the same model (Qwen3-1.7B, bf16), the same GPU (a single 8GB-class card), and the same client (different random inputs per request, 128 output tokens, measuring TTFT and ITL via streaming). vLLM 0.31.0 used --gpu-memory-utilization 0.85 --max-num-seqs 64, SGLang 0.5.21 used --mem-fraction-static 0.8 --max-running-requests 64, and the context length was 4,096 for both. Because the meanings of the two options differ, the memory settings are not identical. For the input 512 table below, the server was restarted three times and the values are the median of the three runs. The values measured on the first run and the values averaged over three runs were within 1% of each other, with the sole exception of vLLM concurrency 64 below. The input 2,048 table shows single-run values without repetition.

Input 512, Output 128 Concurrency 1 Concurrency 8 Concurrency 32 Concurrency 64
Throughput (tok/s) vLLM / SGLang 66 / 66 384 / 386 831 / 809 842 / 836
TTFT Median vLLM / SGLang 73 / 89ms 349 / 454ms 669 / 1,093ms 2.2 / 4.0s
ITL p99 vLLM / SGLang 16 / 15ms 19 / 19ms 224 / 33ms 231 / 101ms

The vLLM column for concurrency 64 varied every time the server was launched. Across three server runs, the throughput was 792, 842, and 843 tok/s, and the median TTFT was 5.7, 2.1, and 2.2 seconds. Only the first launched server was slow, and its server log showed a KV cache of 22,048 tokens (the other two had 26,560 tokens). Even with the same options, vLLM measures memory to determine the KV allocation space, so its capacity varied on every server launch (from 22,048 to 27,504 tokens). At this point where 64 concurrent requests nearly fill the cache with 640 tokens each, a 20% difference in capacity changed the TTFT by 2.7x (Part 14). SGLang had 25,282 tokens in all three runs, and the values were within 1% of each other. The vLLM concurrency 64 in the table above is the median of three runs, and this single cell has a large repetition error.

Input 2,048, Output 128 Concurrency 4 Concurrency 16 Concurrency 32
Throughput (tok/s) vLLM / SGLang 151 / 150 207 / 219 213 / 212
TTFT Median vLLM / SGLang 0.73 / 0.83s 4.0 / 2.5s 12.5 / 12.9s

Differences in a larger model (Qwen3-4B-FP8)

The same test was repeated by launching servers with Qwen3-4B-FP8 (fp8 weights to fit in 8GB), which is about 2.4 times larger than 1.7B bf16. Because the precisions differ, this should not be read as a direct comparison with 1.7B, but rather as "two engines on the same model." Both engines used a context length of 4,096 and a maximum concurrency of 64. The server logs showed KV cache sizes of 8,880 and 13,232 tokens for vLLM, and 13,036 tokens in both runs for SGLang.

Input 512, Output 128 Concurrency 1 Concurrency 8 Concurrency 32
Throughput (tok/s) vLLM / SGLang 49 / 49 251 / 278 323·365 / 450
TTFT Median vLLM / SGLang 166·168 / 113ms 911 / 720ms 7.2·3.4 / 3.6s
ITL p99 vLLM / SGLang 21 / 20ms 24~27 / 25ms 548·583 / 138ms

Cost of structured outputs (JSON schema constraints)

We compared requests using SGLang 0.5.21's JSON schema constrained decoding (response_format of json_schema, with xgrammar as the default grammar backend) against requests asking for JSON solely via prompts on the same server (Qwen3-1.7B, thinking disabled, output approx. 74 tokens, different inputs per request).

Concurrency Unconstrained Throughput Constrained Throughput Valid JSON (Schema Satisfied) Unconstrained / Constrained
1 69 tok/s 67 tok/s (-3%) 4/4 / 4/4
8 482 tok/s 486 tok/s 32/32 / 32/32
32 1,577 tok/s 1,654 tok/s (+5%) 128/128 / 128/128

Reducing weights to fp8: Both engines showed the same trend

Under the same server configuration, to SGLang, --quantization fp8 was added (cutting the 3.28GB bf16 weights roughly in half).

Input 512, Output 128 Concurrency 1 Concurrency 8 Concurrency 32 Concurrency 64
SGLang bf16 Throughput (tok/s) 66 382 808 837
SGLang fp8 Throughput (tok/s) 100 567 1,122 1,250
Increase +52% +48% +39% +49%
fp8 ITL Median (ms) 9 11 19 27

The increase from 66 to 100 tok/s at concurrency 1 is interpreted as the effect of reduced weight bytes read per generated token (the bandwidth upper-bound formula from Part 21). Since it matches the 105.7 tok/s for 1.7B fp8 measured with vLLM in Part 21, this gain is a property of precision rather than the engine. Quality (accuracy) was not measured in this test (unmeasured).

When Long Inputs Intervene: Default Behaviors Differ

We inserted three 3,500-token input requests into a single request currently being decoded and measured the maximum Inter-Token Latency (ITL) across the intervened interval.

Server Configuration Max ITL in Intervened Interval Chunk Size Log
vLLM, --max-num-batched-tokens 512 69ms 512
vLLM, default 227ms Unverified
vLLM, 8192 445ms 8192
SGLang, default (first test 3 runs: 459, 457, 464ms; server retest: 462, 462, 462ms) Approx. 460ms 2048 (log)
SGLang, --chunked-prefill-size 512 472ms 512
SGLang, --enable-mixed-chunk 228ms 2048
SGLang, --enable-mixed-chunk --chunked-prefill-size 512 69ms 512

The most practical takeaway from this test is that default settings matter more than engine names. Options with the same name have different default values, so to make a valid comparison, you must check chunk sizes, mixed chunking, memory settings, and prefix cache in the logs, align them, and then run measurements. The phrase "aligning conditions" mentioned in the first half of Section 5 of this article manifested as concrete numbers in this test.

6. Hands-On

All operations are read-only or local computations.

curl -sL https://raw.githubusercontent.com/vllm-project/vllm/v0.30.0/vllm/benchmarks/serve.py \
  | grep -n -E '"--(request-rate|burstiness|max-concurrency|num-warmups|goodput|metric-percentiles)"'
curl -sL https://raw.githubusercontent.com/sgl-project/sglang/v0.5.21/python/sglang/benchmark/serving.py \
  | grep -n -E '"--(request-rate|max-concurrency|warmup-requests|flush-cache)"'

The open-loop vs. closed-loop comparison in Section 5 is the output of the code below (it is an explanatory simulation rather than actual measurement; increasing the request count will also increase latency in the capacity-exceeded region).

import random, statistics as st
random.seed(1)
S, N = 0.25, 4000                       # 요청 하나에 0.25초 = 용량 4 req/s, 요청 4000개

def open_loop(rate):                    # 포아송 도착: 서버가 느려도 요청은 제 시간에 온다
    t = free_at = 0.0; lat = []
    for _ in range(N):
        t += random.expovariate(rate)
        free_at = max(t, free_at) + S
        lat.append(free_at - t)
    return lat

def closed_loop(users):                 # 사용자 고정: 응답을 받아야 다음 요청을 보낸다
    ready, free_at, lat = [0.0] * users, 0.0, []
    for _ in range(N):
        i = ready.index(min(ready)); t = ready[i]
        free_at = max(t, free_at) + S
        lat.append(free_at - t); ready[i] = free_at
    return lat

def row(name, lat):
    s = sorted(lat)
    print(f"{name:<14} 평균 {st.mean(lat):6.2f}s  p99 {s[int(.99 * (N - 1))]:6.2f}s")

for r in (3, 4.5, 5): row(f"열린 {r} req/s", open_loop(r))
for u in (4, 16):     row(f"닫힌 동시 {u}", closed_loop(u))

Evaluate the experiments from Article 04 against the standards of this article to find weaknesses. Concurrency 1, fixed documents, reusing the same document, and having only 10 documents all make p95 effectively equal to the maximum value, which are all problematic. Areas for improvement include concurrency 4 (from the experimental setup --max-num-seqs=4), Poisson arrivals, length distribution, and recording prefix hit rates. This experiment was run on a temporary pod and this article was not re-run. Risks include memory contention and GPU occupancy with other workloads on the same node, and pods are deleted via --rm and terminated 1 hour after remaining.

7. Potential Interview Questions

  1. How do vLLM's and SGLang's prefix caches differ? vLLM looks up a hash chain—formed by chaining parent hashes every 16 token blocks—in a map and evicts them from the free block queue using LRU. SGLang stores token sequences in a radix tree, evicting them starting from the leaves, with a default page-size of 1. Nowadays, since vLLM also enables this by default, the difference lies less in feature availability and more in granularity, scheduling policy, and eviction policy.
  2. How do you verify the claim that SGLang is better for agent workloads? Send realistic turn-structured traces in an open-loop fashion using the same hardware, model, precision, and client. Record the prefix hit rate alongside, and compare p99 TTFT and SLO-compliant throughput. Paper figures are compared against vLLM v0.2.5, so they cannot be directly applied to current versions.
  3. When should you use open-loop versus closed-loop? To find capacity and SLOs, use an open-loop approach by increasing the request rate to find the surge point. A closed-loop approach is used to mimic environments where an upstream system limits concurrency, and it reveals the limit where latency under overload ceases to visibly increase.
  4. Benchmark results are abnormally good. What should you suspect? Check whether the prefix cache hit due to repeating the same prompt, whether warmup was omitted, whether outputs ended early due to EOS, or whether the request rate was inf causing wait times to mingle into TTFT. Verify hit rate metrics along with input and output token distributions.
  5. How do you translate benchmark results into the required number of GPUs? Treat the maximum request rate that satisfies the SLO as the instance capacity, divide the peak request rate by the capacity multiplied by the target utilization, and add redundancy for failures. Calculations must be based on p99 rather than average throughput, and measurements must be retaken if the workload shape changes.

8. Common Misconceptions

References

Only materials actually opened and inspected are listed.

Verified Versions, Dates, and Unverified Items

Sign in to like

Comments

No comments yet.

Sign in to leave a comment