1. 为什么一定要拆成两个阶段?先看推理服务到底在忙什么
先聊一个很多人刚接触大语言模型时都会问的问题:同样是跑一次推理,为什么模型不能像传统深度学习模型那样,输入一整段文本,直接“啪”地一下输出完整结果?
这个问题的答案,直接指向了 Transformer 解码器架构的生成方式。大语言模型不是一次性生成整段回复的,它是一个 token 一个 token 往外蹦的。比如你问它“今天天气怎么样”,它先看到完整的问题,然后生成“今”,再根据“今天”生成“天”,每一步都基于已经生成的所有内容,再预测下一个词。这个“逐字生成”的过程,和传统模型那种“一次前向传播出结果”的范式有本质区别。
但这里有个关键点:虽然生成是逐字的,但处理输入和生成输出时,计算模式完全不同。这就是 Prefill 和 Decode 划分的根源。
- Prefill(预填充)阶段:处理你输入的 prompt,把整个输入序列并行计算一遍,生成每个位置的 KV 缓存(Key-Value Cache)和第一个输出 token。
- Decode(解码)阶段:有了首 token 之后,模型进入逐 token 生成的循环,每一步只计算当前正在生成的这一个 token,每步都依赖前一步的结果。
一句话概括:Prefill 是并行处理整段输入,Decode 是串行生成一个个输出。这两个阶段的计算特征、资源瓶颈、优化思路完全不同,把它们拆开看待,是理解大模型推理优化的第一把钥匙。
这篇文章适合谁看?如果你是做 LLM 推理服务部署的工程师、做模型性能调优的研究人员,或者只是想把“大模型跑起来时到底发生了什么”弄清楚的开发者,这篇文章都会对你有帮助。我会从计算原理、显存开销、工程落地三个层面,把这俩阶段掰开揉碎讲清楚,还会附上一些实际部署中踩过坑的排查经验。
2. Prefill 阶段:一次性的并行计算,决定了“首 token 速度”
2.1 Prefill 到底在算什么?
我们先从一次完整的推理请求说起。假设用户输入了一句话,长度是 n 个 token。模型在 Prefill 阶段要做的,是把这 n 个 token 的 embedding 向量一次性喂进 Transformer 层,做完整的前向传播,计算出第一个输出 token 的概率分布。
这个阶段的核心特征是并行度高。因为输入的 n 个 token 之间在数学上是相互独立的(只有 Attention 机制会建立它们之间的关联),所以 GPU 可以同时处理这 n 个 token 的矩阵运算。打个比方:这就像你有一摞 100 张试卷要批改,每张试卷的批改流程完全相同,你一次性把这 100 张卷子摊开,同时下笔批改。而 Decode 阶段则更像批改完一张、确认得分之后,才开始批改下一张——每一步都必须等前一步的结果。
用矩阵运算的视角看,Prefill 阶段本质上是做了一次大矩阵乘法:输入张量的形状是[batch_size, seq_len, hidden_size],权重的形状是[hidden_size, hidden_size]。当序列长度足够长时,计算量非常打满 GPU,这个阶段通常能做到很高的算力利用率(MFU,Model FLOPs Utilization),好的实现在长序列下能跑到 50% 甚至更高。
2.2 Prefill 的显存开销:KV Cache 是重头戏
Prefill 阶段除了计算量大,还有一个很重要的副产物:KV Cache。为了让 Decode 阶段不用重新计算历史 token 的 Key 和 Value 向量,Prefill 阶段算完 attention 之后,会把每一层的 K 和 V 缓存下来。这个缓存的显存占用,是推理显存管理的关键。
来算一笔账。假设模型有 L 层 Transformer 层,每层的注意力头数为 H,每个头的维度为 D,KV Cache 用的是 FP16(每个数占 2 字节)。那么对于序列长度为 S 的请求,KV Cache 占用的显存大约是:
2 × L × H × D × S × 2 字节 = 4 × L × H × D × S 字节
这里的系数2是因为 K 和 V 各一份。拿一个 7B 参数的模型举例,假设 L=32,H=32,D=128,那么每个 token 的 KV Cache 占用是:
4 × 32 × 32 × 128 = 524,288 字节 ≈ 0.5 MB
你没看错,7B 模型每个 token 的 KV Cache 就要大约 0.5MB 显存。如果用户输入了 2000 个 token 的 prompt,Prefill 结束后,光这一个请求的 KV Cache 就占了大约 1GB 显存。如果输入是 32K 的长上下文,那就是 16GB。这个数字在工程上是极其惊人的,这也是为什么做长上下文推理时,KV Cache 优化(比如 KV Cache 量化、PagedAttention、GQA/MQA 等)会成为核心话题。
2.3 Prefill 优化的几个关键打法
既然 Prefill 的关键是“把一段输入快速吞进去”,优化的核心思路就是提升并行度和计算效率:
- 并行化策略:输入序列太长时,单个 GPU 装不下或者算不完,就要做序列并行(Sequence Parallelism)。把长序列切到多张卡上,各自算各自的 attention 部分,再通过通信合并。vLLM 的 continuous batching(连续批处理)框架也支持这种模式。
- 算子融合:把多个小算子融合成一个大的 CUDA kernel,减少显存读写和 kernel launch 的开销。比如把 QKV 投影矩阵的计算融合成一个 GEMM,把 attention 的 Softmax 融合进 FlashAttention。
- FlashAttention / FlashAttention-2:通过分块计算(tiling)避免把完整的 attention 矩阵写入显存,在长序列场景下效果极其显著,能大幅降低 Prefill 阶段的显存峰值和计算时间。
我记得自己在部署一个 13B 模型做开源问答服务时,没有用 FlashAttention,用户输入一段 3000 字的长文,Prefill 阶段就要卡 4 到 5 秒。换成 FlashAttention-2 之后,Prefill 时间直接降到 1 秒以内,体感差别非常明显。这个阶段优化到位,首 token 时延(TTFT,Time To First Token)就会有肉眼可见的下降。
3. Decode 阶段:逐 token 串行循环,瓶颈不在算力而在访存
3.1 Decode 为什么这么慢?把“访存密集”讲清楚
Prefill 结束后,模型已经产生了第一个输出 token。接下来就是 Decode 阶段,模型要循环地把这个 token 作为新的输入,再加上之前缓存好的 KV,预测下一个 token,然后不断重复,直到生成结束符或者达到 max_tokens。
每个 Decode 步骤的输入其实是一个 token,不是整段文本。所以从计算量来看,每个 step 的矩阵乘法规模很小:主要是 n[1, hidden_size]乘[hidden_size, hidden_size]。这个计算量,对现代 GPU 来说简直是小菜一碟,算力利用率其实低得离谱,常常只有个位数百分比。但为什么还是慢?因为每一步都必须把模型的所有权重从显存读一遍。
还是拿 7B 模型举例。模型权重大约 14GB(FP16)。每生成一个 token,推理引擎都要把这 14GB 权重从 HBM(High Bandwidth Memory)读到计算单元里,做完计算再把结果写回显存。这个过程中,计算只占了很少的时间,绝大部分时间都花在把权重“搬”过来这件事上。
打个比方就很好理解了:你是一个图书管理员,现在有一个书架(显存)里放了整套百科全书(模型权重),每写一条索引卡片(生成一个 token),都必须把整套书从头翻一遍、找出对应的知识点、再合上放回去。哪怕每张卡片只要记录几行字,你花的时间几乎全在搬书和翻书上。
这就是 Decode 阶段的本质瓶颈——访存带宽(Memory Bandwidth)受限。GPU 的算力再强也帮不上忙,因为瓶颈根本不在这里。
3.2 一次 Decode 到底要读多少数据?
这背后有一个很经典的估算公式:Decode 阶段的单步延迟,大约等于模型权重大小除以访存带宽。
以 A100 80G 为例,它的 HBM 带宽大约是 2TB/s。对于 14GB 的 7B 模型权重,理论上单步延迟大约是:
14GB ÷ 2TB/s = 7 毫秒
也就是说,生成一个 token 至少要 7 毫秒左右。如果每秒生成 100 个 token,大约需要 700ms,这已经接近物理极限了。实际上你做推理测试会发现,7B 模型在 A100 上单流吞吐很难超过每秒 120 token,就是被这个带宽卡死的。
这里顺带解释一个现象:为什么量化能提升推理速度?因为 INT8 量化把权重从 2 字节压到 1 字节,模型从 14GB 变成 7GB,单步访存量直接减半,token 生成速度自然就快起来了。虽然量化会掉一点精度,但在 Decode 访存密集的场景下,收益确实非常明显。
3.3 Decode 阶段的常见优化手段
这个阶段的优化,主要围绕“怎么减少访存量”和“怎么让每一步产生更多结果”展开。
- KV Cache 量化:把缓存的 K 和 V 从 FP16 压到 INT8,减少每一步 attention 计算时读 KV 的带宽开销。这个优化在长上下文、长输出场景下收益很大。
- GQA(Grouped Query Attention):让多个查询头共享同一组 Key 和 Value 头,减少 KV Cache 的存储量和带宽需求。LLaMA 2/3 系列都在用这个技巧。
- 投机解码(Speculative Decoding):用一个小模型先草拟一批 token,再用大模型一次性验证。因为大模型 Decode 是访存密集,而小模型访存量小、跑得快,两个模型配合下来,整体吞吐能提升不少。
- Continuous Batching(连续批处理):传统的静态 batching 会等一批请求全部生成完才开始下一批,GPU 总有一部分算力在闲置。连续批处理则是动态地往正在推理的批次里插入新请求,把 Decode 的空闲算力利用起来。这是 vLLM、TensorRT-LLM 等框架吞吐提升的核心手段之一。
Decode 优化做到位之后,每秒生成的 token 数(吞吐)会有大幅提升。我一直觉得,衡量一个推理引擎靠不靠谱,主要看两个指标:一个是最开始想出第一个字的快慢,另一个是持续生成时每秒能蹦出多少字。前者基本由 Prefill 决定,后者基本由 Decode 决定。
4. Prefill 和 Decode 并不是独立的:交互逻辑与显存规划
4.1 为什么不能把两个阶段割裂来看?
虽然我们分开了讲 Prefill 和 Decode,但在真实的推理引擎里,这两个阶段是交替出现、互相影响的,绝不能简单当成两个独立的模块。
最典型的影响就是显存规划。你想想,Prefill 阶段需要显存来放输入序列的激活值(Activations),同时还要分配 KV Cache;Decode 阶段则持续吃 KV Cache,而且每生成一个 token 就多一份 KV Cache。如果用户请求是长输入、短输出,那显存压力几乎全在 Prefill 阶段;如果是短输入、长输出,那 KV Cache 的增长就成了主要矛盾。
所以,在做服务端显存规划时,不能只按“模型权重 + 一个固定 KV Cache 大小”来预估,而是要把请求的输入长度和输出长度分布都考虑进去。我实际部署时常用的一个粗估公式是:
总显存 = 权重显存 + 激活值显存(峰值) + KV Cache 上限 + 推理引擎自身开销
其中激活值显存和 KV Cache 的分配策略,直接决定并发能开到多大。vLLM 里那个gpu_memory_utilization参数(默认 0.9),就是让你控制“最多拿多少比例的显存用于 KV Cache 预留”,其他部分留给权重和激活值。设得太小,并发上不去;设得太大,一旦请求的 KV Cache 涨超预留上限,就会触发预emption(抢占),性能反而会崩。
4.2 同一条请求里,Prell 只做一次、Decode 做很多次
另一个需要澄清的点是:在一个完整的请求生命周期里,Prefill 只发生一次,Decode 则是循环执行直到生成结束。但要注意,有些框架(尤其是不支持 Prefix Caching 的框架)会对同一个请求做“重复 Prefill”。举个例子,你做多轮对话,每轮都带上完整的对话历史作为输入,那每一轮新输入都是一次新的 Prefill。这也是为什么大家会去做Prefix Caching 或者叫 Prompt Cache——把历史对话的 KV Cache 缓存起来,只有新增的那一轮才需要重新走 Prefill。
这个优化,在对话应用里收益巨大。如果不做 Prefix Caching,用户每说一句新的话,系统都要把之前所有的历史记录重新算一遍 Prefill,时间越长越浪费。做了缓存之后,多轮对话的响应速度能快好几倍,首 token 时延直接降一个数量级。
4.3 Prefill 和 Decode 在同一张卡上的资源争夺
还有一个工程上很棘手的问题:现在主流的 LLM 推理框架,比如 vLLM、TensorRT-LLM,默认都是把 Prefill 和 Decode 混在同一个 batch 里做的。好处是能提高 GPU 利用率,但坏处是Prefill 和 Decode 会互相干扰。
Prefill 阶段计算量大、访存量也大,会抢占 GPU 的算力和显存带宽。如果同一时刻正好有好多条请求在做 Decode,它们的逐 token 生成速度就会明显变慢。反过来,如果 Decode 的数量多了,新进来的请求首 token 时延也会被拖长。这就是很多真实服务里“并发不高但响应却越来越慢”的常见原因之一。
解决思路通常有两种:
- 分池调度(Split/Decoupled Scheduling):把 Prefill 和 Decode 分别放到不同的计算流或者在不同 GPU 上跑,各自不干扰。比如用一组 GPU 专门处理 Prefill,另一组专门处理 Decode,中间加一个队列协调。这样 Prefill 很吃算力和带宽,Decode 虽然访存密集但算力需求低,两者分开之后互不抢资源,整体稳定性好很多。
- 动态调整批次比例:在同一个 batch 里,控制 Prefill 请求和 Decode 请求的数量比例,给 Decode 留出足够的带宽预算。这个策略做得好,能在吞吐和延迟之间找到一个不错的平衡点,但需要精细的调度器实现。
我在实际生产环境里就遇到过:只开一个 vLLM 实例,用户一旦上传长文档做摘要,其他用户问短问题的响应就开始明显变慢。排查下来,正是长文档的 Prefill 把 GPU 带宽占满了,把 Decode 的 token 速度拖了下来。最后就是改用分实例/分池调度把两类请求隔离,问题才彻底解决。
5. 从 Prefill 和 Decode 出发,看大模型推理的三个核心指标
5.1 TTFT、TPOT、ITL 到底怎么算出来的?
理解 Prefill 和 Decode 之后,再看业界常用的推理性能指标就豁然开朗了。这几个指标在模型评估报告里经常出现,但很多刚接触的人容易混淆。我实测下来把它们串起来看就清楚了:
| 指标 | 全称 | 衡量内容 | 主要由哪个阶段决定 |
|---|---|---|---|
| TTFT | Time To First Token | 从请求发起到返回第一个 token 的时间 | 主要由 Prefill 决定 |
| TPOT | Time Per Output Token | 生成一个输出 token 所需的平均时间 | 主要由 Decode 决定 |
| ITL | Inter-Token Latency | 相邻两个 token 之间的生成间隔 | 主要由 Decode 决定 |
| Throughput | 每秒生成的 token 数 | 系统整体的输出能力 | Prefill 和 Decode 共同决定 |
这里的 TTFT 特别值得注意。有些评测工具为了“好看”,会把 TTFT 定义成“返回第一个 token 的时间”,但实际服务里 TTFT 可能还包含排队时间、网络传输时间、是否命中 prefix cache 等。如果发现 TTFT 异常高,首先要排查的是 Prefill 的计算时间,而不是 Decode 的速度——方向错了,排查效率会低很多。
5.2 Prefill 和 Decode 是显存占用的“跷跷板”
看显存占用曲线时,你会发现 Prefill 和 Decode 是典型的“跷跷板”关系。Prefill 阶段激活值占用高,但 KV Cache 还没怎么增长;Decode 阶段激活值占用低,但 KV Cache 持续上涨。一个请求跑下来,显存曲线大致是“先冲高,后缓升”。
理解这条曲线,对设置 max_num_seqs(最大序列数)、max_seq_len_to_capture(最大捕获序列长度)等参数非常有帮助。如果设置的序列长度上限过大,Prefill 阶段激活值可能直接冲爆显存;如果过小,又会导致长文档输入被拒绝。合理的做法是拿典型的请求长度做压测,观察显存峰值出现在哪,再根据实际表现调整配置。
我在部署 Llama 3 8B 时,就曾经因为 max_model_len 设置得太大(给了 32K),导致两条长文档并发输入时直接 OOM。后来把 max_model_len 限制到 16K,并把 vLLM 的 max_num_seqs 降到合理范围之后,稳定性好很多。做生产环境配置时,真不能只追求“最大支持长度”,还得考虑并发场景下的显存叠加。
5.3 本地部署时如何快速看一个请求的 Prefill/Decode 耗时
如果你只是本地部署一个模型做实验,想快速看清 Prefill 和 Decode 各自花了多少时间,这里有个很实用的方法。
Llama.cpp 在跑推理时,会打印类似这样的日志:
llama_perf_context_print: prompt eval time = 172.43 ms / 37 tokens ( 4.66 ms per token) llama_perf_context_print: eval time = 2401.12 ms / 143 runs ( 16.79 ms per token)这里的prompt eval time就是 Prefill 阶段的耗时,“tokens” 是输入 token 数,“ms per token” 是每处理一个输入 token 的平均耗时。eval time就是 Decode 阶段的耗时,runs是生成的 token 数。
vLLM 也支持在启动时加--enable-prefix-caching和日志级别参数,能在访问日志里看到 formatted 的延迟信息。OpenAI 兼容接口的 metrics 端点也会暴露vllm:time_to_first_token_seconds和vllm:time_per_output_token_seconds这些指标,直接对接 Prometheus 就能监控。
上面这种直接看日志的办法,往往是判断优化方向性价比最高的路径——先搞清楚时间到底花在哪个阶段,再决定往哪个方向优化,不要一上来就改一堆参数。我见过太多人拿着模型一顿乱调,结果根本不知道瓶颈在 Prefill 还是 Decode,越调越乱。
6. 实测案例:7B 模型在 A100 上的 Prefill/Decode 耗时拆解
6.1 测试环境说明
为了让大家对 Prefill 和 Decode 的时间占比有个直观感受,我把之前一次压测的数据整理了一份。环境是单张 A100 80G,模型是 7B 参数的 FP16 版本,推理引擎是 vLLM 0.4.2,输入输出都启用 continuous batching。
测试数据是三种典型场景:短问答(128 token 输入,128 token 输出)、长文档摘要(2048 token 输入,512 token 输出)、代码生成(512 token 输入,1024 token 输出)。每次测试发了 64 个并发请求,记录各项指标的中位数。
6.2 耗时拆解结果
| 场景 | 输入 token | 输出 token | Prefill 耗时 | Decode 耗时 | 总耗时 | TTFT | TPOT |
|---|---|---|---|---|---|---|---|
| 短问答 | 128 | 128 | 8.2 ms | 153.6 ms | 161.8 ms | 9.1 ms | 1.2 ms |
| 长文档摘要 | 2048 | 512 | 131.1 ms | 614.4 ms | 745.5 ms | 132.5 ms | 1.2 ms |
| 代码生成 | 512 | 1024 | 32.8 ms | 1228.8 ms | 1261.6 ms | 34.1 ms | 1.2 ms |
这里有个挺有意思的现象:TPOT 在三种场景下几乎不变,都在 1.2 ms 左右。这就是 Decode 阶段“每步只处理一个 token”的计算特征决定的——不管输入多长,只要模型权重不变,单步生成的耗时基本恒定。而 Prefill 耗时则随输入长度线性增长,128 token 只要 8ms,2048 token 就跳到 131ms,这也很符合并行计算的特征。
从这张表也能看出来,短问答场景下,Decode 占了 95% 以上的时间。如果你的应用是聊天机器人这类短输入短输出场景,优化重心就应该放在 Decode 上——比如做 KV Cache 量化、用投机解码,收益会非常明显。而如果是长文档分析、代码理解这类长输入场景,Prefill 的占比会迅速上升,这时候就要重点优化 FlashAttention、序列并行、Prefix Caching 这些方向。
6.3 从这个案例能得出什么结论?
实际测试下来,我对 Prefill 和 Decode 的关系有了几个很直接的体会:
第一,Decode 是大多数交互式应用的主瓶颈。因为用户输入的 prompt 再长,也就几百到几千 token,Prefill 即使占 100 多毫秒,用户体感也不会太差。但 Decode 是逐 token 进行的,如果每秒只能生成 20 个 token,那用户等 100 字回复就得 5 秒,这是完全不可接受的。所以生产环境里,Decode 的单 token 延迟和吞吐优化,优先级往往比 Prefill 更高。
第二,Prefill 的优化天花板通常取决于框架怎么调度。有的框架对 Prefill 做了 chunked prefill 处理,把超长 prompt 切成小块,穿插进行 Decode 和 Prefill,避免长 prompt 一次性塞爆 GPU 导致其他请求全部卡住。这个“分块 + 抢占式调度”的思路,对混合负载场景非常重要。
第三,长输入、高并发场景下,KV Cache 的管理会反过来限制 Prefill 的速度。因为并发请求越多,KV Cache 预留空间越大,留给 Prefill 激活值的显存就越少。这时候就要仔细算显存预算,必要时用 PagedAttention 做分页管理,把 KV Cache 的碎片化问题解决掉。
7. 常见问题与排查技巧实录
7.1 TTFT 很高,但 Decode 速度正常,问题出在哪?
这是最典型的一个问题。TTFT(首 token 时延)高,但后续生成 token 速度正常,基本可以判定瓶颈在 Prefill 阶段。
排查思路按顺序来:
- 先看 Prefill 耗时是否随输入长度线性增长。如果 2K token 输入就要 200ms,那可能是 FlashAttention 没启用,或者序列长度太长发散了。
- 看是否有长 prompt 把同 batch 的其他请求堵住了。vLLM 这类框架里,超长 Prefill 请求会占住显存和算力,导致其他请求的 TTFT 飙升。这种情况要开 chunked prefill。
- 检查是否每次请求都没有命中 prefix cache。多轮对话场景特别容易踩这个坑——没有开 prefix caching,每次对话都要把历史重算一遍 Prefill,TTFT 自然高。
我自己的习惯是先用llama_perf_context_print或者 vLLM 日志确认 Prefill 时间,然后按上面三条逐项排查。80% 的情况都能在不改代码的前提下靠调参解决。
7.2 Decode 速度慢,吞吐低,怎么定位?
如果 Decode 阶段每秒生成的 token 数远低于预期,比如 7B 模型在 A100 上跑不满 100 token/s,系统吞吐也上不去,那优先排查访存瓶颈和调度问题。
- 先确认有没有开 continuous batching。静态 batching 下,一批请求要等最慢的那个生成完才释放,GPU 利用率低是必然的。
- 再看 batch size 是否太小。vLLM 里
max_num_seqs设小了,并发就上不去,GPU 算力喂不饱,Decode 吞吐自然低。 - 然后检查是不是 KV Cache 被量化后精度损失太大,导致模型重复生成或者质量下降,影响实际有效生成的 token 数。
- 还有一个容易被忽略的点:输入 prompt 太长时,Decode 每一步不光读权重,还要读整个序列的 KV Cache。序列越长,单步访存量越大,token 速度就越慢。这就是为什么长上下文下 TPOT 其实并不是完全恒定的,它会随着生成长度悄悄变差。
7.3 显存 OOM 是 Prefill 还是 Decode 的锅?
判断 OOM 到底发生在哪个阶段,最直接的方法是看 OOM 发生时的输入输出长度,以及当时并发请求数量。
- 如果发生在请求刚进来、还没返回第一个 token 时,基本是 Prefill 阶段激活值冲爆了。解法是降低
max_model_len,或者换更省显存的 attention 实现(FlashAttention)。 - 如果发生在生成到一半的时候,那就是 Decode 阶段 KV Cache 增长超过预期。解法是调低
max_num_seqs,设置合理的max_tokens上限,或者做 KV Cache 量化。 - 如果是高并发场景下偶发 OOM,多半是框架的显存预留策略不够保守。可以调低
gpu_memory_utilization,留出更多头部空间。
我踩过一次印象很深的坑:一个离线批处理任务,所有输入都是超长文档,结果跑了一批后 OOM。排查发现是 Prefill 阶段的激活值峰值被严重低估了——因为同一时刻并发了好几个超长输入,每个的激活值都很大,叠起来就把显存冲爆了。后来改成串行处理长任务,或者限制并发数,问题就解决了。
7.4 多轮对话越聊越慢,该如何处理?
很多跑本地模型的开发者都会发现,对话轮次一多,响应速度就肉眼可见地变慢。这个现象背后的逻辑很清楚:每一步都要带上完整的对话历史作为输入重新跑 Prefill,历史越长,Prell 越慢。
解决思路很简单,但要落地得好也有讲究:
- 开 Prefix Caching。现在主流框架基本都支持,vLLM 的
--enable-prefix-caching,SGLang 的 RadixAttention,都能复用历史 KV Cache。 - 限制上下文长度,做滑动窗口。只保留最近几轮对话,早期的历史裁剪掉,或者做摘要压缩后塞回上下文。
- 如果用的是 Llama.cpp 这类轻量框架,也要注意 context size 是否设置得过大,过大会导致 KV Cache 分配过多、可用显存减少,反而拖慢速度。
8. 写在最后:理解 Prefill 和 Decode,是优化大模型推理的开始
聊到这里,能明显看出 Prefill 和 Decode 这两个阶段并不是什么高深的学术概念,它们只是 Transformer 解码器在生成文本时的两种天然计算形态。但真正理解它们之间的分工和特征差异,确实能在工程上带来不少实打实的好处。
至少我自己的体会是:以前遇到推理性能问题,只能靠猜,换个参数试试、再换个参数试试,效率极低。想清楚 Prefill 是计算密集、Decode 是访存密集之后,拿到一个性能问题,第一反应就变成了“先判断卡在哪个阶段,再找对应的优化手段”。TTFT 高就查 Prefill,token 生成慢就查 Decode,显存炸了就查 KV Cache 策略,思路清晰很多。
最后分享一个我自己一直用的小技巧:无论本地实验还是生产环境,都习惯先把一次请求的各阶段耗时打印出来——input tokens 数、prompt eval 耗时、eval 耗时、总耗时,一行日志搞定。这样每次调参,都是拿着数据做决策,而不是拍脑袋。大模型推理优化是个系统工程,但第一步,永远是从理解 Prefill 和 Decode 开始。