☰ DSv4.1 Flash 架构导读

4. 层维度(下):CSA2 与每 token 890 字节

4.1 三件事要一起做

回到第 1 章那个乘积:条目多大、多少条、几层各存一份。GQA/MLA 动第一项,窗口和稀疏动第二项,CED 动第三项。CSA2(压缩稀疏注意力第二版)把三项合成到一个注意力块里,并且额外加了一条:让"选择哪些条目"这件事本身也跨层复用。

先记住它解决的问题。如果 40 层里每层都自己选 512 条,那每层都要跑一遍 Indexer 打分,成本 \(O(N)\) × 层数,第 2.3 节算过这是 \(4.1\times10^9\) 次乘加/查询/层。1M 上下文下,40 层就是每生成一个 token 要扫 \(1.6\times10^{11}\) 次——比全历史主注意力还贵。所以"选择"必须被复用。

判断方法

读这一章之前,先把四个问题分开,后面就不会混:

  • KV 的信息来自哪里——本层隐状态,还是第 20 层(CED);
  • 哪一层负责生产——40 层里只有 4 个来源层(4.3 节);
  • 哪些层重新选地址——8 个索引来源层;
  • 哪些层只重算读出权重——其余做全局分支的层。

第 3 章回答的是第一个问题,这一章回答后三个。四个问题分开之后,"共享"到底共享了什么就清楚了——共享的是缓存的内容与份数,不是注意力计算本身。

4.2 压缩器:把相邻 2 个位置合成 1 条

序列维度的压缩由一个叫压缩器(compressor)的模块做。压缩比 \(m\) 的含义是"每 \(m\) 个原始位置合成一个缓存条目"。V4.1 Flash 的取值是分段函数:

\[ m_l=\begin{cases}2,& l\le 20\ (\text{Encoder})\\[2pt] 1,& l>20\ (\text{Decoder})\end{cases} \]

Encoder 用 \(m=2\) 是有道理的:它的职责是"把长历史压成一份表示",压一半能同时减少条目数和 Indexer 候选数。Decoder 用 \(m=1\)(即不压缩),按报告的取舍是为了保留逐位置的细粒度全局记忆;按块寻址的自回归解码在原理上也能工作,所以这更像是质量上的选择,而不是算法上的必要条件。

注意"压一半"不等于"两两相加"。报告里的压缩器是一个逐通道学习权重的池化:对通道 \(c\),

\[ c^{(c)}_{j}=\frac{w^{(c)}_{0}x^{(c)}_{2j-1}+w^{(c)}_{1}x^{(c)}_{2j}}{w^{(c)}_{0}+w^{(c)}_{1}}, \]

这里 \(w_0^{(c)},w_1^{(c)}\) 是随通道变化、由训练学出来的参数。两点要注意:要让它真的是"池化",权重通常得非负(由 softmax 或 softplus 给出)且分母不为零,否则组合可能落到两个输入之外;如果实现里的权重其实是由输入投影得到的门控,这个式子就只是静态示意。读报告时按实际实现核对。CSA2 相对上一代 CSA 删掉了两样东西:相邻压缩条目之间的重叠(原来每个条目由 \(2m\) 个原始条目产生,与邻居共享一半),以及压缩过程中的绝对位置嵌入。删得掉是因为 Encoder 最前面已有两层 SWA(感受野 255,见 2.2 节),局部顺序信息已经进到表示里了,压缩器不必再自己编码位置。

4.3 三种模式:两个指示量说清一切

每一层的注意力需要四样东西:主查询 \(q\)(本层自己算)、本地窗口 KV(本层自己算)、主 KV 内容 \(C\)、Top-K 选择集 \(S\)。三者的差别只在后两样由谁产出。用两个 0/1 指示量就写完了:

\[ \mathbb 1^{\text{kv}}_l=\big[\kappa(l)=l\big], \qquad \mathbb 1^{\text{idx}}_l=\big[\iota(l)=l\big], \]

其中 \(\kappa(l)\) 是第 \(l\) 层的主 KV 来源层,\(\iota(l)\) 是它的索引来源层。"\(=l\)" 表示本层自己算,"\(\ne l\)" 表示复用更早的某一层。三种模式恰好是三种取值组合:

模式     主 KV    K     Top-K
Full      写      写     算
Reindex   读      读    重算
Reuse     读     不读    读

用体系结构的语言说,这三种就是缓存的三种命中状态:Full 是缺失后写回填充,Reindex 是数据命中、地址重算,Reuse 是全命中、只做一次读出。第 5 章会证明为什么"只读"不算退化。

把它们排到 40 层上,结构就出来了。Encoder(第 1–20 层)= 2 层纯 SWA + 3 个 6 层块,每块是 1 层 Full 加 5 层 Reuse;Decoder(第 21–40 层)= 1 个 4 层块(1 Full + 3 Reuse)+ 4 个 4 层块(各 1 Reindex + 3 Reuse)。

Encoder  SWA SWA
         3 x (F R R R R R)
Decoder  F R R R
         4 x (X R R R)

F=Full  R=Reuse  X=Reindex

数一下:Encoder \(2+3\times6=20\),Decoder \(4+4\times4=20\),合计 40。全局 KV 只有 4 个来源层在产出。报告与代码用 0-based 下标,记作 layer_idx = 2, 8, 14, 20;换算成第几层(1-based),就是第 3、9、15、21 层——正好是"两层 SWA 之后,每个块的第一层"。这就是"全网只保留 3 份 Encoder 缓存和 1 份 Decoder 缓存"。

层号约定:本篇正文一律用 1-based,\(l\in\{1,\dots,40\}\);引用配置时给 0-based 下标。看到"第 20 层"时,它指的是 Encoder 的最后一层,对应 layer_idx=19。

4.4 890 字节的账

现在可以算报告里那个数字了。每层的全局 KV 由两部分组成:主 KV(\(d_c=512\) 个通道)和 Indexer K(\(d_{\text{idx}}=128\) 个通道)。两者都是 4 bit 载荷,但不是同一种 FP4 格式——这是报告里最容易读漏的一处:

报告的取舍写在 2.4.4:MXFP4 是为了兼容尽量多的硬件平台,尽管实验里别的格式精度更高;主 KV 需要更细的分组,才改用 NVFP4 系的 E4M3 分组缩放。于是一个 \(D\) 通道、分组 \(G\) 的条目占

\[ b(D,G)=\underbrace{\frac{D}{2}}_{\text{4 bit/通道}}+\underbrace{\frac{D}{G}}_{\text{缩放因子}}\ \text{字节}. \]

逐项代入(注意主 KV 的 \(G=16\)、Indexer K 的 \(G=32\)):

\[ b_{\text{kv}}=b(512,16)=\frac{512}{2}+\frac{512}{16}=256+32, \] \[ b_{\text{idx}}=b(128,32)=\frac{128}{2}+\frac{128}{32}=64+4, \] \[ \Longrightarrow\quad b_{\text{kv}}=288\ \text{B},\qquad b_{\text{idx}}=68\ \text{B}. \]

接下来乘"多少条"和"几层"。压缩比为 \(m\) 的层每 \(m\) 个 token 才存一条,所以每 token 摊到的是 \(b/m\);来源层只有 4 个,其中三个 \(m=2\)、一个 \(m=1\):

\[ \sum_{l\in\mathcal F}\frac{1}{m_l} =\underbrace{\frac12+\frac12+\frac12}_{\text{第 }3,9,15\text{ 层}}+\underbrace{\frac{1}{1}}_{\text{第 }21\text{ 层}} =2.5 . \] \[ S_{\text{main}}=288\times 2.5=720\ \text{B}, \] \[ S_{\text{idx}}=68\times 2.5=170\ \text{B}, \] \[ S_{\text{global}}=720+170=\boxed{890\ \text{B/token}} . \]

合并成一张账:

来源层     m   主 KV   Index K   小计
3,9,15     2    432      102     534
21         1    288       68     356
合计           720      170     890

1M 上下文就是把 890 B 乘一百万:0.89 GB——这是常驻容量,不是每步读取量(2.5 节)。对比第 2.1 节全注意力 MLA 的 40.96 GB,以及完全不分层的 3514 B/token(4.6 节)。

4.5 FP4 会不会掉精度:动态范围和量化误差是两件事

量化最怕的第一件事是动态范围不够——值一大就被截断。算一下这个格式的上界:E2M1 能表示的最大幅度是 6,缩放因子是 E4M3(最大 448),所以单通道最大可表示

\[ 6\times 448=2688 . \]

而实际需要表示的幅度有多大?报告给了一条完整的不等式链:KV 潜变量在 RoPE 之前过了一次 RMSNorm,训练后最大的 RMSNorm 权重幅度约为 1,所以 512 维向量的 L2 范数上界约为 \(\sqrt{512}\);RoPE 是旋转,保长,旋转后单通道的绝对值仍被 \(\sqrt{512}\approx22.6\) 界住。训练中实际观测到的最大值约 10。

\[ \text{上界 }2688\ \gg\ \sqrt{512}\approx22.6 \ \gtrsim\ \text{实测 }10 . \]

所以不额外加一级全局缩放因子不会因为溢出而出问题。报告的原话是:省略这一级"没有可测量的精度下降,同时简化了缓存布局"。

但要强调:2688 这个上界只说明"不饱和",不说明"4 bit 本身就够"。4 bit 只有 16 个可表示值,小值会下溢,分组内的尺度差异会损害精度,舍入误差还会逐层累积——这些都不是动态范围能回答的。真正让 FP4 可用的是第 8.3 节的 QAT:前向模拟量化、继续训练,把信息挪到量化后仍能分辨的位置上。报告把这两件事分开写,读的时候也要分开。两个工程细节:

常见错误

把 FP4 当成"用 FP4 做矩阵乘"。报告明确说 FP4 的作用是减少存储,注意力计算前会反量化成更精确的格式。所以它能在没有原生 FP4 矩阵乘单元的硬件上跑。

4.6 从 3514 到 890:四步账

报告只给出一个结论数:每 token 全局 KV 是 V4-Flash 的约 1/4(890 B,见 Figure 1b),对 DeepSeek-V1 则是 437 倍。下面这张分解表是本篇按报告配置反推的,不是报告原文;它的用处是看清"哪一步在省、哪一步在花钱",而不是替代那个 1/4。V4-Flash 按 43 层、CSA 与 HCA 混合、每层独立持有全局缓存,条目格式 448 通道 FP8 + 64 通道 BF16 RoPE + 8 字节缩放:

\[ 448\times1+64\times2+8=584\ \text{B}. \]

21 层 CSA(\(m=4\),带 Indexer)与 20 层 HCA(\(m=128\),无 Indexer):

\[ B_{4\text{-Flash}} =21\times\frac{584+68}{4}+20\times\frac{584}{128} \] \[ =21\times163+20\times4.5625 =3514.25\ \text{B}. \]

然后按四步拆:

  1. 加跨层复用:41 个来源坍缩成 4 个,仍是 \(m=4\)、仍是 V4 格式。\(4\times163=652\) B,因子 \(\div5.39\)。
  2. 放松序列压缩:\(m\) 从 4 松到 2(Encoder)/1(Decoder),\(\sum1/m\) 从 1 变成 2.5。\(652\times2.5=1630\) B,因子 \(\times2.5\)。这是一次"花钱"的改动:为了 Decoder 能逐位置寻址,条目数放回来了。
  3. 主 KV 换 FP4:584 B \(\to\) 288 B / 条目(Indexer K 仍是 68 B)。\(1630\times(288+68)/(584+68)=890\) B,因子 \(\div1.83\)。
  4. 净效果:\(3514.25/890=3.95\),即"KV 再压 4 倍"。
判断方法

把三步写成一条链:\(3514.25\div5.39\times2.5\div1.83\approx890\),对应的净节省倍数是 \(5.39\div2.5\times1.83\approx3.95\)。注意第 2 步是乘不是除——它不是节省,而是主动花钱。任何"省了 N 倍"的账都要这样拆开看,否则会把"假装每一步都在省"的 \(7.4\) 当成结论,然后去找一个并不存在的差额。

4.7 HSI:把后续层的搜索域变成常数

Reindex 层仍要自己重选 512 条。若允许它在全历史里选,成本还是 \(O(N)\)。HSI(分层稀疏 Indexer)的做法是:让一个 Full 层顺便产出一个候选池,后续 Reindex 层只在池子里选。注意它常数化的是后续层的搜索域,不是候选池的生成成本——产出池子的那个 Full 层仍要扫全部可见历史。

把因果可见的历史按宽度 \(b=8\) 切块,第 \(r\) 块记为 \(B_r\)。Full 层本来就要给每个位置打分,顺手在同一份分数上做块内取最大:

\[ g_r(t)=\max_{j\in B_r}s(t,j), \] \[ \mathcal B_t=\operatorname{TopM}_{r}g_r(t), \qquad M=2048, \] \[ P_t=\bigcup_{r\in\mathcal B_t}B_r, \qquad |P_t|\le 2048\times8=16384 . \]

于是后续 Reindex 层的搜索空间从"全部 \(N\) 个位置"降到"至多 16,384 个位置",而且这个上界与上下文长度无关:

\[ \underbrace{N}_{\text{Full:扫全部历史}} \;\longrightarrow\; \underbrace{\min(N,16384)}_{\text{Reindex:搜索域上界固定}} . \]

两端都是搜索域的大小,不是最终输出条数——两种层最终都只选至多 512 条。取块内最大而不是均值,是为了避免"峰值被平均稀释":一个块里只要有一个位置极相关,这个块就该入选。另有一个边界策略:包含最新可见位置的块,其分数被临时设成 \(+\infty\),强制占用一个名额,保证"最近的内容一定在池子里"。

HSI 是训练感知的:它在后训练阶段引入,训练与推理施加完全相同的候选限制,使深层 Indexer 在推理真正使用的搜索域下被优化。这也是为什么它不能"事后直接开"——训练时能搜全历史、上线时只给一份名单,训推分布就不一致了。

报告说得很清楚:产池子的是Decoder 里第一个被指定为 Full 的层,config 的 candidate_source_layer_id = 20 就是它(1-based 第 21 层)。所以 Encoder 那三份缓存不参与索引池;池子的地址域与 Decoder 的 Reindex 层一致。同一节还有一句最容易被忽略的话:"第一个 Full 层仍然要扫全部因果可见范围"——HSI 省的是后续层,不是它自己。

4.8 容易算错的地方

常见错误
  • 用 4 字节/通道算主 KV。512×4=2048 B;按最终配置的 2.5 份算就是 5120 B,把 Indexer 也算上则是 6400 B——和 890 B 差一个数量级。
  • 忘了缩放因子。288 里 32 B 是缩放,占 11%。按最终配置(主 KV \(32\times2.5\) + Indexer \(4\times2.5\))算,漏掉它每 token 少 90 B;只有按"40 层各持一份、未压缩"的假想模型算,才是 1.3 KB。
  • 把 2.5 当成"压缩比"。它是 \(\sum 1/m\),是"层数 × 序列压缩"合并后的等效份数。
  • 把 16384 当成最终选中数。16384 是候选池,最终每层只读 512 条。
  • 认为 Reindex 会重建 KV。它复用主 KV 与 Indexer K,只重跑"地址生成"(打分 + Top-K)。