
1. 多轮对话为什么越聊越慢从 KV Cache 重复计算说起做过 LLM 服务的人大概都有这个体感单轮问答时首 token 延迟TTFT还能接受可一旦进入多轮对话第二轮、第三轮开始明显变慢并发一上来 GPU 利用率飙高但吞吐上不去。根因不在模型本身而在于每一轮请求都要把历史对话的 token 重新做一遍注意力预填充prefill也就是重复计算历史 token 的 Key/Value 缓存。Attention Reuse 要解决的就是这件事。它的核心思路是同一会话的历史 KV Cache 没必要每轮重算把它存下来、下一轮直接复用即可。AttentionStore 就是这套思路的一个工程化骨架它维护一个分层的 KV Cache 存储系统用内存/存储介质为所有请求保存 KV 缓存并通过分层预加载、异步保存、调度器感知的获取与驱逐把 KV 访问和 GPU 计算重叠起来。论文里给出的数据是多轮会话 TTFT 降低最高 87%提示预填充吞吐提升 7.8 倍端到端推理成本降低 70%长序列推理 TTFT 降低 95%。这篇不讲论文翻译讲怎么在你的多轮对话服务里把 Attention Reuse 落地AttentionStore 的配置骨架长什么样、settings.json 怎么写、两轮对话之间怎么验证缓存真的命中了。适合正在做高并发推理服务、被多轮对话成本压得难受的工程师。2. 前置准备TaoToken 接入与 AttentionStore 运行环境AttentionStore 本身是推理侧的缓存调度逻辑它需要一个能稳定调用的 LLM 服务端点来做验证。我这边用 TaoToken 作为统一接入层好处是模型对话、API Key、接入文档都在一个控制台里验证缓存命中时不用来回切平台。先拿到调用凭证。打开控制台创建 API Key控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentattentionstore_kv_cacheAPI Key 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentattentionstore_kv_cache接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentattentionstore_kv_cacheAPI 基地址统一用https://taotoken.net/api这个地址不加 UTM 参数直接写进配置即可。如果你只是想先确认模型能不能正常对话可以先用模型对话页跑一轮模型对话https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentattentionstore_kv_cache环境侧需要 Python 3.10、至少一张能跑推理的 GPU本地验证用小模型即可以及一个能读写本地磁盘的目录用来放 KV Cache 的二级存储。下面所有配置都基于这个前提。3. AttentionStore 配置骨架与 settings.json 可复制片段AttentionStore 的配置分三层会话标识层、缓存分层层、调度策略层。会话标识层负责给每个多轮会话一个稳定的 session_id这是缓存能跨轮命中的前提缓存分层层定义 GPU 显存、主机内存、本地磁盘三级介质调度策略层决定 KV 块什么时候预加载、什么时候异步落盘、什么时候驱逐。先看 settings.json 的完整骨架可以直接复制改{ attention_store: { enabled: true, session_key: session_id, tiering: { l0_gpu: { capacity_mb: 8192, eviction: lru }, l1_host: { capacity_mb: 65536, eviction: lru }, l2_disk: { path: /var/lib/attentionstore/kv, capacity_mb: 524288, format: safetensors } }, preload: { enabled: true, lookahead_turns: 1, async_save: true, overlap_with_compute: true }, scheduler_aware: { enabled: true, placement_hint: ttft_priority, evict_on_pressure: true }, position_encoding: { decouple: true, truncate_on_overflow: true, max_context_tokens: 32768 } }, llm_endpoint: { base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY, model: your-model-name } }几个参数值得单独说。session_key决定用请求里的哪个字段做会话标识多轮对话场景必须保证同一会话每轮传同一个值否则缓存永远命中不了。lookahead_turns是预加载的提前轮数设 1 表示提前把下一轮可能用到的 KV 块从慢介质拉到快介质设太大反而占显存。decouple和truncate_on_overflow是位置编码解耦和溢出截断长对话超过max_context_tokens时靠它保证已存的 KV 不失效。对应的 AttentionStore 骨架代码核心是三个方法get取缓存、put存缓存、evict驱逐import json import hashlib from pathlib import Path class AttentionStore: def __init__(self, config_path: str): self.cfg json.loads(Path(config_path).read_text())[attention_store] self.l0 {} # gpu tier: session_id - kv_blocks self.l1 {} # host tier self.l2_dir Path(self.cfg[tiering][l2_disk][path]) self.l2_dir.mkdir(parentsTrue, exist_okTrue) def _key(self, session_id: str, turn: int) - str: raw f{session_id}:{turn}.encode() return hashlib.sha256(raw).hexdigest()[:16] def get(self, session_id: str, turn: int): k self._key(session_id, turn) if k in self.l0: return self.l0[k], l0_hit if k in self.l1: block self.l1.pop(k) self.l0[k] block return block, l1_hit disk_path self.l2_dir / f{k}.safetensors if disk_path.exists(): block self._load_disk(disk_path) self.l1[k] block return block, l2_hit return None, miss def put(self, session_id: str, turn: int, kv_block): k self._key(session_id, turn) self.l0[k] kv_block if self.cfg[preload][async_save]: self._async_save(k, kv_block) def _async_save(self, k: str, block): # 实际实现里丢到后台线程/进程池避免阻塞推理 pass def _load_disk(self, path: Path): # 按 safetensors 格式读回 passget的返回值里带上命中层级l0_hit/l1_hit/l2_hit/miss这是后面验证缓存命中的关键别省。4. 两轮对话间缓存命中验证请求与结果对照配置写完必须验证不然你不知道缓存到底有没有生效。验证思路很简单构造两轮对话第二轮带上第一轮的 session_id观察第二轮是否命中缓存、TTFT 是否下降。先发第一轮请求建立会话并写入缓存export TAOTOKEN_API_KEY你的key curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: your-model-name, session_id: sess-demo-001, messages: [ {role: user, content: 帮我解释一下 KV Cache 在推理中的作用} ] }第一轮结束后AttentionStore 会把这一轮的 KV 块写入 l0并按async_save异步落盘。接着发第二轮注意session_id必须一致且把历史消息带上curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: your-model-name, session_id: sess-demo-001, messages: [ {role: user, content: 帮我解释一下 KV Cache 在推理中的作用}, {role: assistant, content: KV Cache 缓存的是注意力层的 Key 和 Value...}, {role: user, content: 那它和多轮对话的成本有什么关系} ] }判断命中看两个信号。一是服务端日志里get的返回层级第二轮应该出现l0_hit或l1_hit而不是miss二是响应里的 TTFT 字段第二轮的历史 token 部分不再重新 prefillTTFT 应明显低于关闭 AttentionStore 时的值。实测下来两轮短对话的 TTFT 差距可能只有几十毫秒但对话轮数越多、历史越长差距越明显。如果你想更直观地看命中情况可以在get里加一行计数def get(self, session_id: str, turn: int): k self._key(session_id, turn) hit miss if k in self.l0: hit l0_hit elif k in self.l1: hit l1_hit elif (self.l2_dir / f{k}.safetensors).exists(): hit l2_hit print(f[AttentionStore] session{session_id} turn{turn} {hit}) return self._do_get(k, hit)跑两轮后终端会打印turn0 miss和turn1 l0_hit命中链路就通了。5. 本篇常见错排查缓存不命中与显存打满session_id 每轮都变。这是最常见的坑。有些框架默认给每个请求生成新 ID导致缓存键对不上永远 miss。检查方法在_key里打印 session_id确认两轮一致。位置编码没解耦长对话后缓存失效。当对话超过max_context_tokens如果decouple为 false已存的 KV 会因为位置偏移而不可用。把position_encoding.decouple设为 true并确认truncate_on_overflow生效。l0 显存打满触发频繁驱逐。l0_gpu.capacity_mb设太大挤占推理显存设太小又频繁驱逐到 l1。建议先按模型显存的 20% 给 l0观察驱逐日志再调。如果evict_on_pressure为 true 但驱逐后命中率骤降说明 l1 容量不够加大l1_host.capacity_mb。异步保存阻塞了推理。async_save如果实现成同步写盘反而拖慢 TTFT。确认_async_save真的丢到了后台线程或进程池别在推理主路径上做磁盘 IO。多副本部署时缓存不共享。如果你的服务是多实例每个实例的本地 l2 是独立的请求被负载均衡打到不同实例就会 miss。这种情况要么做会话粘性路由要么把 l2 换成共享存储。6. 长期跑多轮对话服务把 Attention Reuse 接进 Coding Plan单机验证通过后下一步是把它接进长期运行的服务里。多轮对话的缓存调度和编码类 Agent 的请求模式很像——都是长会话、多轮追加、对 TTFT 敏感所以如果你在跑 Coding Plan 这类持续编码任务AttentionStore 的分层缓存和调度器感知放置同样适用。Coding Plan 入口https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentattentionstore_kv_cache接入文档含多轮会话参数说明https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentattentionstore_kv_cache落地时我建议先只开 l0l1 两级把 l2 磁盘缓存留到会话量上来再加因为磁盘 IO 的延迟在低并发下反而可能拖后腿。等并发稳定在几百 QPS 以上再打开scheduler_aware和lookahead_turns让预加载真正和 GPU 计算重叠起来。缓存命中率这个指标要持续盯它比 TTFT 更早暴露配置问题。