← 返回资讯

SGLang 与 vLLM 怎么选:前缀重叠比例才是判据

2026-09-15

有一类结局值得先说在前面:把服务从一个引擎换到另一个,压测跑完发现吞吐没涨,甚至比原来还紧了一点。这种时候团队里通常会开始怀疑参数没调对、版本不对、驱动不对,然后花两周去拧旋钮。

但更可能的原因是:判据从一开始就选错了。选型会上摆在桌面的往往是两家发布博客里的对比图,而那些图是在别人的模型、别人的并发曲线、别人的 prompt 分布上跑出来的。它们能告诉你「在某种形状的负载上存在差别」,不能告诉你「你的负载是那种形状」。下面这篇不给倍数,只拆机制:把每一条差别还原成一个你能在自己的日志和硬件清单上验证的判据。SGLang 侧的事实按它的官方文档口径来;涉及另一侧的地方我会明确标出哪些是文档写的、哪些得你自己去核。

一、先把「同源」这件事钉死,它决定你怎么读一切对比

选型讨论里最常见的两种误判是对称的:一种以为 SGLang 是 vLLM 的 fork,所以「本质上一样,换不换无所谓」;另一种以为它们是两套毫无关系的竞品,所以「必须从零重新验证一切」。两种都会把后面的工作量估错。

它不是 fork。但也不是无关竞品——SGLang README 末尾的致谢段落明确写着,它学习了一批项目的设计并复用了其中的代码,被点名的项目里就包括 vLLM,另外还有 Guidance、LightLLM、FlashInfer、Outlines、LMQL。这句话本身已经说明血统:约束解码那一条线来自 Guidance / Outlines / LMQL,服务运行时那一条线与 vLLM、LightLLM 同源。

比致谢段更硬的证据在量化文档里,有两处。一处讲的是加载预量化权重:如果你的模型是按通道量化、激活按 token 动态量化的,可以显式加 --quantization w8a8_fp8,文档说这会让系统改用 SGLang 自己的 W8A8Fp8Config 去调 sgl-kernel,而不是走「vLLM kernels 的 CompressedTensorsConfig」。也就是说默认路径上,某些权重格式的加载与算子确实落在共享的那部分代码里,显式指定参数是为了把它切到自家实现。另一处是已知问题清单里的一条:混合位宽量化支持不完整,文档给的归因是「vLLM 的层融合(例如 QKV 融合)」会让同一个融合层里不同位宽的组件产生兼容性问题。一个项目在自己的文档里用另一个项目的实现细节来解释自己的限制,这是代码共用最直白的痕迹。

把这层关系认清之后,两件事就顺了。第一,两家的特性列表高度重叠不是巧合,也不说明谁抄谁——连续批处理、paged attention、分块预填充、张量/流水/专家/数据并行、多 LoRA 批处理、投机解码、预填充-解码分离这些东西,你在几个成熟引擎里都能找到对应物,用它们来做区分是浪费时间。第二,真正需要拿来做判据的,只有那些机制上确实不同、并且不同得能被你的负载放大的地方。SGLang 自己单独拎出来当招牌的只有一个:RadixAttention 做前缀缓存。如果你对整个候选池子还没底,先过一遍自建推理的框架选型维度,再回来看下面这几节。

二、为什么决定性变量是前缀重叠比例

推理的成本分两段,这两段的性质完全不同。预填充那一段,把整个 prompt 的 key/value 一次算出来,成本随输入长度走;解码那一段,一个 token 一个 token 往后追加,每步都要读整个 KV,成本随输出长度和并发走。

前缀缓存这个机制,动的只是第一段。它把已经算过的前缀 KV 留在池子里,新请求进来先做最长前缀匹配,命中的部分预填充计算直接省掉。解码那一段它一点没碰。

这一句话能推出选型的第一道门:先确认你的瓶颈在哪一段。如果你的服务是长输出、高并发、KV 池长期吃紧,瓶颈在解码侧,那么围绕前缀缓存的所有讨论对你的主要矛盾都是无效的,该看的是吞吐与并发那套手段。只有当瓶颈在预填充侧——输入长、输出短、被投诉的是首个 token 出来得慢——前缀缓存才站在主场上。

第二道门才是重叠比例。这个数怎么量,有几个地方特别容易量错:

  • 按 token 算,不要按字符算。 两个 prompt 在字符层面的公共前缀长度和 token 层面的不是一回事,中英文混排、数字、标点的切分边界都会让两者错开。要用你实际使用的那个 tokenizer 去切。
  • 量的是最长公共前缀,不是相似度。 这是最常见的错误。两段 prompt 内容大部分相同,但第一个 token 不一样,前缀命中就是零。前缀缓存只认前缀,不认相似。所以 prompt 里带时间戳、请求 ID、随机 session 标识而且放在开头的服务,账面上看重叠很高,实际命中是零——这种情况的正确动作是把易变字段挪到末尾,而不是换引擎。
  • 必须带时间窗口。 缓存有生命周期,池子满了会淘汰。相隔很久的两个同前缀请求,理论上共享前缀,实际上前面那份早被回收了。所以真正该统计的不是「全量日志里的重叠比例」,而是「一个窗口内同前缀请求的到达密度」。
  • 抽样一定要覆盖高峰。 这条最反直觉:高峰期 KV 池最紧、淘汰最凶,命中率恰好在这个时候最低。而你需要这个机制救你的时刻,正是高峰。拿平峰数据量出来的命中率会给你一个偏乐观的结论。

把这两道门走完,你手里就有了一个自己的数,后面所有取舍都有依据。至于 RadixAttention 本身的机制和哪些负载形状天然重叠高,SGLang 是什么那篇讲得更细,这里不重复。

三、前缀缓存的两条实现思路:细粒度匹配与固定块哈希

对比文章里常把两家的差异概括成「token 粒度最长匹配」对「固定块哈希」。这个概括的方向大体不算错,但要说清楚一件事:SGLang 的文档只讲自己这一侧,它没有义务也没有兴趣去描述另一个项目某个版本的实现细节;而 vLLM 的前缀缓存这几年一直在演进,默认开不开、按什么粒度组织,不同版本口径不同。所以哪一家在哪个版本上走的是哪条路,你要引用就去它自己的当前文档里查,别拿这篇文章替它下结论

能在机制层面讲清、而且对选型真正有用的,是这两条思路各自的取舍。

细粒度前缀树这条路,匹配精度到 token:公共前缀有多长就能吃多长,一个 token 都不浪费。代价在结构维护上——树要随请求增删而变,匹配开销随树的规模上升,节点粒度越细管理开销越高,淘汰逻辑也复杂得多。SGLang 之所以需要单独给你一个淘汰策略旋钮,根子就在这里:树上「先扔谁」不是一个能靠默认值糊过去的问题。

固定块哈希这条路,把序列切成定长块,对每块(连带前面块的链式哈希)求哈希,命中靠查表。好处很实际:查表是常数开销,天生做内容去重,而且哈希键是一个可以脱离本进程传递的东西——这让前缀感知的路由、跨实例共享缓存、把缓存下沉到别的存储层都容易做。代价是对齐损失:公共前缀不是块长的整数倍时,尾巴那一截吃不到;块内任何一个 token 不同,整块失效。

从这组取舍能推出一个比「谁更快」有用得多的判据:公共前缀越长,两条思路的差别越被摊薄。几千 token 的系统提示、整段检索文档这种场景,尾部对齐损失占比很小,细粒度的优势并不显著;反过来,如果你的公共前缀本身就短、而且零碎(比如多个模板各有几十 token 的共同头部),块粒度的对齐损失占比会陡然上升,细粒度才真正拉开差距。先看看你的前缀是长的还是碎的,比读任何一张对比图都快。

四、被低估的一个维度:缓存留存策略能不能配

选型清单里很少有人写这一条,但它对有稳定热集的业务很实际。池子满了要回收,「先淘汰谁」这个决定,SGLang 是交出来给你配的:--radix-eviction-policy 选策略,命令行上给的几种是最近最少使用(默认)、按命中频次、分段保护,以及跟随请求优先级;带参数的策略再用 --radix-eviction-policy-config 传一个 json 对象,文档说目前只有分段保护那一种有参数可调,调的是「命中多少次才升进保护段」这个门槛。具体的启动命令形态另一篇里抄了文档原文,这里只说它在选型上意味着什么。

可配置是好事,但文档里三条限制比策略表本身更值得抄进选型笔记:

第一,策略只负责打分,不能钉住任何东西。 它不决定要释放多少空间,也不能把某个前缀固定在显存里——如果把其他所有东西都回收完还不够,被策略「保护」的节点照样会被淘汰。指望靠调策略保证热前缀常驻,是指望错了。真要保证,得从容量规划和路由那一侧解决。

第二,你看到的命中计数在某些路径下会偏低。 按文档的说明,命中次数不会在分块预填充的步骤上累加,也不为已被淘汰的节点累加,写回模式的 HiCache 下同样不累加。基于频次的策略吃的就是这个计数,所以既不要拿它当唯一决策依据,也别指望它能等价于真实复用次数。

第三,可选项和「策略注册表里有的」不是一回事。 注册表里还有另外几种策略,但命令行不提供,只有扩展了选项列表的外部代码才能碰到,文档明说那些是给实验用的而不是给线上服务用的。另外那个实验性的 Rust 树内核后端不接受 config 参数,它只从策略名构造策略,两个一起传会在启动时失败——可配置性和实验性后端在这里是互斥的

选型层面的结论很朴素:默认的最近最少使用策略对大多数负载就是对的,因为前缀复用本身随时间衰减。只有当你的流量确实有一个稳定热集、而纯按时间的策略老是把它冲掉时,基于频次或者分段保护的策略才有帮助。反过来,当前缀的热度会随时间迁移,它们是有害的——一个攒够了命中次数的前缀,在它不再有用之后仍然保留着那份优势。这句话的言外之意是:这个旋钮的价值取决于你的热集稳不稳定,而这又是一个你得先去日志里量的东西。

五、结构化输出:开销差异来自语法后端与掩码怎么算

如果下游是要吃 JSON 的程序,这一块的差别会直接摊到每个 token 上,值得单独看。

先说机制上贵在哪里。约束解码不是「求模型输出 JSON」,是每生成一步都根据当前语法状态算出一个 token 掩码,把不合法的候选屏蔽掉——所以输出一定 parse 得过,这是机制保证的,不是提示词求来的。掩码在 CPU 上算,前向在 GPU 上跑,两边能不能重叠、能不能把连续确定的多个 token 一次性跳过,就是各家实现拉开差距的地方。另一个容易被忽略的成本项是语法编译:一个 schema 要先被编译成可以逐步查询的状态机,这份编译产物能不能跨请求复用、还是每个请求都重来一次,在 schema 复杂时会直接落到首个 token 的延迟上。这一层的实现细节 SGLang 文档没有展开,要确认得去各个语法后端自己的文档里查——但你在选型时至少应该知道要问这个问题。

SGLang 这一侧,语法后端是可插拔的,而且不同后端支持的约束形式不一样,这是一条能直接否决选项的硬差别:默认的 XGrammar 支持 JSON Schema、正则和 EBNF 三种;Outlines 支持 JSON Schema 和正则,不支持 EBNF;Llguidance 三种都支持。换后端用 --grammar-backend 指定,参数文档里给的枚举值是 xgrammaroutlinesllguidancenone——注意有个 none,也就是可以显式把语法引导解码整个关掉。文档的建议是用默认的 XGrammar。所以如果你的约束必须用 EBNF 写(比如要约束的是一段带嵌套结构的自定义 DSL 而不是 JSON),后端的可选范围就已经被收窄了,这比任何吞吐差别都更早决定你能不能用。

后端差异还有一层更细的,容易在上线后才被发现:控制 JSON 空白字符的两个参数,覆盖的后端是错开的--constrained-json-whitespace-pattern 用正则指定 JSON 约束输出里允许的语法空白(文档举的例子是想让模型能连续输出空白时把 pattern 设成 [\n\t ]*),它只对 Outlines 和 Llguidance 有效;--constrained-json-disable-any-whitespace 强制输出紧凑形式,它只对 XGrammar 和 Llguidance 有效。两边都吃的只有 Llguidance。这件事的实际后果是:如果你对输出的空白形态有要求——比如下游是个对空白敏感的解析器,或者你不希望模型把 token 预算浪费在缩进上——那么「要哪个行为」会反过来锁死「能用哪个后端」,而「能用哪个后端」又锁死了「能用哪种约束形式」。这一串约束得一次性想清楚,不能分开决策。

还有几条会影响设计的约束和能力:

一次请求里 json_schemaregexebnf 三个约束参数只能指定一个,文档写得很明确。这意味着「用正则卡住外层形状、再用 schema 卡内层字段」这种组合写法在单个请求里走不通,得换思路。

换的思路文档也给了:structural_tag 这种形式。它的形状是给一组触发串,以及每个触发串之后的开始标记、内层 schema、结束标记。它解决的是普通 schema 约束解决不了的场景——模型可以先自由说话,只在触发串出现之后才进入约束状态。工具调用就是典型:你要的不是「整个回复是一个 JSON」,而是「回复里凡是函数调用的那几段必须是合法 JSON」。如果你的产品形态是 Agent 或者带工具的对话,这个能力的有无比 JSON 解码快多少更值得先确认。

另外一条容易被忽略:文档建议,即使上了约束,也应该在 prompt 里显式写清要什么格式,理由是为了输出质量。这句话的潜台词值得说出来——约束解码保证的是「parse 得过」,不保证「字段填对」。模型完全可以合法地把 population 填成一个荒谬的整数。所以上了约束解码之后该做的后置校验一点也不能少,只是校验的内容从「格式」换成了「语义和取值范围」。

至于两家在你那个具体 schema 上谁的掩码开销更小:schema 的嵌套深度、自由文本字段占比、枚举分支多不多,都会改变结论。这一项只能拿你自己的 schema 去量,谁的宣传口径都替你回答不了。

六、硬件与量化格式的交叉点,比跑分更能否决一个选项

「硬件支持广」这种说法在选型时基本没用,因为你不需要广,你只需要你手上那张卡和你手上那份权重格式的交叉点落在支持范围里。SGLang 的量化文档给了一张按方法 × 平台的兼容表,这张表的读法是:找到自己那一格,看是 Yes 还是 No。

表里几组对照能说明这件事有多不平坦:Marlin 系的路径是 CUDA 专有的,所以 awq_marlingptq_marlin 在 NVIDIA 上可用、在 AMD 上不可用;而 gptq 本身按文档当前的口径已经在 NVIDIA 和 AMD 上移除了,要用就改走 gptq_marlin,但在昇腾 NPU 上 gptq 仍然可用、走的是 CANN 算子,在带 AMX 的 Intel CPU 上也还支持。同一个方法名,三个平台三种状态。再往下看,NVFP4 在 ROCm 上要靠 Petit 这条路径(文档说在 AMD 上加载 NVFP4 模型时会自动选中它),在 NVIDIA Blackwell 上则走 modelopt_fp4mxfp8mxfp_w4a8 只在昇腾的特定系列上有;bitsandbytes 在 AMD 上标的是实验性;gguf 在 NVIDIA 走 sgl-kernel 里的 CUDA 算子,在昇腾则是加载时用 CPU 预先反量化——名字一样,性质完全不同。AMD 上多个方法要靠 Aiter 加速,需要显式设 SGLANG_USE_AITER=1

选型层面的意思很清楚:跑分差一点你还能忍,权重格式在你的卡上没有算子就是直接跑不起来。 所以这张交叉表应该排在跑分前面看。而且这件事对两个引擎都成立——你要比的不是「谁支持的方法多」,是「你那一格双方都是 Yes 吗」。

顺手抄三条文档里反复强调、和选型直接相关的操作纪律。一是预量化的模型不要再加 --quantization,量化方法会从下载下来的配置里解析出来,重复指定是常见事故(文档拿已经是 FP8 的 DeepSeek V3/R1 当例子)。二是文档明确建议离线量化优先于在线量化,理由是性能、可用性和便利性;在线量化是运行时算缩放系数,离线量化是直接加载预先算好的权重。三是量化后必须跑基准做质量回归,文档的原话是要用 benchmark 验证量化后的模型,以防出现异常的量化损失退化——这条在换引擎时尤其重要,因为算子路径换了,同一份权重的数值行为不保证一致。

七、迁移成本:客户端不动,其余都要动

迁移成本最容易被低估的原因是,最显眼的那一块确实很轻。两边都提供 OpenAI 兼容端点,所以如果你的调用方是标准客户端加一个 base_url,换引擎这件事在客户端侧基本是改一行配置。如果你现在是用 vLLM 起的 OpenAI 兼容服务,这一层的确不用重写。

真正要重做的是它旁边那一圈:

  • 启动参数不能逐个映射。 两边的旋钮名字、语义、默认行为都不同,照着旧参数表逐条翻译过去,结果往往是把一个调好的配置翻译成一个没调过的配置。正确做法是回到「这个参数原本在解决什么问题」,再在新引擎里找对应手段。
  • 原生端点的形状不兼容。 OpenAI 兼容那一层之外,SGLang 还有自己的 /generate 端点,采样参数走 sampling_params 字段。你如果有任何代码用了旧引擎的原生接口,这部分要重写。
  • 指标、面板、告警阈值全部要重标。 指标名和日志字段不一样,围绕旧引擎写的面板和告警搬不过去;阈值更是必须重标,因为同一个业务 SLO 在两边对应的内部指标水位不同。
  • 压测基线要重建。 这是真实成本的大头:一整套压测、灰度、回滚预案,加上扩缩容逻辑里那些根据旧引擎行为调出来的参数。
  • 量化格式要重新做质量回归。 见上一节最后一条。
  • 部署环境的硬约束要提前核。 容器镜像、CUDA 版本要求、可变标签与固定版本标签的选择,这些在换引擎时会一起变,属于「上线前一天才发现」的高发区。

把这一圈列完,你就能回答那个真正的问题了:迁移的确定成本摆在这里,而收益是一个你还没量出来的数。

八、什么时候别换

回到开头那个场景。我的判断是:

前缀重叠比例低就别换。 如果你的 prompt 基本互不相同、以单轮为主,前缀缓存这个最核心的机制在你这儿是空转的——它不但省不掉计算,那棵树还在占 KV 池。剩下那些特性 vLLM 那边基本都有对应实现,换过去大概不值得那一整轮回归测试和运维重新踩坑的成本。

瓶颈不在预填充也别换。 这是第二道门。解码侧吃紧的服务,换引擎解决不了问题,它只是把同一个问题搬了个家。

已经稳定跑了很久的服务要按住。 SLO 靠得住、面板和告警都调顺了、扩缩容逻辑围绕当前引擎写的,这些都是真实资产。换引擎会一次性把它们全部作废重建,而这个成本比性能收益更确定。

正确的动作顺序是反过来的:先去日志里量重叠比例(按 token、带窗口、覆盖高峰),再确认瓶颈在预填充还是解码,再查你的卡和权重格式在双方的交叉表里都是 Yes,最后才拿线上抽样的真实请求做一次 A/B。前三步都在办公桌上就能做完,而它们能否决掉大部分不必要的迁移。只有全都过了,第四步才值得排期。

最后一句得罪人的话,同时也是对这篇文章本身的免责声明:这两个项目本来就互相借鉴、迭代都很快,引擎之间的差距每个版本都在动,今天这边领先的某个点下个月对面可能就补上了。所以任何一篇横评——包括这一篇——能给你的都只是「在什么形状的负载上、因为什么机制、可能会有差别」。具体差多少、值不值得换,只能在你自己的模型、你自己的 prompt 分布、你自己的并发曲线上量出来。这个结论我没法替你下,谁也不能。

算完账发现自建推理不划算?

先用托管端点把业务跑起来,量上来了再回头算自建的平衡点。

去试用

这个页面有问题?

提交时会附带当前页面地址和浏览器信息,帮助我们定位问题。不填联系方式即为匿名。