Prefill与Decode:为什么首Token延迟更高?
Prefill受计算能力限制,决定首Token延迟;Decode受内存带宽限制,决定每秒Token数。请在您的设备上分别测量两项指标。
Prefill 与 decode:用一段话说明
Prefill 与 decode 的区别,可以解释自托管 LLM(大语言模型)的大多数延迟问题。Prefill 在一次处理中读取完整提示词,受计算能力限制。Decode 一次生成一个 token,受内存带宽限制。首 token 延迟属于 prefill 指标。每秒生成的 token 数属于 decode 指标。
两个阶段使用同一块 GPU(图形处理器)、同一组权重,并在同一进程中运行,因此将它们视为一个工作负载很自然。实际上,它们更像是在同一设备上共享资源的两个不同程序。将两者区分开后,许多看似矛盾的结果就不再难以解释。
为什么 prefill 受计算能力限制?
Prefill 会让完整提示词依次通过每一层。2,000 token 的提示词会为每次矩阵乘法提供 2,000 行计算任务,因此 GPU 每加载一个字节的权重,就要执行大量算术运算。单位数据移动量对应的算术运算量称为算术强度,prefill 的算术强度较高。设备会接近计算能力上限,而内存总线仍有余量。
Prefill 会生成两项内容:提示词中每个 token 对应的 KV cache(key 和 value 张量),以及第一个输出 token。在这次处理完成前,读取端不会收到任何内容。因此,prefill 时间和首 token 时间(TTFT)通常接近同一个指标。
Prefill 的开销会随提示词长度增长。线性部分是每层的矩阵计算。二次部分是 attention,因为每个 token 都要关注此前的每个 token;在上下文较长时,这部分开销会开始显著增加。因此,将提示词长度加倍后,TTFT 至少也会加倍。
您可以在一分钟内观察到这一点。向服务器发送一个 200 token 的提示词,再发送一个 2,000 token 的提示词,并让两次请求生成相同数量的输出 token。TTFT 会明显上升。首 token 之后的流式输出速度几乎不变。
为什么解码受内存带宽限制?
解码每一步只生成一个 token。要生成这一个 token,GPU 必须从内存中读取模型的全部权重,让每个权重参与几次运算,然后丢弃它。算术强度接近 1,因此计算单元大部分时间都在等待。
解码速度慢,是因为每个 token 都需要从内存中读取整个模型。因此,内存总线决定处理速度,而计算单元处于空闲状态。
这意味着,单流解码速度的上限可以直接通过算术计算得出。将内存带宽除以权重占用的字节数即可。
The data behind this chart
[
{
"device": "CPU, dual channel DDR5-5600",
"mem_bandwidth_gb_s": 90,
"decode_ceiling_tok_s": 6
},
{
"device": "NVIDIA A10G",
"mem_bandwidth_gb_s": 600,
"decode_ceiling_tok_s": 38
},
{
"device": "NVIDIA L40S",
"mem_bandwidth_gb_s": 864,
"decode_ceiling_tok_s": 54
},
{
"device": "NVIDIA RTX 4090",
"mem_bandwidth_gb_s": 1008,
"decode_ceiling_tok_s": 63
},
{
"device": "NVIDIA A100 80GB SXM",
"mem_bandwidth_gb_s": 2039,
"decode_ceiling_tok_s": 127
},
{
"device": "NVIDIA H100 SXM",
"mem_bandwidth_gb_s": 3350,
"decode_ceiling_tok_s": 209
}
]带宽列使用各厂商公布的规格值。上限列是该规格值除以 16 GB,后者是以 16 bit 精度存储的 80 亿参数模型的大小。这只是算术结果,不是基准测试结果。实测速率会低于该上限。了解实际值低多少很有用,因为这能告诉您应该调整推理服务栈,还是升级硬件。
按顺序阅读 6 行,规律很明显。采用双通道 DDR5 的 CPU 带宽约为 90 GB/s,因此该模型的解码速度上限约为 6 tokens/s。L40S 的上限约为 54。H100 SXM 公布的带宽为 3350 GB/s,其上限约为 209。
这也是量化对解码速度提升最大的单一因素。将同一个模型从 16 bit 改为 8 bit 存储后,每个 token 需要读取的字节数会减半,因此上限大致会翻倍。计算量没有增加。需要读取的内存减少了。
如何在自己的服务器上测量每个阶段?
Ollama 会在响应正文中返回拆分结果。请求一次非流式补全,然后读取计数器。
curl -s http://localhost:11434/api/generate -d '{
"model": "llama3.2",
"prompt": "Explain memory bandwidth in two sentences.",
"stream": false
}' | jq '{prompt_eval_count, prompt_eval_duration, eval_count, eval_duration}'使用你实际已拉取的模型标签,ollama list 会显示可用标签。prompt_eval_count 和 prompt_eval_duration 是预填充阶段的数据:提示词 token 数量及其耗时。eval_count 和 eval_duration 是解码阶段的数据。耗时单位为纳秒,因此解码速度为 eval_count / eval_duration * 1e9,预填充速度为 prompt_eval_count / prompt_eval_duration * 1e9。对于同一个请求,预填充速率通常会远高于解码速率。本文其余内容解释的就是这两者之间的差距。
对于 vLLM 这类兼容 OpenAI 的服务器,可以使用 curl 测量首字节时间。
curl -N -s -o /dev/null \
-w 'pretransfer %{time_pretransfer}s first_byte %{time_starttransfer}s\n' \
http://localhost:8000/v1/completions \
-H 'Content-Type: application/json' \
-d '{"model": "meta-llama/Llama-3.1-8B-Instruct", "prompt": "Explain memory bandwidth.", "max_tokens": 128, "stream": true}'time_starttransfer 是收到正文第一个字节的时刻,因此结合 "stream": true 可得到包含连接建立时间的 TTFT。减去 time_pretransfer 即可排除连接建立耗时。运行两次并保留第二次结果,因为第一次调用可能包含模型冷加载时间。
vLLM 还会在 /metrics 上以 Prometheus 指标形式发布这组拆分数据。运行 curl -s http://localhost:8000/metrics | grep -E 'time_to_first_token|inter_token_latency',即可获得直方图 vllm:time_to_first_token_seconds 和 vllm:inter_token_latency_seconds。添加 vllm:num_requests_running 和 vllm:num_requests_waiting 可查看队列深度,添加 vllm:kv_cache_usage_perc 可查看缓存压力。这 5 个名称就是完整的仪表板。
在负载下,vllm bench serve --model <name> --num-prompts 200 --request-rate 4 会驱动运行中的服务器,并按百分位数报告首 token 时间和每个输出 token 的延迟。只有这样才能观察两个阶段如何相互争用。在调整任何参数之前,先建立干净的基线:测量本地 LLM 的每秒 token 数中的方法可提供一个重启后仍然有效的基线。
为什么较长的系统提示会延迟第一个 token,却不会影响流式输出速度?
因为系统提示属于预填充工作,仅此而已。它会与提示的其余部分在同一轮处理中完成,并在第一个 token 出现前处理完毕。处理完成后,它只作为 KV cache 条目存在,解码时会与其他内容一起读取。因此,3,000 token 的系统提示会在每个请求中增加 TTFT,但几乎不会改变每秒生成的 token 数。
这里说的是“几乎”,并非完全不变。每个解码步骤都会再次读取这些额外的 KV 条目,因此很长的提示确实会略微降低解码速度。下一节会介绍这一点。
解决方法是停止重复计算相同的前缀。启用前缀缓存的服务器会保留共享前缀的 KV cache 并重复使用,因此,第二个携带相同系统提示的请求可以完全跳过这部分预填充。vLLM 将其称为 automatic prefix caching;请检查您所用版本中的 vllm serve --help,因为该默认设置在不同版本中有所变化。GPU 中的 KV cache 与 API 提供商计费所使用的提示缓存是两种不同的机制。在调整其中任何一种之前,建议先阅读KV cache 与提示缓存的区别】【。
上下文填满后,为什么解码会变慢?
原因有两个,都与 KV 缓存有关。
第一个原因是带宽。在每个解码步骤中,注意力机制都会读取所有前序 token 的键和值。权重是每个 token 的固定开销,而 KV 缓存会不断增长。您可以根据模型的 config.json 计算其大小:每个 token 的字节数等于 2 乘以 num_hidden_layers,再乘以 num_key_value_heads,再乘以头维度(hidden_size 除以 num_attention_heads),最后乘以每个元素占用的字节数。开头的 2 表示一个键和一个值。
对于一种常见的 8 billion 参数布局,它包含 32 个层、GQA(分组查询注意力)下的 8 个键值头、128 的头维度,并使用 16 bit 精度。此时计算结果为 2 x 32 x 8 x 128 x 2 = 131,072 bytes,即每个 token 约 128 KiB。因此,一次 8,000 token 的对话大约需要 1 GB 的 KV 缓存。
第二个原因是容量。这 1 GB 内存无法同时用于存放权重或其他用户的上下文。服务器会在启动时一次性确定 KV 池的大小;在 vLLM 中,该设置通过 --gpu-memory-utilization 配置。KV 池满后,新请求会等待。vllm:num_requests_waiting 持续升高,同时 vllm:kv_cache_usage_perc 接近 1,就是这种状态的准确特征。某些技术栈会暂停正在运行的请求,稍后重新计算其缓存,而不是将请求排队。对用户而言,这会表现为流式输出中途卡顿。
长上下文会带来双重成本:开始时需要执行更多预填充计算,之后生成答案的每个 token 都需要读取更多内存。
为什么批处理有助于吞吐量,却会损害尾部延迟?
由于 decode 受带宽限制,额外请求几乎不会增加计算开销。一次读取权重就可以为批次中的每个序列生成一个 token,因此总吞吐量会随着批大小近似线性增长,直到 KV 池耗尽,或批次变大到再次受计算能力限制。Continuous batching 会在每个 step 重建批次,因此已完成的请求可以退出,排队请求可以加入,无需等待其他请求完成。
代价会体现在百分位数上。现在,每个用户获取下一个 token 都要等待共享 step 中最慢的部分,因此 p50(中位数)仍可接受,但 p99(100 个请求中最慢的 1 个)会变长。用户通常能感知到 p99,因为它表现为句子中间的停顿。
Prefill 会进一步放大这一问题。较大的 prompt 在流式生成过程中到达时,会占用设备完成一个很长的 step,所有当前正在生成的请求都会出现间隔。Chunked prefill 将较长的 prompt 切分为多个片段,并将每个片段混入 decode 批次,从而消除大部分影响。截至 August 2026,vLLM V1 engine 默认启用此功能,并通过 --max-num-batched-tokens 提供平衡控制。vLLM 调优文档明确说明了这一权衡:较小的值(约 2048)可获得更好的 inter token latency (ITL),因为较少的 prefill 会打断 decode;较大的值可获得更好的 TTFT,因为单个批次可以容纳更多 prefill token。这个 flag 将 prefill 与 decode 之间的权衡暴露为一个可调数字。p99 在何处开始变得不可接受,取决于容量;一台自托管 LLM 可以服务多少并发用户会使用相同的指标分析这一问题。
为什么更大的 GPU 有时没有任何改善?
因为更大通常意味着更强的计算能力,而解码并不主要受计算能力限制。
比较上方图表中的两行数据。A100 80GB 的公开带宽为 2039 GB/s,L40S 为 864 GB/s;解码上限也完全对应,分别为 127 tokens/s 和 54。按大多数指标衡量,RTX 4090 都是一款速度很快的显卡,其 1008 GB/s 带宽将解码上限设为 63。无论两张卡在其他方面有何差异,单流解码速度都遵循规格表中的带宽水平。
因此,提高解码速度有两种方法:减少每个 token 需要读取的字节数(对权重进行量化,或运行更小的模型),或者使用带宽更高的显卡。Prefill 则相反,它需要计算能力,因此更快的显卡确实可以缩短长提示词的 TTFT。如果问题是生成第一个 token 需要 4 秒,更好的硬件可能会解决问题。如果问题是文本逐字生成得很慢,升级硬件可能无法解决。
是否应在不同 worker 上分别运行 prefill 和 decode?
大型推理服务栈确实会这样做,这种技术称为 prefill 和 decode 解耦。一个 worker 池只运行 prefill,另一个 worker 池只运行 decode。第一个池构建的 KV cache 通过高速互连传输到第二个池。这样做可行,是因为两个阶段需要不同的硬件和调度方式。Prefill 需要计算能力和较大的 token 批次。Decode 需要内存带宽和大量并发序列。将两者拆分后,每个池都可以独立扩展,也能避免一个超长 prompt 阻塞所有活动流。
在只有一块 GPU 的单台 VPS(virtual private server)上,这样做几乎从来不值得。您实际上是在让一个设备与自身分工,还会把指针传递变成数 GB cache 的网络传输。只有在加速器数量足够、可以为每个阶段分配整台机器,并且流量足够稳定、能够让两个池持续繁忙时,这项技术才有收益。在此规模以下,只需设置一个 flag,chunked prefill 就能提供大部分相同的隔离效果。
数值异常时的调整方法
TTFT 过高时:
- 缩短提示词。预填充开销取决于提示词 token 数量,而且每次请求都要计算系统提示词。
- 启用前缀缓存,使重复前缀只需计算一次,而不是每次都重新计算。
- 提高
--max-num-batched-tokens,让每个 step 执行更多预填充工作。 - 先检查队列,不要立即归咎于模型。
vllm:num_requests_waiting大于 0 表示请求尚未开始处理,问题在于容量不足。
每秒 token 数过低时:
- 对权重进行量化。每个权重占用的字节数越少,每个 token 需要读取的字节数就越少。
- 将显卡公布的内存带宽与上方图表进行比较,确认当前性能距离上限还有多远。
- 降低
--max-num-batched-tokens,减少预填充对解码的中断。 - 检查上下文长度。对话增长到数千个 token 后,每个 step 都需要读取大得多的 KV 缓存。
运行时在这里同样重要,因为 Ollama 和 vLLM 对预填充和解码的调度方式不同,某个设置可能对其中一个有效,但对另一个不起作用。先分别测量这两个阶段,再一次只修改一个设置。
FAQ
为什么第一个 token 需要等待几秒,后续 token 却能快速流式输出?
等待阶段是 prefill,流式输出阶段是 decode。prefill 会在输出产生前,通过一次受计算限制的处理遍历整个提示词,因此耗时会随提示词长度增加。随后 decode 每一步输出一个 token,速度主要由内存带宽决定,几乎不受提示词长度影响。每次请求都携带很长的系统提示词,通常是主要原因。前缀缓存可以消除这部分重复开销。
更长的提示词会降低每秒输出的 token 数吗?
会有一点影响,但原因不同于 TTFT。每个 decode 步骤都会读取此前所有 token 的 keys 和 values,因此 KV cache 越大,每个 token 需要读取的字节数越多。对于常见的 8 billion 参数布局,KV cache 约为每个 token 128 KiB,因此 8,000 token 的上下文约为 1 GB,并且每一步都会访问这部分数据。长提示词的主要影响仍然体现在 TTFT,而不是流式输出速度上。
哪项 GPU 规格最能预测 decode 速度?
内存带宽。将产品公布的带宽除以内存中权重的大小,即可得到单路流的理论计算上限。计算能力更强但带宽相同的显卡,流式输出速度不会更快。这也是量化为 8 bits 后 decode 速度大约翻倍的原因:每个 token 需要读取的字节数减半,但计算量不变。
为什么增加用户后吞吐量会上升,但每个用户感觉更慢?
读取一次权重即可为批次中的每个序列处理一个 token,因此总 tokens per second 会随批次大小增加。每个 token 现在都要等待共享步骤完成,所以单个用户的延迟也会同时上升。应监控 p99 inter token latency,而不是只看聚合吞吐量,并检查 vllm:num_requests_waiting,确认请求是在排队,而不是正在运行。