吞吐量与延迟权衡:理解吞吐量和延迟的关系及调优目标
核心原理:为什么吞吐量与延迟天然对立
在 vLLM 批处理系统中,吞吐量(Throughput)与延迟(Latency)构成一对基础矛盾。吞吐量通常以 tokens/s(每秒生成的 token 数)或 requests/s(每秒完成的请求数)衡量,而延迟则指单个请求从提交到返回完整结果的耗时,常细分为 TTFT(首 token 延迟)与 TPOT(每输出 token 间隔)。两者之所以对立,根源在于 GPU 的计算资源与显存带宽是共享的:增大批大小能摊薄算子调度开销、提升硬件利用率,从而推高吞吐量,但同一批次内每个请求的排队等待时间随之拉长,单个请求的完成时间被显著延后。
更本质的约束来自 连续批处理(Continuous Batching)机制。vLLM 采用按 token 粒度动态调度:只要某请求的当前 token 解码完成,且显存中还有空闲 KV cache 块,新请求即可插入当前迭代。这种机制让吞吐量随并发请求数近似线性增长,直到触及显存上限或计算饱和点。然而,当批大小超过某个阈值后,每个迭代的耗时(step time)开始明显上升,因为 GPU 需要处理更多注意力计算与更大的中间矩阵乘法。此时,吞吐量曲线趋于平缓甚至回落,而延迟曲线则加速上扬——这就是调优中必须面对的“拐点区域”。
方法流程:基于 SLO 的调优路径
吞吐量与延迟的调优不能凭感觉反复试错,而应遵循一套可复用的决策流程。以下方法将 SLO 作为硬约束,把吞吐量作为优化目标,在 vLLM 的典型参数空间中逐步逼近最优配置。
- 定义 SLO 指标:明确业务可接受的延迟上限。常见组合为
TTFT ≤ 500ms(交互式场景)或TPOT ≤ 100ms(流式输出场景)。同时确定统计口径,如 P50、P95 或 P99。 - 建立基准配置:以 vLLM 默认参数启动服务(
--max-num-seqs 256,--max-model-len 4096),使用代表性负载(混合 prompt 长度与输出长度)压测,记录当前吞吐量与延迟分布。 - 单变量扫描:固定其他参数,逐步增大
--max-num-seqs(批大小上限),观察吞吐量增长曲线与延迟拐点。每次扫描记录 3 组数据:吞吐量、P95 TTFT、P95 TPOT。 - 交叉验证:在拐点附近,微调
--gpu-memory-utilization(默认 0.9)与--max-num-batched-tokens,确认显存是否成为瓶颈。若显存充足但延迟超标,可降低批大小;若吞吐不足且延迟有余量,则继续加压。 - 确定最优工作点:选择满足所有 SLO 约束且吞吐量最大的参数组合。记录该配置下的 有效吞吐量(仅统计满足 SLO 的请求),而非原始吞吐量。
| 参数 | 对吞吐量的影响 | 对延迟的影响 | 调优建议 |
|---|---|---|---|
--max-num-seqs |
增大可提升吞吐,但存在饱和点 | 增大批大小直接推高排队延迟 | 从 128 起步,按 64 步进扫描 |
--max-num-batched-tokens |
限制单次迭代 token 总量,过高反而降低效率 | 影响单步计算耗时 | 设为 max-num-seqs × 平均输出长度 的 1.2 倍 |
--gpu-memory-utilization |
预留 KV cache 空间,过低限制并发 | 过高可能导致显存不足触发交换 | 0.85~0.95 之间,观察显存余量 |
--enable-prefix-caching |
命中前缀时吞吐提升明显 | 降低预填充耗时 | 多轮对话或长 prompt 场景建议开启 |
案例场景:聊天机器人的 SLO 调优实战
某智能客服团队部署了基于 vLLM 的 7B 对话模型,业务要求:P95 TTFT ≤ 600ms,P95 TPOT ≤ 80ms。初始配置为 --max-num-seqs 256,压测结果如下:吞吐量 1850 tokens/s,P95 TTFT 420ms,P95 TPOT 65ms——延迟有余量,但吞吐量未达预期。
团队按流程逐步将 --max-num-seqs 提升至 384,吞吐量升至 2300 tokens/s,但 P95 TTFT 飙升至 780ms,超出 SLO。继续观察发现,此时 GPU 利用率已达 97%,但 KV cache 仍有空闲。问题出在预填充阶段:当批大小过大时,长 prompt 的 prefill 计算阻塞了解码迭代,导致 TTFT 恶化。
最终方案是启用 --enable-chunked-prefill,将预填充拆分为多个小 chunk 与解码请求交错执行。调整后,在 --max-num-seqs 320 下,吞吐量稳定在 2150 tokens/s,P95 TTFT 降至 550ms,P95 TPOT 为 72ms,全部满足 SLO。该案例说明:延迟超标不一定要降低批大小,有时需要改变调度策略。
# 启动 vLLM 服务(启用 chunked prefill)
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-2-7b-chat-hf \
--max-num-seqs 320 \
--max-num-batched-tokens 8192 \
--gpu-memory-utilization 0.92 \
--enable-chunked-prefill \
--port 8000
# 压测脚本(简化示意)
for concurrency in 50 100 150 200; do
python benchmark_serving.py \
--backend vllm --model Llama-2-7b \
--num-prompts 500 --request-rate $concurrency \
--percentile 95
done
误区与复盘:常见调优陷阱
误区一:只盯平均延迟,忽略长尾分布。平均延迟可能表现良好,但 P99 请求可能因排队过长而超时。vLLM 的连续批处理天然引入调度抖动,必须用 P95 或 P99 作为决策依据。复盘建议:压测时同时输出延迟分布直方图,观察是否存在双峰现象(prefill 与 decode 混合导致的延迟分层)。
误区二:盲目追求最大吞吐量。当批大小超过拐点后,吞吐量增长趋缓,但延迟呈指数级恶化。此时“有效吞吐量”(满足 SLO 的请求占比)反而下降。例如某团队将批大小从 512 提升至 640,原始吞吐量仅增加 3%,但 P95 TTFT 从 900ms 涨至 1400ms,导致 30% 请求超时被客户端重试,实际系统吞吐量不升反降。
误区三:忽略输入长度分布的影响。若业务中 prompt 长度差异极大(如 100 token 与 4000 token 混合),固定 --max-num-batched-tokens 会导致短请求被长请求阻塞。复盘发现,将 --max-num-seqs 与 --max-num-batched-tokens 解耦,并配合 --enable-chunked-prefill,可显著降低短请求的排队延迟。
复盘整个调优过程,最核心的经验是:吞吐量与延迟的权衡点不是静态的,它随负载特征、模型大小、显存容量动态移动。每次业务迭代(如 prompt 模板变化、输出长度调整)后,都应重新执行上述扫描流程,而非沿用历史配置。将调优记录沉淀为参数基线表,并标注对应的负载特征,才能让团队在后续优化中快速定位问题。