30 秒快速回答
线性注意力(Linear Attention)把注意力从「每步重扫全部历史」改成「维护一份固定大小的记忆矩阵」:每读一个 token 用一次低成本状态更新把它并进记忆,计算随长度线性增长,解码时每步只读常数大小的状态。Gated DeltaNet 是最主流的实现——用「衰减门 + 写入门」控制遗忘与改写,Qwen3-Next 在每 4 层中用它当 3 层主干;Kimi Linear 的 KDA 把衰减粒度从「每个头一个标量」细化到「每维一个标量」。
核心价值一句话:它不追求注意力算得更准,而是把长上下文的成本从「平方级计算 + 线性显存」压成「线性计算 + 常数显存」。
一、先算清 softmax 注意力的两笔账
| 项目 | Full Attention | 线性注意力 |
|---|---|---|
| Prefill 计算 | O(T²·d) | O(T·d²) |
| KV Cache | O(T·d),随长度涨 | O(d²) 固定状态 |
| 解码每步 | 与已生成长度线性相关 | 与长度无关 |
| 精确定位 | 可回看任意位置 | 依赖状态压缩,细节会衰减 |
长上下文其实有两个敌人:算不动(计算平方涨)与塞不下(缓存线性涨)。FlashAttention 优化前者的常数因子,KV Cache 量化与淘汰优化后者的存储效率,线性注意力则直接改掉两者的增长阶。
二、把历史压进一个矩阵
softmax 注意力必须摊开 T×T 的两两关系;线性注意力把相似度换成可分解的核函数 φ,于是矩阵乘法可以重排成「先累加键值、再查询」:
S_t = S_{t-1} + φ(k_t) v_tᵀ # 记忆:d×d 状态矩阵,与长度无关
o_t = φ(q_t)ᵀ S_t # 读取:拿当前 ask 去查记忆
本质是一个带读写的循环:
import torch
def linear_attention_naive(q, k, v):
"""q/k/v: (T, D)。教学简化版,逐 token 循环,非生产实现。"""
T, D = k.shape
S = torch.zeros(D, D) # 状态 = 全部历史的压缩
out = []
for t in range(T):
S = S + torch.outer(k[t], v[t]) # 写入:直接累加进记忆
out.append(q[t] @ S) # 读取:用 q 查这份记忆
return torch.stack(out)
代价随之而来:记忆是「加法式」的,只增不减。两个 token 的 key 方向相近、value 不同时,旧值会被叠加污染——这就是线性注意力在召回类任务(大海捞针、多跳问答、精确引用)上打不过全注意力的根本原因,业界称为记忆干扰(memory interference)。
三、DeltaNet:只写「没记住的那部分」
DeltaNet 的改动很小但关键:写入前先用当前状态做一次预测,只把预测误差写进去,即神经网络里的 delta 规则:
pred = k_tᵀ S_{t-1} # 用旧记忆预测当前 value
delta = v_t - pred # 残差:记忆里还没有的信息
S_t = S_{t-1} + β_t · k_t deltaᵀ # 只写残差,β 控制写入强度
def delta_rule_layer(q, k, v, beta):
"""在上面的基础上加残差写入,beta: (T,),0~1。"""
T, D = k.shape
S = torch.zeros(D, D)
out = []
for t in range(T):
delta = v[t] - k[t] @ S # 先预测,再算差
S = S + beta[t] * torch.outer(k[t], delta) # 只写差量
out.append(q[t] @ S)
return torch.stack(out)
数学上它等价于对每个键做一次「先擦旧值、再写新值」的正交投影更新,所以相近的键不再互相污染,长上下文外推时也不容易出现困惑度爆炸。
四、Gated DeltaNet:三个门各管一件事
纯 delta 规则缺一个能力:该忘时要能忘。Gated DeltaNet 把 Mamba2 的门控衰减与 delta 规则拼在一起:
| 门 | 作用 | 缺了会怎样 |
|---|---|---|
| 衰减门 α | 每步给旧状态整体打折,控制历史保留比例 | 老信息永久占位,长文后期学不进新内容 |
| 写入门 β | 控制当前 token 改写状态的强度 | 噪声 token 也能大幅污染记忆 |
| 输出门 | 读取结果前做门控(SiLU 激活) | 训练早期不稳定,收敛变慢 |
def gated_delta_rule(q, k, v, alpha, beta):
"""Gated DeltaNet 的状态更新(单头、教学简化版)。"""
T, D = k.shape
S = torch.zeros(D, D)
out = []
for t in range(T):
S = alpha[t] * S # ① 衰减
delta = v[t] - k[t] @ S # ② 预测残差
S = S + beta[t] * torch.outer(k[t], delta) # ③ 门控写入
out.append(q[t] @ S) # ④ 读取(生产实现还有输出门)
return torch.stack(out)
Kimi Delta Attention(KDA) 在此之上又细化一层:Gated DeltaNet 每个头只用一个标量衰减门,整头记忆同等对待;KDA 给状态的每个特征维度一个独立衰减门,把「记什么、忘什么」的粒度从「按头」降到「按通道」。第三方整理的公开评测显示,Gated DeltaNet 在 4000 token 的 S-NIAH-2 类召回探针上约 92%,纯加法式线性模型约 56%,差距主要来自 delta 规则抑制了键值干扰;具体数字请以论文与官方复现为准。
五、怎么落地:混合架构 + 分块并行训练
- 不要整网替换,用混合。Qwen3-Next 的 80B MoE 主干采用 3:1 布局——每 4 层里 3 层 Gated DeltaNet、1 层门控全注意力;Kimi Linear 走同一思路(KDA + 全注意力混合)。全注意力层负责「精确回看」,线性层负责「低成本的常态记忆」。
- 训练必须用 chunkwise 并行 kernel。上面的代码是逐 token 循环,只为讲清状态更新;真实训练会把序列切块,块内并行、块间递推,并用紧凑 WY 表示把更新写成矩阵乘才能吃到 Tensor Core。实践上直接用
flash-linear-attention(FLA)库的 chunkwise 实现,别手写循环——O(T) 的 Python 循环会把省下的算力全吃掉。
| 你的场景 | 建议 |
|---|---|
| 长文档/长对话、并发高、显存紧 | 选已内置混合架构的开源模型,别自己改注意力 |
| 需要微调 | 只替换部分层做混合,务必自建评测集 A/B |
| 以逐字引用、精确检索为主 | 保留足够全注意力层,或外挂 RAG |
| 短上下文(32k 以内) | 收益有限,先优化 KV Cache 与批处理 |
六、边界:什么时候不该用
- 精确回忆是硬伤。状态容量固定,逐字复述长文中的某个数字、某行代码仍是全注意力强项;混合架构能补上大部分召回差距但不是全部。
- 不是免费午餐。它省的是长序列上的计算与缓存,代价是容量被压在 d×d 状态里;瓶颈若是「模型不够聪明」而非「上下文太长」,换注意力没用。另外各框架对线性层的 kernel、量化与并行支持差异较大,上生产前先确认推理引擎真覆盖了你用的层类型。
总结
- 线性注意力改的是增长阶:计算 O(T²)→O(T),状态 O(T)→常数。
- 加法式记忆会互相污染,delta 规则靠「只写预测残差」缓解。
- Gated DeltaNet = Mamba2 衰减门 + delta 规则 + 输出门;KDA 把衰减细化到特征维度。
- 落地靠混合架构(典型 3:1)+ chunkwise kernel,不是整网替换。
- 边界清晰:省长上下文的算力与显存,掉逐字精确回忆的能力,选型前先量瓶颈。
延伸阅读建议:先读《什么是 KV Cache?大模型推理提速降本的核心技术全解析》理解「显存随长度增长」这条硬约束,再读《什么是 Mamba?SSM 状态空间模型如何突破 Transformer 的长文本瓶颈全解析》看近亲路线,接着读《什么是隐空间稀疏注意力(LSA)?》对照另一条「不换公式、只换找人方式」的思路。