吞吐量与延迟权衡:理解吞吐量和延迟的关系及调优目标

核心原理:为什么吞吐量与延迟天然对立

在 vLLM 批处理系统中,吞吐量(Throughput)与延迟(Latency)构成一对基础矛盾。吞吐量通常以 tokens/s(每秒生成的 token 数)或 requests/s(每秒完成的请求数)衡量,而延迟则指单个请求从提交到返回完整结果的耗时,常细分为 TTFT(首 token 延迟)与 TPOT(每输出 token 间隔)。两者之所以对立,根源在于 GPU 的计算资源与显存带宽是共享的:增大批大小能摊薄算子调度开销、提升硬件利用率,从而推高吞吐量,但同一批次内每个请求的排队等待时间随之拉长,单个请求的完成时间被显著延后。

更本质的约束来自 连续批处理(Continuous Batching)机制。vLLM 采用按 token 粒度动态调度:只要某请求的当前 token 解码完成,且显存中还有空闲 KV cache 块,新请求即可插入当前迭代。这种机制让吞吐量随并发请求数近似线性增长,直到触及显存上限或计算饱和点。然而,当批大小超过某个阈值后,每个迭代的耗时(step time)开始明显上升,因为 GPU 需要处理更多注意力计算与更大的中间矩阵乘法。此时,吞吐量曲线趋于平缓甚至回落,而延迟曲线则加速上扬——这就是调优中必须面对的“拐点区域”。

关键认知:吞吐量与延迟的权衡并非“二选一”,而是寻找服务等级目标(SLO)约束下的最大吞吐量。例如,若业务要求 P95 TTFT 小于 800ms,则调优目标是在满足此延迟红线的前提下,尽可能提高批大小与并发数。
批大小 / 并发请求数 → 指标值 → 吞吐量 (tokens/s) P95 延迟 (ms) 拐点区域 最优工作点 SLO 延迟红线 延迟敏感区 (低吞吐) 吞吐饱和区 (延迟恶化) 吞吐量 延迟

方法流程:基于 SLO 的调优路径

吞吐量与延迟的调优不能凭感觉反复试错,而应遵循一套可复用的决策流程。以下方法将 SLO 作为硬约束,把吞吐量作为优化目标,在 vLLM 的典型参数空间中逐步逼近最优配置。

  1. 定义 SLO 指标:明确业务可接受的延迟上限。常见组合为 TTFT ≤ 500ms(交互式场景)或 TPOT ≤ 100ms(流式输出场景)。同时确定统计口径,如 P50、P95 或 P99。
  2. 建立基准配置:以 vLLM 默认参数启动服务(--max-num-seqs 256,--max-model-len 4096),使用代表性负载(混合 prompt 长度与输出长度)压测,记录当前吞吐量与延迟分布。
  3. 单变量扫描:固定其他参数,逐步增大 --max-num-seqs(批大小上限),观察吞吐量增长曲线与延迟拐点。每次扫描记录 3 组数据:吞吐量、P95 TTFT、P95 TPOT。
  4. 交叉验证:在拐点附近,微调 --gpu-memory-utilization(默认 0.9)与 --max-num-batched-tokens,确认显存是否成为瓶颈。若显存充足但延迟超标,可降低批大小;若吞吐不足且延迟有余量,则继续加压。
  5. 确定最优工作点:选择满足所有 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 测试会严重低估真实场景下的延迟波动。建议录制线上请求日志,回放压测以获得可信数据。

复盘整个调优过程,最核心的经验是:吞吐量与延迟的权衡点不是静态的,它随负载特征、模型大小、显存容量动态移动。每次业务迭代(如 prompt 模板变化、输出长度调整)后,都应重新执行上述扫描流程,而非沿用历史配置。将调优记录沉淀为参数基线表,并标注对应的负载特征,才能让团队在后续优化中快速定位问题。