Skip to content

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 读写数据路径。

最小充分根因由两部分组成:

  1. 分配域不一致:Target Host KV Pool 使用 Mooncake allocator,位于 standalone real client 可映射的共享内存段中;新增 Draft Host KV Pool 没有继承这一 allocator,回退到普通 mmap。
  2. 注册流程非事务化: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、ControllerScheduler 进程级初始化异常升级为进程退出
EAGLE Draft Worker维护 Draft Model GPU KVDraft runner 创建 GPU KV poolModel / 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 KVMooncakeHostTensorAllocator可以,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 注册设计成五个明确阶段:

  1. 分配:Draft Host Pool 继承 Target Host Pool 的 allocator 类型,或显式使用 hicache_storage_backend
  2. 预检:standalone 模式下检查 allocator 类型和 buffer 所属地址域,在进入外部注册前给出可操作错误。
  3. 外部注册:先注册 buffer,只记录临时注册清单,不修改正式 pool 映射和 Controller 回调。
  4. 提交:所有 buffer 成功后,再一次性提交 registered_pools、Controller refs 和 draft_page_get/set
  5. 回滚:失败时逆序 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
COMMITTEDpool、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、重启和故障注入存在大量组合。完整笛卡尔积成本高,而且会让真正关键的交互被重复的启动用例淹没。

最小矩阵应覆盖三个边界:

  1. EAGLE × L3:是否进入新增 Draft 注册路径。
  2. standalone × allocator:buffer 是否属于跨进程共享域。
  3. register failure × rollback:异常后是否恢复到稳定状态。
CasePDTPEAGLEL3 / Client主要目的关键断言建议层级
R0Off1OnL3 OffEAGLE + L2 baseline启动、请求和 accept length 正常PR GPU
R1Off1OffMooncake standalone隔离 Target Host PoolTarget 注册成功,L3 写入和读回命中PR GPU
R2Off1OnMooncake embedded验证 Draft L3 基础功能Target + Draft 注册,重启后命中PR / Extra
R3Off1OnMooncake standaloneIssue #28873 最小正向回归health ready,无 -1,Draft buffer 位于共享域PR GPU
R4Off2OnMooncake standalone覆盖多 rank allocator 和 key 空间所有 rank ready,无 pointer / key 串扰PR / Nightly
R5P1 + D22Decode Onstandalone + RDMA覆盖生产拓扑、重启和 loadbackP/D 启动、L3 命中、优雅退出Nightly

此外还需要一个独立、确定性的负向单测:

故障注入预期回滚断言
Draft 第一个 buffer 返回 -1DRAFT 不在正式映射中;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 正向回归通过
P0Draft 注册改成事务式提交与回滚CI-1 故障注入全部通过
P1增加 TP2 standalone PR smoke所有 rank 注册、请求和读回稳定
P1明确 fail-closed / fallback 配置与指标行为可配置、可观测、可文档化
P2增加 PD / RDMA restart-loadback nightly生产拓扑长期无回归

参考资料