1. 为什么要在同一台机器上横评 vLLM 与 SGLang
如果你正在把大模型推理服务从「能跑」推进到「跑得省、跑得稳」,vLLM 和 SGLang 这两个名字一定绕不开。vLLM 是高性能推理服务的事实标准之一,核心是 PagedAttention 加 Continuous Batching,把 KV Cache 按页管理,动态拼批,GPU 利用率拉得很高;SGLang 则是后起之秀,用 RadixAttention 把共享前缀的中间状态缓存成前缀树,再配一套面向结构化提示的运行时,专门优化多轮对话、RAG、Agent 这类「提示词高度重复」的场景。
这篇内容要解决的就是选型问题:同样是 7B/8B 级别的模型,同样一张卡,vLLM 和 SGLang 在真实推理服务里的吞吐、首 Token 延迟、P99 尾延迟到底差多少?差异来自架构的哪一层?我会给出可复制的启动参数、压测脚本、指标采集配置,并且用 TaoToken 统一 API 通道做一层端到端的对照验证——因为很多团队的真实调用并不是直连本地端口,而是走统一网关,这一层的表现同样影响最终体验。
适合谁看:正在做推理服务选型的后端/算法工程师、要把本地模型接进业务系统的开发者、以及想搞清楚「为什么我的服务并发一高就崩」的运维同学。你不需要提前读过两个框架的源码,但需要有一张能跑 7B 模型的 GPU,或者至少能起一个 CPU 上的小模型做流程验证。
先说结论方向,免得你带着错误预期读下去:vLLM 在「请求彼此独立、长度均匀」的通用服务里吞吐更稳;SGLang 在「大量请求共享长前缀」的场景里,首 Token 延迟和显存复用会明显占优。没有绝对赢家,只有匹配你工作负载的那一个。下面从架构差异讲起,再落到可复制的实测。
2. 架构差异决定了性能曲线:PagedAttention 与 RadixAttention 到底差在哪
要理解压测结果,得先理解两个框架在「内存怎么管、请求怎么排」上的根本分歧。这一节不堆公式,用类比讲清楚,后面看曲线时你就能对上号。
2.1 vLLM:把 KV Cache 当虚拟内存来分页
vLLM 的 PagedAttention 借鉴了操作系统虚拟内存分页的思路。传统推理里,每个请求的 KV Cache 要占一段连续显存,序列长度不可预测,就得按最大长度预留,浪费严重。PagedAttention 把 KV Cache 切成固定大小的 block,逻辑上连续、物理上可以不连续,用一个 block table 做映射。好处是显存碎片大幅减少,多个请求可以共享物理块,显存利用率能到 90% 以上。
配合 Continuous Batching,vLLM 不再等一整批请求都结束才换批,而是每生成一个 token 就重新调度:谁结束了就踢出去,新请求随时插进来。这让 GPU 在混合长度请求下几乎不空转。代价是调度器本身有开销,请求越碎、长度差异越大,调度压力越高。
2.2 SGLang:把共享前缀缓存成前缀树
SGLang 的 RadixAttention 走的是另一条路。它假设很多请求的前缀是重复的——比如同一个 system prompt、同一段 RAG 检索到的文档、多轮对话里历史轮次。它把这些前缀用基数树(Radix Tree)组织起来,相同前缀只算一次 KV,后续请求直接命中缓存。命中率高的场景下,首 Token 延迟能降一个数量级,因为不用重新 prefill 那几千个 token。
SGLang 还带了一套前端语言,把提示词模板、分支、循环、函数调用当成一等公民。这意味着复杂 Agent 流程可以在服务端一次编排完,减少来回请求。代价是学习曲线,以及前缀不重复时 RadixAttention 的树维护本身有开销。
2.3 一张表看清定位差异
| 维度 | vLLM | SGLang |
|---|---|---|
| 核心机制 | PagedAttention + Continuous Batching | RadixAttention + 结构化运行时 |
| 最擅长 | 独立请求、高并发通用服务 | 共享前缀、多轮、RAG、Agent |
| 显存优化点 | 分页减少碎片、块级共享 | 前缀树复用、跨请求共享 |
| 接口风格 | OpenAI 兼容 API,生态成熟 | OpenAI 兼容 + 自有前端语言 |
| 调度开销 | 请求越碎越高 | 前缀越不重复越高 |
| 上手成本 | 低,几乎零改造 | 中,复杂流程收益大 |
理解这张表,后面的压测数据就不是玄学。你可以这样记:vLLM 优化的是「底层资源怎么切」,SGLang 优化的是「高层交互怎么复用」。
3. 可复制的启动配置:vLLM 与 SGLang 服务端参数
这一节给可直接粘贴的启动命令和配置片段。路径和参数名以官方文档为准,我按常见版本写,你按自己环境微调。两个框架都建议用独立的 Python 虚拟环境,避免依赖打架。
3.1 vLLM 启动 OpenAI 兼容服务
先装依赖,再起服务。模型用 Qwen2.5-7B-Instruct 举例,你可以换成自己本地的路径。
python -m venv venv-vllm && source venv-vllm/bin/activate pip install vllm启动命令,关键参数都加了注释:
python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --served-model-name qwen2.5-7b \ --host 0.0.0.0 \ --port 8000 \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.90 \ --max-model-len 8192 \ --max-num-seqs 256 \ --enable-prefix-caching \ --disable-log-requests--gpu-memory-utilization 0.90控制显存占用上限,压测时别设太满,留点余量给激活值。--max-num-seqs是并发批上限,直接决定吞吐天花板。--enable-prefix-caching打开前缀缓存,这样和 SGLang 对比时更公平。
3.2 SGLang 启动服务
python -m venv venv-sglang && source venv-sglang/bin/activate pip install "sglang[all]"启动命令:
python -m sglang.launch_server \ --model-path Qwen/Qwen2.5-7B-Instruct \ --served-model-name qwen2.5-7b \ --host 0.0.0.0 \ --port 8001 \ --tp-size 1 \ --mem-fraction-static 0.85 \ --context-length 8192 \ --max-running-requests 256 \ --enable-cache-report--mem-fraction-static对应显存静态分配比例,--max-running-requests对应并发上限,--enable-cache-report会输出前缀缓存命中率,压测时非常有用。
3.3 用统一配置片段管理两个端点
如果你要在客户端侧切换两个服务,建议用一个 JSON 配置统一管理,避免脚本里到处改 URL:
{ "endpoints": { "vllm": { "base_url": "http://127.0.0.1:8000/v1", "api_key": "EMPTY", "model": "qwen2.5-7b" }, "sglang": { "base_url": "http://127.0.0.1:8001/v1", "api_key": "EMPTY", "model": "qwen2.5-7b" }, "taotoken": { "base_url": "https://taotoken.net/api/v1", "api_key": "sk-你的TaoTokenKey", "model": "qwen2.5-7b" } } }注意本地两个服务的api_key填EMPTY即可,它们默认不校验。TaoToken 这一项用于端到端对照,Key 在控制台生成,后面会讲。
提示:两个服务不要同时占满显存。压测时先起一个,测完停掉再起另一个,或者用两张卡分别跑,避免显存争抢污染数据。
4. 压测脚本与指标采集:吞吐、TTFT、P99 怎么测才准
光有服务不够,得有可复现的压测。这一节给一个基于 asyncio 的脚本,能同时打两个端点,采集吞吐、首 Token 延迟、P99 尾延迟。核心是记录每个请求的t_first和t_last,再算分位数。
4.1 压测脚本
import asyncio, time, json, statistics import aiohttp CONFIG = json.load(open("endpoints.json")) async def one_request(session, base_url, api_key, model, prompt, max_tokens=128): payload = { "model": model, "prompt": prompt, "max_tokens": max_tokens, "temperature": 0.0, "stream": True, } headers = {"Authorization": f"Bearer {api_key}"} t0 = time.perf_counter() t_first = None tokens = 0 async with session.post(f"{base_url}/completions", json=payload, headers=headers) as resp: async for line in resp.content: line = line.decode("utf-8").strip() if not line.startswith("data:"): continue data = line[5:].strip() if data == "[DONE]": break if t_first is None: t_first = time.perf_counter() tokens += 1 t_last = time.perf_counter() return { "ttft": (t_first - t0) if t_first else None, "total": t_last - t0, "tokens": tokens, } async def run(endpoint_name, concurrency, num_requests, prompt): cfg = CONFIG["endpoints"][endpoint_name] sem = asyncio.Semaphore(concurrency) results = [] async with aiohttp.ClientSession() as session: async def worker(): async with sem: r = await one_request(session, cfg["base_url"], cfg["api_key"], cfg["model"], prompt) results.append(r) await asyncio.gather(*[worker() for _ in range(num_requests)]) ttfts = [r["ttft"] for r in results if r["ttft"]] totals = [r["total"] for r in results] total_tokens = sum(r["tokens"] for r in results) wall = max(totals) print(f"[{endpoint_name}] 并发={concurrency} 请求={num_requests}") print(f" 吞吐: {total_tokens / wall:.1f} tokens/s") print(f" TTFT 均值: {statistics.mean(ttfts)*1000:.1f} ms") print(f" TTFT P99: {sorted(ttfts)[int(len(ttfts)*0.99)-1]*1000:.1f} ms") print(f" 总延迟 P99: {sorted(totals)[int(len(totals)*0.99)-1]*1000:.1f} ms") if __name__ == "__main__": prompt = "请用三百字解释什么是 KV Cache,以及它为什么影响推理性能。" asyncio.run(run("vllm", concurrency=32, num_requests=256, prompt=prompt)) asyncio.run(run("sglang", concurrency=32, num_requests=256, prompt=prompt))脚本用/completions流式接口,逐 token 计时,ttft是首 Token 延迟,total是整请求耗时,吞吐用总 token 数除以最慢请求的墙钟时间。跑之前先pip install aiohttp。
4.2 指标采集配置
压测时服务端也要采数据,否则你不知道瓶颈在哪。vLLM 暴露 Prometheus 指标,SGLang 也有类似端点。用一段 scrape 配置:
scrape_configs: - job_name: vllm static_configs: - targets: ["127.0.0.1:8000"] metrics_path: /metrics - job_name: sglang static_configs: - targets: ["127.0.0.1:8001"] metrics_path: /metrics重点看三个指标:vllm:gpu_cache_usage_perc(显存 KV 占用)、vllm:num_requests_running(在跑请求数)、以及 SGLang 的缓存命中率。如果显存占用早早到 95% 而吞吐上不去,说明并发上限设太高,请求在排队。
4.3 测试场景设计
为了把架构差异逼出来,设计四个场景,每个都跑一遍两个框架:
场景一,短提示独立请求,prompt 约 50 token,输出 128 token,测基础吞吐。场景二,长上下文,prompt 约 4000 token,测 prefill 和显存管理。场景三,共享前缀,所有请求带同一段 2000 token 的 system prompt,测 RadixAttention 命中收益。场景四,多轮对话,模拟 5 轮历史,测 KV 复用。
5. 实测结果与常见报错排查
这一节先给对照数据,再给排障清单。数据来自单卡 24G 显存、7B 模型、并发 32 的环境,你的绝对值会不同,但相对趋势可参考。
5.1 吞吐与延迟对照
| 场景 | vLLM 吞吐 | SGLang 吞吐 | vLLM TTFT | SGLang TTFT |
|---|---|---|---|---|
| 短提示独立 | 高 | 略低 | 低 | 略高 |
| 长上下文 | 中 | 中 | 高 | 中 |
| 共享前缀 | 中 | 高 | 高 | 显著低 |
| 多轮对话 | 中 | 高 | 中 | 低 |
规律很清楚:请求越独立、前缀越不重复,vLLM 越稳;前缀重复度越高,SGLang 的 RadixAttention 收益越大,TTFT 能降一半以上。多轮对话场景里,SGLang 因为历史轮次命中缓存,尾延迟也更平。
5.2 常见报错与排查
401 Unauthorized:本地服务一般不会,但走 TaoToken 时如果 Key 写错或没带Bearer前缀就会报。检查Authorization: Bearer sk-xxx格式,别漏空格。
local proxy failed / connection refused:客户端连不上本地端口。先curl http://127.0.0.1:8000/v1/models确认服务活着,再看是不是--host只绑了 127.0.0.1 而你在容器外访问。绑0.0.0.0解决。
reading choices 报错 / 返回体解析失败:多半是流式和非流式混用。脚本里stream: true就必须按 SSE 逐行解析,别直接resp.json()。非流式才用choices[0].text。
OAuth / token 过期:TaoToken 的 Key 如果失效会返回鉴权错误,去控制台重新生成即可,别在代码里硬编码旧 Key。
显存 OOM:两个服务同时起最容易触发。压测时串行启动,或把--gpu-memory-utilization降到 0.85。
注意:如果你用 Claude Code 或 Cline 这类客户端接本地服务,配置要写全三件套——Base URL、API Key、Model ID,缺一个都会连不上。Base URL 填
http://127.0.0.1:8000/v1,Key 填EMPTY,Model ID 填--served-model-name里的值。
6. 用 TaoToken 统一通道做端到端验证与选型收尾
本地压测只覆盖了「服务端到客户端」这一段。真实业务里,请求往往先经过统一网关再落到推理服务,这一层的稳定性、鉴权和路由同样影响体验。我用 TaoToken 的统一 API 通道做了一层对照:把上面脚本里的base_url换成https://taotoken.net/api/v1,Key 换成控制台生成的,其余不变,就能验证「网关 + 推理」整条链路的吞吐和延迟。
生成 Key 的入口在控制台,模型对话可以直接在网页里试提示词效果,接入文档里有各语言的示例。如果你要长期跑编码类或 Agent 类负载,Coding Plan 更适合按量使用;只是临时验证模型输出,用模型对话就够了。这几个入口分别是:API Keys 在https://taotoken.net/api-keys,接入文档在https://taotoken.net/doc,模型对话在https://taotoken.net/chat,Coding Plan 在https://taotoken.net/coding-plan。
端到端验证时有个细节:网关会引入额外一跳,TTFT 会比直连本地高几十毫秒,这是正常的,别把它算到框架头上。对比时要保证两边都走同一层网关,或者都直连,否则数据不可比。
选型收尾给你三条实操建议。第一,如果你的请求彼此独立、长度均匀、追求通用高吞吐,选 vLLM,它的调度和分页在這種负载下最省心。第二,如果前缀重复度高、多轮和 RAG 为主、愿意接受一点学习成本换极致 TTFT,选 SGLang。第三,别急着二选一,可以先用统一网关把两个后端都挂上,按请求特征路由:短独立请求走 vLLM,长共享前缀走 SGLang,用真实流量跑一周再定。
最后留一个我踩过的坑:压测时一定要关掉服务端的请求日志(vLLM 的--disable-log-requests),否则磁盘 IO 会拖慢吞吐,测出来的数偏低,你会误判框架性能。把日志关掉再跑一遍,数据才干净。