SGLang 是什么:RadixAttention 与它和 vLLM 的根本差异
服务已经用 vLLM 跑着,好好的,某天有人在群里丢一句「要不要换 SGLang,听说快很多」。这种时候最容易犯的错是去翻两家的 benchmark 博客,看见一个倍数就下结论。那些数字是在别人的模型、别人的 batch、别人的 prompt 分布上跑出来的,跟你的线上负载没有可比性。
值得花时间搞清楚的只有一件事:SGLang 引入的机制,在什么形状的负载上才会发挥作用。搞清楚这个,你自己就能判断值不值得试,而不用等谁给你一个倍数。下面这些都按 SGLang 官方文档的口径来说,涉及 vLLM 的部分我会标清楚哪些是文档写的、哪些得你自己去核。
一、它是什么:定位,以及和 vLLM 的真实关系
SGLang 官方 README 给自己的定位是「面向大语言模型和多模态模型的高性能服务框架」,目标是从单卡到大规模分布式集群都能提供低延迟、高吞吐的推理。它目前托管在非营利开源组织 LMSYS 之下,也已经加入了 PyTorch 生态。
先把「它是不是 vLLM 的 fork」这个问题解决掉。不是。但也不是毫无关系的竞品——README 末尾的致谢段落明确写着,SGLang 学习了一批项目的设计并复用了其中的代码,被点名的包括 Guidance、vLLM、LightLLM、FlashInfer、Outlines、LMQL。这个名单本身就说明了它的技术血统:约束解码那一半来自 Guidance / Outlines / LMQL 那条线,服务运行时那一半跟 vLLM、LightLLM 同源,注意力算子用的是 FlashInfer。
所以两者的特性列表会高度重叠,这一点不奇怪。README 的 Fast Runtime 一条里列的东西,连续批处理、paged attention、分块预填充、张量/流水/专家/数据并行、量化(FP4/FP8/INT4/AWQ/GPTQ)、多 LoRA 批处理、投机解码、预填充-解码分离,绝大多数你在别的成熟引擎里也能找到对应物。真正被它当作招牌单独拎出来的,只有一个:RadixAttention for prefix caching。如果你对推理引擎的整体格局还没底,可以先看推理框架选型那篇把候选池子过一遍,再回来看这个机制。
二、RadixAttention 到底做了什么
要理解它,先回到 KV cache 的常规生命周期:一个请求进来,预填充阶段把 prompt 的 key/value 算出来存进显存池,解码阶段一个 token 一个 token 地往后追加,请求结束,这块 KV 被释放,池子还给下一个请求。
RadixAttention 改的是最后一步:请求结束之后不急着丢。已经算好的前缀 KV 留在池子里,按前缀关系组织成一棵 radix 树——公共前缀是靠近根的节点,各自分叉的部分挂在下面。新请求进来时,先在这棵树上做最长前缀匹配,命中多少 token,这部分预填充的计算就直接省掉,只算没命中的尾巴。SGLang 调度侧那个 --schedule-policy lpm 参数里的 lpm 就是 longest prefix match 的缩写,文档说它会重排请求顺序来提高命中率,代价是调度开销上升。
这里要说句实话。很多对比文章会把 SGLang 和 vLLM 的差异总结成「token 粒度前缀匹配 vs 块粒度哈希」的对立,方向大体不算错,但 SGLang 的文档只讲自己这一边,它没有义务也没有兴趣去描述 vLLM 某个版本的实现细节。vLLM 的前缀缓存这几年一直在演进,默认开不开、按什么粒度匹配,不同版本口径不同,你要引用就去它自己的文档里查当前版本的说法,别拿这篇文章、也别拿 SGLang 文档去替它下结论。
机制上能确定的是:匹配粒度越细、缓存留存策略越可控,可复用的前缀就越多。至于在你的负载上这个差异有多大,只能量。
三、判据:先看你的前缀重叠比例
这一节是全文最值钱的部分。前面那套机制的收益,完全取决于一个东西——你的请求之间有多少可复用的公共前缀。这个比例高,RadixAttention 省掉的是实打实的预填充计算;这个比例接近零,它省不掉任何东西,那棵树只是白占显存。
哪些负载天然重叠比例高:
- 长 system prompt + 短用户输入。典型的企业内部助手,几千 token 的角色设定和规则说明每次都一样,用户那句话只有几十个 token。这是前缀缓存最理想的形状。
- 多轮对话。第 N 轮的 prompt 就是第 N−1 轮的完整前缀加上新的一问一答,天生就是树上的一条延伸路径。
- Agent 的 ReAct 循环。同一条任务链上每次调用都在上一次的上下文后面追加观察结果,前缀命中率极高。
- 同一份长文档反复提问。RAG 里把大段检索结果塞在前面、问题放后面的写法,如果多个问题共享同一批检索结果,前缀是一样的。
- 批量跑同一个模板。离线批处理里同一个 few-shot 模板套几万条数据,模板部分只需要算一次。
哪些负载基本吃不到:
- 面向公众的开放对话,每个人的第一句话都不一样,且大多是单轮。
- prompt 里带时间戳、请求 ID、随机 session 信息,而且这些东西被放在了开头。这是最可惜的一种——只要把易变字段挪到 prompt 末尾、把固定内容挪到前面,重叠比例立刻能救回来。前缀缓存只认前缀,第一个 token 不一样,后面全一样也没用。
- 长输入短输出、且输入互不相同的场景,比如批量翻译互不相关的句子。
所以真正该做的动作不是先换引擎,而是先去线上日志里抽一批真实 prompt,算一下两两之间的公共前缀长度占平均输入长度的比例。这个数你心里有底之后,后面所有取舍都有依据。顺带说一句,重叠比例高的负载往往同时也是吞吐瓶颈在预填充侧的负载,吞吐优化那些手段和前缀缓存是叠加生效的,不冲突。
四、缓存满了淘汰谁:SGLang 给你的那颗旋钮
前缀缓存留存得越久越好,但显存池是有限的。池子满了,radix 缓存就要回收空间,这时候「先淘汰谁」变成一个真问题,而 SGLang 把这个决定权交给了你。
文档里对回收过程的描述很值得读,因为它解释了为什么这件事不是简单的 LRU 队列:能被考虑淘汰的只有可淘汰的叶子节点——KV 还在显存里、没有被在途请求锁住、并且没有被仍持有 KV 的子节点遮蔽的节点;根节点永远不可淘汰。策略给每个候选打分,分数最低的先被淘汰。一个叶子被淘汰后,它的父节点可能变成新的叶子重新进入候选集,于是淘汰是从一条分支的末梢往根的方向逐步推进的。
还有两句限制说得很克制,我觉得比策略表本身更重要:策略只负责打分,它不决定要释放多少,也不能把某个节点钉在显存里——如果把其他所有东西都回收掉还不够,被策略保护的节点照样会被淘汰。指望靠调策略来保证某个热前缀常驻,是指望错了。
可选的策略用 --radix-eviction-policy 指定,文档给的口径是:lru 是默认值,也是大多数负载的正确选择,因为前缀复用本身就是随时间衰减的;lfu 先淘汰命中次数最少的,适合「一小撮 prompt 的复用远多于其余」并且你希望它们能扛过一阵一次性流量的冲击;slru 把缓存切成试用段和保护段,前缀进来先进试用段、命中够次数才升到保护段,试用段的会先于保护段被淘汰,相当于给一次性前缀能挤掉老前缀的程度设了个硬下限;priority 按插入该前缀的请求优先级来,配合优先级调度用,如果你没开优先级调度,所有节点优先级都是 0,它就等价于 lru。所有策略在打分打平时都退回最近最少使用的顺序。
目前只有 slru 带参数,通过 --radix-eviction-policy-config 传一个 json 对象:
python3 -m sglang.launch_server \
--model-path MODEL_PATH \
--radix-eviction-policy slru \
--radix-eviction-policy-config '{"protected_threshold": 4}'
protected_threshold 是升入保护段所需的命中次数,默认 2。调高让晋升更难,保护段就更小、更接近真正的热前缀;调到 1 则任何被复用过一次的前缀都晋升,效果接近带一次宽限的 lru。这个配置项有个挺友好的设计:写错的键不会被静默忽略,而是启动时直接报错,比如把 protected_threshold 拼成 protected_treshold 会抛 TypeError。另外文档提了一句,实验性的 Rust 树内核后端不支持这个 config 参数,它只从策略名构造策略,两个一起传会启动失败。
关于怎么选,文档的态度和我的经验一致:先用 lru,只在有实测缓存命中率的情况下才改,命中率计数器通过 --enable-metrics 暴露出来。lfu 和 slru 在流量有稳定热集、而纯按时间的策略老是把它冲掉时才有帮助;反过来,当前缀的热度会随时间迁移时它们是有害的——一个攒够了命中次数的前缀,在它不再有用之后仍然保留着那份优势。
五、结构化输出:这块的差别在实现路径上
如果你的下游是要吃 JSON 的程序,约束解码的开销会直接算在每个 token 上,这块的实现差异很实际。SGLang 把 structured outputs 列在 Fast Runtime 的核心特性里,README 的新闻条目里能看到它在 JSON 解码上用的是压缩有限状态机这条路子,v0.4 那次发布也把「更快的结构化输出」和「零开销 CPU 调度器」放在一起讲。
机制上,约束解码贵在哪里是清楚的:每生成一步都要根据当前语法状态算出一个 token 掩码,把不合法的候选屏蔽掉。这个掩码的计算在 CPU 上,GPU 的前向在另一边,两边能不能重叠、能不能把连续确定的多个 token 一次性跳过,就是各家拉开差距的地方。至于 SGLang 和 vLLM 在你那个具体 schema 上谁更快——schema 的复杂度、嵌套深度、是不是有大量自由文本字段,都会改变结论,这个只能拿你自己的 schema 去量,两家的宣传数字都替你回答不了。
六、模型与硬件支持范围
这部分照 README 抄,你可以直接拿去对照自己的存量资产。
语言模型侧覆盖 Llama、Qwen、DeepSeek、Kimi、GLM、GPT、Gemma、Mistral 等主流系列,并且兼容大多数 Hugging Face 模型;此外还支持嵌入模型(e5-mistral、gte、mcdse)、奖励模型(Skywork)和扩散模型(WAN、Qwen-Image)。接口层兼容 OpenAI API,这一点对迁移成本影响很大,后面第七节会说。
硬件侧除了 NVIDIA GPU(README 点名了 GB200、B300、H100、A100、Spark、5090 这些型号)之外,还支持 AMD GPU(MI355、MI300)、Intel Xeon CPU、Google TPU、昇腾 NPU 等。Quickstart 里给的 NVIDIA 平台门槛是 sm80 及以上、Python 3.10 或更高、推荐 Linux;非 NVIDIA 平台文档各有专门的页面。安装文档里还有两条容易踩的硬约束:当前 SGLang 要求 CUDA 13,CUDA 12 的 wheel 和镜像已经退役;用 uv 安装时要带上 --prerelease=allow,因为部分依赖只在 PyPI 上发预发布版,不带这个标记时较老版本的 uv 会静默装上一个旧版 SGLang。
还有一个容易被忽略的能力:它支持不起 HTTP 服务、直接用 Engine 类做离线批量推理。批量刷数据、离线评测这类场景不需要为了走 HTTP 再包一层。
七、怎么起服务
装法最简单的一条,按 quickstart 原文:
pip install --upgrade pip
pip install uv
uv pip install --prerelease=allow sglang
起服务就一行,文档里用的示例模型是个轻量的小模型:
python3 -m sglang.launch_server --model-path qwen/qwen2.5-0.5b-instruct --host 0.0.0.0 --port 30000
等到终端里出现 The server is fired up and ready to roll! 就算起来了。起来之后 API 文档挂在 http://localhost:30000/docs(Swagger UI)、/redoc 和 /openapi.json 上。服务会自动套用 Hugging Face tokenizer 里的对话模板,要覆盖就在启动时加 --chat-template。
调用是 OpenAI 兼容的:
curl http://localhost:30000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen/qwen2.5-0.5b-instruct",
"messages": [
{"role": "user", "content": "What is the capital of France?"}
]
}'
Python 侧直接用 openai 客户端换 base_url 就行,这也是迁移成本最低的一点——如果你现在的服务是用 vLLM 起的 OpenAI 兼容接口,客户端代码基本不用动:
import openai
client = openai.Client(base_url="http://127.0.0.1:30000/v1", api_key="None")
response = client.chat.completions.create(
model="qwen/qwen2.5-0.5b-instruct",
messages=[
{"role": "user", "content": "List 3 countries and their capitals."},
],
temperature=0,
max_tokens=64,
)
print(response.choices[0].message.content)
除了 OpenAI 兼容接口,它还有个原生的 /generate 端点,采样参数走 sampling_params 字段(例如 temperature、max_new_tokens),加 "stream": True 走流式。容器部署走 lmsysorg/sglang 镜像,生产环境文档建议用体积更小的 runtime 变体(去掉了构建工具和开发依赖),并且提醒 latest 和 dev 都是可变标签,要可复现就钉一个不可变的版本号标签。另外有两个高频故障有现成答案:报 OSError: CUDA_HOME environment variable is not set 就 export CUDA_HOME;FlashInfer 在 sm75+ 设备上出问题,启动时加 --attention-backend triton --sampling-backend pytorch 换后端。
跑起来之后调优看日志里那行 decode 统计。文档的口径是:#queue-req 反映队列里的请求数,长期是 0 说明客户端提交太慢喂不饱,健康区间是几百到两千,但也不宜太大,否则调度开销上升;token usage 反映 KV 池的利用率,超过 0.9 算利用得好,如果长期低于 0.9 而队列里又有请求,说明服务端收新请求太保守,可以把 --schedule-conservativeness 往下调;反过来频繁看到 KV 池满、请求被 retract 的告警,就把它往上调,偶尔出现(一分钟一次这种频率)是正常的。显存分配那边由 --mem-fraction-static 控制模型权重加 KV 池占 GPU 容量的比例,调它的时候看启动日志里那行 max_total_num_tokens=... available_gpu_mem=...,按文档给的区间留够激活和 CUDA graph 的余量。真遇到 OOM,预填充阶段炸就调小 --chunked-prefill-size,解码阶段炸就调小 --max-running-requests。这些旋钮和并行度设置是要一起算的,别单独拧一个。并行策略上文档还提了一句判断口径:显存够的时候,为吞吐考虑优先用数据并行,--dp-size 和 --tp-size 怎么配得看你是缺显存还是缺吞吐。
八、什么时候不该换
回到开头那个问题。我的判断是:
前缀重叠比例低就别换。 如果你的 prompt 基本互不相同、以单轮为主,RadixAttention 这个最核心的机制在你这儿是空转的,剩下那些特性 vLLM 那边也有对应实现,换过去大概不值得那一轮回归测试和运维重新踩坑的成本。
还有几种情况值得先按住。 你的服务已经稳定跑了很久、SLO 靠得住,而换引擎意味着一整套压测、灰度、告警阈值重标;你团队现有的运维脚本、指标面板、扩缩容逻辑全是围绕当前引擎写的;或者你的模型依赖某个特定的量化格式或自定义算子,得先确认新引擎那边的支持状态。这些迁移成本都是真实的,而且往往比性能收益更确定。
反过来,值得认真试的信号也很清楚:长 system prompt、多轮对话、Agent 循环这类前缀重叠明显的负载;预填充是你的瓶颈而不是解码;或者你需要的模型/硬件正好在它的支持列表里而现有引擎支持得别扭。
最后说一句得罪人的话:引擎之间的性能差距每个版本都在动。今天这边领先的某个点,下个月对面可能就补上了,这两个项目本来就互相借鉴、迭代都很快。所以任何一篇文章(包括这一篇)给你的都只能是「在什么形状的负载上、因为什么机制、可能会有差别」,具体差多少、值不值得换,得在你自己的模型、你自己的 prompt 分布、你自己的并发曲线上量出来。拿你线上抽样的真实请求做一次 A/B,比读十篇对比文章都管用。
算完账发现自建推理不划算?
先用托管端点把业务跑起来,量上来了再回头算自建的平衡点。