AI

刚打开 sglang 代码我就已经发现了

Posted by w@hidva.com on July 16, 2026

前言

最近半年起, 我读代码的体验被 AI 拉高了整整一个数量级!

在这之前, 我其实有过好几段啃大型项目源码的经历: linux kernel, PostgreSQL/Greenplum, NCCL 等等. 那时候读代码最痛苦的地方, 倒不是某一处逻辑有多难, 而是那种一叶障目, 盲人摸象的无力感——你看到一个字段, 不知道它到底是干嘛的; 你看到一个 if 分支, 不知道它为什么要存在, 更不知道它是为了兜住哪个犄角旮旯里的 case. 你只能一处一处地啃, 啃到最后脑子里全是碎片, 却始终拼不出一张全局的图. 更别提读着读着还会陷进某个细节里出不来, 抬起头才发现主干早跟丢了.

现在完全不一样了. 有了 AI, 我可以先从全局入手: 这个模块大概分几层? 这条链路的主干是什么? 这个字段是谁写的, 又是谁在消费? 先把骨架搭起来, 再往里填细节, 遇到看不懂的分支随手就能问一句”这里为什么要这么写”. 尤其是最近半年, 我用 AI 读 verl, sglang 这些项目源码时, 那种幸福感真是无以言表.

代码读得多了, 就会不由自主地在读代码的同时大脑开始想象链路如何执行,数据如何流动,状态如何该更,也会自然而然发现一些问题. 之前读 vllm async scheduling 那条链路时, 我就一口气 读出了 3 个 bug; 这次在学习 sglang 时, 一边读一边把觉得可疑的地方记下来, 读着读着, 手里居然攒了 13 个问题:

# 简述 子系统
1 add_chunked_reqextend_input_len 未按 page_size 对齐 调度 (PrefillAdder)
2 EAGLE verify 在 topk>1 时 batch size 反推错误 推测解码
3 init_load_back H2D 分配失败后,前缀节点缺失 mamba state 仍被当作已算 HiMambaRadixCache
4 storage prefetch 在 last_hash=None 时页 hash 链算错,命中失效/错配 HiMambaRadixCache / storage
5 _insert_helper_host 部分加载时 mamba_host_value 与实际 loaded 前缀不一致 HiMambaRadixCache
6 prefetch 时保护 old mamba host 的时机晚于分配新 slot HiMambaRadixCache
7 dec_lock_ref 在锁定窗口内节点状态变化时多减 full KV lock HiMambaRadixCache
8 tombstone leaf 在 host_ref_counter>0 时被删,后续 assert v==node 可能失败 HiMambaRadixCache
9 write_backup 对已在 host 的 mamba 重复发起 D2H HiMambaRadixCache
10 evict() 未回滚 full-kv 附带的 mamba_num_evicted,可能过度驱逐 HiMambaRadixCache
11 HybridReqToTokenPool 映射 tensor 首维用了 max_mamba_cache_size 内存池
12 load_video 调用把 frame_count_limit 误绑到 use_gpu 形参 多模态
13 draft chunked prefill 中间块 shift 补的是采样 token 而非真实下一 prompt token 推测解码

但人脑执行终究是人脑执行, 它有个致命的弱点: 我以为的, 未必是真的. 我盯着代码推演出来的 bug, 换成电脑真刀真枪地跑一遍, 到底会不会复现? 那些我觉得”这里肯定越界”“这里断言肯定挂”的地方, 真的会挂么? 一个自然而然的想法: 我能不能让一个 AI agent 帮我把这些问题挨个验证一遍? 让它去读代码、构造场景、写复现, 看看电脑执行的结论和我人脑执行的结论到底对不对得上. 本文用的 agent 是 qodercli + qwen3-max(400K 上下文). 下面几节就是它给出的复现报告. 所用 prompt:

请帮我验证位于 /zhanyi/docs/sglang/sglang.bug.md 文件中记录的 bug 1 是否在当前 main 分支中仍然存在。最接近我学习的版本是社区 v0.5.10.post1 tag。

SGLang 启动命令如下:

python -m sglang.launch_server --model-path /mnt/models/Qwen3-Next-80B-A3B-Instruct --tp 2 --port 30000 --chunked-prefill-size=256 --page-size=64 --disable-cuda-graph --mamba-radix-cache-strategy extra_buffer --enable-hierarchical-cache --hicache-ratio=1 --hicache-mem-layout page_first_direct

具体要求:

  1. 如有必要,可以通过修改源码来模拟可能发生的场景(例如手动调用驱逐代码来模拟驱逐场景),但必须确保代码模拟的情况是可能在线上实际场景中出现的,修改源码的目的仅为提高复现概率。不要加 assert 这种代码,这种代码对问题复现没有帮助。
  2. 由于 SGLang 是 editable build,修改源码后重启服务即可使更改生效
  3. 仅进行 bug 复现和验证,不需要修复问题
  4. 完整保留所有运行命令、服务启动日志、代码修改记录(可通过 git commit 或创建 branch 的方式保存)以便后续审查
  5. 如果需要 L3 backend,可以考虑使用 fake file backend 实现:put 操作直接写入本地文件,get/exists 操作查询本地文件
  6. 鼓励使用各种 subagent 来避免主 agent 窗口被污染
  7. 记录完整的复现过程、观察到的现象以及相关证据,包括但不限于错误日志、性能指标变化等
  8. 对于每个 bug,请明确说明其状态:已修复、仍存在、或无法复现
  9. 提供详细的测试步骤和环境信息以确保结果可重现

环境:lmsysorg/sglang:nightly-dev-cu12-20260715-50d1edaa

bug1:Chunked Prefill 与 Page 对齐

SGLang 的 chunked prefill 会把一个长 prompt 拆成多轮 extend 来执行,每轮处理 chunked_prefill_size(比如 256)个 token。与此同时,KV cache 的分配是以 page_size(比如 64)为单位的——一个 page 里放 64 个 token 的 KV 索引。这就引出了一个关键约束:每一轮 chunk 参与计算的 token 数必须是 page_size 的整数倍。 否则 radix tree 的 key 无法对齐到 page 边界,下游的 mamba SSM state 跟踪也会出现错位。在 PrefillAdder 里,处理新请求的 add_one_req 方法是知道这个约束的:

# schedule_policy.py, add_one_req 的 chunked 路径
trunc_len = self.rem_chunk_tokens // self.page_size * self.page_size
# 显式 floor 对齐到 page_size

但处理”上一轮遗留的 chunked 请求”的 add_chunked_req 方法,却完全忘了这回事:

# schedule_policy.py:827, add_chunked_req
new_len = min(cand_extend_input_len, _rem_tokens)
# 没有任何 page_size 对齐!

这里的 _rem_tokens = min(rem_chunk_tokens, int(rem_total_tokens)),其中 rem_total_tokens 是当前 KV 池的可用容量(available + evictable - offset)。这个值是个运行时动态变化的数,几乎不可能是 page_size 的整数倍。一旦 rem_total_tokens < rem_chunk_tokens(也就是 KV 池吃紧、成为瓶颈时),new_len 就会被截成一个非对齐值。

触发后果:Mamba State 错位。add_chunked_req 设置了非对齐的 extend_range 后,下游的 mamba state 跟踪链路会把这个错位一路放大。具体来说:mamba_last_track_seqlen 被计算为非 page 对齐值:这个字段记录”mamba SSM 状态实际覆盖了多少个 token”。它是基于 extend_range.length 按 mamba_cache_chunk_size 对齐的,但如果 prefix_indices 本身就带着上一轮遗留的非对齐尾巴,对齐后的结果仍然不是 page_size 的整数倍。Radix tree 插入时静默截断:UnifiedRadixCache.cache_unfinished_req 会把 radix key 用 .page_aligned() 截断到 page 边界。结果就是:mamba state 覆盖 420 个 token 的 SSM 隐状态,但 tree 节点的 key 只覆盖 384 个 token。 那多出来的 36 个 token 的 state 信息,就这么凭空”超前”了。

后续请求复用 prefix cache 时加载到错误的 mamba state:当另一个请求的前缀恰好匹配到这个 384-token 节点时,它会从节点上拿到那个”420-token 版本”的 mamba state,然后以此为基础继续生成。SSM 层从一个错误的隐状态出发,而 attention 层却从正确的 384-token KV cache 出发——两种层看到的历史不一致,输出自然就跑偏了。

验证证据:我让 agent 用 Qwen3-Next-80B-A3B-Instruct(GDN 混合线性注意力 MoE 模型)+ –chunked-prefill-size=256 –page-size=64 的配置做了完整的复现。为了模拟 KV 池压力,在 add_chunked_req 里把可用 token 数 cap 到 100(不是 64 的倍数),这在逻辑上等价于线上 KV 池吃紧时 rem_total_tokens 自然落到非对齐值。

证据 1:日志直接捕获 mismatch。在 UnifiedRadixCache.cache_unfinished_req 里加了一行对比日志,每次 chunked prefill 插入 radix tree 时输出 mamba state 对应的 token 长度和实际插入 tree 的 token 长度:

[BUG1-REPRO] cache_unfinished_req: mamba_state_seqlen=420 != tree_insert_len=384 (dropped 36 tokens)
[BUG1-REPRO] cache_unfinished_req: mamba_state_seqlen=520 != tree_insert_len=512 (dropped 8 tokens)
[BUG1-REPRO] cache_unfinished_req: mamba_state_seqlen=620 != tree_insert_len=576 (dropped 44 tokens)
[BUG1-REPRO] cache_unfinished_req: mamba_state_seqlen=720 != tree_insert_len=704 (dropped 16 tokens)

一次 800 token 的请求里就出现了 4 次 mismatch,dropped 范围为 8~44 tokens。mamba state 始终”跑在” tree key 前面。

证据 2:Tensor 级铁证。最硬的证据来自直接 dump mamba state 的 temporal tensor(shape [36, 16, 128, 128],即 36 个 mamba 层 × 16 通道 × 128×128 隐状态,单张约 36MB)。在 buggy 和 clean 服务器上分别运行相同的请求序列,把 follow-up 请求从 radix tree 节点加载到的 mamba state 张量存下来做逐元素对比:

prefix_len cosine similarity max element diff 结论
384 0.871 1.57 部分相似(错位 36 tokens)
512 0.001 9.50 完全不同
640 0.001 7.94 完全不同
768 0.816 1.55 部分相似
896 0.000 9.19 完全不同
1024 -0.000 8.96 完全不同(几乎正交)

cosine similarity 接近 0 甚至为负意味着,buggy 服务器加载到的 mamba 隐状态向量和 clean 服务器的几乎正交——它们描述的是完全不同的上下文历史。考虑到这些 tensor 的标准差只有约 0.015,max diff 达到 9.5 相当于偏差超过 600 个标准差。这不是数值噪声,不是浮点精度的小扰动,而是 SSM 隐状态被根本性地替换成了一个错误的值。

后语

(知道为什么我只贴了一个么!因为剩下 12 个 main 分支都已经修复了!!!羞=。=

工欲善其事, 必先利其器——我一直觉得, 在你打算介入一个项目、去做新特性或者 bugfix 之前, 一定要先把整体链路(尤其是那些边界情况)摸清楚. 不是”大概知道数据从哪进从哪出”那种熟, 而是能在脑子里把边界条件、异常路径、并发时序都过一遍那种熟. 上面这 14 个问题, 几乎全都藏在边界上: page_size 对齐、topk > 1 的树形草稿、分配失败的回退分支、并发窗口里的驱逐时机——正常路径跑一万遍都碰不到, 可一旦踩中就是正确性问题.

AI 火爆的这两年, 想赶紧在 AI infra 里做出点东西的团队和个人特别多, 我看到最多的情形是: 对整条链路的理解还没搭全, 就先上手了; 短期看确实快, demo 也跑得挺漂亮. 只是功能一点点叠上去之后, 最终在长稳测试里炸,在精度测试里漂,在线上出各种莫名其妙的问题。

说到底, 读代码本身不是目的, 在脑子里建立起对系统完整状态空间的心理模型, 才是目的. 有了它, 你写下的每一行代码才清楚自己在兜住哪个 case; 少了它, 就容易掉进按下葫芦浮起瓢的循环.