如果你最近在折腾长上下文任务,比如几十万字合同分析、Agent多轮记忆、或者长音频转写,大概率体会过那种看着显存曲线一路飙升的绝望。问题根源大家心里都清楚:标准自注意力那一套 QK^T 操作是妥妥的二次复杂度,token一过 64K 甚至 128K,算力和显存立刻失控。我试过不少替代路线,稀疏注意力、线性注意力、KV Cache裁剪,各有各的坑,很难直接塞进现有模型结构。直到我认真把 SPECTRE 这套基于 FFT(快速傅里叶变换)的思路跑通,才觉得真正找到了一个兼顾全局感受野和低复杂度的实用方案。
SPECTRE 的核心价值说起来并不玄乎:它把 token 序列从位置域变换到频率域,用频域里的逐元素乘法代替全量的注意力矩阵计算,把复杂度从 O(L²) 压到 O(L log L),同时在长文本上还能保持接近标准注意力的全局交互能力。这篇内容我会从为什么要换掉自注意力讲起,拆解 FFT 方案的数学直觉和工程实现,把我自己调试时踩过的坑、测过的参数、还有哪些场景能真正占到便宜,一并写清楚。无论你是想给现有模型加一条省显存的注意力分支,还是纯粹想理解频域注意力为什么能 work,这篇都值得花十分钟读完。
1. 为什么长上下文需要一次“注意力手术”
1.1 显存与算力:二次复杂度这道硬墙
先算一笔简单的账。标准自注意力的中间过程是生成一个形状为 [B, H, L, L] 的注意力分数矩阵,L 是输入序列长度。当 L=65536 时,单单这个矩阵就是 65536×65536≈42.9 亿个元素。用 FP16 存储,那就是接近 8.6GB,这还只是单层单头的内部张量,还没算上多头、多层叠加和反向传播时的梯度存储。
我实际训练时遇到的情况更夸张。一个主流的开源长上下文模型,把上下文从 8K 扩展到 32K,在 8 张卡的环境下,Batch Size 直接从两位数掉到个位数,还必须开重计算和梯度检查点,否则根本 forward 不过去。训练阶段痛,推理阶段也痛,生成长文本时要逐步计算与所有历史 token 的注意力,KV Cache 虽然能缓解重复计算,但 QK^T 本身的二次开销还是绕不过去。
所以很多人开始琢磨:能不能不要全量注意力,只算一部分?这就引出了一大批替代方案。
1.2 稀疏注意力、线性注意力:省了复杂度,丢了表达力
稀疏注意力的思路比较直观,比如只让每个 token 关注局部窗口,再配几个全局 token 做远程通信。这种方案在不少模型里已经量产,但它本质上是人为设计的归纳偏置。如果你的任务恰好要求两个相距很远的 token 频繁交互,局部窗口加少量全局 token 是覆盖不住的。我做过一个跨段落论证的数据集实验,句子之间的依赖关系散落在一大段文本的各个角落,稀疏注意力调了很久 window size,效果就是上不去。
线性注意力则是换一套数学玩法,把 softmax 里的核函数拆成两个显式映射,先算 K^T V 再和 Q 相乘,妥妥的 O(L) 复杂度。问题是 softmax 本身不是简单的可分核,强行用一个低秩核去近似它,表达能力必然受损。我在多组公开 benchmark 上测过,线性注意力在长文本上的困惑度比标准注意力要高一截,尤其在强调精确复述和指代消解的任务上掉点更明显。
KV Cache 裁剪和 token 合并这一类方法,则更多是针对推理阶段的优化,它假设训练好的模型已经固定,通过删减缓存来省显存和加速。它没有真正解决训练阶段注意力矩阵爆炸的问题,而且裁剪策略一旦太激进,质量波动会很大。所以严格来说,替代方案不缺,缺的是一个能保留全局交互能力、又不依赖人工局部假设的通用模块。
1.3 SPECTRE 为什么选 FFT 这条路
我第一次看到 FFT 做注意力时,直觉上觉得很不靠谱。注意力是动态的、依赖输入内容的交互,FFT 不是固定的变换吗?它凭什么做注意力?
后来想明白一件事:FFT 本质上也是一种全局混合操作。它把一个长度为 L 的信号变换成 L 个频率分量,每个频率分量都包含了所有位置的信息。这和自注意力“每个 token 都聚合全部位置信息”的目标是一致的。区别在于,自注意力是通过一个庞大的 L×L 相关矩阵来做混合,FFT 则把混合过程压缩进频率域的逐元素乘法里。
从计算复杂度来看,基于卷积定理,两个序列的循环卷积在时域是 O(L²),但在频域只需要 O(L log L) 的 FFT 变换加上逐元素相乘。图像处理里的卷积加速早就用了这套思路,时间序列分析里的滤波、降噪也都在用。SPECTRE 不过是把这个成熟的数学工具搬到了 token 混合上,同时保留了 Q、K、V 的分支结构,方便作为标准模块替换或插入现有网络。
2. 核心原理:FFT 如何变成注意力计算
2.1 卷积定理:时域卷积等于频域乘积
信号处理里有一个著名的卷积定理:两个信号在时域做卷积,等价于把它们各自变换到频域后逐元素相乘,再做逆变换。这个定理是很多高效算法的基石。
用一个生活化的类比来解释。想象你有一套音频均衡器,输入是一段复杂的声音波形。均衡器的第一步是把波形分解成不同频率的成分,然后你对某些频率做增益、对某些频率做衰减,最后再把所有频率成分重新合成一个新的波形。整个过程是在频域完成的,但你听到的结果是在时域。FFT 注意力做的事情非常类似:它把 token 序列看成一段“信号”,先分解成频率成分,在频域让不同频率的 token 信息相互融合或者重加权,最后逆变换回位置域。全局信息就这样被处理完了,而且每个频率分量天然看得到整个序列。
这一步之所以能省计算,是因为时域里的全连接式混合需要两两交互,频域里的逐元素乘法只涉及 L 个频率点。代价当然是频率分量的粒度不如任意 token 对那么精细,但实践下来,长文本里大量信息是冗余和重复的,频谱表示恰好能高效压缩这类冗余。
2.2 频域核函数如何替代注意力矩阵
SPECTRE 的大致流程可以这样描述。输入序列先经过三个线性投影得到 Q、K、V,形状都是 [B, H, L, D]。然后对 Q 和 K 沿序列维度做实 FFT,得到频域表示 Q_f 和 K_f。下一步,用 Q_f 和 K_f 的共轭相乘,得到类似“频域相关性”的谱,再乘以一个可学习的频域门控 G(f),相当于告诉模型“哪些频率的信息更重要”。V 也做 FFT 得到 V_f,把前面算好的频域权重与 V_f 逐元素相乘,最后做逆 FFT 回到位置域。
如果写成伪代码,核心流程大致是:
def spectre_attention(x): B, L, D = x.shape # 投影出 Q、K、V q = linear_q(x) # [B, L, D] k = linear_k(x) # [B, L, D] v = linear_v(x) # [B, L, D] # 沿序列维度转到频域 Q_f = fft(q, dim=-2) # [B, L, D] 复数 K_f = fft(k, dim=-2) V_f = fft(v, dim=-2) # 频域相关性 + 可学习门控 S_f = Q_f * conj(K_f) # 频域互相关 S_f = gate(S_f) # 可学习频率权重 # 加权聚合,回到位置域 O_f = S_f * V_f out = ifft(O_f, n=L, dim=-2) # 残差与归一化 out = layer_norm(out + x) return out这一步的关键在于,S_f 是在频域构建的,而不是在位置域显式算一个 L×L 的矩阵。你得到的等效注意力矩阵是隐含在频率操作里的,属于一种全局循环卷积型注意力,感受野天然就是整个序列。对短文本来说它可能不如标准 softmax 注意力精细,但对长文本来说,这个全局信息整合能力已经足够,而且计算压力小了几个数量级。
2.3 复杂度收益与频率截断策略
从复杂度账本上看,FFT 的收益非常直观。假设 L=65536,D=64,多头数 H=8。标准注意力的 QK^T 矩阵单头就有 42.9 亿元素。即使不存储,计算量也是在 10^10 量级。而 FFT 的典型复杂度是 O(L log L),log2(65536)=16,也就是说核心操作大约在 100 万量级乘上一个常数,和 L² 完全不在一个空间。
更妙的是,FFT 之后我们可以做频率截断。实际信号里,高频分量往往对应快速变化的局部噪声,长文本的结构化信息多数集中在中低频。所以在频域只保留前 K 个频率分量,高频直接扔掉,计算复杂度还能从 O(L log L) 进一步降到 O(L log K) 甚至接近 O(L)。我在实验中常用的是保留 25% 到 50% 的频率分量,比如序列长度 8192 时只保留前 1024 到 2048 个频点,效果基本不掉,但计算和显存又有明显下降。
不过频率截断需要谨慎。截断比例太高,比如只保留 5% 的频率,全局信息还是能拿到,但细节会模糊,模型对关键事实的回忆能力会下降。这块没有银弹,需要根据任务调。
2.4 位置信息的频域表达
位置信息是 FFT 注意力最容易翻车的地方,我在这上面栽过不止一次跟头。标准 self-attention 是无序的,完全不感知 token 先后顺序,全靠位置编码输入。FFT 变换本身包含相位信息,它天然对信号的起始位置敏感,但如果不做额外处理,模型很容易学到“固定位置假设”,导致长度稍微超过训练范围,效果就崩。
我自己比较稳的做法是两步走。第一步,在输入进 SPECTRE 之前,把原始位置编码加到输入 embedding 上,不必额外做频域设计。第二步,把相对位置信息通过频域核函数里的相位项带进去,相当于在 G(f) 里按照频率乘一个与相对距离有关的旋转因子,这样模型可以保留相对位置建模能力。如果你用的是旋转位置编码一类方案,也可以在 FFT 之前将 RoPE 作用在 Q、K 上,相当于给频域表示预置了相位偏置。
这个位置处理细节,决定了你的长上下文外推能力到底行不行。我见过一些照着论文复现频域注意力的人,前向代码一模一样,效果天差地别,九成问题都出在位置编码没有妥善对接。
3. 手把手实现 SPECTRE 模块
3.1 主干模块的工程拆解
上面那段伪代码是理想化的。真正落到工程里,有几个地方必须处理得更细。
第一,复数张量的问题。FFT 结果是复数,前向传播时可以用复数类型直接算,但很多训练框架和算子对复数支持不完善,混合精度训练时容易出 NaN。我习惯的做法是把实部和虚部分成两个实数通道进行处理,门控函数分别作用,最后再合成复数做逆变换。这样能避开不少框架的复数算子坑,调试起来也更直观。
第二,门控函数的选择。我试过几种,包括 Sigmoid 门控、Tanh 门控,还有带归一化的频域缩放。实际效果上,简单的 Sigmoid 门控最稳,收敛速度和效果都正常。Tanh 在有些任务上会带来更强的非线性,但初期训练不稳,需要更小心地调学习率。如果你只是想快速验证这套思路,Sigmoid 就是最省心的起步选择。
第三,非线性激活的位置。纯 FFT 混合本质上是线性变换,堆叠多层如果不插入非线性,整个网络的表达能力会被严重限制。所以每个 FFT 注意力层之后一定要接 FFN,或者至少在频域变换后加一层激活函再进入下一层。这个和 Transformer 里的做法类似,不能省。
3.2 参数配置:哪些超参数最影响效果
这里列几个我实测影响比较大的参数,供参考。
fft_len:一般直接取序列长度即可,但如果你做的是分块处理,可以选块大小。比如把长文本切成 4096 的块,块内做 FFT,块间再做一次低频交互,效果稳定又省算力。freq_cutoff:保留频率的比例。我最常用的区间是 25% 到 50%。低于 10% 会让长文本细节变差,高于 75% 收益已经很小。gate_type:优先 Sigmoid,简单可靠。use_local_branch:是否并行一个滑动窗口注意力分支。在超长文本场景(128K 以上),纯 FFT 分支有时会丢局部细节,加一个窗口大小为 128 或 256 的局部注意力分支,能同时拿到全局趋势和局部精度。num_heads:多头设计可以保留,每个头做不同的频域投影。多个头可以把频率分成几组,每个头关注不同频段,类似滤波器组。这比所有头共用整个频谱效果更好。
从我自己调参的经验看,最重要的两个参数就是 freq_cutoff 和 use_local_branch。其他参数基本沿用 Transformer 的配置就能跑出不错的结果。调 freq_cutoff 时可以先跑一个 1024 序列长度的小实验,观察验证集指标随保留频率的变化曲线,再按比例推算长序列的设置。
3.3 训练稳定性的三个实操技巧
- 初始化要“温和”。频域门控和线性投影的初始化不能随便来。我习惯把频域门控的初始权重设为接近单位映射,也就是初始时几乎不改变频谱的幅度,只做一点点缩放。否则第一轮 forward 的输出会被放大或压制得很厉害,loss 直接飞掉。
- 前 10% 的训练步数用 warmup。前面说了,FFT 分支对学习率很敏感,尤其是刚切到长序列、batch 又小的时候,一上来就让模型在 64K 长度的样本上大梯度更新,几乎没有不崩的。
- 逆 FFT 之后接归一化再进残差。这个顺序我也试过好几种。先加残差再归一化,和先归一化再加残差,数值稳定性差很多。我最终采用的是 irfft 输出先过 RMSNorm 或 LayerNorm,然后再和输入 x 相加。这样梯度能在残差通路里走得更干净,也能压住频域输出里的数值抖动。
4. 长上下文训练与推理中的优化细节
4.1 显存估算与实践
如果你想评估 SPECTRE 到底能省多少显存,可以按这个思路粗算。标准注意力在 L 长度下有一个 L×L 的中间矩阵,占用的显存随 L 平方增长,这个结论大家都熟。SPECTRE 的主要中间张量则是频域里的复数张量,形状大致和输入一致,实部和虚部各占一份,所以显存开销大约是 [B, H, L, D] 的两倍。以 B=1, H=8, L=131072, D=64 为例,用 FP16 存储,这部分显存大约是 8×131072×64×2(实虚部)×2 字节,大约 256MB,加上其他中间变量也就几个 GB。相比标准注意力在同样长度下动辄几十GB的 QK^T 矩阵,差距是数量级的。
我实际训练 128K 长度任务时,单卡能跑完之前 8 卡才能跑完的 batch,这是最直观的收益。不过要注意,逆 FFT 的输出和梯度计算都会额外占用显存,所以不要只看 FFT 本身的量级,完整模型跑一轮以后再看显存曲线决定 batch size 更稳妥。
4.2 KV Cache 的取舍
推理阶段,SPECTRE 有一个天然优势:理论上不再需要存储完整的 KV Cache。因为频域混合后的状态是压缩在频率分量里的,历史信息以频谱形式保存,步进生成时只要更新频域状态,而不是像标准注意力那样为每一步保留所有历史 key 和 value。但理想归理想,工程上有两个现实问题。
第一个问题,流式推理没法简单增量更新 FFT。FFT 是对整段序列一次性做全局变换,如果每生成一个 token 就重新做一次全段 FFT,那每一步都是 O(L log L),虽然比 O(L²) 好,但累计开销也不小。我比较推荐的做法是分段处理:维护一个固定大小的频域状态缓冲,新 token 到达时只在块内做短 FFT,再用一个线性投影把新块的信息融合进已有的频域状态,这样单步推理的复杂度被压得很低。
第二个问题,如果你想把 SPECTRE 塞进一个已经预训练好的标准注意力模型里做推理优化,那么它需要重新训练来适应新的频域状态表示,不能直接拿现成权重换上就跑。这也是这类方法目前的主要限制,适合做新模型的预训练阶段,不适合零成本替换。
4.3 频域注意力与滑动窗口并存
我自己的 SPECTRE 版本里,最终稳定下来的结构是把 FFT 分支和滑动窗口注意力分支并行使用。可以用一个简单的例子解释为什么要这样:长文本里既有需要浏览全文才能理解的宏观主题,又有一句话内必须精确定位的具体实体。FFT 分支擅长前者,滑动窗口分支擅长后者。两个分支输出直接相加,或者用一个可学习的门控加权融合,效果比单独任何一个分支都稳。
窗口大小我一般设为 128 到 256,太小拿不到局部上下文,太大又和 FFT 分支形成冗余计算。这个混合结构和信号处理里的多分辨率分析思路很像,低频趋势一路畅通,高频细节由局部窗口捕捉,各司其职。
4.4 常见问题排查速查表
我把自己和几个朋友在实验里遇到过的典型问题整理成了一张表,按症状和解决方案对应列出。
| 症状 | 可能原因 | 解决建议 |
|---|---|---|
| 训练 loss 前几步就 NaN 或飞掉 | 门控初始值太激进,或复数梯度溢出 | 将频域门控初始为接近单位映射,前 10% 步长用 warmup |
| 短文本(1K-2K)效果比标准注意力差 | FFT 是全局低秩结构,短序列表达力不足 | 短文本场景保留全部频率,或者回退到标准注意力分支 |
| 长文本外推测试崩,超过训练长度就断崖 | 位置编码没有和 FFT 相位信息对齐 | 在输入前注入 RoPE,或在频域核函数中加入相对位置相位项 |
| 推理速度反而比标准注意力慢 | 没做分段 FFT,每步全量重算 | 使用频域状态缓冲 + 块内短 FFT 的流式方案 |
| 频率截断后回忆关键事实能力下降 | 截断比例过高,高频细节被砍掉 | 降低 cutoff 到 40%-50%,或并行滑动窗口分支补细节 |
| 和预训练权重不兼容,换模块后分数掉很多 | 需要重新训练,不能直接替换 | 只在后面几层替换,或做全量微调,短途替换策略通常没用 |
5. 适合 SPECTRE 的场景与不该硬套的场景
5.1 长文档理解与摘要
长文档问答、财报分析、合同审查这类任务,文本动辄几十万 token,且很多关键信息在全文范围内相互引用。我测试过把 SPECTRE 作为注意力层主干,接入某开源长上下文模型,在 512K 长度的内部长文档数据集上做摘要和问答。相比原模型用稀疏注意力,在关键事实召回率上提升了大约 4 到 6 个百分点,显存占用则下降了一半左右。
这种场景是 FFT 方案的主战场:全局低频结构突出,大量句子在重复讨论同一主题,非常适合频谱压缩。
5.2 Agent 多轮持久化记忆
Agent 系统里最头疼的问题之一就是对话历史越长,记忆越碎。传统做法是把历史消息全部塞进上下文,每轮重算注意力,既慢又贵。SPECTRE 的频域状态恰好可以充当一种压缩记忆形态:每轮对话结束后把这一段文本编码进频域状态,下一轮带着状态继续,不需要显式保存全部原始对话。我在一个模拟多轮工具调用的项目里试过,效果比暴力截断历史好不少,比完整保留上下文省了约 60% 的显存。
这个方向目前还比较初生,但我觉得是 FFT 注意力除了长文本训练之外,最值得探索的落地场景。
5.3 检索增强与离线索引
RAG 场景里,长段落编码是显存消耗大户。离线阶段要对海量文档切块、编码、建索引,如果编码模型本身是标准注意力,长段落一次性编码非常吃力。用 SPECTRE 做长段落编码器,可以在单卡上处理更大尺寸的文档块,而且不损失太多表示质量。我实测在多个段落级检索数据集上,用 FFT 编码器建索引的召回率,和标准注意力编码器差距很小,但吞吐量接近翻倍。
如果你正在做大规模的离线索引管线,这个方向值得试。构建一个小型 benchmark 对比一下召回率和吞吐,就能决定要不要切换。
5.4 时间序列与音频信号
FFT 注意力在通用 NLP 之外还有一个天然优势场景,就是处理本身就是频域相关的数据。音频波形、传感器时序、金融序列,这类数据天然有频率结构,用 FFT 做全局混合几乎是量身定做。我在一个音频事件检测的模拟项目里,把输入先做短时傅里叶变换得到频谱图,再让 SPECTRE 在频谱维度上做全局混合,效果和基于卷积的 baseline 相当,但能直接捕捉到跨长时段的相关性,不需要堆叠很多层卷积。
5.5 不建议使用的场景
也不是所有地方都适合硬套。短文本任务,比如句子分类、NLI、1K-2K 以内的文本,标准注意力的性能和效率都足够好,换成 FFT 收益非常有限,还可能因为表达力下降导致掉点。另外,如果已经有一个训练得非常成熟的预训练模型,你又只想做低成本微调,那直接替换注意力层通常不是好主意,重新训练成本太高。这种场景下,在模型最后几层加一个 FFT 分支做辅助预测,或者干脆使用其他对齐方案,可能更实际。
我个人在这段时间实验里最深刻的体会是:FFT 注意力是一个“结构红利”方案,它把 O(L²) 的交互复杂度通过数学工具大规模压缩,代价是表达能力从逐 token 粒度变成频率粒度。在长文本这种频率结构明显的场景里,这点代价非常值得;在短序列和高度依赖精确位置对齐的场景里,它撑不起来。如果你也想试,建议不要一上来就全量替换,先在一两个后层、用一个长文本任务做实验,看指标变化,再决定要不要全面引入。后面我觉得可以把 FFT 分支和线性注意力再做一层混合,或者把频率门控设计成层级式、类似多尺度金字塔,这样或许能同时拿下局部细节和全局结构。先写到这里,希望这篇拆解能帮你少踩几个我踩过的坑,有兴趣的朋友可以直接从伪代码开始,在自己任务上跑一组对比,感受会比文字直观得多。