DeepSeek-V4.1 模型结构(8):KV cache 的管理与 SWA Bounded Replay
连载收尾。890 字节是显存里的全局 KV,磁盘上的缓存还要再减一半:办法是滑窗 KV 不再存盘。这一篇讲滑窗 KV 为什么不适合放在保留 72 小时的磁盘缓存里,V4.1 把它挪到了哪,丢了之后怎么只用 128 个 token 的回放重建,这个近似的代价是什么;再讲 CSA2 的跨层共享在流水线并行下怎么训练,以及多模态训练的三处系统改动。
目录13 节
- 一次请求留下三种 KV
- V4 的 SSD 缓存里将近一半是滑窗 KV
- 滑窗 KV 的访问模式和 72 小时的保留期对不上
- V4.1 把滑窗 KV 从 SSD 挪到主机内存
- SWA Bounded Replay:只回放 128 个 token 的近似重建
- Encoder SWA Bounded Replay:让前缀缓存只依赖全局 KV
- Decoder SWA Bounded Replay:让 prefill 停在 encoder
- 近似的代价:同一段 prompt 的结果依赖缓存从哪里命中
- 推理的 kernel:一个 Reuse 层只有十几个
- CSA2 的跨层共享在流水线并行下怎么训练
- 多模态训练的三处系统改动
- 连载收尾
- 资料
对应论文 §3 "General Infrastructures":§3.2 的推理系统,和 §3.1 里与结构直接相关的训练支持。前七篇讲的结构改动,有几处要靠系统配合才能兑现。最主要的一处是:第 3 篇算出的 890 字节是显存里的开销,论文还说磁盘上的缓存是 V4-Flash 的 1/8。多出来的那一半从哪来,是这一篇的主线。
一次请求留下三种 KV
V4.1 的每一层注意力读两种 KV(第 2 篇)。加上 CED 把层分成两半,一共是三种:
| 多大 | 随上下文增长吗 | 由什么生成 | |
|---|---|---|---|
| 全局 KV | 890 B / token | 线性增长 | 4 个 Full 层 |
| encoder 的滑窗 KV | 20 层 × 128 条 × 528 B ≈ 1.35 MB | 不增长 | 第 0 – 19 层各自的隐藏态 |
| decoder 的滑窗 KV | 同上,≈ 1.35 MB | 不增长 | 第 20 – 39 层各自的隐藏态 |
生成的时候,三种都在 GPU 显存里。问题是请求结束之后留什么。
留的理由是前缀缓存。agent 的下一次请求会带着同一段前缀再来,如果上一次算好的 KV 还在,前缀部分就不用重新 prefill。前缀缓存的一般做法见推理服务的缓存与调度。论文把存在 SSD 或主机内存里、供前缀复用的这部分叫 persistent KV cache。
V4 的 SSD 缓存里将近一半是滑窗 KV
V4 连载的第 7 篇讲过 V4 论文给的三种滑窗 KV 存法。V4.1 的论文补充了 V4 线上实际的做法:
- 全局 KV 和滑窗 KV 分开管理,各自按 LRU 淘汰。
- 全局 KV 整条都存。 命中时整段前缀直接复用。
- 滑窗 KV 只在两个位置存。 prompt 的末尾,和输出的末尾。前一个用于重新生成,后一个用于多轮对话的下一轮。命中时从存的那个位置接着算。
- SSD 配得足够大,一般负载下两种 KV 都能留 72 小时以上。
滑窗 KV 每次只存 128 条,为什么能占掉将近一半的空间?因为它不压缩,而且每一层都有。按 V4-Flash 的配置估一下,这是我的估算:
每一轮存两处,6.3 MB。同一轮新增的全局 KV 是每 token 3514 字节。两者相等的点在
一轮对话新增的 token 少于 1800 个时,滑窗 KV 占的空间比全局 KV 还多。论文的说法是:滑窗 KV 的开销相当大,在每轮都很短的多轮对话里尤其如此。
滑窗 KV 的访问模式和 72 小时的保留期对不上
论文接着指出,把滑窗 KV 存进这个缓存,不只是贵,还没用上。
- 全局 KV 的复用是长尾的。 一段前缀可能在几小时甚至几天后再被用到。长保留期有意义。
- 滑窗 KV 只在几分钟内有用。 它只对「紧接着的下一次请求」有意义:同一个会话还活跃时的重新生成,或者下一轮。会话结束,或者下一轮开始之后,这一份滑窗 KV 就再也不会被读了。
一份只在几分钟内有用的数据,占着为 72 小时准备的 SSD 空间。
V4 的论文提过另一种做法:滑窗 KV 干脆不存,丢了就重算。那篇连载里算过代价:精确重建需要让前缀最后 个 token 重新过一遍所有的层。V4.1 的论文说,这个开销在线上被证明高到无法接受。
V4.1 把滑窗 KV 从 SSD 挪到主机内存
V4.1 做了两处改动。
滑窗 KV 换地方。 它不再进 persistent KV cache,而是放进一个分布式的内存池。这个池用的是每台机器 10% 的主机内存,条目的存活时间只有几分钟。池的总容量比 SSD 小得多,但条目过期得快,空间立刻能给新的会话用。论文说在真实负载下,这样的周转速度足以覆盖绝大多数同时活跃的会话。
全局 KV 仍然在 SSD 上,保证至少留 72 小时。
丢了有便宜的补救。 内存池会淘汰条目,所以一定有请求遇到「全局 KV 命中了,滑窗 KV 没了」。这时用下一节的 Encoder SWA Bounded Replay:只重算 128 个 token。论文把它称为整个设计的基石:有了它,滑窗 KV 丢失从一次昂贵的事故变成一次便宜的降级,这才敢把滑窗 KV 从磁盘上拿掉。
1/8 是两个因子相乘:
SWA Bounded Replay:只回放 128 个 token 的近似重建
回放有两个版本,规则相同,用途不同。第 1 篇讲了 decoder 的那一个,这里先把共同的规则摆出来。
规则。 精确重建 层的滑窗 KV 要回放 个 token,因为每一层的滑窗 KV 依赖上一层往前 128 个位置的输出,一层层往前推。Bounded Replay 只回放最近的 个 token,并且把滑窗截断在回放段以内。设回放从位置 开始,位置 的 query 在滑窗分支里只看
回放段开头的 token 滑窗是残缺的,得到的状态是近似的。全局分支读到的全局 KV 是完整的。
Encoder SWA Bounded Replay:让前缀缓存只依赖全局 KV
什么时候用。 请求的前缀命中了 SSD 上的全局 KV,但内存池里已经没有对应的 encoder 滑窗 KV。
怎么做。 取前缀的最后 128 个 token,和后面没缓存的新内容一起送进 encoder。两部分的待遇不同:
- 回放的 128 个 token 只重建滑窗 KV。它们的全局 KV 已经在缓存里,直接用,不重算,也不覆盖。
- 新内容照常计算,既生成全局 KV,也生成滑窗 KV。
效果。 前缀能不能复用,只取决于全局 KV 在不在。滑窗 KV 丢了随时能补。
Decoder SWA Bounded Replay:让 prefill 停在 encoder
什么时候用。 每一次 prefill 之后都用。decoder 的滑窗 KV 从来不缓存。
怎么做。 取 prompt 的最后 128 个 token,把它们在 encoder 的输出送进 decoder 的 20 层,得到 decoder 的滑窗 KV。这份 KV 只用于接下来的生成,不进前缀缓存。
效果。 prompt 里其余的 token 都不用过 decoder,prefill 的计算量减半。
把三种情况的计算量列出来。 是新后缀的长度,单位是「层 × token」。前两行是论文的做法,第三行是我构造的对照:decoder 按论文 §3.2.2 的说法回放 个 token,这些 token 又需要精确的 encoder 输出,往前再推 个,所以 encoder 要回放 5120 个。
| 情况 | encoder | decoder | 时 |
|---|---|---|---|
| 滑窗 KV 还在 | 12560 | ||
| 滑窗 KV 过期,Bounded Replay | 15120 | ||
| 精确重建(我的估算) | 163600 |
滑窗 KV 丢失的代价是多算 个「层 × token」,比正常情况多两成。精确重建约是它的 11 倍。
近似的代价:同一段 prompt 的结果依赖缓存从哪里命中
Bounded Replay 省掉的计算不是白来的。论文对两个版本都明说了结果不精确。
Encoder 的版本影响更深一层。 回放出来的前缀状态是近似的。新内容接在它后面算,于是新内容的全局 KV 和滑窗 KV 也带上了这个近似。论文的原话是:它们依赖缓存命中的位置,在不同的命中位置下数学上并不相同。新内容的全局 KV 会写回缓存、被以后的请求复用,这一步影响会往后传,这是我的推断。
换句话说,同一段文本,一次读完得到的全局 KV,和分几次接续读完得到的全局 KV,会有细微的差别。
论文给的依据。 对 encoder 的版本,论文说实验证据表明它几乎不影响回答质量。对 decoder 的版本,除了同样的结论,还在 post-training 里模拟了同样的回放,让模型适应。论文没有给具体的数字。
论文自己列的局限。 §6 写道:CSA2 可能的选择错误,和 SWA Bounded Replay 的近似状态重建,在没有测到的边界情况下可能造成能力下降。他们会特别关注长上下文下的稀疏检索,以及缓存恢复边界处的滑窗状态重建。
这是 V4.1 和 V4 在取向上的一个差别。V4 的缓存方案全部是精确的,存多少、算多少之间可以调,但结果不变。V4.1 接受了一个有界的近似,换来磁盘空间减半和 prefill 计算减半。
推理的 kernel:一个 Reuse 层只有十几个
论文 §3.2 开头说,V4.1 的结构看起来复杂,推理时的 kernel 流程却很短。办法是把零碎的操作融合进少数几个大 kernel。论文点了名字和所在的库,融合了什么只对 Mega-mHC 有说明,其余是我按名字推断的:
| kernel | 所在的库 | 融合了什么 |
|---|---|---|
| RoPE、注意力、输出反旋转、精度转换合一的 kernel | FlashMLA | 一层的核心注意力 |
| Mega-mHC | DeepGEMM | 残差更新、输入混合、系数预测,见第 4 篇 |
| Mega-Gate、Mega-MoE | DeepGEMM | MoE 的路由和 expert 计算 |
| TopK | DeepSelect | indexer 的 top-512 选择 |
| TileKernels 里的 kernel | TileKernels | 论文没有说明 |
结果是:一个 Reuse 模式的层,prefill 时只执行 15 个 kernel,decode 时 11 个。40 层里有 30 层是 Reuse。这一点和 CSA2 的设计互相成全:Reuse 层没有压缩算子,也没有 indexer,本来要做的事就少。
部署上,论文说采用 Encoder–Prefill–Decode 分离:视觉编码、prefill、decode 三个阶段各自独立扩缩容,执行上可以重叠。
CSA2 的跨层共享在流水线并行下怎么训练
转到训练。大模型训练用流水线并行:40 层按顺序切成几段,每段放在一组 GPU 上,段与段之间只传隐藏态。
CSA2 打破了「只传隐藏态」这个前提。第 20 层生成的全局 KV 要被第 21 – 39 层读,它们很可能不在同一段。反向时,这 19 层对 KV 的梯度还要传回第 20 层。论文 §3.1.2 列了三项支持。
shadow indexer。 论文的描述是:跨层共用的注意力组件,在每个用到它的流水线段上放一个轻量的可执行副本,参数只有一个逻辑上的归属方。归属方负责优化和存 checkpoint,参数同步和梯度聚合让各个副本保持一致。这样流水线的调度器不用把共用的层当作特殊的执行单元。论文没有展开具体是哪些参数有副本。
扩展流水线传递的内容。 源层和用它的层跨了段的边界时,下游要用的中间表示和稀疏选择的结果,随隐藏态一起走已有的点对点通信。这些数据的切分方式和上下文并行保持一致,避免多余的复制,同时保证梯度能沿原路传回来。
按 micro-batch 管理共享状态。 流水线里同时有好几个 micro-batch 在不同的阶段:有的在前向,有的在重算激活,有的在反向。每份共享状态属于哪个 micro-batch 要跟踪清楚。状态留到最后一个用它的层算完,然后立刻释放,免得多占显存。同一套机制也负责记录各层在哪一段、谁是谁的源,注意力的实现不用知道物理上是怎么切的。
加上对优化器、checkpoint、热身和计算图追踪的少量适配,CSA2 对上层的训练接口和流水线调度是透明的。
多模态训练的三处系统改动
§3.1.1 讲的是加入图像之后训练系统要改什么。三处都和「图像的处理不要拖住语言模型」有关。
对比学习阶段,把通信藏在计算后面。 视觉编码器第一阶段的对比损失要在整个 batch 的图文对上算,所以两边的特征都要跨数据并行的各个 rank 收集一遍。论文利用了一个事实:文本特征的梯度只依赖收集来的图像特征,图像特征的梯度只依赖收集来的文本特征。于是可以这样排:
是图像一侧, 是文本一侧, 表示同时进行。图像特征在文本前向的时候收集,文本特征在文本反向的时候收集。两次通信都被计算盖住了。
把视觉编码器拆到语言模型的参数树之外。 每个训练步分三段:视觉编码器前向,语言模型的前向和反向,视觉编码器反向。视觉的计算只出现在头尾两段。中间那一段没有视觉计算,沿用纯文本训练的并行策略。
长序列里的图像分片加载。 一条 1M token 的序列里如果图像很密,加载它会把一台机器的 I/O、CPU 和内存用尽。做法是把一条序列里的图像分到上下文并行的各个 rank 上去加载,每张图只读一次。
论文给了一个判断加载会不会成为瓶颈的条件。加载不拖慢训练,要求加载的时间小于计算的时间:
是 token 数, 是每个 token 对应的原始字节数, 是每个 token 的计算量, 和 是文件系统和 GPU 的处理速率。 在两边消掉了,所以这个条件和序列多长、集群多大无关,只看每个 token 的两个量。 由视觉通路的配置决定,比如分辨率上限和下采样的倍数。结论是:只有每 token 计算量很小的小模型才会被存储拖住,生产规模的模型始终是计算在限速。
连载收尾
九篇讲了 V4.1 相对 V4 的全部结构改动。按它们各自减掉了什么列一张表:
| 改动 | 减掉的是什么 | 减到多少 | 是否精确 | 哪一篇 |
|---|---|---|---|---|
| CED | prefill 的计算 | 约一半 | 靠近似的回放 | 1 |
| CSA2 跨层共享 | 每 token 的全局 KV 条数 | 5.41 → 2.5 | 改的是结构,不是近似 | 2 |
| Hierarchical Sparse Indexer | decode 时 Reindex 层的打分 | 1M 上下文下 1/64 | 改的是结构 | 3 |
| FP4 main KV | 每条 main KV 的字节 | 584 → 288 | 量化,靠 QAT | 3 |
| Single-Pass mHC | 读写残差流的次数 | → | 改的是结构 | 4 |
| DSpark | 每个 token 的平均解码时间 | 取决于负载 | 不改变输出分布 | 6 |
| 滑窗 KV 不存盘 + Bounded Replay | 磁盘上的缓存 | 再减约一半 | 近似 | 8 |
另外两项是往上加的:Engram 加了 196B 不参与激活的参数(第 5 篇),视觉通路加了读图的能力(第 7 篇)。
把这张表和 V4 的放在一起看,能看出两代的侧重不同。V4 的三个升级(压缩稀疏注意力、mHC、Muon)主要是为了把模型做大、把上下文做长。V4.1 的改动几乎都指向同一件事:让长上下文的 agent 负载跑得便宜。显存、磁盘、prefill、decode 四处各有一项对应。论文的标题只提了 KV cache 的压缩,实际的改动覆盖了这四处。
资料
- DeepSeek-V4.1-Flash 技术报告 §3.1、§3.2、§6:arXiv 2609.19969
- V4 的缓存与并行:DeepSeek-V4 模型结构(7)
- 背景:推理服务的缓存与调度、流水线并行、并行策略总览
- 论文提到的 kernel 库:FlashMLA、DeepGEMM、TileKernels、DeepSelect,都在 github.com/deepseek-ai 下
- 官方的参考实现
inference/model.py不含这一篇的任何部署逻辑:没有前缀缓存,没有回放,每个 token 都过 40 层
评论