跳到主要内容

长文的两张账单 — 一张随长度平方涨,一张随长度线性涨

这一章讲三件事: 长对话为什么必然变慢变贵(两张账单,算法不同); 工程师们想出的几种省法,各自砍的是哪一张;以及一个容易被销量数字盖住的真相: 窗口大,不等于读得懂。

它在全书链条里的位置: 第 09 章末尾提过那张 N×N 的表「随长度平方涨」, 这一章把它算成钱。关心成本、关心长文档、关心「100 万词元上下文」 这类宣传语的人,这一章是解毒剂。

1. 为什么对话一长,就变慢变贵

你和大模型聊天,聊到几十轮,它会明显变慢。第一反应是「服务器(替你跑模型的那台远端机器)忙」—— 但就算整台机器只服务你一个人,它还是会变慢。慢的原因在账单本身。

回忆第 09 章那张打分表:每生成一个新词,这个新位置都要和 前面所有位置各算一次「像不像」。对话有 2000 个词元时, 是 2000 次比较;涨到 10 万个词元时,是 10 万次—— 而且每生成一个词都要重来一遍。

这里先明确一个第 08 章提过、这一章要算账的词:显存 (显卡自己带的工作内存,模型运行时所有中间结果都得住在这里; 它比一般说的内存贵得多、小得多,一块高端卡也就几十到一两百 GB)。

对话变长,有两笔账同时在涨。这两笔账各是什么,第 4 节算。 先讲一个让账单不至于更离谱的基本优化——缓存(把算过的存下来,下次直接用)。

2. 键值缓存:别把旧内容重算一遍

生成第 101 个词时,要和前 100 个位置各比一次。 问题来了:前 100 个位置的键和值,在生成第 100 个词时不是刚算过吗?

它们没变。 第 1 到 100 个位置的内容是确定的, 它们的键和值也是确定的——变的只是多了一个新位置。 于是有一个再自然不过的优化:把算过的键和值存起来,下次直接用。 这个做法叫键值缓存(KV 缓存)1

没有缓存:生成第 101 个词
→ 把第 1~100 个位置的键、值全部重算一遍,再算第 101 个

有缓存: 第 1~100 个位置的键、值早就在显存里
→ 只算第 101 个位置自己的键、值,拿它的查询去和旧缓存比

这张图看的是缓存做的事:旧内容只算一次,存起来反复用。

书里的比方很妙:写侦探小说时的资料夹—— 前面每个角色、地点、线索的索引(随取随查的查询卡片)已经整理好了, 写新一页时不必把前文重读一遍,拿新线索的查询去和旧索引卡比对就行2

键值缓存不改变答案,只避免重复劳动。 没有它,长对话根本不可能实时响应。

3. 缓存吃掉的是显存

但缓存不是白赚的。那些旧键、旧值,要一直住在显存里。

每一个位置,每一层,都要存一份键和一份值。 一个 12 层、每层 768 宽的模型,一个词元要存 12 × 768 × 2 个数; 一段 10 万词元的对话,就是十几亿个数—— 光这份缓存就要吃掉几个 GB 的显存,而且对话每多一个词元,它就多长一点3

书里说得直接:键值缓存要存储所有已生成位置的键和值, 长对话会占用大量显存,这是当前大模型服务的主要瓶颈之一4

所以真实的服务商面对的局面是:同一张卡,既要装模型本身, 又要给每个用户的对话留一块缓存。能同时服务多少人, 常常不是算力决定的,是显存决定的。

4. 走查:同一段对话,从 2K 涨到 100K

这一章的主走查:把上面两笔账,用一段对话的两个长度各算一遍。

假设对话从 2,000 个词元涨到 100,000 个词元——50 倍。 (词元就是第 09 章说的「模型眼里的文字小块」,一个汉字上下。)

第一张账单:计算。 每生成一个新词,新位置要和全文两两全比:

对话长度生成一个词要比较几次参照
2,000 词元2,000 次
100,000 词元100,000 次长度的 50 倍
累积到生成完整段随 N² 涨50² = 2,500 倍

这张表看的是平方的脾气:长度涨 50 倍,计算账单涨 2,500 倍。 书里写得很清楚:序列翻倍、计算量翻四倍5

第二张账单:显存。 键值缓存的大小和长度成正比:

对话长度缓存大小参照
2,000 词元1 份
100,000 词元50 份长度的 50 倍,线性

两张账单,一张平方,一张线性。这个区分是这一章的钥匙—— 后面每一种省法,砍的都是其中特定的一张。

计算账单 ───────────────╱ 平方:长 50 倍,贵 2500 倍
──────╱
显存账单 ──────────────╱ 线性:长 50 倍,贵 50 倍
────────╱
2K 100K

这张图看的是为什么「长文」是个工程主战场:两张账单各有各的陡,一种省法治不了两张。

5. 分组共享键值:少存几份

第一种省法,砍显存那张。

标准的多头注意力里,12 个头各有各的键和值,缓存就要存 12 份。 分组查询(几组查询头共用同一份键和值)注意力的做法:把头分成几组,组内共享同一份键和值—— 比如 12 个头分 4 组,缓存就从 12 份变 4 份6

显存直接省三分之二,而质量损失有限。 分组是那个被广泛验证过的折中点: 太极端的版本(全部 12 个头共享一份)省得更多,但掉的质量开始明显; 分组在两个极端中间,是 Llama 系列等模型的实际选择7

补充(不在书里,依据我们的 frontier 书架): 这个「几组查询共享一份键值」 在 Llama 的真实实现里就是一个整数:注意力头数除以键值头数, 等于每个键值要被几组查询共用8

6. 稀疏注意力:不必两两全比

第二种省法,砍计算那张。

平方账单的根源是「每个位置都要和所有位置比」。 稀疏注意力的质问是:真的都要吗?

一句话里,绝大多数位置只和邻近的几个位置、外加少数几个特殊位置 (句首、标点、关键的标记——序列里辨识度最高的位置)强相关。那就只比这些: 每个位置看一个局部窗口,再留少数几个能看全局的特殊位置。 比较次数从 N × N 降到 N × log N 甚至接近 N9

标准: 每个位置都要和全部 N 个位置比 N×N 个格子全填满
稀疏: 每个位置只看左右邻近几个 + 少数全局位 大部分格子是空的

这张图看的是稀疏:打分表从「填满」变成「只填少数几行几列」。

代价是表达力:远处的两个位置可能恰好有重要关系,稀疏模式没覆盖到,就漏了。 书里点名的 Longformer、BigBird 等都沿这条路走10

7. 线性注意力:用近似换阶数

第三种省法,也砍计算那张,但手法更激进。

线性注意力用数学近似,把 N × N 的计算直接化简成随 N 线性增长—— 不是「少比几对」,是换一种算法,绕开整张打分表11

代价书里说得很诚实:精度。 近似毕竟不是等价, 某些任务上会有损失,至今还没有一个版本能完全替代标准注意力12

记住第 07 章末尾那句话:历史不是直线。 线性注意力和状态空间模型在精神上很近—— 都在试图用「随长度线性涨」的代价,换「能和标准注意力比肩」的效果。 这场竞赛今天还没分出胜负。

8. 闪存注意力:不改数学,只改搬运

第四种省法,思路完全不同:一个字的数学都不改。

观察一个事实:显卡里有两层存储,一层快但小(高速缓存), 一层大但慢(就是第 1 节说的显存)。标准实现里,那张 N × N 的打分表 要真的写进显存、再读回来——大部分的时间不是花在算上,是花在搬运上

闪存注意力重新设计了计算顺序:把打分表切成小块, 每块在快的那层里算完就扔,自始至终不把整张表写进显存13。 数学上它和标准注意力一模一样——结果分毫不差; 工程上,它同时省时间又省显存。

补充(不在书里,依据我们的 frontier 书架): 省的幅度, 项目自己的测试写得明白:序列 2K 时显存省约 10 倍,4K 时省约 20 倍—— 长度越长,省得越多,因为标准注意力的中间表随长度平方涨,它不写这张表14

书里给它的定位值得抄:这是「工程优化如何改变可能边界」的典范—— 靠的不是新算法,而是对硬件架构的深入理解15。 今天训练大模型的框架几乎都集成了它。

9. 显存分页:借操作系统的老招

第五种省法,还是砍显存那张,但砍的是「浪费」而不是「用量」。

给每个用户预留缓存空间时,按最大可能长度整块预留, 大部分时间是空的——就像给每个租客都按八口人分一套房, 实际只住两个人。显存分页借了操作系统管内存的老招: 把缓存切成固定大小的小块,用到哪块分哪块16

这个做法不改变任何一个数,只改变分配方式, 把「按最大预留」的浪费压下去。它是今天高并发(同时涌入大量请求)服务里的标配之一。

10. 上下文长了,就一定读得懂吗

最后一节,泼一盆冷水。

工程界把窗口从几千词元推到十万、百万量级—— 第 4 节那两张账单被各种省法压着往前走。但「塞得进去」和「读得明白」是两回事。

书里点了一个被反复观察到的现象,叫中间丢失: 模型对开头和结尾的内容关注得好,对长文中间那段,最容易漏17。 你给它一份两百页的合同,问一个藏在第 117 页的细节, 它可能答错——不是因为没看见,而是因为那一片正好在它的盲区。

所以读「支持 100 万词元」这类宣传时,正确的翻译是: 「能把 100 万词元装进去」,而不是「能把 100 万词元都用好」。 书里的建议也务实:长上下文要配合在第 19 章讲的检索(回答前先去查资料)等办法, 以及专门的长文评测,才能变成真能力18

11. 作者的判断与证据

有证据的部分。 计算随平方涨、缓存随线性涨,是机制的直接推论; 闪存注意力的显存节省倍数,是项目公开测试的数据; 分组共享是多个公开模型的真实配置。

要分开看的三处:

  1. 「质量损失有限」(分组共享)。 这是多个模型的实践经验, 不是定理。极端压缩下质量会掉,书里也说了分组是「折中点」。
  2. 稀疏和线性注意力的覆盖场景。 它们在不少任务上接近标准注意力, 但「接近」不是「等于」;书里说线性注意力「至今没有完全替代」,这个判断到今天仍成立。
  3. 中间丢失的严重程度。 不同模型、不同任务差异很大, 近年的新模型在这项上普遍比早期好。把它当成「长文默认可疑,需要验证」, 而不是「长文一定没用」。

判断(我们的,不是书里的):这一章五种省法里,真正改变世界的是第 8 节那个「一个字的数学都不改」的。 稀疏和线性想换掉问题本身,至今没赢;闪存注意力接受问题本身,只换掉搬运方式,反而成了全行业标配。 「先问瓶颈在算还是在搬」,是这个时代最值钱的工程直觉之一。 如果错,会错在: 如果未来某种近似注意力在质量上彻底追平, 「不改数学」就会从优点变成包袱——毕竟近似换来的是阶数上的根本改善。 今天的标配,未必是终局。

12. 边界与局限

  • 各省法之间的组合没有展开。 真实系统常常几种一起用, 互相之间的搭配约束,书里没有系统讲。
  • 「缓存具体多大」只给了算法没给完整公式。 它取决于层数、宽度、 头数、数字格式,每个模型不同;第 3 节那笔账是按教学级配置算的。
  • 量化和压缩缓存只提了名字。 把缓存里的数压成更省的格式,是另一条省显存的路, 书里一句话带过。
  • 中间丢失没有量化数据。 书里给了现象和名字,没给「漏掉多少」的统计。
  • 这一章只讲了生成时的账单。 训练时的长文账单另有一套算法(更重), 书里没展开。

13. 可带走的

  1. 长文有两张账单:计算随长度平方涨,显存随长度线性涨。 分不清这两张,就看不懂任何省法。
  2. 键值缓存:旧内容只算一次。 它不改变答案,只避免重复劳动——代价是显存。
  3. 能同时服务多少用户,常常是显存决定的,不是算力。
  4. 分组共享键值:12 份变 4 份,砍显存那张。 分组是被验证过的折中点。
  5. 稀疏注意力:打分表只填少数几行几列,砍计算那张。
  6. 线性注意力:用近似把平方换成线性,至今没能完全替代标准注意力。
  7. 闪存注意力:一个字的数学都不改,只不把中间表写进显存。 2K 省 10 倍、4K 省 20 倍。
  8. 显存分页:用到哪块分哪块,砍的是浪费。
  9. 窗口塞得下,不等于读得懂。 中间那段最容易被漏掉。
  10. 读「100 万词元」的宣传,自动翻译成「装得进去」。 用得好不好,要另测。

14. 原文地图

主题原书章原文位置
键值缓存第5章 注意力:引爆大模型的引擎text/06-ch05.txt:421(搜「KV 缓存」)
资料夹比方第5章 注意力:引爆大模型的引擎text/06-ch05.txt:424(搜「资料夹」)
缓存的显存代价第5章 注意力:引爆大模型的引擎text/06-ch05.txt:445(搜「主要瓶颈之一」)
计算随平方涨第5章 注意力:引爆大模型的引擎text/06-ch05.txt:350(搜「序列翻倍、计算量翻四倍」)
分组查询注意力第5章 注意力:引爆大模型的引擎text/06-ch05.txt:348(搜「把头分成若干组」)
稀疏注意力第5章 注意力:引爆大模型的引擎text/06-ch05.txt:351(搜「稀疏注意力」)
线性注意力第5章 注意力:引爆大模型的引擎text/06-ch05.txt:354(搜「线性注意力」)
闪存注意力第5章 注意力:引爆大模型的引擎text/06-ch05.txt:358(搜「闪存注意力」)
长上下文优化路线第5章 注意力:引爆大模型的引擎text/06-ch05.txt:411(搜「位置编码改进」)
中间丢失第5章 注意力:引爆大模型的引擎text/06-ch05.txt:515(搜「关注开头和结尾」)

Footnotes

  1. 出处:「第5章 注意力:引爆大模型的引擎」第 421 段(text/06-ch05.txt:421,搜「KV 缓存」)。 原文:「下次生成新词时只算新位置的 key、value,与缓存中的旧 key、value 一起做注意力」。

  2. 出处:「第5章 注意力:引爆大模型的引擎」第 424 段(text/06-ch05.txt:424,搜「资料夹」)。

  3. 这笔账是我们按教学级配置算的,不是书里的数: 12 层 × 768 宽 × 键值两份 × 每数 2 字节, 10 万词元 ≈ 3.7 GB。书里只说「长对话会占用大量显存」,没给数。

  4. 出处:「第5章 注意力:引爆大模型的引擎」第 445 段(text/06-ch05.txt:445,搜「主要瓶颈之一」)。

  5. 出处:「第5章 注意力:引爆大模型的引擎」第 350 段(text/06-ch05.txt:350,搜「序列翻倍、计算量翻四倍」)。 走查里 2K 到 100K 的倍数是我们算的:长度 50 倍,平方账单 2,500 倍,线性账单 50 倍。

  6. 出处:「第5章 注意力:引爆大模型的引擎」第 348 段(text/06-ch05.txt:348,搜「把头分成若干组」)。 极端版(全部头共享一份)叫多查询注意力,同一段给出。

  7. 出处:「第5章 注意力:引爆大模型的引擎」第 349 段(text/06-ch05.txt:349,搜「Llama 2」)。

  8. 补充(不在书里,依据我们的 frontier 书架):Llama 的注意力里, 键值要被复用的次数 = 注意力头数 ÷ 键值头数。 依据: shelf=ai-frontier-reference/transformers@src:src/transformers/models/llama/modeling_llama.py:225 @b6c0bfe04c823a7b2ca48f91b8b91b2a7741f309 事实=num_key_value_groups = num_attention_heads // num_key_value_heads。

  9. 出处:「第5章 注意力:引爆大模型的引擎」第 351 段(text/06-ch05.txt:351,搜「稀疏注意力」) 与第 353 段(text/06-ch05.txt:353,搜「Longformer」)。

  10. 出处:「第5章 注意力:引爆大模型的引擎」第 353 段(text/06-ch05.txt:353,搜「Longformer」)。

  11. 出处:「第5章 注意力:引爆大模型的引擎」第 354 段(text/06-ch05.txt:354,搜「线性注意力」)。

  12. 出处:「第5章 注意力:引爆大模型的引擎」第 357 段(text/06-ch05.txt:357,搜「完全替代」)。 原文:「线性注意力的代价是精度,近似毕竟不是等价…… 至今还没有一个版本能完全替代标准注意力」。

  13. 出处:「第5章 注意力:引爆大模型的引擎」第 358 段(text/06-ch05.txt:358,搜「闪存注意力」)。 原文:「让中间结果尽量留在 GPU 的高速缓存(SRAM)里,而不是频繁读写显存(HBM)」。

  14. 补充(不在书里,依据我们的 frontier 书架):FlashAttention 项目的测试报告: 2K 省 10 倍、4K 省 20 倍。 依据: shelf=ai-frontier-reference/flash-attention@src:README.md:515 @0251105a2fb19d2957484b7f023cd8c115286ced 事实=README 写着「We see 10X memory savings at sequence length 2K, and 20X at 4K」。

  15. 出处:「第5章 注意力:引爆大模型的引擎」第 360 段(text/06-ch05.txt:360,搜「工程优化如何改变可能边界」)。

  16. 显存分页这一节是书里「各种缓存压缩、共享、量化技术」一句的展开, 出处:「第5章 注意力:引爆大模型的引擎」第 445 段(text/06-ch05.txt:445,搜「缓存压缩」)。 分页这个具体做法书里没展开,我们按行业通行做法(vLLM 一类)补了机制; 这是补充说明,不是书里的内容。

  17. 出处:「第5章 注意力:引爆大模型的引擎」第 515 段(text/06-ch05.txt:515,搜「关注开头和结尾」)。

  18. 出处:「第5章 注意力:引爆大模型的引擎」第 518 段(text/06-ch05.txt:518,搜「长上下文评测」)。