资讯动态

LLM推理中的Prefill与Decode:两种计算形态的底层原理与性能瓶颈解析

发布时间:2026/10/26 4:34:31 来源:尧图企业网站定制
如果你调过大模型推理服务一定见过这样一个诡异的监控曲线同一个请求进来GPU利用率前半段猛地冲到80%以上后半段却像泄了气一样掉到20%不到。这不是GPU抽风也不是负载均衡出了问题而是同一个模型在推理时被强拆成了两种截然不同的计算形态——Prefill预填充和Decode解码。做过LLM部署的同学都知道这两个词几乎天天挂在嘴边但很多人对它们的理解停在“Prefill就是算首tokenDecode就是算后续token”这个层面。真正把它们拆开揉碎、看清楚各自的矩阵形状、访存模式、并行度瓶颈和优化方向之后你会发现很多工程决策都变得顺理成章为什么batch size拉不高、为什么TTFT那么慢、为什么Speculative Decoding能加速、为什么vLLM要搞Chunked Prefill……背后全是这两种计算形态的物理差异在起作用。这篇文章我不打算念PPT而是从一次真实推理请求的内部视角出发把Prefill和Decode的计算过程、显存足迹、性能瓶颈逐一讲透顺便把我压测时踩过的坑也摆出来。适合正在做模型服务优化、推理框架选型或者单纯想把Transformer推理底层搞清楚的读者。1. 从“首字延迟”说起为什么同一个模型跑起来像两个人1.1 一次普通API调用背后发生了什么假设你向某个对话模型发了一句话“请用三句话概括量子纠缠”模型返回结果可能需要3秒。这3秒里内部实际经历了两个完全不同的阶段第一阶段模型拿到整段输入包括系统提示词、历史消息、当前问题把所有token一次性塞进Transformer算出第一个新token的概率分布。这个阶段叫Prefill。它的特点是输入长、计算量大、但只做一次。第二阶段模型从第一个新token开始把刚才生成的token拼到输入末尾再算下一个token。如此往复直到吐出结束符。这个阶段叫Decode。它的特点是每次只生成一个token但需要循环几十次甚至几百次。我用“两个人”来形容这两种形态一个是拿到整份试卷后快速通读、汇总知识点、在草稿纸上打好框架的“速记员”另一个是拿到题目后一字一句往外蹦单词的“播音员”。速记员一次处理很多内容播音员一次只能挤出一个字但播音员的工作要重复很多遍。开篇说的监控曲线正是这两个人的工作量差异造成的Prefill阶段GPU在做大矩阵乘算力拉满Decode阶段每次只做很小的矩阵乘算力用不满反而被显存带宽卡住。硬件利用率自然是一高一低。1.2 Prefill和Decode是同一个模型的两个视角很多刚入行的同学会以为Prefill用的是一个模型Decode用的是另一个模型。实际上权重完全一样区别在于每一时刻“喂进去多少数据”和“要产出多少个位置的输出”。用一个简单的公式理解。假设输入序列长度为 (L)隐藏层维度为 (d)则Transformer某一层的计算核心是[ \text{Attention}(Q, K, V) \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V ]Prefill时我们同时对 (L) 个位置计算注意力Q是 (L \times d_k) 的矩阵K是 (L \times d_k)V是 (L \times d_v)。这是标准的矩阵乘可以充分利用GPU的张量核心。Decode时我们只关心最新一个位置。Q退化为 (1 \times d_k)K和V则要保留历史所有位置变成 (L_{\text{current}} \times d_k) 和 (L_{\text{current}} \times d_v)。于是注意力计算变成[ Q_{\text{new}} \times K^T \rightarrow 1 \times L_{\text{current}} ]这是一个“一行向量乘以一个矩阵”的操作矩阵乘的其中一个维度变成了1。GPU的Tensor Core擅长做大的方阵乘法对这种瘦矩阵乘的利用率很低。从计算图视角看Prefill是“一棵树”所有位置的输出可以并行计算Decode是“一条链”每一步都依赖上一步的输出天然串行。这就是“同一模型的两种计算形态”最底层的原因。2. 拆开Transformer的“两段式推理”Prefill如何通读全文Decode如何逐字落笔2.1 自回归生成的基本约定每步只吐一个token大语言模型本质上是一个自回归模型给定上下文预测下一个token的概率分布然后从分布中采样一个token把这个token接在后面再预测下一个。训练时我们可以用Teacher Forcing把一整段训练文本同时喂进去计算交叉熵但推理时没有“标准答案”只能一步一步来。这个“一步一步”决定了Decode必须是串行的。有人问为什么不能像BERT那样一次性把所有可能的位置都预测出来因为生成场景下后面位置的输入依赖于前面位置的输出生成顺序是强因果的。除非做非自回归生成比如用扩散模型做文本生成否则所有基于Transformer的自回归LLM都逃不开“逐token”的宿命。而Prefill其实是一次“超前计算”虽然还没开始生成但我们已经知道完整的输入token序列所以可以把这些token的中间状态全部算好。这一步相当于把整个输入上下文“编码”成KV Cache里的数据后续Decode每生成一个token只需要增量更新K和V不需要重新计算历史部分的注意力。2.2 Prefill阶段的计算图谱一次性算出所有位置的注意力Prefill阶段输入是 (L) 个token例如1024个经过Embedding后得到 ((L, d)) 的张量。对于每一层Transformer计算Q、K、V分别做三个线性变换矩阵形状为 ((L, d) \times (d, d_k))得到 ((L, d_k)) 的张量。计算注意力分数(QK^T)形状为 ((L, L))代表任意两个位置之间的注意力权重。计算输出(\text{softmax}(QK^T/\sqrt{d_k})V)形状为 ((L, d_v))。残差连接、LayerNorm、FFNFFN通常是两个大矩阵乘例如把 ((L, d)) 变成 ((L, 4d)) 再变回 ((L, d))。每一步都是“整个序列”一起做所以GPU可以把 (L) 当作batch维度来并行。这也是为什么Prefill阶段GPU利用率高矩阵乘的维度大计算密度高Tensor Core吃得满。需要注意Prefill阶段不需要掩码吗需要。因为自回归模型只能看到左侧的token所以注意力分数矩阵的上三角要mask掉。但在工程实现上我们不是在Prefill阶段“串行地”模拟每一步解码而是用一次前向传播计算出所有位置的“第下一个token”概率。比如输入“我喜欢吃苹果”模型会同时预测“喜”→“欢”→“吃”→“苹”→“果”的转移概率但这些预测只是用来生成KV Cache的副产品实际采样时我们只取最后一个位置的概率分布。也就是说Prefill的输出中的前几个位置的预测结果会被丢弃只有最后一个token被选中作为Decode的起点。这一点很多文章没讲清楚。2.3 Decode阶段的计算图谱新token如何“看着”旧token生成进入Decode后假设当前序列长度已经到 (L)原输入加已生成的token数。每次只需处理一个新的token位置计算量比Prefill小很多但必须串行执行N次N是生成的新token数。Decode的单步计算输入只有一个token的embedding形状 ((1, d))。三个线性变换得到 ((1, d_k)) 的Q、((1, d_k)) 的K、((1, d_v)) 的V。将当前K拼接到历史KV Cache的K矩阵上形状从 ((L-1, d_k)) 变为 ((L, d_k))V同理。计算注意力(QK^T)Q是 (1 \times d_k)K是 (L \times d_k)结果形状 (1 \times L)。softmax后与V(L \times d_v)相乘得到 (1 \times d_v) 的输出。经过FFN最后过一个Logits层维度 (1 \times |V|)(|V|) 是词表大小得到概率分布采样出一个新token。每一步的矩阵乘都发生在“短Q”和“长K”之间本质上是一个矩阵向量乘GEMVgeneral matrix-vector。GPU对GEMV的效率远不如GEMMgeneral matrix-matrix因为数据从显存搬到计算单元的耗时可能比计算本身还长。2.4 KV Cache把“已经读过”的中间结果存下来为什么需要KV Cache因为Decode的每一步新的token都要和历史上所有token算注意力。如果没有缓存每步都要重新算一遍前面所有token的K和V那代价是 (O(L^2)) 的重复计算谁也扛不住。有了KV Cache历史K和V提前存在显存里每步只需要计算新token的K和V然后追加进缓存即可。KV Cache的大小是 (2 \times n_{\text{layers}} \times n_{\text{heads}} \times d_{\text{head}} \times L \times \text{batch} \times \text{bytes})。以13B模型为例如果层数40、头数40、head维度128那么每个token需要缓存 (2 \times 40 \times 40 \times 128 409600) 个float16数值即0.8MB。一个请求生成2048个token就要1.6GB显存。如果并发16个请求KV Cache轻松超过25GB。这块开销甚至超过模型权重本身是推理显存规划的最大变量。这也是很多推理框架把KV Cache当“一等公民”来管理的原因预分配显存池、按序列动态分配、用PagedAttention避免碎片化。理解了KV Cache就理解了Prefill和Decode的显存足迹差异Prefill阶段KV Cache从0快速增长到 (L_{\text{input}}) 的长度Decode阶段每次只增加一个token的KV但总量随生成长度线性膨胀。3. 两张“性格完全相反”的计算账单计算密度、内存带宽与并行度3.1 Prefill是“计算密集型”的大矩阵乘Prefill的核心操作是 ( (L, d) ) 乘以 ( (d, 4d) ) 这类大块GEMM。这种操作的算术强度arithmetic intensity很高即每个数据元素被加载到寄存器后会被重复使用很多次。例如 (L1024, d4096)一次矩阵乘 ( (1024, 4096) \times (4096, 16384) ) 有大约 (2\times 1024\times 4096\times 16384 137) GFLOP而输入数据量只有 (1024\times 4096 4096\times 16384 \approx 71) MB。换句话说每读取1字节数据能做约2000次浮点运算。GPU恰好擅长这种高算术强度的任务大量线程并行加载数据块把数据塞给Tensor Core做乘累加。所以Prefill阶段能把GPU的算力用到90%以上。3.2 Decode是“访存密集型”的瘦矩阵乘Decode的单步计算中(QK^T) 是 (1 \times d_k) 乘以 (d_k \times L)。算术强度急剧下降。以 (d_k128, L1024) 为例这个矩阵乘只有 (2\times 128\times 1024 262K) 次浮点运算但要读取的K矩阵数据是 (128\times 1024\times 2\text{字节} 256KB)。算术强度约为1也就是读1字节数据只做1次运算。而GPU显存带宽通常在1-3TB/s算力却有几十到上百TFLOPS。Decode阶段因为算术强度太低完全受限于显存带宽需要读的数据量太大计算单元闲置等待数据。这解释了为什么Decode单步很快但GPU利用率很低也解释了为什么长上下文会让Decode更慢——K矩阵随 (L) 增长每步要读的数据更多即使FLOPs也增长但访存增长占主导。3.3 一张表看清两种形态的对比对比维度PrefillDecode输入长度整段上下文 (L)单个新token核心操作GEMM大矩阵乘GEMV矩阵向量乘计算类型计算密集型访存密集型GPU利用率高80%低往往20%-40%延迟特征一次算完时间随输入长度增加串行N步总时长随输出长度线性增长Parallelism可对序列维度并行序列维度不可并行只能并行batchKV Cache变化从0增长到输入长度每步追加1个token主要瓶颈算力显存带宽对batch的影响batch增大时利用率更充分batch增大时同样增加带宽压力这张表格基本上可以贴在工位旁。日常排查性能问题先判断当前阶段是Prefill还是Decode再对症下药。3.4 为什么Decode阶段GPU利用率上不去很多同学用nvidia-smi看GPU利用率只有20%以为模型没跑起来。实际上GPU利用率这个指标反映的是“流处理器有多忙”而Decode阶段大量时间花在“等数据从显存搬到寄存器”。我用一个生活类比算力像一个高速厨师显存带宽像上菜的传菜员。Prefill阶段厨师拿到一大筐菜一次可以做好几桌Decode阶段每步只做一道菜但传菜员每次只能端一个小盘子厨师大量时间在等下一道菜。要提升吞吐要么让传菜员一次端更多菜增大batch要么让厨师提前预做投机采样要么减少每道菜需要端过来的配料数量量化、稀疏化。增大batch之所以有效是因为多个请求的Decode可以合并成矩阵乘假设有B个请求同时处于Decode阶段那么Q矩阵变成 (B \times d_k)K矩阵还是 (L \times d_k)但每个请求独立算注意力实际可以组织成 (B \times d_k) 乘以 (d_k \times (B? 不对) ) 的shapeGPU显存会按B个请求分别加载K矩阵。带宽是共享的所以batch越大每个请求享受的有效带宽越低但整体吞吐增加因为每个请求的“加载中转”成本被摊薄了。这也是为什么推理框架宁愿把多个请求拼在一起处理也不要一会儿发一个。4. 工程优化既然形态不同就要“分开治疗”4.1 Continuous Batching不等满batch有token就送进去传统静态batching的做法是把同一批请求固定在一起必须等这批所有Decode都结束才释放资源再接收新请求。这会产生严重的空闲有的请求生成了200个token有的只生成20个就结束了GPU在等待慢请求的时候是闲置的。Continuous Batching连续批处理的做法是每一次迭代前把当前所有正在Decode的请求组成一个batch一起跑一步一步结束后有请求完成就立刻从batch里移除并把等待队列里的新请求加入。它的本质是把“batch的生命周期”从“一个完整请求”缩短到“一次Decode迭代”。由于Decode阶段每一步很小这种细粒度调度能让GPU始终满载。为什么这能大幅提升吞吐因为Decode阶段有大量“时间片”每个时间片只需算一小步Continuous Batching把这些小步拼凑成更大的矩阵提高了算术强度。Prefill虽然本身已经是大矩阵乘但如果请求很小也可能浪费所以连续批处理也会优先把凑够规模的Prefill请求一起处理。4.2 Chunked Prefill把大段Prefill切成小块穿插进DecodeContinuous Batching解决了调度问题但还有一个痛点如果某个请求的Prompt特别长比如几万token它的Prefill阶段会占用GPU很长时间。这段时间里其他已经在Decode的请求只能等待拖慢它们的TTFT和TPOT。而且长Prefill会让显存占用和计算量瞬间飙升造成“波峰”GPU利用率曲线像过山车。**Chunked Prefill分块预填充**的思路是把长Prompt的Prefill拆成多个小块每块几百或几千token穿插到Decode迭代之间执行。这样GPU在每个时间步内同时做一部分Prefill和一部分Decode避免长Prefill独占设备。代价是Prefill整体耗时变长因为矩阵乘被拆小了效率下降但对于在线服务而言让所有请求平滑地推进比单个请求的P99更有利。实现上需要把KV Cache的管理和注意力计算改成支持“块序列片段”。vLLM的chunked prefill、TensorRT-LLM的chunked context、TGI的prefill chunk都是干这个的。如果你自己撸推理框架这块复杂度不小但如果用现成框架往往只需开一个开关。4.3 Speculative Decoding用一个小草稿模型“预写答案”大模型做校验Decode串行带来的瓶颈让一个想法变得自然能不能让一个更小的、跑得更快的模型草稿模型先“猜”出接下来K个token再用大模型一次性校验这K个token如果草稿模型的猜测大部分正确那么大模型原本需要跑的K次Decode就变成了一次Prefill校验时输入K个token一次性算完加少量修正从而省下访存时间。这就是Speculative Decoding投机采样。它之所以能work是因为大模型生成的token序列通常有很强的局部规律小模型在多数位置能和大模型一致。假设草稿模型猜了4个token大模型一次校验后发现前3个正确、第4个错误那么这3个token就等于免费获得了只需要再从头生成第4个。从两种计算形态的角度看投机采样的本质是“把多次Decode合并成一次Prefill”Prefill的算术强度高、GPU利用率高合并后整体计算效率提升。如果草稿模型本身够快且内存占用低整个系统的token吞吐可以翻倍甚至更多。实际上很多框架已经内建了lookahead decoding、draft model等功能但这块调优水很深——比如草稿模型吞吐不够高反而拖累大模型或者草稿模型和base模型差距太大导致接受率低。4.4 其他实用技巧量化、并行策略与显存规划Decode阶段带宽受限降低数据搬运量最直接的办法是量化。从FP16降到INT8KV Cache和权重减半再降到INT4进一步降低带宽压力。需要注意的是量化对Prefill阶段影响更微妙FP8/INT8虽然能用Tensor Core加速但如果量化精度损失导致模型质量下降用户感知到的是生成结果变差不一定值得。我个人建议先量化KV Cache因为它不改变权重矩阵的数值精度对模型质量影响小又能显著降低Decode阶段的访存量。并行策略上张量并行Tensor ParallelismTP可以把权重切到多卡但每步Decode都要做all-reduce通信通信延迟有时会抵消算力提升。流水线并行Pipeline ParallelismPP把层拆到多卡但Decode阶段每步都要跨设备传递激活值同样有通信开销。对于单请求低延迟场景通常TP2/4足够对于高吞吐离线任务PPDP的组合更常见。显存规划上要记住KV Cache的大小和请求并发数、上下文长度强相关。实际调max model length时先算一下权重显存激活显存KV Cache显存的总和是否超过显存否则会发生OOM。后面第5节我会给一个具体的换算例子。5. 实测排坑评估指标、参数调节与一次真实压测记录5.1 TTFT/TPOT/ITL这些指标到底在测哪一段评估在线推理性能常看三个指标TTFTTime To First Token从请求发出到收到第一个token的时间。主要取决于Prefill时长以及网络、调度排队。如果输入很长TTFT会被Prefill拖长。TPOTTime Per Output Token每个输出token的平均时间。主要取决于Decode单步时间在batch中平均。计算公式是 (TPOT T_{\text{decode_step}} / \text{batch_size})严格说如果服务端跑一个batch单用户感受到的TPOT就是一步解码调度的墙钟时间。TPOT直接决定“打字机效果”的速度。ITLInter-Token Latency相邻两个token输出的间隔通常和TPOT相似但可能包含采样调度、网络抖动等因素。我见过不少同学只盯总耗时而忽略这三者结果根本说不清瓶颈在哪。一次压测中如果TTFT只有50ms但生成100个token花掉5秒说明Decode太慢如果TTFT要1.2秒而生成单个token只需要100ms问题在Prefill或调度。分开测定位才快。5.2 max batch size与KV Cache显存的换算假设我们部署一个7B模型权重用BF16约14GB。单卡A100 80GB。再看KV Cache预算如果max context长度设为4096每个序列的KV Cache大小为层数32 × 头数32 × head维度128 × 2KV× 4096个token × 2字节 32×32×128×2×4096×2 每个序列约2.15GB。再留出激活值、临时缓冲约10GB。那么80GB显存里权重14GB 激活/其他10GB 24GB剩下56GB供KV Cache约能缓存26个并发序列。也就是说max batch size上限大概在26。如果模型是13B、上下文8192这个数字会大幅缩小可能只剩6-8个并发。这就是为什么很多服务商默认并发不高。调max batch size时不是越大越好batch太大意味着每个序列的KV Cache都住在显存里同时Decode步骤的矩阵宽度增加带宽争抢变严重单用户TPOT可能上升。要在吞吐和延迟之间找平衡。我的一般做法是先用上面的公式粗算上限再压测找TTFT/TPOT的拐点。5.3 一次压力测试中遇到的“Decode速度崩了”问题排查有次我在某跨平台系统里用一个量化后的7B模型做实时问答服务压测时发现前50个并发响应正常从第51个开始TPOT从70ms暴涨到400msTTFT也开始抖动。一开始怀疑是单纯显存不足看nvidia-smi发现显存确实接近满但没OOM。仔细调日志发现KV Cache命中率正常但每个token的decode步里内存分配耗时显著增加。最后定位是KV Cache池碎片化严重前期不同的请求长度参差不齐显存分配器来回释放、分配把KV Cache池切得七零八落导致每次追加KV时要走很慢的交换路径。换成支持PagedAttention的推理后端把KV Cache按固定大小的分页管理之后碎片化问题消失了并发上限也直接提升到类似配置下的理论值。那次踩坑给我的教训是遇到Decode阶段性能崩先怀疑调度/缓存管理而不是一上来就加大显存或降并发。5.4 根据负载选择Prefer Prefill还是Prefer Decode在线服务的高峰和低谷负载不同两种计算形态的优先级也不同如果请求短、输出长比如写摘要、文章扩写那么Decode次数很多优化重心应放在提高Decode吞吐比如增大batch、量化KV Cache、使用投机采样。如果请求长、输出短比如阅读理解、文档问答那么TTFT和Prefill耗时是关键。优化方向可以是Chunked Prefill减少长请求阻塞、把长Prompt切成并行地Prefill比如用Sequence Parallel或者降低输入token数。我自己维护的一个模拟项目X就是典型的“长输入短输出”初版直接把vLLM的max context拉得很大结果每个请求的Prefill要算很久TTFT飙高。后来把“长文本分片检索只送关键片段”这个逻辑加进去输入长度从4000降到800TTFT从1.5秒降到350ms整体体验好了非常多。这说明很多时候与其死磕推理框架不如先从应用层减少Prefill的负担。6. 小工具与大方向从两种形态到技术选型6.1 用Profiling工具量化两种形态纸上谈兵不如实测。用PyTorch Profiler或NVIDIA Nsight Systems可以清楚看到每一步的时间分布。比如在一次7B模型推理里Prefill阶段的时间占比可能只有10%但计算量占80%以上Decode阶段时间占比90%但FLOPs很小。用ncu等工具还能看到内存吞吐和算力利用率。如果你自己包了一个推理服务可以在日志里记录两类指标Prefill阶段的耗时、显存增长Decode阶段的每步耗时。把它们画成曲线就能直观看到瓶颈在哪。6.2 框架选型时的思考看它如何调度这两种形态现在主流推理框架在Prefill和Decode的调度策略上差异很大有的把两种计算拆成两个独立调度队列有的则混合调度。我们在选型时除了看文档里的吞吐benchmark最好自己压一压“混合负载”比如长短请求交替、输入输出比例差异大看P95的TTFT和TPOT是否都能接受。如果不追求极致的吞吐用带Continuous Batching的框架已经够用如果要同时服务长上下文和高并发需要Chunked Prefill和PagedAttention都支持得好的后端。如果还有离线批量推理需求可以揉进Stable Diffusion式的大batch模式用更高吞吐但不关心延迟的策略跑。6.3 一点扩展Faas、边缘推理和未来方向把视野拉远一点Prefill和Decode的区分也不只在单卡推理中存在。分布式推到时Prefill的并行策略和Decode的通信模式不同甚至可以把两者拆到不同计算资源上用一批高算力卡专门做Prefill把KV Cache通过高速网络传给另一批高带宽、低算力的卡做Decode。这就是一些前沿方案的思路也印证了“两种形态可以分开优化”的观点。未来如果做长上下文推理KV Cache越来越大Decode的带宽压力会更突出可能会出现更激进的缓存压缩、分层注意力、或者基于记忆的架构。但无论模型怎么变只要还是自回归生成Prefill和Decode这对“双生子”就依然存在。最后分享一点个人体会我第一次调优模型服务时总想着怎么“提升GPU利用率”后来发现这个目标本身就有点naive——Decode阶段GPU利用率低是物理规律硬拉利用率反而可能伤到吞吐。正确的心态是“接受两种形态的不同性格然后分别顺着它们的脾气来”Prefill要尽量做大的矩阵乘所以把输入批量、上下文长度利用率做足Decode要把每次访存的“有效载荷”做大所以用更大batch、更少字节数、更聪明的投机和采样策略。如果你正准备做模型服务优化建议先在自己的环境里把Prefill和Decode的耗时、显存数据打出来再对照这篇文章的思路逐项调整。调试过程中你会发现许多所谓的“随机波动”其实都能用这两个阶段的属性解释得清清楚楚。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑