第1章:预填充与解码阶段——两个阶段的计算与访存特征差异
大家好,我是你们这堂课的主讲人。做了这么多年大模型推理的硬件落地,我最大的感触就是:搞懂预填充和解码这两个阶段,你就掌握了推理优化的半壁江山。今天咱们就来把这层窗户纸捅破。
1.1 两个阶段,两种性格
大模型推理,说白了就两步走。第一步叫预填充(Prefill),第二步叫解码(Decode)。
预填充阶段,模型拿到你的输入,比如“请用Python写一个快速排序”,它要把这整句话一次性算完。这个阶段的特点是:计算密集,并行度高。你想想看,几百个token同时往里灌,GPU的矩阵计算单元能跑得飞起。
解码阶段呢?模型开始一个字一个字地往外蹦。每次只生成一个token,然后把这个token拼到已有的序列里,再算下一步。这个阶段的特点是:访存密集,并行度低。说白了,GPU大部分时间都在等数据从显存里搬过来,真正算的时间反而不多。
我当年第一次跑LLaMA-65B的时候,预填充阶段GPU利用率能到90%以上,一到解码阶段直接掉到20%以下。当时我就意识到:这两个阶段的优化思路,完全是两码事。
1.2 计算特征对比:一个吃肉,一个喝汤
咱们来具体看看计算上的差异。我习惯用一张表来对比,这样最直观:
| 对比维度 | 预填充阶段 | 解码阶段 |
|---|---|---|
| 输入长度 | 整个prompt(几十到几千token) | 1个token |
| 计算模式 | 批量矩阵乘法(GEMM) | 矩阵向量乘法(GEMV) |
| 计算强度 | 高(O(n²)) | 低(O(n)) |
| 并行度 | 极高(可跨token并行) | 极低(串行生成) |
| GPU利用率 | 80%-95% | 10%-30% |
| 主要瓶颈 | 算力 | 显存带宽 |
为什么会这样?我给你拆开讲。
预填充阶段,输入序列长度为S,模型要同时计算S个位置的注意力。每个位置都要跟前面所有位置做交互,计算量是O(S²)级别的。这正好是GPU最擅长的——大矩阵乘法。你想想看,NVIDIA的Tensor Core就是为这个设计的,一个时钟周期能算几千次乘加。
解码阶段呢?每次只生成一个token,计算量是O(S)级别的。而且这个S还在不断增长,但每次只算一个位置。这就变成了矩阵向量乘法,说白了就是拿一个向量去跟一个大矩阵做乘法。GPU的Tensor Core在这种场景下根本跑不满,大部分时间都在等数据。
我在项目中遇到过一件事:有次给客户做优化,对方说“我的GPU利用率怎么这么低”。我一看,他们用的是解码阶段的数据来评估。我说你换个思路,看看预填充阶段的利用率。结果一测,预填充阶段利用率92%,解码阶段只有18%。这不是硬件的问题,是工作负载的特性决定的。
1.3 访存特征对比:一个流水席,一个点外卖
访存这块更有意思。预填充阶段像流水席——数据一次性端上来,大家敞开了吃。解码阶段像点外卖——每次只点一份,等吃完了再点下一份。
具体来说:
- 预填充阶段:模型参数(权重)只需要加载一次,然后反复使用。输入数据也是整批加载。访存总量大,但访存次数少,每次访存都能充分利用带宽。
- 解码阶段:每次生成都要重新加载模型参数。更关键的是,KV Cache(键值缓存)的读写量随着序列长度线性增长。每次生成一个token,都要把之前所有token的KV Cache读一遍,再写一个新的进去。
我给大家算笔账。假设模型是7B参数,FP16精度,模型权重占14GB。预填充阶段,这14GB加载一次就够了。解码阶段呢?生成100个token,就要加载100次,总共1.4TB的数据搬运量。你想想看,这访存压力差了多少倍。
核心结论:预填充是“算力墙”,解码是“访存墙”。两个墙不一样,拆墙的工具也不一样。
1.4 对硬件负载的直接影响
搞清楚了特征差异,咱们来看看它对硬件到底意味着什么。
1.4.1 对GPU计算单元的影响
预填充阶段,GPU的Tensor Core能跑满。我记得有一次测试A100,预填充阶段算力利用率达到95%以上,SM(流式多处理器)几乎都在忙。解码阶段呢?SM大部分时间在等数据,Tensor Core闲着没事干。
这就引出一个问题:解码阶段要不要用Tensor Core?
我的建议是:用,但别指望它能跑满。Tensor Core处理小矩阵的效率其实不高,启动开销占比太大。有些框架在解码阶段会退回到CUDA Core来做矩阵向量乘法,反而更快。这个咱们后面讲算子优化的时候会细说。
1.4.2 对显存带宽的影响
解码阶段对显存带宽的依赖,怎么说呢,就像鱼对水的依赖。没有足够的带宽,解码速度直接腰斩。
我给大家一个经验公式:
解码速度(token/s)≈ 显存带宽(GB/s) / 每token需要搬运的数据量(GB)
每token需要搬运的数据量包括:模型权重 + KV Cache读取 + KV Cache写入。对于7B模型,这个值大约是28GB左右。所以A100(带宽2TB/s)的理论解码速度大约是:2000 / 28 ≈ 71 token/s。实际能跑到50-60就算不错了。
小技巧:如果你发现解码速度远低于理论值,先别急着优化算子。看看是不是显存带宽被其他进程占用了,或者PCIe链路降速了。我曾经遇到过因为GPU散热不好导致显存降频,解码速度直接掉了40%。
1.4.3 对内存层次的影响
GPU的内存层次从快到慢依次是:寄存器、共享内存、L1缓存、L2缓存、HBM显存。预填充阶段,数据复用率高,L1/L2缓存的命中率也高。解码阶段,每次访问的数据都不一样,缓存基本帮不上忙。
这就导致了一个现象:解码阶段几乎全靠HBM显存带宽撑着。HBM的带宽虽然高,但延迟也大(几百纳秒级别)。所以解码阶段的延迟主要花在等数据上,而不是计算上。
1.5 一张图看懂全貌
说了这么多,咱们用一张图来总结一下两个阶段的差异:
1.6 实战中的避坑指南
讲完了理论,咱们聊聊实战中容易踩的坑。
避坑1:别用预填充的指标来评估解码性能
我曾经见过一个团队,用预填充阶段的吞吐量来估算整个推理服务的性能,结果上线后延迟直接爆炸。预填充的吞吐量可能是解码的5-10倍,但用户感知的是端到端延迟,解码阶段占了绝大部分时间。
避坑2:别忽视KV Cache的显存占用
解码阶段,KV Cache会随着序列长度线性增长。对于长序列(比如2048个token),KV Cache可能占掉几十GB显存。我遇到过有人把batch size设得太大,结果解码到一半显存爆了,服务直接挂掉。建议提前算好最大序列长度下的显存占用。
避坑3:注意PCIe带宽瓶颈
如果你用多卡推理,卡间通信走NVLink还好。但如果是单卡推理,CPU和GPU之间的数据传输走PCIe。预填充阶段数据量大,PCIe带宽可能成为瓶颈。我建议预填充阶段尽量把数据一次性传到GPU,别分多次传。
1.7 小结
好了,这一章的内容就到这里。总结一下核心要点:
- 预填充阶段:计算密集,并行度高,瓶颈在算力。优化方向是提升矩阵乘法的效率。
- 解码阶段:访存密集,并行度低,瓶颈在显存带宽。优化方向是减少数据搬运量。
- 两个阶段的优化思路完全不同,不能用一套方案打天下。
下一章咱们会深入预填充阶段,讲讲怎么把矩阵乘法做到极致。到时候我会分享一些我在实际项目中用过的优化技巧,包括怎么调tile size、怎么用共享内存做数据复用。咱们到时候见。
公众号:蓝海资料掘金营,微信deep3321