☰ DSv4.1 Flash 架构导读

1. 条目大小:一条 KV 到底占多少字节

1.1 起点:barm 2.8 存了什么

barm 2.8 解释 KV cache 时给的是最朴素的版本:为了不重算历史,把每一层、每个历史位置的 \(k\) 和 \(v\) 都留下来,于是 prefill 的全注意力项是 \(O(T^2d)\);有了缓存,之后每步只处理新位置,查询与历史缓存交互的注意力项是 \(O(Td)\)。那里没有说明的是这份缓存有多大。

把 barm 的写法补上单位。第 \(l\) 层的第 \(t\) 个位置存两个向量 \(k_t^{(l)},v_t^{(l)}\),每个 \(H_{kv}\) 个头、每头 \(d_h\) 维,每个数 \(s\) 字节:

\[ \text{bytes/token/layer}=2\,H_{kv}\,d_h\,s . \]

系数 2 是 K 和 V 各一份。这就是数学基础第 9 节和第 10 节里那条公式的出处;那里用 Qwen3-0.6B 的 \(H_{kv}=8\)、\(d_h=128\)、fp32 算出 224 KiB/token,这一节不再重复,只把同样的式子代进 V4.1 Flash 的量级。

1.2 先看最贵的那种:64 个 KV 头各存各的

V4.1 Flash 有 64 个 Q 头,每个 512 维。假设它用最朴素的 MHA——64 个 KV 头也各存 512 维的 K 和 V,fp16 两字节:

\[ 2\times 64\times 512\times 2 = 131{,}072\ \text{B}. \]

这是一层、一个 token。40 层:

\[ 131{,}072\times 40=5{,}242{,}880\ \text{B}, \] \[ \approx 5.24\ \text{MB}\ (5.0\ \text{MiB}). \]

一个 1M 上下文的会话,光 KV 就要 5.24 TB。这个数字说明了报告为什么不是"优化一下"而是"重新设计":MHA 在长上下文下不是慢,是物理上放不下。

判断方法

看到任何"KV 省了 N 倍"的说法,先问它在算哪一个量:每 token 的容量、每会话总量、每步读取流量,还是持久化到 SSD 的总量。每 token 到会话总量要乘上下文长度;运行时容量到持久化量还取决于保存策略、格式与元数据,不只是长度因子。

1.3 第一步:GQA 只共享"被查询的内容"

GQA 让多个 Q 头看同一组 K/V。数学基础第 9 节已经推过映射 \(g(h)=\lfloor h/G\rfloor\) 和它省的是内存而不是注意力 FLOPs,这里只用它的结论:KV 头数从 64 降到 \(H_{kv}\),字节数按 \(H_{kv}/64\) 线性下降。

GQA/MQA 的代价在 9.1 节已经说过:同一组里的 Q 头虽然权重不同,但只能看到同一份键值内容。压缩到 1 组时,64 个头挤在一个 512 维的键空间里,表达力开始下降。这就逼出了下一步——能不能既只存一份,又不让所有头挤在同一个空间里?

1.4 MLA:把 K 和 V 都变成同一个潜在向量的线性函数

MLA(多头潜在注意力,multi-head latent attention)的想法是:K 和 V 不必各自独立地从隐状态算出来,它们可以是同一个低维向量的两个上投影。设

\[ c_t=W_D x_t\ \in\mathbb{R}^{d_c}, \] \[ k_t^{(h)}=W_K^{(h)}c_t, \qquad v_t^{(h)}=W_V^{(h)}c_t , \]

其中 \(x_t\in\mathbb{R}^{d}\) 是该层输入,\(W_D\in\mathbb{R}^{d_c\times d}\) 是所有头共用的降维矩阵,\(W_K^{(h)},W_V^{(h)}\in\mathbb{R}^{d_h\times d_c}\) 是每个头上投影。缓存里只放 \(c_t\),一个 512 维向量。这是唯一被存下来的东西。

现在关键的一步:既然只存 \(c_t\),那历史位置的 \(k_t,v_t\) 每次都要现算吗?不需要。代进分数:

\[ s_{t,j}^{(h)}=\big\langle q_t^{(h)},\,k_j^{(h)}\big\rangle =\big\langle q_t^{(h)},\,W_K^{(h)}c_j\big\rangle \] \[ =\big\langle \underbrace{W_K^{(h)\top}q_t^{(h)}}_{=\,\tilde q_t^{(h)}},\,c_j\big\rangle . \]

\(\tilde q_t^{(h)}\) 可以在算 \(q\) 的时候顺手算好,是一次的矩阵乘,与历史长度无关。所以历史侧永远只出现 \(c_j\),\(W_K^{(h)}\) 被"吸收"进了查询。同理看输出:

\[ o_t^{(h)}=\sum_{j}p_{t,j}\,v_j^{(h)} =\sum_j p_{t,j}\,W_V^{(h)}c_j \] \[ =W_V^{(h)}\Big(\sum_j p_{t,j}\,c_j\Big). \]

这里用的是矩阵乘对线性组合的分配律:求和与 \(W_V\) 可以交换顺序。于是整条注意力通路变成"用 \(\tilde q\) 对 \(c_j\) 加权求和,最后乘一次 \(W_V^{(h)}\)"。历史侧只需要 \(c_j\),一份 512 维,供给 64 个头共 64 次读取。

严格说,"不损失表达力"要限定一下:MLA 是把模型限制在"K、V 是同一个 512 维向量的线性函数"这个族里,然后在这个族内不做任何妥协。相对 MHA 它是一个结构假设(降秩),不是一个近似;训练时模型已经学会在这个假设下工作。这也是为什么报告可以同时说"参数更多"和"KV 更小"——多出来的参数去补表达力,省下来的字节来自结构。

1.5 为什么 512 维就够了:一个秩的论证

直觉上 64 个头各要 512 维的键,合计 32,768 个数字,凭什么压成 512 个?因为存下来的不是 \(k\) 本身,而是生成 \(k\) 的那 512 个数。把 64 个头的键竖着拼起来:

\[ k_j=\begin{bmatrix}k_j^{(1)}\\ \vdots\\ k_j^{(64)}\end{bmatrix} =\begin{bmatrix}W_K^{(1)}\\ \vdots\\ W_K^{(64)}\end{bmatrix}c_j =\mathcal W_K\,c_j , \] \[ \mathcal W_K\in\mathbb{R}^{32768\times512},\qquad c_j\in\mathbb{R}^{512}. \]

这个"内容 → 键"的映射秩不超过 512。所以只要模型真的采用了这个分解,存 512 个数就足以重建全部 32,768 个键坐标——这是恒等重写,不是近似。查询侧同理:吸收之后历史侧永远只出现 \(c_j\)。

常见错误

把这段读成"512 维必然足够保持模型质量"。它只说明一旦 K 写成 \(W_Kc_j\),存 \(c_j\) 不丢信息;没有证明 \(d_c=512\) 对任意任务都够。一般 MHA 的联合键映射没有"秩不超过 512"的保证,硬压就是有损近似。

1.6 V4.1 Flash 的具体取值

报告里这一块的配置:

于是每 token、每层的条目大小(fp16)是:

\[ \underbrace{512}_{d_c}\times\underbrace{2}_{\text{fp16}}=\boxed{1024\ \text{B}} . \]

对比 1.2 节的 MHA:131,072 / 1024 = 128 倍。对比 MQA:2048 / 1024 = 2 倍——多的那 2 倍正是"K 和 V 分开存"与"K = V = 同一个 \(c_j\)"的区别。

1.7 但 40 层仍是 40 份

别忘了 1.2 节那个乘数。1024 B × 40 层 = 40,960 B/token;1M 上下文就是 40.96 GB。这仍然不可接受。所以"条目大小"这一维压完了,还剩两维:

三样都动完,才得到 890 B/token(第 4.4 节的账)。这里要分清两条基线:从 1.2 节的 MHA(131,072 B)走到 MLA(1024 B)是第一级压缩;而 584 B → 288 B 是 V4-Flash 到 V4.1 的增量(第 4.6 节),两者不是同一条账。

1.8 容易算错的地方

常见错误
  • 把 \(d_c=512\) 当成 \(H\cdot d_h\)。512 是一个潜在向量的长度,64 个头都读它;写成 \(64\times512=32768\) 就退回到 MHA 了。
  • 忘了 RoPE 的 64 维也在 512 里。它不是额外加的 64 维,而是 512 中切出来的一段;所以条目还是 512 个数,不是 576。
  • 用 fp16 算完就当成报告的 890 B。890 B 是 FP4 载荷 + 缩放因子,见 4.4;这里 1024 B 只是这条压缩链上的第三级。
  • 把"吸收"理解成"把 \(W_K\) 删了"。矩阵还在,只是乘在 \(q\) 上、以头为单位、每个新 token 只乘一次,不进历史。