在线推理能扛多少请求:别用固定并发替代到达率压测

Jellow 编辑发布,AI 辅助生成并完成技术核对。 计算示例和实测结果会在文中分别说明。

假设固定并发压测显示一个副本能完成 19 req/s,而线上流量是 6 req/s,能否据此断定容量充足?不能。固定并发中的新请求由旧请求完成触发:服务越慢,客户端发得越慢;真实用户却不会因为 GPU 忙就自动停止到达。

本文要解决的工程问题是:给定请求长度分布、到达模式和延迟 SLO,单副本可以安全接收多大的外生到达率。相关算子、缓存与服务文章汇总在模型推理加速专题页;本文只讨论容量测量。资料和接口核对日期为 2026-09-11,实际复现仍应固定 vLLM 版本或提交哈希。

从张量形状看“一个请求”有多重

先假设普通稠密因果注意力、单请求、h 个注意力头,每头维度为 d_h;输入长度 L≥1,实际生成长度 O≥1

Prefill 中,QK 的形状都是 [h,L,d_h]。由缩放点积注意力 softmax(QKᵀ/√d_h)V 和因果遮罩可知,每层每头需要处理一个三角形。Transformer 原论文给出这一注意力定义。有效 query-key 点积数为:

A_prefill = h × L(L+1)/2

第一个输出 token 可以从 prefill 的 logits 取得。为了得到其余 O-1 个 token,decode 依次让形状 [h,1,d_h] 的 query 读取长度为 L+1L+O-1 的 KV Cache:

A_decode = h × [(O-1)L + O(O-1)/2]

所以每层有效点积总数为:

A(L,O) = h × [L(L+1)/2 + (O-1)L + O(O-1)/2]

每个点积约含 2d_h 次浮点运算。这个式子没有计入投影、MLP、内核填充、批处理和调度,因此不是实际延迟模型;它只解释为什么容量实验必须保留 (L,O) 的联合分布。

一个反例是 (L,O)=(2048,128)(512,1664)。两者都有 L+O-1=2175 个实际参与注意力的位置,每头总计都是 2175×2176/2=2,366,400 个有效点积;但前者的 prefill/decode 分别为 2,098,176/268,224,后者则为 131,328/2,235,072。总工作量相同,不代表 TTFT、TPOT 或批处理效率相同。

NVIDIA 的参数说明也要求按业务保留输入和输出长度分布:长输入主要增加 prefill 压力与 TTFT,长输出增加生成阶段压力与 ITL。固定长度测试可以用 ignore_eos 控制输出长度,但容量验收还应回放生产停止条件。若对 TTFT、TPOT、ITL 的口径不熟悉,可先看站内的推理指标入门

固定并发把等待移到了哪里

用单服务器 FCFS 队列隔离这个问题。第 i 个请求的预定到达时间、服务时间、开始时间和完成时间分别为 a_i、s_i、b_i、f_i

b_i = max(a_i, f_{i-1})

f_i = b_i + s_i

w_i = b_i - a_i

开放环压测先产生与完成事件无关的 a_i。在未条件化的泊松过程中,间隔独立服从 Exp(λ),其中 λ 是目标 RPS。闭环并发为 1 时,下一个请求恰在前一个完成后发出,因此 a_i=f_{i-1}w_i=0:它测到的是 1/E[s],没有测到外生 λ 下的排队尾延迟。

并发上限大于 1 时同样可能发生代价转移。信号量、网关或连接池会把多余请求留在客户端;若计时从真正发往模型服务时才开始,用户等待就从指标中消失。vLLM 将 --request-rate--max-concurrency 分开,并明确指出并发帽可能使实际执行率低于设定值。CLI 参数定义还提供 client_queue_timee2el_including_client_queue 来显示这部分等待。

MLPerf LoadGen 的 Server 场景同样采用随机泊松到达,允许请求保持 outstanding,并按延迟阈值判定;Offline 场景报告吞吐。MLPerf 官方实现说明描述的是两个不同问题,不能互相替代。

vLLM 的有限速率不是未条件化泊松过程

截至 2026-09-11,vLLM 文档把 --burstiness=1 描述为泊松到达,其他正值对应 Gamma 间隔;但源码还有一个必须说明的实现细节。

在固定有限 request-rate、没有 ramp-up、且没有使用 self-timed trace 的路径中,vLLM 会先采样 Gamma 间隔;当 burstiness=1 时,这一步确实是指数间隔。随后程序累加所有间隔,并统一乘以:

normalize_factor = (num_requests/request_rate) / last_cumulative_delay

因此最后一个请求的计划偏移会精确对齐 num_requests/request_rate当前 serve.py直接展示了采样与缩放逻辑。缩放以后,各间隔共同受总时长约束,不再是相互独立的未条件化指数变量;它与未条件化泊松过程存在实现差异。长时间测试中差异可能很小,但本文研究的正是到达过程,不能省略这个边界。

若生产突发和相关性很重要,最终验收应使用带真实时间戳的 timed_trace --self-timed,或使用能够按原始时间戳发送的外部负载发生器。本文后面的标准库例子使用未缩放的独立指数间隔,验证的是未条件化泊松队列,不是 vLLM 调度器的逐行复刻。

容量应由请求级联合 SLO 定义

令第 r 个独立重复运行中,第 i 个请求的联合指示量为:

I_{r,i} = 1[请求成功 且 TTFT≤B_f 且 TPOT≤B_d 且 E2E≤B_e]

未设置的约束可以删去,错误和超时必须记为 0。该次运行的达标率和合格请求吞吐为:

Â_r(λ) = Σ_i I_{r,i}/N_r

Ĝ_r(λ) = Σ_i I_{r,i}/T_r

分别查看 TTFT p99 和 TPOT p99 不等价于联合达标。两类违规若发生在不同请求上,各自有 99% 的请求通过时,联合通过率仍可能只有 98%。vLLM 对同一成功请求的全部 --goodput 条件执行逻辑 AND,再计算 good_completed/duration;它输出的是 request goodput,不是把错误纳入分母后的联合达标率。官方源码可以核对这一区别。DistServe 论文则把 per-GPU goodput 定义为达到 SLO attainment 目标时可承载的最大请求率。论文原文

FCFS 等待时间会向后传播,所以同一次运行中的 I_{r,i} 通常存在序列相关性。不能把几万个请求直接视为独立 Bernoulli 样本,再用逐请求 IID Wilson 区间决定 PASS/FAIL。

本文采用多个相互独立的完整运行。预先指定 R 组 seed,每次重新建立负载随机过程并按实验策略重置服务状态,以整次运行作为实验单位。定义保守下包络:

A_floor(λ) = min_{r=1..R} Â_r(λ)

只有每次运行都满足全部验收条件,候选 RPS 才通过:

λ* = max{λ | 对所有 r:Â_r≥q,错误率_r≤ε,实际发送率合格,且队列无持续上升}

这条 min 规则是预先声明的保守验收规则,不是“95% 置信下界”,也不声称具有某个未证明的覆盖率。如果无法做独立重启,可以按时间分块报告最差块,但块长度必须覆盖业务突发与队列相关时间;相邻小块不能被冒充为独立样本。

一个验证排队效应和重复判定的标准库例子

下面不是 GPU 实测。它运行 12 个相互独立的单服务器队列:80% 请求服务 20 ms,20% 服务 180 ms;E2E SLO 为 250 ms,目标达标率为 95%。不同重复分别生成服务序列与未缩放的泊松到达;不同 RPS 使用配对 seed,便于把负载随机性对齐后观察变化。

import math
import random

N, WARM, RUNS = 50_000, 5_000, 12
SLO, TARGET = 0.250, 0.95
SEEDS = tuple(range(101, 101 + RUNS))

def percentile(xs, p):
    ys = sorted(xs)
    return ys[max(0, math.ceil(p / 100 * len(ys)) - 1)]

def one_run(rate, seed):
    service_rng = random.Random(seed)
    arrival_rng = random.Random(seed + 10_000)
    service = [0.020 if service_rng.random() = WARM:
            waits.append(start - arrival)
            passed += (finish - arrival <= SLO)
    measured = N - WARM
    actual_rate = (measured - 1) / (arrival - first_measured_arrival)
    return percentile(waits, 95), passed / measured, actual_rate

closed_rates = []
for seed in SEEDS:
    rng = random.Random(seed)
    service = [0.020 if rng.random() = TARGET else "FAIL"
    print(f"{rate:>4} {rate*0.052:>4.2f} "
          f"{min(actuals):.2f}-{max(actuals):.2f} "
          f"{max(q95s)*1000:>14.1f} "
          f"{min(attains):.2%}-{max(attains):.2%} "
          f"{min(attains):>7.2%} {check}")

Python 3 标准库的实际输出为:

closed-loop: rate_range=19.07-19.43 req/s, min_attain=100.00%
rate rho actual_rps_range worst_p95q_ms attain_range       floor   slo_check
   2 0.10 1.99-2.01           64.2 98.85%-99.00%  98.85% PASS
   4 0.21 3.98-4.02          137.7 97.22%-97.50%  97.22% PASS
   6 0.31 5.96-6.03          170.7 94.70%-95.26%  94.70% FAIL
   8 0.42 7.95-8.04          232.7 90.84%-91.78%  90.84% FAIL
  10 0.52 9.94-10.05          314.9 85.52%-86.58%  85.52% FAIL
  14 0.73 13.91-14.07          673.9 64.74%-67.65%  64.74% FAIL
  18 0.94 17.89-18.10         3889.7 19.30%-26.52%  19.30% FAIL

固定并发基线在 12 次运行中得到 19.07—19.43 req/s,并且因为单次服务时间都小于 250 ms,最差达标率仍是 100%。开放环中,4 RPS 的 12 次运行全部超过 95%,而 6 RPS 的保守下包络只有 94.70%,因此这里只得到 4—6 RPS 的 SLO 夹点,不能把 4 RPS 杜撰成精确容量。

rho 使用理论均值 E[s]=0.8×0.020+0.2×0.180=0.052 s。即使平均利用率只有约 31%,服务时间方差和到达重叠也已让 6 RPS 的部分运行失守。这个模型没有模拟 GPU、网络或连续批处理;实际服务时间会随批次和调度变化,示例阈值不能搬到生产。

可复现的 vLLM 容量实验

先固定模型与软件版本、权重精度、并行度、调度参数、GPU 与驱动、客户端机器、网络、采样参数和缓存状态。数据集至少保留 (输入 token 数,实际输出 token 数,缓存命中条件) 的联合分布。

层次 负载 回答的问题
隔离基线 并发 1、低到达率 无排队时各长度桶的服务时间
饱和基线 request-rate=inf,扫描固定并发 峰值吞吐和批处理拐点
容量扫描 有限外生 RPS,先不设并发帽 队列和联合 SLO 在哪里失守
生产验收 真实时间戳、网关限制和缓存策略 突发、相关性及代价转移后的体验

截至核对日,下面命令可作为固定 (1024,128) 长度桶的模板。800/50 ms 是占位 SLO,必须替换;它没有在本文环境中连接模型服务,也不是 GPU 实测。

vllm bench serve \
  --backend openai \
  --base-url http://127.0.0.1:8000 \
  --model MODEL_ID \
  --dataset-name random \
  --input-len 1024 \
  --output-len 128 \
  --ignore-eos \
  --temperature 0 \
  --request-rate 8 \
  --burstiness 1.0 \
  --num-prompts 10000 \
  --num-warmups 32 \
  --seed 11 \
  --percentile-metrics ttft,tpot,e2el,client_queue_time,e2el_including_client_queue \
  --metric-percentiles 50,95,99 \
  --goodput ttft:800 tpot:50 \
  --save-result \
  --save-detailed

当前 vLLM 的一个 --seed 不是独立的“到达种子”。serve.py 用它设置 Python random 和 NumPy RNG;数据集代码又把同一个值用于随机请求生成、抽样或顺序洗牌,具体作用取决于数据集和 --disable-shuffle;NumPy RNG 还用于到达间隔调度。serve.py数据集源码共同给出了这一语义。

因此比较配置 A/B 时应使用配对设计:重复 1 的 A 和 B 都用 seed 11,重复 2 都用 seed 23,之后继续使用多组不同 seed。每一组 seed 是一个新的完整重复,同时改变该重复的请求随机性、顺序和到达调度;同一组中的 A/B 则共享这些随机条件。不能把换 seed 描述成只换“到达种子”。若必须固定请求序列而只改变到达时间,应准备固定顺序的数据或 timed trace,并由外部调度器控制时间戳。

不同重复之间还要按实验目标重置状态。vLLM 的基准说明提醒,重复请求可能命中上一次留下的前缀缓存并虚增吞吐;生产不依赖这种复用时,应重置缓存、重启服务或使用会重置缓存的 sweep 流程。

先粗扫得到一过一败的 RPS 夹点,再在夹点中加密。边界候选建议增加到至少 10—12 个预先指定的独立重复,并用更长生产时间窗复验。每次都要单独保存实际发送率、错误、联合达标率和队列时间序列,不能先合并所有请求再计算一个 IID 区间。

若生产网关限制并发,再增加相同的 --max-concurrency,同时报告客户端排队、用户视角 E2E、拒绝和超时。当前 --goodput 只接受 ttft、tpot、e2el,不接受客户端排队指标;有外部背压时必须从详细记录重新计算联合指示量,并把失败请求放回分母。

指标和排障顺序

一次容量扫描至少保存:设定与实际发送 RPS、成功/错误/超时数、客户端排队、TTFT/TPOT/E2E 的 p50/p95/p99、逐运行联合达标率及其最小值,并按 (L,O) 桶分解。

NVIDIA 的指标定义中,TTFT 通常包含排队、prefill 和网络,E2E 还覆盖 batching。数学上的 TPOT 只在实际输出 token 数大于 1 时定义,公式为 (E2E-TTFT)/(输出 token 数-1);当 O=1 时分母为零,应从 TPOT 统计中排除或明确采用工具约定。当前 vLLM 在 goodput 检查中把这类请求的 TPOT 置为 0,属于实现约定,不能当作数学定义。工具实现仍可能不同。根据 vLLM Benchmark CLI,推测解码一次流式返回多个 token 时,ITL 记录的是流式事件间隔,而 TPOT 会把时间摊到 token 上,两者尤其不能混用。

排障按请求生命周期进行:

  1. 负载发生器:实际发送率是否达到设定值;客户端 CPU、连接数或网络是否先饱和;排队是否藏在信号量之前。
  2. 工作量:核对实际 (L,O)、停止原因、采样参数、前缀命中率和失败请求,先排除两次实验负载不同。
  3. 服务队列:观察等待请求数和 queue time 是否持续上升。若上升,先降低 RPS 或增加副本,不要先调算子。
  4. 阶段拆分:队列平稳但 TTFT 变差,检查 prompt 长度、缓存命中和 prefill;TPOT 变差则检查 decode、活动序列数和长输出干扰。
  5. 资源证据:最后结合 KV Cache 使用率、抢占次数、GPU/CPU、显存带宽和网络判断瓶颈。vLLM 的生产指标清单提供等待请求数、queue time、prefill/decode 时间、KV Cache 使用率和抢占计数。

安全余量不是固定的 70% 或 80%。它取决于长度方差、突发程度、SLO、缓存和扩容反应时间。固定并发仍是有价值的饱和基线;只有开放环到达率、多个独立重复的联合 SLO、稳定队列和真实背压同时进入验收,测出的 RPS 才能用于容量决策。

继续阅读


本站的内容生成、审核与纠错说明

发表评论