先固定一组参数。我们用它把单头 Attention、多头、KV Cache 和分组共享完整算一遍。

符号 含义 本文取值
\(D\) 输入 hidden size 4096
\(H_q\) Q head 数 32
\(H_{kv}\) KV head 数 MHA 为 32,GQA 为 8,MQA 为 1
\(d_h\) 每个 head 的 Q/K/V 维度 128
\(S_q\) 当前计算的 query 数 Prefill 示例为 4096,Decode 为 1
\(T\) 当前 query 可见的历史长度,包含当前位置 Decode 示例为 8192
\(b\) 一个缓存元素的字节数 BF16 为 2

为便于读公式,先省略 batch 轴,所有 token 都写成行向量。Q/K 可以先做位置编码,下面的 Attention 公式使用位置处理后的 Q/K。

系列路线 · 下一讲 MLA

1. 一个 head 到底计算什么

给一个输入 \(X\in\mathbb R^{S_q\times D}\)。单个 head 使用三个投影矩阵,把输入变成 query、key、value。首次 Prefill 中 Q/K/V 都由同一段输入生成:

\[Q=XW_Q,\qquad K=XW_K,\qquad V=XW_V, \qquad W_Q,W_K,W_V\in\mathbb R^{D\times d_h}.\]

所以 Q、K、V 的形状都是 \(S_q\times d_h\)。\(W_Q\) 学习怎样提问,\(W_K\) 学习哪些特征便于匹配,\(W_V\) 学习匹配到某个位置后提取哪些信息。三份权重分别学习,结果一般不同。

取第 \(i\) 个 query。它和第 \(j\) 个 key 的匹配分数为

\[s_{ij}=\frac{\sum_{r=1}^{d_h}q_{ir}k_{jr}}{\sqrt{d_h}}+M_{ij}.\]

这里的求和消去了 head 维度,留下 query 位置和 key 位置。因果 mask \(M_{ij}\) 在允许访问的位置为 0,在未来位置为负无穷。对每个 query 的整行分数做 Softmax:

\[p_{ij}=\frac{\exp(s_{ij})}{\sum_{u=1}^{T}\exp(s_{iu})}.\]

然后对 value 加权求和:

\[o_{ir}=\sum_{j=1}^{T}p_{ij}v_{jr}.\]

第一次求和沿特征维 \(d_h\),计算“匹配程度”;第二次沿位置维 \(T\),汇总历史信息。写成矩阵乘就是

\[\underbrace{Q}_{S_q\times d_h} \underbrace{K^{\mathsf T}}_{d_h\times T} \longrightarrow \underbrace{S,P}_{S_q\times T}, \qquad \underbrace{P}_{S_q\times T} \underbrace{V}_{T\times d_h} \longrightarrow \underbrace{O}_{S_q\times d_h}.\]

两个 GEMM 的规约维不同。QK 的 K 维是 head dimension,PV 的 K 维是历史长度。后面看 NPU 阵列时要一直保留这个区别。标准缩放点积 Attention 和多头组合见 Transformer 原论文。Attention Is All You Need

2. 用两个位置手算一次

假设缩放、mask 后的分数为 \([2,0]\)。为了简化,只看 V 的一个通道,两个位置的值为 \([10,20]\)。那么

\[p=\left[\frac{e^2}{e^2+1},\frac1{e^2+1}\right] \approx[0.8808,0.1192],\]
\[o=0.8808\times10+0.1192\times20\approx11.192.\]

换一个 query,分数变成 \([0,2]\),同一份 K/V 就得到

\[o\approx0.1192\times10+0.8808\times20=18.808.\]

这说明同一份历史 K/V 可以被不同 query 以不同方式读取。MQA/GQA 就会利用这一点。

3. MHA 怎样把多个 head 放在一起

32 个 head 各有一套投影。实现中通常把它们的权重横向拼接,一次 GEMM 生成所有 head:

\[W_Q,W_K,W_V\in\mathbb R^{4096\times(32\cdot128)} =\mathbb R^{4096\times4096}.\]

得到 \(Q_{\rm flat}\in\mathbb R^{S_q\times4096}\) 后,把最后一维拆成 \([32,128]\),再按内核需要排列为 \([32,S_q,128]\)。K/V 同理。每个 head 独立执行刚才的 Attention:

\[O_h=\operatorname{softmax}(Q_hK_h^{\mathsf T}/\sqrt{128}+M)V_h.\]

最终把 32 个输出沿特征维拼起来,形状回到 \([S_q,4096]\),再乘输出投影 \(W_O\in\mathbb R^{4096\times4096}\)。这一层可通过 \(W_O\) 组合不同 head 提取的信息。

4. KV Cache 从哪里来

首次 Prefill 一次处理整个 prompt,各层把算好的 K/V 保存下来。因果约束下,过去位置在固定模型、固定前缀和位置处理下的状态无需因新 token 到来而改变。

生成时,每次把一个新 token 送入模型。到某层时,它的输入是 \([1,4096]\)。这一层只为新位置投影 Q/K/V,再把新 K/V 追加到缓存。假设追加后有 8192 个可见位置,单 head 的计算是

\[[1,128]\times[128,8192]\to[1,8192], \qquad [1,8192]\times[8192,128]\to[1,128].\]

旧 Q 无须参加这一步。当前 query 要重新计算自己对所有可见位置的权重,因此旧 K/V 仍然要读。Prefill 最后一个位置的 logits 通常用于采样首个生成 token,下一次 Decode 再消费这个 token。

每 token、每层的 MHA 缓存元素数是 \(2H_qd_h=8192\),乘 BF16 的 2 B,得到 16 KiB。8K 历史是一层 128 MiB;若 32 层均为相同配置,则是 4 GiB。这些值只计 KV 有效载荷。

5. MQA 将 KV head 收到一个

保持 32 个 Q head,但让所有 head 共用一个 K head、一个 V head。投影形状变成

\[W_Q:[4096,4096],\qquad W_K,W_V:[4096,128].\]

每个 Q head 仍然有自己的 query,公式变成

\[O_h=\operatorname{softmax}(Q_hK_{\rm shared}^{\mathsf T}/\sqrt{128}+M)V_{\rm shared}.\]

这里可以直接套用前面两个 query 的手算例子。K/V 相同,分数和输出仍可不同。MQA 的每 token、每层缓存是 \(2\times1\times128\times2=512\) B,8K 历史是一层 4 MiB。相对同维度 MHA 是三十二分之一。共享减少了独立 K/V 投影的表达自由度,质量需要通过训练与评测验证。MQA 原论文

6. GQA 在多个组之间保留独立 K/V

取 8 个 KV head,每 4 个 Q head 组成一组。Q head 0–3 使用 KV head 0,4–7 使用 KV head 1,依此类推。定义

\[g(h)=\left\lfloor h/4\right\rfloor, \qquad O_h=\operatorname{softmax}(Q_hK_{g(h)}^{\mathsf T}/\sqrt{128}+M)V_{g(h)}.\]

投影形状现在是

\[W_Q:[4096,4096],\qquad W_K,W_V:[4096,1024].\]

Prefill 的 Q 是 \([32,4096,128]\),K/V 是 \([8,4096,128]\)。缓存里只需保存后者的 8 组。把 KV 复制到 32 组可以方便写参考实现,优化内核应通过索引或广播读取共享数据。

GQA 的缓存为每 token、每层 \(2\times8\times128\times2=4096\) B,即 4 KiB;8K 历史是一层 32 MiB。GQA 原论文还研究了从 MHA checkpoint 转换并继续训练的方式。已经训练好的独立 K/V 权重不能只靠运行时分组就保持输出不变。GQA 原论文

八个 Q head 的 KV 共享方式

图中为便于观察取八个 Q head。正文算例使用 32 个 Q head。

7. 一组 Q 的 GEMM 形状揭示了复用机会

Decode 取出一个 KV group 的 4 行 query:

\[Q_g:[4,128],\qquad K_g,V_g:[8192,128].\]

这一组可组织成

\[[4,128]\times[128,8192]\to[4,8192], \qquad [4,8192]\times[8192,128]\to[4,128].\]

每行分别归一化。K/V 的一个 tile 只要搬进片上一次,就可以服务四行 Q。

假设 KV tile 长度取 256,QK 是 \([4,128]\times[128,256]\),PV 是 \([4,256]\times[256,128]\)。遍历不同 tile 时需要保存每行的最大值、指数和与输出累积,最后得到整段历史的归一化输出。这部分在线 Softmax 会在后续内核一讲推导。

Prefill 里,一个 KV group 的多个 head、多个 query 位置可以共同提供更多行。例如 16 个位置乘 4 个 head 共 64 行。但每行对应的 query 位置不同,因果 mask 仍需按原位置处理。

8. 缓存节省与计算节省分别有多少

每个 query head 都计算一次 QK 与一次 PV。只看这两项,单 token Decode 合计 FLOPs 约为

\[F_{\rm attention}=2H_qTd_h+2H_qTd_h=4H_qTd_h.\]

代入得到 \(134217728\) FLOPs,约 134.2 MFLOPs。MHA、GQA、MQA 在这个固定配置下相同。K/V 投影的宽度会随 \(H_{kv}\) 缩小,因此投影部分的 FLOPs 确实减少。

结构 8K 历史 KV,每层 QK+PV,单 token 每层 理想 KV 读取算术强度
MHA,32 KV head 128 MiB 134.2 MFLOPs 1 FLOP/B
GQA,8 KV head 32 MiB 134.2 MFLOPs 4 FLOP/B
MQA,1 KV head 4 MiB 134.2 MFLOPs 32 FLOP/B

最后一列假设每份 KV 仅从 HBM 读一次,忽略权重、Q/O、中间结果和页元数据。GQA 若四个 Q head 分开运行并各自重读同一份 KV,实际读量可接近 MHA。任务划分和片上复用会决定理想收益能实现多少。

9. 放到 NPU 上继续看

对一个假设 K tile 宽为 256 的矩阵阵列,QK 的规约只有 128。如果只能补零映射这一维,则有效比例为一半。但阵列可能支持子阵列、不同精度或独立任务打包,要按具体指令形状判断。

同时,Decode 的 \(M=4\) 也很小。即使把 K 维填满,行维仍可能不足。增加活跃请求、组内联合处理 Q head、沿历史拆分工作,都能提供并行任务,不过不同请求的 KV 不同,不能直接把它们当成共享同一 K 的一个大 GEMM。

片上空间还会约束 tile。BF16 的一个 256-token KV tile,两份数据共 \(2\times256\times128\times2=128\) KiB,再加 Q、分数、输出和双缓冲。tile 取大能提升复用,也可能增加缓冲压力。实际调优先固定模型和输入,测 HBM 字节数、矩阵利用率、Vector 归约与同步时间,再决定改变哪个维度。

下一讲把共享进一步推进到 latent 表示。先让 K/V 由同一个低维向量生成,再逐步推导怎样避免每一步展开整个历史。继续阅读 MLA


本站由 Zane Jiang 使用 Stellar 1.33.1 主题创建,一款很棒的 Hexo 主题!

总访问 次 || 本页访问 次
总访客 人 || 本页访客 人