https://x.com/i/article/2099600211739803648
Prefill and Decode
只要三条结论,为什么还得等?
还有十分钟开会。你把一份几十页的项目文档发给 AI:“帮我挑出三条最值得提醒老板的风险,简短一点。”
屏幕安静了一会儿。你忍不住想:只要三条结论,怎么还没开始?
过了一会儿,回答开始出现,三条结论很快就写完了。换一天,你只发了一句“帮我写一篇两千字的演讲稿”,它倒是很快开口,却要写上好一阵。
“多久开始回答”和“之后写得多快”,背后有两段不同的计算:
prefill 处理已经给定的输入,decode 根据已有内容继续生成。
先看图里的第一个输出。请求发出到收到它的时间,叫 TTFT(time to first token);之后相邻输出 token 的间隔,叫 ITL(inter-token latency)。两者都能影响你觉得 AI 快不快。
理解 prefill 和 decode,能帮助我们把问题问得更具体。
Prefill:要先处理几十页输入
那份文档里,前面写着“周五上线”,后面写着“支付接口还没联调”。你想得到的风险判断,需要利用这些输入信息。
Prefill 就发生在这里:模型把 prompt 的 token 转成 representation,逐层执行 attention 和前馈网络,同时保存各层的 K/V,供后续生成使用。
要求回答简短,并不会自动省掉对长文档的输入处理。
不过,处理文档无须像生成回答那样,一个 token 接着一个 token 等。
把例子缩到四个输入 token:它们已经全在 prompt 里,内容都已确定。进入某一层后,可以把四个位置的 representation 排成矩阵,批量计算 Q、K、V。
你可能会问:
第二个位置要用到第一个位置的信息,怎么还能并行?
看下面两行。输入 h₁ 产生 Q₁、K₁、V₁;输入 h₂ 产生 Q₂、K₂、V₂。第一个位置读取自己的 K/V,第二个位置用 Q₂ 读取前两个位置的 K/V。
关键在那条斜线:第二个位置需要 K₁/V₁,它们从这一层的输入就能算出来,无须等待第一个位置的 attention 输出 O₁。 因此,所需 Q/K/V 准备好后,两行 attention 可以批量、并行计算。
“并行”不要求 GPU 在同一个瞬间完成所有操作。Causal mask 仍限定每个位置只能读取自己和前文的 K/V;读取范围有先后边界,计算输出无需按位置排队。
层与层之间则仍有依赖:这一层经过 attention、残差连接和前馈网络等操作得到的输出,成为下一层的输入,再计算新的 Q/K/V。
回到文档,“支付接口还没联调”对应的后方位置可以参考前面的上线安排;前方位置无法读取后来才出现的内容。
走完最后一层,最后一个输入位置的 representation 用于预测第一个新 token。经过采样或其他选择规则,回答终于开始出现。
留意下面的变化:第一个新 token 刚出现时,cache 里仍只有输入位置的 KV。 新 token 自己的 KV,要等它被送回模型才会产生。
Decode:刚写出来的内容,为什么又要送回去?
假设回答刚生成一个 token “领导”,接下来生成冒号
“领导” 会变成下一步的输入:模型处理它,读取前文的 KV,计算并保存它在各层的新 KV,再预测 “冒号” 可以成为下一步的输入,如此继续,直到停止。
图里把这两个新 token 简写成 y1、y2。沿着外侧回路看:上一步选出的输出,就是下一步送入模型的输入。
不妨数一下:如果原来有四个输入 token,选出 y1 时,cache 仍有四个位置;把 y1 送回模型处理后,才增加到五个,并得到预测 y2 所需的输出。
那份几十页的文档呢?它的有效 KV 留在原处。Causal attention 下,追加后文不会反过来改变前文的 representation,因此无须每写一个新 token,就把整份文档从头再算一遍。新 query 仍要读取相关历史 K/V,这部分工作还在。
为什么不把整段回答一起算出来?因为 prompt 已经全部给定,回答后面的 token 还没确定。常规自回归 decode 要先选出当前 token,后一步才拿得到自己的新输入。
读长报告,和写长演讲稿,慢在同一个地方吗?
现在回到开头的两个任务:
A:“读完这份长报告,只给我三条结论。” 输入多、输出少,要重点关注 prefill。
B:“请写一篇两千字的演讲稿。” 输入短、输出长,反复进行的 decode 可能占据大部分等待时间。
两个阶段使用同一个 decoder-only Transformer、同一套参数,硬件面对的工作却不同。
Prefill 一次带来许多已知位置。同一份权重可以参与多行输入的矩阵计算,通常更有机会复用已经读入的权重。单请求的普通 decode,每步只带来一行新输入,却仍要执行模型、读取权重和相关 KV。
于是就有一个常见倾向:
长输入的 prefill 往往更受计算能力影响,小 batch 的 decode 往往更受内存带宽影响。
后者的瓶颈常出在把数据送到计算单元的速度上。
这条经验要结合任务看。增大 batch、改变模型或 kernel,瓶颈都可能变化;full attention 下,上下文越长,新 query 要读取的历史 KV 通常也越多。开头快,不保证一整篇都写得快。
所以下次看到一个 tokens/s 数字,可以多问一句:
它是在读多长的输入、写多长的回答、同时服务多少人?
别人的长文档,会让你的回答卡住吗?
你的演讲稿正写到一半,同事又提交了一整本产品手册。两条请求如果共享计算资源,长 prefill 占用过长的连续执行时间,就可能让你的后续输出出现停顿。
怎么办?一个办法是把长文档的输入计算拆成小段,在调度迭代中给正在生成的回答安排计算机会。这叫 chunked prefill,实现中也可以把 prefill chunk 与 decode 工作放进同一批处理。
这相当于把工作分几次安排。上下文保持连续:处理后面的 chunk 时,仍会读取前面已经保留的 KV。
另一个办法是分配不同的资源:一组 worker 专门处理输入,另一组接着生成回答。这叫 prefill/decode disaggregation,两边可以分别调整容量和调度策略。
这时必须完成一次交接。后一组要拿到文档对应的 KV,以及继续生成所需的状态,例如 token 位置、已经选出的新 token,才能从原来的地方接着算。
两边的模型参数、位置处理和 cache 格式需要兼容,通常也都要具备执行同一模型的能力。分工可以减少阶段间干扰,但交接有传输、转换和协调成本。请求很短时,来回传数据甚至可能比省下的时间还多。
还有更直接的问题:
这份文档刚才已经处理过,再问一次呢?
比如先问“总结风险”,再问“列出截止日期”。如果请求开头的实际 token prefix 完全一致、执行条件兼容,且这部分 KV 仍被保留,就可以通过 prefix caching 复用它。新的问题仍需要 prefill,新的回答仍需要 decode。
三个办法各管一件事:prefix caching 省去可复用的输入计算;chunked prefill 把剩余输入计算分段安排;disaggregation 把两个阶段放到不同资源上。
Spark 处理输入,Mac 接着写,行不行?
沿着这条思路,问题就很自然了:能不能让一台机器处理长文档,再让另一台接着生成回答?
EXO 展示过这样一个组合:DGX Spark 执行 prefill,通过网络传递 KV,再由 Mac Studio M3 Ultra 执行 decode,延续同一条请求。
交接也可以边算边进行:前面某层的 KV 准备好,先发出去,同时继续计算后面的层。这样,部分传输与计算可以重叠;末尾尚未传完的数据、格式转换,仍然要花时间。
回到开会前那份文档:你只要三条结论,输入却有几十页。Prefill 要处理这些已知内容;decode 利用保存的状态,把回答逐步生成出来。
下次觉得 AI 慢,可以先分清:是迟迟没有第一个输出,还是回答开始后写得慢? 再把输入长度、输出长度和并发量放进去看,优化的问题就具体多了。