☰
全注意力机制为什么贵?从O(n²)到线性注意力成本优化解析
2026/10/21 0:54:48 网站建设 项目流程

如果你训练或推理过大语言模型,大概率遇到过这样的现象:序列长度从 2048 加到 4096,你以为只是数据量翻一倍,结果显存直接不够用了,训练速度也肉眼可见地掉下来。如果再加到 8192、16384,很多机器连试都不敢试。这不是优化不到位,而是全注意力机制在结构上就决定了序列越长,成本增长越夸张。它像一位极其尽责的档案管理员:你每问一句新问题,他不是翻最后一页,而是把从第一页到最新一页的所有档案全部重新读一遍。

这篇文章要讲清楚一件事,为什么全注意力机制这么“贵”?它的成本模型究竟是怎么拆的?Kimi Linear 这类以 Linear 为名的方案,为什么被认为是解决这个问题的方向?我不打算停留在“O(n²) 复杂度”这个公式层面,而是会把计算成本、显存成本、推理成本分开拆,配合可运行的最小代码示例,让你真正理解“每个新词都要重翻百万页记录”的含义。读完这篇文章,你会知道:

  • 全注意力为什么是二次方复杂度,以及这个二次方究竟花在了哪里;
  • 训练和推理阶段,注意力机制的成本结构有什么不同;
  • 工业界已经有哪些降低注意力成本的方案,它们各自牺牲了什么;
  • 从“全注意力”到“Linear Attention”这类方案,核心思路发生了怎样的转变;
  • 实际工程里,你应该怎么选择方案,怎么验证效果,怎么避坑。

1. 全注意力机制到底在计算什么

在深入成本分析之前,先把全注意力的计算过程说清楚。注意力机制(Attention)最早被广泛认知,是因为 2017 年的 Transformer 论文Attention Is All You Need。它的核心想法是:序列里的每个 token(可以简单理解为一个词或一个字)在生成新表示时,不应该只看自己,而应该参考序列里所有其他 token,并根据相关性分配不同的权重。

这里有一个公式,也是全注意力最经典的定义:

Attention(Q, K, V) = softmax(Q @ K^T / sqrt(d_k)) @ V
  • Q是查询(Query):表示“我现在想找什么信息”;
  • K是键(Key):表示“我这条信息是什么主题”;
  • V是值(Value):表示“我这条信息的实际内容”;
  • d_k是键向量的维度,用于缩放点积结果。

计算分三步:

  1. 用Q和所有K做点积,得到每个位置和其他所有位置的相似度分数;
  2. 对分数做 softmax,归一化成权重;
  3. 用权重对所有V加权求和,得到最终的输出。

这个过程里,关键在第 1 步:对于序列里的每一个 token,都要和其他所有 token 计算两两之间的相似度。序列长度如果是n,那相似度矩阵的大小就是n × n。

如果你曾经维护过一张两两配对的表,就知道这里的增长有多快:10 个元素有 45 对,100 个元素有 4950 对,1000 个元素有 499500 对。对,不是 100 倍,而是接近 10000 倍。这就是 O(n²) 的来源——两个嵌套循环,每一个 token 都要遍历一遍全部 token。

这个设计的好处是:模型不会遗漏任何跨位置的信息。无论两个词在序列里隔得多远,注意力都能直接建立联系。但坏处也在这:它不做任何取舍,永远在完整地计算两两关系。

2. “重翻百万页记录”到底是什么意思

你可以把全注意力理解成一个极其负责、但不太聪明的档案管理员。他的工作方式是:

  • 你问他一个问题(新的 token);
  • 他会从档案柜里取出所有历史记录(前面的全部 token);
  • 把新问题和每一页档案逐一比对,计算相关程度;
  • 最后把相关程度最高的几页内容摘出来,作为回答依据。

问题在于,他从来不会把比对结果缓存起来。下一次你再问一个新问题,他又把整个档案柜从头翻一遍。序列越长,档案越厚,他每回答一个新问题消耗的时间就越长。这个比喻对应的是 Decoder 推理阶段的行为。生成第n+1个 token 时,模型需要将新 token 的 Query 和之前所有 token 的 Key、Value 重新计算一遍注意力。虽然 Key 和 Value 本身可以缓存(这就是 KV Cache 的由来),但注意力权重计算这一步,仍然需要和全部历史 token 逐一交互。

这就带来了两个直观后果:

  • 总成本随序列长度线性增长——每生成一个新词,都要翻一遍越来越厚的档案;
  • KV Cache 也随之增长——每多一个 token,就要多存一组 Key 和 Value。

这就是为什么长文本场景下,推理引擎最怕的不是计算,而是“上下文越来越长”。从表面看,模型只是在多生成一个词,但它的成本却取决于这个词离输入开头有多远。你让模型生成第 100 个词和第 10000 个词,后者的单步成本要高得多。

3. 成本拆解:训练、推理和显存各贵在哪

标题里说“全注意力为什么贵”,这个“贵”不能笼统地说,要拆成三个维度看。

3.1 训练阶段:计算量随序列长度二次方增长

训练阶段的最大特点是:序列里的所有位置都是已知的,我们可以并行计算所有 token 之间的注意力。这让训练比推理快很多,但也带来了一个问题:所有 token 两两之间的注意力分数都要计算一遍。

假设输入序列长度是n,每个 token 的向量维度是d。一次注意力计算中:

  • 计算Q @ K^T,需要n × d和d × n的矩阵乘法,得到n × n的分数矩阵;
  • 这个矩阵的大小是n²;
  • 对n²个元素做 softmax;
  • 再用n²个权重去加权n × d的 V 矩阵。

所以单次注意力的时间复杂度和空间复杂度都是 O(n²)。Transformer 里通常有多个注意力头,假设有h个头,每个头的维度是d_h,那总的复杂度仍然是 O(n²),只是系数更大。

直观感受一下:训练一个 2K 上下文的模型,单条样本的注意力分数矩阵是 2048 × 2048,约 400 万个元素。如果上下文提升到 8K,矩阵变成 8192 × 8192,约 6700 万个元素。序列变成 4 倍,计算量变成了 16 倍。这就是为什么长上下文训练那么贵的根本原因。

3.2 推理阶段:KV Cache 让显存压力线性增长

推理阶段大家都会用 KV Cache 来做加速。所谓 KV Cache,就是把已经计算过的 Key 和 Value 缓存下来,避免生成每个新 token 时重新计算前面所有位置的 K 和 V。

这个策略很有效,但也带来了新的成本。假设模型有L层、h个注意力头、每个头的维度是d_h,序列长度是n,那么每一层需要缓存:

  • n × h × d_h的 Key;
  • n × h × d_h的 Value。

两者合计2 × n × h × d_h。再乘以层数L,KV Cache 的总大小就是2 × L × n × h × d_h。

用具体数字感受一下。一个 7B 规模的模型,层数约 32 层,注意力头数约 32 头,每头维度约 128。上下文长度如果是 8192,那么 KV Cache 大约是:

2 × 32 × 8192 × 32 × 128 = 2,147,483,648 个 float16 数值

换算成显存,约 4 GB。这还没算模型参数本身和中间激活值。如果上下文长度翻到 32K,光 KV Cache 就要 16 GB 以上。很多显卡根本放不下。

这就是为什么长文本推理时,你常常看到显存被 KV Cache 吃满,而不是被模型参数吃满。模型参数是固定的,KV Cache 却随着对话轮数和上下文长度动态增长。

3.3 模型参数量增加带来的附加成本

还有一个容易忽略的成本:为了提升注意力表达力,模型往往需要更大维度的d和更多层数。维度越大,Q、K、V 的线性投影矩阵也越大,但这部分成本是线性增长的,不是全文重点。真正导致长文本场景崩溃的,仍然是注意力分数矩阵的二次方增长。

4. 用代码理解复杂度:一个最小示例

纸上谈兵容易,直接看代码最直观。下面用 Python 和 NumPy 实现一个简化版的全注意力计算,并测量不同序列长度下的耗时变化。

import numpy as np import time def full_attention(Q, K, V): # Q, K, V shape: (n, d) d_k = Q.shape[-1] scores = Q @ K.T / np.sqrt(d_k) # (n, n) weights = np.exp(scores - np.max(scores, axis=-1, keepdims=True)) weights = weights / weights.sum(axis=-1, keepdims=True) output = weights @ V # (n, d) return output def measure_time(n, d=64): np.random.seed(42) Q = np.random.randn(n, d) K = np.random.randn(n, d) V = np.random.randn(n, d) # 先跑一次 warmup full_attention(Q, K, V) start = time.time() for _ in range(10): full_attention(Q, K, V) avg_time = (time.time() - start) / 10 return avg_time for n in [512, 1024, 2048, 4096, 8192]: t = measure_time(n) print(f"序列长度 {n:6d} -> 平均耗时 {t*1000:8.2f} ms")

运行结果大致如下(具体数值取决于 CPU 性能):

序列长度 512 -> 平均耗时 2.31 ms 序列长度 1024 -> 平均耗时 9.05 ms 序列长度 2048 -> 平均耗时 36.12 ms 序列长度 4096 -> 平均耗时 143.88 ms 序列长度 8192 -> 平均耗时 571.20 ms

可以看到,序列长度翻倍,耗时接近翻 4 倍。512 → 1024耗时从 2.31ms 涨到 9.05ms,约 3.9 倍;4096 → 8192从 143.88ms 涨到 571.20ms,约 4 倍。这符合 O(n²) 的预期。

上面的代码里,scores = Q @ K.T / np.sqrt(d_k)就是那个产生n × n矩阵的关键操作。当n变大时,这个矩阵本身就需要 O(n²) 的内存来存储,weights @ V也需要 O(n²) 次乘加运算。真正的成本从这一行就开始了。

5. 一个更直观的“档案翻页”实验

为了更直观地理解“每生成一个新词,都要重翻前面所有记录”,可以模拟一个简化版的推理循环。假设我们每次只生成一个新 token,但要和前面所有的 token 计算注意力。

import numpy as np def generate_one_token(history_Q, history_K, history_V, new_q): """ 模拟解码器生成一个 token 的过程。 history_Q/K/V: (n, d) 历史 token 的 Q/K/V new_q: (d,) 新 token 的 Query """ # 新 token 要和历史所有 K 计算注意力分数 scores = new_q @ history_K.T # shape: (n,) weights = np.exp(scores - np.max(scores)) weights = weights / weights.sum() # 加权求和历史 V,得到新 token 的输出 new_output = weights @ history_V # shape: (d,) return new_output def simulate_generation(total_tokens, d=64): np.random.seed(0) # 初始历史:第一个 token history_K = np.random.randn(1, d) history_V = np.random.randn(1, d) cumulative_time = 0.0 for step in range(total_tokens - 1): new_q = np.random.randn(d) start = time.time() generate_one_token(history_K, history_V, new_q) cumulative_time += time.time() - start # 生成后,把新 token 的 K/V 加入历史 new_k = np.random.randn(d) new_v = np.random.randn(d) history_K = np.vstack([history_K, new_k.reshape(1, -1)]) history_V = np.vstack([history_V, new_v.reshape(1, -1)]) return cumulative_time import time for total in [100, 500, 1000, 2000]: t = simulate_generation(total) print(f"生成 {total:5d} 个 token,累计注意耗时 {t*1000:8.2f} ms")

这个模拟演示了一个关键点:每生成一个新 token,历史长度都会增加,导致下一次注意力计算更慢。累计下来,生成n个 token 的总耗时接近 O(n²)。这里还没有包含 KV Cache 用于解决 K/V 重算的优化,但仍然体现了“新词要和所有历史记录交互”的核心成本。

无论你用什么框架、什么优化器,注意力分数矩阵的n × n增长都是绕不开的物理成本。KV Cache 可以避免重复计算 K 和 V,但新 token 和旧 token 的交互仍然需要逐对计算。

6. 既然全注意力贵,那为什么还要用

听起来全注意力又慢又贵,为什么不直接换掉?

其实是因为它在表达能力上有不可替代的优势。全注意力让每一个位置都能直接访问整个序列的任意位置,信息通路极短,路径长度只有 1。相比之下,早期的循环神经网络(RNN)和卷积神经网络(CNN)在处理长距离依赖时,信息需要经过很多步才能从一个位置传递到另一个位置,传递过程中容易衰减和丢失。

比如句子“小明昨天去了北京,他今天早上给我打电话说……”,代词“他”要指代“小明”,这两个词之间隔了很长的距离。全注意力可以直接建立“他”和“小明”的联系,而 RNN 需要一步一步地把信息传过去,传着传着可能就丢了。这就是为什么 Transformer 能在大规模语料上取得更好效果的核心原因之一。

所以,“全注意力贵”不是一个需要被否定的缺点,而是一个需要被“管理”的成本。工业界的思路从来不是简单丢弃全注意力,而是在保留表达能力的同时,用各种方式降低它的计算和存储开销。

7. 工业界降本的四条路线

理解了全注意力的成本模型后,再看工业界的优化方案,会清晰很多。目前主流路线可以分成四类。

7.1 稀疏注意力:只算一部分两两关系

稀疏注意力的核心思路是:不是每个 token 都需要和其他所有 token 都建立联系,我们可以根据位置模式,只保留一部分注意力分数。

典型代表是 Local Attention(滑动窗口注意力),每个 token 只和它附近的w个 token 计算注意力,复杂度变成 O(n × w),其中w是窗口大小。因为大部分语言现象确实只依赖局部上下文,局部窗口往往就能覆盖大部分需求。Longformer、BigBird 都采用了类似的思路。

问题也很明显:如果某个重要信息出现在窗口之外,模型就无法直接建立联系。虽然 BigBird 里加入了一些全局 token 和随机连接来弥补,但本质上仍然是一个信息通路受限的系统。

7.2 线性注意力:用核函数近似

线性注意力的核心思路是:把 softmax 注意力中的exp(Q @ K^T)分解为φ(Q) @ φ(K)^T,这样矩阵乘法的结合顺序可以被改变,把 O(n²) 的复杂度降成 O(n)。

具体来说,原始的注意力输出是:

Output = softmax(Q @ K^T) @ V

其中 softmax 里的exp是逐元素计算的,无法和矩阵乘法直接结合。如果用一个函数φ把 Q 和 K 各自映射到新的空间,让exp(Q @ K^T) ≈ φ(Q) @ φ(K)^T,那么:

Output ≈ φ(Q) @ (φ(K)^T @ V)

注意这里的运算顺序:先算φ(K)^T @ V,这是一个(n, d)和(d, d)的矩阵乘法,复杂度 O(n × d²);再算φ(Q)和它的乘积,同样是 O(n × d²)。整个注意力计算变成了关于n的线性复杂度。

这就是 Linear Attention 这个名称的由来。它把注意力计算从 O(n²) 降到了 O(n)。从标题里看,Kimi Linear 的 Linear 很可能就是沿着这条技术路线在做优化,用线性复杂度的注意力来替代传统的全注意力,从而让长文本处理成本大幅降低。

当然,线性注意力也有代价:核函数的近似不一定能精确模拟 softmax 的注意力分布,某些任务上效果会有损失。这也是为什么工业界往往不是简单地替换,而是做混合。

7.3 Flash Attention:不改变算法,改变实现

Flash Attention 的路线完全不同。它不做任何近似,而是通过 IO 感知的 CUDA kernel 设计,把注意力计算的中间结果尽量留在 SRAM 里,减少和 HBM(显存)之间的数据搬运,从而大幅提升速度、降低显存占用。

它有两个关键技巧:

  • Tiling(分块):把 Q、K、V 切成小块,在 GPU 的 SRAM 上完成局部计算,避免把n × n的完整分数矩阵写回显存;
  • Online Softmax:在分块计算的同时,用一个 online 的方式更新 softmax 的归一化项,保证结果和标准 softmax 完全一致。

这意味着 Flash Attention 能在结果不变的前提下,让显存占用从 O(n²) 降到 O(n),速度也大幅提升。今天的主流大模型训练基本都离不开 Flash Attention。

它的限制在于:它优化的是实现效率,不是算法复杂度。序列极长时,O(n²) 的理论复杂度仍然存在,最终还是会遇到瓶颈。

7.4 综合方案:多级混合

实际产品通常不是只用一种方案。更常见的做法是:

  • 底层用 Flash Attention 做标准全注意力的高效实现;
  • 某些层用稀疏注意力或线性注意力降成本;
  • 推理阶段用 KV Cache、PagedAttention 等管理显存。

这是一个系统工程问题。算法结构、算子实现、内存管理、调度策略,每一层都能省一点,合起来才有质的改变。

8. Kimi Linear:从标题能看到什么信号

目前关于 Kimi Linear 的公开信息还不多。这里只能基于标题做合理推理,不构成对具体产品的定论。从命名习惯看,“Linear”大概率指 Linear Attention,也就是线性注意力机制。这类方案要解决的核心问题,恰恰就是全注意力机制在长文本场景下的二次方成本。

如果 Kimi Linear 确实沿着线性注意力的方向优化,它可能在解决这么几件事:

  • 长文本处理成本下降:把序列长度对计算量的影响从二次方降到线性,长上下文的边际成本大幅降低;
  • 单次推理的显存占用下降:线性注意力不需要显式构建n × n的注意力矩阵,KV 缓存压力也可能更小;
  • 长对话场景的延迟优化:每一步生成不再需要和越来越长的历史逐一计算,延迟随上下文增长的趋势会更平缓。

但要强调的是,线性注意力方案在部分任务上可能存在精度损失,需要在效果和效率之间做取舍。真正的产品方案一定会在算法层、工程层做大量补偿和调优。

对开发者来说,关注 Kimi Linear 这类方案的意义在于:如果你正在做长文档处理、多轮对话、代码仓库级上下文分析,或者需要把大模型部署到显存有限的机器上,线性注意力的思路很可能直接关系到你的成本和体验。

9. 什么时候需要关注全注意力的成本优化

不是所有场景都需要立刻优化全注意力成本。你可以用下面这张表来对照判断:

场景序列长度是否需要优化
短文本分类、情感分析几百 token 以内全注意力完全够用,无需额外优化
中等长度摘要、翻译1K ~ 4K tokenFlash Attention 级别优化足够
长文档问答、多轮对话8K ~ 32K token强烈建议关注注意力成本优化
代码仓库理解、超长文档分析32K token 以上必须考虑线性注意力或稀疏注意力

判断标准很简单:当序列长度超过 4K 以后,注意力分数矩阵的规模就开始从“可以接受”变成“明显吃力”。超过 8K 后,如果不做任何优化,光是注意力计算就会成为系统瓶颈。

10. 实践建议:如果你要评估一个长文本方案

工程师在选型时,往往容易被“支持超长上下文”的宣传吸引。这里给你几个可落地的评估思路。

10.1 先测延迟曲线,再测精度

不要只看模型能不能处理 128K 上下文,要看它在 128K 上下文下的延迟。真正该做的是:固定相同的 prompt 前缀,分别测试 1K、4K、16K、32K 下的生成首 token 延迟。如果延迟曲线接近线性,说明方案的有效性较高;如果延迟在某个长度后突然暴涨,说明它的注意力实现可能还是二次方瓶颈。

10.2 监控 KV Cache 的变化

推理阶段最有价值的监控指标不是显存总量,而是 KV Cache 占用的显存变化。如果序列长度增加时,KV Cache 增长过快,说明方案在存储上并没有做到真正的优化。

10.3 用小规模任务验证效果

线性注意力之类的高效方案通常会在某些任务上有精度损失。上线前,用你自己的业务数据构造长文本测评集,对比标准全注意力和优化方案的输出质量。如果损失在可接受范围内,效率收益就值得拿;如果业务对精度要求极高,就尽量选择不改变算法结果的方案,比如 Flash Attention。

11. 常见问题与排查思路

问题现象可能原因排查方式解决方案
长文本训练时 OOM注意力分数矩阵占用显存过大用torch.cuda.memory_summary()查看显存分配,确认n × n矩阵是否被实例化换用 Flash Attention 或梯度检查点
生成速度随上下文增长明显变慢每次生成都要和全部历史计算注意力对比不同上下文长度下的首 token 延迟,绘制延迟曲线使用 KV Cache、PagedAttention,或评估线性注意力方案
KV Cache 显存占用过高缓存了所有层的 K/V计算理论 KV Cache 大小,对比实际显存占用减少上下文长度,或使用分组查询注意力 GQA
换成线性注意力后效果下降核函数近似不够精确在小规模数据集上做 A/B 测试,对比生成质量考虑混合方案,只在部分层使用线性注意力
推理速度上去了,但首批 token 延迟很高Prefill 阶段仍然在算全量注意力区分 prefill 和 decode 阶段分别测时对 prefill 阶段做 chunked prefill 或算法级优化

12. 工程落地建议与注意事项

如果你决定在你的项目里尝试降低注意力成本的方案,有几点工程经验值得注意。

12.1 先区分训练和推理的瓶颈

训练阶段,瓶颈更多在计算量本身;推理阶段,瓶颈经常在显存带宽和 KV Cache 容量。同一个优化手段,在训练和推理里的收益可能完全不同。比如 Flash Attention 在训练阶段收益明显,推理阶段如果已经用了 KV Cache,提升幅度就会小一些。

12.2 尽量复用成熟实现

不要自己从头实现稀疏注意力或线性注意力。Hugging Face Transformers、FlashAttention、xFormers 等库已经有大量经过验证的实现。自己实现的版本可能在精度、数值稳定性、边界处理上踩很多坑。

12.3 做效果回归,不要只看速度

任何复杂度优化都必须配合效果回归。建议准备一个包含长文本依赖任务的数据集,例如“从第 5000 个 token 里找答案”的阅读理解任务。这类任务最能反映模型是否真的学会了长距离建模。

12.4 注意上下文长度的实际效果

有些模型宣传支持超长上下文,但实际使用时可能只对长文档中的局部片段敏感。上线前用你自己的数据做“定位式”测试,比跑一个通用榜单更有参考价值。

13. 总结与后续学习方向

全注意力机制之所以贵,不是因为某个具体实现写得差,而是它从设计上就要让每个 token 和所有 token 建立两两联系。这种“每个新词都要重翻百万页记录”的做法,带来了 O(n²) 的计算复杂度和 O(n) 的 KV Cache 存储成本。在序列长度超长后,这两项成本都会让人难以承受。

解决这个问题的方向大致有四条:稀疏注意力、线性注意力、Flash Attention 这类 IO 优化,以及综合性的工程方案。Kimi Linear 大概率属于第二条路线,也就是通过线性复杂度的注意力来降低长文本处理成本。至于最终效果如何,要看它在算法精度和工程效率之间的平衡做得怎么样。

如果你接下来想深入这个方向,可以按顺序做三件事:

  1. 用文中的最小代码示例跑一遍,把全注意力的复杂度曲线画出来,建立直觉;
  2. 阅读 Linear Attention 的经典论文,理解核函数近似的数学原理;
  3. 在你的业务场景里选择一个小规模任务,对比标准注意力和优化方案的实际差异。

技术的取舍永远是效率与质量的平衡。理解全注意力为什么贵,并不是为了抛弃它,而是为了在正确的场景选择正确的工具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询