模型读过的内容存在哪里?从全注意力到混合注意力

1 -> 引言
把一本书交给模型,随后问一句“第七章提到的那台设备,最后换成了什么型号”,看起来只是一次普通提问。对模型来说,这个问题却包含两种性质不同的工作:理解整本书在讲什么,以及从大量背景中找回一个具体细节。前者可以容忍概括,后者往往不能。
这个差别会落实到内存里。保留每个历史位置的表示,查询时可以回头逐项匹配;把历史压进一个固定大小的状态,存储更可控,却要决定哪些信息还能留下。输入窗口写着同一个长度,两套模型可能采用完全不同的保存办法。
混合注意力把几种办法放进同一个网络:部分层处理局部信息或更新状态,另一些层保留更直接的历史访问。设备型号能否找回来,不能只靠窗口长度推断,还要看这些层怎样分工。
2 -> 全注意力保留了什么,又付出了什么
在常见的因果自注意力层中,一个位置产生查询向量 Q,历史位置提供键 K 和值 V。查询与历史键计算相关性,经过归一化之后,对对应的值进行加权汇总。这里的“查询”不是用户输入的搜索词,“键”也不是数据库索引,而是模型训练出来的向量表示。
这种机制的一个重要特点,是当前查询可以重新决定哪些历史位置值得关注。刚才回答人物关系时重要的段落,不一定是现在回答设备型号时重要的段落。保留历史位置的表示,就为这种按问题重新取用信息的方式留下了空间。不过,保存了表示也不保证必然检索正确:训练分布、位置编码、注意力分配和任务难度都会影响结果。
在处理整段输入时,标准全注意力涉及长度随序列增长的成对交互。若序列长度为 n,单头维度为 d,直接计算注意力分数的工作量包含 n 的平方项。FlashAttention 一类实现能改善访存和中间结果存储,但不能因此把完整交互本身称为线性复杂度。数学上的计算量、硬件上的耗时和显存占用,是三个需要分别说明的量。
在逐步生成阶段,系统通常缓存历史键和值,避免每生成一个 token 就把全部前缀重新计算一遍。这里也要注意,“复用历史计算”不等于“读取历史没有成本”。全注意力层仍然需要使用越来越长的缓存;在长序列、多请求并发的服务中,缓存大小与读写带宽都可能成为约束。
多查询注意力、分组查询注意力、压缩潜在表示等设计可以降低缓存常数,但是否随上下文长度增长,要根据实际架构判断。讨论一个新模型时,看到“降低了缓存”便推断它拥有固定大小的全部记忆,通常会把不同层次的优化混为一谈。
3 -> 窗口与状态,是两种不同的取舍
滑动窗口注意力的做法比较直观:每个位置只看最近的一段历史。若窗口宽度固定为 w,一层的直接注意力范围受到这个窗口限制。它仍然能在窗口内逐项匹配,却不再直接访问窗口外所有位置。
“直接访问不到”不等于“完全没有影响”。多层计算可以传递更远的信息,某些架构也会加入全局位置或跨层机制。但这种间接传播不能自动等价为随时读取任意旧位置。若任务是逐字找回很早出现的一个编号,只依赖局部交互是否足够,需要通过任务测试回答。
线性注意力走的是另一条路。以一种经典的核化形式为例,它使用特征映射 phi,将相似性写成 phi(q) 与 phi(k) 的内积,并利用乘法结合律先累积历史的键值外积。对于因果生成,可以维护两个状态:S 保存键值关联的累加,z 保存键特征的累加。新位置到来时更新状态,再用当前查询读出结果。原始线性注意力论文
这个过程不需要逐项保存所有历史键和值。在特征维度和数值精度固定时,状态大小可以不随序列长度增长。代价也在这里:历史信息被汇总进有限的状态,不再是一份逐条保留、可以随意回看的原始记录。不同历史可能产生相近的状态,某些精确检索任务因此更困难。

线性注意力也不是一个只剩下“求和”的固定配方。门控、衰减、状态更新规则以及不同的特征映射,会明显改变模型的记忆行为。某些设计允许模型主动遗忘,也允许不同通道使用不同的保留速度。把这些模型统称为“只能记住一个平均值”,同样不准确。
下面这段 NumPy 示例展示一个很小的事实:对于选定的正值特征映射,显式计算所有历史权重,与递归维护状态,可以得到相同结果。它不实现 softmax 注意力,也不代表任何完整语言模型。
import numpy as np
rng = np.random.default_rng(12)
n, features, value_dim = 12, 4, 3
q = rng.normal(size=(n, features))
k = rng.normal(size=(n, features))
v = rng.normal(size=(n, value_dim))
def phi(x):
return np.maximum(x, 0.0) + 1.0
qf, kf = phi(q), phi(k)
explicit = []
recurrent = []
state = np.zeros((features, value_dim))
normalizer = np.zeros(features)
for t in range(n):
weights = kf[:t + 1] @ qf[t]
explicit.append((weights @ v[:t + 1]) / weights.sum())
state += np.outer(kf[t], v[t])
normalizer += kf[t]
recurrent.append((qf[t] @ state) / (qf[t] @ normalizer))
error = np.max(np.abs(np.array(explicit) - np.array(recurrent)))
assert error < 1e-10
print("equivalence error:", error)
print("state elements:", state.size + normalizer.size)
print("stored K/V elements:", kf.size + v.size)
reverse_state = sum(np.outer(kf[i], v[i]) for i in reversed(range(n)))
assert np.allclose(state, reverse_state)
print("same final state after reordering:", np.allclose(state, reverse_state))
两种计算的最大误差在本次运行中约为 4.44×10⁻¹⁶,已经接近浮点舍入误差。因为它们只是重新安排同一组乘加,并没有换掉核函数。状态里有一个 4×3 的矩阵和一个四维向量,共 16 个数;逐项保存这 12 个位置的键和值,则需要 12×(4+3)=84 个数。投影、其他层和运行缓冲都没算在这里,因此不能把 84/16 当成整个模型的显存收益。
最后三行还暴露了这个简单更新规则的一个局限:保持每组 k、v 不变,只把这些对的累加顺序倒过来,最终 S 和 z 不变。于是,对同一个查询,最终读出的结果也不变。若任务恰好需要区分“先升温再停机”和“先停机再升温”,这个没有时间信息、没有门控的求和状态就不够用。
这并不说明所有线性注意力都忽略顺序。重新编码位置会改变 k、v,带衰减或门控的更新也可能依赖顺序。小例子的用途,是指出当前公式究竟丢掉了什么,进而理解为什么实际架构要增加那些看起来更复杂的更新项。固定维度状态解决了存储增长,如何组织记忆仍需另外设计。
4 -> 为什么把几种机制混在一起
如果固定状态更节省历史存储,为何不把所有全注意力层都替换掉?一个原因是,不同任务对记忆的要求不同。连续理解一段文章、维持语气和局部结构,可能与精确找回远处的一个实体,需要不同的信息保留方式。架构必须同时面对这些需求,而不是只把某个成本指标压到最低。
常见混合设计会在部分层使用递归或局部机制,在另一些层保留全注意力。这样做希望让多数层承担相对便宜的序列处理,让少数层保留更直接的跨位置交互能力。具体比例、顺序和状态形式都属于设计变量,不存在一个脱离任务和训练过程的固定最佳配方。
一个公开实例是 Kimi Linear,其 Transformers 文档描述了 KDA 递归层与间隔出现的完整注意力层相结合的结构。这个例子适合用来理解“混合”发生在网络内部,而不是简单把两个独立模型的答案拼起来。它的具体层配置属于该架构,不能直接推广到所有混合模型。模型结构文档
训练方式也不能从推理形式直接反推。递归状态可以逐步更新,不代表训练时只能一项接一项串行执行;一些算法能够利用扫描或分块计算获得并行性。反过来,训练时能并行,也不代表推理中没有状态依赖。讨论性能时,应明确说的是整段训练、提示处理,还是逐步生成,避免把不同阶段的优势放进同一个数字。
| 机制 | 历史信息主要如何保留 | 需要特别检查的能力 |
|---|---|---|
| 全注意力 | 保留可按位置访问的键值表示 | 很长输入中的有效检索、成本与并发 |
| 滑动窗口 | 保留或访问有限的近期范围 | 远距离依赖如何跨层传递 |
| 递归状态 | 将历史更新进固定维度状态 | 精确回忆、干扰、遗忘行为 |
| 混合结构 | 不同层采用不同方式 | 各层分工是否真正覆盖目标任务 |
只要混合结构里还存在使用完整历史缓存的层,整个模型的推理状态就可能继续随序列增长。增长速度降低,与增长被彻底消除,不能用同一句“常量显存”概括。实际部署还要考虑框架是否提供高效内核,否则理论上少一些运算,未必换来更低延迟。
2026 年 10 月的相关预印本也继续研究混合结构的位置行为差异。这说明“长上下文模型”内部并不是一个同质类别。预印本中的实验值得参考,但不能当成所有架构已经得到一致结论的证据。研究论文
5 -> 测试时,把“读得长”和“记得准”分开
设备记录这个例子可以直接做成测试:先让模型总结最近一段记录,再要求找回很早出现的型号;接着在中间加入多条外观相似、型号不同的记录,最后要求比较前后两次更换。局部概括、远处检索、抗干扰和多点组合,分别给分,才能看出是哪种能力发生了变化。
这些任务还需要改变信息出现的位置。只把答案埋在开头或结尾,可能错过模型对中间位置的弱点。也应区分唯一信息和重复信息:一个型号出现二十次,通常比只出现一次容易找回。测试材料的长度增加时,要保证题目难度变化有记录,否则无法判断下降来自架构、噪声还是题目本身。
成本测试则应分别记录输入处理时间、开始输出的时间、后续生成速度、峰值显存和目标并发下的延迟。相同硬件、相同精度、相同输出长度是基本条件。一个模型单请求更快,却可能在目标并发下出现排队;另一个模型节省缓存,却可能需要额外的状态更新开销。没有具体负载,单一速度数字很难指导上线。
最后还要保留应用层的补救方式。若任务要求精确引用历史原文,可以在外部保存原始文档,通过检索提供必要片段,而不是要求网络内部状态独自承担全部记忆。外部检索不替代模型架构的改进,但能把“必须精确保存的事实”与“允许概括的上下文”分开处理。
若系统用于查设备档案,型号和更换日期应继续保存在可检索的原始记录里;模型内部状态负责组织当前问题和读取到的上下文。这样即使换了一种记忆机制,也能沿着引用回到原文核对。对于允许概括的对话历史和必须精确找回的档案字段,没有必要要求它们使用同一种保存方式。
更多推荐
所有评论(0)