Concurrency Sweep · RTX 4090

Qwen2.5-7B 并发曲线

固定 1024 输入 / 256 输出,并发从 1 扫到 64,共 1,332 条请求全部成功。 这条曲线回答一个问题:这张卡上,吞吐要涨到多少才开始拿延迟去换。

GPU
RTX 4090 · 24 GB
vLLM
0.27.1 · V1
Torch
2.13.0+cu130
Dtype
bfloat16
In / Out
1024 / 256 tok
KV 池
4.64 GiB
KV 容量
86,896 tok
失败请求
0 / 1332

结论拐点在 C=8 到 C=16 之间

吞吐一路涨到 C=64 都没有真正饱和,但"每多给一个并发槽换来多少 token/s"从 C=8 起就掉得很快。 真正划线的不是吞吐曲线,是 ITL 的尾部 —— 它在 C=8 那一点突然裂开。

推荐运行点

C = 16  ·  683 tok/s

吞吐达到单流的 11.1 倍,p50 ITL 仍只有 17.4 ms(比单流慢 9%),p99 TTFT 1.39 秒。 再往上走,C=32 多拿 51% 吞吐,代价是 p99 TTFT 翻一倍到 2.85 秒。

单流实测 / 理论上限

92.7 %

61.5 vs 66.3 tok/s,decode 已贴住显存带宽

C=64 吞吐增益

22.8 ×

用 64 倍并发换来,扩展效率 35.7%

ITL p99 / p50 最大比

10.3 ×

C=16 处,均值完全看不见这个裂口

C=64 时 KV 池占用

94.3 %

真实并发天花板只有 67 条,不是 128

曲线吞吐与延迟的兑换率

横轴是实际产出的 token/s,纵轴是 p99 首字延迟(对数)。曲线越往右越贵 —— 从 C=1 走到 C=16 花了 1.28 秒 p99 TTFT 买到 11 倍吞吐;从 C=32 走到 C=64 只多买到 36% 吞吐,却付出了 2.9 秒。

Figure 1

吞吐 – 延迟权衡曲线

p99 TTFT p50 TTFT 各并发点

灰色横带是三档常见 SLO。p50 与 p99 在 C=8 之后急剧分离,说明高并发下的慢请求 不是整体变慢,而是少数请求被排到了后面。

效率并发翻倍,吞吐没有翻倍

双对数坐标下,理想线性扩展是一条直线。实测曲线从 C=4 开始明显偏离, 到 C=64 时只剩理想值的 35.7%。

Figure 2

扩展效率:实测 vs 理想线性

实测吞吐 理想线性(61.5 × C)

点旁标注为该点的扩展效率。

每多一个并发槽,还能换来多少

这是判断拐点最直接的量。第一个并发槽带来 56.6 tok/s,到 C=32→64 那一段, 每个槽只剩 11.6 tok/s —— 边际收益衰减到五分之一。

Figure 3

边际吞吐收益(每新增一个并发槽)

柱高 = 该区间内新增吞吐 ÷ 新增并发数。虚线为 C=1 的基准值 61.5 tok/s。

断点ITL 的尾部在 C=8 裂开

这是整轮压测里最值得看的一张图,也是"P99 ITL 才暴露抖动"这句话最干净的实证。 p50 ITL 从 C=1 到 C=64 只从 15.9 ms 涨到 22.7 ms —— 涨了 43%,温和得看不出问题。 但 p99 ITL 在 C=8 那一点从 17.4 ms 直接跳到 99.6 ms,一步跳了 5.7 倍。

Figure 4

ITL 分位数分裂(对数纵轴)

p50 p90 p99 均值(= TPOT)

阴影区是 p50 与 p99 之间的跨度。C=4 时几乎看不见,C=8 之后张开成一个数量级。 注意均值线始终贴着 p50 与 p99 之间 —— 它两头都不代表。

这不是抢占

服务端日志里 preempt 出现 0 次,Waiting 队列最长只有 3 条。 所以尾部延迟不是 KV 不够导致的请求被踢出重算。

真正的原因是 enable_chunked_prefill=True(日志确认): 新请求的 prefill 被切块后混进正在解码的批次。 对已经在解码的请求来说,那一步要等 prefill 块算完,于是出现一个 100–200 ms 的停顿。 C=64 时 p90 ITL 已达 188 ms —— 超过一成的 token 间隔卡在别人的 prefill 上。

这些停顿最终反映在端到端时间上

Figure 5

E2EL 构成:首字等待 vs 解码耗时(p50)

TTFT(首字等待) 解码 255 个 token

解码段从 4.06 秒涨到 10.92 秒,是 E2EL 增长的主体。TTFT 虽然 p99 很难看, 但在中位数上始终只占总时长的 3%–7%。

天花板真正的上限是 67,不是 128

服务端启动参数写的是 --max-num-seqs 128,但那只是名义上限。 实际能同时跑多少条,由 KV cache 池除以单请求占用决定。

# 服务端日志实测
Available KV cache memory: 4.64 GiB
GPU KV cache size:         86,896 tokens

# 单请求占用 = 输入 + 输出
1024 + 256 = 1,280 tokens

# 真实并发上限
86,896 ÷ 1,280 = 67.9 条
Figure 6

各并发点的 KV cache 池占用

C=64 需要 81,920 tokens,占池 94.3% —— 与服务端观测到的峰值 92.0% 吻合。 被注释掉的 C=128 需要 163,840 tokens,是池子的 1.9 倍,根本跑不起来。

有 1.48 GiB 显存没被用上

显存账:权重 + non-torch 15.49 GiB,峰值激活 1.04 GiB,CUDA Graph 0.34 GiB, KV 池 4.64 GiB,合计 21.51 GiB。而 --gpu-memory-utilization 0.90 给出的预算是 21.17 GiB,卡上总共有 23.52 GiB。

vLLM 自己在日志里给了建议:--kv-cache-memory=6569114112(6.12 GiB)。 照做能把池子扩到约 114,600 tokens,并发上限从 67 提到 89, 而且不需要动 gpu-memory-utilization。

数据完整测量结果

并发请求数耗时 s 输出 tok/s效率 TTFT p50TTFT p99 TPOT p50 ITL p50ITL p99 E2EL p50KV 占用
延迟单位 ms。效率 = 吞吐 ÷ (C × 61.48)。KV 占用 = C × 1280 ÷ 86896。

选点按 SLO 反查配置

如果 SLO 是选并发能拿到吞吐实际 p99 TTFTp50 ITL
高亮行为推荐默认值。

下一步该改什么