1. 从一次长上下文推理的卡顿说起
如果你正在做 LLM 推理服务,大概率遇到过这种场景:单条请求跑得好好的,一旦并发上来,尤其是长上下文请求混进来,TTFT(首 token 延迟)直接飙到十几秒,TBT(token 间延迟)也开始抖动,GPU 利用率却上不去。问题往往不在模型本身,而在 KVCache 的管理方式上。
Mooncake 是 Moonshot AI 为 Kimi 打造的服务平台,它最核心的设计思路就是以 KVCache 为中心:把预填充(prefill)和解码(decode)拆成独立的资源池,再把 GPU 集群里闲置的 CPU、DRAM、SSD 通过 RDMA 组织成一个分层的 KVCache 缓存池。这样一来,长上下文请求的前缀缓存可以跨节点复用,预填充阶段不用反复重算,解码阶段的显存压力也被释放出来。
这套架构适合谁?适合正在自建推理服务、被长上下文和高并发同时折磨的团队;也适合想理解“为什么分离式架构能提升吞吐”的工程师。下面我会先讲清楚 Mooncake 的 KVCache 分层与调度逻辑,然后给出一份可复制的config.toml骨架,再通过 TaoToken 的统一 Key/API 通道把请求链路跑通,最后用缓存命中率和延迟数据验证效果。
2. Mooncake 的 KVCache 分层与调度到底在做什么
2.1 分解架构:预填充池与解码池分离
传统推理服务把预填充和解码耦合在同一个实例里,长上下文请求的预填充会长时间占用 GPU,导致解码批次被打断,TBT 直接崩掉。Mooncake 的做法是把两者拆开:
- 预填充池:负责处理输入 token,生成 KVCache,采用分块流水线并行(CPP)来加速长上下文。
- 解码池:负责逐 token 生成输出,维护连续批处理(continuous batching)。
- Conductor(全局调度器):为每个请求选择一对预填充实例和解码实例,并决定 KVCache 的复用、迁移和复制策略。
这个拆分的关键在于:KVCache 在预填充阶段产生,在解码阶段被消费,它天然就是连接两个阶段的“货物”。谁掌握了 KVCache 的分布和调度,谁就掌握了整个系统的吞吐和延迟平衡。
2.2 KVCache 分层存储:GPU → DRAM → SSD
Mooncake 把 KVCache 按访问热度分层存放:
| 层级 | 介质 | 特点 | 适用场景 |
|---|---|---|---|
| L1 | GPU VRAM | 带宽最高,容量最小 | 当前批次正在使用的 KVCache |
| L2 | CPU DRAM | 带宽中等,容量大 | 近期可能复用的前缀缓存 |
| L3 | SSD | 带宽低,容量最大 | 冷门但偶尔命中的长文档缓存 |
每个 KVCache 块都会附带一个哈希值,由自身内容和前缀哈希共同决定,用于跨请求去重。传输层由一个叫 Messenger 的组件负责,基于 GPUDirect RDMA 在 CPU 和 GPU 之间异步搬运数据。关键在于:加载和存储是逐层异步执行的,与注意力计算重叠,所以预填充实例的执行时间大致等于 KVCache 加载时间或标准预填充时间,取较大者。
2.3 缓存感知调度:不是简单的负载均衡
Conductor 的调度算法不是看哪个实例请求少就往哪扔,而是综合三个因素:
- 前缀匹配长度:请求的 block keys 与各预填充实例缓存键的匹配程度。
- 排队时间:该实例上已有请求的预估预填充耗时总和。
- 传输时间:如果要把远程 KVCache 迁移过来,网络传输的预估耗时。
算法会计算每个候选实例的 TTFT 预估值,选最小的那个。如果最佳远程前缀匹配长度不够理想,Conductor 会触发热点迁移:把热门 KVCache 块复制到多个节点,避免单点获取拥塞;冷门块则被换出,降低保留成本。
注意:这套调度假设你能拿到每个实例的缓存键和负载状态。在本地复现时,你需要自己维护一个轻量的缓存索引,或者用支持前缀缓存的推理引擎(如 vLLM 的
--enable-prefix-caching)来近似。
3. 可复制的 config.toml 骨架与 TaoToken 接入配置
3.1 推理服务端 config.toml 骨架
下面这份配置假设你用 vLLM 作为推理后端,通过环境变量注入 TaoToken 的 API Key。你可以直接复制到项目根目录,按需改端口和模型路径。
# config.toml - Mooncake 风格 KVCache 分层推理服务配置骨架 [server] host = "0.0.0.0" port = 8000 # 预填充与解码分离部署时,用不同端口区分角色 role = "prefill" # 可选 prefill / decode / hybrid [model] name = "your-model-name" path = "/models/your-model" dtype = "float16" max_model_len = 131072 # 长上下文场景,按实际模型调整 [kvcache] # 分层缓存开关 enable_prefix_caching = true enable_chunked_prefill = true # 预填充分块大小,Mooncake 论文建议大于 1000 token prefill_chunk_size = 2048 # CPU DRAM 缓存池大小(GB),用于存放可复用的 KVCache cpu_cache_pool_gb = 64 # SSD 缓存路径,冷门 KVCache 落盘 ssd_cache_path = "/data/kvcache_ssd" ssd_cache_pool_gb = 512 # 缓存逐出策略:lru / lfu / request_aware eviction_policy = "lru" # 热点迁移阈值:当远程前缀匹配长度超过本地可复用长度 * 该阈值时,触发迁移 kvcache_balancing_threshold = 1.5 [scheduler] # 调度器类型:cache_aware 对应 Mooncake 的缓存感知调度 type = "cache_aware" # TTFT SLO 上限(秒),超过则拒绝请求 ttft_slo_seconds = 30.0 # TBT SLO 上限(秒/token) tbt_slo_seconds = 0.1 # 过载时是否启用早期拒绝 enable_early_rejection = true # 基于预测的早期拒绝,缓解负载波动 enable_predictive_rejection = true [transport] # KVCache 传输后端:rdma / tcp backend = "rdma" # Messenger 服务监听端口 messenger_port = 9100 # 异步传输重叠开关 async_transfer_overlap = true [taotoken] # TaoToken 统一 API 通道,用于模型对话、coding plan 等上游调用 api_base = "https://taotoken.net/api" # API Key 从环境变量读取,不要硬编码 api_key_env = "TAOTOKEN_API_KEY" # 默认模型 default_model = "claude-sonnet-4-20250514" # 请求超时(秒) timeout_seconds = 1203.2 环境变量与启动命令
# 设置 TaoToken API Key export TAOTOKEN_API_KEY="sk-your-key-here" # 启动预填充实例 python -m vllm.entrypoints.openai.api_server \ --config config.toml \ --role prefill \ --port 8000 # 启动解码实例(另一台机器或另一个进程) python -m vllm.entrypoints.openai.api_server \ --config config.toml \ --role decode \ --port 8001如果你暂时没有多机环境,可以先用单机 hybrid 模式跑通链路,把role改成hybrid,预填充和解码共用一个实例,但 KVCache 分层逻辑仍然生效。
3.3 TaoToken 统一 Key 的获取与配置
TaoToken 在这里扮演的是统一 API 通道的角色:你不需要为每个上游模型单独维护 Key,而是通过一个 Key 访问模型对话、coding plan 等能力。获取方式:
- 访问 TaoToken 官网 注册账号。
- 进入 Console 创建 API Key。
- 在 API Keys 页面 复制你的 Key,写入环境变量
TAOTOKEN_API_KEY。
提示:API 基础地址是
https://taotoken.net/api,不要加 UTM 参数,直接用于代码里的base_url。
4. 验证请求:缓存命中与延迟实测
4.1 用 curl 发一条带前缀缓存的请求
先准备一段长 system prompt,模拟可复用的前缀:
# 第一次请求,冷启动,KVCache 未命中 curl -X POST http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -d '{ "model": "your-model-name", "messages": [ {"role": "system", "content": "你是一个技术助手,请根据以下文档回答问题。文档内容:...(此处省略 8000 字长文档)..."}, {"role": "user", "content": "总结文档的核心观点"} ], "max_tokens": 256, "temperature": 0.7 }'记录返回的usage字段和响应时间。然后发第二次请求,system prompt 完全相同,只改 user 问题:
# 第二次请求,前缀缓存应命中 curl -X POST http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -d '{ "model": "your-model-name", "messages": [ {"role": "system", "content": "你是一个技术助手,请根据以下文档回答问题。文档内容:...(与上面完全相同)..."}, {"role": "user", "content": "文档里提到了哪些优化手段?"} ], "max_tokens": 256, "temperature": 0.7 }'4.2 用 Python 脚本批量验证缓存命中率
import time import requests import os API_BASE = "http://localhost:8000/v1" TAOTOKEN_KEY = os.environ["TAOTOKEN_API_KEY"] LONG_PREFIX = "你是一个技术助手,请根据以下文档回答问题。文档内容:" + "..." * 2000 def send_request(question, use_cache=True): messages = [ {"role": "system", "content": LONG_PREFIX}, {"role": "user", "content": question} ] payload = { "model": "your-model-name", "messages": messages, "max_tokens": 128, "temperature": 0.7 } headers = {"Authorization": f"Bearer {TAOTOKEN_KEY}"} start = time.time() resp = requests.post(f"{API_BASE}/chat/completions", json=payload, headers=headers) elapsed = time.time() - start data = resp.json() return { "elapsed": elapsed, "prompt_tokens": data["usage"]["prompt_tokens"], "completion_tokens": data["usage"]["completion_tokens"], "cached_tokens": data["usage"].get("prompt_tokens_details", {}).get("cached_tokens", 0) } # 冷启动 r1 = send_request("总结文档核心观点") print(f"冷启动: 耗时 {r1['elapsed']:.2f}s, prompt_tokens={r1['prompt_tokens']}, cached={r1['cached_tokens']}") # 热缓存 r2 = send_request("文档里提到了哪些优化手段?") print(f"热缓存: 耗时 {r2['elapsed']:.2f}s, prompt_tokens={r2['prompt_tokens']}, cached={r2['cached_tokens']}") # 计算缓存命中率 hit_rate = r2["cached_tokens"] / r2["prompt_tokens"] if r2["prompt_tokens"] > 0 else 0 print(f"缓存命中率: {hit_rate:.2%}") print(f"TTFT 降低幅度: {(r1['elapsed'] - r2['elapsed']) / r1['elapsed']:.2%}")4.3 预期结果与判读
在长上下文场景下,如果 KVCache 分层和前缀缓存配置正确,你应该看到:
- 第二次请求的
cached_tokens接近prompt_tokens的 80% 以上。 - 第二次请求的端到端延迟比第一次降低 40%–70%。
- 如果启用了
enable_chunked_prefill,TBT 的 P90 值应该保持稳定,不会因为长上下文预填充而抖动。
如果cached_tokens始终为 0,检查enable_prefix_caching是否开启,以及两次请求的 system prompt 是否完全一致(包括空格和换行)。
5. 本篇常见错排查
5.1 报错KVCache transfer timeout或 RDMA 连接失败
这是传输层配置问题。先确认transport.backend设置正确:如果没有 RDMA 网卡,改成tcp。然后检查messenger_port是否被防火墙拦截。在单机 hybrid 模式下,Messenger 仍然会启动,但传输走本地内存拷贝,不会触发 RDMA。
# 检查 Messenger 进程是否在监听 ss -tlnp | grep 9100 # 如果使用 TCP 后端,测试连通性 nc -zv <prefill_host> 91005.2 缓存命中率低,cached_tokens远小于预期
常见原因有三个:
- 前缀不一致:两次请求的 system prompt 有任何字符差异,哈希就不同。建议把长前缀抽成变量,确保完全一致。
- 缓存池太小:
cpu_cache_pool_gb设置过小,KVCache 被提前逐出。长上下文场景建议至少 64GB 起步。 - 逐出策略不匹配:如果工作负载有明显的热点,
lru可能不够,试试request_aware。
5.3 TTFT 仍然很高,SLO 被违反
检查prefill_chunk_size是否合理。太小会导致分块过多,流水线开销上升;太大则单块计算时间过长,TTFT 增加。Mooncake 论文建议大于 1000 token,实测 2048 是个不错的起点。另外确认async_transfer_overlap已开启,否则 KVCache 传输会阻塞计算。
5.4 过载时请求被大量拒绝,但 GPU 利用率不高
这是典型的负载波动问题。开启enable_predictive_rejection,让调度器预测预填充阶段完成后的解码负载,而不是只看当前负载。如果仍然波动,检查ttft_slo_seconds和tbt_slo_seconds是否设置得过紧,导致调度器过于保守。
5.5 TaoToken API 返回 401 或 403
确认TAOTOKEN_API_KEY环境变量已正确导出,且 Key 没有过期。可以在 API Keys 页面 重新生成一个 Key。如果是在容器里运行,注意环境变量是否传递进去了。
# 验证 Key 是否生效 curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"claude-sonnet-4-20250514","messages":[{"role":"user","content":"ping"}],"max_tokens":10}'6. 把链路跑通之后,下一步做什么
如果你已经用上面的配置跑通了缓存命中和延迟验证,接下来可以往两个方向深入。
第一个方向是多实例调度。把预填充和解码拆到不同机器上,用 Conductor 的缓存感知调度逻辑做请求分发。这时候你需要一个轻量的全局缓存索引,记录每个预填充实例上有哪些 KVCache 块。可以从最简单的哈希表开始,后续再引入热点迁移。
第二个方向是长期编码与 Agent 场景。Mooncake 的 KVCache 复用对多轮对话和代码生成特别友好,因为 system prompt 和项目上下文可以长期驻留在缓存池里。如果你在用 Claude Code 或类似的编码 Agent,可以通过 Coding Plan 把 TaoToken 的统一通道接进去,让 Agent 的每次请求都走缓存感知的推理后端。
接入文档在 TaoToken 文档,里面有完整的 API 参数说明和示例。模型对话的调试可以用 模型对话页面 快速验证。如果你在用 Claude Code 的 Anthropic 兼容接口,参考 ClaudeCodeAnthropic 接入说明。
实测下来,KVCache 分层最容易被忽略的是 SSD 层的逐出策略。很多人只配了 CPU DRAM 池,结果长文档缓存一多就被挤掉,命中率上不去。把ssd_cache_pool_gb设大一点,配合lru,冷门长文档的复用率会有明显改善。