Appearance
EAGLE × HiCache × Mooncake 故障面分析
从 SGLang Issue #28873 看 Draft KV 的内存所有权、跨进程注册与失败回滚。
结论先行
SGLang Issue #28873 报告了一个 v0.5.13 回归:同时启用 EAGLE speculative decoding、HiCache L3 和 Mooncake standalone client 时,Scheduler 会在启动阶段崩溃;相同配置在 v0.5.12 可以启动。
这不是 RDMA 数据传输本身失败,也不是 EAGLE 推理计算错误。故障发生在服务初始化阶段,尚未进入请求执行和 KV 读写数据路径。
最小充分根因由两部分组成:
- 分配域不一致:Target Host KV Pool 使用 Mooncake allocator,位于 standalone real client 可映射的共享内存段中;新增 Draft Host KV Pool 没有继承这一 allocator,回退到普通 mmap。
- 注册流程非事务化:Draft Pool 的内部状态先被写入,随后才执行
register_buffer();外部注册失败后没有完整撤销引用、回调和资源。
因此,一个本应局限在 Draft buffer 注册阶段的错误,被放大为 Scheduler 初始化失败和整棵进程树退出。
日期说明
Issue 页面显示其创建日期为 2026 年 6 月 22 日。本文以 Issue 中的 v0.5.13 traceback 为故障基线,并参考 2026 年 6 月 24 日的 SGLang 主干代码解释调用关系;两者行号不应机械对应。
故障链路
把控制路径和内存边界放在同一张图里,问题会比按模块逐个阅读更清晰:
text
Request
│
▼
Scheduler
│ 启动阶段创建、挂载并注册 Draft Host KV
▼
EAGLE Draft Worker
│ 持有 Draft Model GPU KV;不是本次被拒绝的 buffer
▼
Draft KV Pool
│ 新建 CPU Host KV;错误地落到普通 mmap 分配域
▼
HiCache Controller
│ 先保存 Draft 引用,再触发存储后端注册
▼
Mooncake Dummy Client
│ 将 ptr / size 转发给 standalone real client
▼
Shared Memory / RDMA
real client 发现指针不属于已注册共享段
register_buffer() → -1 → RuntimeError
→ Scheduler 构造失败 → SIGQUIT → 进程树退出实际调用路径可以压缩为:
text
Scheduler.__init__
→ maybe_register_hicache_draft()
→ CacheController.set_draft_kv_pool()
→ _maybe_register_draft_with_storage()
→ MooncakeStore.register_mem_host_pool_v2(..., DRAFT)
→ register_buffer(draft_host_pool.kv_buffer)
→ store.register_buffer(ptr, size) == -1
→ RuntimeError关键边界位于 Mooncake Client:standalone 模式下,SGLang 进程中的 Dummy Client 不能让任意本地 mmap 指针自动获得跨进程意义。被注册的 buffer 必须来自 real client 预先建立、双方都能识别的共享内存域。
逐层看所有权和风险
| 层级 | 主要职责 | Buffer 创建与所有权 | 生命周期 | 本次风险 |
|---|---|---|---|---|
| Request | 触发推理请求 | 不创建 Draft Host KV | 请求级 | 服务在接收请求前已经失败 |
| Scheduler | 初始化 KV cache 和 Controller | 持有 Worker、Tree Cache、Controller | Scheduler 进程级 | 初始化异常升级为进程退出 |
| EAGLE Draft Worker | 维护 Draft Model GPU KV | Draft runner 创建 GPU KV pool | Model / Worker 级 | GPU KV 不是被 Mooncake 拒绝的 host buffer |
| Draft Host KV Pool | 提供 Draft KV 的 L2/L3 host 镜像 | SGLang Host KV 构造路径创建 | 由 Scheduler 路径持有 | 未继承 Mooncake allocator,落到普通 mmap |
| HiCache Controller | 连接 Draft cache 与存储后端 | 不直接创建 buffer,但保存 pool 引用 | Controller 级 | 注册成功前已有部分内部状态可见 |
| Mooncake Client | 注册 host buffer,连接存储与传输引擎 | standalone real client 拥有共享段 | 外部进程级 | Dummy Client 无法注册共享段之外的指针 |
| Shared Memory / RDMA | 跨进程映射与远程传输 | Mooncake allocator / real client 创建 | 映射与 MR 注册级 | 地址合法性是传输前置条件,故障时 RDMA 尚未开始 |
这里最容易混淆的是两块不同的 Draft KV:
- EAGLE Draft Worker 的 GPU KV Pool 用于推测模型计算。
- HiCache 新建的 CPU Draft Host KV Pool 用于 L2/L3 offload。
Mooncake 拒绝的是第二块内存。
根因一:Target 与 Draft 落在不同分配域
Target HiCache Host Pool 构造时会显式传入 storage backend。当 backend 是 Mooncake 时,allocator 路径会选择 MooncakeHostTensorAllocator,其底层内存来自 Mooncake 管理的共享内存。
新增 Draft Host Pool 的构造路径复制了容量比例、数据大小、page size 和 layout 等参数,却没有同步 allocator 类型,于是回退到默认 HostTensorAllocator,最终使用普通 mmap。
| 对象 | allocator 选择 | standalone 下能否注册 |
|---|---|---|
| Target Host KV | MooncakeHostTensorAllocator | 可以,buffer 位于 real client 的共享内存域 |
| Draft Host KV | 默认 HostTensorAllocator → mmap | 不可以,指针不属于 real client 已注册共享段 |
这也解释了为什么故障只在特定组合出现:
| 配置组合 | 结果 | 原因 |
|---|---|---|
| EAGLE On,L3 Off | 正常 | 不进入 Mooncake Draft L3 注册路径 |
| EAGLE Off,Mooncake L3 On | 正常 | 只有 allocator 正确的 Target Host Pool |
| EAGLE On,Mooncake embedded | 预期正常 | allocator 与 real client 位于同一进程或映射域 |
| EAGLE On,Mooncake standalone | 失败 | Draft mmap 指针跨进程交给 Dummy Client 注册 |
因此,修复不能只围绕“忽略 -1”展开。首先必须确保 Draft Host Pool 与 Target Host Pool 使用同一套 storage allocator 策略。
根因二:注册状态不是原子提交
当前故障还暴露了第二个独立问题:注册流程先修改内部状态,再执行可能失败的外部操作。
概念化后的顺序如下:
python
registered_pools[DRAFT] = host_pool # 内部状态已经可见
register_buffer(host_pool.kv_buffer) # 外部调用可能失败
draft_page_get = page_get_v2 # 只有成功后才会继续设置
draft_page_set = page_set_v2当 register_buffer() 返回 -1 时,异常向上传播,但已经写入的 pool 映射、Controller 引用和已分配资源没有在同一事务边界内完整撤销。
allocator 错误是这次复现的必要条件;非事务注册则是故障放大的条件。只修 allocator 可以解决当前地址域问题,但未来任何注册异常仍可能留下半初始化状态。
建议的注册与回滚模型
建议把 Draft L3 注册设计成五个明确阶段:
- 分配:Draft Host Pool 继承 Target Host Pool 的 allocator 类型,或显式使用
hicache_storage_backend。 - 预检:standalone 模式下检查 allocator 类型和 buffer 所属地址域,在进入外部注册前给出可操作错误。
- 外部注册:先注册 buffer,只记录临时注册清单,不修改正式 pool 映射和 Controller 回调。
- 提交:所有 buffer 成功后,再一次性提交
registered_pools、Controller refs 和draft_page_get/set。 - 回滚:失败时逆序 unregister,清空临时状态,销毁本路径创建的 host pool,再按显式策略降级或终止启动。
python
draft_host_pool = allocate_with_storage_allocator()
registered = []
try:
validate_address_domain(draft_host_pool)
registered = register_all_buffers(draft_host_pool)
commit_draft_l3_state(draft_host_pool)
except Exception:
unregister_in_reverse_order(registered)
clear_draft_l3_refs_and_callbacks()
draft_host_pool.destroy()
fallback_to_l2_or_raise_by_policy()对应状态机应保持以下不变量:
| 状态 | 允许存在的对象 | 失败动作 |
|---|---|---|
ALLOCATED | 只有局部 draft_host_pool,Controller 不可见 | destroy host pool |
REGISTERING | 只有临时注册清单,不写正式映射 | 逆序 unregister,再 destroy |
COMMITTED | pool、refs、callbacks 同时有效 | 运行期 detach 走完整清理 |
FALLBACK_L2 | 保留 EAGLE 与 L2,Draft L3 callbacks 为空 | 输出 warning 和 metric |
FAILED | 无 Draft L3 残留引用或注册 | 返回明确的启动错误 |
降级还是启动失败
这个选择应该由配置明确表达:
- 用户显式要求 Mooncake L3,并依赖持久化或跨实例命中时,默认应 fail closed,避免服务悄悄运行在不同语义下。
- 如果提供类似
allow_draft_l3_fallback的显式开关,可以关闭 Draft L3,保留 EAGLE + L2,但必须产生结构化日志、metric 和 cache report。
最小回归测试矩阵
PD、TP、EAGLE、L3、client mode、transport、重启和故障注入存在大量组合。完整笛卡尔积成本高,而且会让真正关键的交互被重复的启动用例淹没。
最小矩阵应覆盖三个边界:
EAGLE × L3:是否进入新增 Draft 注册路径。standalone × allocator:buffer 是否属于跨进程共享域。register failure × rollback:异常后是否恢复到稳定状态。
| Case | PD | TP | EAGLE | L3 / Client | 主要目的 | 关键断言 | 建议层级 |
|---|---|---|---|---|---|---|---|
| R0 | Off | 1 | On | L3 Off | EAGLE + L2 baseline | 启动、请求和 accept length 正常 | PR GPU |
| R1 | Off | 1 | Off | Mooncake standalone | 隔离 Target Host Pool | Target 注册成功,L3 写入和读回命中 | PR GPU |
| R2 | Off | 1 | On | Mooncake embedded | 验证 Draft L3 基础功能 | Target + Draft 注册,重启后命中 | PR / Extra |
| R3 | Off | 1 | On | Mooncake standalone | Issue #28873 最小正向回归 | health ready,无 -1,Draft buffer 位于共享域 | PR GPU |
| R4 | Off | 2 | On | Mooncake standalone | 覆盖多 rank allocator 和 key 空间 | 所有 rank ready,无 pointer / key 串扰 | PR / Nightly |
| R5 | P1 + D2 | 2 | Decode On | standalone + RDMA | 覆盖生产拓扑、重启和 loadback | P/D 启动、L3 命中、优雅退出 | Nightly |
此外还需要一个独立、确定性的负向单测:
| 故障注入 | 预期回滚断言 |
|---|---|
Draft 第一个 buffer 返回 -1 | DRAFT 不在正式映射中;callbacks 为空;host pool 只销毁一次 |
| 多 buffer 的第 N 个失败 | 前 N-1 个临时注册被逆序撤销;无双重释放 |
| fallback policy 开启 | 服务进入 ready;Draft L3 disabled metric 为 1;请求语义不变 |
正向 R3 证明真实 allocator 域正确;负向单测证明所有未来注册错误都不会留下脏状态。两者不能互相替代。
建议补充的 3 个 CI Case
CI-1:CPU 单测——Draft 注册事务原子性
使用 Mock Host Pool 和 Fake Mooncake Store,对 register_buffer() 注入 -1,断言:
- 正式注册映射中不存在
DRAFT; - Controller 的 Draft L3 引用和 callbacks 为空;
- 已完成的注册被逆序撤销;
destroy()只调用一次;- fail-closed 和 fallback 策略分别符合配置。
补充原因:这是本次事故机制最短、最稳定的保护链。E2E 通常只验证成功路径和 HTTP 结果,无法精确检查异常后的内部状态。CPU 单测运行快、定位直接,也能防止其他 backend 重复出现“先提交、后失败”的问题。
CI-2:PR GPU Smoke——standalone × EAGLE × Mooncake
使用 TP1,并在资源允许时增加 TP2;启用 EAGLE、Mooncake standalone 和本机共享内存或 TCP 传输。发送两次具有固定长前缀的请求,检查:
- 服务 health ready;
- Target 与 Draft buffer 注册成功;
- L3 cached tokens 增长;
- speculative accept length 不异常回退;
- 每个 TP rank 的 allocator 类型和注册数一致。
补充原因:Mock 不能证明真实指针位于 Mooncake 共享段,也无法发现 rank 间 allocator 参数差异、外部 client 生命周期或 key 命名冲突。这个用例能直接阻止同类 allocator 参数遗漏再次合入。
CI-3:PD / RDMA Nightly——生产拓扑的重启、命中与退出
采用 Prefill 1 rank + Decode 2 ranks,Decode 启用 EAGLE,连接 Mooncake standalone real client,并使用 RDMA。写入长前缀后重启 SGLang,再验证 L3 loadback、accept length 和 teardown。
补充原因:PD 与 RDMA 同时引入进程拓扑、rank 命名、共享段映射、服务重启和资源释放,这些风险不能由 PR 中的单进程 smoke 完全代理;但每次 PR 执行成本过高,因此适合 nightly 或发布前门禁。
修复验收标准
- Draft Host Pool 与 Target Host Pool 使用相同的 storage allocator 策略。
- standalone 模式下,Draft buffer 位于 Mooncake real client 可映射的共享内存域。
- 任意
register_buffer()失败后,正式映射中不存在DRAFT,Controller refs 和 callbacks 为空。 - 本路径创建的 host pool 被准确销毁一次,无泄漏、重复 unregister 或悬挂引用。
- fallback 模式可进入 ready,但日志、metric 和 cache report 明确显示 Draft L3 已关闭。
- fail-closed 模式输出 allocator 类型、client mode、buffer 大小和失败阶段。
- TP 各 rank、PD 各角色的注册和 key 空间互不污染。
- 启动失败和正常关闭都不会遗留 mooncake client、共享段或僵尸 Scheduler。
实施优先级
| 优先级 | 工作项 | 完成标志 |
|---|---|---|
| P0 | 传播 allocator_type,复用 Target allocator 策略 | R3 standalone 正向回归通过 |
| P0 | Draft 注册改成事务式提交与回滚 | CI-1 故障注入全部通过 |
| P1 | 增加 TP2 standalone PR smoke | 所有 rank 注册、请求和读回稳定 |
| P1 | 明确 fail-closed / fallback 配置与指标 | 行为可配置、可观测、可文档化 |
| P2 | 增加 PD / RDMA restart-loadback nightly | 生产拓扑长期无回归 |