Published on

DSA(DeepSeek Sparse Attention)

Authors
  • avatar
    Name
    Vegetog
    Twitter

DSA 先用轻量索引器为当前可见的 Token 打分,再让正式注意力集中计算选中的 Top-K 位置。

本文先说明 Indexer → Top-K → Sparse MLA 的数据流,再讨论复杂度、缓存、训练与实现边界。“轻量”是相对正式注意力而言,实际加速仍取决于上下文长度、硬件和 Kernel。

本文以 DeepSeek-V3.2 / V3.2-Exp 中的 DSA(DeepSeek Sparse Attention) 为准。

1. DSA 要解决什么问题?

假设当前一步需要访问约 100,000 个已知 Token。下文将当前可见前缀统称为“历史”;标准自回归注意力也允许读取当前输入位置自己的 K/V。

普通完整注意力需要:

当前 Query
分别和 100,000 个历史 Key 计算相关性
读取 100,000 份 Value
加权求和

问题不仅是算 QKTQK^T,还包括:

  • 从显存读取大量 KV Cache;
  • 对所有注意力头计算分数;
  • 对所有 Value 做加权求和;
  • 多卡场景中的数据传输。

而真正对当前 Token 有用的历史内容,可能只有少量位置。

例如当前问题是:

王教授把保险箱密码告诉了谁?

那么真正有用的可能只是前文中提到“王教授”“保险箱”“密码”的几十个 Token,而不是全部 10 万个。

DSA 的策略是:

10万个历史 Token
Lightning Indexer 快速打分
选出最重要的2048个 Token
正式 MLA 只在这2048个 Token 上计算

它属于:

内容驱动的、动态的、Token 级稀疏注意力。


2. DSA 的两个核心组件

DSA 可以分成两个阶段:

阶段一:Lightning Indexer
便宜地判断“哪些历史 Token 可能重要”

阶段二:Sparse MLA
只对选中的 Token 做正式注意力

第一阶段:Lightning Indexer

对于当前 Query Token tt 和某个历史 Token ss,索引器计算:

It,s=j=1HIwt,jIReLU(qt,jIksI)I_{t,s}=\sum_{j=1}^{H^I}w^I_{t,j}\cdot\operatorname{ReLU}\left(q^I_{t,j}\cdot k^I_s\right)

先不用害怕这个公式,它表达的事情很简单:

当前 Token → 产生若干个小 Query
历史 Token → 产生一个小 Key

小 Query 和小 Key 做点积
经过 ReLU,去掉负相关结果
不同索引头的结果加权求和
得到一个总分 I(t,s)

各个符号的含义:

  • qt,jIq^I_{t,j}:当前 Token 在第 jj 个索引头上的查询向量;
  • ksIk^I_s:历史 Token 的索引 Key;
  • wt,jIw^I_{t,j}:当前 Token 对不同索引头的权重;
  • It,sI_{t,s}:当前 Token 认为历史 Token ss 有多重要。

注意,这里算的是一个廉价的“预估分数”,还不是正式注意力分数。

在公开参考配置中:

  • Indexer Head 数量:64;
  • Indexer Head Dimension:128;
  • Top-K:2048。

以单步 decode 为例,选择位置的逻辑可简化为:

topk_indices = index_score.topk(
    min(index_topk, visible_length), dim=-1
).indices

也就是说,当前可见位置不超过 2048 时,有效候选全部保留。Prefill 必须按每个 query 的因果前缀处理;参考实现的 Top-K 输出还可能包含被掩码的位置,它们不能参与正式注意力。DeepSeek 官方参考实现


第二阶段:正式 MLA 只计算 Top-K

得到所有历史 Token 的索引分数后:

St=TopKIndicesst(It,s)S_t=\operatorname{TopKIndices}_{s\le t}(I_{t,s})

其中 StS_t 是被选中的位置集合。

接下来,正式注意力只读取这些位置:

ut=Attention(qt,{cssSt})u_t=\operatorname{Attention}\left(q_t,\left\{c_s\mid s\in S_t\right\}\right)

这里将 csc_s 作为 MLA 缓存条目的简写;实际还包括解耦 RoPE 的位置 Key。上式是概念表达,省略了各头投影、位置分支和输出投影。

假设共有 100,000 个历史 Token:

Indexer:
对100,000个历史 Token 做廉价打分

Top-K:
选出2048个位置

正式 MLA:
只读取并计算这2048个位置

例如选出来的位置可能是:

[17, 82, 1064, 7811, 23004, ..., 99871]

它们不要求连续,也不要求位于当前 Token 附近。

这就是 DSA 与滑动窗口注意力最重要的区别:

滑动窗口:
根据距离选择,只看最近的 Token

DSA:
根据内容选择,可以跳回非常久以前

3. 为什么要先做 Indexer?直接 Top-K 不行吗?

假设直接使用正式 MLA 分数来选 Top-K:

先计算当前 Query 与所有历史 Key 的完整注意力
再取 Top-K

这没有解决问题,因为最昂贵的完整注意力分数已经算完了。

DSA 相当于建立了一个两级系统:

廉价初筛                           昂贵精算
Lightning Indexer → Top-K位置 → 正式MLA Attention

类似招聘:

  • Indexer:HR 用简历快速筛出 2048 人;
  • 正式注意力:技术专家只认真面试这 2048 人;
  • 如果让技术专家先面试全部候选人,再选 Top-K,就没有节省成本。

因此,DSA 的关键并不只是 Top-K,而是:

用一个明显比正式注意力便宜的模块完成 Top-K 候选选择。


4. Lightning Indexer 为什么比较便宜?

索引表示更小

Indexer 只负责判断“重要不重要”,不负责生成最终上下文向量,因此可以使用更轻量的表示。

它不像正式多头注意力那样,需要完整完成:

QK 打分
Softmax
读取 V
多头加权求和
输出投影

Indexer 主要做:

小型 QK 打分
Top-K 选择

Key 可以提前缓存

历史 Token 的 ksIk^I_s 只需要计算一次,然后保存在 Indexer Key Cache 中。

Decode 时:

新 Token:
生成新的 Indexer Query

历史 Token:
直接读取已经缓存的 Indexer Key

使用 FP8

参考实现将索引 Query 和 Key 量化为 FP8,缓存 Key 与量化尺度,并调用 fp8_index

FP8 可以减少索引数据的存储和搬运,并利用支持 FP8 的计算单元。但正式 MLA 也可能采用 FP8 KV Cache,不能把“Indexer 用 FP8、MLA 必定用 BF16”当作固定前提;端到端速度还需要测量。

使用 ReLU

Indexer 使用:

ReLU(qk)\operatorname{ReLU}(q\cdot k)

而不是完整的 Softmax 注意力流程。

ReLU:

  • 计算简单;
  • 负分直接归零;
  • 省去了索引阶段的 Value 聚合;
  • 最终只需要提供排序依据。

官方还开放了:

  • DeepGEMM 中的 Indexer Logit Kernel;
  • FlashMLA 中的 Token 级稀疏注意力 Kernel。

也就是说,DSA 不只是论文中的稀疏掩码,还配套了真正跳过未选 Token 的 GPU 实现。DeepSeek-V3.2-Exp 官方仓库 FlashMLA 官方仓库


5. DSA 和 MLA 是什么关系?

两者解决的不是同一个问题。

MLA:每个 Token 存得更小

普通 MHA 的 KV Cache 大致需要按多个 KV Head 分别保存。

MLA 将每个 Token 的 KV 信息压缩成低维潜在表示:

普通注意力:
每个历史 Token 保存很多份 K、V

MLA:
每个历史 Token 主要保存一份压缩表示

它解决的是:

每个历史 Token 的 KV Cache 有多大?

DSA:每一步读取更少的 Token

即使每个 Token 已经压缩了,100K 上下文仍然需要读取 100K 份压缩 KV。

DSA 进一步解决:

当前 Query 到底需要读取多少个历史 Token?

所以二者可以组合:

MLA:
把每个 Token 的 KV 压小

DSA:
只从全部历史 Token 中读取 Top-2048

简单说:

MLA 是“每本书变薄”,DSA 是“只从书架上拿相关的书”。


6. DSA 是不是把复杂度彻底变成 O(nk)O(nk)

严格来说,不能直接这么说。下面先讨论长度为 nn 的整段 prefill,固定模型维度和头数,kk 表示每个 query 的选择上限。这些是核心打分与聚合的工作量,不是完整模型的耗时公式。

正式注意力部分

完整注意力:

O(n2)O(n^2)

DSA 的正式 MLA 部分只计算 Top-K:

O(nk)O(nk)

如果 k=2048k=2048 固定,那么正式注意力部分相对于 nn 接近线性。

但 Indexer 仍然需要扫描历史

对每个 Query,Indexer 需要扫描其可见前缀;整段 prefill 累计的打分工作量为:

O(n2)O(n^2)

只不过这个平方计算使用的是:

  • 更小的索引表示;
  • 更少、更轻量的操作;
  • FP8;
  • 专用 Kernel;
  • 不需要读取并聚合完整 Value。

所以更准确的表述是:

DSA 成本=O(n2) 的廉价索引Lightning Indexer+O(nk) 的昂贵正式注意力Sparse MLA\text{DSA 成本}=\underbrace{O(n^2)\text{ 的廉价索引}}_{\text{Lightning Indexer}}+\underbrace{O(nk)\text{ 的昂贵正式注意力}}_{\text{Sparse MLA}}

DSA 不是让所有计算都在数学上变成线性,而是:

把最昂贵的正式注意力变成稀疏计算,只留下一个非常轻量的全局扫描器。

这也意味着,当上下文继续增长到极端长度时,Indexer 本身可能成为新的瓶颈。

单步 decode 则不同:只有一个新 query,Indexer 打分约为 O(n)O(n),正式注意力核心约为 O(min(n,k))O(\min(n,k))。Top-K 的实现开销、投影、索引访存、通信及缓存读取还需单独计入。


7. DSA 会把 KV Cache 降成固定的 2048 个 Token 吗?

不会。

这是非常容易误解的地方。

当前 Query 只选择 2048 个 Token,但下一次 Query 可能选择另一组历史位置:

Query A 选择:1、30、500、9000……
Query B 选择:8、42、700、50000……

因此,不能在本轮没被选中时,就把其他历史 KV 全部删除。

DSA 仍需要让所有历史位置保持“可检索”:

  • 历史 MLA 压缩 KV 仍需保存;
  • 历史 Indexer Key 也需要保存;
  • 当前 Query 只读取其中 Top-K 对应的正式 KV。

所以 DSA 主要减少的是:

  • 正式注意力 FLOPs;
  • 每步读取正式 KV 的显存带宽;
  • 正式注意力的临时计算量。

但历史缓存容量依然随上下文长度增长,通常还是 O(n)O(n)

这与滑动窗口不同:

  • 对始终只使用局部窗口、且没有其他历史访问路径的层,窗口外 KV 可以淘汰;
  • DSA 未来可能重新选择任意旧 Token,所以不能简单删除。

8. Indexer 怎么知道哪些 Token 重要?

Indexer 一开始当然不知道,因此 DeepSeek 先让它模仿完整注意力。

第一阶段:Dense Warm-up

训练初期仍然运行完整 MLA,然后把正式注意力的分布当作老师。

对于 Query tt

  1. 取得正式 MLA 各个注意力头的注意力分布;
  2. 把各头的重要性聚合起来;
  3. 归一化成目标分布 pt,:p_{t,:}
  4. 训练 Indexer 产生接近的分布。

使用 KL 散度:

LI=tDKL(pt,:Softmax(It,:))\mathcal{L}^I=\sum_t D_{\mathrm{KL}}\left(p_{t,:}\parallel\operatorname{Softmax}(I_{t,:})\right)

直观上就是:

正式注意力认为哪些 Token 重要,Indexer 就学习怎样用更低成本把它们找出来。

论文中的 Dense Warm-up:

  • 暂时冻结主模型;
  • 只训练 Indexer;
  • 使用 128K 长序列;
  • 训练 1000 步,共约 2.1B Token。

第二阶段:Sparse Training

等 Indexer 学会基本排序后:

开启 Top-K
正式 MLA 只看选中的 Token
主模型继续语言模型训练
Indexer 继续学习预测注意力重要性

稀疏训练阶段的对齐只在选中集合上进行,并重新归一化。Indexer 由 KL 损失训练,主模型由语言模型损失训练;索引器输入做梯度分离。硬 Top-K 的离散位置选择不能直接提供普通梯度,因此需要明确的索引监督。DeepSeek-V3.2 技术报告


9. 一个完整的 Decode 流程

下面描述一个注意力层处理当前输入 token 的步骤;采样下一个 token 要等所有层和输出头完成。

1. 对当前输入隐状态做 RMSNorm
2. 生成 MLA / Indexer 的 Query,以及当前位置的缓存条目
3. 将当前 MLA KV、位置 Key、Indexer Key 与尺度写入缓存
4. Indexer 扫描当前可见前缀,产生索引分数
5. 选出最多 2048 个有效位置
6. Sparse MLA 按位置读取对应缓存,计算打分、Softmax 与聚合
7. 输出投影、残差连接,再进入 MoE/FFN

单步 decode 只暴露当前及过去位置时,不必另建一个未来位置掩码;prefill 的多个 query 则需要因果约束。

下面是解释选择与计算关系的伪代码,不是官方 API,也不是可直接运行的 MLA 实现

# 当前位置的缓存已追加;visible_length 包含当前输入位置
index_score = indexer_score(query_index, index_key_cache)
ids = index_score.topk(min(k, visible_length), dim=-1).indices
output = sparse_mla(query_mla, mla_cache, position_key_cache, ids)

必须区分两种实现:

  • 官方 Python 参考实现先计算完整 MLA scores,再用 Top-K 掩码和 Softmax 表达稀疏语义;它适合阅读与验证,不能据此证明省掉了稠密计算。
  • FlashMLA 等高性能稀疏 Kernel 接收选择位置,按索引访问缓存并跳过未选位置。Gather 可以在 Kernel 内完成,并不一定单独生成一个连续的 selected-KV 张量。

Python 参考实现 · FlashMLA


10. DSA 与其他稀疏注意力的对比

方法如何选择 Token能否找很久以前的信息主要特点
Sliding Window按距离较弱简单,旧 KV 可淘汰
Global Token人工指定枢纽可以依赖全局 Token 汇总
Random Attention随机连接间接可以规则简单,但不理解内容
Block Sparse按 Token 块选择取决于规则GPU 友好,粒度较粗
DSA学习内容相关性并取 Top-K可以直接选择Token 级、动态、需要 Indexer

DSA 最大的特点是:

每一层、每一个 Query Token 都可以根据当前内容,选择不同的历史 Token。

它不是固定稀疏图,而是实时生成稀疏图。


11. DSA 的局限

Indexer 可能选错

如果某位置没进入 Top-K,本层当前 query 就不能直接读取它;相关信息仍可能已由前层或其他被选位置间接携带。

这是一种早期淘汰:

Indexer漏选
本层无法直接访问漏选位置

Indexer 仍然扫描全部历史

它降低的是昂贵正式注意力的成本,但超长上下文下,Indexer 的全历史打分和 Top-K 也会越来越贵。

随机 Gather 对 GPU 不友好

2048 个位置可能散布在整个 KV Cache 中:

位置 7、891、10234、55001、99980……

这种读取不如连续滑动窗口规整,实际工程常结合:

  • Paged KV Cache;
  • 高效 Top-K Kernel;
  • 稀疏 Gather Kernel;
  • FlashMLA 等专门优化。

短上下文下未必划算

历史长度不超过 2048 时,DSA 基本还是全选,却额外运行了一次 Indexer。

所以它的主要价值出现在长上下文场景。


12. 如何记住 DSA

可以把 DSA 想成搜索引擎:

Lightning Indexer:
快速搜索全部历史,返回2048条结果

Sparse MLA:
认真阅读这2048条结果,完成推理

但它和 RAG 不一样:

  • RAG 在模型外部检索文档;
  • DSA 在每个 Transformer 注意力层内部检索历史 Token;
  • RAG 返回文本片段;
  • DSA 返回 KV Cache 中的 Token 位置。

最准确的记忆方式是:

DSA 的核心是 Lightning Indexer 初筛 + Top-K 稀疏注意力;在 DeepSeek-V3.2 中,它与 MLA 结合使用。2048 是该配置的选择上限,不是 DSA 的定义。

参考资料