如果你训练或推理过大语言模型,大概率遇到过这样的现象:序列长度从 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)) @ VQ是查询(Query):表示“我现在想找什么信息”;K是键(Key):表示“我这条信息是什么主题”;V是值(Value):表示“我这条信息的实际内容”;d_k是键向量的维度,用于缩放点积结果。
计算分三步:
- 用
Q和所有K做点积,得到每个位置和其他所有位置的相似度分数; - 对分数做 softmax,归一化成权重;
- 用权重对所有
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 token | Flash 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 大概率属于第二条路线,也就是通过线性复杂度的注意力来降低长文本处理成本。至于最终效果如何,要看它在算法精度和工程效率之间的平衡做得怎么样。
如果你接下来想深入这个方向,可以按顺序做三件事:
- 用文中的最小代码示例跑一遍,把全注意力的复杂度曲线画出来,建立直觉;
- 阅读 Linear Attention 的经典论文,理解核函数近似的数学原理;
- 在你的业务场景里选择一个小规模任务,对比标准注意力和优化方案的实际差异。
技术的取舍永远是效率与质量的平衡。理解全注意力为什么贵,并不是为了抛弃它,而是为了在正确的场景选择正确的工具。