DeepSeek 上下文硬盘缓存深度解析:KV Cache 如何把 API 成本打到一折以下

从 KV Cache、MLA 到滑动窗口下的完整前缀匹配,逐段拆解 DeepSeek 的省钱引擎

Posted by iceyao on Thursday, August 20, 2026

一、引言:一个"不用改一行代码"的成本革命

原文链接:上下文硬盘缓存 | DeepSeek API Docs 关联公告:DeepSeek API 创新采用硬盘缓存,价格再降一个数量级(2024 年 8 月)

DeepSeek 官方文档里有一篇看起来毫不起眼的短文——《上下文硬盘缓存》。它的第一句话是:

DeepSeek API 上下文硬盘缓存技术对所有用户默认开启,用户无需修改代码即可享用。

没有新参数、没有新接口、没有 opt-in 开关。但就是这一个功能,撑起了三个数字:

  • 成本:缓存命中部分的输入,按未命中价的约 1/30 计费(2024 年首发时是 1/10);
  • 延迟:官方实测,128K 输入且大部分重复的请求,首 token 延迟从 13 秒降到 500 毫秒
  • 规模:按 2024 年公告的说法,DeepSeek 可能是全球第一家在 API 服务中大范围采用硬盘缓存的大模型厂商。

这篇文档每句话背后都挂着一条技术线:KV Cache 挂着 Transformer 推理原理,MLA 挂着 V2 的架构创新,Sliding Window Attention 挂着 V3.2 的稀疏注意力与 V4 的 CSA/HCA,“完整匹配"挂着一套和 2024 年完全不同的新机制。本文要做的事,就是把每条线都拉出来讲透。

还有一个值得提醒的背景:网上大量讲"DeepSeek 缓存"的文章,讲的是 2024 年那套 64-token 块前缀匹配的旧机制。而当前文档已经改成了"缓存前缀单元 + 完整匹配”,两者的可观测行为并不相同(后文详述)。读完这篇,你能分清自己看到的是哪个版本。

先把全文的地基画成一张图:

KV Cache 原理示意

图注:本文配图均为作者基于官方文档与公开技术资料整理的概念示意,并非 DeepSeek 官方架构图。


二、地基:KV Cache 到底在缓存什么

2.1 自回归推理的两个阶段

Transformer 的自注意力可以写成:

Attention(Q, K, V) = softmax(Q · Kᵀ / √d) · V

大模型生成文本是自回归的:一次请求分为两个阶段。

Prefill(预填充):把整段 prompt 一次性并行喂入模型,计算每个 token 的 Q、K、V,产出第一个输出 token。计算量约 O(L²)——上下文翻倍,计算量接近四倍。首 token 延迟(TTFT)主要耗在这里。

Decode(解码):此后每一步只新算一个 token。新 token 只需要自己的 Q,去和全部历史 token 的 K/V 做注意力;算完把自己的 K/V 追加进缓存。

注意 Decode 阶段的一个不对称性:每步计算量很小,但每步都要读一遍完整的 KV Cache。所以长上下文解码的瓶颈往往不是算力,而是显存容量和显存带宽。

2.2 为什么 K/V 可以缓存

因果掩码(causal mask)规定 token i 只能看到位置 ≤ i 的 token。因此:

  • KᵢVᵢ 只由前缀(token 1..i)决定,和后面出现什么内容无关
  • 生成过程中它们永不改变——算一次就够了

这就是一切 prefix caching(不管哪家厂商)的物理基础。想想多轮对话的浪费:第二轮请求把第一轮的 system、问题、回答全部重新输入,如果没有缓存,模型要把第一轮的 prefill 原样重算一遍——而这些 token 的 K/V 其实和上一轮分毫不差。

2.3 KV Cache 有多大:显存才是第一瓶颈

做一个量级演算(示意)。对一个 32 头、head_dim=128、BF16 精度的 MHA 模型,每 token 每层的 KV 缓存为:

2 (K和V) × 32 (头数) × 128 (维度) × 2 (字节) = 16 KB / token / 层

乘以 61 层,约 1 MB / token。128K 上下文就是约 128 GB——单卡 H100(80GB)连一个请求的缓存都装不下。这解释了为什么长上下文推理必须做两件事之一:

  1. 压缩 KV 体积(MLA、GQA、V4 的 CSA/HCA);
  2. 分层存储(HBM → 主机内存 → 硬盘)。

DeepSeek 的答案是:两个都做,而且做到了业界独一份的深度。


三、为什么 DeepSeek 敢把 KV 缓存放到硬盘上

3.1 MLA(V2):把 KV Cache 压缩 93.3%

DeepSeek-V2 提出的 MLA(Multi-head Latent Attention)是整个故事的第一块基石。它的思路是联合压缩:不再为每个注意力头单独缓存 K/V,而是把 KV 投影到一个低维 latent 向量 c_KV(512 维),再加一个解耦的 RoPE 键(64 维)——推理时只缓存这份 latent 表示,需要时再"升维"还原成各头的 K/V。

每 token 每层的缓存从 16 KB 量级降到约 (512 + 64) × 2 B ≈ 1.1 KB,V2 论文给出的数字是 KV cache 削减 93.3%

2024 年 8 月的公告把因果链说得很直白:

这得益于 DeepSeek V2 提出的 MLA 结构,在提高模型效果的同时,大大压缩了上下文 KV Cache 的大小,使得存储所需要的传输带宽和存储容量均大幅减少,因此可以缓存到低成本的硬盘上。

体积每降一个数量级,“把前缀放进更便宜的存储"就从不可行变成可行。

3.2 从 DSA 到 CSA + HCA(V3.2 → V4):再压一个量级

V3.2 引入 DSA(DeepSeek Sparse Attention):Lightning Indexer 用低秩、小头数(可用 FP8)的轻量打分器为每个历史位置算一个 index score,只取 top-k(k=2048) 个 KV 条目进入正式注意力,把主注意力的 O(L²) 降到 O(Lk)。为了让模型适应稀疏访问,DeepSeek 做了两阶段继续训练:dense warm-up(1000 步、约 2.1B tokens,只训 indexer 去模仿 dense 注意力分布)+ 稀疏训练(15000 步、约 943.7B tokens,全参数适应)。

V4 更进一步,用 CSA + HCA 混合注意力替代 MLA+DSA:

  • CSA(压缩稀疏注意力):每 m=4 个连续 token 的 KV 压缩成 1 个条目 + Lightning Indexer top-k 选择 + 共享 KV 的 MQA 执行 + 滑动窗口(n_win=128)保留局部未压缩 KV
  • HCA(重压缩注意力)m'=128 的极端压缩,压缩后序列足够短,直接做稠密注意力——100 万 token 的 KV 被压到约 7800 个条目,提供"鸟瞰式"全局视野;
  • 前 2 层纯滑动窗口注意力,其余层 CSA 与 HCA 交替堆叠。

结果:按 V4 技术报告口径,1M 上下文设定下 V4-Flash 的 KV Cache 约为 V3.2 的 7%、V4-Pro 约为 10%;相对传统 GQA 配置,官方甚至给出"综合约 2%“的表述。

KV Cache 体积演进

3.3 不可变性:KV 能下硬盘的架构前提

V4 推理框架把 KV Cache 分成了两类,这个划分直接决定了"什么能落盘”:

类型 内容 特性 位置
Classical KV Cache CSA/HCA 压缩后的 KV 条目 生成后不可变 可下沉到硬盘
State Cache 滑动窗口的未压缩 KV + 尚未压缩的尾部 token 热度高、频繁更新 GPU HBM

“不可变"三个字是关键:不可变意味着可以安全落盘、可以被后续任意请求复用而不必担心一致性。这和数据库领域"append-only 数据适合冷存"是同一个工程直觉。而滑窗 KV 因为始终随生成位置滚动更新,必须留在 HBM(或按周期 checkpoint)。


四、逐段精读:当前的缓存机制到底怎么工作

以下进入正题,把官方文档逐段拆开。

用户的每一个请求都会触发硬盘缓存的构建。若后续请求与之前的请求在前缀上存在重复,则重复部分只需要从缓存中拉取,计入"缓存命中”。

4.1 默认开启,每个请求都在构建缓存

两句话两个信息点:

  1. 没有开关。缓存对所有人默认开启,你即使完全不知道它的存在,也已经在为它付费(享受它的折扣);
  2. 构建是被动且持续的。不是"你声明要缓存"才缓存,而是每个请求结束都会自动沉淀缓存材料。你唯一能做的是把 prompt 组织得更容易命中——这是第七节的主题。

4.2 完整匹配:缓存前缀单元与三条落盘时机

文档的核心规则:

缓存命中的前提是相应前缀已被"落盘”。受 Sliding Window Attention 机制的影响,缓存前缀的存取与判别与之前有所不同。每条缓存前缀是一个独立的完整单元。后续请求只有在完整匹配缓存前缀单元时,才能命中缓存。

落盘发生在三个时机(官方原文三条规则):

  1. 请求结束位置落盘:每次请求的用户输入结束位置与模型输出结束位置,各产生一个缓存前缀单元;
  2. 公共前缀检测落盘:系统检测到多次请求间存在公共前缀时,把该公共前缀作为独立单元落盘;
  3. 固定 token 间隔落盘:长输入/长输出中按一定 token 数量为间隔截取单元,避免长前缀因迟迟达不到结束位置而完全无法缓存。

这三条分别覆盖三类场景:对话延伸(多轮对话天然在前缀上追加)、共享前缀(多个请求引用同一份长文档/系统提示词)、超长输入(防止"全有或全无")。

缓存前缀单元与完整匹配

官方两个举例值得逐字读:

举例 1(能命中):第一轮输入 A + B,第二轮输入 A + B + C。第二轮能完整匹配 [A+B] 这个单元 → A、B 计入缓存命中,只有 C 需要计算。

举例 2(先不能、后能):第一轮 A + B,第二轮 A + C。第二轮无法命中——因为 A + C 不能完整匹配 [A+B]。但系统此时识别出两轮存在公共前缀 A,把 [A] 落盘为新单元。于是第三轮 A + D 到来时,能完整匹配 [A],命中。

多轮对话的例子更直观——第二次请求就是第一次请求原样加上几条消息:

// 第一次请求
{"messages": [
    {"role": "system", "content": "你是一位乐于助人的助手"},
    {"role": "user", "content": "中国的首都是哪里?"}
]}

// 第二次请求:前两条消息逐字复用
{"messages": [
    {"role": "system", "content": "你是一位乐于助人的助手"},
    {"role": "user", "content": "中国的首都是哪里?"},
    {"role": "assistant", "content": "中国的首都是北京。"},
    {"role": "user", "content": "美国的首都是哪里?"}
]}

长文本问答(财报)的例子则揭示了公共前缀检测的价值:前两次请求(“总结关键信息”、“分析盈利情况”)都不命中——它们的前缀相同,但第一轮结束时落盘的单元是"第一轮的完整输入",第二轮的尾部问题不同,无法完整匹配。但两轮过后,系统检测出 system + <财报内容> 是公共前缀,将其落盘为独立单元——第三次请求(“分析收入与支出占比”)就能命中了

这个例子对工程上的启示是:同一份长文档上连发多个问题,从第三个问题开始享受缓存折扣;如果你的应用只问两个问题就换文档,命中率必然难看。

4.3 为什么滑动窗口让匹配变成"整单元"?(机制推断)

官方文档只给了一句"受 Sliding Window Attention 机制的影响",没有展开。以下是基于 V4 架构公开信息的推断,属于笔者的分析,非官方表述

严格来说,因果性保证了前缀的 KV 本身与后续 token 无关——单看数学,从任意位置截断复用都"正确"。真正发生变化的是工程组织方式

  1. 滑窗状态是"位置相对"的。滑动窗口只保留最近 n_win 个未压缩 KV,某段窗口状态只对特定生成位置有意义。若允许从单元中间恢复推理,截断点之后的窗口内容与当初落盘时并不对齐,系统必须重建对齐状态;
  2. V4 的 KV 以"单元"为组织粒度(压缩条目 + 窗口状态 + 对齐信息一起构成快照)。从中途恢复意味着快照边界和请求边界不一致,与其做昂贵的部分恢复,不如整单元命中或整单元重算。

对照旧机制就明白差异从哪来:2024 年公告明确写着"缓存系统以 64 tokens 为一个存储单元"。在 MLA(无滑窗)时代,块 i 的 K/V 只依赖 ≤ 块尾的 token,与切分位置无关,所以任意 64-token 边界截断复用都是正确的——这正是 vLLM 的 Automatic Prefix Caching 用 block hash 做 64-token 粒度匹配的原理。而滑窗 + 压缩 + 稀疏选择进来之后,“块"变成了"快照”,粒度被迫升到整单元。

行为差异总结:

场景 旧机制(64-token 块) 当前机制(完整单元)
A+C 紧跟 A+B A 部分直接命中 不命中,但催生 [A] 落盘
A+C 之后再发 A+D 命中 A 命中 [A]
首个请求 不命中 不命中
部分匹配一个长单元 按块部分命中 只有整单元才算命中

所以如果你观察到"明明前缀大部分一样却没命中",先检查是否只匹配了某个落盘单元的一部分——等一两个请求让公共前缀被检测落盘,命中就来了

4.4 输出随机性与 best-effort:两条容易被忽略的说明

文档最后两条说明常被忽略,但很重要:

  1. 缓存只匹配输入前缀,输出仍靠推理得到temperature 等参数的随机性不受影响——“其输出效果与不使用硬盘缓存相同”。换句话说,命中缓存不会让模型变笨或变呆;
  2. 缓存是尽力而为(best-effort):不保证 100% 命中;构建耗时秒级;不再使用后自动清空,一般为几小时到几天。加上 2024 公告补充的隔离性——每个用户的缓存独立、逻辑上相互不可见、不用于其他用途——可以得出结论:缓存是纯性能/成本优化,永远不要把正确性押在它上面

五、计费模型与真实省钱幅度

5.1 怎么观察命中:usage 的两个字段

{
  "usage": {
    "prompt_cache_hit_tokens": 6000,
    "prompt_cache_miss_tokens": 4000,
    "completion_tokens": 500
  }
}
  • prompt_cache_hit_tokens:本次输入中命中缓存的 token 数(按命中价计费);
  • prompt_cache_miss_tokens未命中、需要现场计算的 token 数(按未命中价计费)。

命中率 = hit / (hit + miss)。这是长上下文应用最值得放进监控大盘的第一个指标。

5.2 价格:从 1/10 到 1/30

输入单价(元 / 百万 tokens) 2024-08 首发 V4-Flash 空闲 V4-Flash 高峰 V4-Pro 空闲 V4-Pro 高峰
缓存命中 0.1 0.05 0.10 0.15 0.30
缓存未命中 1.0 1.5 3.0 4.5 9.0
输出 —(另见当时价格页) 4.5 9.0 13.5 27.0
命中折扣 1/10 1/30 1/30 1/30 1/30

注:高峰时段为北京时间 9:00–12:00、14:00–18:00,其余为空闲(半价)。两个时代值得注意的事实:折扣从 1/10 加深到了 1/30,而缓存存储本身始终免费(公告原文:“缓存占用存储无需付费”),构建也无额外费用。

5.3 一笔账

三档计费模型

以 V4-Flash 高峰价、10,000 token 输入为例:

无缓存:     10,000 × 3.0 / 1M          = 0.0300 元
命中率 60%:  6,000×0.1 + 4,000×3.0 (/1M) = 0.0126 元  → 省 58%
命中率 90%:  9,000×0.1 + 1,000×3.0 (/1M) = 0.0039 元  → 省 87%

反解一下官方口径的"最高降低 87.5%":成本降到 1/8 时,对应的命中率约为 90%。也就是说,省多少钱几乎完全由命中率决定——这就是第七节工程实践的价值所在。第三方博客的实测案例也印证这个量级:一个金融风控问答接口(单次输入 8600 tokens、62% 为重复的合规声明),接入缓存后命中率稳定在 73.5%,单次调用成本从 1.82 元降到 0.49 元。

5.4 延迟收益:13 秒 → 500 毫秒的物理含义

官方实测数字:128K 输入且大部分重复的请求,TTFT 从 13 秒降到 500 毫秒。做个量级演算(示意):

  • 重算路径:128K prefill 的计算量在万亿 FLOPs 量级(671B 模型),单请求 13 秒合理;
  • 缓存路径:MLA 压缩后 128K 前缀的 KV 约 9 GB(70 KB/token × 128K),从分布式硬盘阵列并行顺序读回——多盘 NVMe 聚合带宽几十 GB/s 起步,半秒内完成完全说得通。

本质上是把 O(L²) 的计算换成了顺序 I/O。这也是"硬盘缓存"这个词的精确含义:不是 HTTP 那种响应缓存,而是把推理的中间状态(KV)当数据落盘。


六、横向对比:各家的前缀缓存长什么样

厂商 触发方式 匹配粒度 命中计价 生命周期
DeepSeek 全自动,默认开启 缓存前缀单元,完整匹配 未命中的约 1/30,存储免费 几小时到几天,best-effort
Anthropic 显式 cache_control 标记 断点前缀 写入 1.25×,读取 0.1× 5 分钟滑动刷新
OpenAI 自动(≥1024 tokens 起) 1024-token 块 自动 50% 折扣 分钟级到 1 小时
Google Gemini 隐式自动 + 显式声明两种 token 前缀 缓存 token 折扣(隐式/显式不同),显式另收存储费 隐式数小时;显式可自定义
vLLM / SGLang 自建 自动(enable_prefix_caching 64-token block hash 免费(自担显存/内存) 进程内 radix 树,重启即失

注:各家策略持续调整,具体以官方文档为准。三点观察:

  1. 只有 DeepSeek 把缓存放到了"硬盘"这一层。其他家的前缀缓存本质上是显存/内存级的(TTL 分钟级),DeepSeek 的 TTL 是天级且跨请求、跨节点——这依赖 MLA 以来的体积压缩,别家短期难以复制;
  2. 显式 vs 自动是两种产品哲学:Claude 给你控制权但要付写入溢价(1.25×)且要操心 5 分钟 TTL;DeepSeek 全自动免费但不可控——你甚至无法主动失效一段缓存,只能等它自然过期;
  3. 匹配粒度在收敛:大家都是"token 前缀匹配",差异在边界语义——OpenAI 按 1024-token 块、Claude 按显式断点、DeepSeek 按落盘单元。写跨厂商应用时,prompt 布局原则是通用的(稳定在前、动态在后)。

七、工程实践:把命中率吃到 90%

缓存机制你控制不了,但 prompt 布局完全在你手里。所有原则都源自一条约束:完整匹配 + 从第一个 token 开始

Prompt 布局最佳实践

具体清单:

  1. system 提示词逐字节固定。完整匹配是全字符串精确匹配——末尾多一个空格、换行符从 \n\n\n,整个前缀全部失效。把 system prompt 当基础设施做版本管理,别在代码里动态拼接;
  2. 动态内容一律后置。时间戳、请求 ID、用户名、“当前是第 N 轮"这类每轮必变的内容,放到消息末尾。经典反例:[当前时间 10:32] + [system] + [知识库] + [问题]——命中率直接归零;
  3. 多轮对话模板逐字一致。messages 的 role 顺序、分隔符、工具定义的序列化顺序都要稳定——Anthropic 在 Claude Code 的经验总结里把这叫作 “Static content first, dynamic content last”,同一个原则;
  4. 长文档场景连发请求。同一份文档上的多个问题连续发送(参考财报例子):前两次请求负责"喂"出公共前缀单元,从第三次开始吃折扣。只问一两个问题就换文档的应用,天然吃不到缓存;
  5. few-shot 示例、知识库、工具定义全部前置。它们越靠前、越稳定,可复用前缀越长——官方列出的受益场景(长预设提示词问答、角色扮演、固定文本集数据分析、代码仓库级分析、few-shot)全部符合这个模式;
  6. 监控命中率并设告警。用 usage 字段算 hit / (hit + miss):第三方经验值是 > 40% 健康,< 20% 说明 prompt 结构需要整改。把它做成和错误率同级的业务指标;
  7. 不要把正确性押在缓存上。best-effort、秒级构建、几小时到几天过期——缓存没命中时应用必须同样正确,只是更贵更慢;
  8. 输出质量不用担心。缓存只作用于输入前缀,temperature 的随机性原样保留,输出效果与不使用缓存相同。

八、总结

把两年多的演进压缩成三句话:

  1. MLA(V2)让硬盘缓存成为可能——KV Cache 压缩 93.3%,传输带宽和存储容量降到"低成本的硬盘也装得下、读得动”;
  2. V3.2/V4 的稀疏与压缩架构重塑了缓存语义——滑动窗口 + 压缩条目让 KV 以"不可变单元"组织,匹配从 64-token 块升级为完整前缀单元匹配,部分匹配不再计费;
  3. 计费随之演进——命中折扣从 1/10 加深到 1/30,存储免费、构建免费、不限并发;你唯一需要做的是把 prompt 布局成"稳定在前、动态在后",然后盯住 usage 里的命中率。

对长期运行的 Agent 应用( system prompt + 工具定义 + 项目上下文动辄数万 token)来说,这块"看不见的基础设施"往往就是成本结构里最大的一根杠杆。读懂这篇文档,等于读懂了自己账单的一半。


参考资料

「真诚赞赏,手留余香」

爱折腾的工程师

真诚赞赏,手留余香

使用微信扫描二维码完成支付