推理延迟与吞吐的数学权衡:Pareto 边界上的最优 Batch Size 搜索
一、在延迟和吞吐之间——不存在"又快又多"的可能
推理系统中存在一条无形的性能边界:延迟与吞吐的 Pareto 前沿。你可以在前沿上的任何点运行(慢但吞吐高,或快但吞吐低),但无法同时突破这两个维度。这条边界的形状由三个物理因素决定:GPU SM 利用率曲线、显存带宽的饱和点、以及 KV Cache Block 的并发容量。
理解 Pareto 边界的意义不是去"打破"它,而是根据业务的延迟 SLA 在边界上找到最优运行点——即在满足 P99 延迟约束的前提下最大化吞吐。这本质上是一个约束优化问题:max(Throughput) subject to TPOT P99 < 40ms。
二、延迟-吞吐 Pareto 边界的物理根源
flowchart LR subgraph Single_Request["单请求 Forward Pass"] A1["KV Cache Pre-fill<br/>GPU 利用率: 80~95%<br/>(计算密集型)"] A2["Auto-regressive Decode<br/>GPU 利用率: 5~15%<br/>(显存带宽密集型)"] end subgraph Batched["Batched 推理"] B1["Batch=1<br/>Decode 阶段 SM 空转<br/>等待 HBM 数据"] B2["Batch=8<br/>多请求交织计算<br/>SM 利用率提升至 30%"] B3["Batch=32<br/>SM 利用率 ~60%<br/>但每请求 TPOT 同步增长"] B4["Batch=128<br/>SM 利用率 ~85%<br/>接近计算上限<br/>但单请求 TPOT 劣化显著"] end B1 --> D1["延迟最低 (TPOT=15ms)<br/>吞吐最低 (800 Token/s)"] B2 --> D2["均衡点 (TPOT=18ms)<br/>吞吐 (5200 Token/s)"] B3 --> D3["高吞吐 (TPOT=25ms)<br/>吞吐 (12000 Token/s)"] B4 --> D4["吞吐极限 (TPOT=45ms)<br/>吞吐 (18000 Token/s)"] D1 & D2 & D3 & D4 --> E["Pareto 前沿<br/>• 每个点都是最优(不可同时改进两个维度)<br/>• 选点取决于业务的延迟 SLA"]三、在延迟 SLO 约束下搜索最优配置
import numpy as np from dataclasses import dataclass @dataclass class InferenceConfig: batch_size: int max_num_seqs: int tensor_para_size: int def search_optimal_config( target_qps: float, max_tpot_p99_ms: float = 40.0, max_ttft_p99_ms: float = 2000.0 ) -> InferenceConfig: """ 在给定的延迟 SLO 约束下,搜索最大化吞吐的推理配置。 基于实测数据拟合的性能曲线——而非基于解析公式的理论值。 """ # 实测性能曲线(H100 × 8, LLaMA-3.1-70B, BF16) # 数据来源于基准测试数据库,此处为函数拟合值 batch_sizes = [1, 2, 4, 8, 16, 32, 64, 128] tpot_p50 = [15.2, 15.8, 16.5, 18.1, 21.3, 26.8, 35.2, 48.6] # ms tpot_p99 = [22.1, 23.4, 25.1, 28.7, 34.5, 44.2, 58.3, 82.1] # ms throughput = [0.8, 1.5, 2.9, 5.2, 9.1, 14.3, 19.8, 24.1] # K Token/s # 找到满足 P99 延迟约束的最大 Batch Size valid_configs = [] for i, bs in enumerate(batch_sizes): if tpot_p99[i] <= max_tpot_p99_ms: valid_configs.append({ 'batch_size': bs, 'tpot_p99': tpot_p99[i], 'throughput': throughput[i], 'can_meet_qps': throughput[i] >= target_qps / 1000, # 转换为 K/s }) if not valid_configs: raise ValueError("无配置满足 P99 < %dms 的 SLO 约束" % max_tpot_p99_ms) # 在满足 SLO 的配置中选择吞吐量最高的 best = max(valid_configs, key=lambda c: c['throughput']) # 根据 Batch Size 推导 vLLM 的 max-num-seqs max_seqs = best['batch_size'] * 2 # headroom 系数 return InferenceConfig( batch_size=best['batch_size'], max_num_seqs=max_seqs, tensor_para_size=8 # 70B on H100 × 8 ) # 示例: 交互式聊天的典型约束 cfg = search_optimal_config( target_qps=15.0, # 15 请求/秒 max_tpot_p99_ms=40.0, # Token 延迟 P99 < 40ms ) # 输出: batch_size=16, max_num_seqs=32四、Pareto 前沿移动的技术手段
投机解码(Speculative Decoding):在不改变主模型 Forward Pass 延迟的情况下,将有效 TPOT 降低 40%~60%(原 25ms → 实际感知 12ms)。这意味着相同的延迟 SLO 下可以增大 Batch Size(从 16 → 32),吞吐提升 60%~80%。
Prefill-Decode 分离(Disaggregated Serving):将 Pre-fill 和 Decode 部署在不同的 GPU 组上。Prefill 节点使用大 Batch Size 加速 TTFT(计算密集型,受益于大 Batch),Decode 节点使用小 Batch Size 降低 TPOT(带宽密集型,大 Batch 无益)。这一架构将两个阶段的 Pareto 边界解耦,整体延迟-吞吐前沿向外推移。
FP8 量化:H100 的 FP8 Tensor Core 吞吐是 BF16 的 2 倍(624 vs 312 TFLOPS)。将权重和 KV Cache 量化到 FP8 后,相同延迟下的有效吞吐翻倍——这相当于整条 Pareto 边界向右移动了 2 倍。
五、总结
推理延迟与吞吐的 Pareto 前沿是由 GPU 架构的物理参数决定的不可违背的约束。核心公式:Batch Size 增大 → SM 利用率升(吞吐升)→ 每请求计算量稀释(TPOT 升)→ TPOT 超过延迟 SLO 时即为可行性边界。最优 Batch Size 是在延迟 SLO 约束下的最大吞吐点——而非纯粹的最大吞吐或最小延迟点。
生产优化策略:基准测试绘制完整的 Pareto 前沿(batch_size: 1~128),在 SLO 约束下选择最优 Batch Size,通过投机解码和 FP8 量化将整体边界向外移动。最关键的一点:延迟 SLO 必须是由用户体验研究定义的,而非工程师武断设定的——25ms 和 40ms 的 TPOT 在用户感知中的差异需要通过 A/B 测试验证,而非通过直觉判断。