SGLang v0.5.20 升级实录:官方默认参数,为什么在双 3090 上把服务跑崩了

SGLang v0.5.20 升级实录:官方默认参数,为什么在双 3090 上把服务跑崩了

拉镜像是上周五的事,拉到一半停电了。第二天开机先查了一件事——镜像拉完没有。答案是没拉完,但顺手对了一下远端 digest,发现 lmsysorg/sglang:latest 已经在 9 月 18 号换成了 v0.5.20。于是这场升级从”补个断点”变成了”跨版本迁移”。本来以为是个常规动作:v0.5.19 到 v0.5.20,中间隔了一个版本,我们那套已经被反复精调到头发丝的参数原样搬过去就行。结果 GPU0 一路绿灯,GPU1 却直接进入了崩溃循环。而真正让我意外的,不是它崩了,是它崩的原因——官方这一版把默认参数调得比我的显卡还激进。
先说这一版到底有多大。713 个 PR、237 位贡献者,这是 SGLang 少见的”大版本”。新模型一口气上了一批,GLM-5.3-Flash、腾讯 Hy4-Preview、Qwen3.8-Flash-Next、K2 Horizon、Nanbeige4.2,扩散模型那边还塞进了几个蒸馏版。机制层面也有几件像样的事:给 RL rollout 加了 sampling mask,让训练器能原样重放采样过程;统一 radix 树新增了 SWA 分支点缓存,官方给的数字是 DeepSeek-V4-Flash 上共享系统提示词场景 token 命中率从 43.8% 提到 60.8%、平均 TTFT 从 1.57 秒降到 1.07 秒;还上线了一个纯 CPU 的 SGLang Simulator,用真实调度器和 radix cache 配一个延迟预测器代替模型前向,号称 TTFT 预测误差 6% 左右——想做调度和缓存研究又没卡的人,这个挺香。另外有几条破坏性变更:CUDA 12 通道正式退役(v0.5.19 是最后一个带 cu12x 的版本),prefill 上下文并行 v1 被删除,/v1/responses 的结果存储改成了必须显式开 --enable-response-store 才保留。这三条我们都不沾——机器上是 CUDA 13,不用 prefill CP,也没碰 responses API。
但真正值得多看一眼的,是一条和我们”上个月的旧账”直接撞上的更新:#39148 [MM] Add flag to force Kimi image preprocessing onto CPU。就在升级前一天,我刚处理完一个图片上传导致的 CUDA OOM——现象是文本请求一切正常,一传图片就 500,报错进程还偏偏是只占几百兆的 tokenizer。查到最后发现是 SGLang 默认会把 fast image processor 放到 cuda:0 上跑,让 resize 和 reshape 在显存池外做分配;再叠加模型自带的 longest_edge=16777216(是 Qwen2-VL 默认 1MP 的 16 倍),一张 12MP 手机照片能撑出 291.5MB(约 278MiB)的 float32 张量,余量不够就炸。当时的解法是三处配合:关掉 GPU 解码、把图像处理器后端换成 pil、再用 --mm-process-config 把分辨率压到 4MP。现在看到上游加了这个开关,第一反应是”能不能把手搓的补丁换掉”,但读完 PR 就明白了:它只支持 Kimi-K2.5,而且作者在正文里把理由写得很实在——”全局 CPU 预处理开关很难一致地应用,因为不同模型走不同的处理路径、还有硬编码的设备选择”。这句话某种程度上替我们确认了问题的普遍性,也确认了我们的方案在 Qwen3.5 上依然是唯一解。
动手之前先量尺。升级最怕的不是升不上去,是升上去之后某个补丁悄悄失效、模型输出变成乱码,或者某个参数在新版里被改名/删除导致起不来。所以我们那三个补丁和二十三个启动参数,得逐个对着 v0.5.20 的 tag 源码核一遍。补丁这块结论是”一个都没省掉”:上游的 CompressedTensorsConfig.get_quant_method 依然只处理 Linear、LMHead 和注意力层,没有 VocabParallelEmbedding 分支;Qwen3_5._build_embed_tokens 依然没把 quant_config 传进去。也就是说,从 0.5.19 起固化成运行时补丁的这套方案里,负责量化 embedding 的那部分(对应我们提交给上游的 PR #36137,截至发稿仍在 open)还是得自己扛。参数这边倒是顺:v0.5.20 把近万行的 server_args.py 拆成了 arg_groups/fields/*.py 一堆小文件,多模态参数搬进了 fields/mm.py,但参数名一个没改,--image-processor-backend、--mm-process-config、--mamba-radix-cache-strategy、--sleep-on-idle 这些全在,二十三个逐个对完,一个不缺。顺手还确认了 4MP 上限的机制没变——image_config 合并进 images_kwargs 那段代码原样保留。补丁和参数都过关,可以升了。
GPU0 先升。升完一看,功能全绿:表格 OCR 逐字识别,十八项内容全中;两张 12MP 图片的视觉 token 数是 7776,和升级前一模一样——这说明昨天刚修好的图片路径在新版本上完好;长输入、非文字图片也都正常。当时我心里还挺轻松,觉得这次又是”换个版本号”而已。
然后轮到 GPU1。容器重建之后,ready to roll 迟迟不来,docker ps 里状态一直在跳。一查 RestartCount=3,是崩溃循环。日志拉出来,错在 CUDA graph 捕获阶段:torch.AcceleratorError: CUDA error: out of memory。有意思的是它不是在推理时炸的,是启动时就炸——graph 还没捕完,显存就不够了。
顺着这条线挖下去,原因很干净:v0.5.20 的 CUDA graph 默认配置,比 v0.5.19 激进得多。 chunked_prefill_size 从 2048 翻到 4096,decode 图的 batch size 桶上限从 24 翻到 48,prefill 图的桶上限从 2048 翻到 4096。桶越多越大,捕获时要预留的显存就越多——实测 prefill 图捕获阶段的显存占用从 1.13GB 涨到 1.69GB,整整多要 0.56GB。GPU0 余量宽一些(available_gpu_mem 从 2.02GB 掉到 1.48GB),扛住了;GPU1 跑的是 150K 上下文、mem-fraction-static 0.92,升级前(v0.5.19)余量就只有 1.33GB,直接不够。这也解释了为什么文本请求一直没事——问题不在推理路径,在启动时的图捕获。
到这里有个岔路口。摆在面前的选择是:要么把参数显式调回 0.5.19 的等效值,保住 150K 上下文;要么接受官方默认,把上下文砍掉一截(多要的 0.56GB,按我们实际的 KV 密度 32KB/token 折算,大约相当于 1.8 万个 token),换取官方认为更优的性能。后者听起来很合理——毕竟官方把默认值调大,总该有它的道理。而且我们手上正好有一张卡(GPU0)跑的就是官方默认,天然具备对照条件。于是决定先测,不猜。
第一轮测量出来,我差点就直接认输了。同一张卡、同一组约 2.4 万 token 的请求,官方默认的 TTFT 是 0.424 秒,回落配置是 0.910 秒——按中位数折算成 prefill 速度,就是 58998 对 27500 token/s,看起来官方默认快了一倍多。按这个结论,砍掉一万八千个 token 的上下文去换一倍 prefill 提速,简直是白菜价。
但那个数字太大了,大到不合理。我停了一下,回去看测量脚本——问题就出在这里:脚本为了降噪,把同一个 prompt 连发三次取中位数。而 SGLang 有 radix 前缀缓存,同一个 prompt 第二次来,前缀全命中,压根不用重新 prefill。单独验一次就很清楚:同一个约 3.6 万 token 的 prompt 连发三次,耗时是 31.7 秒、1.68 秒、1.68 秒——十九倍的差距。所谓”中位数”,取到的其实是缓存命中后的那个 1.68 秒。顺手还发现,这种长请求真实的冷 prefill 只有 1150 token/s 上下(和后面修正后测到的 1204 token/s 是同一量级,差异来自 prompt 长度不同),和第一轮那个 58998 token/s 差了约五十倍——那个数字从一开始就是假的。
改法很朴素:每次请求在开头插一段唯一随机串,让第一个 token 就对不上缓存;同时把 cached_tokens 打出来自证。重测之后,cached_tokens 全部是 0,数据终于可信了。而结论也彻底反转:

指标 官方默认 回落配置 差异
单路冷 prefill(2.4 万 token) 1209 tok/s 1204 tok/s +0.4%
三路并发冷 prefill(5.9 万 token) 1229 tok/s 1227 tok/s +0.2%
解码速度 54.0 tok/s 53.3 tok/s +1.3%
三并发聚合吞吐 138.9 ± 3.1 tok/s 139.1 ± 2.4 tok/s 0%
available_gpu_mem(GPU0) 1.48 GB 2.03 GB +0.55 GB
GPU1(0.92 / 150K) 崩溃循环 稳定运行 —

四项性能指标全部落在噪声里,唯一确定的差异是显存:回落配置多出 0.55GB。也就是说,官方那套激进默认对我们的负载是”零收益、纯占显存”。

这个结果其实有结构性原因,想明白之后就不意外了。我们的 max_running_requests 是 3——这不是随便设的,是被 Mamba 状态缓存卡死的(max-mamba-cache-size 9 除以每请求 3 个槽)。解码阶段的并发永远是 3 及以下,而 decode 图的桶列表是 [1, 2, 4, 8, 12, 16, 24, 32, 40, 48]——4 到 48 这些桶,我们永远命中不到,纯属占着显存不干活。prefill 那边也一样:单路冷 prefill 是 1204 tok/s,三路并发也才 1227 tok/s,说明 prefill 早就算力饱和了,把 chunk 从 2048 加到 4096 换不来额外吞吐。官方的默认值是按”高并发、大批量”的服务形态调的,而我们是”3 并发、长上下文、延迟敏感”的 agentic 负载——两边的形状根本不一样。别人的最优默认,搬到你的机器上可能就是纯负担。

所以最终决定:显式回落。在 docker-compose.yaml 里把三个参数钉死成 0.5.19 的等效值:

--chunked-prefill-size 2048
--cuda-graph-max-bs-prefill 2048
--cuda-graph-max-bs-decode 24

改完之后双卡逐节点显存账本是这样的:权重加载 15.32GB,KV 池 GPU0 133120、GPU1 153600 全部保住,prefill 图捕获回到 1.13GB,available_gpu_mem 分别是 2.03GB 和 1.35GB——对比 v0.5.19 的 2.02GB 和 1.33GB,升级之后余量不降反升,各多了一点点。也就是说,这一版底层的显存管理确实略有改进,只是被激进的默认值盖住了。
回归测试的成绩单,双卡基本一致:基础文本问答正常;表格 OCR 用已知答案比对,十八项内容逐字全中,视觉 token 数 2596;两张 12MP 图片的视觉 token 数是 7776(等于 2 × 3888,正好是 4MP 上限生效后的数字,和升级前完全一致);非文字图片描述准确;长输入压到 74629 个 token,GPU0 用了 76.9 秒、GPU1 用了 75.5 秒,全程无 OOM、无崩溃。速度方面,解码约 53 到 54 tok/s,冷 prefill 约 1200 tok/s,三并发聚合约 139 tok/s。所有指标都跟 v0.5.19 持平——这次升级没有白捡的性能,但也没有任何回退。
如果要给这次折腾留一条最有价值的经验,不是某个参数该填多少,而是别太相信第一次测出来的大差异。我第一轮拿到”官方默认快一倍”的结论时,如果直接照着改,就会用一个假的 2.15 倍提速,去换掉真实的一万八千个 token 上下文。拦住我的不是更聪明的方法,只是”这个数字太大了”这一丝怀疑,以及回去看一眼测量脚本的习惯。服务端压测天然有前缀缓存这个陷阱,重复发同一个请求几乎必然污染结果;解决办法也不复杂——请求开头加唯一随机串,再把 cached_tokens 打出来自证。这条以后我会一直用。
另一条经验是关于”官方默认”的。默认值是一个被精心调过的参数,但它调优的样本不是你的机器、不是你的模型、更不是你的负载形态。SGLang 0.5.20 把 CUDA graph 桶开到 48,对跑几十路并发的服务是合理的;但对我们这种被 Mamba 缓存锁死在 3 并发的部署,那就是纯粹的浪费。判断”该不该跟”,不能只看”官方是不是这么写的”,得回到自己的瓶颈上算一遍。
最后留个后路。旧镜像已经打好了标签 lmsysorg/sglang:v0.5.19-rollback,配置文件也备份成了 docker-compose.yaml.bak.20260922_110812。万一新版本出现什么诡异问题,改一行 image: 指向旧标签、docker compose up -d 重建,十几分钟就能回到昨天。升级这件事,能随时退回去,才敢往前迈。
至于 v0.5.20 本身,我的评价是:它是一次扎实的版本更新——新模型、新机制、破坏性变更都有,和我们相关的 GDN prefill 布局优化、tokenizer 开销优化、CUDA graph 池碎片修复、Mamba radix cache 索引修复也都实实在在地落了地。但它对我们这台机器的净收益,是零性能提升、外加一点显存余量的改善。真正的收获不在版本号里,而在那半天里搞清楚的一件事:哪些默认值得跟,哪些必须自己钉死。


皖ICP备2025105865号-2|皖公网安备34010402704739号