δDeltaNet / 图解与推导
/ 139
LINEAR TRANSFORMERS · READING NOTES

从 Attention
DeltaNet

把历史写进一个矩阵,
然后学会更好地记住它。

从一次内积开始,走到分块并行与误差驱动的记忆。
用原始 slide 作配图,把省略在公式之间的那几步补回来。

基于 Yoon Kim 的报告参考 Songlin Yang 的 DeltaNet 三部曲
全文记号

token 向量按列表示,Q / K / V 按行堆叠。状态为 $S\in\mathbb R^{d_v\times d_k}$,读取为 $o=Sq$。不同 slide 的转置约定在正文中统一。

CHAPTER 01 / SLIDES 1—17

长序列,需要怎样的记忆

从逐 token 预测开始,找到 Attention 的成本来源。

001

长序列,需要怎样的记忆?

长序列建模有一个朴素的愿望:读得更多,却不必为每一段新内容付出越来越高的代价。Attention 保留每个历史位置的 key 和 value,让当前 query 重新检索前文;线性注意力则尝试把历史写进一个固定大小的状态。Yoon Kim 的这份报告从两者的计算差异出发,逐渐走到一个更有意思的问题:压缩了历史之后,怎样才能记得准、改得动、读得出来?

FIG. 001 · 原报告第 1 页点击图片放大 ↗

这份图解沿着 Yoon Kim 的报告展开:先把 Attention 的计算拆开,再推导线性注意力的分块训练,最后理解 DeltaNet 如何用预测误差修正记忆。正文中的公式统一采用 $S\in\mathbb R^{d_v\times d_k}$、$o=S q$ 的方向;slide 如果采用转置约定,描述的是同一个运算。

原始材料:报告 PDF · Songlin Yang 的模型篇 · 算法篇 · 架构篇。后面的数值小例子与逐元素推导为这份图解补写,原博客插图在图下注明来源。

002

从一个前缀开始

给语言模型一个前缀 “MIT is located in”,它可能接出 Cambridge。这一步实际产生的是词表上的概率分布,再从中选出下一个 token。我们先把模型内部看作黑盒,只关注它做的事情:根据已经出现的文本,预测接下来最可能出现什么。

FIG. 002 · 原报告第 2 页点击图片放大 ↗
003

把刚生成的词接回输入

Cambridge 一旦生成,就成为下一次预测的条件。输入因此延长为 “MIT is located in Cambridge”,模型会基于这个新的前缀继续生成。这种把输出接回输入的过程,就是自回归生成;后一步要等前一步完成,因为它需要知道刚生成的词究竟是什么。

FIG. 003 · 原报告第 3 页点击图片放大 ↗
004

同一个模型,继续预测

在更长的前缀上,同一个模型接着预测 Massachusetts。模型参数没有因为生成了 Cambridge 就重新训练,变化的是它所看到的上下文。这个区别很重要:后面讨论的 KV cache 和递归状态,保存的都是当前序列带来的信息,而非一次次修改后的整套模型参数。

FIG. 004 · 原报告第 4 页点击图片放大 ↗
005

一段文本的概率如何组成

把 Massachusetts 也接回前缀,继续重复,就得到一段完整文本。概率上,这对应链式分解 $P(x_{1:L})=\prod_{t=1}^L P(x_t\mid x_{<t})$。这条生成顺序来自任务本身,至于每一步怎样利用已有前缀,则由模型内部的序列架构决定。

FIG. 005 · 原报告第 5 页点击图片放大 ↗
006

从自回归生成走进 Transformer

Transformer 是实现这套预测过程的一种架构。它需要把前缀中的词转成向量,并让这些向量交换信息,才能知道当前位置适合接什么。我们接下来关心的 Attention,正是其中负责位置间交互的运算,也是长序列成本的主要来源之一。

FIG. 006 · 原报告第 6 页点击图片放大 ↗
007

当前词的表示还缺少上下文

单看 in 这个词,很难猜出后面会接 Cambridge;结合前面的 MIT,答案才变得明确。因此,模型需要的并非 in 的孤立词义,而是它在当前句子里的表示。Vaswani 等人在 2017 年提出的 Transformer,用 Attention 来完成这种上下文整合。

FIG. 007 · 原报告第 7 页点击图片放大 ↗
008

用当前需求检索前面的内容

Attention 可以理解为一次由当前需求发起的检索:当前位置用 query 与历史 key 比较,再按匹配程度汇总相应的 value。在 “MIT is located in” 中,这让最后一个位置能够吸收 MIT 等前文信息,形成适合预测的表示。Q、K、V 都来自学习到的投影,模型通过训练决定什么信息值得匹配、什么内容值得取回。

FIG. 008 · 原报告第 8 页点击图片放大 ↗
009

检索结果参与下一词预测

汇总上下文之后,模型还要把得到的向量转成下一词的概率。下图中从表示到 Cambridge 的过程,连接了上下文整合与词预测这两个阶段。Attention 本身输出的仍是向量,它会经过后续网络与词表投影,才成为我们看到的文本。

FIG. 009 · 原报告第 9 页点击图片放大 ↗
010

生成越往后,历史越长

当 Cambridge 成为新的输入位置,它也会产生自己的 key 和 value,供后续 token 检索。历史因此不断增长:刚才还是预测结果的内容,下一步就成了上下文的一部分。生成越往后,可供读取的信息越多,需要保存的数据也越多。

FIG. 010 · 原报告第 10 页点击图片放大 ↗
011

新的 query,重新读取前缀

为了接出 Massachusetts,模型现在要从 Cambridge 这个位置发起新的检索。即使前文的大部分内容没变,query 已经变了,各历史位置的重要性也可能随之改变。旧 key/value 可以复用,但新的匹配分数和注意力权重仍需要重新计算。

FIG. 011 · 原报告第 11 页点击图片放大 ↗
012

Attention 的表达力来自直接交互

同样的机制可以作用在每个位置上:MIT 的表示预测 is,is 的表示预测 located,依次类推。下图把这些预测放在一起,也揭示了训练与生成的差别。训练样本已经给出整段文本,因此一层内的各位置可以同时计算,只需用因果 mask 限制可见范围;生成时,未来 token 尚不存在,必须逐个产生。

FIG. 012 · 原报告第 12 页点击图片放大 ↗
013

序列不限于文字

只要能编码成一串向量,序列就不必局限于文字。图像 patch、音频片段和机器人动作也可以成为 Transformer 的输入或输出,下图展示的就是这种通用接口。不同模态需要各自的编码方式,但都可能面对同一个难题:序列很长时,位置之间的交互怎样才能算得起?

FIG. 013 · 原报告第 13 页点击图片放大 ↗
014

这套架构从哪里来

下图展示的是《Attention Is All You Need》,也就是前面这套 Transformer 架构的出处。它把 Attention 放到序列建模的中心,让各位置能够直接整合上下文,同时保留适合并行训练的计算形式。报告截取的引用历史说明了它的影响力;我们接下来关心的,则是这套成功设计在长序列上遇到的成本。

FIG. 014 · 原报告第 14 页点击图片放大 ↗
015

当上下文长到一本书

一段话可能只有几十个词,一套《哈利·波特》却有约百万词,人类 DNA 更达到数十亿核苷酸。把序列拉长到这样的尺度,两两交互就会迅速变贵:长度为 $L$ 的因果序列约有 $L(L+1)/2$ 对可见关系。因果约束省掉了未来位置的交互,却没有改变平方级增长。

FIG. 015 · 原报告第 15 页点击图片放大 ↗
016

省计算之后,还能找回信息吗?

我们希望降低成本,又不想丢掉 Attention 带来的预测能力。因此,高效序列建模要同时回答三个问题:模型能否学好,训练是否高效,生成时是否便宜?把历史压缩得更小只是起点,真正困难的是需要某条信息时仍能把它找回来。

FIG. 016 · 原报告第 16 页点击图片放大 ↗
017

两条相互配合的路线

这份报告围绕两项工作展开。GLA 研究由输入决定的遗忘门,并为线性注意力设计硬件友好的训练实现;DeltaNet 则让写入取决于记忆的预测误差,再解决这种更新如何并行。下图列出的两篇论文分别发表于 ICML ’24 与 NeurIPS ’24,它们将带我们从“保留多少旧信息”走到“这次究竟需要修正多少”。

FIG. 017 · 原报告第 17 页点击图片放大 ↗
CHAPTER 02 / SLIDES 18—37

Attention 的计算与缓存

沿着 Q、K、V 追踪训练、生成和两种存储。

018

先把 Attention 的账算清楚

先从熟悉的 Attention 开始,把一层计算的输入、输出和缓存追踪清楚。训练时我们同时处理整段序列,生成时则每次只处理一个新位置,同一个模型在这两个阶段的成本很不一样。后面会分别计算运算量、存储量和顺序依赖,而不是只用一个“复杂度”概括它们。

FIG. 018 · 原报告第 18 页点击图片放大 ↗
019

长度、头维度与矩阵方向

把 $L$ 个 token 的表示按行排在一起,就得到输入矩阵 $X\in\mathbb R^{L\times d}$。这里 $L$ 是序列长度,$d$ 是表示宽度。为了看清后面的矩阵乘法,正文进一步区分 query/key 宽度 $d_k$ 与 value 宽度 $d_v$:单个向量按列表示,按 token 行堆叠后,$Q,K\in\mathbb R^{L\times d_k}$、$V\in\mathbb R^{L\times d_v}$;简化成本时再令两种宽度等于 $d$。

FIG. 019 · 原报告第 19 页点击图片放大 ↗

后面的状态 $S$ 把 key 空间映射到 value 空间,所以形状为 $d_v\times d_k$。因此单位置读取写成 $Sq$,一批按行排列的 query 则写成 $QS^\top$。记住这次转置,就不会在分块公式里混淆行与列。

020

输出仍然按位置排列

Self-attention 将一串输入表示变成一串包含上下文的输出,位置数仍然是 $L$。如果 value 宽度为 $d_v$,输出就是 $O\in\mathbb R^{L\times d_v}$。下图中的 $O=\mathrm{SelfAttention}(X)$ 概括了这一层的接口;多头结果随后拼接并经过输出投影,接回模型宽度。

FIG. 020 · 原报告第 20 页点击图片放大 ↗
021

Q、K、V 是怎样来的

Attention 首先从同一个输入产生三组表示:$Q=XW_Q$、$K=XW_K$、$V=XW_V$。Query 表达当前要找什么,key 用于匹配,value 提供匹配后取回的内容。每个位置都能独立完成这些投影,因此这一步随长度线性增长;在简化的同宽设置下,成本为 $O(Ld^2)$。

FIG. 021 · 原报告第 21 页点击图片放大 ↗

标准缩放点积注意力写成:

$$ A=\operatorname{softmax}_{\rm row}\!\left(\frac{QK^\top}{\sqrt{d_k}}+B\right),\qquad O=AV. $$

这里 $B_{rt}=0$ 当 $t\le r$,否则为 $-\infty$。必须在 softmax 前把未来位置置为负无穷;把分数乘零会让它在指数运算后变成 $e^0=1$,并不能屏蔽未来。后文线性注意力没有 softmax,才会直接乘 0/1 掩码 $M$。

022

哪一步引入了平方项

接下来计算 $QK^\top$,其中第 $(r,t)$ 项是位置 $r$ 的 query 与位置 $t$ 的 key 的内积。因果 mask 排除未来位置,再经 softmax 得到注意力矩阵 $A$。下图中的下三角格子就是这些权重:每行对应一个 query。位置对共有平方量级,每对又要做一次 $d$ 维内积,于是成本达到 $O(L^2d)$。

FIG. 022 · 原报告第 22 页点击图片放大 ↗
023

加权求和也要付同一笔账

有了权重,还要计算 $O=AV$,把它们应用到 value 上。第 $r$ 行输出是 $\sum_t A_{rt}v_t$,也就是当前 query 对所有可见 value 的加权和。整批执行又需要一次 $O(L^2d)$ 的矩阵乘法,加上输入投影,一层的主要工作量为 $O(Ld^2+L^2d)$。

FIG. 023 · 原报告第 23 页点击图片放大 ↗

这个计算量与是否把 $A$ 完整存入显存是两件事。普通实现会保存大中间矩阵,FlashAttention 则通过分块与重算避免这样做。我们在第 75 页会具体推导它如何跨块合并 softmax。

024

因果约束不等于逐位置串行

一层 Attention 的输出会继续经过 FFN 和后续 Attention 层,如下图所示。层与层之间必须顺次计算,但同一层内的不同位置可以并行:第 $r$ 个位置只需通过 mask 屏蔽未来,无需等待第 $r-1$ 个位置的输出。因果性限制的是可读取的信息,不会自动把训练变成逐 token 递推。

FIG. 024 · 原报告第 24 页点击图片放大 ↗
025

大矩阵运算适合 GPU

投影、两两打分和加权求和,都能组织成少数几次大型矩阵运算。这就是 Attention 容易并行训练的原因,也是下图将顺序步骤写为 $O(1)$ 的含义。与此同时,矩阵会随序列变大,总工作量仍是 $O(L^2d+Ld^2)$;固定数量的运算轮次,并不意味着实际耗时固定。

FIG. 025 · 原报告第 25 页点击图片放大 ↗
026

三种成本要分开看

把前面的分析汇总,Attention 训练需要平方级计算,却可以用固定数量的大型运算完成。下表省略了特征维度,分别记为 $O(L^2)$ Compute 和 $O(1)$ Steps。Memory 的 $O(L)$ 则采用优化存储的口径:完整的注意力矩阵虽然有 $L^2$ 个元素,却不一定需要同时保存在显存中,后面的 FlashAttention 正是利用了这一点。

FIG. 026 · 原报告第 26 页点击图片放大 ↗
027

生成时只计算新位置

进入生成阶段后,未来 token 还不存在,每一步只能处理新出现的那个位置。好在旧 token 的 key 和 value 已经算过,可以留到下一步复用。因此解码时不必把整个前缀重新投影,只需生成新位置的 Q/K/V,再用新的 query 读取历史。

FIG. 027 · 原报告第 27 页点击图片放大 ↗
028

新 token 的三次投影

同一个新输入 $x_t$ 会产生三种不同用途的向量。$q_t$ 发起当前的读取,$k_t$ 和 $v_t$ 则既参与本步,也保存下来供未来使用。它们由各自的投影参数生成,因此一个 token 可以写入某种关联,同时提出另一种查询。

FIG. 028 · 原报告第 28 页点击图片放大 ↗
029

一个 query 扫过所有历史 key

新 query 要和每个可见的历史 key 计算匹配。下图中的分式就是 softmax 权重:分子是某个位置的指数分数,分母是整个前缀上的指数和。即使历史 key 没变,换一个 query 也会改变分子与分母,所以之前算出的注意力权重无法直接沿用。

FIG. 029 · 原报告第 29 页点击图片放大 ↗
030

用归一化权重,汇总历史 value

归一化之后,一步输出为 $o_t=\sum_{j\le t}a_{tj}v_j$。可以把它分成两层理解:Q/K 决定标量权重 $a_{tj}$,权重再决定每个 value 对输出贡献多少。当前虽然只有一个 query,它读取的仍是整个前缀,而非只取出一条历史记录。

FIG. 030 · 原报告第 30 页点击图片放大 ↗
031

一次读取怎样走到最终预测

一次 Attention 读取还不是最终预测。这个位置的表示会继续经过后续 FFN 与 Attention 层,最后形成词表上的分布。各层都有自己的历史 K/V,因此完整解码成本要把这些读取全部累加;在长上下文中,反复从显存搬运历史向量也会变得昂贵。

FIG. 031 · 原报告第 31 页点击图片放大 ↗
032

预测结果进入下一轮

得到本轮输出 $y_t$ 后,生成出的 token 又成为下一轮输入 $x_{t+1}$。模型会为它计算新的各层表示,同时利用已经保存的历史 K/V。这样,序列在逐步延长,已完成的历史计算却可以复用,避免每次都从第一个词重新开始。

FIG. 032 · 原报告第 32 页点击图片放大 ↗
033

缓存复用了什么

KV cache 保存的是历史 key 和 value,因为未来 query 还要读取它们。旧 query 已经完成了自己的任务,不再需要跨步保留。缓存因此省掉了历史投影和前缀重算,但新 query 仍要与旧 key 匹配,再汇总相应的 value。

FIG. 033 · 原报告第 33 页点击图片放大 ↗
034

每一步都会扩展缓存

每生成一个 token,缓存就增加一组 K/V,下一次读取也要多考虑一个位置。长度为 $t$ 时,单层单头约保存 $t(d_k+d_v)$ 个元素;换成实际显存,还需乘 KV 头数、层数、batch 和每个元素的字节数。这种随历史增长的存储,正是固定状态模型希望改变的部分。

FIG. 034 · 原报告第 34 页点击图片放大 ↗
035

KV cache 与注意力矩阵是两种存储

这里需要区分两种常被混在一起的存储。KV cache 是跨解码步保存的历史向量,大小随长度线性增长;注意力矩阵则记录 query 与 key 的配对权重,整段训练时可能达到平方规模。下图讨论的是前者。FlashAttention 可以避免保存完整注意力矩阵,精确的全局解码仍需要访问历史 K/V。

FIG. 035 · 原报告第 35 页点击图片放大 ↗
036

从一步成本到整段成本

单步解码在第 $t$ 个位置花费 $O(td)$ 的注意力计算,生成整段长度 $L$ 的序列,就累积为 $\sum_{t=1}^L O(td)=O(L^2d)$。因此下表中训练和生成的总计算量都写成平方级,区别在于顺序步骤:训练可以并行处理位置,生成则要经历 $L$ 轮预测。

FIG. 036 · 原报告第 36 页点击图片放大 ↗
037

保留历史,换取灵活检索

Attention 的取舍现在清楚了:它保留每个历史位置,让未来 query 可以灵活地重新检索,因此表达能力很强、训练也容易并行;代价是平方级交互和不断增长的解码缓存。接下来尝试另一种组织历史的方式——先把它聚合进一个状态,让每次读取的成本不再跟着前缀长度增长。

FIG. 037 · 原报告第 37 页点击图片放大 ↗
CHAPTER 03 / SLIDES 38—52

把历史写成一个状态

结合律如何把注意力变成矩阵状态的 RNN。

038

去掉 softmax,发生了什么

先考虑去掉 softmax 的简化模型:$O=(QK^\top\odot M)V$,直接用内积给 value 加权。它属于 Katharopoulos 等人在 2020 年探索的线性 Transformer 路线。这里已经改变了权重函数,因此不再是原 softmax Attention 的等价实现;经典方法还可能包含特征映射与归一化,下面先沿用报告中的简化版本。

这里的 $\odot$ 表示对应位置相乘:两张表同一行、同一列的数字相乘,位置不变。比如 $(2,6)\odot(3,2)=(6,12)$。后面还会用到 $\oslash$,表示对应位置相除:$(2,6)\oslash(3,2)=(2/3,3)$。它们都不会把多个位置加起来;普通矩阵乘法则要做“对应相乘,再求和”。这里与 0/1 矩阵 M 做 ⊙,就是把未来位置的分数乘成 0。

FIG. 038 · 原报告第 38 页点击图片放大 ↗
039

两种注意力,使用两种因果 mask

去掉 softmax 时,因果 mask 的用法也要一起改变。Softmax 前要给未来分数加 $-\infty$,使其指数为零;直接使用线性权重时,则将未来内积乘零即可。下图两种 mask 的取值分别为 $\{-\infty,0\}$ 和 $\{0,1\}$,对应的正是这两种操作。若在 softmax 前仅把未来分数置零,$e^0=1$ 反而会让它参与归一化。

FIG. 039 · 原报告第 39 页点击图片放大 ↗
040

直接乘 QK,仍然是平方复杂度

仅仅删除 softmax,还没有省掉平方计算。如果仍按 $O=(QK^\top\odot M)V$ 执行,$QK^\top$ 依旧包含 $L\times L$ 个分数。下图所说的 “Haven’t really gained anything (yet)” 就是这个意思。效率上的突破,要等我们重新安排乘法和求和的顺序。

FIG. 040 · 原报告第 40 页点击图片放大 ↗
041

把 query 移到求和外面

逐位置写出线性注意力,得到 $o_t=\sum_{j\le t}(q_t^\top k_j)v_j$。与 softmax 版本相比,它没有把所有历史位置耦合起来的指数归一化分母。于是,同一个 query 可以移到求和外,历史 K/V 则先聚合成一个与 query 无关的量。下面的等价式是后面所有递推与分块计算的起点。

$$ o_t=\sum_{j\le t}v_j(k_j^\top q_t) =\left(\sum_{j\le t}v_jk_j^\top\right)q_t. $$

括号里的量只由已经读过的 key/value 决定,与这次 query 无关。于是它可以保存下来,供任意后续 query 使用。原博客 Part I:Linear attention as RNN

FIG. 041 · 原报告第 41 页点击图片放大 ↗
042

外积到底写入了什么

每一项 $(q_t^\top k_j)v_j$,既可以先做内积再缩放 value,也可以先构造外积 $v_jk_j^\top$,再右乘 query。这个外积是一个 $d_v\times d_k$ 矩阵,第 $(a,b)$ 项为 $v_{j,a}k_{j,b}$。它将 key 的各分量与 value 的各分量绑定起来,之后的 query 决定沿 key 空间怎样读取这些关联。

FIG. 042 · 原报告第 42 页点击图片放大 ↗
043

两个历史位置的小展开

用两个历史位置就能看清这次重排。输出原本是 $v_1(k_1^\top q)+v_2(k_2^\top q)$;先计算 $S=v_1k_1^\top+v_2k_2^\top$,再取 $Sq$,结果完全相同。下图的求和式只是把历史从两个位置推广到任意多个。这一步使用结合律,没有引入新的近似。

FIG. 043 · 原报告第 43 页点击图片放大 ↗
044

把历史聚合到 query 之外

把所有历史外积放进括号后,输出变成 $o_t=(\sum_{j\le t}v_jk_j^\top)q_t$。括号只由过去的 K/V 决定,可以在 query 到来前算好并保存。下图用转置方向写成 $q_t^\top(\sum_{j\le t}k_jv_j^\top)$,描述的仍是同一件事:先聚合历史,再用 query 读取。

FIG. 044 · 原报告第 44 页点击图片放大 ↗
045

用一个矩阵表示历史

把这个历史外积和记为 $S_t$,我们就得到了一个固定大小的矩阵状态。无论读过多少 token,它的形状始终是 $d_v\times d_k$;当 key 与 value 同宽时,就是下图标出的 $d\times d$。更进一步,新的求和只比旧求和多一项,因此状态更新也可以递推完成。

$$ S_t=S_{t-1}+v_tk_t^\top,\qquad o_t=S_tq_t. $$

如果实现采用 $k_tv_t^\top$ 存状态,就应相应写成 $o_t^\top=q_t^\top S_t$。两种方向都成立,但不能把一种状态方向与另一种读取公式混用。

FIG. 045 · 原报告第 45 页点击图片放大 ↗
046

不再需要保存每一条 KV

处理新 token 时,只需在旧状态上加一次外积:$S_t=S_{t-1}+v_tk_t^\top$,随后读取 $o_t=S_tq_t$。下图采用转置的状态方向,原理相同。读取发生在更新之后,因此包含当前 token 的贡献;而未来所需的历史聚合已经保存在 $S_t$ 中,不必再留下完整的 K/V 列表。

FIG. 046 · 原报告第 46 页点击图片放大 ↗
047

一步投影,一步写入

一次写入可以拆成两个层次:投影参数决定怎样从当前输入提取 key 和 value,外积则把这条关联加入状态。前者是训练得到、在序列间共享的参数,后者随当前序列不断变化。下图的单步计算展示了这一区别,也为后面“慢权重生成快权重更新”的解释做好了准备。

FIG. 047 · 原报告第 47 页点击图片放大 ↗
048

query 只负责读取

写入以后,query 从更新后的状态读取 $o_t=S_tq_t$。状态更新只依赖 K/V,读取才使用 Q,这让模型可以在同一步写入一种关联,同时查询另一种信息。Query 和 key 来自不同投影,通常不会相等。

FIG. 048 · 原报告第 48 页点击图片放大 ↗
049

固定大小,并不代表无限容量

下一时刻直接接收上一时刻留下的状态,重复写入与读取。这样跨时间传递的矩阵大小固定,解码成本不会随着历史变长而增加。不过,所有关联也共享这片有限空间;不同 key 若不正交,读取时就可能混入其他 value,固定大小并不等于无限记忆容量。

FIG. 049 · 原报告第 49 页点击图片放大 ↗
050

训练与推理可以用不同计算图

不断重复这个单步更新,就得到下图中的时间序列。每个旧 query 用完即可丢弃,后续计算只需要传入的状态。它与前面的带 mask 矩阵形式描述同一运算,因此模型可以在训练时选择更并行的计算方式,在生成时使用这种固定状态的递推。

FIG. 050 · 原报告第 50 页点击图片放大 ↗
051

线性注意力也是一个矩阵状态 RNN

从这个角度看,线性注意力就是一个带矩阵隐状态的 RNN。它能用不随序列长度增长的状态内存完成推理,具体大小仍是 $d_vd_k$。给定本步的 K/V 后,这个核心对旧状态做加法更新,对 query 做线性读取;固定维度时,处理整段序列的递推计算量也随长度线性增长。外围投影、激活、门控和前馈网络仍可以包含非线性。

FIG. 051 · 原报告第 51 页点击图片放大 ↗
052

为什么叫 Fast Weights

这个矩阵还有另一种身份:它是一个随上下文快速更新的权重。普通网络参数由训练优化器修改,$S_t$ 却每读一个 token 就改变一次,并立即用于映射 query,因此称为 Fast Weights。下图中的 Hinton 与 Plaut(1987)、Schmidhuber(1992)工作展示了这一思路的历史:慢网络生成向量,再通过外积修改快网络。

FIG. 052 · 原报告第 52 页点击图片放大 ↗
CHAPTER 04 / SLIDES 53—65

Chunkwise:从逐项到矩阵

展开块内每一步,再把它们一行行堆成矩阵。

053

两种等价形式,两种计算取舍

线性注意力现在有两种等价形式:完整矩阵形式容易并行,却要做平方级工作;递推形式只做线性级工作、保留固定状态,却要顺次处理整段序列。下表省略特征维度,将这组取舍并列出来。对生成来说,递推很合适;对训练来说,我们还想保留矩阵形式的并行优势。

FIG. 053 · 原报告第 53 页点击图片放大 ↗
054

为什么不直接用递推训练

既然递推更省计算,直接拿它训练会怎样?训练需要得到每个位置的输出,并最终传播梯度,而非读完整段后只留下最后一个状态。于是,计算量之外还要考虑每一步之间的依赖,以及中间状态如何保存。接下来的三个问题,解释了为什么“算得少”不一定“跑得快”。

FIG. 054 · 原报告第 54 页点击图片放大 ↗
055

第一个障碍:时间轴上的长队

第一个问题是时间依赖。$S_2$ 必须等待 $S_1$,$S_3$ 又等待 $S_2$,一个注意力头内部因此形成长度为 $L$ 的计算链。Batch 与 head 之间仍可以并行,但长序列训练往往只能使用较小 batch,这些独立任务未必足以让 GPU 的计算单元都忙起来。

FIG. 055 · 原报告第 55 页点击图片放大 ↗
056

第二个障碍:小运算喂不满 Tensor Cores

第二个问题是运算形状。一次外积更新、一次矩阵向量读取,主要由较小的逐元素操作与归约组成,难以像大矩阵乘法那样利用 Tensor Cores。即使总 FLOPs 更少,执行效率也可能更低。后面将多次外积合成一次矩阵乘法,就是为了把工作变成硬件更擅长的形式。

FIG. 056 · 原报告第 56 页点击图片放大 ↗
057

第三个障碍:逐步保存状态的 I/O

第三个问题是中间状态的搬运。如果训练时把每一步的完整矩阵状态都存回显存,保存量会达到 $O(Ld_vd_k)$,还要付出相应的读写成本。递归推理只保留当前状态,训练若保留全部中间状态,内存需求就不同了。分块希望同时减少依赖链长度和这些中间矩阵的存取。

FIG. 057 · 原报告第 57 页点击图片放大 ↗
058

Chunkwise:先把一个位置的计算展开

分块并行的关键,是先看清一个输出真正依赖什么。普通线性注意力每次做 $S_t=S_{t-1}+v_tk_t^\top$,然后用 $o_t=S_tq_t$ 读取。如果只盯着递推式,好像一定要先算完上一步的状态;但把状态代入输出以后,这条依赖可以换一种写法。

先固定一个长度为 $C$ 的块。$S_{[i]}=S_{iC}$ 表示进入第 $i$ 块之前的状态,包含所有更早的历史;$S_{[i]}^r$ 表示读完本块第 $r$ 个 token 后的状态。上标 $r$ 是位置编号,不是乘方。块内的 $q_r,k_r,v_r$ 分别是 query、key、value,以下省略它们共同的块下标 $[i]$。

从入口状态出发,做完本块前 $r$ 次外积写入,就得到:

$$ S_{[i]}^r=S_{[i]}+\sum_{t=1}^{r}v_tk_t^\top. $$

再用当前 query 读取。把右边的每一项分别乘上 $q_r$,并用矩阵乘法结合律重排括号:

$$ \begin{aligned} o_r&=S_{[i]}^r q_r\\ &=\left(S_{[i]}+\sum_{t=1}^{r}v_tk_t^\top\right)q_r\\ &=S_{[i]}q_r+\sum_{t=1}^{r}v_t(k_t^\top q_r). \end{aligned} $$

最后一行很关键:$k_t^\top q_r$ 是一个标量匹配分数,用它给向量 $v_t$ 加权即可。因此输出分成两部分:$S_{[i]}q_r$ 读取块外历史,求和项读取本块中 $t\le r$ 的位置。右侧没有 $S_{[i]}^{r-1}$ 了。只要入口状态已知,任何位置的输出都可以直接算,不必先构造前一个位置的完整状态。

展开推导:把块内前三个位置逐项写出来

令入口为 $S$。先看状态如何累加:

$$ \begin{aligned} S^1&=S+v_1k_1^\top,\\ S^2&=S+v_1k_1^\top+v_2k_2^\top,\\ S^3&=S+v_1k_1^\top+v_2k_2^\top+v_3k_3^\top. \end{aligned} $$

分别乘上当前 query:

$$ \begin{aligned} o_1&=Sq_1+v_1(k_1^\top q_1),\\ o_2&=Sq_2+v_1(k_1^\top q_2)+v_2(k_2^\top q_2),\\ o_3&=Sq_3+v_1(k_1^\top q_3)+v_2(k_2^\top q_3)+v_3(k_3^\top q_3). \end{aligned} $$

每行都只依赖入口 $S$ 和已知的 Q/K/V。第一行没有 $k_2,v_2$,第二行没有 $k_3,v_3$,因此并行计算不改变因果性。接下来要做的,就是把这几行共同的运算堆成矩阵。

这一组织方式沿用原博客 Part II 的 Chunkwise Parallel Form for Linear Attention,下面逐项解释它怎样变成矩阵乘法。

FIG. 058 · 原报告第 58 页点击图片放大 ↗
059

把外积求和,写成一次矩阵乘法

先处理最简单的块尾状态。读完整块后,要加进去的是 $\sum_{t=1}^C v_tk_t^\top$。每个外积都生成一个 $d_v\times d_k$ 矩阵,再把这些矩阵相加。矩阵乘法本来就包含一次求和,恰好可以把这里对 token 的求和接过去。

约定单个向量是列向量,所有 token 按行堆叠

$$ K=\begin{bmatrix}k_1^\top\\ \vdots\\k_C^\top\end{bmatrix}\in\mathbb R^{C\times d_k},\qquad V=\begin{bmatrix}v_1^\top\\ \vdots\\v_C^\top\end{bmatrix}\in\mathbb R^{C\times d_v}. $$

于是 $V^\top K$ 的形状是 $(d_v\times C)(C\times d_k)=d_v\times d_k$。看其中第 $a$ 行、第 $b$ 列,乘法会沿着中间的 $C$ 个位置求和:

$$ (V^\top K)_{ab}=\sum_{t=1}^{C}V_{ta}K_{tb} =\sum_{t=1}^{C}v_{t,a}k_{t,b}. $$

而 $v_tk_t^\top$ 的第 $(a,b)$ 个元素恰好是 $v_{t,a}k_{t,b}$。两边每个元素都相同,所以:

$$ \sum_{t=1}^{C}v_tk_t^\top=V^\top K,\qquad S_{[i+1]}=S_{[i]}+V^\top K. $$

这是第一处从“一个 token 一次外积”变成“一整个块一次矩阵乘法”。如果配图中写成 $K^\top V$,它采用的是 $d_k\times d_v$ 状态;转置整条更新式就能与这里对应。

FIG. 059 · 原报告第 59 页点击图片放大 ↗
060

先算每个块将写入什么

记第 $i$ 块的局部增量为 $\Delta S_i=V_i^\top K_i$。它只用到本块的 K/V,因此即使不知道入口状态,也能先算出来。比如有四个块,GPU 可以同时计算 $\Delta S_0,\Delta S_1,\Delta S_2,\Delta S_3$。

这些增量回答的是“这个块新增多少关联”,还不是“读到这个块时的完整记忆”。要得到完整记忆,下一步需要把更早块的增量累积起来。数学上可以把这两步分开讨论;实际内核也可以把局部乘法与边界累加融合,以减少中间结果的搬运。

FIG. 060 · 原报告第 60 页点击图片放大 ↗
061

块边界传递的是全部更早历史

块的局部增量算好后,入口状态由前缀和给出。第 0 块的入口是初始状态 $S_0$;每往后一个块,就再加入一个增量:

$$ \begin{aligned} S_{[0]}&=S_0,\\ S_{[1]}&=S_0+\Delta S_0,\\ S_{[2]}&=S_0+\Delta S_0+\Delta S_1. \end{aligned} $$

所以 $S_{[2]}$ 包含前两个块的全部累积信息。分块没有截断历史,也不只关注紧邻的上一块。它只是把逐 token 的状态交接,改为每 $C$ 个 token 交接一次。

长度为 $L$ 的序列有约 $L/C$ 个块,朴素的块间递推只需这么多次交接。对普通线性注意力,加法满足结合律,也可以用并行前缀扫描;具体实现是否采用它,要看额外搬运与并行收益是否划算。无论边界怎样算,拿到每个入口状态以后,块内输出都不再需要逐 token 传状态。

FIG. 061 · 原报告第 61 页点击图片放大 ↗
062

历史贡献为什么是 Q Sᵀ

回到单个输出:$o_r=S_{[i]}q_r+\text{本块贡献}$。先只看历史项。一个块有 $C$ 个 query,它们都在读取同一个 $S_{[i]}$。如果逐个计算,就是 $C$ 次矩阵向量乘法;把结果按行排起来,则是一次矩阵乘法。

因为输出按行存放,第 $r$ 行应当是 $(S_{[i]}q_r)^\top=q_r^\top S_{[i]}^\top$。因此:

$$ O^{\mathrm{history}}= \begin{bmatrix}(S_{[i]}q_1)^\top\\\vdots\\(S_{[i]}q_C)^\top\end{bmatrix} =QS_{[i]}^\top. $$

尺寸也正好对应:$(C\times d_k)(d_k\times d_v)=C\times d_v$。第 $r$ 行就是第 $r$ 个位置从更早历史读到的向量。

这一步只读一次共同的入口状态,就能得到所有位置的历史贡献。我们最终需要的是输出,并不需要把本块的 $C$ 个完整中间状态都保存下来。

FIG. 062 · 原报告第 62 页点击图片放大 ↗
063

从逐项加权,到整块输出的矩阵式

剩下的块内贡献是 $\sum_{t\le r}v_t(k_t^\top q_r)$。它包含两件事:先算位置 $r$ 对各个历史位置 $t$ 的匹配分数,再用分数加权 value。把所有 query 按行排成 $Q\in\mathbb R^{C\times d_k}$ 后,$QK^\top$ 的第 $(r,t)$ 项就是 $q_r^\top k_t$,与求和里的标量 $k_t^\top q_r$ 相同。

不过,$QK^\top$ 也算了未来位置。令 $M_{rt}=1$ 当 $t\le r$、否则为 0,逐元素相乘便得到因果分数矩阵 $A=(QK^\top)\odot M$。三个 token 时,它是:

$$ A=\begin{bmatrix} q_1^\top k_1&0&0\\ q_2^\top k_1&q_2^\top k_2&0\\ q_3^\top k_1&q_3^\top k_2&q_3^\top k_3 \end{bmatrix}. $$

接着算 $AV$。矩阵乘法会让 $A$ 的第 $r$ 行,给 $V$ 的各行加权。因此 $(AV)_{r,:}=\sum_{t\le r}(q_r^\top k_t)v_t^\top$,正是块内贡献转成行向量后的结果。求和上限 $t\le r$ 变成了下三角 mask,标量匹配变成了 $QK^\top$,value 加权和变成了再乘 $V$。

再把上一节的历史贡献加回来,就得到完整算法:

$$ \begin{aligned} S_{[i+1]}&=S_{[i]}+V_i^\top K_i,\\ O_i&=Q_iS_{[i]}^\top+\big((Q_iK_i^\top)\odot M\big)V_i. \end{aligned} $$

第一行把整块新增的关联交给下一块;第二行同时得到本块所有位置的输出。它们与逐 token 递推完全等价,只是计算形状不同。注意 $M$ 只负责因果屏蔽,这里并没有 softmax 或概率归一化。

普通线性注意力的分块计算:边界状态携带更早历史,块内矩阵乘法产生所有输出。

原图来自原博客 Part II。图中的两条输出路径,就是公式中的历史项与块内项。

展开推导:核对每个元素,并用三个 token 手算

对输出的第 $a$ 个分量,有:

$$ \begin{aligned} (AV)_{ra}&=\sum_{t=1}^{C}A_{rt}V_{ta}\\ &=\sum_{t=1}^{r}\left(\sum_{b=1}^{d_k}q_{r,b}k_{t,b}\right)v_{t,a}\\ &=\left[\sum_{t=1}^{r}v_t(k_t^\top q_r)\right]_a. \end{aligned} $$

中间对 $b$ 的求和计算内积,对 $t$ 的求和累加各个 value 的贡献。它们分别对应两次矩阵乘法,所有位置的输出都可以按这个规则核对。

取 $d_k=d_v=2$,入口状态为单位阵,令 $q_r=k_r$,且 $k_1=(1,0)^\top$、$k_2=(0,1)^\top$、$k_3=(1,1)^\top$,$v_1=(2,0)^\top$、$v_2=(0,3)^\top$、$v_3=(1,1)^\top$。下面可以切换位置,比较两条路径。

算一遍 / C = 3,入口状态 S = I

逐步递推 · 先写入,再读取

矩阵计算 · 取下三角矩阵的对应行

第三步的状态是 $S^3=\left[\begin{smallmatrix}4&1\\1&5\end{smallmatrix}\right]$,因此 $S^3q_3=(5,6)^\top$。矩阵路线中,第三行分数为 $(1,1,2)$,块内加权和为 $(4,5)$,再加入口历史 $(1,1)$,仍是 $(5,6)$。

FIG. 063 · 原报告第 63 页点击图片放大 ↗
064

三种算法:算多少、存什么、等几步

这张表是在比较同一个线性注意力模型的三种算法。先只看序列长度 $L$ 和块长 $C$,把头数、向量宽度当作固定常数。Compute 问“总共做多少运算”,Memory 问“计算时要保留哪些数据”,Steps 问“沿序列有多少轮必须等前一轮”。后者不是 GPU 指令数,也不代表实际耗时。

完全并行:所有位置一起算

每个 query 与它前面的 key 配对,再汇总 value。第 1 个位置看 1 个,第 2 个看 2 个,直到第 $L$ 个看 $L$ 个,所以一共有 $1+2+\cdots+L=L(L+1)/2$ 对,Compute 是 $O(L^2)$。各位置不必等待前一个位置的输出,可以一起交给矩阵运算,因此表中 Steps 记为 $O(1)$

要保留的是整段 Q/K/V 与输出,每个位置占固定数量的数,合计随 $L$ 增长。表中的 Memory $O(L)$ 还隐含了一个条件:分数矩阵分小片计算、用完就丢,不把全部 $L\times L$ 分数留在显存中。 如果直接保存完整 $QK^\top$,那一项就是 $O(L^2)$。

完全递推:每次只处理一个位置

每来一个 token,先更新 $S\leftarrow S+vk^\top$,再读取 $o=Sq$。状态矩阵的大小固定,所以每个位置的工作量固定,处理 $L$ 个位置就是 Compute $O(L)$。但下一步需要上一步的新状态,只能沿序列走 $L$ 轮,因此 Steps 是 $O(L)$

只看前向递推中正在使用的数据,要保留的是一个状态 S,加当前 q/k/v 和输出。旧 S 可以被新 S 覆盖,历史 token 已经汇入状态,不必全部留下,所以这部分 Memory 是 $O(1)$。“1”表示不随序列变长,绝不是只存一个数:S 本身仍是一个 $d_v\times d_k$ 矩阵。

Chunkwise:每次一起处理 C 个位置

每块内部约有 $C^2$ 对交互,全序列有 $L/C$ 块,所以局部计算为 $(L/C)\times C^2=LC$。再加上每个 token 固定成本的状态写入和历史读取,Compute 简写为 $O(LC)$。块内可以一起算,块与块之间交接状态,因此 Steps 是 $O(L/C)$

若采用逐块前向的方式,此刻要保留的是当前块的 C 组 Q/K/V、输出,以及一个入口状态 S。算完这块就把输出交出去,用块尾状态接着算下一块,因此随块长增长的工作数据是 $O(C)$。这同样假设块内分数分小片处理;显式保留完整 $C\times C$ 分数块会额外占 $O(C^2)$。

以 L = 12、C = 4 为例完全并行Chunkwise完全递推
主要怎么计算整段 78 对因果交互3 块各 10 对,共 30 对,另加状态更新与读取12 次固定大小的状态更新与读取
同时处理的序列数据12 个位置的 Q/K/V 与输出当前 4 个位置的 Q/K/V 与输出,加一个 S当前 1 个位置的 q/k/v 与输出,加一个 S
必须等待的序列轮次1 轮并行输出3 轮块间交接12 轮逐位置递推

于是,$C$ 变大意味着一次处理更多位置、块内配对更多,但块间等待更少。$C=1$ 对应逐位置递推,$C=L$ 对应整段并行。

Memory 最后还要分清:工作数据与训练时保存的数据

上面的 Memory 行可以按流式前向、当前处理范围内的数据来理解,不能直接当作三种训练实现的总显存比较。训练如果统一把整段输入、输出和梯度保留在显存中,三种方法都有随 $L$ 增长的部分;还需要考虑为了反向传播额外保存了什么。

例如,朴素递推若保存每一步 S,就有 $L$ 个状态矩阵。后面 FLA 的两阶段算法为并行计算输出,只保存每隔 C 步的边界 S,约有 $L/C$ 个状态矩阵。这份边界检查点占用随 $L/C$ 增长,不能塞进表中的 $O(C)$。 它与“一个块当下需要多少数据”是两笔不同的存储。反向也可以用重算减少保存量,具体由实现决定。

展开计算:把向量维度放回来

令 $d_k=d_v=d$。一个状态有 $d^2$ 个数,一个 token 的 Q/K/V 与输出合起来是 $O(d)$ 个数。因此,前面逐块前向的工作数据约为 $O(Cd+d^2)$,逐位置前向为 $O(d+d^2)$;不保存完整分数的整段并行为 $O(Ld)$,另外还需内核的小片临时空间。这里把小片尺寸视为固定;并发处理多个块时,还要合计它们各自的工作空间。

计算也可以按两类操作记:每个位置写状态、读状态各要 $O(d^2)$,全序列是 $O(Ld^2)$;每块内部的打分与 value 加权是 $O(C^2d)$,乘块数后为 $O(LCd)$。所以分块的完整算术量是 $O(Ld^2+LCd)$。固定 d 后,表里才把它简写为 $O(LC)$。

训练检查点另算:每个状态 $d^2$ 个数,逐步保存为 $Ld^2$,只存边界约为 $(L/C)d^2$。例如 $L=4096,C=64,d=128$,每个 FP32 状态占 64 KiB;约 64 个边界占 4 MiB,而 4096 个逐步状态占 256 MiB。这只是单层单头的状态保存量,不包含 Q/K/V、梯度和其他网络模块。

因此,大 C 会增加当前块的数据与局部计算,却减少边界检查点和交接次数。这也是为什么选择块长不能只看表中的一个复杂度。存储与重算的讨论可参照 GLA 论文 §2 的 Memory considerations

FIG. 064 · 原报告第 64 页点击图片放大 ↗
065

同一个模型,三种执行方式

令 $C=1$,分块就退化为逐 token 递推;令 $C=L$,则回到完整并行形式。两者之间是一系列计算方式,描述的模型始终相同。块外历史通过边界状态进入输出,块内交互通过因果矩阵计算,因此分块并没有截断长程信息,只是重新安排了它的计算。

FIG. 065 · 原报告第 65 页点击图片放大 ↗
CHAPTER 05 / SLIDES 66—79

让算法真正跑在 GPU 上

数据搬运、在线 softmax 与 FlashLinearAttention。

066

为什么线性算法也会更慢

线性复杂度的算法,也可能在短序列上输给平方复杂度的优化实现。下图中,FlashAttention-2 的青色曲线在短序列段低于纯 PyTorch 线性注意力的红线,约到 $2^{11}$ 附近才交叉。纵轴是对数时间,越低越快。随着长度增加,渐近复杂度开始起作用;在更短的区间,实现效率却足以改变排序。

FIG. 066 · 原报告第 66 页点击图片放大 ↗
067

速度之外,还有质量差距

质量上也有一笔账要算。先认识后面会反复出现的两个指标:

PPL(Perplexity,困惑度)衡量模型预测真实文本中“下一个 token”的能力,越低越好。模型给真实出现的 token 分配的概率越高,PPL 就越低。它不是错误率,也不是答题正确率;PPL 为 16.9 不表示有 16.9% 的预测错误。这里报告的是 WikiText 文本上的语言建模结果。

LM Eval(Language Model Evaluation)是这组实验里一篮子语言理解与常识任务的平均得分,越高越好。它包括 LAMBADA、PIQA、HellaSwag、WinoGrande、ARC-easy、ARC-challenge 等任务,考察补全文本、选择合理续写或回答常识问题。50.9 是按百分数口径报告的任务平均分,不代表所有语言能力都达到了 50.9%,也不是让另一个 LLM 主观打分。这里 slide 写的是 LM Eval;不同论文可能采用不同任务集合,不能只凭列名跨论文比较。

算一个最小的 PPL 例子

设一段文本有 N 个待预测 token,模型给第 t 个真实 token 的概率为 $p_t$,则:

$$ \mathrm{PPL}=\exp\left(-\frac1N\sum_{t=1}^N\log p_t\right). $$

两个位置的真实 token 概率若是 $1/2$ 与 $1/8$,几何平均概率是 $\sqrt{(1/2)(1/8)}=1/4$,PPL 就是 4。若两个概率都提高到 $1/2$,PPL 就降到 2。比较时要使用相同测试文本、分词方式与评测口径,数值才有意义。

这些指标的任务组成见原博客 Part III 的实验设置。在下表的实验中,softmax Attention 的 PPL 为 16.9、LM Eval 为 50.9,带固定 decay 的 RetNet 则为 18.6、48.9。PPL 越低越好,LM Eval 越高越好,因此这里两项都存在差距。RetNet 的 $\gamma$ 已经让旧状态逐步衰减,但要进一步接近 Attention,可能还需要更灵活的记忆管理。

FIG. 067 · 原报告第 67 页点击图片放大 ↗
068

从分块公式走向 GLA

GLA 将速度与质量放在同一项工作里考虑。下图展示的论文《Gated Linear Attention Transformers with Hardware-Efficient Training》,一方面设计高效的线性注意力实现,另一方面引入由输入决定的遗忘门。前者让现有计算跑得更快,后者则改变模型处理历史的方式。

FIG. 068 · 原报告第 68 页点击图片放大 ↗
069

一条路线处理实现,一条路线处理记忆

这两项改进可以分别理解。FlashLinearAttention 关注计算与数据搬运怎样安排,尽量发挥 GPU 的吞吐;Gated Linear Attention 关注哪些旧信息应该留下,让有限状态不再无条件累积全部历史。它们相互配合,但各自需要速度实验和质量实验来检验。

FIG. 069 · 原报告第 69 页点击图片放大 ↗
070

先进入 GPU 看看

先解决执行效率。Chunkwise 已经把公式整理成边界递推与块内矩阵运算,剩下的是让这些运算真正适合 GPU:哪些数据值得留在片上,哪些阶段可以融合,又有多少独立工作能同时执行?理解这些问题之后,FlashLinearAttention 的实现选择就会自然很多。

FIG. 070 · 原报告第 70 页点击图片放大 ↗
071

GPU 既要算得快,也要拿得到数据

先把 GPU 看成许多个可以同时干活的计算单元 SM。整张卡的大容量显存叫 HBM;每个 SM 附近还有小得多、访问更快的寄存器和共享内存。矩阵通常从 HBM 搬到计算单元附近的小片存储,再参与运算。下图的 HBM、L2、L1/shared memory 和 registers,就是这条数据路径上的不同层级。

问题在于:计算能力增长得很快,数据未必来得及供应。若某个中间矩阵刚写回 HBM,下一个操作又把它读回来,GPU 就为同一份数据付了两次搬运成本。把已经拿到的数据多用几次,比反复搬同一份数据更划算。 分块让一小片数据可以放在计算单元附近,融合则让前一步的临时结果直接交给后一步。

具体容量和带宽取决于 GPU,图中的数值只用于展示量级。这里需要记住的性质是:HBM 容量大,片上存储容量小;后者适合保存正在反复使用的小片数据,而非整个长序列。

FIG. 071 · 原报告第 71 页点击图片放大 ↗
072

融合:让中间结果直接交给下一步

以一块线性注意力为例,先算局部增量 $\Delta S=V^\top K$,再做 $S\leftarrow S+\Delta S$。如果用两个独立的 GPU 内核,第一个可能把 $\Delta S$ 写回 HBM,第二个再从 HBM 读出它来相加。把两步融合后,矩阵乘法得到的临时结果可以直接累加进当前 S,省去 $\Delta S$ 的这次写回和读回。

这里的 kernel(内核)可以理解为一次提交给 GPU 的计算程序。融合既可能减少程序启动次数,也可能减少中间数据往返显存。它并不意味着最终结果全都不用保存:后续任务要读取的边界状态和输出,仍然需要写回。

融合也受片上容量限制。如果一个程序想同时留住太多数据,就可能占满寄存器、减少同时运行的任务,甚至迫使数据溢出到更慢的存储。因此目标是保留最值得复用的中间量,而非把所有计算塞进一个程序。

FIG. 072 · 原报告第 72 页点击图片放大 ↗
073

并行:让多个 SM 同时有事可做

纯递推是一条依赖链:同一条序列的 $S_2$ 要等 $S_1$,$S_3$ 要等 $S_2$。即使 GPU 有很多 SM,也不能把这些依赖随意拆开。不同样本和不同注意力头可以同时算,但长序列训练经常只能放较小的 batch,独立工作仍可能不够多。

分块提供了另一批独立任务。先算好各块的入口状态,再让每块各自计算输出,这些输出任务就可以分配给不同 SM。比如一条序列有 64 块,一个头在输出阶段便有 64 个块任务可调度;并不要求一次全部同时运行,但 GPU 有更多机会填满空闲计算单元。

所以后面的 FLA 会分成两个阶段:第一阶段保留必要的块间顺序,第二阶段沿序列块展开并行。是否高效既取决于单个任务有多快,也取决于有多少任务能同时执行。

FIG. 073 · 原报告第 73 页点击图片放大 ↗
074

矩阵乘法:让一批 token 共同使用 Tensor Cores

GPU 上的 Tensor Cores 专门擅长小块矩阵乘加。逐 token 写入 $vk^\top$ 时,两向量的外积沿归约维度只有一项,不容易发挥这类单元的吞吐;把 C 次写入合成 $V^\top K$,就变成有 C 项参与累加的矩阵乘法。

例如 $C=64,d_k=d_v=128$,$V^\top K$ 是 $(128\times64)(64\times128)$ 的乘法。计算同一片 $128\times128$ 状态时,可以分小片搬入 K/V,并在片上重复利用这些数据,累计多个 token 的贡献。历史读取也从很多次矩阵向量乘法,变成 $QS^\top$:同一片 S 可以服务一批 query。

这解释了分块为何即使增加了一些局部位置配对,也可能比纯递推更快:它把更多工作变成适合硬件的矩阵运算,还提高了数据复用率。实际实现需要选择合适的小片尺寸、数据布局与数值精度;“公式写成矩阵乘法”是起点,内核要按这些要求组织数据,Tensor Cores 才能发挥作用。GLA 论文 §2:硬件与计算分析

FIG. 074 · 原报告第 74 页点击图片放大 ↗
075

FlashAttention:分块累加分子与分母

先看一个 query 的 softmax 输出。令分数 $s_j=q^\top k_j/\sqrt{d_k}$,则:

$$ o=\frac{\sum_j e^{s_j}v_j}{\sum_j e^{s_j}}. $$

这其实是一个加权平均。为了得到最终结果,我们只需要分子这个向量和、分母这个标量和。普通实现常常先保存所有分数,再保存 softmax 概率,最后乘 V;FlashAttention 则一块块读 K/V,算出一块的贡献后马上累加,不把完整的注意力矩阵写回显存。

先不管数值稳定,看看为什么能分块

假设三个位置的未归一化权重 $e^{s_j}$ 是 $1,2,4$,对应的标量 value 是 $2,4,8$。直接计算的结果为 $(1\cdot2+2\cdot4+4\cdot8)/(1+2+4)=42/7=6$。

现在把前两个位置放在第一块,最后一个位置放在第二块:

处理到哪里这一块的分子贡献这一块的分母贡献
第一块$1\cdot2+2\cdot4=10$$1+2=3$
第二块$4\cdot8=32$$4$
累计后$42$$7$

最终仍是 6。读完第一块时,我们不必知道整行的分母,只要保留尚未归一化的两笔和。不能把每块各自的 softmax 输出直接相加:第一块的平均值是 $10/3$,第二块是 8,它们还必须按各自的权重总量 3 和 4 合并。

实际计算多保留一个“最大分数”

直接算 $e^{s_j}$ 可能溢出。softmax 通常先给所有分数减去同一个最大值 $m$;分子分母同时乘 $e^{-m}$,比值不变。分块时,新块可能带来更大的最大值,因此要把之前累积的分子、分母一同缩小到新的基准。做完这次尺度对齐,就仍然只是两笔和相加。

所以 FlashAttention 为每个 query 保留三个量:最大分数 $m$、经过缩放的分母 $\ell$、经过缩放的分子 $z$。最终输出为 $z/\ell$。下面的推导只是把“换统一基准,再累加”写成公式。

展开推导:在线 softmax 的三个更新式

假设已处理集合的最大值为 $m$,已有 $\ell=\sum_{\mathrm{old}}e^{s_j-m}$、$z=\sum_{\mathrm{old}}e^{s_j-m}v_j$。新块到来后,整个已见集合的新最大值为:

$$ m'=\max\left(m,\max_{j\in\mathrm{new}}s_j\right). $$

旧项原来减的是 $m$,现在应当减 $m'$。因为 $e^{s_j-m'}=e^{m-m'}e^{s_j-m}$,所以所有旧项都只需乘同一个系数 $e^{m-m'}$。分母和分子分别更新为:

$$ \ell'=e^{m-m'}\ell+\sum_{j\in\mathrm{new}}e^{s_j-m'}, $$
$$ z'=e^{m-m'}z+\sum_{j\in\mathrm{new}}e^{s_j-m'}v_j. $$

这不是近似:旧项和新项都使用同一个基准,恰好等于整个已见集合的指数和。实际浮点计算可能因归约顺序产生舍入差异。第一块可以直接初始化这三个量;因果 mask 禁止的分数按 $-\infty$ 处理,全被 mask 的块应跳过或专门处理。

前面的例子,分数对应 $0,\log2,\log4$。第一块使用 $m=\log2$,因此 $\ell=1/2+1=1.5$、$z=(1/2)\cdot2+1\cdot4=5$。新块令最大值变成 $\log4$,旧累计都乘 $1/2$,得到 $\ell'=0.75+1=1.75$、$z'=2.5+8=10.5$。结果 $10.5/1.75=6$,与直接计算一致。

为什么在 GPU 上更快

实际内核同时处理一组 query,把它们的累计量留在靠近计算单元的小容量快速存储中,依次读取 K/V 小块。每次算出分数块、处理 mask、更新累计量,用完就丢弃临时分数。这样避免了完整 $L\times L$ 分数与概率矩阵的显存往返;反向传播也可以重新计算这些局部量。

FlashAttention 仍然计算合法 query–key 位置对,核心计算量仍为 $O(L^2d)$;它主要省掉的是大中间矩阵的存储与搬运。KV cache 也仍然存在。FlashAttention-2 进一步改进任务划分,让不同 query 块更好地并行、减少非矩阵运算和线程间通信。来源:FlashAttentionFlashAttention-2

FIG. 075 · 原报告第 75 页点击图片放大 ↗
076

FLA 为什么快:从逐 token 递推到整块计算

FlashLinearAttention 的“Flash”来自对 GPU 执行方式的优化:少搬中间数据,把小运算合成高速矩阵乘法,再让更多计算单元同时有活干。 线性注意力的运算量随长度线性增长,并不意味着直接写出递推公式就能在 GPU 上跑得快。

传统逐 token 实现怎么做

先看最直接的递推写法。每来一个 token,取出它的 q、k、v,更新状态,再读出输出:

$$ \begin{aligned} \Delta S_t&=v_tk_t^\top,\\ S_t&=S_{t-1}+\Delta S_t,\\ o_t&=S_tq_t. \end{aligned} $$

处理 4096 个 token,就沿这条链重复 4096 次。每一步的外积和矩阵乘向量都比较小,而且下一步更新要等上一步状态。GPU 虽有很多计算单元,一条串行状态链却很难让它们都忙起来,尤其在 batch 和 head 数较少时。

如果直接用独立的张量算子拼出这三行,还可能反复启动内核,把 $\Delta S_t$ 或更新后的状态写进大显存 HBM,再由下一算子读回来。算得少,却花了很多时间等待和搬运,这就是朴素实现的瓶颈。已有的融合递推内核可以把状态留在片上,缓解搬运与启动开销;但逐 token 的依赖仍在,也没有自动把小运算变成适合 Tensor Cores 的矩阵乘法。

FLA 第一步:把 64 次外积合成一次矩阵乘法

现在把同一段序列切成 64 个块,每块 64 个 token。先只计算各块的边界状态,暂时不计算每个 token 的输出。把本块的 k、v 按行堆起来:

$$ V_i^\top K_i=\sum_{t=1}^{64}v_i^t k_i^{t\top}. $$

因此,原本连续 64 次“做外积、加进状态”,可以合成:

$$ S_{[i+1]}=S_{[i]}+V_i^\top K_i. $$

这里先沿用正文的编号:$S_{[i]}$ 是本块入口。比如 key 和 value 都是 128 维,那么 $V_i^\top K_i$ 就是一次 $(128\times64)(64\times128)$ 的矩阵乘法。外积求和这部分的算术工作量基本没变,但执行它的方式变了:GPU 可以使用专门加速矩阵乘加的 Tensor Cores,并让读入的数据参与多次运算。

第二个收益:临时结果就近使用,状态只在边界保存

GPU 的 HBM 容量大,但访问它比访问片上的寄存器、共享内存慢。FLA 按小片读入 K/V,让当前状态的相应小片留在片上,将矩阵乘法的临时结果直接累加进去。这样 $\Delta S$ 不必先单独写回 HBM、再读回来相加。状态可以按特征维度分片,并不要求每个线程保存整个矩阵。

训练时,若为后续计算保存每一步 S,4096 个 token 就需要约 4096 份状态;FLA 只保存约 64 份块边界状态,块内需要的中间量可以重算。这里减少的是状态检查点这一项,不是总显存必然减少 64 倍。

FLA 仍然会把边界 S 写到 HBM,因为下一阶段的不同块任务需要独立读取它们。配图中的 Store to HBM 指的就是这些检查点:保存边界的目的,是让接下来的输出计算可以分块并行。

图中把处理完第 i 块后的状态记为 $S_{[i]}$,所以 $K_{[i]},V_{[i]}$ 把 $S_{[i-1]}$ 更新到 $S_{[i]}$;这是以块尾命名,和正文的入口记号相差一个块的标签。图里的 Sequential 表示同一状态链仍按块先后执行。这里主要依靠 batch、head,以及实现中的状态分片提供并行;沿序列块大幅增加独立任务,要看下一步。

FIG. 076 · 原报告第 76 页点击图片放大 ↗
077

FlashLinearAttention 第二步:把各块输出交给不同任务

入口状态都准备好后,一个块不再需要等待前一块的输出。它只需读取自己的入口 S 和本块 Q/K/V,然后计算:

$$ \begin{aligned} O^{\rm inter}&=QS^\top,\\ O^{\rm intra}&=\big((QK^\top)\odot M\big)V,\\ O&=O^{\rm inter}+O^{\rm intra}. \end{aligned} $$

第一行把一批 query 作用到相同入口历史;第二行只处理当前块内部的因果交互。图中 $O^{\rm inter}_{[i+1]}$ 读取前一块留下的 $S_{[i]}$,$O^{\rm intra}_{[i+1]}$ 则来自本块 Q/K/V,正好是这两条路径。

让数据在片上多用几次

处理输出时,内核读入需要的小片数据:S 的一个小片可用于多个 query,K/V 的一个小片也可用于多个位置的打分与汇总。算出的局部分数随即用于加权 V,再累加进输出,不必把完整分数块保存到 HBM。临时结果尽可能在片上传递,最终输出写回显存。历史读取与局部读取都能组织为矩阵乘法,使大部分工作用上 Tensor Cores。

让各块同时算,而不只让一个块算得快

继续看 4096 个 token、每块 64 个的例子。第一阶段已经准备好各块的入口状态,输出阶段就有 64 个互不依赖的块任务。第 20 块知道自己的入口 S,就能直接算这一块的 64 个输出,不用等第 19 块的输出完成。GPU 可以把这些任务调度到多个 SM,也就是不同的计算单元,再叠加 batch、head 与特征分片的并行。

这正是保存边界检查点的收益:逐 token 递推把状态更新与输出绑在一条时间链上;FLA 先准备边界状态,再把大量输出计算从这条链上解开。输出阶段获得了沿序列的并行,边界状态的传递仍有依赖。 64 个块提供的是 64 个可调度任务,不代表实际运行必然快 64 倍。

对照传统做法,Flash 快在这三处

传统直接实现的瓶颈FLA 怎么组织计算为什么更快
每个 token 做小外积、矩阵乘向量,难以充分使用矩阵计算单元将一批 token 合成 $V^\top K$、$QS^\top$ 等矩阵乘法用上 Tensor Cores 的高吞吐,也增加数据复用
独立算子传递临时结果;训练时可能保存每一步状态分片加载、融合运算,只保留块边界状态,块内中间量按需重算减少中间结果与状态的显存读写
状态与输出沿 token 串行推进,独立任务不足先准备边界,再把各块输出交给独立任务让更多 SM 同时工作

所以,“Flash”的收益不是只用一个更小的复杂度概括就够了。FLA 甚至增加了块内 query–key 配对的运算,却可能因为同样的数据少搬几次、主要运算交给高速矩阵单元、更多任务同时推进而更快。分块为这三件事提供了共同的组织方式;具体能快多少,还取决于序列长度、块长、batch/head 数与硬件。

这个算法并没有消除所有搬运:第一阶段读过的 K/V,输出阶段通常还会读;边界检查点也要写回再读入。它用这些有目的的读写,换掉更频繁的状态往返,并提高矩阵吞吐与并行度。因此要比较实际耗时,不能只看 FLOPs,也不能只看总共读了几遍 K/V。

FlashAttention 同样会分块、融合并复用数据,但它计算 softmax 的全局历史交互;这里的 FLA 把跨块历史汇入固定形状的矩阵状态。相同的硬件原则,服务于不同的注意力计算。以上对应本报告中的两阶段算法;代码版本可以采用不同的融合与调度策略。GLA 论文的硬件分析与算法

FIG. 077 · 原报告第 77 页点击图片放大 ↗
078

让理论优势变成实际加速

经过这样的优化,同一种线性注意力可以明显加快。下图中绿色 FLA 曲线低于红色纯 PyTorch 实现,在长度 $2^{15}$ 处,标出的差距约为四倍;青色则是 FlashAttention-2,提供另一种机制的参照。纵轴是对数时间,越低越快。这里的 4× 比较的是 FLA 与纯 PyTorch 线性注意力,衡量的是实现带来的收益。

FIG. 078 · 原报告第 78 页点击图片放大 ↗

FLA 在长序列段增长较缓,说明分块与硬件优化让长度优势在这组测量中兑现。与 FlashAttention-2 比较时,则还涉及注意力机制的差异。这里展示的是运行时间,整模型训练还包含投影等其他工作,后面的吞吐实验会进一步检验实际收益。

079

从算法走到 Flash Linear Attention 实现

这些算法也被整理成了可使用的实现。下图展示 Flash Linear Attention 仓库的 README,其中汇集多种模型的高效 Triton 实现,并将论文、原始代码与 FLA 实现放在一起。研究一种更新规则时,可以先从论文核对定义,再沿实现检查它如何落到内核中。这是报告当时的仓库截图,具体支持范围会随项目发展变化。

FIG. 079 · 原报告第 79 页点击图片放大 ↗
CHAPTER 06 / SLIDES 80—96

GLA:学会保留与遗忘

门控如何作用于记忆,又如何进入分块计算。

080

让当前输入决定遗忘

执行效率改善之后,再来看记忆本身。固定 decay 让所有输入遵循同一套衰减规律,但不同内容需要的保留时间未必相同。GLA 引入输入依赖的 forget gate,让当前 token 参与决定旧状态保留多少,这种记忆策略可以和模型一起训练。

FIG. 080 · 原报告第 80 页点击图片放大 ↗
081

从无条件累加到选择性遗忘

先回到最简单的更新:$S_t=S_{t-1}+v_tk_t^\top$。旧状态完整保留,本次关联再叠加进去,模型没有显式决定哪些旧内容该淡出。下图采用转置方向写出同一个公式。要加入遗忘,最直接的想法就是在传递 $S_{t-1}$ 时乘上一个由输入生成的门。

FIG. 081 · 原报告第 81 页点击图片放大 ↗
082

门控 RNN 提供的直觉

门控 RNN 已经提供了一个直觉。以下图的 GRU 为例,更新门 $z_t$ 通过 $h_t=(1-z_t)\odot h_{t-1}+z_t\odot\tilde h_t$ 分配旧状态与候选状态的比例;重置门 $r_t$ 则参与候选状态 $\tilde h_t$ 的构造。图来自 colah 的《Understanding LSTMs》。GLA 借用的是这种由内容决定保留程度的思想,但要将它用在矩阵状态上,并保持高效训练的可能性。

FIG. 082 · 原报告第 82 页点击图片放大 ↗
083

GLA 怎样给矩阵状态加上遗忘门

这里的门(gate)就是模型算出来的保留比例。例如,一份旧数值是 10,门为 0.8,先乘一下就剩 8;下一步的门为 0.5,再乘一下就剩 4。接近 1 表示大多保留,接近 0 表示大多遗忘,不一定是非开即关的 0/1 开关。GLA 会根据当前 token 的表示生成这些比例,并在训练中学会何时该保留多少。

一个状态矩阵有多个特征坐标,因此 GLA 一次生成一列数字 $\alpha_t$,为不同 key 特征提供不同的保留率。这里 t 是 token 位置,$\alpha_{t,b}$ 是第 t 步、第 b 个 key 特征的门;它没有直接给“第几个历史 token”单独贴上保留标签。

GLA 在写入当前外积之前,先缩小旧状态的一部分。本文的状态 $S\in\mathbb R^{d_v\times d_k}$ 有 $d_v$ 行、$d_k$ 列;每一列对应 key 空间的一个特征坐标。门向量 $\alpha_t\in(0,1)^{d_k}$ 为每一列提供一个保留率。

先用一个 $2\times3$ 状态看清矩阵形态。若 $\alpha_t=(a,b,c)^\top$,则:

$$ \begin{bmatrix}s_{11}&s_{12}&s_{13}\\s_{21}&s_{22}&s_{23}\end{bmatrix} \underbrace{\begin{bmatrix}a&0&0\\0&b&0\\0&0&c\end{bmatrix}}_{D_t=\operatorname{Diag}(\alpha_t)} =\begin{bmatrix}as_{11}&bs_{12}&cs_{13}\\as_{21}&bs_{22}&cs_{23}\end{bmatrix}. $$

右乘对角矩阵 $D_t$,就是把第一列乘 $a$、第二列乘 $b$、第三列乘 $c$,列之间不相加、不混合。比如 $\alpha_t=(1/2,1,1/4)^\top$,第一列保留一半,第二列不变,第三列只保留四分之一。

遗忘后,再加当前关联:

$$ S_t=S_{t-1}D_t+v_tk_t^\top. $$

逐元素看,就是 $S_{t,ab}=\alpha_{t,b}S_{t-1,ab}+v_{t,a}k_{t,b}$。第一项修改已有记忆,第二项写入新关联。因此本次写入不乘本次遗忘门,它只会在后续步骤中受到遗忘。这一点将决定后面的门积从哪里开始。

展开推导:对角乘法、广播门矩阵与门的生成

对角矩阵只有第 $(b,b)$ 项非零,所以:

$$ (S_{t-1}D_t)_{ab}=\sum_{c=1}^{d_k}S_{t-1,ac}(D_t)_{cb} =S_{t-1,ab}\alpha_{t,b}. $$

也可以把 $\alpha_t^\top$ 复制到每一行,得到 $G_t=\mathbf1_{d_v}\alpha_t^\top\in\mathbb R^{d_v\times d_k}$,再逐元素乘:$S_{t-1}D_t=S_{t-1}\odot G_t$。代码里的广播乘法便是在做这件事,不必真的构造一个稠密对角矩阵。

配图采用转置状态 $\widetilde S_t=S_t^\top\in\mathbb R^{d_k\times d_v}$。转置整条公式得 $\widetilde S_t=D_t\widetilde S_{t-1}+k_tv_t^\top$,此时门缩放的是行,广播门矩阵写成 $\alpha_t\mathbf1_{d_v}^\top$。两种约定控制的都是 key 维度。

门由当前隐向量 $x_t\in\mathbb R^{d_{\rm model}}$ 生成。用列向量写,可记为 $z_t=W_2W_1x_t+b$,其中 $W_1$ 将输入压到较小的中间维度,$W_2$ 再投影到 $d_k$ 维;然后逐元素计算 $\alpha_t=\sigma(z_t)^{1/\tau}$。若 $\tau>1$,例如 $0.25^{1/2}=0.5$,门被推近 1,保留时间变长。门控制特征坐标的保留率,并不直接指定某个历史 token 应当被删除。

来源:GLA 论文 §3.1

FIG. 083 · 原报告第 83 页点击图片放大 ↗
084

GLA 并行:Q⊙B 和 K⊘B 是怎样一步步推出来的

先不记 $Q\odot B$ 和 $K\oslash B$。我们从一次普通读取开始,看遗忘门怎样改变它,最后再把同一个计算整理成矩阵乘法。这两个式子是把已有乘除重新分组得到的,不是另外添加的两条模型规则。

第一步:没有门时,一笔写入怎样参与输出

先把 key、query、value 都简化为一个数字,状态 s 也只有一个数字。从 $s_0=0$ 开始,普通线性注意力每一步做 $s_t=s_{t-1}+v_tk_t$,再用 $o_t=s_tq_t$ 读取。

例如,第 1 个位置写入了 $v_1k_1$。到第 3 个位置用 $q_3$ 读取时,这笔写入对输出的贡献是:

$$ (v_1k_1)q_3=v_1\,(q_3k_1). $$

括号里的 $q_3k_1$ 是一个匹配分数,再用这个分数乘 $v_1$。下面只追踪这笔写入,暂时不管其他位置写了什么。

第二步:加入门后,这笔写入会经历哪些衰减

GLA 每一步先给旧状态乘一个保留率 $\alpha_t$,再加入新写入:

$$ s_t=\alpha_ts_{t-1}+v_tk_t. $$

第 1 步新写入 $v_1k_1$,此时它还不乘 $\alpha_1$,因为 $\alpha_1$ 作用于写入前的旧状态。第 2 步它已经属于旧状态,所以乘 $\alpha_2$;第 3 步再乘 $\alpha_3$。这笔内容的变化是:

$$ v_1k_1 \ \longrightarrow\ \alpha_2v_1k_1 \ \longrightarrow\ \alpha_3\alpha_2v_1k_1. $$

到第 3 步用 query 读取,它对输出的贡献就是:

$$ q_3\alpha_3\alpha_2v_1k_1 =v_1\,\underbrace{(q_3k_1\alpha_2\alpha_3)}_{\text{加入遗忘后的匹配分数}}. $$

所以,门改变的是匹配分数:原来的 $q_3k_1$,现在还要乘从写入之后到读取时刻之间的保留率 $\alpha_2\alpha_3$。

第三步:用累计乘积,把“中间这一段门”写成一个比值

如果每次读取都重新乘一遍中间所有门,会很不方便。先为每个位置记下“从起点到这里的门全部乘起来”的结果:

$$ b_1=\alpha_1,\qquad b_2=\alpha_1\alpha_2,\qquad b_3=\alpha_1\alpha_2\alpha_3. $$

这就是门的前缀积。“前缀”只是从起点到当前位置的这一段;b 是累计结果,α 才是每一步生成的门。要取第 1 步写入到第 3 步读取之间的门,只需:

$$ \frac{b_3}{b_1} =\frac{\alpha_1\alpha_2\alpha_3}{\alpha_1} =\alpha_2\alpha_3. $$

分母消掉了写入前已经发生的门。比如 $\alpha_1=0.5,\alpha_2=0.2,\alpha_3=0.5$,则 $b_1=0.5,b_3=0.05$,比值是 $0.05/0.5=0.1$,恰好等于后两步的 $0.2\times0.5$。

因此,刚才的匹配分数可以改写为:

$$ q_3k_1\alpha_2\alpha_3 =q_3k_1\frac{b_3}{b_1}. $$

第四步:这一步重新分组,才出现“query 乘、key 除”

目前式子里的 q、k、b 都只是数字,可以交换相乘的顺序。因此:

$$ \underbrace{q_3k_1\frac{b_3}{b_1}}_{\text{原来的分数乘路径保留率}} =\underbrace{(q_3b_3)}_{\text{只与读取位置 3 有关}} \underbrace{\left(\frac{k_1}{b_1}\right)}_{\text{只与写入位置 1 有关}}. $$

query 乘 $b_3$,是因为路径比值的分子属于读取位置;key 除 $b_1$,是因为分母属于写入位置。 我们只是把同一组乘除分到了两边,结果没有改变。

继续用数字验证,取 $q_3=2,k_1=4$。原来的算法是 $2\times4\times0.2\times0.5=0.8$。重新分组后:

$$ (2\times0.05)\times(4/0.5)=0.1\times8=0.8. $$

除法使 key 这边的数变大,但它会与 query 那边的小数一起使用,两者合起来仍是正确的遗忘量。不能只把 $k_1/b_1$ 单独看成“把记忆增强了”。

这个分组还有一个计算上的好处:$k_1/b_1$ 对后面所有 query 都相同,可以提前算好;$q_3b_3$ 对它要读取的所有历史 key 都相同,也只需算一次。

第五步:多个特征,就在每个特征上做同样的分组

真正的 q 和 k 各有 $d_k$ 个数字,匹配分数是对应特征相乘后求和。GLA 每个 key 特征也有自己的门,所以刚才的推导要逐特征进行,再把结果相加

先只看两个特征。用 $q_{r,a}$ 表示第 r 个位置、第 a 个特征的 query 数值;$b_{r,a}$ 是这个特征累计到 r 的门积。第 r 个 query 读取第 t 个 key,分数为:

$$ q_{r,1}k_{t,1}\frac{b_{r,1}}{b_{t,1}} +q_{r,2}k_{t,2}\frac{b_{r,2}}{b_{t,2}}. $$

第一项是特征 1 的贡献,第二项是特征 2 的贡献。对每一项重复刚才的重新分组:

$$ (q_{r,1}b_{r,1})\left(\frac{k_{t,1}}{b_{t,1}}\right) +(q_{r,2}b_{r,2})\left(\frac{k_{t,2}}{b_{t,2}}\right). $$

现在,这恰好是下面两列数字做内积:

$$ \widehat q_r= \begin{bmatrix}q_{r,1}b_{r,1}\\q_{r,2}b_{r,2}\end{bmatrix}, \qquad \widehat k_t= \begin{bmatrix}k_{t,1}/b_{t,1}\\k_{t,2}/b_{t,2}\end{bmatrix}. $$

内积就是对应位置相乘,再把乘积相加,得到一个数。用简写记号,逐特征相乘是 ⊙,逐特征相除是 ⊘,所以上面两列数字分别就是:

$$ \widehat q_r=q_r\odot b_r,\qquad \widehat k_t=k_t\oslash b_t. $$

这时 $b_r=\alpha_1\odot\cdots\odot\alpha_r$ 是一列累计保留率,不能把不同特征的门混乘成一个数。帽子 $\widehat{\phantom q}$ 只是给重新缩放后的向量起个名字;它们不是额外学习出来的新 query/key。

第六步:把所有位置按行排起来,才得到 Q⊙B 和 K⊘B

小写 $b_t$ 记录一个位置的累计保留率;大写 B 把所有位置的 $b_t$ 横过来,逐行排成一张表。B 的行是 token 位置,列是 key 特征。Q、K 的行列含义与它完全对应。

例如三个 token、两个特征,门及其累计表为:

$$ \text{每一步的门}=\begin{bmatrix}0.5&0.8\\0.2&0.5\\0.5&0.25\end{bmatrix},\qquad B=\begin{bmatrix}0.5&0.8\\0.1&0.4\\0.05&0.1\end{bmatrix}. $$

B 第一列依次是 $0.5$、$0.5\times0.2$、$0.5\times0.2\times0.5$;第二列也独立这样计算。这张表仅由门算出,不需要学习额外参数。

刚才已经知道,每个 query 要乘自己位置的累计值。对全部 query 一起做,就是 Q 与 B 对应位置相乘:

$$ \widehat Q=Q\odot B= \begin{bmatrix} 0.5Q_{11}&0.8Q_{12}\\ 0.1Q_{21}&0.4Q_{22}\\ 0.05Q_{31}&0.1Q_{32} \end{bmatrix}. $$

每个 key 则除以自己位置的累计值。对全部 key 一起做,就是 K 与 B 对应位置相除:

$$ \widehat K=K\oslash B= \begin{bmatrix} K_{11}/0.5&K_{12}/0.8\\ K_{21}/0.1&K_{22}/0.4\\ K_{31}/0.05&K_{32}/0.1 \end{bmatrix}. $$

比如第三个 query 是 $(2,3)$,第一个 key 是 $(4,5)$。重新缩放后,它们分别是 $(0.1,0.3)$ 和 $(8,6.25)$,内积为 $0.1\times8+0.3\times6.25=2.675$。直接按两条特征的遗忘路径计算,也得到 $2\times4\times0.1+3\times5\times0.125=2.675$。

第七步:把所有位置对的分数一次算出来,再汇总 value

现在每个位置的 query 和 key 都准备好了。让 $\widehat Q$ 的每一行与 $\widehat K$ 的每一行做内积,就得到所有位置对的分数。矩阵乘法恰好做这件事:

$$ P=\widehat Q\widehat K^\top=(Q\odot B)(K\oslash B)^\top. $$

这里转置 $\top$ 只是把 K 的行变成列,让“左边一行乘右边一列”的矩阵乘法规则,变成我们需要的“一个 query 与一个 key 做内积”。$P_{rt}$ 是第 r 个 query 读取第 t 个 value 的权重;P 不是归一化的概率表。

矩阵乘法还会算到未来位置。乘一个下三角 0/1 表 M,把它们清零。三个 token 时:

$$ P\odot M= \begin{bmatrix} P_{11}&0&0\\ P_{21}&P_{22}&0\\ P_{31}&P_{32}&P_{33} \end{bmatrix}. $$

第一行只读 $v_1$,第二行读 $v_1,v_2$,第三行读 $v_1,v_2,v_3$。例如第三个输出是 $o_3=P_{31}v_1+P_{32}v_2+P_{33}v_3$。把每个位置的输出也按行排好,就是本页 PPT 的完整公式:

$$ \boxed{O=\big((Q\odot B)(K\oslash B)^\top\odot M\big)V.} $$

这条式子里,乘 B 与除 B 合起来负责遗忘,M 负责不看未来,最后乘 V 负责把内容取回来。对角线处 $b_r/b_r=1$,所以本步刚写入的内容也会参与输出,且不会凭空多乘一个遗忘门。

并行具体发生在哪里

上面的公式对应整段已知输入、零初始状态,B 从序列开头累计,有 L 行。各位置的 Q/K/V 与门生成后,先计算 B,再计算缩放后的 Q/K,最后用矩阵乘法同时处理许多位置对。计算 $o_3$ 不再要求先构造完整 $S_1,S_2,S_3$:中间状态对这笔写入的作用,已经被门积比值明确写在分数里。

B 的前缀积仍有计算成本,可以使用前缀扫描组织;所有位置对也仍有平方级数量。下一页开始才将这套公式用于较短的块:把累计起点改为本块入口,B 只有 C 行,再单独处理入口历史。逐 token 生成时未来输入还不存在,则仍使用递推。

接到分块:如果入口还有旧历史,输出需要加什么

以上为看清主线,先令初始状态为零。若本块入口已有状态 S,入口历史会经历从本块第一步到读取位置 r 的所有门,即 $b_r$。沿用第 83 页的“按列缩放状态”,这部分读取为:

$$ S\operatorname{Diag}(b_r)q_r=S(q_r\odot b_r). $$

因为对角矩阵乘 q,只是把 q 每个特征乘相应保留率。因此所有 query 对入口历史的读取为 $(Q\odot B)S^\top$,整块输出变成:

$$ O=(Q\odot B)S^\top+ \big((Q\odot B)(K\oslash B)^\top\odot M\big)V. $$

这与第 87 页的式子一致:入口历史和块内写入分别读取,最后相加。这里 B 是本块累计表,不再是整段累计表。

对齐 PPT 的 log 形式:同一个比值的另一种写法

用 i 表示 query 位置、j 表示 key 位置、a 表示特征,把第七步的矩阵乘法展开:

$$ \begin{aligned} P_{ij} &=\sum_{a=1}^{d_k}(Q_{ia}B_{ia})\frac{K_{ja}}{B_{ja}}\\ &=\sum_{a=1}^{d_k}Q_{ia}K_{ja}\frac{B_{ia}}{B_{ja}}\\ &=\sum_{a=1}^{d_k}Q_{ia}K_{ja}\exp(\log B_{ia}-\log B_{ja}). \end{aligned} $$

求和号表示把所有特征的贡献相加;最后一步用 $x/y=\exp(\log x-\log y)$。PPT 用 k 作特征下标,含义与这里的 a 一样。直接相除是数学推导;长序列中的累计值可能非常小,实际内核还需对数计算、二级分块等数值处理,详见第 87 页的实现补充。

FIG. 084 · 原报告第 84 页点击图片放大 ↗
085

GLA 的块尾增量:把每笔写入衰减到同一时刻

为了把状态传到下一块,我们需要本块结束时的记忆。令块长为 $C$,把上一节的读取时刻 r 固定到块尾 C,就得到每笔写入到块尾的保留率 $b_C\oslash b_t$。沿用本文状态的方向,先将这段保留率逐特征乘到 key 上,再把各笔写入的外积相加:

$$ \bar k_t=k_t\odot(b_C\oslash b_t),\qquad \Delta S=\sum_{t=1}^{C}v_t\bar k_t^\top. $$

以三个位置为例,$\bar k_1=k_1\odot\alpha_2\odot\alpha_3$,$\bar k_2=k_2\odot\alpha_3$,$\bar k_3=k_3$。这些向量已经把各自的写入换算到同一个块尾时刻,所以可以直接合并。

把它们按行堆成 $\bar K\in\mathbb R^{C\times d_k}$,就像普通线性注意力那样,有 $\Delta S=V^\top\bar K$。矩阵实现中,先算出形状为 $C\times d_k$ 的门前缀矩阵 $B$(第 $t$ 行是 $b_t^\top$),然后:

$$ \bar K=K\odot\left((\mathbf1_Cb_C^\top)\oslash B\right). $$

$\mathbf1_Cb_C^\top$ 只是把最后一行门前缀复制 $C$ 次;逐行除以各自的 $b_t$,再逐元素乘 K。这一步完成后,原来的“许多外积先各自衰减再相加”,就变成了一次 $V^\top\bar K$。

PPT 的 Λ、Γ、γ:三段不同的遗忘路径

为了对上接下来三页的公式,现在切换到 PPT 的全局编号:第 $i+1$ 块包含位置 $iC+1$ 到 $(i+1)C$;PPT 的 $b_t=\prod_{u=1}^{t}\alpha_u$ 从整段开头累计,并以行向量表示,$b_0=\mathbf1$。这与上文从当前块起点重新累计的局部 $b_j$ 不同:局部前缀就是全局前缀除以块入口的前缀

对块内第 j 个位置,PPT 定义:

$$ \begin{aligned} \Lambda_{iC+j}&=b_{iC+j}\oslash b_{iC},\\ \Gamma_{iC+j}&=b_{(i+1)C}\oslash b_{iC+j},\\ \gamma_{i+1}&=b_{(i+1)C}\oslash b_{iC}. \end{aligned} $$
记号它描述的路径对应前面的局部记号
$\Lambda_{iC+j}$块入口 → 当前第 j 个位置$b_j^\top$,即 B 的第 j 行
$\Gamma_{iC+j}$当前写入之后 → 块尾$(b_C\oslash b_j)^\top$
$\gamma_{i+1}$块入口 → 块尾$b_C^\top$

例如 C = 3,第二块是位置 4、5、6。三笔写入到块尾的保留率 Γ 依次为 $\alpha_5\odot\alpha_6$、$\alpha_6$、1,正是下图每条写入路径的权重。把 Γ 按行堆成 $\Gamma_{[i+1]}$,就有 $\bar K=K_{[i+1]}\odot\Gamma_{[i+1]}$。

最后还有一次状态转置:本文的 S 是 $d_v\times d_k$,PPT 的状态是 $d_k\times d_v$。暂记 $S^{\rm ppt}=S^\top$,则本块增量转置后为:

$$ \boxed{\Delta S^{\rm ppt} =\bar K^\top V_{[i+1]} =\big(K_{[i+1]}\odot\Gamma_{[i+1]}\big)^\top V_{[i+1]}.} $$

这就是 PPT 状态更新式的第二项:先把每一行 key 衰减到块尾,再与 value 做外积求和。这里 Λ 和 Γ 都是 $C\times d_k$ 矩阵,γ 则是 $1\times d_k$ 的行向量。

不熟悉广播?用上面的 B 算出整张 Γ

本块三个位置、两个特征,块尾累计保留率是 B 的最后一行 $(0.05,0.1)$。把这行复制三遍,再除以 B 的对应位置:

$$ \Gamma= \begin{bmatrix}0.05&0.1\\0.05&0.1\\0.05&0.1\end{bmatrix} \oslash\begin{bmatrix}0.5&0.8\\0.1&0.4\\0.05&0.1\end{bmatrix} =\begin{bmatrix}0.1&0.125\\0.5&0.25\\1&1\end{bmatrix}. $$

第一行是第一笔写入到块尾的保留率;第二行只经历最后一个门;第三行全是 1,因为最后一笔刚写完。再将 K 与这张表逐元素相乘,就把所有 key 都换算到块尾。代码里的“广播”只是自动重复使用最后一行,不必真的先复制出一张大表。

FIG. 085 · 原报告第 85 页点击图片放大 ↗
086

GLA 的边界更新:先缩放历史,再加入本块

块尾不仅有本块新增的关联,也有进入这个块之前的历史。先沿用正文的局部记号:$b_C$ 是从本块入口累计到块尾的门积,S 的形状为 $d_v\times d_k$。入口状态 $S_{[i]}$ 经历了本块所有门,所以要右乘 $\operatorname{Diag}(b_C)$;本块增量则是刚才得到的 $V^\top\bar K$。两者相加:

$$ S_{[i+1]}=S_{[i]}\operatorname{Diag}(b_C)+V^\top\bar K. $$

每个块可以事先、独立地算好自己的 $b_C$ 与 $V^\top\bar K$,因为二者只依赖本块输入。随后沿边界递推:拿到入口状态,按列缩放,再加局部增量,便得到下一块的入口。门控使边界更新从纯加法变成了缩放加平移的仿射变换,但仍只需在块边界交接一次状态。

展开推导:两个块的更新怎样合并

把第 $i$ 块的更新写为 $F_i(S)=SD_i+E_i$,其中 $D_i$ 是总门的对角矩阵,$E_i$ 是局部增量。连续经过两块:

$$ F_2(F_1(S))=(SD_1+E_1)D_2+E_2 =S(D_1D_2)+(E_1D_2+E_2). $$

合并后仍是同样的形式,所以也有仿射前缀扫描的可能。顺序仍然重要:第一块写入的 $E_1$ 要经过第二块的遗忘,第二块写入的 $E_2$ 不必再经过第一块。实际内核是否采用扫描、保存多少检查点,是实现选择。

转置后,就是 PPT 的状态传递式

上一页已对齐本块增量,现在只看旧历史。本文的 $S\operatorname{Diag}(b_C)$ 是按列缩放;转置成 PPT 的状态后,就是按行缩放。令 $\mathbf1$ 是长度为 $d_v$ 的全 1 行向量,那么 $\gamma_{i+1}^\top\mathbf1$ 会把每个 key 特征的保留率复制到这一整行,形状为 $d_k\times d_v$。

因此,完整更新可写成与图中相同的形式:

$$ \boxed{ S^{\rm ppt}_{[i+1]} =(\gamma_{i+1}^\top\mathbf1)\odot S^{\rm ppt}_{[i]} +\big(K_{[i+1]}\odot\Gamma_{[i+1]}\big)^\top V_{[i+1]}. } $$

第一项保留经过整个块遗忘后的旧历史,第二项加入已衰减到块尾的新写入。去掉用于提醒方向的上标 ppt,就是配图里的 S。以第二块为例,$\gamma_2=\alpha_4\odot\alpha_5\odot\alpha_6$;所以从第一块块尾传到第二块块尾时,整份旧历史经历的正是图中的三个门。

FIG. 086 · 原报告第 86 页点击图片放大 ↗
087

GLA 的整块输出:逐行缩放 Q/K,再做因果乘法

第 84 页已经逐步得到:写入 t 到读取 r 的门积是 $b_r/b_t$,所以逐特征把 $q_rk_t(b_r/b_t)$ 重新分组为 $(q_rb_r)(k_t/b_t)$,最后按行堆成 $Q\odot B$ 与 $K\oslash B$。下面沿用这个结果,为一个块计算完整输出。

块尾状态只算一个时间点;输出则要为块内每个位置各算一行。这里先使用本块的局部门前缀矩阵 $B\in\mathbb R^{C\times d_k}$:第 $r$ 行为 $b_r^\top$,只累计本块前 r 个门。先定义两张经过门前缀缩放的矩阵:

$$ \widehat Q=Q\odot B,\qquad \widehat K=K\oslash B. $$

$\widehat Q$ 的第 $r$ 行是 $(q_r\odot b_r)^\top$;$\widehat K$ 的第 $t$ 行是 $(k_t\oslash b_t)^\top$。两者相乘后,第 $(r,t)$ 项就是上一节逐坐标推出的门控匹配分数。因此完整输出为:

$$ O=\underbrace{\widehat Q S_{[i]}^\top}_{\text{入口历史的读取}} +\underbrace{\big((\widehat Q\widehat K^\top)\odot M\big)V}_{\text{块内写入的读取}}. $$

第一项的第 $r$ 行是 $[S_{[i]}(q_r\odot b_r)]^\top$,恰好对应历史经历前 $r$ 个门后再被读取。第二项先计算所有位置对的分数,用下三角 $M$ 去掉未来位置,最后用分数加权 V。两个结果都是 $C\times d_v$,相加就是本块所有输出。

这里用的是各行自己的门前缀 $b_r$,不能把所有行都乘块尾的 $b_C$。另外,输出使用的 $\widehat K=K\oslash B$ 与边界更新使用的 $\bar K=K\odot(b_C/B)$ 也不同:前者要和 query 所在位置的 $b_r$ 配对,后者已经固定换算到块尾。

用 Λ 写回 PPT 的输出公式

第 85 页已经说明,本块的局部门前缀矩阵 B 就是 PPT 的 $\Lambda_{[i+1]}$;而本文入口状态的转置,就是 PPT 中前一块的块尾状态 $S^{\rm ppt}_{[i]}$。将这两个对应关系代入历史项:

$$ \boxed{O^{\rm inter}_{[i+1]} =(Q_{[i+1]}\odot\Lambda_{[i+1]})S^{\rm ppt}_{[i]}.} $$

这正是本页 PPT 展示的输出式。图里强调的是 inter,也就是来自之前块的贡献,并未在这一行写出完整输出。比如第二块的三个 query 分别乘 $\alpha_4$、$\alpha_4\odot\alpha_5$、$\alpha_4\odot\alpha_5\odot\alpha_6$,就把同一入口历史换算到了三个不同的读取时刻。

把本块内的贡献也用同一套记号写出,就得到:

$$ \begin{aligned} P^{\rm local}_{[i+1]}&=(Q_{[i+1]}\odot\Lambda_{[i+1]}) (K_{[i+1]}\oslash\Lambda_{[i+1]})^\top,\\ O^{\rm intra}_{[i+1]}&=(P^{\rm local}_{[i+1]}\odot M)V_{[i+1]},\\ O_{[i+1]}&=O^{\rm inter}_{[i+1]}+O^{\rm intra}_{[i+1]}. \end{aligned} $$

局部匹配中的比值 $\Lambda_r/\Lambda_t$ 会消掉共同的块入口前缀,剩下从写入 t 到读取 r 的门积。现在可以用三句话区分 PPT 的三个门量:Λ 用于把历史读到当前时刻,Γ 用于把新写入送到块尾,γ 用于把旧状态传过整个块。 三者来自同一条门序列,只是起点和终点不同。

展开推导:用两个 token 核对每一项

两步门分别为向量 $\alpha_1,\alpha_2$,故 $b_1=\alpha_1$,$b_2=\alpha_1\odot\alpha_2$。因果分数矩阵为:

$$ A=\begin{bmatrix} q_1^\top k_1&0\\ \sum_a q_{2,a}k_{1,a}\alpha_{2,a}&q_2^\top k_2 \end{bmatrix}. $$

左下角的 $b_2/b_1$ 只剩 $\alpha_2$;两个对角位置的 $b_r/b_r$ 都消掉了。因此:

$$ \begin{aligned} o_1&=S_{[i]}(q_1\odot\alpha_1)+v_1(k_1^\top q_1),\\ o_2&=S_{[i]}(q_2\odot\alpha_1\odot\alpha_2) +v_1\big[(k_1\odot\alpha_2)^\top q_2\big]+v_2(k_2^\top q_2). \end{aligned} $$

这与先更新 $S^1$、再更新 $S^2$ 后分别读取,逐项一致。

实现补充:数学上可以相除,代码为什么还需要二级分块

很长的门积可能接近零,直接计算 $K/B$ 会放大数值。令 $g_r=\sum_{j=1}^r\log\alpha_j$,合法位置对的分数可以稳定地写成 $A_{rt}=\sum_a q_{r,a}k_{t,a}\exp(g_{r,a}-g_{t,a})$。但如果逐个位置对、逐个特征算指数,就不再是普通的矩阵乘法。

GLA 因此把大块再分成小块。对一个较早的 key 小块与较晚的 query 小块,选两者之间的共同边界 $p$,把 $g_r-g_t$ 拆成 $(g_r-g_p)+(g_p-g_t)$。将两个指数因子分别乘到 Q、K 上,这个非对角小块就又能用矩阵乘法。沿因果方向,这两段差都非正,避免把极小门积取倒数。

同一个小块内部还存在三角因果关系,原 GLA 实现对这部分使用更直接的逐位置处理。二级分块让大部分位置对交给 Tensor Cores,同时把更复杂的数值处理限制在较小的对角区域。来源:GLA 论文 §3.2—3.3 与附录 B

FIG. 087 · 原报告第 87 页点击图片放大 ↗
088

固定每批 token 数,看长序列吞吐

这些门控操作没有让长序列训练的吞吐大幅下降。在下图的实验中,每批大致保持 16K tokens,配置从 2048/8 逐步变到约 16K/1,也就是序列变长、batch 相应缩小。GLA 的吞吐约维持在 41–44 K tokens/s,而 Transformer++ 随长度增加明显下降,Mamba 在这组设置下更低。纵轴衡量每秒处理的 token 数,柱子越高越快。

FIG. 088 · 原报告第 88 页点击图片放大 ↗
089

语言建模已经接近

质量方面,GLA 已经明显缩小了与 Transformer 的差距。下表比较 1.3B、训练 100B tokens 的模型:GLA 的 PPL 为 17.2,略高于 Transformer++ 的 16.9;LM Eval 为 51.1,略高于其 50.9。相比 RetNet 的 18.6 / 48.9,两项都改善;Mamba 则为 17.1 / 50.0。PPL 越低越好,LM Eval 越高越好,因此这些结果要分别理解。

FIG. 089 · 原报告第 89 页点击图片放大 ↗
090

检索任务在问什么

但语言建模表现接近,不代表所有信息都记得同样准确。下图来自 Arora 等(2024)讨论的回忆任务,示例文档包含 Number、Purpose、Type of Test、Product Code 等许多字段。理解它属于什么主题是一回事,在读完长文后取回某个字段的原文,则需要保留更细的关联。

FIG. 090 · 原报告第 90 页点击图片放大 ↗
091

一个字段,考察怎样的记忆

例如,问题只问 “Type of Test”,正确答案是文档中的 “Nucleic acid amplification assay (Helicase-dependent Amplification, HDA)”。下图将对应字段与答案放在一起。仅仅知道这是一份检测文档,无法恢复这段具体内容;模型必须在问题出现之前,就已经把可能被查询的细节留在记忆里。

FIG. 091 · 原报告第 91 页点击图片放大 ↗
092

把 Retrieval 从平均分里单独拿出来

Retrieval 指上下文检索:给模型一段文档,再问其中的具体事实或字段,看它能否准确取回答案。这组结果汇总 FDA、SWDE、SQuAD 等任务,分数越高越好;它衡量的是读入上下文后的信息恢复能力,不是搜索引擎的网页召回率。单独看这一列,差距就显现出来了。下表中 GLA 为 37.7,高于 RetNet 的 30.6 与 Mamba 的 27.6,却仍低于 Transformer++ 的 41.8。它在 LM Eval 上略占优势,在检索上落后 4.1 分,这两件事可以同时成立。平均预测表现与精确回忆考察的是记忆的不同方面。

FIG. 092 · 原报告第 92 页点击图片放大 ↗
093

超出训练长度后,还能预测好吗

另一个问题是训练长度之外还能否继续利用上下文。下图在 PG19 上按文本位置分桶计算 PPL,横轴 Position bucket (K) 表示位置,图例中的 “(2K)” 表示训练长度。进入更后的文本区间后,GLA 的青色曲线总体低于 RetNet 和 Mamba,而 Transformer++ 很快超出图中的纵轴范围。这说明在这组设置下,递推式记忆能够更好地维持训练长度外的预测表现。

FIG. 093 · 原报告第 93 页点击图片放大 ↗

具体来看,GLA 在约 28K 的位置桶降到 6.2 左右,末端又有回升,RetNet 也有类似变化,Mamba 则大致停在 8 附近。增加历史并不保证每个位置都更容易预测;这里每个点统计的是相应位置桶的 PPL。它衡量的是长文后段的预测表现,与找回某个字段的检索能力仍有区别。

094

从零理解 SSM:保存状态,接收输入,读出结果

状态空间模型(State Space Model,SSM)可以先理解成一个带记忆的递推系统。先不用微分方程,只看 $h_t=0.5h_{t-1}+x_t$:每来一个输入,把旧记忆保留一半,再加入新输入。若 $h_0=0$,输入依次为 $2,0,0$,状态就是 $2,1,0.5$。过去的输入没有立刻消失,而是在状态中逐渐衰减。

把一个记忆数值扩展成 $N$ 个数,就得到向量状态 $h_t\in\mathbb R^N$。一个简单的离散 SSM 可以写成:

$$ h_t=\bar A h_{t-1}+\bar b x_t,\qquad y_t=c^\top h_t. $$

这里暂用标量输入 $x_t$ 和标量输出 $y_t$。$\bar A\in\mathbb R^{N\times N}$ 决定旧状态怎样变化;$\bar b\in\mathbb R^N$ 决定当前输入写到哪些状态坐标;$c\in\mathbb R^N$ 决定怎样把状态读成输出。上横线只是表示离散时间系数,下一段可选推导会解释它的来源。

例如 $\bar A=\operatorname{Diag}(0.9,0.2)$,两个坐标分别保留 90% 和 20%。前者可以记录较慢变化的历史,后者更偏向最近的输入;输出再组合这两种时间尺度。这与 GLA 的“不同 key 坐标有不同保留率”已经很接近了。

把 GLA 的一行拿出来,就能看到同样的结构

取矩阵状态 $S_t$ 的第 $a$ 行,转成列向量 $h_t^{(a)}=S_t[a,:]^\top\in\mathbb R^{d_k}$。GLA 的更新与读取便成为:

$$ h_t^{(a)}=D_t h_{t-1}^{(a)}+k_t v_{t,a},\qquad o_{t,a}=q_t^\top h_t^{(a)}. $$

逐项对照:状态转移是 $D_t$,写入方向是 $k_t$,这一路的输入是 value 的第 $a$ 个分量 $v_{t,a}$,读取方向是 $q_t$。矩阵 S 的各行相当于多路这样的状态系统。这个对应解释的是核心递推结构,不要求整个 GLA 网络与某个具体 SSM 架构完全相同。

从离散式回看配图中的连续方程:不需要先学会微分方程

连续 SSM 写成 $\frac{dh(t)}{dt}=Ah(t)+b x(t)$。它说的是“状态每一瞬间怎样变化”;语言 token 是一项项输入的,因此模型还需要把一段时间内的变化折算成一步更新。

先取单个状态:$h'(t)=a h(t)+b x(t)$。若在长度 $\Delta$ 的时间段内输入保持为常数,精确解为:

$$ h_t=e^{a\Delta}h_{t-1}+\left(\int_0^\Delta e^{as}\,ds\right)b x_t. $$

所以离散系数是 $\bar a=e^{a\Delta}$、$\bar b=\left(\int_0^\Delta e^{as}ds\right)b$。当 $a\ne0$,积分等于 $(e^{a\Delta}-1)/a$;当 $a=0$,积分为 $\Delta$。例如 $a=-1$、$\Delta=\log2$,得到 $\bar a=1/2$,恰好是前面“每步保留一半”的例子。

矩阵版本完全类似:$\bar A=e^{\Delta A}$,$\bar b=\left(\int_0^\Delta e^{sA}ds\right)b$。这里的指数是矩阵指数;若 A 对角,每个对角元素各自取指数即可。配图中常见的 $A^{-1}(e^{\Delta A}-I)b$ 是 A 可逆时的等价写法。主文只需要离散递推便能理解模型,这段解释的是那些符号为什么会出现。

SSM 的离散化与选择性设计可参照 Mamba 论文 §2—3

FIG. 094 · 原报告第 94 页点击图片放大 ↗
095

SSM 为什么能写成卷积,选择性又改了什么

现在可以理解“SSM 也能并行”这句话从何而来。先看固定参数的简单递推 $h_t=a h_{t-1}+b x_t$,输出 $y_t=c h_t$,并取零初始状态。把前几步展开:

$$ \begin{aligned} y_1&=cb\,x_1,\\ y_2&=cab\,x_1+cb\,x_2,\\ y_3&=ca^2b\,x_1+cab\,x_2+cb\,x_3. \end{aligned} $$

每个输入的系数只由它距离当前位置有多远决定:当前输入乘 $cb$,前一步输入乘 $cab$,再前一步乘 $ca^2b$。这就是一个固定卷积核。我们既可以逐步维护状态,也可以用这组权重直接计算整段输出。

向量状态只是把这些系数换成 $c^\top\bar A^j\bar b$。如果 $\bar A,\bar b,c$ 固定,同一个距离 $j$ 总有同一个系数;如果它们随当前内容改变,同样相隔三步的两段输入,就不一定经历相同的遗忘与读取。Mamba 的选择性设计让步长及部分写入、读取参数依赖输入,因此通常不能再用一个固定卷积核处理整段序列。

GLA 也属于输入改变状态动态的情形:$D_t,k_t,q_t$ 随位置变化,历史的贡献取决于沿路遇到的门。前面推出的前缀积与分块算法,就是在处理这种变化。SSM 与线性注意力在这里相遇:都可以写成“旧状态转移、当前输入写入、从状态读取”,但对各个系数施加的结构约束不同。

展开推导:固定卷积核与输入依赖的路径权重

固定参数、零初始状态时,将 $h_t=\bar A h_{t-1}+\bar b x_t$ 反复代入,得到:

$$ h_t=\sum_{j=1}^{t}\bar A^{t-j}\bar b x_j,\qquad y_t=\sum_{j=1}^{t}\underbrace{c^\top\bar A^{t-j}\bar b}_{\text{只取决于距离 }t-j}x_j. $$

若换成输入依赖的 $\bar A_t,\bar b_t,c_t$,那么输入 $x_j$ 对当前输出的贡献变为:

$$ y_t=\sum_{j=1}^{t}c_t^\top\bar A_t\bar A_{t-1}\cdots\bar A_{j+1}\bar b_j x_j. $$

系数取决于实际经过哪些时刻,不能只按距离查一张固定表。对角转移时,连乘可以化为逐坐标乘积;若每个头的转移进一步限制为一个标量乘单位矩阵,处理就更简单。这也是比较 GLA、Mamba、Mamba-2 等模型时,首先要看状态转移形状的原因。

下表列出的不同门控参数化,可按这条思路阅读:标量门让所有坐标一起衰减,向量门允许各坐标有不同保留率,更一般的结构则增加控制自由度。仅有这种对应还不能推断整个网络相同,投影、归一化与离散化选择仍有差异。延伸阅读:MambaMamba-2 / State Space Duality

FIG. 095 · 原报告第 95 页点击图片放大 ↗
096

下一步需要更有针对性的写入

到这里,线性注意力已经从纯粹累加发展成了会选择性遗忘的矩阵状态模型,分块算法也让它能够高效训练。用状态空间模型的语言描述它们,还能看清不同门控形式之间的联系。不过,遗忘之外仍有一个问题:同一条关联再次出现时,模型能否根据自己已经记住的部分,决定这次究竟还要写多少?

FIG. 096 · 原报告第 96 页点击图片放大 ↗
CHAPTER 07 / SLIDES 97—117

Delta Rule:用误差修正记忆

把状态看成预测器,理解每一次写入的方向和大小。

097

让记忆知道自己的预测误差

DeltaNet 的想法是让记忆先检查自己的预测,再决定如何更新。这会改善关联存储,却也让当前写入依赖之前的状态,训练并行因此更难。下图的 NeurIPS ’24 论文《Parallelizing Linear Transformers with the Delta Rule over Sequence Length》解决的正是这个问题。先理解它为什么值得做,再来看怎样把这条依赖拆开。

FIG. 097 · 原报告第 97 页点击图片放大 ↗
098

先写入五组关联,再换顺序查询

可以用一个很小的任务单独考察联想记忆:先给模型 A 4、B 3、C 6、F 1、E 2,再依次查询 A、C、F、E、B 对应什么数字。这就是下图的 MQAR(Multi-Query Associative Recall,多查询联想回忆),例子来自 Arora 等(2024)。复杂的语言理解暂时被移开,留下的是把上下文中的关联写入、再准确读出的能力。它是人为构造的记忆测试,不等于真实语言任务的综合能力。

FIG. 098 · 原报告第 98 页点击图片放大 ↗
099

关联在输入里,而非训练参数里

这些关联由当前输入提供,而非永久存进训练参数。A 在一次样本里可能对应 4,在另一次样本里可以对应别的数字。下图将写入的 Key-Value 与读取的 Query 分开,正是为了强调这层区别:模型需要学会一种记忆方法,再用它处理每段上下文给出的新关联。

FIG. 099 · 原报告第 99 页点击图片放大 ↗
100

按查询取回对应的 value

对这组查询,正确答案依次是 4、6、1、2、3。A 对应最早的 A 4,最后查询的 B 则要回到 B 3;查询顺序已经打乱,所以照抄输入数字的顺序无法完成任务。下图展示了每个答案对应的关联来源,多次查询还要求记忆在被读取后仍能保留。

FIG. 100 · 原报告第 100 页点击图片放大 ↗
101

维度变大后,哪些模型能记准

状态宽度会影响这些关联能否被准确保存。下图以 Model dimension 为横轴、Accuracy 为纵轴。Accuracy 就是回答正确的查询占比:例如 100 次查询答对 80 次,准确率为 80%,越高越好。维度 64 时,GLA 约为 22%,Mamba 约为 80%,增大到 128 后都明显改善;报告同时指出 Transformer 在较小维度也能达到 100%。这促使我们追问,改进写入规则,能否让有限维的状态更有效地存储关联?

FIG. 101 · 原报告第 101 页点击图片放大 ↗

为什么维度可能帮助记忆?可以先看原博客的解释:不同 key 越容易互相区分,读取一条关联时混入其他 value 的干扰就越小。 一个干净的理想情况是各 key 两两正交,也就是内积为 0。二维空间至多提供两个这样的独立方向,三维空间可以提供三个;更高维就有更多互不干扰的方向可用。下一页会从状态的读取公式推导这一点。

这里也要区分两个“维度”:图的横轴是模型表示宽度 model dimension;推导中的 $d_k$ 是单个头的 key 宽度。模型变宽如何影响 $d_k$、头数、总状态容量,取决于实验配置,因此不能直接把“模型维度 64”解释成“最多记住 64 条关联”。图展示了实验趋势,正交方向分析解释其中一种容量与干扰机制,不是这几条曲线的定量预测。

102

维度怎样通过正交方向影响记忆干扰

沿用原博客 Part I 的联想记忆分析,假设我们从零状态写入 N 组 key–value,所有 key 都归一化到单位长度。普通线性注意力保存的是:

$$ S=\sum_{i=1}^{N}v_i k_i^\top. $$

现在想取回第 j 条关联,直接用它的 key 查询。把 $k_j$ 代入,再把属于自己的项单独拿出:

$$ \begin{aligned} Sk_j &=\sum_{i=1}^{N}v_i(k_i^\top k_j)\\ &=v_j(k_j^\top k_j)+\sum_{i\ne j}v_i(k_i^\top k_j)\\ &=\underbrace{v_j}_{\text{想取回的内容}} +\underbrace{\sum_{i\ne j}v_i(k_i^\top k_j)}_{\text{其他关联混进来的内容}}. \end{aligned} $$

最后一步用到了单位 key 的 $k_j^\top k_j=1$。每条别的 value 混入多少,恰好由它的 key 与查询 key 的内积决定。两者正交时内积为 0,这条 value 不会混入;两者接近时内积较大,就容易串扰。这里“正交”不是要求 key 的每个数都不同,而是整个向量的内积为零。

两个方向够存两条干净关联,再加一条会怎样

取二维 key $k_1=(1,0)^\top$、$k_2=(0,1)^\top$,它们正交。为了方便手算,value 暂时只用一个数,分别为 10 和 20。状态为 $S=[10,20]$,用 $k_1$ 读回 10,用 $k_2$ 读回 20,互不干扰。

现在想再存第三条 value 30。如果选单位 key $k_3=(1,1)^\top/\sqrt2$,写入后:

$$ S'=\left[10+\frac{30}{\sqrt2},\ 20+\frac{30}{\sqrt2}\right]. $$

这时再用 $k_1$ 查询,结果变成 $10+30/\sqrt2$,第三条内容混进来了。问题不是没有挑对第三个二维方向:任何非零二维向量,都不可能同时与 $(1,0)$ 和 $(0,1)$ 正交。

若把 key 扩成三维,就可以使用 $(1,0,0)^\top$、$(0,1,0)^\top$、$(0,0,1)^\top$,状态为 $[10,20,30]$,三条都能独立读回。这就是“维度增加,能够支撑的正交方向增加”的具体含义。

为什么 d 维空间至多有 d 个非零正交方向

若 $k_1,\ldots,k_N$ 两两正交,考虑一个等式 $c_1k_1+\cdots+c_Nk_N=0$。两边与任意 $k_j$ 做内积,其他项都消失,只剩 $c_j\|k_j\|^2=0$。因为 key 非零,所以每个 $c_j$ 都必须为 0。这说明这些向量线性无关,而 $d_k$ 维空间最多有 $d_k$ 个线性无关向量,因此 $N\le d_k$。

这个界针对的是“为各条关联分配互不干扰的正交 key”这套方案。它不是所有模型的通用记忆条数上限:value 之间的结构、允许的近似误差、学习到的编码、门控与纠错都会影响实际容量。即使 N 超过 $d_k$,也仍然可以存储并近似读取许多关联,只是无法让所有 key 都两两正交。

不完全正交时,更高维也可能让干扰变小

实际模型不必为每条记忆找到严格正交的 key;内积足够小,也有帮助。一个说明性的数学模型是:独立、均匀地选取 $d_k$ 维单位向量,那么两个不同 key 的平方内积期望为 $1/d_k$。维度越大,随机方向通常越接近正交。

平方内积为什么是 1/d,以及这个结论的边界

由于各方向对称,可以把查询 key 看成第一个坐标方向。随机单位 key 的各坐标平方之和为 1,各坐标的平方期望又相同,所以每个都是 $1/d_k$;它与查询 key 的内积就是第一坐标,因此 $\mathbb E[(k_i^\top k_j)^2]=1/d_k$。

进一步,若各 value 固定、keys 独立均匀,交叉项的期望为零,上面读取误差 e 满足:

$$ \mathbb E\|e\|^2=\frac1{d_k}\sum_{i\ne j}\|v_i\|^2. $$

若各 value 的平方长度相近,误差能量就大致随 $(N-1)/d_k$ 增长:记得越多,干扰越大;key 越宽,干扰可能越小。真实网络的 key/value 是学习出来的,通常不满足这些独立随机假设,因此这里只解释一种机制,不把它当作实验准确率的预测公式。

回到前页的结果,增大宽度可能提供更多可区分方向和更大的状态容量,而 DeltaNet 的下一步改进是改变写入规则:不是所有目标都无条件叠加,而是先检查已经预测出多少,只补剩余误差。它提高有限状态的使用效率,也不会消除全部容量限制。

FIG. 102 · 原报告第 102 页点击图片放大 ↗
103

读取是一种预测

读取则相当于让这个线性映射做一次预测。如果拿存过的 $k_a$ 查询,结果中既有 $v_a\|k_a\|^2$,也有其他写入的 $v_j(k_j^\top k_a)$。理想情况下,key 彼此正交且长度合适,就能干净地取回目标;当 key 重叠时,交叉项会带来干扰。这也是为什么把状态称为联想记忆,并不意味着它像字典一样有独立槽位。

FIG. 103 · 原报告第 103 页点击图片放大 ↗
104

张量积绑定的历史出处

用外积绑定 key 与 value,有着更早的思想来源。下图展示 Smolensky(1990)的《Tensor Product Variable Binding and Representation of Symbolic Structures in Connectionist Systems》,研究如何通过张量积表达角色与内容的绑定。线性注意力可以放在这个视角下理解:关联被编码进共享的向量空间,而 key 之间的几何关系决定了它们能否被清晰区分。

FIG. 104 · 原报告第 104 页点击图片放大 ↗
105

重复写入,反而会越记越偏

累加式写入的另一个问题是,它不知道某条关联是否已经被记住。取最简单的一维例子:$k=1$,目标一直是 $v=3$,从 $S=0$ 连续写三次,状态与读取就变成 $3,6,9$。输入没有提供新信息,重复写入却不断放大它。我们需要一种能够感知已有预测、只补上缺少部分的更新规则。

FIG. 105 · 原报告第 105 页点击图片放大 ↗
106

Delta Rule:让写入量由预测误差决定

Delta Rule 是一个误差纠错原则:先用当前模型做预测,再按照预测与目标之间的差修改模型。对线性预测器 $\hat y=w^\top x$,更新为 $w\leftarrow w+\eta(y-\hat y)x$。当预测已经正确时,差值为零,就无需重复写入;预测偏低或偏高时,更新方向也随之改变。

DeltaNet 将这一原则应用于线性注意力。把状态矩阵 $S$ 看成预测器,当前 key $k_t$ 是输入,value $v_t$ 是希望记住的目标,$S_{t-1}k_t$ 就是当前记忆的预测。状态更新由此写成:

$$ \begin{aligned} S_t&=S_{t-1}-\beta_t(S_{t-1}k_t-v_t)k_t^\top\\ &=S_{t-1}-\beta_t S_{t-1}k_tk_t^\top+\beta_t v_tk_t^\top. \end{aligned} $$

第一行最直接地表达了纠错:预测减目标得到误差,沿负误差方向更新状态。 第二行把同一次更新拆开:保留原状态,减去它沿当前 key 所产生的预测关联,再写入相应比例的目标关联。两个修正项共同作用,并不意味着必须先从一个独立缓存里取出“旧 value”。

等价地,记 $e_t=v_t-S_{t-1}k_t$,就有 $S_t=S_{t-1}+\beta_t e_tk_t^\top$。普通线性注意力每次加 $v_tk_t^\top$,DeltaNet 每次加的是尚未预测对的部分对应的外积。这个区别会同时影响记忆效果与后面的并行算法。

这一解释沿用原博客 Part I:What is Delta Rule?。它也可以从平方损失的一步梯度下降严格推出,后面会把矩阵梯度逐元素算出来。

看代码:先预测,再只修正误差

下面沿用原博客“预测 → 误差 → 更新”的写法,换成一个可手算、可直接运行的 NumPy 例子。同一个输入 x,每次都希望预测为 3;步长 0.5 表示每次只修正一半差距。

import numpy as np

x = np.array([1.0, 0.0])
w = np.zeros(2)
target = 3.0
lr = 0.5

for step in range(3):
    prediction = w @ x
    error = target - prediction
    w = w + lr * error * x
    print(f"{step + 1}: {prediction:.3f} -> {w @ x:.3f}")
# 1: 0.000 -> 1.500
# 2: 1.500 -> 2.250
# 3: 2.250 -> 2.625

@ 表示内积或矩阵乘法;代码里的 * 表示标量缩放或逐元素相乘。这里没有求导函数,因为我们已经把这一步的更新公式直接写出来了。

将一个数值预测改成向量预测,就得到 DeltaNet 的状态更新。下例中的 k 和 v 是已经生成好的 key/value,S 有两行两列;固定单位 key 便于直接观察纠错:

S = np.zeros((2, 2))  # 行是 value 特征,列是 key 特征
k = np.array([1.0, 0.0])
v = np.array([3.0, 2.0])
q = k.copy()  # 仅为演示;实际 query 可以与 key 不同
beta = 0.5

for step in range(3):
    prediction = S @ k        # 用 key 检查写入前的记忆
    error = v - prediction   # 还没有预测对的部分
    S = S + beta * np.outer(error, k)
    output = S @ q           # 用 query 读取更新后的状态
    print(output)
# [1.5 1. ]
# [2.25 1.5 ]
# [2.625 1.75 ]

np.outer(error, k) 产生一个矩阵:第 a 行第 b 列等于 error[a] * k[b],就是公式中的 $e_tk_t^\top$。这段循环展示的是前向状态更新,不是完整语言模型的训练代码;实际网络会根据每个 token 生成不同的 q/k/v 和 β。

FIG. 106 · 原报告第 106 页点击图片放大 ↗
107

先用 key 检查当前记忆

纠错的第一步是预测:$\hat v_t=S_{t-1}k_t$。这次读取使用 key,因为我们正在学习“这个 key 应当对应这个 value”的关联。只有针对同一个 key 比较预测与目标,误差才有意义。

例如 $S_{t-1}=\left[\begin{smallmatrix}2&0\\1&3\end{smallmatrix}\right]$,$k_t=(1,0)^\top$,那么预测为 $(2,1)^\top$。若目标是 $v_t=(4,1)^\top$,误差就是 $(2,0)^\top$:第一分量需要增加 2,第二分量无需修改。配图中的 $v_t^{\rm old}$ 指的就是这里的现时预测 $\hat v_t$。

FIG. 107 · 原报告第 107 页点击图片放大 ↗
108

β 控制这次纠错走多远

有了误差 $e_t=v_t-S_{t-1}k_t$,步长 $\beta_t$ 决定写入其中多少:$\Delta S_t=\beta_t e_tk_t^\top$。DeltaNet 可以从当前输入生成 $\beta_t=\sigma(W_\beta x_t)\in(0,1)$,让不同 token 使用不同的纠错强度。

最简单的一维例子是 $k=1$、目标始终为 $v=3$。若 $\beta=0.5$,状态从 0 开始,依次为 $1.5,2.25,2.625,\ldots$:每一步只补上一半误差,越接近目标,改动越小。若把步长取为极限值 1,第一次便达到 3,此后的误差为零。这里与单纯累加的 $3,6,9,\ldots$ 形成了直接对照。

试着改变更新强度k = 1 · 目标 v = 3 · 初始 S = 0
— Delta Rule— 普通累加┄ 目标 3

滑块的 0 和 1 用来演示两端行为;实际 sigmoid 输出位于两者之间。这个例子把 key 固定为单位长度,并排除了不同 key 之间的干扰,目的是单独观察误差反馈。

对照配图:目标与旧预测的混合为什么也是同一次更新

配图引入 $v_t^{\rm new}=(1-\beta_t)\hat v_t+\beta_t v_t$。这个中间量减去旧预测后,得到:

$$ v_t^{\rm new}-\hat v_t=\beta_t(v_t-\hat v_t). $$

因此“先减旧预测的外积,再加混合值的外积”,合起来正是误差更新:

$$ S_t=S_{t-1}-\hat v_tk_t^\top+v_t^{\rm new}k_t^\top =S_{t-1}+\beta_t(v_t-\hat v_t)k_t^\top. $$

$v_t^{\rm new}$ 不是最终模型输出,输出还要用 query 读取。理解核心算法时,直接使用预测误差即可,不必额外记住 old/new 两个名字。

FIG. 108 · 原报告第 108 页点击图片放大 ↗
109

为什么单位 key 能让纠错成为精确插值

误差写入后,用同一个 key 再读一次,新的预测究竟是什么?把 $k_t$ 乘到更新式右侧:

$$ S_tk_t=\hat v_t+\beta_t(v_t-\hat v_t)(k_t^\top k_t). $$

若 $\|k_t\|=1$,最后的内积等于 1,因此 $S_tk_t=(1-\beta_t)\hat v_t+\beta_t v_t$。新的预测恰好在旧预测和目标之间插值;当 $\beta_t=1$,便准确达到当前目标。

若 key 没有单位化,有效步长会变成 $\beta_t\|k_t\|^2$,可能越过目标。所以“完全覆写当前关联”的说法需要单位 key 条件。并且它只保证当前方向上的预测,不保证其他 key 完全不受影响;共享状态空间中的干扰,后面还会单独分析。

FIG. 109 · 原报告第 109 页点击图片放大 ↗
110

逐元素求梯度,得到 DeltaNet 的更新

误差纠错还可以从一个明确的目标函数推出。把当前 key/value 当作一个小型学习样本,希望状态 S 对这个 key 的预测尽量接近 value:

$$ \ell_t(S)=\frac12\|Sk_t-v_t\|_2^2. $$

这个平方损失的矩阵梯度为 $\nabla_S\ell_t=(Sk_t-v_t)k_t^\top$。用步长 $\beta_t$ 做一次梯度下降,便是前面得到的 Delta Rule。为什么梯度恰好是“误差外积 key”,可以逐个矩阵元素验证。

展开推导:状态矩阵中一个元素的偏导数

令第 $a$ 个预测分量为 $\hat v_a=\sum_b S_{ab}k_b$,则:

$$ \ell(S)=\frac12\sum_a\left(\sum_bS_{ab}k_b-v_a\right)^2. $$

元素 $S_{ab}$ 只影响第 $a$ 个输出,而且 $\partial\hat v_a/\partial S_{ab}=k_b$。根据链式法则:

$$ \frac{\partial\ell}{\partial S_{ab}}=(\hat v_a-v_a)k_b. $$

把所有 $(a,b)$ 的偏导排成矩阵,就是 $(Sk-v)k^\top$,形状为 $(d_v\times1)(1\times d_k)=d_v\times d_k$,与 S 完全一致。因此:

$$ \begin{aligned} S_t&=S_{t-1}-\beta_t\nabla_S\ell_t(S_{t-1})\\ &=S_{t-1}-\beta_t(S_{t-1}k_t-v_t)k_t^\top. \end{aligned} $$

这里是前向计算中的显式局部更新,无需在推理时调用通用优化器。训练整个网络时,语言建模损失仍会学习怎样生成 Q/K/V 与 $\beta$。局部状态纠错与全模型训练,是两个不同层次;配图引用的 Widrow–Hoff 规则提供了前者的经典出发点。

FIG. 110 · 原报告第 110 页点击图片放大 ↗
111

进入本步的旧状态与新输入

把一次更新单独拿出来,它开始时只有已有记忆 $S_{t-1}$ 和新输入 $x_t$。旧状态可能已经汇总很长的前文,新输入则决定本步要写入什么、读取什么。下图就从这两个对象开始展开;若是一个独立序列的起点,则按模型约定提供初始状态,常见选择是零状态。

FIG. 111 · 原报告第 111 页点击图片放大 ↗
112

慢网络生成本步学习任务

慢网络先从 $x_t$ 生成本步的 key、value、query 与步长 $\beta_t$。这相当于同时给记忆一个小型学习任务——让 key 对应 value——以及一个读取请求。完整架构还会在这些向量周围安排短卷积、Q/K 归一化等操作;第 116 页之后会用原博客的架构图,把这些组件沿数据流接起来。

FIG. 112 · 原报告第 112 页点击图片放大 ↗
113

用 key 计算预测,不是用 query

纠错前的读取使用 $\hat v_t=S_{t-1}k_t$,因为我们要检查的是当前 key 对应的预测。最终输出使用 $q_t$,回答的是当前 token 想从记忆里获取什么。两次读取虽然都经过状态矩阵,目的却不同;下图的旧预测是前一种,它必须在本次写入发生之前算出。

FIG. 113 · 原报告第 113 页点击图片放大 ↗
114

真正写入的是带步长的误差

记本步实际写入向量为 $u_t=\beta_t(v_t-S_{t-1}k_t)$。它的形状与 value 相同,都是 $d_v$ 维,但它的含义是本次需要修正多少。状态只需做 $S_t=S_{t-1}+u_tk_t^\top$。

若目标和预测一致,$u_t=0$,这一关联不会被无意义地重复累加。配图中的混合式经过相减后,也得到同一个 $u_t$。后面推导并行算法时,会一直用这个实际写入量作为出发点。

FIG. 114 · 原报告第 114 页点击图片放大 ↗
115

修正一个 key,会不会动到另一个 key

写入残差后,状态完成了沿当前 key 方向的一次修正。它也可能影响另一个 key:对查询 $k'$,预测变化为 $\beta_t e_t(k_t^\top k')$。若两者正交,影响为零;若它们相似,更新就会传播过去。因此下图的 remove/write 应理解为共享线性空间中的定向修改,而非删除一个与其他内容隔离的字典条目。

$$ \Delta S=\beta_t e_tk_t^\top,\qquad \Delta S\,k'=\beta_t e_t(k_t^\top k'). $$

这也解释了 DeltaNet 为什么仍然有容量和干扰问题:纠错改善了写入规则,却没有取消共享有限维空间这一事实。相比 GLA 按坐标遗忘,Delta Rule 的方向由当前 key 决定。

FIG. 115 · 原报告第 115 页点击图片放大 ↗
116

最后才用 query 读取更新后的状态

最后,query 从更新后的状态读取 $o_t=S_tq_t$,本步输出就完成了。整轮计算依次是:生成向量与步长,用 key 检查预测,写入误差修正,再用 query 读取。下图给出了完整计算图。由于输出使用的是新状态,当前写入也参与读取,后面的分块输出必须保留这项对角线贡献。

FIG. 116 · 原报告第 116 页点击图片放大 ↗

放在 Llama 3 旁边:DeltaNet 替换了注意力里的什么

上面讲的是核心记忆操作。要理解周围为什么还有卷积、激活、归一化,最好把它与一个具体的 softmax attention 子层对照。这里选择 Llama 3 官方实现:两边都接收当前层的表示,都输出相同宽度的向量并加回残差;变化发生在序列信息怎样被保存和读取。

同一个注意力子层位置,两条信息处理路径

Llama 3 · Softmax Attention

依据 Meta 官方实现重绘 · GQA

  1. 输入 x → RMSNorm子层之前的归一化
  2. Linear → Q / K / VQ、K 加 RoPE;V 保持投影结果
  3. Q 与历史 K 匹配点积分数 ÷ √d,加因果 mask
  4. Softmax → 加权 V每个 query 的历史权重非负、和为 1
  5. 合并各头 → Linear投影回模型宽度
  6. 加回输入 x完成注意力子层的残差更新
历史在哪里?逐 token 保存 K / V;GQA 让多个 query 头共享 KV 头。

DeltaNet · 原博客架构

依据 Part III 重绘 · 未加 RoPE

  1. 输入 x → RMSNorm沿用子层之前的归一化
  2. 并行生成 Q / K / V 与 βQKV:Linear → Conv → SiLU,QK 再 L2 Norm
    β:输入 → Linear → Sigmoid
  3. 用当前 K 读取旧状态预测 SK 与目标 V 比较,得到误差
  4. Delta Rule → 用 Q 读状态用 K 检查预测,以 β(V − SK) 修正记忆
  5. 输出 RMSNorm → Linear调节读取幅度,再投影回模型宽度
  6. 加回输入 x完成注意力子层的残差更新
历史在哪里?固定形状的状态 S,另有短卷积的近邻缓存。
仅比较序列混合子层,省略 FFN;β 为并列输入分支,不由 Q/K/V 分支串行生成。此图为讲解重绘,非论文原图。

左侧依据 Llama 3 官方 Attention 与 TransformerBlock 实现 重绘,右侧依据原博客 Part III。Llama 3 在子层之前有 RMSNorm,对 Q/K 使用 RoPE,以缩放点积加因果 mask 后做 softmax,并通过 GQA 共享 KV 头。这个版本没有 Q/K 的 L2 Norm,也没有注意力输出投影前的额外 RMSNorm。不要把这一具体配置推广成“所有 softmax 模型都不做 QK 归一化”。

先看 softmax 原本完成的工作

对一个 query,Llama 3 从缓存中取出历史 K/V,计算每个合法位置的分数,再归一化为权重:

$$ z_{tj}=\frac{\widetilde q_t^\top\widetilde k_j}{\sqrt{d_k}},\qquad a_{tj}=\frac{e^{z_{tj}}}{\sum_{l\le t}e^{z_{tl}}},\qquad o_t=\sum_{j\le t}a_{tj}v_j. $$

波浪号表示已应用 RoPE。分数除以 $\sqrt{d_k}$ 调节尺度,softmax 让一个 query 对各历史位置的权重非负且总和为 1,因此形成由当前 query 决定的加权平均。位置关系由 RoPE 注入,信息条目仍按 token 单独保存。

DeltaNet 把这条路径换成“更新共享状态,再从状态读取”。它没有那一整行经过 softmax 归一化的历史权重,因此要重新设计状态稳定性、局部信息与输出尺度。这些组件共同构成另一套注意力机制,并非逐个与 softmax 的步骤等价替换。

需要解决的问题Llama 3 的 softmax attention原博客 DeltaNet 的安排
怎样保存与选择历史保存各位置 KV,每个 query 重新打分历史压进 S,用 β 控制预测误差写入,再用 q 读出
怎样控制匹配或更新尺度点积除以 $\sqrt{d_k}$;softmax 归一化权重L2 Norm 固定 Q/K 长度,尤其避免 key 长度放大纠错步长
怎样引入位置与近邻信息Q/K 上的 RoPE 提供相对位置关系更新顺序与短因果卷积提供顺序、近邻信息;此版本未用 RoPE
非线性发生在哪里softmax 在一个 query 的历史分数之间产生竞争SiLU 逐元素加工 Q/K/V;β 控制内容依赖的纠错
输出幅度怎样处理归一化权重加权 V,然后做输出投影先读取 $Sq$,再做输出 RMSNorm 和投影

原博客的完整架构图如下。这里仅关注注意力子层,FFN 不展开:

DeltaNet 原博客架构图:左侧是重复堆叠的网络层,右侧展开单个 DeltaNet 模块

左侧的 $N\times$ 表示重复堆叠 N 层,旁路与加号表示将子层输出加回输入的残差连接。右侧展开一个 DeltaNet 模块,各分支的顺序是:

分支经过的处理送入核心更新的内容
q、kLinear → ShortConv → SiLU → L2 Norm结合近邻上下文、长度归一化的读取与写入方向
vLinear → ShortConv → SiLU希望关联到 key 的目标内容
βLinear → Sigmoid当前 token 的纠错步长
输出Delta Rule → RMSNorm → Linear将读出的记忆调整幅度,再投影回模型宽度

Linear 和 ShortConv:先变换特征,再加入近邻

Linear 是可学习的线性投影:对当前 token 的特征做加权组合,分别生成 q、k、v 等。它本身不读取相邻 token。ShortConv 是一个很短的因果卷积窗口,让投影后的向量再看一眼附近的历史。例如窗口为 4,位置 t 会组合自己与前 3 个位置,而不会读取未来。

这里使用 depthwise 形式,每个特征通道有自己的一组卷积权重,独立沿时间轴计算。可以把它理解为:q/k/v 在进入长期记忆之前,先带上几个邻居的线索。短卷积负责近邻信息,矩阵状态负责跨更长距离的关联。原博客还提出,这可能帮助模型在一层里形成“看见相似上下文,就预测随后内容”的归纳模式;这是作者对收益来源的解释。

ShortConv 的一个通道究竟算什么

设 z 是 Linear 的输出,a 是某个特征通道,忽略偏置,一个宽度为 4 的因果卷积可以写为:

$$ \widetilde z_{t,a}=c_{0,a}z_{t,a}+c_{1,a}z_{t-1,a} +c_{2,a}z_{t-2,a}+c_{3,a}z_{t-3,a}. $$

四个 c 是这个通道学习到的权重,并在不同时间位置复用。比如该通道最近四个数为 8、4、2、0,权重为 0.5、0.25、0.25、0,那么结果为 $4+1+0.5=5.5$。换一个通道,用的是另一组权重。序列开头缺少的历史通常用零补齐;生成时保留短窗口缓存即可,不需要为这层卷积保存整段序列。

SiLU 与 β:非线性变换和纠错强度

SiLU 对每个数字 z 计算 $z\,\sigma(z)$,其中 $\sigma(z)=1/(1+e^{-z})$ 把输入映射到 0 与 1 之间。SiLU 平滑地调整特征,允许少量负值;它不会把所有特征变成总和为 1 的概率。β 分支则只用 sigmoid,把步长控制在 $(0,1)$,决定这次误差修正走多远。它与 GLA 中对旧状态逐列缩放的 α 门,作用不同。

L2 Norm:让 key 的长度不再偷偷改变步长

L2 Norm 将一个向量除以它的长度。例如 $(3,4)$ 的长度为 $\sqrt{3^2+4^2}=5$,归一化后是 $(0.6,0.8)$,长度为 1,方向不变。这里对每个 head、每个位置的 q 和 k 分别操作,不是在 token 之间做归一化。

softmax 本身并不会把 key 的长度归一化:分数过大时,即使权重总和仍为 1,也可能变得过度尖锐。这里 L2 Norm 的直接依据来自 Delta Rule 的更新式。

这对 key 尤其关键。前面已经算出,一次更新后,同一个 key 的预测变化为 $\beta(v-Sk)\|k\|_2^2$。如果 key 的长度不受控制,实际纠错幅度除了 β,还会被 $\|k\|_2^2$ 放大,甚至越过目标。单位 key 使这一项变为 1,于是 β 才直接对应“修正多少比例”。query 归一化则控制读取时的向量尺度;它并不要求不同 key 彼此正交。

L2 Norm 和 RMSNorm:计算很接近,目标尺度不同

两者确实都在用当前向量自身的平方和来缩放它。在这里,它们都沿同一个 token 的特征维度计算,不跨 token,也不跨 batch。 差别首先在于分母用“平方和”还是“平方的平均值”,其次在于 RMSNorm 通常还带有可学习的逐特征缩放。

把一个 token 的待归一化向量记为 $x=(x_1,\ldots,x_d)$。先忽略防止除零的 ε,也暂时不加可学习参数:

$$ \begin{aligned} \operatorname{L2Norm}(x)&=\frac{x}{\sqrt{\sum_{a=1}^{d}x_a^2}},\\ \operatorname{RMSNorm}_{g=1}(x)&=\frac{x}{\sqrt{\frac1d\sum_{a=1}^{d}x_a^2}} =\sqrt d\,\operatorname{L2Norm}(x). \end{aligned} $$

所以,它们不是两种完全不同的运算:不计 ε 和可学习缩放时,输出方向相同,只差一个 $\sqrt d$ 的倍数。 用同一个 $x=(3,4)$ 来算,差别会很直观:

操作分母输出归一化后固定的量
L2 Norm$\sqrt{9+16}=5$$(0.6,0.8)$整个向量的长度为 1
RMSNorm,先令 g=1$\sqrt{(9+16)/2}\approx3.536$$(0.849,1.131)$特征的均方根为 1;向量长度为 $\sqrt2$

“均方根为 1”不表示每个特征都等于 1,而是这些特征的平方平均为 1。若有 1024 个特征,且原来全是 1,L2 Norm 会把每个特征变成 $1/32$,RMSNorm 则仍输出 1。L2 控制整根向量的长度,RMSNorm 控制每个特征的整体量级,使它不因特征数变多就自动缩小。

实际常用的 RMSNorm 还会乘一个训练得到的向量 $g$:

$$ y_a=g_a\frac{x_a}{\sqrt{\frac1d\sum_{b=1}^{d}x_b^2+\varepsilon}}. $$

同一向量的各特征先共用一个分母,再分别乘自己的 $g_a$。g 通常在不同 token 间共享,但共享参数不等于混合各 token 的统计量。当 g 的分量不同,它还会改变各特征的相对比例,最终向量不必保持原方向,均方根也不必恰好为 1。L2 归一化通常没有这一步可学习缩放;两者都不做 LayerNorm 那样的减均值操作。RMSNorm 论文 §4

“沿特征归一化”具体是在哪个轴上算

对一批多头向量,形状可以写成 $[\text{batch},\text{token},\text{head},\text{feature}]$。对 Q/K 做每头 L2 Norm 时,先固定 batch、位置 t 和头 h,只沿最后一个特征轴求平方和:

$$ \widehat k_{b,t,h,a}=\frac{k_{b,t,h,a}} {\sqrt{\sum_{c=1}^{d_k}k_{b,t,h,c}^{\,2}}}. $$

每个 token、每个 head 都有自己的分母。比如两个 token 分别是 $(3,4)$ 和 $(30,40)$,L2 的分母分别为 5 和 50;不会将四个数放在一起求和。

RMSNorm 同样可以对每个 head 的输出特征独立计算。若实现将所有头合并后再归一化,则分母覆盖合并后的特征;RMSNorm 这个名字本身不决定 head 轴是否合并,要看模块指定的归一化宽度。Llama 3 子层前的 RMSNorm 覆盖当前 token 的整个 hidden 向量;Q/K 的 L2 Norm 则覆盖各自 head 内的特征。两者在本文这些用法中都不沿序列轴做统计。

输出 $o_t=S_tq_t$ 本身已经包含历史信息,但归一化时只使用这个输出向量的特征,不会再把 $o_1,\ldots,o_t$ 一起算分母。这与 softmax 沿历史位置归一化注意力分数是不同的轴。

为什么 key 用 L2,读取结果用 RMSNorm

数学形式接近,不代表可以在模型里直接互换。Key 的长度会进入状态更新的步长;读取结果的幅度则决定传给下一层的信号有多大。 两处所需要的尺度不同。

对 key,我们需要的是 $\|k\|_2^2=1$,使同一 key 上的纠错比例就是 β。若不改其他公式,直接用 g=1 的 RMSNorm 替代,key 的平方长度会变成 $d_k$,实际纠错比例就成了 $\beta d_k$。例如 $d_k=128$、$\beta=0.5$,原本修正一半误差,会变成修正 64 倍误差,可能严重越过目标。可以重新配套设计尺度与步长,但不能原样互换。

对输出,核心先读出 $o_t=S_tq_t$。这个向量的整体大小会受状态内容、写入过程和 query 匹配影响;即使 Q/K 已归一化,也没有规定每次读出的均方根是多少。RMSNorm 把这种整体幅度变化压下去,让后面的投影主要处理各特征之间的模式,再由 g 学习合适的通道尺度。

例如两次读取分别得到 $(3,4)$ 和 $(30,40)$:它们有相同的特征比例,第二次只是整体大了 10 倍。忽略 ε,两者经同一 RMSNorm 后相同,后续 Linear 就不会仅因这 10 倍幅度差而产生 10 倍输出。若是 $(3,4)$ 与 $(4,3)$,特征比例不同,归一化后仍能区分。它主动去掉整体正缩放信息,并保留可供后续层加工的特征模式;这是架构选择,并非完全不损失信息。

这里归一化的是读出的 o,不是状态 S,因此不会在这一步改写记忆,也不能替代 key 归一化来约束状态递推。它有助于控制传给下一层的信号尺度,但不是“状态或梯度绝不会不稳定”的保证。原博客将输出归一化放在最终投影之前;图左侧子层前的 RMSNorm 则调节当前层输入,二者位置与对象不同。原博客的输出归一化说明

为什么不用一个线性注意力分母来模仿 softmax

早期线性注意力常用正特征映射 φ,把读取写成:

$$ o_t=\frac{\sum_{j\le t}v_j\big(\phi(k_j)^\top\phi(q_t)\big)} {\sum_{j\le t}\phi(k_j)^\top\phi(q_t)}. $$

分母归一化的是对各历史位置的匹配权重。它过小时,商及梯度可能被放大。原博客援引的研究因此考虑去掉这个分母,改为对输出特征做归一化。RMSNorm 的分母来自输出自身的平方均值,并有 ε 保护;它不恢复原来的注意力概率分布,也不会保证与 softmax 相同的输出。

同样,SiLU 是对每个特征独立做非线性变换,不能复现 softmax 在不同历史位置之间的竞争;ShortConv 也不是 RoPE 的等价替代。它们各自为新的状态式架构提供有用的性质。

这些组件也有消融实验支持:在原博客 340M 参数、15B 训练 tokens 的设置下,移除 short conv 后,PPL 从 28.24 升到 29.08,Retrieval 从 22.7 降到 18.6。在保持 1+ELU 激活不变时,L1 换为 L2 归一化使 Retrieval 从 11.5 提高到 21.8;因此不能把归一化与激活同时改变后的收益都归给某一项。这里的结果属于这组架构消融,和后面的 1.3B 结果不是同一规模。原博客的组件分析与消融表

117

小维度下,更准确的联想回忆

回到前面介绍的 MQAR 多查询联想回忆测试,这种写入规则在小维度下很有优势。下图固定序列长度 512、key/value 对数 64,DeltaNet 在模型维度 64 时已经达到图示的 100%,同处的 Mamba 约 80%、GLA 约 22%。随着维度增大,若干基线也趋近满分。这支持 DeltaNet 在这组 MQAR 设置中更有效地使用有限状态,真实语言和检索任务则还要继续考察。

FIG. 117 · 原报告第 117 页点击图片放大 ↗
CHAPTER 08 / SLIDES 118—128

DeltaNet 也能分块并行

从残差依赖走到三角求解、WY 表示和完整输出式。

118

DeltaNet 并行:先把真正的依赖找出来

普通线性注意力的写入向量 $v_t$ 由当前输入产生,可以提前算好;DeltaNet 写入的则是扣除旧预测后的修正量。沿用 slide 的记号,把它叫作 $u_t$:

$$ \begin{aligned} u_t &=v_t^{\rm new}-v_t^{\rm old}\\ &=\big[\beta_tv_t+(1-\beta_t)v_t^{\rm old}\big]-v_t^{\rm old}\\ &=\beta_t(v_t-S_{t-1}k_t). \end{aligned} $$

最后一步用到了 $v_t^{\rm old}=S_{t-1}k_t$。因此,图中的 old/new 写法正好回到前面讲过的预测误差:u 是本次修正多少,v 是希望记住的目标内容。

用 u 表示后,状态更新变得很简洁:

$$ S_t=S_{t-1}+u_tk_t^\top. $$

这已经与普通线性注意力的外积累加长得一样了。但 u 要用旧状态算,不能像 v 一样只看当前输入就生成。后面并行推导要解决的,正是怎样高效求出这一组 u。

FIG. 118 · 原报告第 118 页点击图片放大 ↗
119

假如 U 已知,输出就回到普通线性注意力

先与 slide 一样,从 $S_0=0$ 开始。连续累加刚才的外积,到第 p 步便有:

$$ S_p=\sum_{t=1}^{p}u_tk_t^\top,\qquad o_p=S_pq_p=\sum_{t=1}^{p}u_t(k_t^\top q_p). $$

它与普通线性注意力的区别,只是把原始 value $v_t$ 换成了修正量 $u_t$。将 u 按 token 行排列成 U,与 Q/K 的排列方式一致,整段输出就是:

$$ \boxed{O=\big((QK^\top)\odot M\big)U.} $$

这就是 slide 的公式。$(QK^\top)\odot M$ 仍负责因果匹配,U 则在输出计算中承担原来 V 的角色,所以被称为 pseudo-values。它不是另一个学习到的 value 投影,而是已经扣除了旧预测的写入修正。

M 包含对角线,因为 $o_p$ 读取的是完成第 p 次写入后的状态。于是问题被集中到一处:如果能高效算出 U,后面的输出计算就可以直接使用矩阵乘法。

FIG. 119 · 原报告第 119 页点击图片放大 ↗
120

写成了矩阵公式,为什么还不能直接并行?

上一页虽然已经写出 $O=((QK^\top)\odot M)U$,但其中的 U 还没有算出来。普通线性注意力的 v 只需从当前输入生成;这里的 u 却是:

$$ u_t=\beta_t(v_t-S_{t-1}k_t). $$

它需要先知道旧状态对当前 key 的预测。例如计算 $u_2$ 要用 $S_1$,而 $S_1$ 又要先完成第一个 token 的更新;计算 $u_3$ 则要等 $S_2$。按照原来的递推做法,这些步骤仍然排成一条长链。

这就是下图 “Not scalable” 指出的障碍:最后一步可以做矩阵乘法,不代表前面准备 U 的过程已经能并行。 如果 U 仍要沿整段序列逐 token 计算,主要的串行瓶颈就还在。

FIG. 120 · 原报告第 120 页点击图片放大 ↗
121

并行化的关键:怎样高效算出 U?

既然拿到 U 后,输出就能按普通线性注意力计算,接下来真正要解决的便是 U 的构造。这正是本页提出的想法:找到高效计算 pseudo-values 的办法,就能复用前面已经介绍过的注意力计算路径。

但“有了 U 就好了”还只是目标,这一页并没有给出求解算法。接下来报告会先展开状态更新,观察其中反复出现的矩阵乘积,再借助它的特殊结构,把计算组织到较小的块内。我们顺着这条路线看下去。

FIG. 121 · 原报告第 121 页点击图片放大 ↗
122

从误差更新,展开到 slide 的状态转移连乘

先把更新式中含旧状态的两项合并:

$$ \begin{aligned} S_t &=S_{t-1}+\beta_t(v_t-S_{t-1}k_t)k_t^\top\\ &=S_{t-1}-\beta_tS_{t-1}k_tk_t^\top+\beta_tv_tk_t^\top\\ &=S_{t-1}\underbrace{(I-\beta_tk_tk_t^\top)}_{H_t} +\beta_tv_tk_t^\top. \end{aligned} $$

最后一行就是 slide 的第二行。$H_t$ 描述旧历史如何经过本次纠错,$\beta_tv_tk_t^\top$ 则是本次目标带来的新增项。两项一起构成同一次 Delta Rule 更新,不能只留下后面的新增项。

连续代入三步,看清每笔写入经过什么

从零初始状态出发,前两步为:

$$ S_1=\beta_1v_1k_1^\top,\qquad S_2=\beta_1v_1k_1^\top H_2+\beta_2v_2k_2^\top. $$

第三步将整个 $S_2$ 乘 $H_3$,再加入第三笔目标:

$$ S_3=\beta_1v_1k_1^\top H_2H_3 +\beta_2v_2k_2^\top H_3 +\beta_3v_3k_3^\top. $$

第一笔写入经历后两步修正,第二笔经历最后一步修正,第三笔刚写完。把这个规律推广到 t 步,就是 slide 的最后一行:

$$ \boxed{S_t=\sum_{i=1}^{t}\beta_i(v_ik_i^\top) \left(\prod_{j=i+1}^{t}(I-\beta_jk_jk_j^\top)\right).} $$

这里求和枚举每一笔写入,连乘枚举这笔写入之后经历的所有更新。约定乘积按 $H_{i+1}H_{i+2}\cdots H_t$ 排列,不能任意换序;$i=t$ 时没有后续更新,空乘积就是 I。

这条式子按 slide 假设 $S_0=0$。若从一个块入口 S 开始,还应加上 $SH_1\cdots H_C$,代表更早历史经过整个块的变化。分块公式中的历史项就来自这里。

FIG. 122 · 原报告第 122 页点击图片放大 ↗
123

为什么要处理 generalized Householder 矩阵的连乘

上一页每笔写入之后都跟着一段 $H_j=I-\beta_jk_jk_j^\top$ 连乘。本页 slide 将它称为 generalized Householder matrices 的乘积。真正有用的结构是:H 不是任意大矩阵,而是单位矩阵减去一个外积。

对单位 key,$H_tx=x-\beta_tk_t(k_t^\top x)$:它只缩小 x 沿当前 key 方向的分量,其余正交分量保留。当 $\beta\in(0,1)$ 时是定向收缩;经典 Householder 反射对应单位 key 且 $\beta=2$。因此这里的 generalized 不应被理解成每一步都在做普通镜面反射。

这个特殊结构提供了一个机会:不必把每一步都当成任意大矩阵来保存和相乘,可以尝试用少量向量表示它们的累计效果。下一页介绍的 WY 表示,就是实现这件事的工具。

补充推导:两步连乘怎样整理成外积和

直接展开 $H_1H_2$:

$$ \begin{aligned} H_1H_2 &=I-\beta_1k_1k_1^\top-\beta_2k_2k_2^\top +\beta_1\beta_2k_1(k_1^\top k_2)k_2^\top\\ &=I-(\beta_1k_1)k_1^\top -\left[\beta_2k_2-\beta_1\beta_2k_1(k_1^\top k_2)\right]k_2^\top. \end{aligned} $$

第一行只是把括号逐项乘开,第二行把末尾同为 $k_2^\top$ 的两项合并。于是只需定义:

$$ w_1=\beta_1k_1,\qquad w_2=\beta_2\left(k_2-w_1(k_1^\top k_2)\right), $$

就有 $H_1H_2=I-w_1k_1^\top-w_2k_2^\top$。关键不在于消掉所有计算,而是将大矩阵的连乘整理成向量系数:$w_2$ 已经包含第一步怎样改变第二步方向的信息。下一页的 WY 表示就是把这次整理推广到 n 步。

FIG. 123 · 原报告第 123 页点击图片放大 ↗
124

两条表示:旧记忆怎样变,新记忆怎样加

这一页的两条箭头,分别整理一次状态更新的两个部分:原先已有的记忆经过这一段更新后变成什么,以及这一段输入自己带来了什么。 $P_n$ 和 $S_n$ 的作用不同,先分清这一点,才容易理解为什么需要 W 和 U。

Pₙ 是变换,不是记忆内容

DeltaNet 每一步都让旧状态右乘 $I-\beta_tk_tk_t^\top$。连续做 n 步,旧状态就要依次经过 n 个这样的变换。把它们合在一起,叫作 $P_n$:

$$ P_n=\prod_{t=1}^{n}(I-\beta_tk_tk_t^\top) =I-\sum_{t=1}^{n}w_tk_t^\top. $$

如果进入这一段之前的状态是 $S_{\rm in}$,原有记忆经过这一段后留下的贡献就是 $S_{\rm in}P_n$。 P 只描述“怎样变”,本身不包含要记住的 value。它是 $d_k\times d_k$ 的变换矩阵,与存内容的 $d_v\times d_k$ 状态矩阵不同。

第一条箭头的意义在于:累计变换原本是一长串矩阵乘积,现在可以用一组 w 和已有的 k 表示它。每个 $w_t$ 都包含前序更新对当前方向的影响,所以不用把那串连乘逐个保留为大矩阵。w 怎样计算,可以在后面的折叠推导中查看。

Sₙ 是从零开始,这一段输入自己形成的记忆

现在把初始状态设为零,只处理这 n 个 token。它们最终形成的状态可以写成:

$$ S_n=\sum_{t=1}^{n}u_tk_t^\top. $$

这是 slide 的第二条箭头。u 就是先前介绍的 $\beta_t(v_t-S_{t-1}k_t)$,已经扣除了当时的预测;因此这不是把原始 v 简单相加,而是把经过纠错的写入累积起来。后面的 token 对前面记忆的修正,也已经算在这些 u 里。

u 里面还有 S,究竟怎样算出来?

确实还有依赖。$S_n=\sum u_tk_t^\top$ 只是表示方式,单独写出它,并没有解决 u 的计算。关键是:旧状态也由前面的 u 构成,可以把它代进去。 先看从零开始的前两步。

第一步没有旧记忆,所以:

$$ u_1=\beta_1v_1,\qquad S_1=u_1k_1^\top. $$

第二步原本需要用 $S_1$ 预测。代入刚才的外积表示:

$$ \begin{aligned} u_2&=\beta_2(v_2-S_1k_2)\\ &=\beta_2\big(v_2-u_1(k_1^\top k_2)\big). \end{aligned} $$

现在只需拿已算好的 $u_1$,乘两个 key 的匹配程度 $k_1^\top k_2$,就得到第一笔写入对当前预测的贡献;不用先把整个 $S_1$ 矩阵构造出来。第 t 步同理,扣除所有更早写入的贡献:

$$ \boxed{u_t=\beta_t\left(v_t-\sum_{j<t}u_j(k_j^\top k_t)\right).} $$

这条递推式说明 u 的值由什么决定:依次代入可以算出正确答案。但它不规定高效实现必须按 $u_1,u_2,\ldots$ 逐个执行。原博客后续使用 UT 变换,将这些相互影响先整理成只依赖本块 K、β 的系数矩阵,再通过矩阵乘法批量生成 U;W 也使用同一份系数。

因此需要分清两步:这里展开 S,解释 u 的定义;下一页补充 UT 如何把这个递推关系变成块内矩阵计算。最终的加速不仅来自“不同 chunk 同时递推”,还来自块内计算方式本身的改写。

两条表示怎样合起来

两条表示合起来,就能处理非零的入口历史:

$$ \text{实际出口状态} =\underbrace{S_{\rm in}P_n}_{\text{原有记忆经过变换}} +\underbrace{S_n}_{\text{本段从零开始形成的记忆}}. $$

这里右侧的 $S_n$ 专指本页零初始状态的结果。不是额外运行两遍模型,而是利用这两部分的表示,把实际更新重新组织起来;下一页的分块算法正要用到这件事。

W、U 就是把 w、u 按 token 排在一起

保存的内容在本页公式中的作用
W:各个 $w_t^\top$ 按行排列由这一段 K、β 算出的变换系数配合 K 表示 $P_n=I-W^\top K$,用于处理入口历史
U:各个 $u_t^\top$ 按行排列这一段从零开始时的写入修正,由 K、V、β 算出配合 K 表示 $S_n=U^\top K$,用于累积本段内容

W 和 U 都是处理当前序列时计算的中间结果,不是模型参数中的投影矩阵。w 与 key 同为 $d_k$ 维,u 与 value 同为 $d_v$ 维;大写只是将这些向量按位置放进矩阵,并没有引入另一种操作。

为什么算出 pseudo-values,就能用 regular linear attention?

关键看第二条表示。普通线性注意力先累加 $v_tk_t^\top$,再用 query 读取;现在把 v 换成 u,就有相同的运算形式。对任意位置 p:

$$ o_p=S_pq_p=\sum_{t\le p}u_t(k_t^\top q_p) \quad\Longrightarrow\quad O=\big((QK^\top)\odot M\big)U. $$

因此 slide 的 idea 是:先把 Delta Rule 的纠错影响算进 U,再用普通线性注意力的因果加权方式计算输出。 “Regular”说的是后半段可以复用的计算形式;模型仍然使用 Delta Rule,不能把 U 换回原始 V,也不能省掉准备 U 的成本。第一条 P/W 表示则为下一页接入各块的旧历史做好准备,两条线在分块时合到一起。

图中第二个求和末尾印成了 $k_n^\top$,各项应分别配自己的 $k_t^\top$;正文使用修正后的下标。

用一步更新核对:旧记忆与本段新增为什么能相加

取一维状态,$k=1$、$\beta=0.5$、目标 $v=3$。这一段只有一个 token,所以 $P_1=1-0.5=0.5$,从零开始的状态为 $S_1=0.5\times3=1.5$。此时 w=0.5、u=1.5。

若实际入口为 2,直接做 Delta Rule 得到 $2+0.5(3-2)=2.5$;用两部分的表示,则是 $2\times0.5+1.5=2.5$。P 决定旧记忆的贡献,零入口的 S 决定本段内容的贡献,二者一起得到同一个实际结果。

补充推导:w 怎样从前面的变换得到

令 $H_t=I-\beta_tk_tk_t^\top$。若前面的累计变换已经写成 $P_{t-1}=I-\sum_{j<t}w_jk_j^\top$,再乘一步:

$$ P_t=P_{t-1}H_t =P_{t-1}-\beta_t(P_{t-1}k_t)k_t^\top. $$

将新外积前的向量记为 $w_t$,就有:

$$ w_t=\beta_tP_{t-1}k_t =\beta_t\left(k_t-\sum_{j<t}w_j(k_j^\top k_t)\right). $$

这样每增加一步,只需在外积和里加上 $w_tk_t^\top$,第一条表示就能继续成立。

来源:原博客 Part II 的 WY 与分块表示

FIG. 124 · 原报告第 124 页点击图片放大 ↗
125

同一组 W/U,怎样接到分块图里

这页 slide 和后面博客图中的 W/U,正是上一页的那两组向量,只是现在每个 chunk 各算一组。 从每个块的第一个 token 开始,按零入口构造本块的 u,同时构造描述本块变换的 w;不需要先等到真实的块入口状态。

Slide 写着 Recurrent W/U construction,展示的是按递推关系构造 W/U 的这一步。这是理解算法的中间形式,不能据此把最终实现概括成“chunk 内仍然逐 token 算 u”。 原博客接下来专门引入 UT 变换,进一步将这项构造改为适合 GPU 的矩阵运算。

FIG. 125 · 原报告第 125 页点击图片放大 ↗

从递推定义到块内批量计算,还差 UT 这一步

前面 u 的公式中,“第 j 次写入影响第 t 次写入多少”的系数,由 K 和 β 决定,不需要等真实状态 S 到来。UT 变换先把直接、间接的影响整理成一个块内矩阵 T。拿到 T 后,就可以计算:

$$ U=T\operatorname{diag}(\beta)V,\qquad W=T\operatorname{diag}(\beta)K. $$

$\operatorname{diag}(\beta)$ 给每个位置乘自己的步长,T 将前序纠错的累计影响作用到这些目标上。结果与递推定义相同,但这里用矩阵运算批量生成各位置的 U/W,不必逐 token 更新完整状态后再求下一个 u。T 本身也要计算;“块内并行”并不意味着所有工作无需准备、一次矩阵乘法就结束。

完整的分工是:

阶段怎样计算序列方向上怎样组织
准备 U/W构造块内变换,再用矩阵乘法生成 U/W各 chunk 独立准备,块内运算矩阵化
传递状态接入真实入口,计算下一块的入口图中的同一条状态链按 chunk 递推
计算输出历史读取加块内因果矩阵乘法入口准备好后,各 chunk 及块内各位置批量计算

这就是“chunk 间递推、chunk 内并行”的主要含义。递推公式用于描述因果关系,UT 与分块算法决定怎样高效执行,不能将两者混为一谈。原博客:UT Transform Through the Lens of Graph Theory

只看 T 的定义:它为什么不需要等待 u 或 S

沿用原博客的记号,将直接影响系数放入矩阵:

$$ A=\operatorname{tril}(-\operatorname{diag}(\beta)KK^\top,-1),\qquad T=(I-A)^{-1}. $$

A 的第 (t,j) 项是 $-\beta_tk_t^\top k_j$,只保留 $j<t$ 的项。u 的递推关系合起来就是 $U=\operatorname{diag}(\beta)V+AU$;将 AU 移到左侧,就得到 $U=T\operatorname{diag}(\beta)V$。

A、T 只由本块 K 和 β 决定,因而不用等 u 或真实入口状态。T 的构造仍是一个有计算步骤的小型三角问题;原博客讨论利用其结构求解,而不是调用一般稠密求逆。这里给出定义以说明依赖关系,不将它展开成另一套推导教程。

博客图接着画了什么?

Slide 展示 W/U 的递推构造关系,博客图展示“准备好之后怎样传递状态、怎样算输出”;它本身并没有展开 U/W 的 UT 构造步骤。两张图使用同一套分工。博客下标 $[i]$ 表示第 i 个 chunk;$S_{[i]}$ 是这一块收到的真实入口状态,$S_{[i+1]}$ 是交给下一块的出口。它们与上一页表示零初始结果的 $S_n$,所处的计算范围不同。

博客图的上半部分正是上一页“旧记忆经过变换 + 本块从零形成的记忆”:

$$ S_{[i+1]}= \underbrace{S_{[i]}(I-W_{[i]}^\top K_{[i]})}_{\text{入口历史经过本块变换}} +\underbrace{U_{[i]}^\top K_{[i]}}_{\text{本块从零形成的记忆}}. $$

也就是说,$I-W_{[i]}^\top K_{[i]}$ 就是本块的 P,$U_{[i]}^\top K_{[i]}$ 就是本块的零入口状态。 第 124 页的两条线在这里合成一次完整的块更新。每块的 W/U 可以提前准备,但这个式子仍要等上一块传来 $S_{[i]}$,所以图中称它为 Sequential Chunk-Level State Passing。

DeltaNet 的分块算法:上半部分传递块边界状态,下半部分并行计算各块输出。

图源:原博客 Part II

为什么图中的输出用 U − WSᵀ,而不直接用 U?

因为本块 U 是按零入口计算的;真实入口已经带着旧记忆,对当前 key 可能已经有了部分预测,所以实际需要写入的修正还要扣除历史影响。W 正是把这份影响带入本块计算的系数,整块扣除项写成 $WS^\top$。入口为零时,它消失,直接使用 U 即可。

于是博客图下半部分的输出式可以分成两项理解:

输出中的部分在做什么
$Q_{[i]}S_{[i]}^\top$当前 query 直接读取块入口携带的历史
$((Q_{[i]}K_{[i]}^\top)\odot M)(U_{[i]}-W_{[i]}S_{[i]}^\top)$用普通线性注意力的因果加权方式,读取本块的实际写入修正

这仍然是前面熟悉的“历史读取 + 块内贡献”。博客的 Parallel Output Computation 指:入口状态准备好后,每块已经拥有计算输出所需的数据,不用再等上一块的输出完成。图中从入口状态直接通向各输出的路径对应第一项,经过块内下三角匹配的路径对应第二项。

因此这两页连起来讲的是:用 W/U 表示一块的更新效果,以 UT 将它们的构造矩阵化;随后按块传递真实历史,再批量计算输出。下一页的实验检验这种分工能加速多少。

公式补充:块入口怎样与 W/U 合起来

设块长为 C,入口状态为 S。本块的 W/U 按零入口构造;将它们按 token 行排列后,上一页两个外积和分别为:

$$ P_C=I-W^\top K,\qquad \sum_{t=1}^{C}u_tk_t^\top=U^\top K. $$

旧历史经过 $P_C$,再加上本块新增内容,得到:

$$ \boxed{S_{\rm out}=S(I-W^\top K)+U^\top K.} $$

若只处理到块内第 p 步,同样有:

$$ \begin{aligned} S^p&=S\left(I-\sum_{t\le p}w_tk_t^\top\right) +\sum_{t\le p}u_tk_t^\top\\ &=S+\sum_{t\le p}(u_t-Sw_t)k_t^\top. \end{aligned} $$

因此非零入口带来的写入修正是 $u_t-Sw_t$。乘 query 后,每个位置的输出为:

$$ o_p=Sq_p+\sum_{t\le p}(u_t-Sw_t)(k_t^\top q_p). $$

将这些输出按行排列,就回到普通线性注意力的分块形式:

$$ O=QS^\top+\big((QK^\top)\odot M\big)(U-WS^\top). $$

当 S 为零,历史项消失,就恢复前面的 $O=((QK^\top)\odot M)U$。M 包含对角线,因为读取发生在本步更新之后。

126

长序列与大头维度上的加速

在单张 H100 上,这种分块算法相对 DeltaNet 的递推实现获得了明显加速。下图横轴是序列长度,纵轴是加速倍数;head dimension 为 64、128、256 时,16K 位置分别达到约 24×、33×、37×。比较的是同一模型的两种执行方式,因此这组结果最直接地说明了并行与硬件利用带来的收益。

FIG. 126 · 原报告第 126 页点击图片放大 ↗

原博客 Part II 的速度比较 补充了配置:两种版本都用 Triton 实现,模型维度固定 2048,通过调整 batch 使总 token 数保持 16,384。于是序列变长时 batch 变小,头维度变大时头数也减少;两种变化都会压缩朴素递推可利用的 batch×head 并行度。

这也解释了为什么序列越长、头维度越大,加速越明显。Chunkwise 仍能在序列块上展开部分计算,递推却越来越缺少独立工作;更大的头维度也更有利于发挥块状矩阵乘法的吞吐。接下来还需要看,这种算法上的收益放到模型训练中能兑现多少。

127

回到训练吞吐,加速还能兑现多少

把范围扩大到模型训练,长序列上的吞吐优势依然存在。下图将每批 token 数固定在约 16K,长度与 batch 从 2K×8 调到 16K×1。DeltaNet 的吞吐大致从 41K 降到 37K tokens/s,略低于 GLA;Transformer++ 则从约 51K 降到 29K,在长序列组被两者超过。这里纵轴是每秒处理的 token 数,越高越快。

FIG. 127 · 原报告第 127 页点击图片放大 ↗

Mamba 在这组设置下约为 23–26K tokens/s。DeltaNet 比 GLA 稍慢,可能与额外构造 W/U、处理残差有关,具体开销占比还需单独测量。这里比较的是不同模型的训练吞吐,前面的 24–37× 则比较同一个 DeltaNet 的分块与递推算法,二者回答的是不同层次的效率问题。

128

DeltaNet 改善了哪些指标

质量收益则更有取舍。在下表相同的 1.3B / 100B tokens 设置下,DeltaNet 的 PPL 为 16.9、LM Eval 为 51.6、Retrieval 为 34.7。相比 GLA 的 17.2 / 51.1 / 37.7,前两项改善,检索却低了 3.0 分。合成联想回忆更好,并未自动变成所有检索都更好;这也引出了用直接 Attention 补足压缩记忆的想法。

FIG. 128 · 原报告第 128 页点击图片放大 ↗
CHAPTER 09 / SLIDES 129—139

混合架构与统一视角

压缩状态与直接检索怎样配合,还有哪些问题未解。

129

用滑动窗口保留近处的精细信息

一种互补方式是让 DeltaNet 负责长程压缩记忆,滑动窗口 Attention 负责近处的精细交互。下图的 Hybrid 1 交替使用这两类层,其中窄带因果 mask 表示每个 query 只读取最近窗口。若窗口大小固定为 $w$,注意力部分约花 $O(Lwd)$,同时保留窗口内的 K/V。

FIG. 129 · 原报告第 129 页点击图片放大 ↗
130

用少数全局 Attention 层提供直接检索

另一种方式是只在少数层保留全局检索。下图的 Hybrid 2 在第二层和中间层使用 Global Attention,其余层使用 DeltaNet;完整下三角 mask 表示这些全局层仍能访问全部前缀。模型因此保留了直接读取远处信息的机会,也仍承担这些层的平方交互与随长度增长的 KV cache,只是全局层的数量减少了。

FIG. 130 · 原报告第 130 页点击图片放大 ↗
131

压缩记忆与直接检索的互补

两种混合都改善了结果,但优势并不完全相同。下表中,滑窗交替的 Hybrid 1 为 PPL 16.6、LM Eval 52.1、Retrieval 40.0;两层全局的 Hybrid 2 则为 16.6、51.8、47.9。两者 PPL 相同,滑窗的平均语言分数略高,而全局混合更擅长检索。这与它们分别强调局部交互和完整历史访问的设计相呼应。

FIG. 131 · 原报告第 131 页点击图片放大 ↗

只沿 Retrieval 列做差会更清楚:滑窗混合比纯 DeltaNet 的 34.7 提高 5.3 分,但仍比 Transformer++ 的 41.8 低 1.8 分;两层全局混合提高 13.2 分,并比 Transformer++ 高 6.1 分。表下注明仍是 1.3B / 100B tokens 的这组实验,说明少量直接检索与压缩状态能够互补。全局 Attention 带来的额外成本需要另行衡量,质量表并没有证明它与纯 DeltaNet 一样便宜。

132

用同一组问题理解不同架构

从不同起点出发,可以走到相近的状态递推。Transformer 去掉 softmax,得到线性注意力,再加入输入依赖的遗忘门,就走到 GLA;连续状态空间模型经过离散化、结构化转移与选择性设计,也会得到类似的形式。下图汇总了这些联系,理解它们的关键是每一步怎样改变状态,而不只是模型名称之间的相似。

FIG. 132 · 原报告第 132 页点击图片放大 ↗
133

两种选择性,两种状态转移

DeltaNet 为这张关系图补充了另一种选择性。普通线性注意力保留旧状态,GLA 按通道衰减它,DeltaNet 则使用 $I-\beta_tk_tk_t^\top$ 沿当前 key 方向修正。对角门与秩一修正对应不同的几何结构,也影响它们能够采用怎样的高效并行算法。

FIG. 133 · 原报告第 133 页点击图片放大 ↗
134

与 Test-time Training 的连接

把状态看成在线更新的学习器,还能连接到 Test-time Training(TTT)。下图中从 TTT 到 DeltaNet 的关系,依赖于使用线性预测器 $f(x_t;W_t)=W_tx_t$,再配合平方损失与相应的在线更新约定。这样就回到前面推导过的误差外积;如果学习器非线性,或损失与批内更新方式不同,得到的模型也会不同。

FIG. 134 · 原报告第 134 页点击图片放大 ↗
135

Relax 了什么:从固定纠错结构到结构化矩阵乘法

前面几种模型都在回答同一个问题:当前 token 到来时,怎样变换旧状态?先将 DeltaNet 写成“旧状态的变化 + 本次写入”:

$$ S_t=S_{t-1}\underbrace{(I-\beta_tk_tk_t^\top)}_{\text{旧状态的转移矩阵}} +\beta_tv_tk_t^\top. $$

这里的转移不是任意矩阵。I 表示默认原样保留;减去的 $\beta_tk_tk_t^\top$ 是一个秩至多为 1 的修正,而且左右两侧绑定同一个 key。于是它沿当前 key 方向修改记忆,对正交方向保持不变。

slide 的 Relax,是放宽两项结构限制

图上的 “Relax identity plus rank one” 意思是:保留“简单主体加低秩修正”这一思路,但不再要求主体必须是 I,也不再要求修正两侧必须来自同一个 k。plus 是结构名称里的“加上一项”,这一项本身可以带负号。箭头终点写成:

$$ \boxed{S_t=S_{t-1}\big(\operatorname{diag}(\alpha_t)-a_tb_t^\top\big) +v_tk_t^\top.} $$

将 $D_t=\operatorname{diag}(\alpha_t)$,就能把前后变化逐项对上:

被放宽的限制DeltaNet 原来的形式slide 的更一般形式新增的自由度
默认怎样保留旧状态I$D_t=\operatorname{diag}(\alpha_t)$每个 key 通道可有不同的保留比例
从旧状态读什么来修正左因子 $\beta_tk_t$$a_t$可以学习不同的读取组合
修正写回哪个方向右因子 $k_t$$b_t$写回方向可以与读取方向不同,也不必等于新内容的 key

a、b、k 都是 $d_k$ 维向量,v 是 $d_v$ 维向量。这里仍保留一个秩一修正,并没有把它放宽成任意满秩矩阵。 将 D 设为 I,a 设为 βk,b 设为 k,就恢复 DeltaNet 的旧状态转移。slide 新式的写入项没有显式 β,可将原来的 βv 整体吸收到新式的 v 中;恢复完整 DeltaNet 时,这一项也必须对应,不能只对上括号。

Structured Matmuls,就是利用矩阵的特殊结构来做乘法

Matmul 是 matrix multiplication,即矩阵乘法;structured 表示这个矩阵由少量有规律的部分构成。这里是对角矩阵减去秩一外积,属于 diagonal-plus-low-rank(DPLR,对角加低秩)结构。外积 $ab^\top$ 的各列都只是 a 的不同倍数,因此它看似有 $d_k^2$ 个数,实际上只需 a、b 两个向量就能描述。

计算时,不必先生成一个完整的 $d_k\times d_k$ 转移矩阵。把乘法展开:

$$ \begin{aligned} S_t&=S_{t-1}D_t-(S_{t-1}a_t)b_t^\top+v_tk_t^\top. \end{aligned} $$

这三个部分各自有明确操作:$S_{t-1}D_t$ 对旧状态逐列缩放;$S_{t-1}a_t$ 从旧状态读取一个 value 向量,再沿 $b_t$ 方向将它扣回去;$v_tk_t^\top$ 加入本次新内容。这样就同时有了“各通道保留多少”和“从哪里读取、向哪里修正”的自由。

例如 $a=(1,0)^\top$、$b=(0,1)^\top$,Sa 取出状态的第一列,$(Sa)b^\top$ 将这列内容放在第二列的位置,因此减去它会用第一列去修正第二列。如果 a、b 都等于 $(1,0)^\top$,则是读取第一列、再从第一列扣除。这个例子只演示读写方向解耦,不表示实际模型的通道天然对应某种语义。

为什么有结构就能少算,以及它没有保证什么

一般稠密转移 H 有 $d_k^2$ 个元素,计算 $S H$ 的工作量为 $O(d_vd_k^2)$。利用上述结构,SD 只需缩放状态的 $d_vd_k$ 个元素;Sa 是一次矩阵向量乘;$(Sa)b^\top$ 与 $vk^\top$ 各构造一次外积。每项都是 $O(d_vd_k)$,所以一步总工作量仍为 $O(d_vd_k)$。当 $d_v=d_k=d$ 时,就是从一般稠密转移的 $O(d^3)$ 降到 $O(d^2)$;转移的参数也只需 α、a、b 这几个向量。

这是单步计算的优势。跨很多步训练时,仍要处理转移的连乘;两个“对角减秩一”相乘,通常会产生更多低秩项,并不会始终只剩一个外积。怎样组织分块、控制中间表示与充分利用 GPU,仍需专门算法。独立的 a/b 也不再自动具有单位 key 的投影几何,稳定性需要额外设计。

因此 slide 的问号提出的是一个设计方向:在限制较强但高效的 Delta Rule 与完全一般的状态转移之间,能否找到表达力更强、仍然算得便宜的结构?它没有在这一页给出一套已经完成的模型。

接到延伸阅读:KDA 是其中一个受约束的特例

KDA 先用 D 衰减,再做 Delta Rule,按本文状态方向展开为:

$$ \begin{aligned} S_t&=S_{t-1}D_t(I-\beta_tk_tk_t^\top)+\beta_tv_tk_t^\top\\ &=S_{t-1}\big(D_t-\underbrace{(\beta_tD_tk_t)}_{a_t} \underbrace{k_t^\top}_{b_t^\top}\big)+\beta_tv_tk_t^\top. \end{aligned} $$

所以 KDA 也落在这种结构里,但 a、b 仍由 D、β、k 绑定生成,没有任意独立学习。这保留了“先遗忘、再纠错”的含义,也给专门的分块算法留下可利用的结构。D 与 key 外积一般不能交换顺序;本文采用 $d_v\times d_k$ 状态,所以 D 位于括号中纠错矩阵的左侧。Kimi Linear 报告 §3、§6.2

延伸阅读中的矩阵例子 会具体演示 D 怎样逐通道遗忘。

FIG. 135 · 原报告第 135 页点击图片放大 ↗
136

可结合,还需要可高效执行

再一般化,就得到 $S_t=S_{t-1}M_t+v_tk_t^\top$。把每一步看成仿射变换,连续步骤的组合满足结合律,因此可以考虑 parallel scan。但一般矩阵 $M_t$ 的组合也可能很昂贵。下图提出的 “General Associative Operators?” 留下了这个问题:除了可结合,还能否找到紧凑、适合硬件的表示?

FIG. 136 · 原报告第 136 页点击图片放大 ↗
137

将三条线重新接起来

DeltaNet 将三件事连在一起:用预测误差改善有限状态中的关联记忆,用 Householder 的紧凑表示实现高效分块,再通过混合 Attention 补充直接检索。前面的实验也分别检验了这三层收益。好的更新规则与好的并行算法需要一起设计,而模型最终能做到什么,仍要放回具体任务中考察。

FIG. 137 · 原报告第 137 页点击图片放大 ↗
138

仍然开放的问题

报告最后留下了两个开放判断:某种 Attention 式检索可能对我们期待的能力很重要,语言也未必是亚平方序列模型最有影响力的应用领域。混合模型的检索结果支持了前一种探索,音频、视频和其他超长序列则让后一种可能值得关注。更具体的问题是,在目标任务与总成本约束下,压缩记忆、直接检索和更新规则怎样配合才合适?

FIG. 138 · 原报告第 138 页点击图片放大 ↗
139

带着公式回到原文

这套思路最终落在几次可以亲手检查的等价变形上:历史外积和如何变成状态,块内逐步输出如何变成下三角矩阵乘法,误差驱动的更新又如何通过 W/U 接回分块计算。若想再读一遍,可以从三个 token 的数值例子出发,顺着推导回到下面列出的原文,让每个公式都对应到一次具体计算。

FIG. 139 · 原报告第 139 页点击图片放大 ↗

完整材料:原始报告。进一步阅读:DeltaNet 论文 · GLA 论文 · FlashAttention · FlashAttention-2

AFTER THE TALK · 更新于 2026.09.13

新模型架构:记忆、检索与混合

从前文的 Delta Rule 出发,读懂这些模型如何组织历史与计算。

A01

从 Delta Rule 走向实际模型

前面的报告以 Thanks 收尾,但它提出的问题还在继续:怎样让一个模型既能高效处理很长的历史,又能在需要时准确找回其中的细节?如今许多模型把这两种能力分给不同的层。大部分层用 DeltaNet 一类的递推状态处理序列,隔几层再用一次能访问历史条目的注意力。这里的“混合”通常是沿网络深度交替排列不同层,一个 token 会依次经过它们。

读这些架构时,可以先问三个问题:历史保存成什么、当前 query 能读取哪些历史、层与层之间怎样传递结果。GDN/KDA 主要改变第一个问题;稀疏注意力主要改变第二个问题;AttnRes、mHC 和 Gated Residual 则改变第三个问题。MoE 另管每个 token 的前馈计算,和注意力的选择是两件事。

Gated DeltaNet:先遗忘,再按预测误差写入

沿用前文 $S\in\mathbb R^{d_v\times d_k}$ 的记号,一个注意力头的 Gated DeltaNet(GDN)可以写成:

$$ \bar S_t=\alpha_t S_{t-1},\qquad e_t=v_t-\bar S_t k_t,\qquad S_t=\bar S_t+\beta_t e_t k_t^\top,\qquad o_t=S_tq_t. $$

这仍然是我们熟悉的误差修正,只是在读取当前 key 的预测之前,先对旧状态乘上遗忘系数 $\alpha_t$。$\alpha_t$ 控制历史保留多少,$\beta_t$ 控制本次修正多大。例如,单位 key 对应的旧预测为 3、目标为 5,若 $\alpha=0.8$、$\beta=0.5$,遗忘后的预测为 2.4,误差为 2.6,更新后的预测就是 3.7。把两个门分开看,就容易理解它为何既能清理旧记忆,又能有针对性地更新关联。

下图给出了 Qwen3.8-Flash-Next 报告中的 GDN 模块。Q、K、V 先经过短因果卷积,吸收邻近 token 的信息;Q、K 再做 L2 归一化,进入中间的 Gated Delta Rule。状态读取之后还有归一化和输出门,决定哪些输出通道进入下一层。输出门、遗忘门和写入门作用在三个不同位置,不能只因为都叫 gate 就混为一谈。

GDN 的完整计算路径。Qwen3.8-Next 技术报告,图 2,PDF 第 3 页;这是该版本的实现示意。

来源:Qwen3.8-Next 技术报告 §2.1.1。报告使用 $d_k\times d_v$ 状态,上式已转置到本文约定;图中的 sigmoid 输出门也是这一版本的具体设计。

KDA 的通道遗忘:同一个头里,不同列可以留得不一样久

先固定一个 attention head。GDN 在每个 token 到来时产生一个标量 α,将这个头的整个旧状态统一乘 α。KDA 则产生 $d_k$ 个数 $\boldsymbol\alpha_t=(\alpha_{t,1},\ldots,\alpha_{t,d_k})$,分别控制这个头的 key 特征通道。这里的“通道”是 key 向量的某个坐标,对应状态矩阵的一列;不是一个历史 token,也不是一个 attention head。

按本文 $S\in\mathbb R^{d_v\times d_k}$ 的方向,两者的遗忘步骤是:

$$ \begin{aligned} \text{GDN:}\quad \bar S_t&=\alpha_tS_{t-1},\\ \text{KDA:}\quad \bar S_t&=S_{t-1}D_t, \qquad D_t=\operatorname{diag}(\alpha_{t,1},\ldots,\alpha_{t,d_k}). \end{aligned} $$

右乘这个对角矩阵,就是将第 c 列的所有数都乘 $\alpha_{t,c}$:$\bar S_{t,a,c}=S_{t-1,a,c}\alpha_{t,c}$。同一列中所有 value 特征共用一个系数,但不同列的系数可以不同。若每个通道的系数恰好都等于 α,就退回 GDN 的统一衰减。门由当前输入经过可学习分支产生,因此不同 token、不同 head 的门也可以不同。KDA 原始报告 §3

一个两列的状态,怎样分别保留 90% 和 20%

设旧状态和两个查询方向为:

$$ S=\begin{bmatrix}10&0\\0&20\end{bmatrix},\qquad q_A=\begin{bmatrix}1\\0\end{bmatrix},\qquad q_B=\begin{bmatrix}0\\1\end{bmatrix}. $$

用 $q_A$ 读取得到 $(10,0)^\top$,用 $q_B$ 读取得到 $(0,20)^\top$。暂时只观察遗忘步骤,先不做后面的纠错写入:

$$ \begin{aligned} \text{GDN,}\alpha=0.9:\quad \bar S&=\begin{bmatrix}9&0\\0&18\end{bmatrix},\\ \text{KDA,}\boldsymbol\alpha=(0.9,0.2):\quad \bar S&=S\begin{bmatrix}0.9&0\\0&0.2\end{bmatrix} =\begin{bmatrix}9&0\\0&4\end{bmatrix}. \end{aligned} $$

GDN 将两种读取都保留到原来的 90%;KDA 则让第一列保留 90%,第二列只保留 20%。仍用 $q_A$ 查询,会得到 $(9,0)^\top$;用 $q_B$ 查询则得到 $(0,4)^\top$。更准确的说法是:KDA 可以让不同 key 坐标承载的状态分量,以不同速度衰减。

若连续三步都用这组门,而且暂时不做任何纠错写入,第一列保留 $0.9^3=72.9\%$,第二列只剩 $0.2^3=0.8\%$,状态变成 $\operatorname{diag}(7.29,0.16)$。这才是“一部分保留得久、另一部分衰减更快”的来源。实际门会随输入改变,状态也会继续更新,不能用这两个固定幂完整描述真实模型。

遗忘之后,仍然要按预测误差更新

KDA 并不是只有上述列缩放。接着使用遗忘后的状态做预测、纠错、读取:

$$ \begin{aligned} \bar S_t&=S_{t-1}D_t,\\ e_t&=v_t-\bar S_tk_t,\\ S_t&=\bar S_t+\beta_te_tk_t^\top,\\ o_t&=S_tq_t. \end{aligned} $$

例如接着前面 $\bar S=\operatorname{diag}(9,4)$ 的结果,本步 $k=(0,1)^\top$、目标 $v=(0,8)^\top$、$\beta=0.5$。记忆预测为 $\bar Sk=(0,4)^\top$,所以误差为 $(0,4)^\top$,写入其中一半后,状态成为 $\operatorname{diag}(9,6)$。第二列先从 20 衰减到 4,再根据当前目标修正到 6:α 控制保留,β 控制纠错,两者作用不同。

“一个方向”不等于“一条独立的记忆”

上例特意让查询对齐坐标轴,便于单独观察两列。真实 key 往往分布在多个坐标上,一条关联的外积 $vk^\top$ 会同时写入多列。同一列也会承载许多历史 token 的贡献,因此 KDA 不能直接指定“只忘掉第 7 个 token”,也不能保证一个通道只对应某个固定概念。

对于混合方向 $q=(1,1)^\top/\sqrt2$,遗忘前读取为 $(10,20)^\top/\sqrt2$,遗忘后为 $(9,4)^\top/\sqrt2$。后者并不是前者乘上一个统一系数:各坐标保留程度不同,读出的特征比例也改变了。因此“不同 key 方向有不同遗忘速度”只是直觉,严格的操作是沿学习到的 key 坐标逐列缩放,再做共享状态上的 Delta Rule 更新

从通道连乘接到完整状态转移

若只考虑连续遗忘,某个通道 c 从位置 i 到位置 t 的保留量为:

$$ \prod_{j=i+1}^{t}\alpha_{j,c}. $$

GDN 在同一个 head 内对所有通道共用这份乘积;KDA 每个通道各有一份,这与前面 GLA 的门前缀相呼应。不过,加上 Delta Rule 后,各步的 key 修正也会影响状态,完整历史不能只看这个乘积。

将本步的完整更新展开:

$$ S_t=S_{t-1}\underbrace{D_t(I-\beta_tk_tk_t^\top)}_{\text{先遗忘,再纠错}} +\beta_tv_tk_t^\top. $$

这里 D 与后面的纠错矩阵一般不能调换。进一步展开为 $D_t-(\beta_tD_tk_t)k_t^\top$,就能接到第 135 页的“对角减秩一”结构。论文用 $d_k\times d_v$ 状态,因此把同一操作写在状态左侧;上式已经整体转置为本文的约定。

这些细化的门不改变 S 的形状,历史缓存仍是固定大小;训练仍可通过专门推导的分块算法实现。Kimi K3 使用这一 KDA 机制,并进一步约束门的数值范围以便高效计算,后面 K3 小节会继续说明。Kimi K3 报告 §2.1.1

MLA 与稀疏注意力:另一条历史读取路径

MLA(Multi-head Latent Attention)压缩的是每个 token 的 KV 表示。 它把完整的多头 K、V 表示为一个较小潜向量的投影,缓存主要保留潜向量;计算时可以利用投影分解来完成注意力。历史中每个 token 仍有自己的缓存条目,所以缓存总量仍随序列长度增长。Gated MLA 则在注意力读出后再加输出门。MLA 本身没有规定只看哪些 token,既可以用于全局注意力,也可以和稀疏选择结合。Kimi K3 报告 §2.1.2

DSA、QSA 这类稀疏注意力先筛选,再做主要的 softmax 注意力计算。 一个便宜的 indexer 估计哪些位置值得读取,主注意力只处理选出的集合。这里省下的是 query 与大部分历史条目之间的计算;若仍逐 token 保留历史,缓存并不会因此变成固定大小。至于 FlashAttention,它解决的是给定注意力运算怎样分块和减少显存读写,可以与这些架构中的 softmax 注意力配合;它本身不把 softmax 改成 Delta Rule。IndexCache 论文 §2.1

A02

Qwen3.5:三层状态记忆,一层全局读取

Qwen3.5 是把前文思想接到实际大模型上最直观的例子。以 Qwen3.5-397B-A17B 为例,60 层由 15 组重复单元构成:每组三层 Gated DeltaNet,接一层 Gated Attention;每个注意力子层之后都有 MoE。沿着一个 token 的路径看,就是连续三次用状态整合历史,再通过一次全局 softmax 注意力直接读取历史条目,然后把结果送往下一组。

  1. GDN + MoE
  2. GDN + MoE
  3. GDN + MoE
  4. Gated Attention + MoE

这里的 Gated Attention 使用 GQA(Grouped-Query Attention):32 个 query 头共享 2 个 KV 头,减少重复的 KV 缓存;attention 输出再经过门控。它与 GDN 中负责遗忘的门不同,也不是 MLA 的潜向量压缩。因此,Qwen3.5 这一组合可以准确地概括为 Gated DeltaNet + 带输出门的 GQA。45 个 GDN 层保存递推状态,15 个全局层仍保留随上下文增长的 KV 缓存。整个模型由此保留直接检索能力,同时减少需要维护长 KV 序列的层数。

MoE 接在每次注意力处理之后,负责当前 token 的非线性特征变换。这个版本有 512 个路由专家,每个 token 选择其中 10 个,另有 1 个共享专家。这也解释了 397B 总参数与约 17B 激活参数为何可以同时成立:模型拥有很大的专家库,一次前向只调用其中一部分。视觉编码器把图像转换成可供主干处理的特征,原生多模态训练让文本与视觉信息共同进入这套混合架构。模型还训练了 MTP(多 token 预测)模块,为一次提出多个候选 token 的推测解码提供支持。

这一节的层数、头数和专家数都对应 397B-A17B,不能原样套到 35B、27B 等其他尺寸。来源:Qwen3.5-397B-A17B 官方模型卡 · Model Overview

A03

Kimi K3:KDA 与 MLA 之外,还改了信息传递方式

Kimi K3 把同样的三比一节奏换成了 KDA + Gated MLA。93 层中有 69 层 KDA 和 24 层 Gated MLA:23 组“三层 KDA、一层 MLA”之后,末尾再放一层 MLA,让最终表示再做一次全局读取。KDA 用通道级遗忘和 Delta Rule 整合历史,MLA 则保留逐 token 的全局访问。K3 的 MLA 使用 NoPE,不给 query 和 key 加显式位置编码;穿插的 KDA 提供顺序与远近敏感性。

下图把 K3 的几种信息流放在一起。沿主干向上看,KDA 和 MLA 交替处理序列,前馈部分主要采用 Stable LatentMoE(最初一层为 dense FFN);连接早期 block 与后续模块的路径则属于 Attention Residuals。视觉输入经 MoonViT-V2 与投影模块进入同一个主干。

Kimi K3 整体架构。Kimi K3 技术报告,图 2,PDF 第 3 页。KDA/MLA、Stable LatentMoE 与 AttnRes 分别处理序列、特征和深度方向的信息。

KDA 的门控,也要适合分块计算

通道级遗忘更灵活,但分块计算会用到累积衰减的比值;若某个通道连续衰减得过快,倒数就可能超出数值范围。K3 把每一步的 log-decay 限制在 $(-5,0)$。在 16-token 小块内,累积 log-decay 因而不小于 $-80$,相关重缩放落在 BF16 的动态范围内。这使原本需要逐位置对计算的小块对角区域也能使用 Tensor Core 矩阵乘。它正好呼应了前文:递推式可以分块只是第一步,门控的数值范围还会影响能否高效地做矩阵计算。

AttnRes 选择从哪些深度取信息,LatentMoE 决定怎样处理特征

普通残差连接不断把新结果加进同一个向量。AttnRes 则让当前模块对早期表示计算权重,选择性地组合它们。K3 实际使用 block 级版本:块内先累加,块间再做 attention,以减少保存每层输出的开销。这里的 attention 沿着网络深度选择信息,和 KDA/MLA 沿着 token 序列处理信息是两个维度。

Stable LatentMoE 则先把路由分支从 7168 维压到 3584 维,在较窄空间中调用 896 个专家中的 16 个,再把聚合结果归一化、投影回主干宽度;2 个共享专家保留全宽路径。这样扩大专家池时,路由通信与专家计算更可控。额外的归一化、有界的 SiTU-GLU 激活和负载均衡设计用于稳定大规模稀疏训练。K3 因而同时改进了“读哪些历史”“取哪些层的表示”与“用哪些专家加工”,并非只替换了一个注意力算子。

来源:Kimi K3 技术报告 §2.1—2.4,图 2官方架构配置摘要

A04

GLM-5.3-Flash:KDA 搭配可检索的稀疏 MLA

GLM-5.3-Flash 进一步把混合架构中的“直接读取历史”做成稀疏计算。它的 45 层主干有 34 层 KDA 线性注意力、11 层 DSA 稀疏注意力,主体重复三层 KDA 接一层 DSA,最后还有一层 KDA。整体上,KDA 负责持续整合历史;需要访问具体位置时,DSA 先筛选候选,再用 MLA 形式的主注意力读取这些位置。因此可以把它理解为 KDA + 稀疏 MLA

  1. KDA
  2. KDA
  3. KDA
  4. DSA(MLA + indexer)

这个组合中,MLA 和 DSA 并不代表两层互相替代的注意力。MLA 决定 KV 怎样表示,DSA 决定这次读取哪些位置。 开源配置把 MLA 的 KV 潜维度设为 512,主注意力采用 NoPE;indexer 则用学习到的权重把 key 按 4 个 token 一组压缩来估计相关性,再展开选中块的 token 索引。主注意力使用这些索引访问 KV,配置中的选择预算为 2048,并保留末尾未完成块。这减少了检索阶段的开销,同时保留对所选 token 的细粒度读取。

除了注意力,GLM-5.3-Flash 使用四路 mHC(Manifold-Constrained Hyper-Connections) 残差流。可以把它理解为:层与层之间有多条表示通道,模块从中读取、向其写回;残差混合矩阵经过约束,避免多层传播时任意放大。前馈部分在前 3 层采用 dense FFN,之后使用 MoE:288 个路由专家中选 8 个,另加 1 个共享专家。视觉编码器负责图像与视频输入,MTP 模块用于多 token 预测。官方发布口径为约 320B 总参数、18B 激活参数。

这里的 KDA 类型、34/11 层数、NoPE 与压缩 indexer 细节,依据官方权重配置和 Transformers 的对应模型定义核对;GLM-5 的早期报告并不能独自说明这套 Flash 新架构。来源:GLM-5.3-Flash 官方模型卡官方 config.jsonTransformers 模型实现

A05

Qwen3.8-Flash-Next:状态记忆与按块检索结合

Qwen3.8-Flash-Next 延续 Gated DeltaNet 的三比一混合比例,但在继续预训练阶段把原来的全局注意力层换成了 Qwen Sparse Attention(QSA)。这就得到三层 GDN、一层 QSA 的重复结构。下图展示的正是发布模型的这套架构:GDN 和 QSA 后面都接 MoE,子层通过 GR Read/Write 读写四路残差表示;靠近输入处另有一个 n-gram embedding 模块。

Qwen3.8-Flash-Next 整体架构。技术报告图 1,PDF 第 2 页。三层 GDN 与一层 QSA 交替,每个注意力与 MoE 子层都通过 Gated Residual 读写表示。

QSA:先给小块打分,再读其中的 token

普通 DSA 的主要注意力虽然稀疏了,indexer 仍要比较 query 和整个历史。QSA 把 indexer 的 key 每 4 个 token 做一次平均池化,用一个代表向量给整块打分。query 从这些块里选最多 512 个,再把块索引展开为最多 2048 个原始 token 的位置;末尾尚未凑满 4 个 token 的部分另行保留,以满足因果处理。压缩发生在检索用的 key 上,主注意力读到的仍是选中块内各 token 的 KV。

QSA 的压缩 indexer 与稀疏主注意力。技术报告图 3,PDF 第 5 页。块级分数产生 top-k 索引,展开后形成主注意力的 micro-block mask。

图中两种 mask 对应这两个尺度:检索端每一列代表一个压缩块,主注意力端则展开成连续几个 token。于是,在长度为 $L$ 的输入上,indexer 的比较规模从 $L^2$ 降到约 $L^2/4$,主注意力按所选集合计算。这里降低了常数和主注意力开销,indexer 的渐近复杂度仍是二次的。它与 GDN 的固定大小状态有不同的成本来源。

GR 与 n-gram embedding:另外两处改动

Gated Residual(GR) 保留四路残差表示,读取时用逐元素门决定各通道贡献多少,再把模块输出写回。与 mHC 相比,GR 去掉了残差分支之间额外的混合算子,把表达能力更多放在读取门上,也减少了一次残差数据搬运。它与 Kimi 的 AttnRes 都试图改善跨深度的信息传递,但保存和组合表示的方式不同。

n-gram embedding 则根据连续 token 的组合查表,在第 2 层提供额外的局部模式特征。查表位置可由 token ID 提前确定,因此大表可以放在主机内存并提前取回。报告将规模记为 125B 主体参数、每 token 约 6B 激活,另有 51B 的 n-gram 表;这张表既不是上下文 KV cache,也不是 DeltaNet 的递推状态。MoE 提供稀疏前馈容量,MTP 负责多步候选预测,并跨预测步复用 QSA 索引。

来源:Qwen3.8-Next 技术报告 §2.1—2.3官方发布仓库。图与说明均对应 Flash-Next,不能仅凭“Qwen3.8”这个系列名推广到所有版本。

A06

DeepSeek-V3.2:先选 token,再用 MLA 读取

理解 DeepSeek 后来的稀疏注意力,可以先回到 V3.2。它延续 V3 系列的 MLA 与 MoE 主干,在注意力中加入 DeepSeek Sparse Attention(DSA)。这里有两件彼此配合的事:MLA 把每个 token 的多头 KV 压成一份较小的表示,DSA 再决定当前 query 需要读取哪些 token。V3.2 与 V3.2-Exp 的模型架构相同;正式版还强化了后训练,不能把能力提升都归因于注意力改动。V3.2 技术报告 §2

MLA:每个 token 存一份潜向量,供多个头读取

普通多头注意力为历史位置保存各个头的 key 和 value。MLA 则先把位置 $s$ 的隐状态压成潜向量 $c_s$;各头需要的内容 key 和 value,都是它的线性投影。这样,历史条目仍然一一对应 token,只是每条记录变小了。

更巧妙的是,读取时未必需要把这些多头 K/V 全部还原出来。若第 $i$ 个头的内容 key 为 $k^C_{s,i}=W_i^{UK}c_s$,那么它与当前 query 的内积可以改写为:

$$ (q^C_{t,i})^\top W_i^{UK}c_s =\big((W_i^{UK})^\top q^C_{t,i}\big)^\top c_s. $$

左边先把每个历史条目投影成 key;右边先把当前 query 变换一次,再直接与缓存中的 $c_s$ 匹配。Value 侧也能把投影移到加权求和之后。于是,各头虽然有不同的 query 和输出变换,却能共享同一批潜向量作为核心注意力的 K/V,这就是 MLA 的 MQA 计算形态MLA 的基础定义见 V3 报告 §2.1.1

位置关系走另一条较窄的 RoPE 分支:主注意力缓存的是 $c_s$ 和位置 key $k_s^R$。V3.2 配置中,它们分别为 512 维和 64 维,因此每个 token 的主注意力缓存是 576 个数,再另加索引器缓存与量化元数据。这里的 512 维压缩作用在特征维度,并没有把多个 token 合成一条记录。官方配置

展开推导:投影怎样移到 query 和输出侧,RoPE 又放在哪里

用 $c_s\in\mathbb R^{d_c}$ 表示已经投影并归一化后的 KV 潜向量。记内容 key/value 宽度为 $d_h$,则 $W_i^{UK},W_i^{UV}\in\mathbb R^{d_h\times d_c}$。为了区分内容与位置,完整匹配分数是两次内积之和:

$$ \begin{aligned} q^A_{t,i}&=(W_i^{UK})^\top q^C_{t,i}\in\mathbb R^{d_c},\\ \ell_{t,s,i}&=\gamma\left[(q^A_{t,i})^\top c_s+(q^R_{t,i})^\top k_s^R\right]. \end{aligned} $$

这里 $q^R_{t,i}$ 与 $k_s^R$ 已分别施加位置 $t$、$s$ 的 RoPE;$\gamma$ 表示模型使用的分数缩放,包含头维度缩放及长上下文设置。投影重排前后的内容内积相等,因此沿用同一个 $\gamma$。不能因为重排后的 query 变成 512 维,就擅自换一套缩放。

设本次选中的位置集合为 $\mathcal T_t$,每个头独立计算权重:

$$ a_{t,s,i}=\frac{\exp(\ell_{t,s,i})}{\sum_{u\in\mathcal T_t}\exp(\ell_{t,u,i})},\qquad s\in\mathcal T_t. $$

Value 侧的变形只用到了线性变换对求和的分配律:

$$ \begin{aligned} o_{t,i} &=\sum_{s\in\mathcal T_t}a_{t,s,i}\,W_i^{UV}c_s\\ &=W_i^{UV}\underbrace{\left(\sum_{s\in\mathcal T_t}a_{t,s,i}c_s\right)}_{z_{t,i}\in\mathbb R^{d_c}}. \end{aligned} $$

先在潜空间把 $c_s$ 加权成 $z_{t,i}$,再一次性投影为该头输出,最后拼接各头并做输出投影。共享潜向量不等于共享注意力权重:每个头有自己的 $q^A_{t,i}$、$q^R_{t,i}$,因而仍能读出不同的内容。

下面两张并列图展示同一个 MLA 的两种计算顺序。左边在注意力之前展开各头 K/V;右边把 key 投影移到 query 侧,把 value 投影移到加权汇总之后。右图的 $q^A$ 就对应上面定义的变换后 query。

MLA 的 MHA 与 MQA 计算形态。DeepSeek-V3.2 技术报告附录 A,图 7,PDF 第 20 页;两者通过投影重排连接。

来源:V3.2 报告附录 A。以上按单头补写等价变形,省略量化误差。这里重排的是 MLA 自身的运算;后面删除未选中的位置,则会改变注意力的可见集合。

DSA:用小索引器找位置,再让主注意力细读

MLA 降低了每条历史记录的存储成本,但当前 query 仍可能扫过整个前缀。DSA 在这之前加了一个 Lightning Indexer。它使用专门的检索 query/key,为每个可见位置给出一个分数,再取最高的至多 2048 个位置。所有主注意力头使用这份位置列表,并各自计算 softmax 权重。

索引器的打分可以拆成“各头匹配,再汇总”:

$$ I_{t,s}=\sum_{j=1}^{H^I}w^I_{t,j}\,\operatorname{ReLU}\!\left((q^I_{t,j})^\top k_s^I\right),\qquad s\le t. $$

$q^I_{t,j}$ 表示当前 token 在第 $j$ 个检索头上的需求,$k_s^I$ 是历史位置的一份共享检索 key。先做内积,把负匹配截为零,再用当前 token 产生的 $w^I_{t,j}$ 汇总各头结果。这个分数用于选位置,最终读出 value 的权重仍由主 MLA 重新计算。 上标 $I$ 表示索引器,不能与主注意力的 query/key 混用。官方实现使用部分 RoPE,并在 FP8 打分前对 query/key 做相同的 Hadamard 旋转;在精确算术下这种正交旋转保持内积,同时重新分配各坐标上的幅值。官方 Indexer 实现

下图把两条路径合在一起:索引器产出位置列表,Top-k Selector 据此取出相应的潜向量与位置 key,主注意力再完成匹配和汇总。图中的 $c^Q$ 是当前 query 的中间表示,$c^{KV}$ 才是需要跨 token 保留的内容缓存。

DSA 如何接入 MLA。DeepSeek-V3.2 技术报告图 2,PDF 第 4 页。Lightning Indexer 选择 token,主注意力读取这些 token 的共享潜向量。

举一个缩小的例子:当前有 6 个可见位置,索引分数为 $(0.2,3.1,0.4,2.7,0.8,1.5)$,只保留 2 个位置,就得到 $\mathcal T_t=\{2,4\}$。主 MLA 随后读取 $c_2,c_4$ 及各自的位置 key,每个头只在这两个位置上归一化。未入选的条目仍留在缓存中;下一个 query 可能重新选中它们。这个例子是对选择过程的演示,实际预算为 2048。报告 §2.1 与图 2

怎样学会筛选,以及成本究竟省在哪里

索引器先向已有的密集注意力学习:冻结主模型,保留完整 attention,让索引分布通过 KL 损失拟合主注意力各头汇总后的分布。之后开启 top-k 稀疏训练,让主模型适应所选位置;索引器继续用选中集合上的 KL 目标训练。它的输入做梯度截断,索引器由 KL 目标更新,主模型则由语言建模损失更新。报告 §2.1.1

省下的主要是昂贵的主注意力:忽略头数和宽度时,每个 query 读取至多 $k$ 个条目,整段主要交互变成 $O(Lk)$。索引器仍逐 token 给整个可见历史打分,所以这一路依然有 $O(L^2)$ 的位置对,只是使用更轻的表示与 FP8 计算。DSA 因而适合概括为“低成本全局筛选 + 稀疏精读”;它没有获得 DeltaNet 那样固定大小的历史状态。报告 §2.3

展开计算:从 token 对数量算到缓存与实际内核

长度为 $L$ 的因果序列,索引器需要比较 $1+2+\cdots+L=L(L+1)/2$ 对位置。若有 $H^I$ 个检索头,每个内积宽度为 $d^I$,主要打分量约为 $O(L^2H^Id^I)$,此外还要做 top-k 选择。V3.2 的配置是 $H^I=64$、$d^I=128$;主注意力则有 128 个头。

主注意力的可见位置对数在 $L\ge k$ 时为:

$$ \sum_{t=1}^{L}\min(t,k) =\frac{k(k+1)}2+(L-k)k =Lk-\frac{k(k-1)}2. $$

对 $L=131072$、$k=2048$,这是约 2.66 亿对,而完整因果注意力约有 85.90 亿对,位置对数约为原来的 3.10%。这只是按公式得到的主注意力交互数比例,不能直接当作端到端加速倍数:索引器、投影、MoE、数据搬运等成本仍在,内核的计算形态也会影响实际耗时。

缓存也要分开数:主 MLA 每个 token 保留 512 维潜向量和 64 维位置 key;索引器另外保存一份 128 维 key,FP8 实现还要存缩放因子。历史长度翻倍,条目数仍翻倍。top-k 减少本次读取的条目,不会把其余历史从缓存中删除。维度来自官方配置缓存路径见官方实现

最后还要区分算法与演示代码。官方参考代码有“计算分数后加稀疏 mask”的写法,便于表达语义,但仅加 mask 并不会自动省掉前面的密集乘法;真正的稀疏内核需要根据索引访问并计算选中的 KV。DeepSeek 在 FlashMLA 中提供相应内核。报告也注明,短序列 prefill 可以用带 mask 的 MHA 形态实现相同的稀疏可见集合,因此不能把所有执行场景都概括成同一种内核。

从 V3.2 接到 V4

注意力之外,V3.2 主干有 61 层,前 3 层使用 dense FFN,之后采用 DeepSeekMoE:256 个路由专家中每次激活 8 个,另有 1 个共享专家;配置还包含 1 个 MTP 预测层。注意力选择历史位置,MoE 选择加工当前表示的专家,MTP 则服务于额外的未来 token 预测。这几种选择与预测发生在不同环节。官方配置

带着这套分工再看 V4,变化就更容易定位:V3.2 的 MLA 压缩每个 token 的特征,DSA 从 token 中筛选;V4 的 CSA/HCA 进一步沿序列压缩主 KV,把若干 token 变成一个历史条目。 下一节就沿着“条目本身怎样改变”继续看下去。

A07

DeepSeek-V4:把历史压成多个条目,再分层读取

DeepSeek-V4 展示了另一条路线:历史不必全部收进一个固定大小的状态,也可以压成一系列更短的 KV 条目。它交替使用 Compressed Sparse Attention(CSA)Heavily Compressed Attention(HCA),二者都保留压缩后的历史序列。CSA 压缩得较轻,再挑选相关条目;HCA 压缩得很重,直接访问所有可见的压缩条目。

CSA 的逻辑可以顺着下图理解。历史隐状态先经过带学习权重的压缩器,名义上每 4 个 token 形成一个条目;另一路轻量 indexer 给压缩条目打分,选出 top-k。主注意力再把选中的压缩 KV 与最近 128 个 token 的未压缩 KV 一起读取。局部窗口负责近期细节,也补上当前压缩块尚未完成时的信息。

DeepSeek-V4 的 CSA。技术报告图 3,PDF 第 9 页。先压缩 KV,再筛选压缩条目,与滑动窗口中的细粒度 KV 共同参与注意力。

HCA 则把压缩率提高到 128,不再经过 top-k indexer。压缩条目已经少了很多,当前 query 直接读取全部可见的压缩条目,并同样保留局部窗口。两类层交替后,一部分层进行较细粒度的选择性访问,另一部分层以更粗的分辨率覆盖整个历史。

DeepSeek-V4 的 HCA。技术报告图 4,PDF 第 11 页。更强的 KV 压缩替代了 top-k 筛选,局部窗口仍然保留。

这里有一个与 QSA 很值得比较的差别:QSA 主要压缩检索器的 key,选中后回到原始 token;CSA 连主注意力读取的 KV 也压缩了。 V4 的核心注意力使用 shared-KV MQA,压缩条目同时充当 key 和 value;不能简单把它写成“沿用 V3 的 MLA 再加一个筛选器”。由于历史条目数量仍随输入增长,CSA/HCA 也不同于 GDN 的固定大小状态。

V4-Flash 与 V4-Pro 都采用 4 倍 CSA 压缩和 128 倍 HCA 压缩,但 CSA 的 top-k 分别为 512 与 1024。Flash 的前两层是纯滑窗,Pro 的前两层是 HCA,此后再交替使用两类压缩注意力。前馈部分使用共享专家与路由专家组成的 DeepSeekMoE,最初几层采用 token ID 驱动的 hash routing;四路 mHC 改善残差信息传递,MTP 支持多 token 预测。这些部分分别降低序列读取、前馈计算和生成过程的成本。

来源:DeepSeek-V4 技术报告 §2、§4.2.1官方模型仓库。本节对应 V4 的 CSA/HCA 架构。

A08

GLM-5.2:让相邻层复用“去哪里读”的决定

GLM-5.2 的重点是 MLA + DSA + IndexShare。它没有把大部分注意力层换成 DeltaNet,而是在稀疏注意力已经选出历史位置之后,减少重复选择的成本。主干为 78 层,MLA 的 KV 潜维度为 512,DSA 的 top-k 预算为 2048;前 3 层使用 dense FFN,后续 MoE 每次选 256 个路由专家中的 8 个,并有 1 个共享专家。

为什么还需要 IndexShare?即使主要注意力只读取 top-k,轻量 indexer 仍要为整个历史打分。如果相邻层反复选中相近的位置,每层都独立重算就有冗余。GLM-5.2 在主体部分每四层复用一次索引:一层计算 top-k,后面三层沿用这些位置。开头几层保留独立 indexer,因此实际排布要以配置为准。

下面的算法图来自官方发布说明链接的 IndexCache 论文。右侧 Shared 层直接取缓存的 $T$,但仍然调用自己的 Sparse Attention 与 FFN。复用的是位置列表,不是这些位置的 KV 向量,也不是上一层的 attention 输出。 各层仍用自己的表示完成读取和特征变换。论文称 IndexCache,GLM-5.2 发布说明将采用的跨层共享方案称为 IndexShare。

跨层索引复用的计算过程。IndexCache 论文图 2,PDF 第 3 页;用于说明 GLM-5.2 IndexShare 的机制,并非该模型的完整架构图。

GLM-5.2 还把索引复用用于多步 MTP,并结合 KVShare,降低候选 token 生成阶段的重复工作。需要分清主干跨层共享与 MTP 内的缓存共享:前者只是复用“读取哪些位置”,不能由此推出主干各层共用同一份 KV。也正因为如此,IndexShare 降低计算量时,并不会同比例降低模型的 KV 缓存容量。

把它与 QSA 放在一起看,两者都在处理 indexer 自身的成本:QSA 沿序列方向压缩候选,IndexShare 沿网络深度减少重复选择。来源:GLM-5.2 发布说明 · Architecture for 1M Context官方配置IndexCache 论文

A09

放在一起看:历史在哪里,细节怎样找回

这些模型用不同方式降低历史读取的成本:有的沿深度混合不同注意力,有的压缩每个条目的表示,有的筛选位置或合并序列条目。真正决定差别的,是历史保存在哪里,以及读取时允许损失哪些细节。下表沿着这两个问题回看前面的七种架构;各项出处见对应小节。

模型/版本序列处理的主要组合怎样处理长历史其他关键模块
Qwen3.5-397B-A17B45 GDN + 15 Gated GQA多数层保存状态,周期性全局读取 token KVMoE、视觉编码器、MTP
Kimi K369 KDA + 24 Gated MLA通道级状态记忆,MLA 保留逐 token 全局访问AttnRes、Stable LatentMoE、MoonViT-V2
GLM-5.3-Flash34 KDA + 11 DSA状态记忆与稀疏 MLA 交替,indexer 按块压缩mHC、MoE、视觉编码器、MTP
Qwen3.8-Flash-Next3 GDN : 1 QSA状态记忆与块级筛选交替,读取所选原始 tokenGR、MoE、n-gram embedding、MTP
DeepSeek-V3.2MLA + DSA按 token 缓存潜向量,轻量 indexer 选择 top-2048,主注意力读取所选 tokenDeepSeekMoE、MTP
DeepSeek-V4CSA 与 HCA 交替主 KV 按序列压缩;轻压缩后筛选,重压缩后全读局部窗口、mHC、DeepSeekMoE、MTP
GLM-5.2MLA + DSA + IndexShare保留 token KV,相邻层复用检索位置MoE、带索引与 KV 共享的 MTP

回到 DeltaNet 的出发点,固定大小状态省下了逐 token 保存历史的成本,也把历史关联写进了有限容量的矩阵。混合层给模型增加了另一种访问路径:在部分深度上重新读取 token 或压缩条目。稀疏选择与序列压缩又继续降低这条路径的代价。于是,理解一种新架构时,可以沿着 状态更新 → 历史读取 → 残差传递 → 专家计算 走一遍,比只记一个注意力缩写更容易看清模型实际做了什么。

资料核对于 2026-09-13。本文按具体发布版本记录架构;文中的报告图保留原图结构,仅裁去页边和无关正文,可点击放大。