模型架构 / 注意力机制 原理笔记进阶 19 分钟阅读 查看源文件 ↗

稀疏注意力:选择与压缩机制比较#

本页内容5 个部分

1. Full Attention#

1.1 想解决什么问题#

输入是一个 token 序列 (比如 "The cat sat on the mat") , 经过 embedding 后变成:

x=[x1,x2,…,xL]∈RL×dx = [x_1, x_2, \ldots, x_L] \in \mathbb{R}^{L \times d}

每个 xix_i 是一个 dd 维向量, 代表第 ii 个 token 的当前表示。

核心问题: 当模型处理 "sat" 这个词时, 它的含义其实依赖于 "cat" (谁在 sat) 和 "mat" (sat 在哪) 。所以我们需要一种机制, 让 xix_i 能从其他位置 xjx_j 那里 "取信息"。

  • RNN: 按时间步顺序传递信息 (慢, 远距离信息容易丢)
  • CNN: 固定窗口卷积 (窗口外看不见)
  • Attention: 每个位置都直接和所有位置算一遍相似度, 按相似度加权融合

1.2 Q / K / V: 数据库的软查询#

类比一下数据库:

数据库 Attention
你输入一个查询 (query) qiq_i: 位置 ii "想找什么"
数据库每行有一个 key kjk_j: 位置 jj "我提供什么标识"
匹配上的行返回 value vjv_j: 位置 jj 实际 "装了什么内容"
硬匹配: 要么返回要么不返回 软匹配: 按相似度加权返回所有 value

所以 attention 是个可微的、软的数据库查询。

怎么从 xx 得到 Q/K/V?#

三个不同的线性投影:

Q=xWQ,K=xWK,V=xWVQ = x W_Q, \quad K = x W_K, \quad V = x W_V

其中 WQ,WK,WV∈Rd×dkW_Q, W_K, W_V \in \mathbb{R}^{d \times d_k} 是学出来的参数矩阵。

为什么要三个不同的投影? 因为同一个 token 在 "我想找什么" (query) 、"我能被找到什么" (key) 、"我能提供什么" (value) 这三种角色下应该呈现不同的特征。让模型自己学这三个投影是 attention 最关键的设计决定。

形状:

Q,K,V∈RL×dkQ, K, V \in \mathbb{R}^{L \times d_k}

1.3 Attention 的前向, 一步一步#

Step 1: 算所有 (query, key) 对的相似度#

S=QK⊤∈RL×LS = Q K^\top \in \mathbb{R}^{L \times L}

Sij=qi⋅kjS_{ij} = q_i \cdot k_j = 位置 ii 的 query 和位置 jj 的 key 的点积。这就是 O(L2)O(L^2) 的来源: 你算了一个 L×LL \times L 的矩阵。

Step 2: 除以 dk\sqrt{d_k} (scaled)#

S←SdkS \leftarrow \frac{S}{\sqrt{d_k}}

为什么? 当 dkd_k 大时, qi⋅kjq_i \cdot k_j 是 dkd_k 项加起来的, 方差是 O(dk)O(d_k) 。如果不除, 数值会很大, 后面 softmax 会被推到极端 (几乎 one-hot) , 梯度就消失了。除以 dk\sqrt{d_k} 把方差拉回 O(1)O(1)。

Step 3: Causal mask (只看 LM)#

在自回归语言模型里, 位置 ii 不能看到未来的 j>ij > i 。把上三角设为 −∞-\infty:

Sij←−∞if j>iS_{ij} \leftarrow -\infty \quad \text{if } j > i

经过 softmax 后这些位置就变成 0。

Step 4: Softmax (按行)#

A=softmax(S),Aij=exp⁡(Sij)∑kexp⁡(Sik)A = \text{softmax}(S), \quad A_{ij} = \frac{\exp(S_{ij})}{\sum_{k} \exp(S_{ik})}

A∈RL×LA \in \mathbb{R}^{L \times L}, 每一行加起来等于 1。AijA_{ij} 就是 "位置 ii 给位置 jj 分配多少注意力"。

Step 5: 用注意力权重加权 value#

O=AV∈RL×dkO = A V \in \mathbb{R}^{L \times d_k}

第 ii 行: oi=∑jAijvjo_i = \sum_j A_{ij} v_j —— 把所有位置的 value 按 ii 给出的权重加权平均。

Step 6: 输出投影#

output=OWO,WO∈Rdk×d\text{output} = O W_O, \quad W_O \in \mathbb{R}^{d_k \times d}

回到 RL×d\mathbb{R}^{L \times d}, 可以接残差连接、下一层。

一行公式总结#

Attention(Q,K,V)=softmax ⁣(QK⊤dk)V\boxed{\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{Q K^\top}{\sqrt{d_k}}\right) V}

1.4 Multi-Head Attention: 并行多个 attention#

动机: 单个 attention 只能学到一种 "关注模式"。你可能想同时关注:

  • 语法结构 (主语在哪)
  • 语义关联 (代词指代谁)
  • 位置远近
  • ...

做法: 把 dd 维拆成 hh 个 head, 每个 head 维度 dk=d/hd_k = d / h, 并行跑 hh 个独立的 attention, 然后把结果在 dd 维度拼起来。

MHA(x)=Concat(head1,…,headh)WO\text{MHA}(x) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h) W_O

每个 head 有自己的 WQ(i),WK(i),WV(i)W_Q^{(i)}, W_K^{(i)}, W_V^{(i)}。

总参数量和单 head 差不多, 但模型获得了 hh 种不同视角的能力。典型配置: d=4096,h=32,dk=128d=4096, h=32, d_k=128。

1.5 KV Cache: 推理时的关键优化#

自回归生成时, 你一个 token 一个 token 地往外吐:

step 1:  输入 [x1]              → 生成 x2
step 2:  输入 [x1, x2]          → 生成 x3
step 3:  输入 [x1, x2, x3]      → 生成 x4
...

每次都要重算所有 key 和 value, 但 x1,…,xt−1x_1, \ldots, x_{t-1} 的 K 和 V 上一步就算过了, 且不会变 (因为前面的 token 不依赖未来) 。

KV cache = 把已经算出的 K,VK, V 缓存起来, 新 token 来时只算它自己的 kt,vtk_t, v_t, append 到 cache 上。

代价: cache 占显存。每层、每个 head 都要存, 长度 LL 时:

KV cache size=2×L×d×nlayers×batch\text{KV cache size} = 2 \times L \times d \times n_\text{layers} \times \text{batch}

(2 是 K 和 V 各一份)

这就是为什么 1M context 这么贵 —— KV cache 会爆显存。

1.6 复杂度的两面: 训练 vs 推理#

计算量 内存 (KV cache)
训练 (并行处理整段序列) O(L2⋅d)O(L^2 \cdot d) 不需要 cache
推理 prefill (一次吃完 prompt) O(L2⋅d)O(L^2 \cdot d) 写入 O(L⋅d)O(L \cdot d)
推理 decode (逐 token 生成) 每步 O(L⋅d)O(L \cdot d) 读取 O(L⋅d)O(L \cdot d)

无论哪一面, LL 一大就爆。这就是为什么需要 sparse attention:

  • 训练时: 把 O(L2)O(L^2) 降到 O(L⋅k)O(L \cdot k) (NSA、DSA 都是干这个)
  • 推理时: 压缩 KV cache (MLA、CSA、HCA 都涉及)

1.7 一句话总览#

Attention = 每个 token 用自己的 qq, 在所有 kk 上算相似度 → softmax → 加权融合所有 vv

MHA = 多个 attention 并行, 每个看不同模式

KV cache = 推理时缓存已算过的 K,VK, V, 用显存换计算

核心瓶颈 = QK⊤QK^\top 是 L×LL \times L, 长上下文必须想办法 sparse / compress

2. NSA (Native Sparse Attention)#

DeepSeek 2025-02 的论文 (arXiv:2502.11089) 。它提出面向硬件效率、支持端到端训练的原生稀疏注意力方案;不把它表述为整个领域的“第一个”方案。

2.1 出发点: full attention 的瓶颈, 以及它给的提示#

Full attention 在长序列上是 O(L2)O(L^2) 。但仔细看注意力矩阵 AA:

Aij=softmax(qi⋅kj/dk)A_{ij} = \text{softmax}(q_i \cdot k_j / \sqrt{d_k})

经验上, 一个 query 的注意力分布高度集中 —— 通常 99% 的概率质量落在不超过几百个 key 上, 剩下的全是接近 0 的"无效计算"。

这给了一个明显的优化方向: 别算那 99% 的接近-0 的项。但有两个挑战:

  1. 怎么知道哪些项接近 0? —— 你得算了才知道。Sparse attention 的核心难点就是 "不算就预测哪些 key 重要"。
  2. 能不能端到端训练? —— 早期方案 (BigBird、Longformer) 用手工预设的稀疏模式, 推理快, 但 sparse 模式不参与学习, 精度有限。

NSA 的回答: 用一个 "compressed 视角" 先粗略扫一遍, 根据这个粗略结果决定哪些细粒度位置值得精确算。整个过程可微, 可以端到端训练。

2.2 三路并行: 总览#

对于每个 query 位置 tt, NSA 同时跑三条 attention 分支:

            ┌────────────────────────────────────────┐
            │                                        │
   q_t  ────┼──→  Compress branch  (粗略 + 全局)     ──┐
            │                                          │
            ├──→  Select branch    (精确 + 选择性)    ──┼─→  门控融合 → o_t
            │                                          │
            └──→  Window branch    (本地 + 永远看)    ──┘

每条分支只看序列的一小部分, 但合在一起覆盖了 "全局粗略 + 局部精准 + 本地兜底" 三个层次。

2.3 三条分支的细节#

Compress branch: 粗略的全局视角#

做法: 把 KV 序列按 block 长度 ll (典型 l=32l=32) 分成段:

原序列 (长度 L)→block 0, block 1,…,block L/l−1\text{原序列 (长度 } L\text{)} \to \text{block } 0, \text{ block } 1, \ldots, \text{block } L/l - 1

每个 block 通过一个学出来的压缩函数 φ\varphi (就是个小 MLP) 压成 1 个 token:

kbcmp=φK(kbl,kbl+1,…,kbl+l−1)∈Rdkk^{\text{cmp}}_b = \varphi_K(k_{bl}, k_{bl+1}, \ldots, k_{bl+l-1}) \in \mathbb{R}^{d_k}

vbcmp=φV(vbl,vbl+1,…,vbl+l−1)∈Rdkv^{\text{cmp}}_b = \varphi_V(v_{bl}, v_{bl+1}, \ldots, v_{bl+l-1}) \in \mathbb{R}^{d_k}

(注意 kk 的 φK\varphi_K 和 vv 的 φV\varphi_V 是两个不同的学习模块, 不共享参数。)

压缩后, query tt 对全部 L/lL/l 个压缩 entry 做 dense attention:

attncmp(t)=softmax(qt⋅Kcmp⊤/dk)⋅Vcmp\text{attn}_{\text{cmp}}(t) = \text{softmax}(q_t \cdot K^{\text{cmp} \top} / \sqrt{d_k}) \cdot V^{\text{cmp}}

复杂度: O(L⋅L/l⋅dk)O(L \cdot L/l \cdot d_k)。当 l=32l = 32 时, 这一支比 full attention 快 32 倍。

作用: 给 qtq_t 一个全局但模糊的视野 —— 它知道 "远处某个 block 大致讲了啥", 但精度有限 (因为一个 block 被压成 1 个向量了) 。

Select branch: 精确的稀疏定位#

这是 NSA 最关键、最巧妙的一步。

问题: 我想在某些 block 里看原始 token (精确), 但只看少数几个 block (稀疏)。怎么决定挑哪几个 block?

NSA 的做法: 复用 compress branch 的注意力分数。

具体来说: 在 compress branch 算 softmax 时, 我们已经得到了 qtq_t 对每个压缩 block 的注意力 pb∈[0,1]p_b \in [0,1]。这个 pbp_b 正好就是 "block bb 整体对 qtq_t 的重要程度" —— 直接拿来当 block 的 "重要性分数"。

然后 top-nn 选最重要的 nn 个 block (典型 n=16n=16):

selected_blocks(t)=top-n by importance score pb\text{selected\_blocks}(t) = \text{top-}n \text{ by importance score } p_b

被选中的 block 里, query tt 对原始 (未压缩的) token 做 dense attention:

attnsel(t)=softmax(qt⋅Ksel⊤/dk)⋅Vsel\text{attn}_{\text{sel}}(t) = \text{softmax}(q_t \cdot K^{\text{sel} \top} / \sqrt{d_k}) \cdot V^{\text{sel}}

其中 Ksel,VselK^{\text{sel}}, V^{\text{sel}} 是 selected blocks 里的原始 KV (总长度 n⋅ln \cdot l) 。

复杂度: O(L⋅n⋅l⋅dk)O(L \cdot n \cdot l \cdot d_k), 典型 n⋅l=16⋅32=512n \cdot l = 16 \cdot 32 = 512, 比 full attention 快 L/(nl)L/(nl) 倍 (L=128KL=128K 时快 256 倍) 。

关键设计: block 选择本身不需要额外算分, 免费复用 compress branch 已经算好的注意力。这就是 "native" 的含义 —— 稀疏机制天然 baked-in, 不是事后挂上去的。

训练时怎么传梯度过 top-n? top-nn 选择本身是离散的、不可微的。但 NSA 不需要让 selection 可微 —— 它通过 compress branch 的连续注意力分数 pbp_b 来训练 "该选哪些块"。也就是说, 梯度通过 compress 分支流回 WQ,WK,φW_Q, W_K, \varphi 等参数, 这些参数学出来 "压缩特征", 而 top-nn 只是依赖这些特征的一个确定性后处理。

Window branch: 本地兜底#

做法: 无论上面两支怎么选, 最近 ww 个 token (典型 w=512w=512) 永远被精确看到:

attnwin(t)=softmax(qt⋅Kt−w:t⊤/dk)⋅Vt−w:t\text{attn}_{\text{win}}(t) = \text{softmax}(q_t \cdot K_{t-w:t}^\top / \sqrt{d_k}) \cdot V_{t-w:t}

为什么需要这一支?

  • 语言里最近几十到几百个 token 几乎总是相关的 (代词指代、句法关联、刚提过的实体)
  • 但 compress branch 把它们和远处一样压成块了, 丢精度
  • Select branch 可能因为某个 query 觉得远处更重要而跳过本地
  • Window branch 强制兜底 —— 本地永远 dense 看, 绝不漏

复杂度: O(L⋅w⋅dk)O(L \cdot w \cdot d_k), 固定大小, 跟 LL 无关。

2.4 三路融合: 门控#

每条分支都给出一个 dkd_k 维向量。怎么合?

NSA 用一个学出来的门控 —— 对 qtq_t 过一个小 MLP, 输出 3 个 sigmoid 标量 (gcmp,gsel,gwin)(g_{\text{cmp}}, g_{\text{sel}}, g_{\text{win}}):

ot=gcmp⋅attncmp(t)+gsel⋅attnsel(t)+gwin⋅attnwin(t)o_t = g_{\text{cmp}} \cdot \text{attn}_{\text{cmp}}(t) + g_{\text{sel}} \cdot \text{attn}_{\text{sel}}(t) + g_{\text{win}} \cdot \text{attn}_{\text{win}}(t)

注意每个位置的门控都不一样 —— 每个 query 自己决定 "我现在更需要全局还是局部还是本地"。这是动态、依赖输入的 (input-dependent) 。

2.5 "三路并行" 严格说不完全独立#

注意 select 依赖 compress 的 softmax 分数, 所以不是数据流意义上的完全独立。真实计算图:

                ┌─→ compress(t)  ─┬─→ attn_cmp(t)  ──┐
                │                  │                   │
   q_t  ────────┤                  └─→ scores p_b      │
                │                       │              │
                │                       ↓              ├─→ 门控融合 → o_t
                │                  select top-n blocks │
                │                       ↓              │
                │                  attn_sel(t)  ───────┤
                │                                       │
                └─→ attn_win(t)  ──────────────────────┘

但这是结构意义上的并行 (类似 MHA 的多 head 并行):

  • 横向几个分支, 不是纵向几层 attention 串起来
  • compress → select 只是 1 跳的浅依赖, GPU launch overhead 可忽略
  • window 跟 compress 真正完全独立, 可以放不同 stream
  • Kcmp,VcmpK^{\text{cmp}}, V^{\text{cmp}} 每层只建一次 (不是每个 query 重算), 所有 LL 个 query 共享

所以 "并行" 的准确含义是: 三路是 "宽" 的而不是 "深" 的, 它们汇总成一个输出, 而不是串成一个深链。

2.6 复杂度对比#

设 L=128K,dk=128,l=32,n=16,w=512L = 128K, d_k = 128, l = 32, n = 16, w = 512:

分支 每个 query 的计算量 L=128KL=128K 时
Full attention O(L⋅dk)O(L \cdot d_k) 128K×128≈16M128K \times 128 \approx 16M
Compress O(L/l⋅dk)O(L/l \cdot d_k) 4K×128≈0.5M4K \times 128 \approx 0.5M
Select O(nl⋅dk)O(nl \cdot d_k) 512×128≈0.07M512 \times 128 \approx 0.07M
Window O(w⋅dk)O(w \cdot d_k) 512×128≈0.07M512 \times 128 \approx 0.07M
NSA 总和 ≈0.7M\approx 0.7M

对比 full attention 加速 ≈25×\approx 25\times, LL 越大优势越明显。

2.7 Hardware-aligned: 为什么这样设计是 "硬件友好" 的#

NSA 论文花了大量篇幅讲 GPU kernel 设计。核心问题是 arithmetic intensity (计算访存比):

  • Full attention 在 GPU 上其实是访存瓶颈, 而不是算力瓶颈 —— K,VK, V 太大, 从 HBM 读到 SRAM 比算 QK⊤QK^\top 还慢
  • 朴素的 sparse attention (随机选 token) 会让访存模式完全不规则, GPU 缓存命中率暴跌, 反而比 full attention 还慢

NSA 的三路设计全部都是 block-wise 的:

  • Compress branch: 按 block 压, 访存连续
  • Select branch: 选整块 block (不是单 token), 每块 l=32l=32 个 token 在内存里挨着, 可以一次性读入 SRAM
  • Window branch: 连续的最近 ww 个 token, 完美的连续访问

这让 sparse 不只是理论上 FLOPs 少了, 实测 wall-clock 时间也真的快了 (64K 序列上 forward / backward 都比 full attention 快几倍) 。

2.8 跟 full attention 的关系#

可以把 NSA 看作是 "近似 full attention" 的一种方式:

  • Full attention 的注意力矩阵 AA 经验上高度集中 → 大部分项接近 0
  • NSA 用三个互补的低秩 / 稀疏视角 (compress / select / window) 去近似 AA 真正非零的部分
  • 三路加权和 ≈\approx 真正的 AVAV, 但只算了一小部分

所以 NSA 的精度损失是轻微的 —— 论文实验显示在大模型上 NSA 训出来的精度甚至略好于 full attention (可能因为 sparse 起到了正则化作用)。

2.9 一句话总览#

NSA = 三路并行的可微稀疏 attention: compress 分支给全局粗略视野 (并顺便给出 block 重要性分数), select 分支用这个分数挑 top-nn 个 block 看原始 token (精准), window 分支强制看最近 ww 个 token (本地兜底)。三路通过学出来的门控融合。

关键创新: 稀疏选择天然集成在压缩分支里 (免费复用注意力分数), 不是事后挂的。block-wise 设计让稀疏在硬件上真的快, 不是只在 FLOPs 上快。

代价: 多三个分支带来一些 overhead; 需要写定制 kernel。

后继: V3.2 的 DSA 把这个简化成了 "lightning indexer + top-k MLA", 牺牲了 compress 分支的全局视野; V4 的 CSA + HCA 又把全局视野通过另一种方式补回来了。

3. DSA (DeepSeek Sparse Attention)#

V3.2 (2025-09) 引入, 把 NSA 思路工业化。核心变化: 砍掉三路并行, 只保留 "select" 这一路, 但选得更准更快, 并和 V3 的 MLA 整合在一起。

3.1 为什么要简化 NSA?#

NSA 在学术上很漂亮, 但工业部署有几个不爽:

  1. 三路 = 三套 kernel —— compress、select、window 都需要定制 GPU kernel, 工程维护成本高
  2. Compress 分支的 overhead 不小 —— 给每层每个 token 都建 Kcmp/VcmpK^{\text{cmp}}/V^{\text{cmp}}, 开销在长序列下不可忽略
  3. 跟现有 KV cache 优化的耦合 —— V3 已经用 MLA 把 KV cache 压到了 1/10, NSA 的 compress 跟 MLA 怎么共存是另一个工程问题

DSA 的取舍: 砍掉 compress 分支 (承认丢了一部分全局视野) , 保留最有效的 select 分支, 把它做到极致。

3.2 先快速过一下 MLA (DSA 的底座)#

DSA 是基于 MLA 改的, 所以得先知道 MLA 是什么。

MLA = Multi-head Latent Attention (V3 引入)#

MHA 的 KV cache 大小 =2⋅L⋅d⋅nlayers⋅batch= 2 \cdot L \cdot d \cdot n_{\text{layers}} \cdot \text{batch} 。现代 LLM 这一项非常大 —— 70B 模型在 128K 上, KV cache 可能就要 100GB。

MLA 的核心想法: KK 和 VV 的信息量并没有 dd 维那么大, 可以投到一个低维 latent 空间:

ct=xt⋅WDKV∈Rdcc_t = x_t \cdot W^{DKV} \in \mathbb{R}^{d_c}

其中 dc≪dd_c \ll d (典型 dc≈512d_c \approx 512) 。

推理时, cache 的不再是 KK 和 VV, 而是 cc —— 一个低维 latent。每次需要算 attention 时, 实时从 cc 升回 kk 和 vv:

kt=ct⋅WUK,vt=ct⋅WUVk_t = c_t \cdot W^{UK}, \quad v_t = c_t \cdot W^{UV}

节省: KV cache 从 2⋅L⋅d2 \cdot L \cdot d 变成 L⋅dcL \cdot d_c, 几乎 1/10。

代价: 需要额外的"上投影"矩阵 WUK,WUVW^{UK}, W^{UV}, 计算稍微多一点; 需要数学技巧把 attention 整合成"在 latent 空间里直接算"以保持高效。

MLA = KV 用低维 latent 缓存, 推理时升回来, KV cache 显存大幅下降

DSA 的稀疏机制就是叠加在 MLA 之上的 —— 不是替换 MLA, 是在 MLA 的基础上再加一层 sparse 筛选。

3.3 DSA 的两个组件#

   x_t  →  q_t                           ← 跟 MHA 一样
                ↘
                  Lightning Indexer  →  得分 s(q_t, j) for all j < t
                       ↓
                  挑 top-k (典型 k=2048)
                       ↓
   x_t  →  c_t (latent KV) ──→  MLA Attention  仅在选中的 k 个 KV 上跑
                                         ↓
                                       o_t

两个核心模块: Lightning Indexer 和 Top-K MLA。

3.4 Lightning Indexer: 轻量、FP8 的 "哪些 token 重要" 打分器#

它的角色#

Lightning Indexer 不参与最终 attention 计算, 它只做一件事:

给每个 (query tt, preceding token jj) 算一个索引分数 s(qt,kj)s(q_t, k_j)

这个分数告诉真正的 attention "你应该看 jj 还是不看 jj" 。打分完, top-k 选出来交给 MLA。

为什么叫 "lightning"#

整个 indexer 设计得极便宜, 以至于不构成瓶颈:

维度 full MLA attention lightning indexer
Head 数 多 (典型 128 heads) 少 (典型 4 heads)
维度 dhd_h (每 head 128 维) 极低维
数值精度 BF16 / FP16 FP8
输出 value 输出加权 VV (L×dL \times d) 不输出 VV, 只输出标量分数

省下了:

  • ∼32×\sim 32 \times heads
  • ∼2×\sim 2 \times 数值精度
  • 整个 VV 矩阵相关的计算和访存 (因为 indexer 不需要 VV)

实测 indexer 的 FLOPs 和访存大约是真正 MLA 的 1/100, 是真 · "lightning" 。

怎么算分数#

简化版的写法:

qt′=qt 投到 indexer 的低维空间 (用 indexer 自己的 WQ′)q'_t = q_t \text{ 投到 indexer 的低维空间 (用 indexer 自己的 } W'_Q \text{)}

kj′=kj 投到 indexer 的低维空间 (同样有自己的 WK′)k'_j = k_j \text{ 投到 indexer 的低维空间 (同样有自己的 } W'_K \text{)}

s(qt,kj)=qt′⋅kj′s(q_t, k_j) = q'_t \cdot k'_j

注意: 这个不再除 dk\sqrt{d_k}, 也不 softmax, 只是个排序用的相对分数。

这一步在 FP8 下做大矩阵乘 —— 非常快。

注意 indexer 用的 q′/k′q'/k' 完全是独立学出来的另一套投影, 不是真正 attention 的 q/kq/k 复用。这是它和 NSA 的关键差别 (NSA 复用 compress 分支的 attention 分数, DSA 用独立的小模型) 。

为什么这能学出 "重要性"#

直觉上, lightning indexer 是一个学出来的近似 attention —— 它的目标是模仿 full attention 会给哪些 jj 高分。只要它对 top-k 的预测够准 (不必每个分数都准, 只要排名靠前的几个对就行) , 就够用。

实测: indexer 选出的 top-2048 token, 跟 full attention 真正最关注的 top-2048 重合度很高, 能保留 >99% 的注意力质量。

3.5 Top-K + MLA Attention#

打分完, DSA 选 top-k (默认 k=2048k=2048, 在 128K context 中) , 然后让 MLA 只在这 k 个 token 上做完整 attention:

ot=MLA(qt,Kselected,Vselected)o_t = \text{MLA}(q_t, K_{\text{selected}}, V_{\text{selected}})

其中 Kselected,VselectedK_{\text{selected}}, V_{\text{selected}} 是从 latent cc 空间升回来的 kk 和 vv, 只对选中的 k 个位置升。

复杂度#

复杂度 L=128KL=128K 时
Full MLA attention O(L⋅dc)O(L \cdot d_c) 128K⋅512≈67M128K \cdot 512 \approx 67M
Lightning Indexer O(L⋅d′)O(L \cdot d') (d′≪dcd' \ll d_c) 128K⋅64≈8M128K \cdot 64 \approx 8M
Top-K MLA O(k⋅dc)O(k \cdot d_c) 2K⋅512≈1M2K \cdot 512 \approx 1M
DSA 总和 ≈9M\approx 9M

对比 full MLA 加速 ∼7×\sim 7\times; 在 1M context 时差距更大。

3.6 训练: Warm-up + Switch-over#

DSA 的训练有个工程细节, 值得专门讲: lightning indexer 不能从零开始训。

问题: 如果一上来就用 indexer 选 top-k, 然后只在 top-k 上算 attention ——

  • Indexer 一开始是随机初始化的, 选出来的全是垃圾
  • MLA 在垃圾 token 上算 attention, 输出全是垃圾
  • 模型 loss 巨大, 梯度信号被噪声污染
  • Indexer 永远学不出来 "哪些重要"

DeepSeek 的解法: 两阶段训练。

阶段 1: Warm-up (把 indexer 训到能用)#

  • 全模型其他参数冻结 (不动)
  • attention 仍然跑 full (dense) 的 MLA, 作为 "教师信号"
  • Lightning indexer 跟着跑, 但不影响实际输出
  • 训练目标: 让 indexer 的分数 s(qt,kj)s(q_t, k_j) 回归到真正 MLA attention 的分数

这一步本质是蒸馏: 用真正 attention 的输出作为标签, 把 indexer 训成它的轻量近似。

阶段 2: Switch-over (切到 sparse, 继续训)#

  • Indexer 训得差不多了 → 切换到 sparse 模式
  • attention 现在只在 indexer 选出的 top-k 上算
  • 整个模型 (包括 indexer 和原模型参数) 继续训练
  • Loss 收敛后部署

这种 warm-up 是 sparse attention 工业化的重要实践 —— 别指望 indexer 和模型同时从零冷启动, 先让 indexer 学个谱, 再让全模型适应 sparse 模式。

3.7 NSA vs DSA: 对比#

维度 NSA DSA
分支数 3 (compress + select + window) 1 (只有 select)
选择机制 复用 compress 的 softmax 分数 独立的 lightning indexer
选择粒度 block-level (整块 l=32l=32) token-level (单 token top-k)
全局视野 有 (compress 分支) 没有 (被砍了)
局部兜底 有 (window 分支强制) 没有硬保证 (期待 indexer 自己学着选近邻)
跟 KV cache 优化的耦合 独立设计 跟 MLA 深度整合
训练 端到端单阶段 warm-up + switch-over 两阶段
工程复杂度 三套 kernel 一套 kernel, FP8 优化
部署成熟度 学术原型 已部署到 V3.2 量产模型

关键 trade-off#

DSA 砍掉了 NSA 的 compress 和 window 分支, 在 128K 内问题不大 (因为 indexer 选 2048 个已经能覆盖大部分相关信息) , 但:

  • 失去全局视野: 推到 1M context 时, 远处的 token 即使是相关的, 如果不在 top-2048 里就完全看不到 —— 没有 compress 分支提供 "模糊但全局" 的信息
  • 失去本地兜底硬保证: 理论上 indexer 应该会把最近的 token 都选上, 但没有强制保证

这正是 V4 的 CSA + HCA 要解决的问题 —— 把丢掉的 "全局粗略" 和 "本地兜底" 用不同方式补回来。

3.8 直觉总结#

可以这样理解 NSA → DSA → V4 的演化:

  • NSA: 三个不同视角 hard-coded 在每一层 (compress + select + window)
  • DSA: 学一个小神经网络 (indexer) 来动态决定看哪些 token, 只保留 selection; 靠 indexer 的学习能力替代 NSA 的手工设计三分支
  • V4 (CSA + HCA): NSA 的"多视角"思想是对的, 但不该挤在同层; 专门化的层 + 交替堆叠, 让每层只做自己最擅长的事

3.9 一句话总览#

DSA = Lightning Indexer (轻量、FP8、不输出 V) + Top-K + MLA。

Indexer 学出来是 "用便宜的方式预测哪些 token 重要" 的小神经网络; 选出 top-k 之后, 真正的 MLA attention 只在 k 个位置算。

关键工程: warm-up 阶段先用 dense attention 蒸馏出 indexer, 再切到 sparse 模式继续训。

代价: 砍掉了 NSA 的 compress (全局粗略) 和 window (本地兜底) 分支, 在 1M 上下文时全局视野不够 —— 这就是 V4 要解决的问题。

一行精确版: DSA = NSA 砍掉 compress 输出 + 砍掉 window 兜底 + 用 lightning indexer 替代 "基于 compress 分数的 block 选择", 并把粒度从 block-level 改成 token-level。余下的就是 "indexer 选 → top-k 个 token → MLA dense attention"。

4. V4 (CSA + HCA)#

V4 的 Hybrid Attention 结合压缩、选择和滑窗。技术报告给出的口径是:V4-Pro 在 1M 上下文下单 token 推理 FLOPs 约为 V3.2 的 27%,KV cache 约为 10%;这是特定配置与序列长度下的比较,不是所有负载的固定比例。

核心思想: 别在同一层挤太多功能, 用整个网络的层堆叠本身来获得多视角。

4.1 出发点: DSA 推到 1M 时的问题#

DSA 在 128K 内工作得不错, 但推到 1M context 时:

  • top-2048 在 1M 里只有 0.2% —— 远处的相关 token 大概率被漏掉
  • 没有 compress 分支提供 "我知道远处大致讲了什么" 的兜底
  • 还是 token-level 选择, 1M 里排序成本本身也变贵

NSA 的回答 (三个互补视角) 在概念上是对的, 但 DSA 砍掉了一些。V4 的回答: 把多视角再加回来, 但用一种 GPU 友好的方式。

4.2 关键设计转向: 从 "层内并行" 到 "层间交替"#

NSA 把三路并行塞在同一层里: 每个 attention 模块同时跑 compress + select + window。

V4 反过来:

每一层只做一种 attention, 但不同层做不同种, 然后交替堆叠。

具体来说, V4 有两种 attention 层:

  • CSA 层: 轻度压缩 + sparse top-k (精准定位)
  • HCA 层: 重度压缩 + dense (全局粗略)

整个网络: CSA 层、HCA 层、CSA 层、HCA 层 …… 交替出现。

为什么这是好主意?

  • 每层 attention 模块结构简单一倍 (只一种, 不是三种并行) → kernel 简单、开销小
  • 模型仍然能在每两层之内得到 "精准 + 全局" 两种视角
  • 信息在残差流里累积 —— CSA 层精准定位的信息流到下一层 HCA, 被全局视角接着处理, 反过来也成立

4.3 CSA (Compressed Sparse Attention): 精准的稀疏查找#

CSA 在概念上最像 NSA, 但参数选择不同。

三段式#

原始 KV (长度 L)
   ↓ 学出来的压缩函数 φ, 每 m=4 个 token 压成 1 个
压缩后 KV (长度 L/4)
   ↓ 类 DSA 的 lightning indexer 打分
   ↓ top-k 选择 (V4-Flash: k=512; V4-Pro: k=1024)
选中的 k 个压缩 entry  →  query 对它们做 dense attention

外加:
最近 n_win 个 token 的 sliding window  → 永远 dense 看

跟 NSA 比#

NSA CSA
压缩率 m=32m=32 m=4m=4 (轻得多)
top-k 数 16 个 block (block size 32 = 512 token) 512 或 1024 个压缩 entry
选择粒度 block-level 压缩 entry-level (等价于每 4 个 token 一组)
是否有滑窗 有 (单独分支) 有 (单独分支)
compress 分支输出 加到最终输出 不加 (只用来给 indexer 提供素材)

关键差别:压缩率为 4 时,top-512 个压缩 entry 对应约 2048 个原始 token 的分组范围,但不保留这 2048 个 token 的独立 KV。它与 DSA 选取原始 token 的取舍不同,不能据此断言更精细或精度更高。

一句话直觉#

CSA = 轻度压缩 (几乎不丢信息) + 精准 top-k (只看相关的) + 滑窗 (本地兜底) —— 在每一层提供精确的 sparse 检索能力。

4.4 HCA (Heavily Compressed Attention): 廉价的全局俯视#

HCA 的设计理念跟 CSA 正好相反。

两段式#

原始 KV (长度 L)
   ↓ 学出来的压缩函数 φ', 每 m'=128 个 token 压成 1 个
压缩后 KV (长度 L/128)
   ↓ 直接对所有压缩 entry 做 dense attention (不 top-k, 不 sparse)

外加:
最近 n_win 个 token 的 sliding window  → 永远 dense 看

注意两个关键点:

  1. m′=128m' = 128 极激进: 1M token 被压成 ∼8K\sim 8K 压缩 entry
  2. dense attention, 不 sparse: 既然只剩 8K, 直接 dense 也很便宜

为什么这么设计#

回想 NSA 的 compress 分支: 它用 m=32m=32 的压缩率给 query 一个 "全局粗略视野"。HCA 把这个想法推到极致:

  • m=128m=128 的极端压缩 → 序列足够短, 可以 dense attention
  • Dense 比 sparse 简单 (不需要 indexer、top-k) , kernel 实现极简
  • 虽然每个压缩 entry 信息保真度低, 但 query 能看到整个 1M context 的全局摘要

代价: 精度上, 远处某个具体 token 的信息已经被压糊了。但这层不是用来精确定位的, 精确定位交给前一层或后一层的 CSA 去做。

一句话直觉#

HCA = 重度压缩 (信息糊但短) + dense (看全部压缩 entry) + 滑窗 —— 在每一层提供廉价的全局俯视。

4.5 滑窗: CSA 和 HCA 都有#

⚠️ 注意: V4 把 NSA 的 window 分支显式恢复了, 而且两种层都有。

每个 attention 层 (无论 CSA 还是 HCA) 都附带一个最近 nwinn_{\text{win}} tokens 的强制 sliding window 分支。这保证:

  • 本地 token 永远 dense 看到, 绝不漏
  • 不依赖 indexer 是否选近邻
  • 句法、代词指代、最近实体引用都有兜底

4.6 整体架构#

V4 有两个版本:

V4-Flash V4-Pro
总参数 284B 1.6T
激活参数 13B 49B
第 1-2 层 纯 sliding window HCA
之后所有层 CSA / HCA 交替 CSA / HCA 交替
CSA 的 top-k 512 1024

为什么前两层特殊? —— 因为最初几层的特征还没成形, sparse 选择会错得多。Flash 用纯滑窗 (只看近邻) , Pro 用 HCA (全局粗略) 做铺垫, 从第 3 层开始稳定的 CSA/HCA 交替。

4.7 为什么 "层间交替" 比 "层内并行" 好#

NSA 在同一层并行三路。V4 把不同视角分到不同层。后者的好处:

(a) 每层 kernel 极简#

NSA 一个 attention 模块要处理 compress + select + window 三种逻辑, kernel 复杂、bug 多、优化空间小。V4 一个层只一种 attention, kernel 跟普通 attention 几乎一样 (就是 KV 是压缩过的) 。这极大降低了工程复杂度和 kernel 开销。

(b) 残差流自然组合多视角#

回想残差: 每层的输出是 xi+1=xi+Fi(LN(xi))x_{i+1} = x_i + F_i(\text{LN}(x_i)) 。残差流积累所有层的贡献。

所以 CSA 层算出来的 "精准信息" 和 HCA 层算出来的 "全局粗略信息", 会被自动累加到 xx 里, 后续每层都能看到这两种视角的混合体 —— 不需要在同一层显式 concat 或门控。

(c) 计算分配更灵活#

NSA 三路在同一层意味着每层都要付三路成本。V4 一半的层只付 sparse 成本, 一半只付 dense-on-compressed 成本 —— 总成本是两者各一半的平均, 而不是两者相加。

4.8 为什么这能做到 1M context#

把数字过一遍。设 L=1M,nlayers=60,m=4L = 1M, n_{\text{layers}} = 60, m=4 (CSA) , m′=128m'=128 (HCA) , k=1024k=1024:

Per-query 计算量 备注
一层 CSA ∼k=1024\sim k = 1024 个 KV 上 attention 不依赖 LL
一层 HCA ∼L/m′=8K\sim L/m' = 8K 个 KV 上 dense attention 跟 LL 线性
一层 sliding window nwinn_{\text{win}} 个 KV 上 dense 不依赖 LL

CSA 层的成本几乎跟 LL 无关; HCA 层是 O(L/128)O(L/128) 。即使 L=1ML=1M , HCA 层也只是在 8K 长的压缩序列上做 dense —— 可控。

KV cache: cache 的是压缩之后的 entry, 而不是原始 KV。

  • CSA cache: L/4L/4 个 entry
  • HCA cache: L/128L/128 个 entry

总和远小于 V3.2 的 MLA cache。论文报告 V4 的 KV cache 是 V3.2 的 10%。

4.9 一段总结演化#

Full attention            一种视角: dense, O(L²)
        ↓
NSA                       三种视角并行, 在同一层
        ↓ 工业化简化
DSA (V3.2)                一种视角: sparse top-k, 丢了全局
        ↓ 把多视角加回来, 但分散到层间
V4 (CSA + HCA)            两种视角分别专门化为不同层, 交替堆叠
                          外加每层都有滑窗兜底

4.10 一句话总览#

V4 = CSA 层 (轻压缩 + sparse top-k + 滑窗, 精准) 和 HCA 层 (重压缩 + dense + 滑窗, 全局) 交替堆叠。

每层 kernel 极简, 残差流自动组合两种视角, KV cache 全用压缩后的 entry 来 cache。

核心思想: 不在同一层做三件事, 而是让网络的层堆叠本身扮演 "多视角组合" 的角色。层间交替 > 层内并行, 因为残差流是免费的胶水。

原始资料:NSA、DeepSeek-V4 技术报告。

延伸阅读#

相关阅读路线

理解 Transformer 与架构变体 ↗