4.1 压测摸清上限:并发、延迟与显存的实测与拐点定位 把 vLLM 跑起来,只是万里长征第一步。真正让你睡得着的,是你知道它在什么水位会开始变慢、在什么水位会直接 OOM、以及「我能接住多少并发」这个结论是用数据算出来的,而不是凭感觉猜的。 本章要替你踩的坑,正是「部署成功 ≠ 能扛住流量」。我在内网第一次把 GLM-5.2 的推理服务亮给业务方时,压测一上来就翻车:并发到 32 路,P99 延迟从 800 毫秒直接飙到 12 秒,接着 Pod 被 OOMKilled 重启,重启又触发冷启动,雪崩。这一节,我们就把「它到底能扛多少」这件事,用一套可复用的压测方法讲透。 为什么压测不是「curl 一下」就完事 很多团队对压测的认知停留在「写个 for 循环并发请求」。
把 vLLM 跑起来,只是万里长征第一步。真正让你睡得着的,是你知道它在什么水位会开始变慢、在什么水位会直接 OOM、以及「我能接住多少并发」这个结论是用数据算出来的,而不是凭感觉猜的。
本章要替你踩的坑,正是「部署成功 ≠ 能扛住流量」。我在内网第一次把 GLM-5.2 的推理服务亮给业务方时,压测一上来就翻车:并发到 32 路,P99 延迟从 800 毫秒直接飙到 12 秒,接着 Pod 被 OOMKilled 重启,重启又触发冷启动,雪崩。这一节,我们就把「它到底能扛多少」这件事,用一套可复用的压测方法讲透。
很多团队对压测的认知停留在「写个 for 循环并发请求」。问题在于:LLM 推理是计算密集且显存受限的服务,它的吞吐曲线不是线性的,而是有明显的「饱和拐点」。在拐点之前,加并发几乎不掉速;过了拐点,延迟指数级恶化,再往上就是 OOM 或请求被拒。
所以压测的第一性原理是:找到拐点,并在拐点左下方留安全水位。这决定了你该给 Pod 配多少显存、vLLM 该开多大的 --max-num-seqs、以及要不要上多副本。我见过太多团队把「单路延迟 800ms」直接乘以「我要 100 并发」得出结论,结果上线当天就崩——因为单路延迟和饱和并发下的延迟根本不是同一个数量级。
对于 GLM-5.2 这类聊天/生成模型,我首推两类工具组合:
benchmark_serving.py(位于 vLLM 仓库的 benchmarks 目录下)最贴近真实引擎行为。它能精确统计首 Token 延迟(TTFT,Time To First Token)、每输出 Token 间隔(ITL,Inter-Token Latency)、请求级吞吐(requests/s)和 Token 级吞吐(tokens/s)。它直接打你的 K8s Service 地址,所以测得的就是「线上形态」的性能。常用参数包括指定后端为 openai、传入服务地址、选定数据集、设定目标请求速率(request-rate)与请求总数(num-prompts)。具体参数以你使用的 vLLM 版本为准,建议结合官方文档核对。我的判断:先用基准脚本标定引擎上限,再用真实流量回放验证「你的业务形态下」的实际水位。两者差值,就是你要为「长尾 prompt」预留的缓冲。举个例子:基准脚本用固定 512 输入/256 输出测得 32 路仍平稳,但回放真实日志(含大量 4k 输入的长 prompt)时 16 路就显存打满——这说明你的真实容量远低于基准数字,必须以回放为准对外承诺。
我见过最浪费时间的做法,是一上来就 256 并发梭哈,结果直接 OOM,什么有效数据都没拿到。正确姿势是等比数列梯度 + 每档稳定观测:
nvidia-smi 或 DCGM 采样)。这里有个关键细节:输出长度对延迟的影响远大于输入。生成 2048 token 比生成 256 token 慢一个数量级,因为 decode 阶段是逐 token 串行计算。所以压测的 output 长度必须贴近你业务的实际生成长度,否则数字毫无参考价值。如果你的业务是「短问答」(output 128),就别用 2048 输出压测;如果是「长文生成」(output 2k),就必须用 2k 输出,否则你会严重高估容量。
光看延迟不够,显存才是先行指标。两个抓手:
nvidia-smi:重点看 memory.used 与 memory.total 的比值。vLLM 启动后会预先向显存申请一块 KV Cache 池(由 --gpu-memory-utilization 决定占比),这块是常驻的;推理时实际占用会在此基础上浮动。你要观测的是:随并发升高,memory.used 是否逼近 memory.total——一旦到 95%+,下一档基本就是 OOM 或被迫丢弃请求。dcgm-exporter + Prometheus 采集。重点计数器包括显存带宽利用率、SM 利用率、显存占用等。如果你的 SM 利用率长期低于 40% 但延迟却很高,说明瓶颈不在算力,而在调度或显存碎片——这是非常典型的「KV Cache 争抢」信号,下一步该调的是 --max-num-seqs 而非加卡。我建议把 nvidia-smi 采样做成每 1 秒一次的旁路采集脚本,压测结束后拉出显存曲线叠加在延迟曲线上——显存触顶永远早于延迟崩塌,它是最可靠的预警线。
把每档的 P99 延迟和显存画成曲线,拐点非常明显:
nvidia-smi 显示显存占用在某一档后逼近 100%,且出现 requests 排队(vLLM 的 --max-num-seqs 决定并发排队上限)。显存先满,延迟才崩,所以显存是先行指标。我的经验公式:以「P99 仍在 SLA 内、显存占用 ≤ 85%、成功率 100%」的最高档为基准容量,然后按 70%~80% 规划生产水位。例如 24 路是基准容量上限,你就对外承诺 16~19 路并发,留 20%+ 余量扛突发和长尾。这个余量不是浪费,而是为「某路请求用了超长上下文」「某时段突发流量」兜底——没有余量的线上服务,一次长尾就能把你打挂。
拿到拐点数据后,回到配置层:
--max-num-seqs(并发序列上限):拐点往往就发生在这个值附近。如果你的拐点并发是 24,就把该参数设成略高于舒适区(如 32),但不要拉到 OOM 档。它是「超过就排队」的保险丝,比 OOM 优雅得多。--gpu-memory-utilization:显存先满导致崩,适当下调(如 0.85→0.80)可换来更稳的排队而非 OOM,代价是 KV Cache 池略小、可缓存的并发略少。这是「稳」与「容量」的取舍。--max-model-len:长上下文 1M 是 GLM-5.2 的卖点,但预留 1M 的 KV Cache 池会吃掉巨量显存(KV Cache 随 max-model-len 线性增长)。实测中绝大多数业务用不到满长,按业务真实上限(如 32k~128k)设置,能释放大量显存给并发。这是对「显存预算」影响最大的一个旋钮。单卡单 Pod 到顶了怎么办?两条路:
但要清醒:多副本是线性加算力的钱,真正的瓶颈常在显存和单卡算力,不是副本数。压测要分别测「单副本天花板」和「N 副本线性度」,确认加副本确实按比例提升吞吐,而不是被某处锁/网络拖成边际递减。如果 2 副本只换来 1.3 倍的吞吐,说明瓶颈在共享资源(如节点网络带宽、存储 I/O),加副本是浪费钱。
--max-num-seqs 是否过小、是否该开 prefix caching。--gpu-memory-utilization 或 --max-model-len;确认没有别的进程偷占显存。最后给你一个可直接填的模板(数字为示意量级,请以你的实测为准):
| 并发档 | P50 延迟 | P99 延迟 | 吞吐 tok/s | 显存占用 | 成功率 | 判定 |
|---|---|---|---|---|---|---|
| 4 | 0.6s | 1.0s | 220 | 62% | 100% | 舒适 |
| 16 | 0.9s | 1.1s | 760 | 78% | 100% | 舒适 |
| 24 | 1.8s | 2.8s | 880 | 85% | 100% | 临界 |
| 32 | 4.5s | 9.0s | 910 | 97% | 98% | 拐点 |
| 48 | 12s | OOM | — | 100% | 80% | 红线 |
读这张表的方法:24 路是临界上线,生产水位定在 16~19 路,单副本天花板约 880 tok/s。要扛更高并发,上多副本而非硬顶单 Pod。
读完这一节,你应该能用一句话说清:压测的本质是用梯度并发找到延迟/显存的饱和拐点,再在拐点下方留 20%+ 安全水位去规划生产容量。具体动作是:基准脚本标引擎上限、真实流量补业务形态、按等比数列梯度记录 P99 与显存、用显存先做先行指标、最后反推 --max-num-seqs / --gpu-memory-utilization / --max-model-len 三个旋钮。下一节,我们讲怎么把这些容量结论落成「灰度发布 + 生产化监控 + 回滚」的闭环。