专栏DeepSeek-V4.1 模型结构·系统9 / 9
12 min学习

DeepSeek-V4.1 模型结构(8):KV cache 的管理与 SWA Bounded Replay

连载收尾。890 字节是显存里的全局 KV,磁盘上的缓存还要再减一半:办法是滑窗 KV 不再存盘。这一篇讲滑窗 KV 为什么不适合放在保留 72 小时的磁盘缓存里,V4.1 把它挪到了哪,丢了之后怎么只用 128 个 token 的回放重建,这个近似的代价是什么;再讲 CSA2 的跨层共享在流水线并行下怎么训练,以及多模态训练的三处系统改动。

目录13 节

对应论文 §3 "General Infrastructures":§3.2 的推理系统,和 §3.1 里与结构直接相关的训练支持。前七篇讲的结构改动,有几处要靠系统配合才能兑现。最主要的一处是:第 3 篇算出的 890 字节是显存里的开销,论文还说磁盘上的缓存是 V4-Flash 的 1/8。多出来的那一半从哪来,是这一篇的主线。

图像文本 token视觉特征写到图像 token 的位置上,和文本 embedding 排成同一条序列复制成 4 份,进入 4 条残差流残差流 ×4每条 5120 维重复 3 组第 2 – 19 层,6 层一组每组 1 层 Full + 5 层 Reuse重复 4 组第 24 – 39 层,4 层一组每组 1 层 Reindex + 3 层 Reuseencoder:第 0 – 19 层decoder:第 20 – 39 层prefill 时,绝大部分 prompt token 只算到这里decoder 的全局 KV 全部由投影得到只有滑窗分支,不读全局 KV40 层的注意力后面都接一个 MoE,下面各行省略不画写读写读重选索引读encoder 的三组各有一份全局 KV,由该组的 Full 层写入,组内 6 层共用encoder 共享池(每组一份)main KV:每 2 个 token 一条,512 维 FP4indexer K:每条 128 维 FP4top-512 索引:每个 query 一份,不进缓存decoder 只有一份全局 KV,由第 20 层从 encoder 末态投影出来,20 层共用decoder 共享池(只有一份)main KV:每个 token 一条,512 维 FP4indexer K:每条 128 维 FP4候选池:第 20 层选出的2048 块 × 8 = 16384 个位置,Reindex 层只在池内打分top-512 索引:第 20 层先写,每个 Reindex 层覆盖一次全局 KV 合计 890 B / tokenSingle-Pass mHC:每个子层一次读、一次写 + 混读用的是上一个子层算好的查表结果经门控后加进残差流logits最后一次只读:4 条流压成 1 条读主干第 37 – 39 层入口处 4 条流的平均一次前向出 5 个草稿 token 和各自的置信度encoder 末态:第 19 层的输出,也就是第 20 层的输入。decoder 所有层的全局 KV 都只从它投影(论文式 1)encoder 末态mHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 B从头训练的视觉编码器:2D-RoPE、RMSNorm、SwiGLU,hidden 1024DeepSeek-ViT32 层 · patch 143×3 pixel-unshuffle 把 9 个相邻 patch 拼到通道维,再过两层 MLP 投影到主干宽度3×3 重排 + MLPtoken ÷ 9 → 5120 维图像位置上的 embedding 被视觉特征覆盖Token Embedding129280 × 5120前两层只有滑窗分支,没有全局 KV滑窗注意力第 0、1 层 · 窗口 128sqrt(softplus) 打分;文本和图像 token 各用一套负载均衡偏置;routed expert 权重 FP4DeepSeekMoE每层的 FFN · 384 选 6 + 1 sharedFull 模式:自己压缩 main KV(每 2 个 token 一条),投影出 indexer K,打分选 top-512,全部写进共享池CSA2 · Full第 2 / 8 / 14 层 · m = 2Reuse 模式:只有自己的 query、滑窗 KV 和输出投影;全局 KV 与 top-512 索引都从共享池读CSA2 · Reuse ×5每组其余 5 层decoder 唯一的 Full 层:输入是 encoder 末态,每个 token 一条 main KV;另选出最多 16384 个位置作为候选池CSA2 · Full第 20 层 · m = 1 · 建候选池Reuse 模式CSA2 · Reuse ×3第 21 – 23 层Reindex 模式:全局 KV 和 indexer K 从共享池读,用自己的 indexer query 在候选池里重新打分,选出新的 top-512CSA2 · Reindex第 24 / 28 / 32 / 36 层Reuse 模式:用本组 Reindex 层选出的 top-512CSA2 · Reuse ×3每组其余 3 层最终归一化RMSNorm不与 embedding 共享权重LM Head→ 129280投机解码的草稿模块:三个 block 一次前向给出 5 个草稿 token 的 logits,Markov head 补上草稿 token 之间的依赖,confidence head 估计每个位置被接受的概率DSpark 草稿层 ×3滑窗注意力 + MoE(128 选 3)按 2、3、4-gram 的哈希查表,取出的向量经门控后加进 4 条残差流;两张表共 196B 参数Engram第 1、14 层入口各一个
序列 · CSA2序列 · 共享池序列 · CED序列 · 滑窗深度 · Single-Pass mHC宽度 · DeepSeekMoE记忆 · Engram解码 · DSpark输入与输出
这一篇讲的是图上两类缓存在部署系统里的去处:右侧的全局 KV,和每一层自己的滑窗 KV。

一次请求留下三种 KV

V4.1 的每一层注意力读两种 KV(第 2 篇)。加上 CED 把层分成两半,一共是三种:

多大随上下文增长吗由什么生成
全局 KV890 B / token线性增长4 个 Full 层
encoder 的滑窗 KV20 层 × 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 的配置估一下,这是我的估算:

43⏟层×128⏟条×576 B⏟每条≈3.2 MB.\underbrace{43}_{\text{层}} \times \underbrace{128}_{\text{条}} \times \underbrace{576\ \text{B}}_{\text{每条}} \approx 3.2\ \text{MB}.

每一轮存两处,6.3 MB。同一轮新增的全局 KV 是每 token 3514 字节。两者相等的点在

6.3 MB3514 B≈1800 个 token.\frac{6.3\ \text{MB}}{3514\ \text{B}} \approx 1800\ \text{个 token}.

一轮对话新增的 token 少于 1800 个时,滑窗 KV 占的空间比全局 KV 还多。论文的说法是:滑窗 KV 的开销相当大,在每轮都很短的多轮对话里尤其如此。

滑窗 KV 的访问模式和 72 小时的保留期对不上

论文接着指出,把滑窗 KV 存进这个缓存,不只是贵,还没用上。

  • 全局 KV 的复用是长尾的。 一段前缀可能在几小时甚至几天后再被用到。长保留期有意义。
  • 滑窗 KV 只在几分钟内有用。 它只对「紧接着的下一次请求」有意义:同一个会话还活跃时的重新生成,或者下一轮。会话结束,或者下一轮开始之后,这一份滑窗 KV 就再也不会被读了。

一份只在几分钟内有用的数据,占着为 72 小时准备的 SSD 空间。

V4 的论文提过另一种做法:滑窗 KV 干脆不存,丢了就重算。那篇连载里算过代价:精确重建需要让前缀最后 L×nwinL \times n_{\text{win}} 个 token 重新过一遍所有的层。V4.1 的论文说,这个开销在线上被证明高到无法接受。

V4.1 把滑窗 KV 从 SSD 挪到主机内存

V4V4.1GPU 显存正在处理的请求全局 KV3514 B / token滑窗 KV每层 128 条全局 KV890 B / token滑窗 KV40 层,每层 128 条主机内存分钟级不放 KVencoder 的滑窗 KV每台机器 10% 的内存,几分钟后过期SSD72 小时以上全局 KV整条前缀都存滑窗 KV每轮存两处,各 128 条全局 KV整条前缀都存decoder 的滑窗 KV 不进任何缓存,每次 prefill 后回放 128 个 token 重建
两代模型的 KV 缓存分别放在哪一层存储。青色是全局 KV,橙色是滑窗 KV。

V4.1 做了两处改动。

滑窗 KV 换地方。 它不再进 persistent KV cache,而是放进一个分布式的内存池。这个池用的是每台机器 10% 的主机内存,条目的存活时间只有几分钟。池的总容量比 SSD 小得多,但条目过期得快,空间立刻能给新的会话用。论文说在真实负载下,这样的周转速度足以覆盖绝大多数同时活跃的会话。

全局 KV 仍然在 SSD 上,保证至少留 72 小时。

丢了有便宜的补救。 内存池会淘汰条目,所以一定有请求遇到「全局 KV 命中了,滑窗 KV 没了」。这时用下一节的 Encoder SWA Bounded Replay:只重算 128 个 token。论文把它称为整个设计的基石:有了它,滑窗 KV 丢失从一次昂贵的事故变成一次便宜的降级,这才敢把滑窗 KV 从磁盘上拿掉。

1/8 是两个因子相乘:

14⏟全局 KV:890 对 3514×≈12⏟滑窗 KV 不再存盘≈18.\underbrace{\frac{1}{4}}_{\text{全局 KV:890 对 3514}} \times \underbrace{\approx \frac{1}{2}}_{\text{滑窗 KV 不再存盘}} \approx \frac{1}{8}.

SWA Bounded Replay:只回放 128 个 token 的近似重建

回放有两个版本,规则相同,用途不同。第 1 篇讲了 decoder 的那一个,这里先把共同的规则摆出来。

规则。 精确重建 LL 层的滑窗 KV 要回放 L×nwinL \times n_{\text{win}} 个 token,因为每一层的滑窗 KV 依赖上一层往前 128 个位置的输出,一层层往前推。Bounded Replay 只回放最近的 nwin=128n_{\text{win}} = 128 个 token,并且把滑窗截断在回放段以内。设回放从位置 ss 开始,位置 ii 的 query 在滑窗分支里只看

[max⁡(s, i−W+1), i ],W=128.[\max(s,\ i - W + 1),\ i\,], \qquad W = 128.

回放段开头的 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 的计算量减半。

滑窗 KV 还在内存池里encoder 只算新来的后缀命中缓存的前缀新的后缀层 × token:滑窗 KV 已经过期Encoder SWA Bounded Replay命中缓存的前缀新的后缀层 × token:对照:精确重建(我的估算)encoder 回放 5120 个,decoder 回放 2560 个命中缓存的前缀新的后缀层 × token:encoderdecoderencoder 正常计算只为重建滑窗 KV 的回放decoder 的回放
前缀命中缓存后一次请求的计算量。S 是新后缀的 token 数。横向不按比例。

把三种情况的计算量列出来。SS 是新后缀的长度,单位是「层 × token」。前两行是论文的做法,第三行是我构造的对照:decoder 按论文 §3.2.2 的说法回放 20×128=256020 \times 128 = 2560 个 token,这些 token 又需要精确的 encoder 输出,往前再推 20×12820 \times 128 个,所以 encoder 要回放 5120 个。

情况encoderdecoderS=500S = 500 时
滑窗 KV 还在20 S20\,S20×12820 \times 12812560
滑窗 KV 过期,Bounded Replay20 (S+128)20\,(S + 128)20×12820 \times 12815120
精确重建(我的估算)20 (S+5120)20\,(S + 5120)20×256020 \times 2560163600

滑窗 KV 丢失的代价是多算 20×128=256020 \times 128 = 2560 个「层 × 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、注意力、输出反旋转、精度转换合一的 kernelFlashMLA一层的核心注意力
Mega-mHCDeepGEMM残差更新、输入混合、系数预测,见第 4 篇
Mega-Gate、Mega-MoEDeepGEMMMoE 的路由和 expert 计算
TopKDeepSelectindexer 的 top-512 选择
TileKernels 里的 kernelTileKernels论文没有说明

结果是:一个 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 收集一遍。论文利用了一个事实:文本特征的梯度只依赖收集来的图像特征,图像特征的梯度只依赖收集来的文本特征。于是可以这样排:

前向(V)→(前向(T) ∥ 收集(V))→∇T→(反向(T) ∥ 收集(T))→∇V→反向(V).\text{前向}(V) \to \big(\text{前向}(T)\ \|\ \text{收集}(V)\big) \to \nabla_{T} \to \big(\text{反向}(T)\ \|\ \text{收集}(T)\big) \to \nabla_{V} \to \text{反向}(V).

VV 是图像一侧,TT 是文本一侧,∥\| 表示同时进行。图像特征在文本前向的时候收集,文本特征在文本反向的时候收集。两次通信都被计算盖住了。

把视觉编码器拆到语言模型的参数树之外。 每个训练步分三段:视觉编码器前向,语言模型的前向和反向,视觉编码器反向。视觉的计算只出现在头尾两段。中间那一段没有视觉计算,沿用纯文本训练的并行策略。

长序列里的图像分片加载。 一条 1M token 的序列里如果图像很密,加载它会把一台机器的 I/O、CPU 和内存用尽。做法是把一条序列里的图像分到上下文并行的各个 rank 上去加载,每张图只读一次。

论文给了一个判断加载会不会成为瓶颈的条件。加载不拖慢训练,要求加载的时间小于计算的时间:

NρBIO<NCBGPU⟺ρ<BIOBGPU C.\frac{N \rho}{B_{\text{IO}}} < \frac{N C}{B_{\text{GPU}}} \quad\Longleftrightarrow\quad \rho < \frac{B_{\text{IO}}}{B_{\text{GPU}}}\, C .

NN 是 token 数,ρ\rho 是每个 token 对应的原始字节数,CC 是每个 token 的计算量,BIOB_{\text{IO}} 和 BGPUB_{\text{GPU}} 是文件系统和 GPU 的处理速率。NN 在两边消掉了,所以这个条件和序列多长、集群多大无关,只看每个 token 的两个量。ρ\rho 由视觉通路的配置决定,比如分辨率上限和下采样的倍数。结论是:只有每 token 计算量很小的小模型才会被存储拖住,生产规模的模型始终是计算在限速。

连载收尾

九篇讲了 V4.1 相对 V4 的全部结构改动。按它们各自减掉了什么列一张表:

改动减掉的是什么减到多少是否精确哪一篇
CEDprefill 的计算约一半靠近似的回放1
CSA2 跨层共享每 token 的全局 KV 条数5.41 → 2.5改的是结构,不是近似2
Hierarchical Sparse Indexerdecode 时 Reindex 层的打分1M 上下文下 1/64改的是结构3
FP4 main KV每条 main KV 的字节584 → 288量化,靠 QAT3
Single-Pass mHC读写残差流的次数20d20d → 10d10d改的是结构4
DSpark每个 token 的平均解码时间取决于负载不改变输出分布6
滑窗 KV 不存盘 + Bounded Replay磁盘上的缓存再减约一半近似8

另外两项是往上加的:Engram 加了 196B 不参与激活的参数(第 5 篇),视觉通路加了读图的能力(第 7 篇)。

把这张表和 V4 的放在一起看,能看出两代的侧重不同。V4 的三个升级(压缩稀疏注意力、mHC、Muon)主要是为了把模型做大、把上下文做长。V4.1 的改动几乎都指向同一件事:让长上下文的 agent 负载跑得便宜。显存、磁盘、prefill、decode 四处各有一项对应。论文的标题只提了 KV cache 的压缩,实际的改动覆盖了这四处。

资料

评论