如果你调过大模型推理服务,一定见过这样一个诡异的监控曲线:同一个请求进来,GPU利用率前半段猛地冲到80%以上,后半段却像泄了气一样掉到20%不到。这不是GPU抽风,也不是负载均衡出了问题,而是同一个模型在推理时被强拆成了两种截然不同的计算形态——Prefill(预填充)和Decode(解码)。
做过LLM部署的同学都知道,这两个词几乎天天挂在嘴边,但很多人对它们的理解停在“Prefill就是算首token,Decode就是算后续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、FFN:FFN通常是两个大矩阵乘,例如把 ((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”之间,本质上是一个矩阵向量乘(GEMV,general matrix-vector)。GPU对GEMV的效率远不如GEMM(general 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)很高,即每个数据元素被加载到寄存器后会被重复使用很多次。例如 (L=1024, d=4096),一次矩阵乘 ( (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_k=128, L'=1024) 为例,这个矩阵乘只有 (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 一张表看清两种形态的对比
| 对比维度 | Prefill | Decode |
|---|---|---|
| 输入长度 | 整段上下文 (L) | 单个新token |
| 核心操作 | GEMM(大矩阵乘) | GEMV(矩阵向量乘) |
| 计算类型 | 计算密集型 | 访存密集型 |
| GPU利用率 | 高(80%+) | 低(往往20%-40%) |
| 延迟特征 | 一次算完,时间随输入长度增加 | 串行N步,总时长随输出长度线性增长 |
| Parallelism | 可对序列维度并行 | 序列维度不可并行,只能并行batch |
| KV 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? 不对) ) 的shape,GPU显存会按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切成小块,穿插进Decode
Continuous 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降到INT8,KV Cache和权重减半;再降到INT4,进一步降低带宽压力。需要注意的是,量化对Prefill阶段影响更微妙:FP8/INT8虽然能用Tensor Core加速,但如果量化精度损失导致模型质量下降,用户感知到的是生成结果变差,不一定值得。我个人建议先量化KV Cache,因为它不改变权重矩阵的数值精度,对模型质量影响小,又能显著降低Decode阶段的访存量。
并行策略上,张量并行(Tensor Parallelism,TP)可以把权重切到多卡,但每步Decode都要做all-reduce通信,通信延迟有时会抵消算力提升。流水线并行(Pipeline Parallelism,PP)把层拆到多卡,但Decode阶段每步都要跨设备传递激活值,同样有通信开销。对于单请求低延迟场景,通常TP=2/4足够;对于高吞吐离线任务,PP+DP的组合更常见。
显存规划上,要记住KV Cache的大小和请求并发数、上下文长度强相关。实际调max model length时,先算一下权重显存+激活显存+KV Cache显存的总和是否超过显存,否则会发生OOM。后面第5节我会给一个具体的换算例子。
5. 实测排坑:评估指标、参数调节与一次真实压测记录
5.1 TTFT/TPOT/ITL这些指标到底在测哪一段
评估在线推理性能,常看三个指标:
- TTFT(Time To First Token):从请求发出到收到第一个token的时间。主要取决于Prefill时长(以及网络、调度排队)。如果输入很长,TTFT会被Prefill拖长。
- TPOT(Time Per Output Token):每个输出token的平均时间。主要取决于Decode单步时间(在batch中平均)。计算公式是 (TPOT = T_{\text{decode_step}} / \text{batch_size})?严格说,如果服务端跑一个batch,单用户感受到的TPOT就是一步解码+调度的墙钟时间。TPOT直接决定“打字机效果”的速度。
- ITL(Inter-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 × 2(K+V)× 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暴涨到400ms,TTFT也开始抖动。
一开始怀疑是单纯显存不足,看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降到800,TTFT从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的耗时、显存数据打出来,再对照这篇文章的思路逐项调整。调试过程中你会发现许多所谓的“随机波动”其实都能用这两个阶段的属性解释得清清楚楚。