- Published on
DSA(DeepSeek Sparse Attention)
- Authors

- Name
- Vegetog
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
↓
加权求和
问题不仅是算 ,还包括:
- 从显存读取大量 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 和某个历史 Token ,索引器计算:
先不用害怕这个公式,它表达的事情很简单:
当前 Token → 产生若干个小 Query
历史 Token → 产生一个小 Key
小 Query 和小 Key 做点积
↓
经过 ReLU,去掉负相关结果
↓
不同索引头的结果加权求和
↓
得到一个总分 I(t,s)
各个符号的含义:
- :当前 Token 在第 个索引头上的查询向量;
- :历史 Token 的索引 Key;
- :当前 Token 对不同索引头的权重;
- :当前 Token 认为历史 Token 有多重要。
注意,这里算的是一个廉价的“预估分数”,还不是正式注意力分数。
在公开参考配置中:
- 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 的索引分数后:
其中 是被选中的位置集合。
接下来,正式注意力只读取这些位置:
这里将 作为 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 的 只需要计算一次,然后保存在 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 使用:
而不是完整的 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 是不是把复杂度彻底变成 ?
严格来说,不能直接这么说。下面先讨论长度为 的整段 prefill,固定模型维度和头数, 表示每个 query 的选择上限。这些是核心打分与聚合的工作量,不是完整模型的耗时公式。
正式注意力部分
完整注意力:
DSA 的正式 MLA 部分只计算 Top-K:
如果 固定,那么正式注意力部分相对于 接近线性。
但 Indexer 仍然需要扫描历史
对每个 Query,Indexer 需要扫描其可见前缀;整段 prefill 累计的打分工作量为:
只不过这个平方计算使用的是:
- 更小的索引表示;
- 更少、更轻量的操作;
- FP8;
- 专用 Kernel;
- 不需要读取并聚合完整 Value。
所以更准确的表述是:
DSA 不是让所有计算都在数学上变成线性,而是:
把最昂贵的正式注意力变成稀疏计算,只留下一个非常轻量的全局扫描器。
这也意味着,当上下文继续增长到极端长度时,Indexer 本身可能成为新的瓶颈。
单步 decode 则不同:只有一个新 query,Indexer 打分约为 ,正式注意力核心约为 。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 的显存带宽;
- 正式注意力的临时计算量。
但历史缓存容量依然随上下文长度增长,通常还是 。
这与滑动窗口不同:
- 对始终只使用局部窗口、且没有其他历史访问路径的层,窗口外 KV 可以淘汰;
- DSA 未来可能重新选择任意旧 Token,所以不能简单删除。
8. Indexer 怎么知道哪些 Token 重要?
Indexer 一开始当然不知道,因此 DeepSeek 先让它模仿完整注意力。
第一阶段:Dense Warm-up
训练初期仍然运行完整 MLA,然后把正式注意力的分布当作老师。
对于 Query :
- 取得正式 MLA 各个注意力头的注意力分布;
- 把各头的重要性聚合起来;
- 归一化成目标分布 ;
- 训练 Indexer 产生接近的分布。
使用 KL 散度:
直观上就是:
正式注意力认为哪些 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 张量。
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 的定义。