1. 从一次长上下文推理的卡顿说起如果你正在做 LLM 推理服务大概率遇到过这种场景单条请求跑得好好的一旦并发上来尤其是长上下文请求混进来TTFT首 token 延迟直接飙到十几秒TBTtoken 间延迟也开始抖动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 → SSDMooncake 把 KVCache 按访问热度分层存放层级介质特点适用场景L1GPU VRAM带宽最高容量最小当前批次正在使用的 KVCacheL2CPU DRAM带宽中等容量大近期可能复用的前缀缓存L3SSD带宽低容量最大冷门但偶尔命中的长文档缓存每个 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_KEYsk-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_cacheTrue): 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: fBearer {TAOTOKEN_KEY}} start time.time() resp requests.post(f{API_BASE}/chat/completions, jsonpayload, headersheaders) 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(fTTFT 降低幅度: {(r1[elapsed] - r2[elapsed]) / r1[elapsed]:.2%})4.3 预期结果与判读在长上下文场景下如果 KVCache 分层和前缀缓存配置正确你应该看到第二次请求的cached_tokens接近prompt_tokens的 80% 以上。第二次请求的端到端延迟比第一次降低 40%–70%。如果启用了enable_chunked_prefillTBT 的 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冷门长文档的复用率会有明显改善。
