当我们向大语言模型发送一句话时,看起来只是:
输入问题 → 模型回答
但在模型内部,这个过程其实可以明显分成两个阶段:
Prefill
和
Decode
这两个阶段的工作方式、计算特征、性能瓶颈都不一样。
如果继续往 LLM 推理、vLLM、推理加速、GPU 部署这些方向学习,Prefill 和 Decode 基本是绕不开的两个概念。
简单来说:
Prefill 负责读懂输入。
Decode 负责一个 Token 一个 Token 地生成输出。
但真正理解它们以后,会发现很多常见问题都能顺着这两个概念解释清楚。
比如:
- 为什么 Prompt 越长,第一个 Token 出现得越慢?
- 为什么模型开始回答以后,后面的 Token 可以持续生成?
- 为什么 GPU 算力很强,但生成速度不一定特别快?
- 为什么 KV Cache 对推理这么重要?
- 为什么大模型服务要分别优化 Prefill 和 Decode?
一次 LLM 请求从哪里开始?
假设我们向模型发送:
请解释一下 Transformer 的工作原理。
这句话不会直接以文字形式进入模型。
第一步仍然是 Tokenization。
Tokenizer 会把文本拆成一系列 Token,例如:
请
解释
一下
Transformer
的
工作
原理
实际 Token 划分会根据具体模型的词表而不同。
这些 Token 会进一步转换成 Token ID,再通过 Embedding 变成向量。
到这里,模型才真正开始处理输入。
接下来进入第一个阶段:
Prefill。
什么是 Prefill?
Prefill 有时也会被叫做:
Prompt Processing
它的任务就是:
一次性处理用户已经提供的全部输入 Token。
假设 Prompt 有 2000 个 Token。
那么 Prefill 阶段会把这 2000 个 Token 一起送进 Transformer。
模型会计算它们经过每一层后的隐藏状态,同时生成每一层 Attention 所需要的:
Key 和 Value。
这些 K 和 V 会被保存到 KV Cache 中。
所以 Prefill 可以理解为:
模型先把已经存在的上下文完整读一遍,并建立后续生成需要使用的缓存。
这一阶段结束后,模型已经拥有了关于当前 Prompt 的内部表示。
接下来才真正开始生成第一个 Token。
Prefill 不是在生成答案
这里有一个很容易混淆的地方。
Prefill 虽然运行了完整的 Transformer,但它主要不是在生成很多新 Token。
它是在处理:
已经存在的 Token。
例如输入有:
1000 Token
Prefill 的主要工作是处理这 1000 个输入 Token。
当这些 Token 全部处理完成以后,模型才使用最后位置的隐藏状态预测第一个输出 Token。
所以我们点击“发送”以后,模型有时候会短暂停顿。
这段时间很大一部分就是在执行 Prefill。
输入越长,这个阶段通常也会越重。
TTFT:为什么第一个 Token 最慢?
LLM 推理中经常会看到一个指标:
TTFT
全称:
Time To First Token
也就是:
从请求发送,到模型产生第一个 Token,需要多长时间。
TTFT 和 Prefill 有很强的关系。
假设两个请求:
请求 A:
你好。
只有几个 Token。
请求 B:
这里有一篇 50,000 Token 的文档,请你总结……
即使两个请求最终都只生成 100 个 Token,它们的首 Token 延迟也可能完全不同。
因为请求 B 在开始生成之前,需要先处理那 50,000 个输入 Token。
所以一般来说:
Prompt 越长,Prefill 越重,TTFT 越高。
当然,实际 TTFT 还会受到排队、网络、Batch、硬件等因素影响。
但从模型计算本身来看,Prefill 是最主要的组成部分之一。
为什么 Prefill 很适合 GPU?
Prefill 有一个特点:
一次处理大量 Token。
比如同时处理:
4096 个 Token。
这些 Token 会进行大规模矩阵运算。
而 GPU 最擅长的事情恰好就是:
大规模并行矩阵计算。
因此 Prefill 可以比较充分地利用 GPU 的计算单元。
从硬件角度来说,Prefill 往往更加偏向:
Compute Bound
也就是:
计算能力成为主要瓶颈。
这时候 GPU 的 FLOPS、Tensor Core 性能等指标会非常重要。
例如一张 GPU 的矩阵计算能力越强,通常越有利于快速完成大型 Prompt 的 Prefill。
Prefill 结束以后发生什么?
假设 Prompt 已经全部处理完。
模型根据最后一个位置的隐藏状态,得到词表上的概率分布。
比如:
| Token | Probability | | ----------- | ----------: | | Transformer | 0.31 | | 它 | 0.21 | | 是 | 0.16 | | 可以 | 0.06 |
然后根据 Sampling 策略选出一个 Token。
假设选择的是:
Transformer
这个 Token 会被作为模型的第一个输出。
接下来就进入:
Decode 阶段。
什么是 Decode?
Decode 就是模型真正开始:
一个 Token 一个 Token 地生成答案。
例如模型最终生成:
Transformer 是一种基于 Attention 的神经网络架构。
它不是一次性生成整句话。
而是:
先生成:
Transformer
然后:
是
再然后:
一种
继续:
基于
直到整个回答完成。
每生成一个 Token,就需要再执行一次 Transformer。
所以 Decode 本质上是一个循环。
但这个循环和 Prefill 有一个非常重要的区别:
每轮通常只处理一个新的 Token。
KV Cache 为什么在 Decode 阶段这么重要?
如果没有 KV Cache,那么每生成一个新 Token,模型都要重新处理全部历史上下文。
例如已经有:
5000 个输入 Token
又生成了:
1000 个输出 Token
接下来准备生成第 1001 个输出 Token。
如果没有 KV Cache,模型就需要重新计算前面 6000 个 Token。
这显然非常浪费。
于是 KV Cache 会保存过去 Token 在每一层 Attention 中对应的:
- Key
- Value
Decode 时只需要计算新 Token 对应的:
- Query
- Key
- Value
然后把新的 K 和 V 追加到缓存中。
当前 Token 的 Query 再去查询整个历史 KV Cache。
因此可以把 Decode 理解成:
每次只新增一个 Token,但继续读取过去全部 Token 的 Attention 信息。
KV Cache 就是让这件事情可行的关键。
Decode 为什么和 Prefill 完全不同?
Prefill 一次处理很多 Token。
Decode 每次通常只处理一个 Token。
这个差别非常重要。
Prefill 时,矩阵规模比较大。
GPU 可以同时进行很多运算。
Decode 时,单次计算规模小很多。
GPU 很可能无法像 Prefill 那样充分利用全部计算单元。
但模型仍然需要不断读取大量数据。
比如:
- 模型权重
- KV Cache
所以 Decode 很容易变成:
Memory Bandwidth Bound
也就是:
显存带宽成为主要瓶颈。
这也是为什么大模型生成速度不能只看 GPU 的理论算力。
TFLOPS 高,不代表 Token/s 一定高
假设一张 GPU 拥有非常强的理论计算性能。
理论上可以进行海量浮点运算。
但 Decode 时,每次只生成一个 Token。
模型需要不停从显存中读取几十 GB 甚至上百 GB 的模型参数。
如果显存带宽不足,计算单元就只能等数据。
所以真实情况可能是:
GPU 算力还有很多没有用满。
但是显存已经在疯狂传输数据。
这就是典型的:
Memory Bound。
因此在 LLM 推理场景里,有几个硬件指标都非常重要:
- 显存容量
- 显存带宽
- 计算性能
而不是只看 FLOPS。
Token/s 是什么?
普通用户最容易感受到的指标其实是:
Token/s
也就是:
每秒生成多少 Token。
比如:
20 Token/s
意味着模型平均每秒生成 20 个 Token。
如果平均一个汉字或词片段对应一个左右的 Token,那么 20 Token/s 通常已经可以明显感觉到持续流式输出。
但 Token/s 主要描述的是:
Decode 速度。
它并不能准确反映 Prefill 性能。
一个模型可能:
首 Token 等 5 秒
然后以 100 Token/s 输出。
另一个模型可能:
0.3 秒出首 Token
但只有 20 Token/s。
两者体验完全不同。
所以完整衡量 LLM 推理性能时,需要同时看多个指标。
TPOT
除了 TTFT,还有一个常见指标:
TPOT
全称:
Time Per Output Token
也就是:
每生成一个新 Token 平均需要多少时间。
例如:
TPOT = 50ms
那么理论生成速度大约就是:
$ \frac{1000}{50}=20 $
也就是约:
20 Token/s。
所以 TTFT 主要描述:
第一个 Token 等多久。
TPOT 主要描述:
开始生成以后,每个 Token 等多久。
这两个指标分别对应了用户体验中的两个阶段。
Latency 和 Throughput 不是一回事
部署 LLM 时还有两个经常混淆的概念:
Latency
和:
Throughput
Latency 是:
一个请求需要多久。
Throughput 是:
整个系统在单位时间内能处理多少工作。
比如一台服务器每秒只能处理一个用户,但每个用户都非常快。
那么:
Latency 可能很好。
Throughput 却很低。
另一台服务器可以同时处理 100 个用户。
每个用户稍微慢一点。
但是整体 Token 输出量非常大。
那么:
Throughput 很高。
这两个目标经常存在冲突。
LLM Serving 系统需要在它们之间做平衡。
Batch 为什么能提高吞吐量?
GPU 擅长并行计算。
如果每次只让 GPU 给一个用户生成一个 Token,GPU 很可能吃不满。
于是推理框架会把多个请求放在一起处理。
例如:
用户 A 正在生成第 120 个 Token。
用户 B 正在生成第 35 个 Token。
用户 C 正在生成第 802 个 Token。
服务器可以把这些请求组合成一个 Batch,一起送给 GPU。
这样可以提高 GPU 利用率。
这也是为什么大模型服务中:
Batching
非常重要。
但是传统静态 Batch 有一个问题。
不同请求长度不同。
有人很快结束,有人还需要继续生成。
所以后来出现了更加灵活的:
Continuous Batching。
Continuous Batching
Continuous Batching 是现代 LLM Serving 中非常重要的一项技术。
传统 Batch 可能需要等:
整个 Batch 的请求都结束
才能加入新请求。
但 LLM 请求的长度非常不可预测。
一个用户生成 50 Token 就结束。
另一个用户可能要生成 5000 Token。
如果必须一直等最长的那个请求,GPU 资源会被严重浪费。
Continuous Batching 会动态管理请求。
某个请求生成结束后:
马上把新的请求加入。
这样 Batch 可以持续保持比较高的利用率。
vLLM 之所以能实现很高的吞吐量,Continuous Batching 就是其中一个关键部分。
Prefill 和 Decode 能不能一起 Batch?
可以。
但问题是:
它们的计算特征不一样。
Prefill:
一次处理很多 Token。
Decode:
每个请求通常只处理一个 Token。
如果直接混在一起,可能出现一个问题。
比如某个用户提交了:
50,000 Token 的 Prompt。
服务器开始执行非常大的 Prefill。
与此同时,其他用户正在等待自己的 Decode Token。
如果 Prefill 长时间占据 GPU,那么正在聊天的用户可能突然发现:
Token 不动了。
所以现代推理系统需要非常认真地处理:
Prefill 和 Decode 的调度问题。
Chunked Prefill
一个解决方案叫:
Chunked Prefill。
假设用户一次提交:
32K Token。
传统做法可能一次把 32K 全部 Prefill 完。
这可能长时间占用 GPU。
Chunked Prefill 会把长 Prompt 拆成多个小块。
例如:
每次处理 2048 Token。
中间可以穿插其他请求的 Decode。
这样可以避免某个超长 Prompt 阻塞整个服务器。
代价是调度和实现会更加复杂。
这也是推理框架不断优化的一个方向。
Prefill 和 Decode 甚至可以分到不同 GPU
因为 Prefill 和 Decode 的硬件需求差别很大,人们进一步想到:
为什么一定要让它们在同一种机器上运行?
于是出现了一类架构:
Prefill-Decode Disaggregation
也就是把 Prefill 和 Decode 拆开。
例如:
一组 GPU 专门处理 Prefill。
另一组 GPU 专门负责 Decode。
Prefill 完成后,再把 KV Cache 或相关状态交给 Decode 节点。
这样可以分别针对两个阶段优化硬件资源。
Prefill 节点更加看重:
计算能力。
Decode 节点更加看重:
显存容量和显存带宽。
在超大规模 LLM Serving 系统中,这是一种非常重要的方向。
为什么 Prompt Cache 能降低 TTFT?
现在重新看上一篇提到的:
Prefix Cache / Prompt Cache。
假设 Agent 有一个固定的 System Prompt。
长度:
8000 Token。
每个请求都携带完全一样的系统提示词。
如果每次都重新 Prefill 这 8000 Token,会造成大量重复计算。
如果系统已经缓存了对应的 KV Cache,那么新请求可以直接复用这部分结果。
于是需要重新 Prefill 的内容可能只剩:
用户刚刚输入的几十个 Token。
TTFT 就会明显下降。
所以 Prompt Caching 本质上优化的主要就是:
Prefill。
为什么 Agent 很容易 Prefill 很重?
普通聊天的输入可能只有:
100 Token。
但 Agent 不一样。
Agent 的 Context 可能包含:
- System Prompt
- Tool Schema
- Memory
- RAG 文档
- 历史消息
- Tool Result
- Observation
- 工作状态
这些东西全部加起来,很容易达到:
10K
20K
甚至几十 K Token。
于是每轮 Agent 调用都可能需要进行非常重的 Prefill。
这也是为什么很多 Agent 看起来:
模型生成其实挺快,但是每一步 Tool Call 前都要停顿一下。
问题不一定是模型不会推理。
很可能是:
每一轮都在重新 Prefill 很大的 Context。
为什么 Tool 太多也会拖慢 Agent?
假设一个 Agent 注册了:
100 个 Tool。
每个 Tool 都包含:
- Tool Name
- Description
- Parameter Schema
这些定义通常都会被放进 Prompt。
如果 Tool Schema 总共占据:
5000 Token。
那么每次模型推理,都要处理这部分上下文。
这意味着:
更多 Prefill 时间。
更多 KV Cache。
更高推理成本。
所以 Agent Tool 设计中一个很重要的问题并不是:
能不能给 Agent 更多工具?
而是:
当前任务到底需要暴露哪些工具?
Tool Routing、Dynamic Tool Loading、Tool Retrieval 等技术,本质上也可以从 Context 成本的角度理解。
RAG 为什么也可能拖慢推理?
RAG 可以让模型获得外部知识。
但如果检索系统一次返回大量内容:
例如 20 个 Chunk,每个 1000 Token。
那么一次就会向 Context 注入:
20,000 Token。
模型虽然获得了更多信息,但 Prefill 也会明显变重。
所以 RAG 不是:
检索越多越好。
而是:
检索最相关、最有价值的内容。
优秀的 RAG 系统通常都会继续做:
- Rerank
- Filtering
- Context Compression
- Chunk Selection
因为最终所有进入 Prompt 的 Token,都要付出推理成本。
Prefill 的复杂度为什么会随着 Context 变长?
在标准 Self-Attention 中,输入序列里的 Token 需要计算彼此之间的 Attention。
如果 Context Length 是:
$ n $
Attention Matrix 的规模大约是:
$ n\times n $
所以长 Context 会快速增加计算量。
不过实际现代模型中可能使用:
- FlashAttention
- Sliding Window Attention
- Sparse Attention
- Chunked Attention
等各种优化。
因此真实性能不能单纯用最基础的复杂度直接推断。
但整体趋势仍然是:
Prompt 越长,Prefill 越昂贵。
Decode 的成本为什么也会越来越高?
有 KV Cache 以后,Decode 不需要重新计算过去所有 Token 的 K 和 V。
但它仍然需要:
读取过去的 KV Cache。
上下文越长,KV Cache 越大。
新 Token 的 Query 需要与更多历史 Key 进行 Attention。
所以即使有 KV Cache:
生成到第 100 个 Token
和
生成到第 100,000 个 Token
成本也不会完全一样。
Context 越长,Decode 的 Attention 成本和显存读取压力也会增加。
因此 KV Cache 解决的是:
避免重复计算。
而不是:
让长 Context 完全没有成本。
Streaming 为什么让模型感觉更快?
很多 AI 产品会使用:
Streaming Output。
模型一旦生成第一个 Token,就立刻发给前端。
然后继续发送后面的 Token。
这样用户会看到文字持续出现。
假设完整回答需要:
10 秒。
如果不 Streaming:
用户会等 10 秒。
然后突然看到整段回答。
如果 Streaming:
用户可能 0.8 秒看到第一个 Token。
然后看着内容逐渐出现。
虽然完整生成时间可能差不多,但主观体验会好很多。
所以 LLM 产品非常看重:
TTFT。
用户通常对:
多久开始回答
非常敏感。
一个请求真正的时间可以怎么理解?
把网络和调度先忽略,一个请求的总时间可以粗略理解成:
$ Total\ Time = Prefill + Decode $
进一步:
$ Decode \approx OutputTokens \times TPOT $
所以可以大概理解为:
$ Total\ Time \approx TTFT + OutputTokens\times TPOT $
这不是一个严格的系统性能公式,但非常适合理解基本关系。
如果 Prompt 很长:
Prefill 占比会很高。
如果回答很长:
Decode 占比会越来越高。
因此不同任务的性能瓶颈也不同。
总结生成和聊天的优化重点可能完全不同
比如一个文档摘要任务:
输入:
50K Token
输出:
500 Token
这种任务通常:
Prefill 很重。
而普通聊天:
输入:
100 Token
输出:
2000 Token
这种任务:
Decode 的占比可能更高。
代码生成也是类似。
用户 Prompt 可能不长,但要求生成:
5000 Token 的代码。
这时候生成阶段非常重要。
所以不存在一个统一的:
LLM 推理性能。
必须看实际 Workload。
为什么推理框架这么复杂?
现在再看 vLLM、SGLang、TensorRT-LLM 这些框架,就会发现它们解决的其实不是:
怎么调用模型。
而是在解决一整套资源调度问题。
比如:
怎么管理 KV Cache。
怎么 Batch 多个请求。
怎么减少显存碎片。
怎么调度 Prefill 和 Decode。
怎么处理超长 Prompt。
怎么复用 Prefix Cache。
怎么提高 GPU 利用率。
怎么降低 TTFT。
怎么提高 Token Throughput。
真正的大模型 Serving,本质上已经非常接近:
一个专门为 Token 生成设计的分布式计算系统。
从一次请求重新理解 LLM 推理
现在再完整看一次请求。
用户输入 Prompt。
Tokenizer 把文本转换成 Token。
模型进入 Prefill。
输入 Token 一次性经过 Transformer,同时生成对应的 KV Cache。
Prefill 完成后,模型得到第一个输出 Token。
此时 TTFT 结束。
然后进入 Decode。
模型利用已经存在的 KV Cache,每次只为新 Token 计算新的状态。
一个 Token 生成后,再生成下一个。
这个过程不断循环,直到:
生成 EOS Token
或者:
达到最大输出长度
或者:
被用户主动停止。
所以整个 LLM 推理过程,本质上可以看成两个完全不同的阶段:
先理解已有上下文,再持续生成新的上下文。
前半段就是 Prefill。
后半段就是 Decode。
为什么理解这两个词很重要?
如果只是调用 OpenAI、Claude 或 Qwen 的 API,不理解 Prefill 和 Decode 当然也能开发应用。
但一旦开始做:
- LLM 部署
- Agent
- RAG
- 长上下文
- 推理优化
- vLLM
- GPU 性能分析
这两个概念几乎一定会出现。
它们还会连接起很多之前看起来互不相关的词:
KV Cache、TTFT、TPOT、Token/s、Continuous Batching、PagedAttention、Prompt Cache、Chunked Prefill。
而这些概念最终都在回答同一个问题:
如何让 Transformer 更高效地完成一次真实的生成任务?
理解了 Prefill 和 Decode,也就开始从“模型怎么生成文本”,进一步走到了:
大模型到底是怎么被运行起来的。