🎯 文章概述
这篇文章来自智谱 AI (GLM 团队),记录了他们在服务 GLM-5 系列模型应对超大规模 Coding Agent 任务时,遭遇并解决的一系列底层系统级 race condition 问题的全过程。这些问题——乱码输出、复读循环、生僻字生成——仅在高并发、长上下文的生产负载下出现,标准测试环境完全无法复现。
文章的价值远超普通的 bug 修复记录。它揭示了当大模型应用从简单对话迈向复杂的长程 Coding Agent 任务时,推理基础设施面临的全新挑战:Scaling 的瓶颈不仅在于模型参数和数据规模,更在于支撑它的系统工程极限。作者将这一过程称为 "Scaling Pain"—— Scaling Law 信仰驱动下的必经阵痛。
🔍 第一章:从线下复现到异常识别
自今年三月起,团队通过线上监控和用户反馈观察到 GLM-5 的三类异常现象。排查的第一步是区分"模型问题"与"基础设施问题":如果异常由模型引起,它应当对特定输入稳定可复现;如果与系统压力或运行时状态相关,则更指向推理基础设施的 bug(调度、状态管理、缓存一致性)。
复现的艰难之路
- 本地回放无效: 对用户反馈的 bad cases 做本地回放,同一批请求重复推理数百次,始终未能复现异常。说明大概率不是模型本身的问题。
- 全量日志回放仍无效: 对线上日志脱敏后保留原始并发分布和请求时序,在本地全量回放,起初仍未复现。
- 关键突破: 直到调整 PD (Prefill-Decode) 分离比例并持续提高系统负载,模拟高峰期的 Prefill 堆积和 Decode 侧 KV Cache 压力后,才在约每万次请求中稳定复现 3–5 次异常。
核心特征: 异常"与请求内容无关、与系统压力相关"。这强烈暗示问题来自高负载下的推理状态管理,而非模型权重本身。
异常检测的意外武器:投机采样指标
三类异常中,复读相对容易检测,但乱码与生僻字非常棘手。启发式方法(正则、字符集匹配)存在明显漏判与误伤,模型判别方式又太慢,无法满足大规模消融实验的效率要求。异常检测本身成为定位流程的瓶颈。
团队在反复分析推理日志后,发现了一个意想不到的信号:投机采样 (Speculative Decoding) 的指标可以作为异常检测的重要参考。
乱码 & 生僻字 通常伴随极低的 spec_accept_length,意味着草稿模型提出的候选 token 几乎被目标模型全部拒绝。这暗示目标模型与草稿模型的 KV Cache 状态存在严重不匹配。
复读 通常伴随极高的 spec_accept_rate,暗示损坏的 KV Cache 导致注意力模式退化,将生成推向高置信度的重复循环。
基于这一发现,团队实施了在线异常监控策略:当生成序列超过 128 tokens 后 spec_accept_length 持续低于 1.4,或 spec_accept_rate 超过 0.96 时,系统主动终止当前生成并将请求交回负载均衡器重试。这一策略将投机采样从纯性能优化扩展为输出质量的实时监控信号。
🐛 BugFix #1:PD 分离架构下的 KV Cache 竞态
观察到异常输出与并发压力具有明显相关性后,团队进一步分析请求生命周期以及 PD 分离执行时序,发现问题的根源在于请求生命周期与 KV Cache 回收/复用时序之间的不一致。
根因分析:异步 Abort 引发的 KV Cache 复用竞态
为限制尾延迟 (TTFT),推理引擎引入了基于超时的请求终止机制:当 Prefill 阶段未在规定时间内完成时,Decode 侧会对请求执行 Abort,并回收其占用的 KV Cache 资源。然而,这个设计存在致命的同步缺失:
- Abort 信号未正确传播至 Prefill 侧:Decode 侧单方面决定终止请求,但 Prefill 节点对此一无所知。
- Decode 侧缺乏安全回收的充分信息:它无法判断 KV Cache 是否仍正在被 Prefill 侧的 RDMA 写入操作使用。
- 内存复用导致跨请求污染:Decode Abort 并将 KV Cache 空间分配给新请求后,Prefill 侧先前已发起的 RDMA 写入以及正在执行的计算仍持续执行,未被同步取消。
时序破坏链:
1. Req1 被分配至 Prefill-1 (P1) 和 Decode。P1 因排队延迟未及时开始 Prefill。
2. Decode 侧超时,Abort Req1 并回收其 KV Cache 槽位(未通知 P1)。
3. 新请求 Req2 到达,被分配到与 Req1 相同的 KV Cache 物理地址。
4. P2 完成 Req2 的 Prefill 并将 KV Transfer 至 Decode,Decode 开始生成。
5. P1 侧针对 Req1 的 RDMA 写入仍在继续,覆盖 Req2 的 KV Cache 数据。
6. Req2 在 Decode 阶段读取到被污染的数据,输出异常。
修复方案:KV Cache 释放的时序一致性保证
为消除竞态,团队在推理引擎中引入了更严格的时序约束:在请求终止与 KV Cache 写入完成之间建立显式同步关系。
同步协议:
1. Decode 触发 Abort 后,向 Prefill 侧发送通知。
2. Prefill 仅在以下条件满足时返回"可释放"信号:(a) 相关 RDMA 写入尚未开始,或 (b) 所有已提交写入均已完成。
3. Decode 仅在收到该确认后,才允许回收并复用对应的 KV Cache 槽位。
修复效果: 异常输出率从约 0.1% 降至 0.03% 以下。这一结果证明,在 PD 分离架构中,跨节点 KV Cache 传输与内存复用之间的显式一致性保证是避免此类问题的必要条件。
🐛 BugFix #2:HiCache 加载时序缺失
Coding Agent 场景显著提高了输入长度(平均超过 70K tokens),同时伴随较高的前缀复用率。这类负载使 HiCache(多级 KV Cache) 成为线上服务中的关键优化手段。然而,在 KV Cache 换入与计算重叠执行的情况下,当前实现未能保证数据在使用前已完成加载。
根因分析:流水线同步缺失导致的 Read-Before-Ready
通过对 HiCache 执行时序的分析,团队将问题定位在 DSA-based HiCache 的缓存读取路径上。系统从 CPU 内存异步换入(swap-in)历史前缀缓存,并通过 Load Stream 与 Forward Stream 的重叠执行来提高吞吐。
- Load Stream 负责加载 KV Cache 与 Indexer Cache。
- Forward Stream 依次执行 Index 计算与后续的 Sparse Attention。
- 关键缺失: Indexer 算子在启动时未对 "Load Indexer Cache 的完成" 建立同步约束。
- 后果: Forward Stream 可能先于 Load Stream 完成数据加载而开始执行,出现 Read-before-Ready 的访问模式——在数据尚未完成加载时即被读取。
该问题导致 Index 计算基于不完整或未初始化的 KV Cache 数据执行,错误传播至后续 Sparse Attention,最终反映为输出异常。
修复方案:重构算子流水线的原子性
团队对 HiCache 的读取流水线进行了重构,在数据加载与计算之间引入显式的同步约束,确保 Forward Stream 中的 Indexer 计算仅在对应 Indexer Cache 完全加载后才启动。修复后,在相同负载条件下,由执行时序不一致导致的异常被完全消除。
社区贡献: 该修复已作为 PR 提交至 SGLang 开源社区(PR #22811),惠及更广泛的推理生态。
⚡ 优化:KV Cache 分层存储 LayerSplit
上述两个竞态条件揭示了一个共同的系统瓶颈:在长上下文 Coding Agent 服务负载中,Prefill 阶段已成为系统性能的主导因素。为控制 Prefill 排队导致的 TTFT,团队引入了基于超时的 Abort;为缓解 Prefill 侧的 KV Cache 容量压力,引入了 HiCache。在修复这些状态一致性问题后,团队回到瓶颈本身:如何在减少 Prefill 侧 KV Cache GPU 内存压力的同时提升吞吐量?
问题:SGLang 的 KV Cache 冗余存储
Coding Agent 负载呈现出上下文长度较长、Prefix Cache 命中率较高的特征。在这一场景下,Context Parallel (CP) 成为 Prefill 节点的主要并行策略。然而,现有 SGLang 开源实现存在 KV Cache 冗余存储 的问题:每张 GPU 都保存全部层的 KV Cache,导致有限的 KV Cache 容量成为 GPU 计算资源利用率的瓶颈。
LayerSplit 方案设计
- 分层存储: 每张 GPU 不再保存全部层的 KV Cache,而是仅持有部分层的 KV Cache,显著降低单卡显存占用。
- 协同计算: 不同 CP rank 协同完成 Prefill。持有某一层 KV Cache 的 rank 在执行 Attention 计算前,将该层 Cache 广播给其他相关 rank。
- 通信隐藏: 设计了 KV Cache 广播与 indexer 计算的重叠机制,使二者在时间上相互掩盖,隐藏通信延迟。
- 低开销: 整个流程中仅引入 Indexer Cache 广播的额外开销,其规模约为 KV Cache 的 1/8,整体通信成本对性能影响可忽略。
性能提升
在 90% 缓存命中率、请求长度从 40K 到 120K tokens 的测试条件下,LayerSplit 带来的系统吞吐量提升为 10% 至 132%,且上下文越长增益越大。这一优化显著增强了 Coding Agent 工作负载下的系统吞吐能力。
💡 核心洞察与方法论总结
- 生产级问题的特征是与压力相关而非与输入相关。 当异常无法在本地稳定复现时,应怀疑系统状态管理而非模型权重。模拟真实并发分布和请求时序是复现的关键。
- 性能指标可以兼职质量监控。 投机采样的 accept_length/accept_rate 原本是性能优化指标,却被创造性地转化为异常检测信号。这提示我们:系统各组件的"不正常行为"往往会在其自身指标中留下痕迹。
- 分布式系统中的显式同步是不可省略的。 两个 bug 的共同根源都是"隐式假设某个操作已完成"——PD 分离中假设 Abort 后写入已停止,HiCache 中假设加载已完成。在高并发下,这些假设必然被打破。
- 修复竞态后应回到瓶颈本身做优化。 团队没有止步于修 bug,而是从"为什么需要这些复杂机制"出发,设计了 LayerSplit 从根本上减少 Prefill 侧内存压力,消除产生竞态的土壤。
- 开源回馈: HiCache 修复已提交至 SGLang 社区,体现了基础设施问题需要社区协作解决的理念。
📝 总结评价
这是一篇极其珍贵的生产系统排障实战记录。它没有停留在抽象的架构讨论,而是深入到具体的 race condition 时序分析、内核流水线同步约束、以及 RDMA 写入与内存复用的交叉边界。
文章最有价值的贡献在于揭示了 Scaling Law 的一个常被忽视的侧面:当模型能力跨越某个应用阈值后,推理基础设施的可靠性挑战会呈非线性放大。Coding Agent 不是"对话+代码补全"的简单延伸,而是对系统并发、长上下文管理、KV Cache 生命周期、跨节点一致性提出了全新量级的考验。
"当智能真正进入高并发、长上下文的 Coding Agent 场景后,推理基础设施的挑战已经不只是吞吐、延迟和可用性,维护它的输出质量变得至关重要。每一次对 Scaling Law 的追求,都必须有同等强度的系统工程作为支撑。"
— 智谱 AI GLM 团队
对于正在构建或运营大模型推理服务的工程师来说,这篇文章提供了可直接借鉴的方法论:如何用投机采样指标做异常检测、如何在 PD 分离架构中保证 KV Cache 时序一致性、如何设计分层 KV Cache 以降低内存压力。这些经验的价值,不亚于任何一个新的模型架构改进。