把 A 请求的上下文写进 B:SGLang KVCache 竞态复盘
原文:LMSYS Blog - 《SGLang 披露并修复 PD 分离服务中的跨请求 KVCache 竞态问题》(https://www.lmsys.org/blog/2026-10-26-cf-security)
做 Agent 开发的人大多把多租户隔离当成应用层的事:提示词里别串会话、检索库按租户分片。但 10 月 26 日 LMSYS 与 Cloudflare 的协同披露提醒了一件事:隔离边界也可能是推理服务自己给的,而且可能在毫秒级的内存复用里破掉。这篇按原文把 SGLang 这次跨请求 KVCache 泄漏的机制、触发条件和修复拆开讲。
一、事件本身:生产环境真的漏了
这次披露对应的是 Cloudflare 在 Workers AI 上的 GLM-5.3 部署:2026 年 8 月 27 日到 9 月 9 日之间受影响。9 月 9 日 Cloudflare 通知 SGLang 维护者,当天关闭该模型的公开访问、扩大 prefill 缓存池、打上自研引擎补丁,当晚恢复服务;其调查确认没有其他模型受影响。
值得注意的是触发它的配置错位:部署配置的上下文长度是 1,048,576 tokens,但 prefill 的 KVCache 池只装得下 681,280 tokens。这个 mismatch 让「超限请求走拒绝路径」变成可达路径,而拒绝路径恰好能踩中这次要讲的竞态。
二、背景三十秒:PD 分离为什么要把 KVCache 搬来搬去
PD 分离(prefill/decode disaggregation)把预填充和解码拆到不同引擎上跑,每个请求的 KVCache 必须从 prefill 引擎搬进 decode 引擎的内存池。为了吞吐,这个搬运是异步的——请求的解码可能已经开始,远端的写入还在路上。问题就出在「搬运还没结束,槽位已经被回收再分配」。
三、竞态四步:一次分布式 use-after-free
原文把失败过程拆成四步:
- 请求 A 在传输中途被中止——客户端断开、准入拒绝或传输故障——但它已发出的至少一个写请求还没落地;
- decode 在中止时立刻把 A 的 KVCache 槽位标成空闲,而远端写入仍在进行;
- 分配器把这些槽位分给请求 B;
- A 的写入落进 B 的 KVCache,B 拿着别人的 key 和 value 继续解码,全程无任何报错。
原文给了一个很准的定性:每个这类 bug 都是一次分布式 use-after-free,悬空指针就是挂在另一台机器网卡队列里的那条写请求。后果是单向跨请求泄漏——被中止请求的上下文片段可能出现在另一个客户端的输出里。
四、触发条件:三件事凑齐才会发生
按原文,竞态需要同时满足:
- 启用了 PD 分离并带 KVCache 传输后端(Mooncake、NIXL、MORI 或自定义后端)。单实例部署没有跨引擎传输路径,不受此竞态影响;
- 一个请求在传输中途终止(断连、路由取消、超时、传输故障等),且至少一个写已发出未落地;
- 槽位在写入落地前被重新分配——内存压力下这是毫秒级的事。
生产环境里它是概率性的:能提高触发概率的手段只有发长上下文然后中途断开,既选不了受害者也选不了落点。另外原文强调竞态窗口随 prefill 上下文长度增长,而长上下文请求恰恰最容易超时断开——两者叠加,让这条路径比直觉里更容易踩到。
五、修复思路:一条不等式
decode 取消不了 prefill 已经发出的 RDMA 写——RDMA API 没有取消已提交工作的调用,one-sided write 又绕过接收端 CPU,decode 根本收不到「每写一次」的信号。所以修法不是取消,而是把中止请求的槽位多保留一段时间,判断依据是一条不等式:
T_hold > t_abort_propagation + t_remaining_chunks + t_in_flight_call
保持时长必须盖过 abort 传播、剩余分块与在途调用的总和。围绕它,上游做了一串配套:准入早拒(#38935)、路由器两侧配对 abort、失败发送不再 ack(#43719、#45973)、PD 校验和(Adler-32,随末块发送,默认关闭)。
版本线:v0.5.21(10 月 2 日)对 Mooncake 和 NIXL 默认保护;v0.5.22 加入 MORI 与带代际标记的 ack;v0.5.23(10 月 22 日)补上失败 NIXL/MORI 发送的 ack 问题。默认开启的吞吐代价实测 -0.38%(置信区间 -2.12% 到 +1.36%),基本免费。
六、运维自检清单
服务跑在 SGLang PD 分离上的,按原文建议过一遍:
- 升级到 v0.5.21 以上,MORI 后端要 v0.5.22 以上;
- 跑一个配对 abort 的路由器:sgl-router 的 PD 模式会把请求对的两半一起取消,是第一层防线;
- 停在 v0.5.19/20 的,在 prefill 和 decode 两端都设环境变量 SGLANG_DISAGGREGATION_DEFERRED_DECODE_KV_RELEASE=1;v0.5.20 还要加批量索引限制和 60–120 秒的释放超时;
- 升级后盯 sglang:decode_deferred_kv_release_total 这个指标,健康部署绝大多数释放走 drained 出口;
- 顺带查一遍自己有没有 Cloudflare 那种配置错位:声明的上下文长度不能超过 prefill 缓存池容量。
七、怎么自己验证(复现要点整理,非官方)
原文给出了复现 harness 的关键参数,整理成要点(非官方脚本,思路来自原文):
# 复现环境:2 张 H200/H100,prefill 与 decode 各一实例# 1. 把 KVCache 总池压到极小,强制槽位立即复用python-msglang.launch_server... --max-total-tokens96# 2. 并发发送约 132 个长上下文请求,全部在中途主动断开# 3. 检查后续请求的输出里是否混入前序请求的上下文片段窗口一旦被参数撑开就能稳定复现;生产里要防的恰恰是它平时闭着。
八、收束
对 Agent 开发者,这次披露有两层提醒。部署层:KVCache 复用是性能优化的命根子,但「槽位空闲」的隐含前提是「没有远端写入还在路上」,两个定义之间的缝隙就是泄漏。应用层:你通过推理 API 拿到的租户隔离,是部署方用版本号、路由器和一堆环境变量换来的——选自托管方案时,安全披露的响应速度应该进选型表。