设想你受邀设计一场万人“同好交流大会”的规则。最老实的做法是让所有人都互相打一遍招呼——一万人就是一万乘一万场对话,哪怕每人只说三秒钟,这场大会也要办上一年。标准 Transformer 干的就是这件事:每个 token 都要和序列里的每一个 token 计算注意力分数,序列一长,显存和算力就以平方级速度爆炸。 Reformer 是这场大会的“精明主办方”,它改了两条规则。第一条:入场时每位来宾做一次“兴趣指纹”测试,按你的兴趣方向发一个颜色的手环,同色的人坐在同一个区域。你不需要满场跑,把同区的人聊透就行——这是局部敏感哈希(LSH)注意力。第二条:大厅的留言板只有巴掌大,记不下所有对话,但主办方发现交流过程是“可逆”的,只需记每个人离开时的最终状态,需要细节时倒放录像反推;小吃摊的烤箱也很小,面包只能一炉一炉地烤——这是可逆层与分块前馈。 这篇文章就带你拆解这两个“省”字妙招背后的数学与工程细节,并把它们的代价与适用场景讲明白。
理解 Reformer 的 LSH 注意力与分块可逆层,是掌握长序列 Transformer 显存优化方案的必修课——面试高频考点,工程实战常备。
可视化演示
展示神经网络前向传播、损失计算和反向传播梯度更新的完整过程,动态演示信号在各层间的正向传递与梯度反向回传。
本节配有交互动效(进入视口后自动加载,约 3KB)。
前向传播 输入信号经权重矩阵线性变换后通过激活函数逐层传递 每层计算:z=Wx+b,a=σ(z),σ为激活函数 输出层产生预测值,用于与真实标签比较计算损失 a^(l) = σ(W^(l) × a^(l-1) + b^(l))
注意力瓶颈
假设大会最初的规则是“见面礼制”: 每位来宾都必须和所有其他来宾握手并自我介绍一次。一万人就是将近五千万次握手,大厅再大也塞不下这种社交强度。Transformer 的 self-attention 正是这个“见面礼制”——每个 token 都要去“认识”序列里的每个其他 token。
在 Transformer 中,每个 token 有“查询向量 Q”和“钥匙向量 K”。注意力机制让每个查询去询问所有钥匙:我们俩匹配吗?匹配分数就是查询和钥匙的内积。所有分数拼成一个 n 行 n 列的矩阵,这就是注意力矩阵。n 是序列长度:BERT 时代的 n=512,矩阵只有 26 万格;但当 n 涨到 8192 时,单层单头就是 6700 万格,而 Transformer 往往有几十层、十几个头,逐层保存激活,显存直接爆炸:
显然,“每人都认识所有人”只有在几百人的小聚会才可行。序列一旦变长,这种平方级的社交成本就不可接受了。Reformer 主办方的第一刀,就是砍掉“全员握手”——用一个巧妙的测试,让大家只和“同频”的人深聊,这就是下一节的 LSH。
- 内积打分查询矩阵 Q 与键矩阵 K 的转置相乘,得到 n×n 的相似度得分矩阵,第 i 行第 j 列表示第 i 个 token 对第 j 个 token 的关注候选强度。
- 缩放稳定除以键向量维度 d\_k 的平方根,避免维度变大后点积方差增大、softmax 进入饱和区导致梯度消失。
- 概率归一softmax 把每一行分数变成总和为 1 的注意力权重,相当于“分配给每个 token 的注意力预算”。
- 加权汇聚用归一化后的权重对值矩阵 V 做加权求和,得到每个 token 的最终输出。
哈希分桶原理
主办方在入口摆了一台“兴趣指纹机”: 每位来宾要回答一组关于兴趣方向的问题,机器据此算出你的“兴趣方向角”,然后发给你对应颜色的手环。同色的人大概率兴趣相近,可以坐在一起深聊。
局部敏感哈希(LSH)就是这台机器的数学化身:它是一族特殊的哈希函数,核心性质是——两个向量越相似,被分到同一个桶的概率越高。这和普通哈希(均匀打散,越像越要分开)正好相反。正是这个性质,让“只算桶内注意力”成为一个合理的近似。
Reformer 用的是“角度 LSH”。把每个 token 的向量想象成高维空间里的一支箭,先乘一个随机旋转矩阵 R,把箭头转到一组随机“指南针”方向上,再比较它在所有方向(正负都算)上的投影大小,取投影最大的那个方向作为桶号:
这个函数妙在:两支箭头方向越接近,投影到各随机方向的大小越相近,取到同一个 argmax 的概率就越高。翻译成大会语言:兴趣方向几乎相同的人,大概率拿到同一个颜色的手环;而方向差异大的人偶尔也会撞组,但概率低得多——这就是“局部敏感”的含义。
- 随机旋转投影x 是 token 的向量表示,R 是 d×b 的随机矩阵,xR 得到 b 个方向上的投影分量,相当于把高维向量“压扁”到 b 个随机方向上观察。
- 正负方向拼接把 xR 和 -xR 首尾拼接,同时覆盖每个轴的正反两个方向,候选桶数翻倍为 2b。
- 取最强方向argmax 返回分量最大的下标,这个下标就是桶号 h(x),也就是手环颜色。
LSH注意力机制
拿到手环颜色后,主办方让同色的人坐在同一区域,并且把每个区域再分成固定人数的圆桌。你不需要满场跑,把同桌的人聊透就够了。
LSH 注意力在拿到每个 token 的桶号后做三件事:第一,按桶号排序,让同桶 token 在序列上相邻;第二,把排序后的序列切成固定大小的块;第三,在每一块内部,只允许同桶 token 互相注意。为什么不直接按桶边界切?因为严格按桶切会让块大小参差不齐、还有空桶问题,GPU 并行效率很低;“排序+分块”则让同桶 token 必然相邻,块大小又完全规整。掩码实现如下:
排序之后,同桶的 token 在注意力矩阵上聚成沿对角线的“块”,块外全部被掩码清空——这正是可视化演示里那张块对角矩阵。为了让“聊得来却在不同组”的朋友不丢失,Reformer 还会用多套独立的问题做多轮测试,每轮各自分组、算一遍注意力再取平均,这就是多轮哈希。总复杂度随之从 O(n²) 降到约 O(n×m×n\_r),其中 m 是桶内平均候选数,n\_r 是哈希轮数。
- 全局打分仍然先算所有 token 对的相似度得分,保证数学上还是标准注意力的骨架。
- 掩码矩阵M 的每个元素只有两个取值——同桶为 0,不同桶为 -∞,加到分数上相当于把“跨组对话”永久屏蔽。
- 归一化加和softmax 把同桶分数归一化,再乘 V 得到输出。由于 -∞ 处的权重为 0,实际有效的注意力只存在于桶内。
可逆层设计
第二道坎是“留言板”。大厅只有一张巴掌大的留言板,记不下上万人的每一句对话。主办方想:如果交流过程可以“倒放”,那我只需记每个人离开时的最终状态就够了。
标准 Transformer 训练时确实要“记笔记”: 每一层的前向激活都要保存下来,反向传播才有的算。层数 L、头数 h、序列长度 n 一起乘,内存根本扛不住。Reformer 把“记笔记”改成“拍电影”——只拍结果,过程靠倒放。可逆残差把每层输入对半切成两路 X₁ 和 X₂:
这样一来,前向传播只需把最后一层的输出记在“留言板”上,中间每一层反向时“倒放”一遍就能算出来。标准 Transformer 要存 L 份逐层激活,可逆层只需要 1 份,层数越深省得越多。特别提醒:省的是激活值,不是模型参数——那些权重矩阵该占多少还占多少。
- 输入分流把上一层的输出按特征维对半切开,得到 X₁ 和 X₂,注意力只吃 X₂ 这一路。
- 注意力残差对 X₂ 做注意力计算,结果加在 X₁ 上得到 Y₁。
- 前馈残差对 Y₁ 做前馈网络,结果加在 X₂ 上得到 Y₂。
- 先倒推 X₂反向时手里只有 Y₁、Y₂,先用 Y₂ 减去 FeedForward(Y₁),就还原出 X₂。
- 再倒推 X₁用还原出的 X₂ 计算注意力,从 Y₁ 中减去,得到 X₁。
前馈分块
即使留言板够用,烤箱也不够大。大厅小吃摊的烤箱一次只能烤四片面包,大批量烤会挤爆台面。主办方干脆规定:一炉一炉来,烤完一盘端走一盘。
Transformer 里的前馈网络 FFN 也面临同样的“台面危机”: 它把每个 token 的向量从 d 维映射到中间维度 d\_ff 再映射回来,而 d\_ff 往往是隐藏层的 4 倍(如 512→2048)。一次性处理整条序列时,中间激活要占 n×d\_ff 的显存。Reformer 的做法朴素却有效——把序列沿长度方向切成 c 块,算完一块释放一块:
为什么敢这样切?因为 FFN 对每个 token 是独立运算的,块与块之间没有任何信息交换,所以这是“精确分块”而非近似。它和 LSH 的“近似分组”有本质区别——LSH 牺牲了一点质量换效率,分块则一分钱质量都不牺牲,只是把显存峰值摊平。
- 线性升维输入 x 乘 W₁ 加 b₁,从 d 维升到 d\_ff 维,相当于“临时多出几倍的工作面”。
- ReLU 激活max(0, ·) 把负数清零,给模型引入非线性。
- 线性降维再乘 W₂ 加 b₂ 回到 d 维,完成一次完整的 FFN。
- 逐块拼接把序列切成 C₁…C\_c 共 c 块,每块独立过 FFN,最后按原顺序拼接,结果与整段一起算完全一致。
复杂度总账
大会结束后,主办方要算一笔总账:新规则到底省了多少?
把三招合起来看显存构成:
对比标准 Transformer 每层每头要存 n×n 的分数、L 层激活各留一份,Reformer 把显存随长度的增长从平方级拉到近似线性,这是它能处理几万 token 长序列的根本原因。但账本上有一行小字必须看清:省显存 ≠ 省算力。LSH 哈希投影、桶排序、多轮重复计算注意力、可逆层反向重算前向,这些都是额外的 FLOPs。当序列只有几百 token 时,这些开销远大于省下来的部分,直接用标准注意力反而更快更省。
所以这场大会的账本结论是:人越多(序列越长),新规则越划算;人少的小型聚会,老规矩“全员握手”反而简单高效。
- 注意力项桶内注意力只存同桶分数,量级约为 n×m,再乘轮数 n\_r。
- 前馈项FFN 临时激活从 n×d\_ff 降为每块的 (n/c)×d\_ff。
- 可逆项可逆层只需保存最后一层输出 O(n·d),而不是 L 层各存一份。
局限与取舍
再精明的主办方也有遗憾。分组是“大概率”而不是“保证”: 总有聊得来的两个人被分到不同色区,这种跨桶的依赖在注意力中被永久屏蔽了。此外,搞多轮测试要租更多场地(算力),调手环颜色数量、轮数、每桌人数也需要大量试错。
多轮哈希能把召回率补到多高,取决于单轮碰撞概率 p₁ 和轮数 n\_r:
大会办得值不值,取决于你图什么:要极致质量、序列也不长,那就保留老规则;要在一张卡上啃下十万 token 的长文,那就接受分组近似和额外算力,用 Reformer 把“不可能”变成“可行”。这,就是它的取舍之道。
- 单轮碰撞概率两个相似向量在单独一轮哈希中落入同一桶的概率 p₁。
- 全没撞上n\_r 轮独立测试都没同桶的概率为 (1-p₁) 的 n\_r 次方。
- 至少命中一次用 1 减去上面的值,得到至少一轮同桶的概率,轮数越多越接近 1。
AI 应用场景
超长合同文档的一体化审查
阶段:模型训练与推理部署
解决的问题:法律合同动辄几万字、数万 token,传统 Transformer 要么截断(丢失跨页条款关联),要么因注意力矩阵 O(n²) 直接显存溢出。Reformer 的 LSH 注意力把注意力矩阵压成稀疏块对角,分块 FFN 与可逆层再压激活缓存,单卡就能喂入全文。
具体案例:某法务科技公司要审查一份 6 万 token 的并购合同,排查“定义条款”与“违约责任”之间的跨页引用。原先模型只能分段处理再人工拼接;换用 Reformer 后全文一次送入,关键条款的跨段引用召回明显改善,显存占用从 48GB 降到 24GB。
长用户行为序列建模
阶段:特征工程与模型训练
解决的问题:推荐系统里用户一年的点击序列长达数千到数万项,标准注意力无法整段建模。LSH 把“兴趣方向相近”的行为 item 分进同桶,注意力自然聚焦在同类兴趣的交互上,序列可以更长、信号更完整。
具体案例:短视频推荐场景中,将用户最近 2 万条观看行为编码后送入 Reformer 做序列编码,在同样一块 A100 上 batch size 从 8 提到 24,离线 AUC 提升 0.8%。
整本书级的长文本生成
阶段:推理部署与缓存优化
解决的问题:自回归生成 10 万 token 小说时,每一步都要参考整个历史,注意力缓存平方级增长。Reformer 让新 token 只回看历史中“同桶”的小范围位置,兼顾上下文广度与显存。
具体案例:开源社区用 Reformer 在 TPU 上做 64k 长度的小说续写演示,显存比同长度标准 Transformer 降低一个量级;代价是生成质量对桶大小敏感,需要针对语料调参。
Python 演示
import numpy as np def angular_hash(x, R): """角度 LSH: 向量在随机方向的投影,正负拼接后取 argmax 作为桶号。""" scores = np.concatenate([x @ R, -(x @ R)], axis=-1) return scores.argmax(axis=-1) def masked_attention(q, k, v, mask=None): """缩放点积注意力,可选整数掩码(掩码中 -1e9 表示屏蔽)。""" d = q.shape[-1] scores = q @ k.T / np.sqrt(d) if mask is not None: scores = scores + mask weights = np.exp(scores - scores.max(axis=-1, keepdims=True)) weights = weights / weights.sum(axis=-1, keepdims=True) return weights @ v def lsh_attention(x, n_buckets, seed=0): """LSH 注意力: 哈希分桶 -> 排序 -> 桶内掩码注意力。""" rng = np.random.default_rng(seed) n, d = x.shape R = rng.normal(size=(d, n_buckets // 2)) q = x @ rng.normal(size=(d, d)) k = x @ rng.normal(size=(d, d)) v = x @ rng.normal(size=(d, d)) h = angular_hash(x, R) order = np.argsort(h) q, k, v, h = q[order], k[order], v[order], h[order] same = h[:, None] == h[None, :] mask = np.where(same, 0.0, -1e9) out = masked_attention(q, k, v, mask) inv = np.empty_like(order) inv[order] = np.arange(n) pairs = int(same.sum()) print("LSH 分桶后只计算 %d/%d 对注意力 (%.1f%%)" % (pairs, n * n, pairs / (n * n) * 100)) return out[inv] def chunked_ffn(x, d_ff, chunks): """分块前馈: 沿序列方向分块计算,峰值显存约为整体的 1/chunks。""" rng = np.random.default_rng(1) d = x.shape[-1] w1, b1 = rng.normal(size=(d, d_ff)), rng.normal(size=d_ff) w2, b2 = rng.normal(size=(d_ff, d)), rng.normal(size=d) n = len(x) out = np.empty_like(x) for i in range(chunks): s = n * i // chunks e = n * (i + 1) // chunks hidden = np.maximum(0, x[s:e] @ w1 + b1) out[s:e] = hidden @ w2 + b2 return out def main(): rng = np.random.default_rng(42) x = np.concatenate([ rng.normal(loc=i * 3, scale=0.5, size=(16, 8)) for i in range(4) ]) out = lsh_attention(x, n_buckets=4) y = chunked_ffn(x, d_ff=32, chunks=4) print("输出形状:", out.shape, y.shape) if __name__ == "__main__": main()
scores = np.concatenate([x @ R, -(x @ R)], axis=-1)对应公式[xR; -xR],把向量投影到随机方向并同时考虑正负,相似向量会得到相近的分布。return scores.argmax(axis=-1)取冲劲最大的方向下标作为桶号,方向相近的向量大概率同桶。order = np.argsort(h)按桶号排序,让同桶 token 在序列上相邻,这是块对角化的前提。same = h[:, None] == h[None, :]构造 n×n 布尔矩阵,标记所有“同桶 token 对”。mask = np.where(same, 0.0, -1e9)把不同桶的位置填 -1e9,softmax 后这些位置的权重自动归零。weights = weights / weights.sum(axis=-1, keepdims=True)数值稳定的 softmax 归一化,保证每行权重和为 1。hidden = np.maximum(0, x[s:e] @ w1 + b1)分块后只对当前块做一次升维 + ReLU,临时矩阵只占整段的 1/chunks。out[s:e] = hidden @ w2 + b2逐块降维写回,块之间互不影响,最终拼接结果与整体计算完全一致。
常见误区
误区我看 Reformer 论文说复杂度从 O(n²) 降到了 O(n),那它肯定又快又省,以后长序列都用它准没错。
正解复杂度记号省的是“注意力分数的数量”,不是全部成本。哈希投影、桶排序、多轮重复计算、可逆层反向重算都是新增的算力开销。序列只有几千 token 时,这些额外开销可能超过省下的部分;当序列长到几万 token,固定成本被摊薄,收益才真正显现。一句话:省显存是真的,省算力看场合。
为何常见:看到 O(n²)→O(n×m) 的记号,直觉上以为是全面提速,忽略了复杂度的常数项和 LSH 引入的工程开销。
误区“可逆”听起来像压缩,那 Reformer 的参数肯定比普通 Transformer 少一半,显存自然省一半。
正解可逆层省的是“前向传播过程中每层要缓存的激活值”,这是训练时反向传播的原材料;模型参数(W₁、W₂ 这些矩阵)的数量完全没变。收益体现在训练阶段——标准模型要存 L 层激活,可逆模型只存最后一层输出,反向时逐层倒推重算。纯推理如果不做反向传播,这项收益基本不存在。
为何常见:把“激活显存”和“参数显存”混为一谈,以为可逆等于信息压缩、参数减少。
误区LSH 是近似的,那分块 FFN 肯定也是近似,块切小了质量会下降,我都不敢用。
正解分块 FFN 是数学上精确等价的。FFN 对每个 token 独立运算,没有跨 token 交互,先算第 1 到 100 个 token 再算第 101 到 200 个,拼起来和一次性全算结果一模一样。它只改变显存峰值的时间分布,不改变任何数值。真正有近似性的是 LSH 分桶,不要把两者混为一谈。
为何常见:因为 LSH 和分块经常一起出现,人容易把“近似”这个标签一锅端地贴给所有省内存手段。
误区复杂度公式里桶内候选数 m 越小越省,那把桶数调大,每组人少一点,不是更省吗?
正解桶太细会把本来相似的向量切散:两个高度相关的 token 一旦被分到不同桶,注意力直接归零,长程依赖就断了。桶大小要和序列长度、多轮轮数一起调,Reformer 论文里桶大小是超参数,需要在验证集上调出来。空桶、边界截断也要额外处理,桶数绝不是越大越好。
为何常见:只看复杂度公式的直觉,忽略了“分组质量”这个约束条件。
面试题
Q1. Reformer 主要针对标准 Transformer 的哪两大痛点?
- A.显存和算力随序列长度平方暴涨← 正确答案
- B.参数过多与训练样本要求过高
- C.推理速度慢且无法并行计算
- D.梯度消失与过拟合难以控制
答案:A。标准注意力的矩阵是 n×n,序列一长,显存与算力都按平方级上涨,这正是 Reformer 最想解决的两个痛点。B 的参数量、C 的并行性(Transformer 反而擅长并行)、D 的优化问题都是真实存在但并非 Reformer 的靶心。
Q2. 局部敏感哈希 LSH 最核心的性质是什么?
- A.相似的向量大概率落入同一个桶← 正确答案
- B.任意向量都映射成固定长度编码
- C.哈希值可以反向还原出原始向量
- D.桶的编号会随训练轮次不断更新
答案:A。LSH 的核心是“局部敏感”: 两个向量越相似,被分到同一桶的概率越高;反之概率骤降。这正是 Reformer 敢只算桶内注意力的前提。B 是普通哈希/编码的特性,C 与哈希单向性矛盾,D 混淆了哈希与模型参数更新。
Q3. Reformer 中分块 FFN 带来的效果是什么?
- A.减小中间激活的显存峰值← 正确答案
- B.减少模型参数的存储总量
- C.降低注意力分数的计算量
- D.让哈希函数收敛速度更快
答案:A。FFN 的中间激活维度通常是隐藏层的 4 倍,分块后任意时刻只保留一块的临时激活,峰值显存下降,但总计算量和参数都不变。B/C/D 分别把分块的收益错安到参数、注意力和哈希头上。
Q4. LSH 注意力中按桶号排序后再分块,主要目的是什么?
- A.让同桶 token 相邻且块长固定← 正确答案
- B.让不同桶 token 完全随机打散
- C.让注意力矩阵变成严格对角阵
- D.让哈希冲突概率降为零
答案:A。按桶排序后同桶 token 聚合到相邻位置,再切成固定大小的块,既保证块形状规整便于并行,又让桶内注意力落在块内。B 与排序目的相反;C 太绝对——块内还会包含相邻桶的部分 token,并非严格对角;D 不现实,LSH 本来就是概率性分组。
Q5. 可逆残差层反向传播时,输入 X₁ 和 X₂ 是如何得到的?
- A.利用逆推公式从输出逐层倒算← 正确答案
- B.从硬盘重新加载训练数据
- C.对梯度做一次随机采样模拟
- D.用另一组参数再跑一遍前向
答案:A。可逆层只保存输出 Y₁、Y₂,反向时先用 X₂ = Y₂ − FFN(Y₁) 倒推 X₂,再用 X₁ = Y₁ − Attention(X₂) 倒推 X₁,完全不需要缓存中间激活。B/C/D 都没有利用“可逆”这一结构特性。
Q6. 下列关于 Reformer 省显存的说法,正确的是?
- A.省的是激活值而非模型参数量← 正确答案
- B.省的是参数矩阵的存储空间
- C.推理时省钱效果比训练时更大
- D.梯度本身会被压缩得更加紧凑
答案:A。可逆层和分块省的都是前向激活的缓存——训练反向传播需要它们,所以收益主要在训练阶段。参数矩阵和梯度大小都不变。C 说反了:纯推理若不做反向,可逆层重算的收益有限;D 的梯度压缩不是 Reformer 的机制。
Q7. 多轮哈希为什么能提高注意力的质量?
- A.多轮互补,降低相似向量被拆散的概率← 正确答案
- B.每轮都会让桶的数量自动翻一倍
- C.多轮可以显著降低特征的维度数
- D.多轮会把所有 token 放进同一个桶
答案:A。单轮哈希可能让两个相似 token 恰好被分到不同桶,多轮用多套独立哈希各分一次、各算一遍注意力再平均,大大增加“至少一轮撞上”的概率(见 P=1−(1−p₁)^n\_r)。B/C/D 都不是多轮哈希的机制。
Q8. 与标准 Transformer 相比,Reformer 引入了哪些额外开销?
- A.每层都要计算哈希和排序操作← 正确答案
- B.多轮哈希使注意力重复多次← 正确答案
- C.可逆层反向要重算一遍前向← 正确答案
- D.模型参数量随层数线性翻倍
答案:ABC。哈希投影、桶排序、多轮注意力、反向重算都是 Reformer 新增的算力成本,这也是“省显存不省算力”的由来。D 错误:参数量由隐藏维度决定,可逆层并不增加参数。
Q9. 哪些场景更适合选择 Reformer 而非标准注意力?
- A.序列长度达到数万级的训练任务← 正确答案
- B.显存受限但需要完整的长上下文← 正确答案
- C.超短序列的高频小模型推理场景
- D.需要严格捕捉跨桶全局依赖的任务
答案:AB。Reformer 的平方级降维收益只在长序列上才能盖过哈希等额外开销,短序列用标准注意力更简单高效;C 正是它的劣势场景。D 中“跨桶依赖”恰是 LSH 近似的薄弱点,不应选 Reformer。
Q10. 关于 LSH 注意力中“分桶”与“分块”的关系,正确的理解有?
- A.分块保证每块大小固定,便于硬件并行← 正确答案
- B.严格按桶边界切分会产生空桶和大小不均← 正确答案
- C.分块后同一块内可能出现相邻桶的 token← 正确答案
- D.分块能够彻底消除哈希冲突带来的影响
答案:ABC。排序后严格按桶边界切分,桶大小不均、可能为空,GPU 并行效率低,所以 Reformer 改成分块近似——块大小固定,代价是块边界可能截断同一个桶,或把相邻桶的 token 包进来。D 说反了:分块只是工程折中,哈希冲突与边界截断依然存在。