DeepSeekV3.2之DSA
DeepSeek-V3.2 的 DSA(DeepSeek Sparse Attention)详解
面向小白:从「为什么需要 DSA」讲起,结合论文数学公式和 sglang 源码, 最后把 MLA + DSA 串起来,看清 DeepSeek-V3.2 完整的注意力是怎么跑的。
建议先读完同目录的MLA,本文大量复用 MLA 的结论。

第 0 部分:一句话先记住
- MLA 解决的是 「KV 太占显存」:把每个 token 的 KV 压成一个 512 维的小向量
c_kv。 - DSA 解决的是 「序列太长,注意力算不动」:每个 query token 不再看全部历史 token, 而是先用一个轻量打分器(lightning indexer)挑出最相关的 top-k 个 token,只对这 k 个做注意力。
MLA 省的是显存(KV cache 大小),DSA 省的是计算量(attention 的 FLOPs)。 两者正交,DeepSeek-V3.2 把它们叠在一起用。
第 1 部分:为什么需要 DSA —— 长序列的「平方墙」
标准注意力的代价是 序列长度的平方。query 第 个 token 要和前面所有 个 token 算分:
- 时还能忍。
- 时, 比 大了约 1000 倍。decode 阶段每生成一个新 token, 都要把它和前面 12.8 万个 token全部算一遍注意力分 —— 这就是长上下文又慢又贵的根源。
核心观察:对某个 query 来说,前面 12.8 万个 token 里,真正重要的其实只有几千个, 绝大多数 token 的注意力权重接近 0。那何必全算?
DSA 的思路:先花极小的代价(一个很轻的打分器)估计「哪些 token 重要」, 只挑 top-k 个(V3.2 里 通常是 2048)做真正的注意力。 于是计算量从 降到 , 是常数 → 近似线性。
第 2 部分:DSA 的两个部件
DSA = Lightning Indexer(闪电索引器) + Top-k 稀疏注意力。
1 | ┌─────────────────────────────────────┐ |
关键点:索引器很便宜,主注意力很贵。所以让便宜的索引器去筛选, 让昂贵的主注意力只处理被筛中的少数 token。
第 3 部分:Lightning Indexer 的数学公式
3.1 论文公式
对 query token 和历史 token ,索引分数定义为:
逐项解释(这是理解 DSA 的核心,慢慢看):
| 符号 | 含义 | 在 sglang 里的来源 |
|---|---|---|
| 索引器的「头数」,很小(比主注意力头数少很多) | index_n_heads |
|
| query token |
wq_b(q_lora) |
|
| 历史 token |
wk(hidden) |
|
| 点积,衡量 |
FP8 矩阵乘 | |
| 把负相关「砍成 0」,只保留正向关联 | kernel 内 relu |
|
| query token |
weights_proj(hidden) |
|
| 把所有索引头的贡献加起来 | kernel 内 logits_sum |
直觉:索引器是一个简化版多头注意力打分。它对每个 (t, s) 对, 在
个小头里各算一个 ReLU 后的相似度,再用 加权求和,得到一个标量分数 。 分数越高 = token 对 query 越重要。
3.2 为什么这么设计「便宜」?
对比一下主注意力(MLA)和索引器的成本:
| 主注意力(MLA) | Lightning Indexer | |
|---|---|---|
| 头数 | 128(num_attention_heads) |
|
| key 头数 | 共享 1 份 512 维 latent | 共享 1 份 |
| 精度 | bf16 | FP8(更快、更省) |
| 每个 token 算什么 | 完整 softmax 注意力 + 加权 V | 只算一个标量分数 |
所以索引器对全部
3.3 Top-k 选择
拿到分数向量
然后主注意力只在
如果历史长度
第 4 部分:对应 sglang 源码
源码主目录:python/sglang/srt/layers/attention/dsa/dsa_indexer.py,核心类是 Indexer。
4.1 索引器的三个投影层(__init__,对应公式里的 )
1 | # dsa_indexer.py __init__ (约 346–367 行) |
逐行对应公式 (DSA-1):
| 公式符号 | 代码 |
|---|---|
wq_b(q_lora) 然后 reshape 成 [L, H_I, d_I] |
|
wk(hidden) → k_norm → 单头 |
|
weights_proj(hidden) → [L, H_I] |
注意
wq_b的输入是q_lora_rank,直接复用了 MLA 把 Q 压成的低秩 latent—— 这是 MLA 和 DSA 的第一个交汇点:索引器不另算 Q,而是蹭 MLA 已经算好的q_lora。
4.2 算 q / k 并加 RoPE(_get_q_k_bf16,约 439–529 行)
1 | query, _ = self.wq_b(q_lora) # q^I |
索引器有自己独立的一套 RoPE,和主注意力的 RoPE 分开。
4.3 算分数 + ReLU + 加权求和(核心公式 DSA-1)
这一步落在 FP8 kernel 里。看 tilelang_kernel.py 的 fp8_index 注释(约 244–247 行),
它字面就是公式 (DSA-1):
1 | fp8 q @ fp8 k -> fp32 logits # q^I_{t,h} · k^I_s(每个头一个点积) |
而 weights(即 forward_cuda(约 1393–1498 行)里算的:
1 | weights = self._get_logits_head_gate(x_for_gate, q_scale) |
也就是 weights_proj,再乘上
4.4 Top-k 选择(公式 DSA-2)
最干净的参考实现是 tilelang 路径 forward_indexer(约 1207–1214 行):
1 | index_score = fp8_index(q_fp8_partial, weights_partial, k_fp8, k_scale) # 公式 DSA-1 |
topk(...)[1] 取的是下标(indices),不是分数本身——这正是我们要的「挑哪些 token」。
高性能路径(_get_topk_ragged / _get_topk_paged)用 deep_gemm.fp8_mqa_logits
metadata.topk_transform做同样的事,只是融合得更狠、支持分块防 OOM。
4.5 一个重要优化:短序列直接跳过打分
forward_cuda(约 1362–1383 行):
1 | if max_kv_len <= self.index_topk: |
对应公式 (DSA-2) 里
第 5 部分:把 MLA + DSA 串起来(DeepSeek-V3.2 完整流程)
DeepSeek-V3.2 的每个注意力层,是 MLA 当「主注意力」+ DSA 当「token 筛选器」。
在 sglang 里,索引器被 DeepseekV2AttentionMLA(models/deepseek_v2.py)持有,
在 forward_absorb_prepare(deepseek_common/attention_forward_methods/forward_mla.py)里调用。
5.1 一次 decode 的完整数据流
1 | 输入 hidden_states x (一个新 token) |
5.2 三个关键交汇点
-
共享 Q latent:索引器的
wq_b输入是 MLA 算出来的q_lora,不重复算 Q 下投影。 (源码:forward_mla.py里把q_lora同时喂给self.indexer(...)。) -
DSA 决定「看谁」,MLA 决定「怎么看」:
- DSA 输出
topk_indices(一个长度 k 的下标列表)。 - 这个
topk_indices被传进 MLA 的注意力 kernel(attn_mqa), 让它只读取 KV cache 里这 k 个槽位的c_kv,跳过其余历史。 - 于是 MLA 那个
num_kv_heads=1的 MQA,从「读全部 L 个 c_kv」变成「读 k 个 c_kv」。
- DSA 输出
-
两套独立的 cache:
- MLA 存的是主注意力用的 512 维
c_kv(kv_a_proj_with_mqa产出)。 - DSA 索引器额外存一份很小的 index-K cache(
wk产出,FP8),只给打分用。 - 两者都按 token 存,靠同一套
topk_indices对齐。
- MLA 存的是主注意力用的 512 维
5.3 复杂度对比(直观感受收益)
设上下文长度
| 阶段 | 不用 DSA(只 MLA) | 用 DSA(MLA+DSA) |
|---|---|---|
| 索引器打分 | — | |
| 主注意力 | ||
| 当 |
看 131072 个 token | 看 2048 个 token,约省 64× |
MLA 让每个 token 的 KV 只占 512 维(省显存); DSA 让每步只对 2048 个 token 算注意力(省计算)。 两者叠加 → DeepSeek-V3.2 在 128K 长上下文下又省显存又快。
第 6 部分:三句话总结
- MLA 把每个 token 的 KV 压成一个 512 维 latent
c_kv, 并通过权重吸收退化成num_kv_heads=1的 MQA —— 解决显存瓶颈。 - DSA 用一个 FP8 的轻量 lightning indexer,按公式
给每个历史 token 打分, 选 top-k(2048)个,让主注意力只算这一小撮 —— 解决长序列的 计算瓶颈。 - DeepSeek-V3.2 = MLA + DSA:索引器复用 MLA 的
q_lora打分得到topk_indices, MLA 的 MQA 再只读取这 k 个槽位的压缩 KV 做注意力 —— DSA 管「看谁」,MLA 管「怎么省着看」。
附:关键源码索引
| 内容 | 文件 : 行 |
|---|---|
索引器投影层 wq_b/wk/weights_proj |
dsa/dsa_indexer.py:346-367 |
| 算 q/k + 索引器 RoPE | dsa/dsa_indexer.py:439-529 |
| 公式 (DSA-1) 的 FP8 kernel 注释 | dsa/tilelang_kernel.py:244-247 |
| Top-k 选择(参考实现) | dsa/dsa_indexer.py:1207-1214 |
| 短序列跳过打分优化 | dsa/dsa_indexer.py:1362-1383 |
| MLA 里实例化 Indexer | models/deepseek_v2.py:1576-1636 |
| MLA 调用 indexer 拿 topk_indices | deepseek_common/attention_forward_methods/forward_mla.py:262-292 |
- 标题: DeepSeekV3.2之DSA
- 作者: 鱿鱼圈
- 创建于 : 2026-04-21 23:50:00
- 更新于 : 2026-06-30 22:31:19
- 链接: https://yuyanqi.com/2026/04/21/DeepSeekV3.2之DSA/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。