SGLang之MambaRadixCache机制
本文详细解析 SGLang 中 MambaRadixCache 的内存空间布局、KV Cache 与 SSM Cache 的缓存机制原理、Prefill 和 Decode 的完整流程,并通过端到端的例子串联所有概念。
1. 内存空间布局
1.1 三大内存池
MambaRadixCache 管理三块独立的 GPU 显存池:每 256 token 做一次 track(不是为了 tree 缓存,而是为了):
Chunked prefill 被抢占: scheduler 打断后,需要从 checkpoint 恢复
Speculative decoding 回滚: draft 被 reject 后回退到最近 checkpoint
1 | ┌──────────────────────────────────────────────────────────────────────────────┐ |
1.2 KV Pool 空间
1 | # memory_pool.py: MHATokenToKVPool._create_buffers |
- 分配粒度: 1 slot = 1 个 token 在所有 Full Attention 层的 K 和 V
- 容量:
size个 slot(例如 100,000) - 一个 request 占用:
seq_len个 slot(随序列长度线性增长) - 管理方式:
TokenToKVPoolAllocator维护 free list
1.3 SSM Pool 空间
1 | # memory_pool.py: MambaPool.__init__ |
- 分配粒度: 1 slot = 所有 Mamba 层的完整隐状态(conv + temporal)
- 容量:
mamba_size个 slot(例如 2,000) - 单个 slot 大小: 固定,与序列长度无关(例如 ~112MB for Kimi-K2)
- 一个 request 占用: 1 个工作 slot + 2 个 ping-pong track slot = 共 3 个
- 管理方式:
MambaPool维护 free_slots tensor
1.4 ReqToTokenPool 映射表
1 | # memory_pool.py: ReqToTokenPool |
每个 request 分配一行,记录:
req_to_token[req_pool_idx, pos]= token 在 KV Pool 中的 slot indexreq_index_to_mamba_index_mapping[req_pool_idx]= SSM Pool 的 slot index
1.5 空间对比
| 维度 | KV Pool | SSM Pool |
|---|---|---|
| 每 slot 存什么 | 1 token 的 K、V(所有 full-attn 层) | 完整 SSM state(所有 mamba 层的 conv + temporal) |
| slot 大小 | 小(~KB 级) | 大(~百 MB 级) |
| 每 request 占用 | O(seq_len) 个 | 固定 1~3 个 |
| 总容量决定 | 能同时服务多少 token | 能缓存多少个前缀 checkpoint |
| 淘汰 LRU | full_lru_list | mamba_lru_list(独立) |
2. Radix Tree 数据结构
2.1 TreeNode 定义
1 | class TreeNode: |
关键点:value 和 mamba_value 存的都是 Pool 的 slot index(指针),不是数据本身。
2.2 Radix Tree 如何存储前缀
1 | 示例:3 个 request 分别处理过以下 prompt: |
2.3 Tombstone 机制
当 SSM Pool 不够时,mamba_lru_list 淘汰最久未用的节点的 SSM state:
1 | 淘汰前: node.mamba_value = tensor([5]) ← SSM 有效 |
Tombstone 节点:
- KV 仍可用(Full Attention 前缀匹配仍有效)
- 但不能作为 Mamba 的有效匹配点(没有 SSM state 就无法从此继续)
3. 缓存机制原理
3.1 前缀共享——不 copy 数据,只共享 index
核心设计:多个 request 可以通过 radix tree 共享同一批 KV Pool slot,而不是各自持有副本。
1 | Request A 和 B 共享前缀 "You are a helpful assistant.": |
通过 lock_ref 保护共享节点不被淘汰。
3.2 SSM Copy-on-Write (COW)
SSM state 不能直接共享(因为后续 forward 会修改它),所以用 COW:
match_prefix命中带mamba_value的节点- 分配一个新的 SSM Pool slot 给 request
- 记录
req.mamba_cow_src_index = node.mamba_value(延迟 copy) - Forward 开始时执行真正的 copy:
1 | # MambaPool.copy_from: |
之后 request 在自己的 slot 上独立更新,tree 中的原始 slot 不受影响。
3.3 双 LRU 独立淘汰
1 | full_lru_list: 管理 KV Pool slot 的淘汰 |
3.4 Ping-Pong Track Buffer
解决 overlap schedule 下的读写竞争问题:
1 | 每个 request (extra_buffer 模式) 的 SSM 资源: |
Overlap schedule 下,batch N+1 的 forward 和 batch N 的 cache 操作并行执行:
- Forward 写入 slot A
- 同时 cache_unfinished_req 读取 slot B(上次写完的)
- 读写分离,互不干扰
每隔 mamba_track_interval(默认 256)token 做一次 checkpoint,交替使用 A/B。
4. Prefill 流程
4.1 非 Chunked Prefill(短 prompt 一次处理完)
1 | Request: "You are a helpful assistant." |
4.2 Chunked Prefill(长 prompt 分多次处理)
1 | Request: "ABCDEFGH" (8 tokens, chunk_size=4) |
4.3 free 重复 KV 的场景
1 | 接上面,假设在 Chunk 1 和 Chunk 2 之间,Request B 已经 cache 了 [A,B,C,D,E,F]: |
5. Decode 流程
5.1 单 token decode
1 | Request 在 decode 阶段,每次生成 1 个 token: |
5.2 为什么 decode 中间不存 SSM 到 tree
- Decode 产生的每个 token 是该 request 独有的采样结果
- 不会有其他 request 产生完全相同的 token 序列
- 所以中间 SSM state 永远不会被其他 request 命中
- 存了也是浪费 SSM Pool 空间
唯一有价值的是 最终 state(用于多轮对话的下一轮复用)。
5.3 Track Interval 的权衡
每 256 token 做一次 track(不是为了 tree 缓存,而是为了):
- Chunked prefill 被抢占: scheduler 打断后,需要从 checkpoint 恢复
- Speculative decoding 回滚: draft 被 reject 后回退到最近 checkpoint
| Track 频率 | Decode 性能 | 被抢占时重算量 |
|---|---|---|
| 每 token | 慢(每次多一次 memcpy) | 0 |
| 每 256 token | 快 | 最多 255 token |
| 不 track | 最快 | 全部 |
6. 完整端到端例子
场景设定
1 | 模型: Kimi-K2 (KDA hybrid, 28 full-attn layers + 32 mamba layers) |
6.1 Server 启动 + Warmup
1 | 启动参数: --warmups kda_cache --warmup-prompts-file prompts.json |
6.2 Request 1 到来
1 | prompt = "You are a helpful assistant. Tell me a joke." |
6.3 Request 2 到来(复用前缀)
1 | prompt = "You are a helpful assistant. Write Python code." |
6.4 复用限制:长无法复用于短
1 | 假设 tree 中只有: |
7. 关键设计总结
7.1 为什么 KV 可以共享但 SSM 需要 COW
| KV Cache | SSM State | |
|---|---|---|
| 读/写模式 | Attention 只读历史 KV | Forward 每个 token 都修改 state |
| 能否共享 | 可以(只读) | 不行(会被修改) |
| 方案 | 直接共享 Pool index | Copy-on-Write |
7.2 Radix Tree 存指针不存数据
1 | Tree node.value = [10, 11, 12] ← 3 个 int,指向 KV Pool |
7.3 Lock 引用计数
1 | inc_lock_ref: request 开始使用 tree 节点 → lock_ref++,节点不可淘汰 |
7.4 Warmup 的价值
1 | 不用 warmup: 第一个请求必须从头计算 Mamba(串行,慢) |
8. KDA Speculative Decoding 机制
8.1 整体架构
KDA 模型的 speculative decoding 使用 FROZEN_KV_MTP 算法:
1 | Target Model (完整 KDA LLM): |
8.2 Frozen KV 设计
“Frozen”(冻结)指 draft 推理全程中,target 的 KV 序列不增长、不变化:
- Draft 模型不写 KV Pool
- Draft 每步的 RoPE position 固定为
seq_lens - 1 - 通过上下文管理器临时将 attention backend 的 KV pool 切换为 target 的(只读)
1 | # frozen_kv_mtp_utils.py |
8.3 Verify 阶段的 Mamba 处理
Verify 时 target 模型对 EAGLE tree 所有 draft tokens 做一次 batched forward。 所有 draft tokens 从 layer 0 到 layer 31 并行走一遍(层间串行,层内 batch 并行)。
对 Mamba 层的特殊处理:
disable_state_update=True:不直接修改主 SSM state- 每个 draft position 的递归结果写入
intermediate_states_buffer - 通过
retrieve_parent_token数组实现 EAGLE tree 的 parent→child SSM 继承
1 | # mamba.py:651 (verify 路径) |
8.4 Intermediate Buffer 布局与回滚
1 | # memory_pool.py:301 — 额外分配的显存 |
Verify 后通过 fused_mamba_state_scatter_with_mask O(1) 回滚:
1 | # eagle_worker.py:1087 |
8.5 当前方案的内存开销
Intermediate buffer 是主 SSM state 的 draft_token_num 倍:
| topk | steps | draft_token_num | 额外内存相对主 state |
|---|---|---|---|
| 1 | 3 | 3 | 3× |
| 2 | 3 | 7 | 7× |
| 4 | 3 | 63 | 63× |
每层的 intermediate buffer 独立保留(不可释放),因为 verify 判定后要从所有层的 buffer 中 scatter 正确 step 的值。
8.6 与 RadixCache 的集成
Spec decode 全程中 Radix Tree 不被修改。所有操作在 Pool 级别闭环:
- Verify 后:scatter 正确 SSM state 回主 pool
- 如果跨越 track interval(256):scatter 到 ping-pong buffer
- Request 结束:
cache_finished_req从 ping-pong buffer 取 SSM 存入 tree
1 | Verify → scatter 正确 step → 主 state 更新 |
9. 延迟更新方案(优化 Intermediate Buffer 内存)
9.1 问题动机
当前方案的 intermediate buffer 常驻显存:[32 layers × N × T × state_size]。
当 topk=4, T=63 时,额外显存可达数 GB,非常昂贵。根本原因是 verify 后需要从每一层的 buffer 中 scatter,所以所有层的 buffer 都不能释放。
9.2 方案核心思想
Verify 时不更新主 SSM state,verify 结束后也不立即更新,而是等到下一轮开始时,用被接受 tokens 的"输入激活值"把 SSM 重新递推到正确状态。
1 | Round t: |
9.3 什么是"输入激活值"
SSM 的递归公式为 S_new = A × S_old + B × x,其中:
A— 模型参数(永远在那里)S_old— 主 pool 里的当前值(永远在那里)B, x, dt— 线性投影算出的中间值(只在 forward 瞬间存在)
1 | 输入激活 = (hidden_states_d, dt, B, C) ← Mamba 层做 SSM 递归需要的原料 |
Replay 时:只要有激活值 + 模型参数 + 当前 S_old,就能重算出 S_new:
1 | # 下一轮开始时 |
9.4 为什么只保留 accepted 的激活值就够了
时序上,verify 判定在保存之前:
1 | 1. Verify forward → 得到 63 个 logits |
从语义上,被拒绝的 tokens 等于从未发生过。request 的真实序列永远是一条线性链——只有 accepted 路径上的 tokens 对 S(t) 有贡献:
1 | S(t-1) → process(d0) → S_a |
被拒绝的 tokens 的激活值对 S(t) 没有任何贡献,无需保存。
9.5 Verify 期间的 1 层复用 Buffer
虽然不存全量快照,verify forward 中 Mamba 层仍需临时计算 tree 各节点的 S(因为 output = C × S_new 需要 S 才能算出 logits)。
但这些临时 S 只在单层计算期间有用:
1 | 模型前向是逐层串行执行的: |
所以可以只分配 1 层大小的 buffer,逐层覆盖复用:
1 | Mamba Layer 0 执行: |
当前方案不能这样做:因为 verify 结束后需要从每层的 buffer 中 scatter,如果 Layer 0 的 buffer 被 Layer 1 覆盖了就取不回来了。延迟方案不做 scatter(改为 replay),所以可以覆盖。
9.6 EAGLE Tree 内部 parent→child 的 S 共享
单层 buffer 内部,tree 节点的依赖通过 retrieve_parent_token 数组解决:
1 | Buffer layout (单层, 单 request): [T slots, state_size] |
Kernel 内部按 step 顺序处理:
1 | Step 0: d0, d1, d2 从主 state 读 → 结果写入 buffer[0,1,2] |
9.7 内存对比
设 L=32 layers, N=32 requests, T=63 (topk=4), accepted=4, bf16
大小对比:
1 | SSM state 单份: nheads × head_dim × dstate ≈ 500K elements |
总内存对比:
| 当前方案 | 延迟方案 | |
|---|---|---|
| 常驻内存(verify 后) | L×N×T×state = 32×32×63×500K×2B ≈ 数 GB | L×accepted×activation = 32×4×10K×2B ≈ 2.5 MB |
| 峰值内存(verify 期间) | 同上(不释放) | 1×N×T×state ≈ 当前的 1/32 |
| 缩减比(常驻) | 基准 | ~1000× |
| 缩减比(峰值) | 基准 | ~32× |
9.8 延迟代价
下一轮开始时的 replay 耗时:
1 | replay = num_layers × num_accepted × 单次 SSM update kernel |
9.9 正确性保证
主 state 更新顺序:
1 | Round t-1 结束: 保存了 accepted 的激活值, S 仍为 S(t-2) |
前提条件:Draft 模型本身不含 Mamba 层(Frozen-KV MTP 满足,draft 只读 target KV)。
Accepted 路径线性链保证:Verify 贪心判定保证接受路径从根到叶连续——不可能"跳着接受"。所以逐个 replay accepted tokens 就能正确递推 S。
9.10 Track Interval 兼容
Replay 时顺便检查是否跨越 256 边界:
1 | for i, token in enumerate(accepted_tokens): |
比当前方案(从 intermediate buffer scatter 特定 step)更直观。
9.11 完整时序对比
1 | ═══ 当前方案 ═══════════════════════════════════════════════════════ |
9.12 方案优缺点总结
| 维度 | 当前方案(全量快照) | 延迟更新方案 |
|---|---|---|
| 峰值内存 | 32 × N × T × state | 1 × N × T × state(32× 缩减) |
| 常驻内存 | 同峰值 | 32 × accepted × activation(~1000× 缩减) |
| Verify 后延迟 | 0(O(1) scatter) | +100-300 μs(replay) |
| 实现复杂度 | 现有 | 需改动:1) 缓存激活逻辑 2) buffer 复用 3) replay kernel |
| 正确性 | ✓ | ✓(replay 后再 draft,顺序正确) |
| 适用条件 | 通用 | Draft 无 Mamba 层(Frozen-KV MTP 满足) |
| Track 兼容 | 需要从 buffer scatter 特定 step | replay 过程中自然检查,更简洁 |
9.13 适用场景选择
- topk=1, T=3:当前方案只需 3× 主 state,延迟方案收益有限
- topk=2, T=7:延迟方案可节省约 90% intermediate buffer
- topk=4, T=63:延迟方案价值最大,从 ~GB 级降到 ~MB 级
- 内存紧张 + 需要大 batch:延迟方案释放的显存可换来更多并发 request
- 标题: SGLang之MambaRadixCache机制
- 作者: 鱿鱼圈
- 创建于 : 2026-05-29 23:50:00
- 更新于 : 2026-06-14 22:47:27
- 链接: https://yuyanqi.com/2026/05/29/SGLang之MambaRadixCache机制解析/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。