Attention Architecture Evolution

导言

Attention 的发展并不是从 Full Attention 排成一条单向淘汰链。更准确的结构是三条并行路线:共享或压缩 KV cache、把历史压入固定状态、对历史 token 做稀疏选择。MQA、GQA 与 MLA 仍然读取全部历史;GDN 与 KDA 改写了记忆算法;DSA 则在 MLA 前增加轻量索引器,只让主注意力读取 top-k。

本文以首个公开论文或官方发布日为时间点,并把每种结构落到版本固定的开源代码:Q/K/V 从哪里产生、什么对象进入 cache、score 如何形成、复杂度到底被搬到了哪里。

结论

先给出最重要的判断:

  1. Full/MHA、MQA、GQA、MLA 是同一 dense softmax 家族中的不同 KV 组织方式。后三者主要减少 KV cache 或带宽,并不自动把全部 token 两两交互变成线性复杂度。
  2. GDN 与 KDA 属于递归状态路线。历史不再以逐 token K/V 保存,而是写入固定大小矩阵状态;KDA 把 GDN 的 head-wise 标量遗忘细化为 key-channel-wise 对角遗忘。
  3. DSA 属于 token 稀疏路线。Lightning Indexer 仍扫描历史并产生 top-k,主 MLA 才只读取选中位置,所以“主注意力稀疏”不等于整个模块没有历史扫描成本。
  4. 公开时间并不等于概念顺序。DSA 于 2025 年 9 月公开,KDA 于 2025 年 10 月公开;本文按真实时间线排列,而不是按缩写的相似度排列。
![Attention 的三条演进路线](https://pic.shaojiemike.top/shaojiemike/2026/07/126eff6e8371c0195d2790b2bf974eec.png){ width=94% }
小黑认知示意图:Attention 优化的核心不是统一“少算一点”,而是在共享、压缩、固定状态与稀疏选择之间重新组织历史记忆。

时间线

首次公开 结构 首个一手来源 主要变化 历史表示
2017-06-12 Full Attention / MHA Attention Is All You Need[^mha-paper] 每个 Query head 拥有独立 K/V,读取全部历史 完整 K/V cache
2019-11-06 MQA Fast Transformer Decoding: One Write-Head is All You Need[^mqa-paper] 多个 Q head 共享一组 K/V 单组 K/V cache
2023-05-22 GQA GQA: Training Generalized Multi-Query Transformer Models[^gqa-paper] Q head 分组共享 K/V 多组 K/V cache
2024-05-07 MLA DeepSeek-V2[^mla-paper] K/V 联合低秩压缩,解耦 RoPE cKV + kR
2024-12-09 GDN Gated Delta Networks[^gdn-paper] head-wise 衰减的 delta-rule 固定状态 S ∈ R^(K×V)
2025-09-29 DSA DeepSeek-V3.2-Exp[^dsa-release] Indexer 选 top-k,主 MLA 稀疏读取 MLA cache + Indexer K
2025-10-30 KDA Kimi Linear[^kda-paper] key-channel-wise 对角衰减的 delta-rule S ∈ R^(K×V)

Full Attention 与 MHA

Full Attention 描述 token 连通性:每个位置可读取全部允许的历史位置;MHA 描述 head 组织:每个 Query head 拥有独立 K/V head。自回归解码器通常同时使用 causal Full Attention 和 MHA,因此工程讨论里经常连写,但两者不是完全同义词。

三条演进路线

定义:序列长度为 T,Query head 数为 Hq,KV head 数为 Hkv,head 维度为 d,MLA 的 KV latent 维度为 r,DSA 选出的历史位置数为 k

结构 Token 连通性 T 增长的主要缓存 单步 decode 主计算 优化焦点
Full/MHA Dense 2 × T × Hq × d O(T × Hq × d) 基线
MQA Dense 2 × T × d O(T × Hq × d) KV cache 与带宽
GQA Dense 2 × T × Hkv × d O(T × Hq × d) 质量与 KV 成本折中
MLA Dense T × (r + d_rope) 仍扫描全部历史 latent 低秩 KV cache
GDN Recurrent T 无关的 H × K × V 状态 O(H × K × V) 固定状态
KDA Recurrent T 无关的 Hv × K × V 状态 O(Hv × K × V) 更细粒度遗忘
DSA 主注意力 top-k MLA cache + Indexer K Indexer 扫描 T,主 MLA 读取 k 长上下文主计算稀疏化

这张表解释了一个常见误区:KV cache 更小、token 连通性更稀疏、算法复杂度更低是三个不同命题。MQA/GQA 改第一个,DSA 改第二个,GDN/KDA 改第三个所依赖的历史表示。

代码阅读约定

下文代码块从固定 revision 的开源实现中摘取决定语义的关键行;为了聚焦 Q/K/V、cache 和状态更新,省略了异常处理、并行通信、shape 检查和部分无关参数。变量名与核心运算保持源码一致,脚注链接到完整文件和行号。

Dense Softmax 家族

Full Attention

对输入 X,每个 head 独立计算:

1
2
3
4
Qh = X WQh,  Kh = X WKh,  Vh = X WVh
Ah = softmax(Qh Kh^T / sqrt(d) + causal_mask)
Oh = Ah Vh
O = concat(O1, ..., OH) WO

Hugging Face 的 Llama eager 实现把投影、score、softmax 和 value 聚合完整暴露出来:[^llama-code]

1
2
3
4
5
6
7
query_states = self.q_proj(hidden_states).view(hidden_shape).transpose(1, 2)
key_states = self.k_proj(hidden_states).view(hidden_shape).transpose(1, 2)
value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)

attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling
attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32)
attn_output = torch.matmul(attn_weights, value_states)
![Full Attention QKV 计算逻辑](https://pic.shaojiemike.top/shaojiemike/2026/07/982ed5a640579e999f5d9fbb34221b50.png){ width=100% }
Full/MHA:每个 Query head 拥有独立 K/V,并对全部可见 token 计算 dense score;decode cache 随 `T × Hq` 增长。

瓶颈有两层:prefill 需要形成 T × T score;decode 虽然每步只有一个新 Query,却要读取越来越长的 K/V cache,常被显存带宽限制。

MQA

MQA 保留 Hq 个 Query head,但只生成一组 K/V:

1
2
3
Qi = X WQi,  K = X WK,  V = X WV
Ai = softmax(Qi K^T / sqrt(d))
Oi = Ai V

Llama 的同一个实现通过 num_key_value_heads 表达 MHA、GQA 与 MQA。MQA 的配置边界是 num_key_value_heads = 1repeat_kv 只创建与 Query head 对齐的计算视图,不代表 cache 中保存了 Hq 份 K/V:[^llama-code]

1
2
3
4
5
self.num_key_value_groups = (
config.num_attention_heads // config.num_key_value_heads
)
key_states = repeat_kv(key, module.num_key_value_groups)
value_states = repeat_kv(value, module.num_key_value_groups)
![MQA QKV 计算逻辑](https://pic.shaojiemike.top/shaojiemike/2026/07/0f6ed8603111b9b9df54e528bc64826b.png){ width=100% }
MQA:多个 Query head 对同一组 K/V 做 attention。逻辑 score 数量仍由 `Hq` 决定,但 KV cache 从 `Hq` 组降为 1 组。

MQA 的优势集中在 decode cache 和内存读取。代价是所有 Query head 只能从同一份 K/V 表示读取信息,可能损失 head 多样性。

GQA

GQA 在 MHA 与 MQA 之间引入 Hkv 组 K/V,每组服务 Hq / Hkv 个 Query head:

1
2
3
group(i) = floor(i / (Hq / Hkv))
Ai = softmax(Qi Kgroup(i)^T / sqrt(d))
Oi = Ai Vgroup(i)

对应的开源边界仍是 Llama 的两行投影和 repeat_kv:[^llama-code]

1
2
3
4
5
self.k_proj = nn.Linear(hidden_size, num_key_value_heads * head_dim)
self.v_proj = nn.Linear(hidden_size, num_key_value_heads * head_dim)

# Hkv = Hq: MHA; 1 < Hkv < Hq: GQA; Hkv = 1: MQA
key_states = repeat_kv(key, num_attention_heads // num_key_value_heads)
![GQA QKV 计算逻辑](https://pic.shaojiemike.top/shaojiemike/2026/07/64a40dcd42043ee11b45cd441c7f5584.png){ width=100% }
GQA:Query head 在组内共享 K/V。`Hkv` 是连续的工程旋钮,而不是与 MHA、MQA 完全分离的算子。

因此 GQA 的准确理解是:用一部分 head 多样性换取一部分 KV cache 和带宽收益。它仍然是对全部历史 token 的 dense attention。

MLA

MLA 不再以“减少 KV head 数”压缩 cache,而是把所有 head 的 K/V 联合投影到低维 latent:

1
2
3
4
cKV = WDKV h
kR = RoPE(WKR h)
kC, vC = WUKV cKV
score = qC kC^T + qR kR^T

DeepSeek-V2 的兼容实现直接展示了联合降投影和位置分量拆分:[^deepseek-v2-code]

1
2
3
4
5
6
7
8
compressed_kv = self.kv_a_proj_with_mqa(hidden_states)
compressed_kv, k_pe = torch.split(
compressed_kv, [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1
)
kv = self.kv_b_proj(self.kv_a_layernorm(compressed_kv))
k_nope, value_states = torch.split(
kv, [self.qk_nope_head_dim, self.v_head_dim], dim=-1
)

真正体现 memory-optimal decode 的是 DeepSeek 原生推理代码:cache 只分配 kv_lora_rankqk_rope_head_dim;decode 时把 WUKV 的 K 部分吸收到 Query,把 V 部分吸收到输出路径:[^deepseek-v32-mla-code]

1
2
3
4
5
6
7
8
9
self.kv_cache = torch.zeros(max_batch, max_seq, self.kv_lora_rank)
self.pe_cache = torch.zeros(max_batch, max_seq, self.qk_rope_head_dim)

kv, k_pe = torch.split(self.wkv_a(x), [self.kv_lora_rank, self.qk_rope_head_dim], -1)
self.kv_cache[:bsz, start_pos:end_pos] = kv
self.pe_cache[:bsz, start_pos:end_pos] = k_pe.squeeze(2)

q_nope = torch.einsum("bshd,hdc->bshc", q_nope, wkv_b[:, :self.qk_nope_head_dim])
scores = torch.einsum("bshc,btc->bsht", q_nope, self.kv_cache[:bsz, :end_pos])
![MLA QKV 计算逻辑](https://pic.shaojiemike.top/shaojiemike/2026/07/6dab7d66272ce9abf28c9e17262ae1ce.png){ width=100% }
MLA:训练或 prefill 可语义上重建各 head 的 K/V;memory-optimal decode 通过矩阵吸收直接在 `cKV` 上计算,并缓存独立的 RoPE 分量 `kR`。

不要从兼容实现误判 cache

某些通用 Transformers 代码会先用 WUKV 展开完整 K/V,再交给标准 attention/cache 接口。这便于兼容,但会掩盖 MLA 的核心收益。判断实际 cache 必须看部署路径中保存的张量,而不只看数学等价的 forward。

MLA 压缩的是 每个 token 的历史表示,并没有选择更少的历史 token。没有 DSA 时,score 仍覆盖全部可见位置。

递归状态家族

GDN

GDN 仍产生 Q/K/V,但 K/V 不进入按 token 增长的 cache。它先经过短卷积与归一化,再把当前 token 写入固定状态 S

1
2
3
S_bar_t = exp(g_t) S_(t-1)
S_t = S_bar_t + beta_t k_t (v_t - S_bar_t^T k_t)^T
o_t = q_t^T S_t

其中 g_t 在 GDN 参考实现中是 [B,T,H],即每个 head 一个标量衰减;beta_t 控制 delta 写入强度。FLA 层代码展示 Q/K/V 的短卷积和 gate 输入:[^gdn-layer-code]

1
2
3
4
5
6
7
8
9
q = self.q_conv1d(x=self.q_proj(hidden_states), ...)[0]
k = self.k_conv1d(x=self.k_proj(hidden_states), ...)[0]
v = self.v_conv1d(x=self.v_proj(hidden_states), ...)[0]
beta = self.b_proj(hidden_states)

o, recurrent_state = chunk_gated_delta_rule(
q=q, k=k, v=v, g=self.a_proj(hidden_states), beta=beta,
initial_state=recurrent_state, use_qk_l2norm_in_kernel=True,
)

Naive recurrence 则把状态更新写得最清楚:[^gdn-naive-code]

1
2
3
4
5
6
7
8
9
b_q = q[:, :, i]
b_k = k[:, :, i]
b_v = v[:, :, i].clone()
h = h.clone() * g[:, :, i].exp()[..., None, None]
b_beta = beta[:, :, i]
b_v = b_v - (h.clone() * b_k[..., None]).sum(-2)
b_v = b_v * b_beta[..., None]
h = h.clone() + b_k.unsqueeze(-1) * b_v.unsqueeze(-2)
o[:, :, i] = torch.einsum("bhd,bhdm->bhm", b_q, h)
![GDN QKV 与状态更新逻辑](https://pic.shaojiemike.top/shaojiemike/2026/07/cd60fb9c69c8523072ab78f98438ff33.png){ width=100% }
GDN:Q 读取固定状态,K/V 通过 delta rule 写入状态,head-wise 标量 gate 统一衰减该 head 的 `K×V` 记忆;cache 不随序列长度增长。

固定状态不是无损 KV 压缩。多个历史 token 会竞争同一矩阵容量,模型需要学习何时遗忘、何时覆盖。训练时可用 chunk/parallel 算法加速,但语义上仍是按时间递推。

KDA

KDA 延续 delta-rule,却把 g_t[H] 扩展为 [Hv,K]。于是每个 value head 的每个 key channel 都有独立衰减:

1
2
3
S_bar_t = Diag(exp(g_t)) S_(t-1)
S_t = S_bar_t + beta_t k_t (v_t - S_bar_t^T k_t)^T
o_t = q_t^T S_t

Kimi Linear 模型实现先投影并短卷积 Q/K/V,再从两层低秩投影产生 per-dimension gate:[^kimi-model-code]

1
2
3
4
5
6
q = self.q_conv1d(x=self.q_proj(hidden_states), ...)[0]
k = self.k_conv1d(x=self.k_proj(hidden_states), ...)[0]
v = self.v_conv1d(x=self.v_proj(hidden_states), ...)[0]
g = self.f_b_proj(self.f_a_proj(hidden_states))
g = fused_kda_gate(g, self.A_log, self.head_dim, g_bias=self.dt_bias)
beta = self.b_proj(hidden_states).float().sigmoid()

FLA naive KDA 明确记录 g: [B,T,HV,K],并在 key channel 维衰减状态:[^kda-naive-code]

1
2
3
4
5
6
7
8
9
10
11
# g: [B, T, HV, K], S: [B, HV, K, V]
for i in range(0, T):
q_i, k_i, v_i = q[:, i], k[:, i], v[:, i]
g_i, b_i = g[:, i], beta[:, i]
S = S * g_i[..., None].exp()
S = S + torch.einsum(
"bhk,bhv->bhkv",
b_i[..., None] * k_i,
v_i - (k_i[..., None] * S).sum(-2),
)
o[:, i] = torch.einsum("bhk,bhkv->bhv", q_i, S)
![KDA QKV 与状态更新逻辑](https://pic.shaojiemike.top/shaojiemike/2026/07/aa167554501dc40d8e83868a563bb9c1.png){ width=100% }
KDA:相较 GDN 的关键变化不是 Q/K/V 投影,而是 `g` 从每 head 一个标量变成每 key channel 一个对角 gate,使不同记忆方向拥有不同时间尺度。

更细粒度 gate 提高了状态表达能力,也增加 gate 生成和状态更新的复杂度。Kimi Linear 因此采用 KDA 与 MLA 混合层,而不是宣称固定状态可以完全替代 softmax attention。

Token 稀疏家族

DSA

DSA 建立在 MLA 上。Lightning Indexer 为 Query 生成多 head 索引向量,为每个历史 token 生成共享 Indexer K 和按 head 权重,再选择 top-k:

1
2
3
s_tj = sum_h w_th <qI_th, kI_j>
I_t = topk_j(s_tj, k)
O_t = MLA(Q_t, K[I_t], V[I_t])

DeepSeek-V3.2-Exp 的开源 Indexer 使用 FP8 Q/K、独立权重和 topk:[^deepseek-v32-code]

1
2
3
4
5
6
7
q = self.wq_b(qr).view(bsz, seqlen, self.n_heads, self.head_dim)
k = self.k_norm(self.wk(x))
q_fp8, q_scale = act_quant(q, block_size, self.scale_fmt)
k_fp8, k_scale = act_quant(k, block_size, self.scale_fmt)
weights = self.weights_proj(x.float()) * self.n_heads ** -0.5
index_score = fp8_index(q_fp8, weights, self.k_cache[:, :end_pos], self.k_scale_cache[:, :end_pos])
topk_indices = index_score.topk(min(self.index_topk, end_pos), dim=-1)[1]

主 MLA 用这些 indices 构造稀疏 mask;decode 仍从压缩 cKV cache gather,而不是回到完整 K/V:[^deepseek-v32-code]

1
2
3
4
5
6
topk_indices = self.indexer(x, qr, start_pos, freqs_cis, mask)
index_mask = torch.full((bsz, 1, end_pos), float("-inf"), device=x.device)
index_mask = index_mask.scatter_(-1, topk_indices, 0)
scores += index_mask.unsqueeze(2)
scores = scores.softmax(dim=-1)
x = torch.einsum("bsht,btc->bshc", scores, self.kv_cache[:, :end_pos])
![DSA Indexer 与 MLA 计算逻辑](https://pic.shaojiemike.top/shaojiemike/2026/07/f9dff49b040eef256bd2d9ba465f8bc5.png){ width=100% }
DSA:MLA 主路径保留压缩 cache;Lightning Indexer 额外缓存共享 K、扫描历史并返回 top-k,主 attention 只在选中位置做高维计算。

稀疏的边界

DSA 降低的是 主 MLA 的高维 score/value 计算。Indexer 本身仍需对历史位置打分,只是 head 维和精度更小。index_topk=2048 是 DeepSeek-V3.2-Exp 的具体配置,不是 DSA 定义中的固定常数。

混合架构

长上下文模型越来越少押注单一 Attention。线性或递归状态层负责大部分低成本历史传播,周期性 Full/MLA 层补充精确 token 检索,是更现实的质量—效率折中。

![Kimi Linear 混合架构](https://pic.shaojiemike.top/shaojiemike/2026/07/0be2c372768092289c465956c0b05bdf.png){ width=76% }
Kimi Linear 论文 Figure 3:以三层 KDA 配一层 MLA 的混合结构。它说明 KDA 的目标是承担大部分历史传播,而非证明 softmax attention 已无必要。
![Kimi Linear 长上下文效率](https://pic.shaojiemike.top/shaojiemike/2026/07/baa3d8e8bdd4dc6e8a5016537f6fea88.png){ width=86% }
Kimi Linear 论文 Figure 7:图中 1M context 的可见标注约为 prefill 2.9×、decode TPOT 2.2×(相对 MLA)。论文摘要/正文另有“最高 6× decode”的表述,两者属于不同统计或边界,不能直接合并。

这种混合思想也适用于其他组合:GQA 降低 cache,DSA 降低主 attention 的 token 数,二者解决不同维度;MLA 与 DSA 更是明确的基座和索引关系。

选择框架

主要瓶颈 优先考察 原因 必验风险
Decode KV cache / 带宽 GQA、MQA、MLA 直接减少每 token 历史表示 质量回退、kernel 与 cache layout
极长上下文的固定内存 GDN、KDA recurrent state 不随 T 增长 状态容量、训练 kernel、跨序列边界
极长上下文主 attention FLOPs DSA 主 MLA 只读取 top-k Indexer recall、top-k 通信、稀疏 kernel
质量优先、上下文适中 Full/MHA、GQA 精确 dense retrieval,生态成熟 cache 和带宽上限
质量与极长上下文同时要求 Hybrid 递归层传播、周期性 softmax 检索 层型比例、状态/cache 双系统

工程选型时至少测四件事:

  1. Prefill 与 decode 分开:二者的算力、带宽和并行特性完全不同。
  2. 峰值 cache 与实际吞吐同时测:更小的理论 cache 可能被不合适的 layout、dequant 或 gather 抵消。
  3. 固定准确率条件下比较:MQA/GQA group 数、MLA rank、DSA top-k、GDN/KDA state 维度都属于质量—成本旋钮。
  4. 按实现边界比较:FlashAttention、FlashMLA、chunk recurrence 与稀疏 gather 的 kernel 成熟度往往比大 O 公式更决定真实速度。

常见误区

  • “MQA/GQA 是稀疏 attention”:错误。它们共享 K/V head,但仍读取所有历史 token。
  • “MLA 把 attention 变成线性复杂度”:错误。MLA 压缩每个 token 的 K/V 表示,没有压缩 token 数。
  • “GDN/KDA 只是更激进的 KV cache 压缩”:不准确。它们改变了历史记忆算法,用固定状态递推替代逐 token softmax 检索。
  • “DSA 只算 top-k,所以模块没有全历史成本”:不完整。主 MLA 是 top-k,Indexer 仍扫描历史。
  • “后出现的结构必然取代前者”:错误。真实模型广泛使用混合层,不同方法解决的是 cache、状态容量、token 连通性或 kernel 效率中的不同问题。

总结

Attention 的发展路径可以压缩成一句话:先共享 K/V,再联合压缩 K/V;另一条路线把历史写入固定状态;最新的稀疏路线先检索,再让昂贵的主注意力读取少数 token。

理解这些结构时,不要只背缩写。顺着代码追四个对象就足够:

  1. Q/K/V 如何投影、按多少 head 组织;
  2. cache 保存完整 K/V、latent、Indexer K,还是固定状态;
  3. score 是对全部历史、top-k,还是根本不存在显式 T×T 矩阵;
  4. 被省掉的成本是否转移到了低秩重建、状态更新、索引器或稀疏 gather。

本文七张计算图均依据版本固定源码自绘,SVG 源文件与 1920×1080 PNG 同时保留,并通过结构、箭头、碰撞、语义几何与版式校验。

参考文献

[^mha-paper]: Vaswani et al., Attention Is All You Need, first submitted 2017-06-12.
[^mqa-paper]: Shazeer, Fast Transformer Decoding: One Write-Head is All You Need, first submitted 2019-11-06.
[^gqa-paper]: Ainslie et al., GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints, first submitted 2023-05-22.
[^mla-paper]: DeepSeek-AI, DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model, first submitted 2024-05-07.
[^gdn-paper]: Yang et al., Gated Delta Networks: Improving Mamba2 with Delta Rule, first submitted 2024-12-09.
[^dsa-release]: DeepSeek-AI, DeepSeek-V3.2-Exp release, 2025-09-29; model and paper repository.
[^kda-paper]: Zhang et al., Kimi Linear: An Expressive, Efficient Attention Architecture, first submitted 2025-10-30.
[^llama-code]: Hugging Face Transformers, modeling_llama.py, commit 29985e67.
[^deepseek-v2-code]: DeepSeek-AI, modeling_deepseek.py, revision 604d5664.
[^deepseek-v32-mla-code]: DeepSeek-AI, inference/model.py MLA, commit 87e509a2.
[^deepseek-v32-code]: DeepSeek-AI, inference/model.py, commit 87e509a2.
[^gdn-layer-code]: FLA, fla/layers/gated_deltanet.py, commit c70f11c5.
[^gdn-naive-code]: FLA, fla/ops/gated_delta_rule/naive.py, commit c70f11c5.
[^kimi-model-code]: Moonshot AI, modeling_kimi.py, revision e1df551a.
[^kda-naive-code]: FLA, fla/ops/kda/naive.py, commit c70f11c5.

Author

Shaojie Tan

Posted on

2026-07-21

Updated on

2026-07-21

Licensed under