读是并行的,写是一个一个的
用过大模型 API 的人都注意过一件事:输出的 token 比输入的贵好几倍。直觉的解释是「写比读难」——生成需要更多智能。这一章拿 GPT-2 在本机量一遍就会发现,这个解释完全不对:每个 token 的计算量,读和写一模一样。贵的是别的东西。弄清楚它,你会顺便看懂价目表上的另外两行:为什么长上下文贵,为什么「缓存命中」便宜得离谱。
同一台 GPT-2、同一台笔记本,做两件事:
- 读:一次把 512 个 token 的前文喂进去,算出每个位置的候选表;
- 写:一个 token 一个 token 地往外生成 511 个(已经用上了下面要讲的「KV 缓存」,不做任何重复计算)。
问:按「每秒处理多少个 token」算,读比写快多少倍?
答案在本章末尾。
读:一次前向,算出所有位置
第 9 章讲过,GPT-2 的注意力带着一道因果遮罩:每个位置只能看它前面的位置。这道遮罩有一个重要的副作用:一次前向就能同时算出所有位置的候选表,而且第 t 个位置的表和「后面写了什么」毫无关系。
所以「读」一段已经存在的文字,是彻底并行的:512 个 token 的向量排成一个 512 行的矩阵,每一层都是几次大矩阵乘法,一口气推到底。训练也正是这样做的——一段 1024 个 token 的文字跑一次前向,就同时得到了 1023 个「猜下一个」的练习题,一起算损失、一起反向传播(这种「每个位置都用真实的前文去猜」的做法,叫 teacher forcing)。
写:只能一个一个来
生成就不同了。第 t+1 个 token 要等第 t 个 token 定下来才能算——而第 t 个是从一张表里抽出来的,抽之前谁也不知道是什么。于是写 511 个 token,就得老老实实跑 511 次前向,每次只处理 1 个新 token。
每次只处理 1 个 token,计算量并不大:每个参数大约一次乘、一次加。麻烦在于,为了这一点点计算,整个模型的权重都得从内存里搬到计算单元上走一趟。GPT-2 small 的权重是 500 MB(float32),每写一个 token 就要搬 500 MB;而读的时候,搬一次权重可以喂饱 512 个 token 的计算。本机实测:
| 用时 | 每秒处理 | |
|---|---|---|
| 读:一次吃下 512 个 token | 0.086 s | 5947 token |
| 写:一个一个吐出 511 个(带 KV 缓存) | 5.21 s | 98 token |
| 读比写快 | 60.6 倍 | |
同一个模型、同样的计算量,读比写快 60 倍。读的时候,瓶颈是算力,硬件被喂得饱饱的;写的时候,瓶颈是搬运权重的内存带宽,算力大部分时间在等。这就是为什么所有大模型服务都把这两段分开对待:处理你的提示词叫 prefill(预填充),一个个生成叫 decode(解码),各有各的指标——「第一个字出来要多久」主要是 prefill,「之后每秒吐几个字」就是 decode。
KV 缓存:读过的不要再算一遍
写的过程还有一个隐藏的浪费。每生成一个新 token,注意力要拿它的查询去和前面所有位置的键、值做运算。如果每一步都把整段前文从头算一遍,第 100 步就要重算前 99 个位置的键和值——而它们和上一步算的一模一样。
省掉这份浪费的办法是把每一层、每个位置的键和值存起来,叫 KV 缓存。新的一步只算新 token 自己的那一份,前面的直接查缓存。本机用 transformers 量「带不带缓存」写同样多的 token(两种方式写出来的 token 逐个相同):
| 写多少个 token | 不带缓存 | 带缓存 | 快了 |
|---|---|---|---|
| 256 | 8.41 s | 2.58 s | 3.26 倍 |
| 768 | 46.83 s | 8.33 s | 5.62 倍 |
写得越长,差距越大:不带缓存时,第 n 步要重算 n 个位置,总代价随长度平方增长。(在本书自己的 numpy 版 GPT-2 上,带缓存逐步算和一次全算,最后一个位置的 logits 最大只差 5.8 × 10⁻⁶——同一个结果,只是浮点舍入的先后不同。)
缓存不是白给的,它吃内存。每个 token 要存:2(键和值)× 12 层 × 768 维 × 2 字节(半精度)= 36864 字节,也就是 36 KiB;装满 GPT-2 的 1024 个位置是 36 MiB。这个数在小模型上不起眼,在今天的模型上是大事:第 19 章会读真实的配置文件算出来,一个 80 亿参数的模型每个 token 要 128 KiB,一段 128K token 的长对话光 KV 缓存就是 16 GiB。长上下文贵,一半贵在注意力的平方级计算,另一半贵在这块随长度线性增长的内存。
顺带把计算量也算清楚:GPT-2 small 不算嵌入表有 8506 万个参数,每处理一个 token 大约要 2 × 8506 万 ≈ 1.7 亿次浮点运算;加上 1024 长度时的注意力和最后乘回 50257 个候选的那一步,约 2.66 亿次。读和写,每个 token 都是这么多——差别只在能不能一起算。
这就是价目表的样子
把这一章的物理,对到一张真实的价目表上(Anthropic 公开的 API 定价,2026 年 10 月):
| 模型 | 输入(每百万 token) | 输出 | 缓存命中的输入 |
|---|---|---|---|
| Claude Opus 5.5 | 4 美元 | 20 美元 | 0.20 美元 |
| Claude Sonnet 5.5 | 2 美元 | 10 美元 | 0.20 美元 |
- 输出是输入的 5 倍:读能把整段摊开并行算,写只能一个一个来、每个都要把权重搬一遍。不是「写更聪明」,是「写更难并行」。
- 缓存命中的输入只要原价的 1/20(Opus)到 1/10(Sonnet):如果你每次请求都带着同一段很长的开头(系统提示、文档、工具定义),服务端可以把读这段开头算出来的中间结果存下来,下次直接接着用——原理就是这一章的 KV 缓存(各家的具体实现不公开)。省下的是整段 prefill。
- 长上下文按 token 线性收费,但服务方的成本不止线性:注意力随长度平方增长,KV 缓存随长度线性占内存、挤掉同时服务别人的余量。所以「把整本书塞进上下文」能用,但不是免费的午餐。
最后一个有意思的推论。Thank you very 后面,␣much 的概率是 99.16%——这种「下一个几乎确定」的位置,在真实文字里一抓一大把(第 14 章:14.2% 的位置,第一名就超过 90%)。既然写慢、读快,能不能让一个小模型先猜出接下来的好几个 token,再让大模型一次性读一遍、核对哪些猜对了?这就是投机解码(speculative decoding):把「写」偷偷换成「读」。猜对的那几个 token 等于白捡,猜错了就从错的那里重来,结果和大模型自己一个一个写完全相同。
同样的两组对比——带不带 KV 缓存、读和写——用 transformers 量:
import time, torch
from transformers import AutoTokenizer, AutoModelForCausalLM
tok = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2").eval()
x = tok(open("chatgpt-2026-10-21.txt").read(), return_tensors="pt").input_ids[:, :512]
with torch.no_grad():
for use_cache in (True, False): # 同样写 256 个 token:带不带 KV 缓存
t = time.perf_counter()
model.generate(x[:, :16], max_new_tokens=256, do_sample=False, use_cache=use_cache, pad_token_id=50256)
print(f"use_cache={use_cache!s:5} 写 256 个 token 用 {time.perf_counter() - t:.2f} s")
model(x) # 预热
t = time.perf_counter(); model(x); read = time.perf_counter() - t
out = model(x[:, :1], use_cache=True); past = out.past_key_values
t = time.perf_counter()
for i in range(1, 512): # 一个一个写,每次只喂一个新 token
out = model(x[:, i:i + 1], past_key_values=past, use_cache=True); past = out.past_key_values
write = time.perf_counter() - t
print(f"读:一次吃下 512 个 token {512 / read:6.0f} token/s")
print(f"写:一个一个吐 511 个 {511 / write:6.0f} token/s(慢 {(512 / read) / (511 / write):.0f} 倍)")
本机输出(计时类的数字换台机器、换一次运行都会变;这段代码和正文表格是分开跑的两次,比值都在 60 倍上下):
use_cache=True 写 256 个 token 用 2.78 s use_cache=False 写 256 个 token 用 8.28 s 读:一次吃下 512 个 token 5951 token/s 写:一个一个吐 511 个 99 token/s(慢 60 倍)
pip install torch transformers && python 15.py
在 Colab 的 GPU 上跑,绝对速度会快很多,但「读远快于写」这一点不会变——GPU 的算力比内存带宽多得更多,这个倍数往往更大。
- 流式输出(字一个个蹦出来)不是为了好看:第一个字之前的等待是 prefill,之后每个字的间隔是 decode。提示词很长时,第一个字出来得慢;回答很长时,后面慢。
- 服务端会把很多人的请求拼成一批一起 decode:搬一次权重,同时给几十个用户各算一个 token,把「写」的效率拉回来。代价是每个人的延迟会受别人影响——《撞墙》里讲的「利用率越高、排队越长」在这里一模一样。
- 写提示词时,把不变的内容放在前面、变化的放在后面,能最大化缓存命中——因为缓存只能按「从头开始一字不差的前缀」复用。
- 同样的钱,让模型「读」比让它「写」划算得多。给它一份详细的参考资料让它挑出答案,往往比让它从头生成一大段更便宜、也更准。
「输出 token 更贵,是因为生成比理解更难、需要更多智能。」
每个 token 的计算量,读和写是一样的(GPT-2 small 约 2.66 亿次浮点运算)。差别在于能不能一起算:读可以把整段前文排成一个矩阵并行地推,写却必须等上一个 token 抽出来才能算下一个,每一步都要把全部权重从内存里搬一遍。本机实测,读比写快 60.6 倍。
这个区分很实用:凡是能让模型「读」来完成的事(从给定的资料里找、在给定的选项里挑),都比让它「写」便宜得多;而降低延迟的工程技巧,几乎都是在想办法把写变成读(批处理、投机解码、缓存)。
正确答案是 D:读每秒 5947 个 token,写每秒 98 个,快 60.6 倍。
A 「每个 token 的计算量一样」——前提完全正确,结论却错了:速度不只取决于计算量,还取决于能不能并行、数据要搬多少。这是这一章最想纠正的那个直觉。 B 「写要多做一步挑」——从表里挑一个 token 的开销小到可以忽略。慢的不是挑,是挑完之后才能开始下一步。 C 「10 倍左右」——不带 KV 缓存、写得又长的时候,浪费会更多;但即使带了缓存、没有一点重复计算,写依然比读慢 60 倍——那是带宽的账,缓存省不掉。《撞墙》讲排队:把请求攒成一批能提高吞吐,但每个请求的等待会变长——decode 的批处理就是这道取舍题。《独占》讲内存层级:从内存搬数据比算数据慢得多,这一章的「带宽瓶颈」就是那本书里的价目表在 GPU 上的版本。《同时》讲并行与串行:自回归生成是一个天生串行的依赖链。
这一章的一句话
读和写每个 token 的计算量一样,但读能把整段前文并行地算,写必须等上一个 token 定下来、每步都把权重搬一遍——所以读比写快几十倍。KV 缓存省掉了重复计算,代价是随长度增长的内存;而价目表上「输出贵、缓存便宜、长上下文不便宜」,就是这一章的物理。
下一章回到「读」的那一侧:你写的提示词,在模型眼里是什么?一个实验就够说明问题——直接问 GPT-2「法国的首都是什么」,Paris 只有 1.77%;在前面先摆一个已经答好的例子(德国的首都是柏林),Paris 跳到 63.71%。你没有教它任何新东西,你只是换了一份它要续写的文档。