4.1 压测摸清上限:并发、延迟与显存的实测与拐点定位


文档摘要

4.1 压测摸清上限:并发、延迟与显存的实测与拐点定位 把 vLLM 跑起来,只是万里长征第一步。真正让你睡得着的,是你知道它在什么水位会开始变慢、在什么水位会直接 OOM、以及「我能接住多少并发」这个结论是用数据算出来的,而不是凭感觉猜的。 本章要替你踩的坑,正是「部署成功 ≠ 能扛住流量」。我在内网第一次把 GLM-5.2 的推理服务亮给业务方时,压测一上来就翻车:并发到 32 路,P99 延迟从 800 毫秒直接飙到 12 秒,接着 Pod 被 OOMKilled 重启,重启又触发冷启动,雪崩。这一节,我们就把「它到底能扛多少」这件事,用一套可复用的压测方法讲透。 为什么压测不是「curl 一下」就完事 很多团队对压测的认知停留在「写个 for 循环并发请求」。

4.1 压测摸清上限:并发、延迟与显存的实测与拐点定位

把 vLLM 跑起来,只是万里长征第一步。真正让你睡得着的,是你知道它在什么水位会开始变慢、在什么水位会直接 OOM、以及「我能接住多少并发」这个结论是用数据算出来的,而不是凭感觉猜的。

本章要替你踩的坑,正是「部署成功 ≠ 能扛住流量」。我在内网第一次把 GLM-5.2 的推理服务亮给业务方时,压测一上来就翻车:并发到 32 路,P99 延迟从 800 毫秒直接飙到 12 秒,接着 Pod 被 OOMKilled 重启,重启又触发冷启动,雪崩。这一节,我们就把「它到底能扛多少」这件事,用一套可复用的压测方法讲透。

```mermaid flowchart TD A[明确目标水位
预期并发/延迟 SLA] --> B[选压测工具
基准 vs 真实流量] B --> C[设计梯度
并发 1→8→16→32→64] C --> D[记录指标
吞吐/延迟/显存/排队] D --> E[定位拐点
延迟陡增 or OOM] E --> F[反推配置
max-num-seqs/gpu-mem/副本数] F --> G[留安全水位
按 70%~80% 容量规划] ```

为什么压测不是「curl 一下」就完事

很多团队对压测的认知停留在「写个 for 循环并发请求」。问题在于:LLM 推理是计算密集且显存受限的服务,它的吞吐曲线不是线性的,而是有明显的「饱和拐点」。在拐点之前,加并发几乎不掉速;过了拐点,延迟指数级恶化,再往上就是 OOM 或请求被拒。

所以压测的第一性原理是:找到拐点,并在拐点左下方留安全水位。这决定了你该给 Pod 配多少显存、vLLM 该开多大的 --max-num-seqs、以及要不要上多副本。我见过太多团队把「单路延迟 800ms」直接乘以「我要 100 并发」得出结论,结果上线当天就崩——因为单路延迟和饱和并发下的延迟根本不是同一个数量级。

```mermaid graph LR subgraph 健康区[拐点左侧·线性区] H1[并发 1-16
延迟平稳] end subgraph 拐点区[饱和拐点] H2[并发 24-32
延迟陡增] end subgraph 危险区[过饱和] H3[并发 48+
OOM/排队雪崩] end H1 --> H2 --> H3 ```

压测工具的取舍:基准脚本 vs 生产流量回放

对于 GLM-5.2 这类聊天/生成模型,我首推两类工具组合:

  • 基准吞吐:用 vLLM 自带的 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 版本为准,建议结合官方文档核对。
  • 真实流量回放:用 Locust、k6 或自研脚本,按业务日志里的 prompt 长度分布和并发曲线重放。基准脚本的 prompt 往往是固定长度,而真实流量的 prompt 长短不一,对 KV Cache 压力差异巨大,必须用真实分布补一刀。

我的判断:先用基准脚本标定引擎上限,再用真实流量回放验证「你的业务形态下」的实际水位。两者差值,就是你要为「长尾 prompt」预留的缓冲。举个例子:基准脚本用固定 512 输入/256 输出测得 32 路仍平稳,但回放真实日志(含大量 4k 输入的长 prompt)时 16 路就显存打满——这说明你的真实容量远低于基准数字,必须以回放为准对外承诺。

```mermaid sequenceDiagram participant T as 压测客户端 participant S as K8s Service participant V as vLLM Pod participant G as GPU 显存 T->>S: 并发 N 路请求 S->>V: 路由到 Pod V->>G: 分配 KV Cache G-->>V: 显存占用上升 V-->>T: 流式返回 token T->>T: 记录 TTFT/ITL/吞吐 Note over G: 显存打满 → 后续请求排队或 OOM ```

设计梯度:别一上来就拉满

我见过最浪费时间的做法,是一上来就 256 并发梭哈,结果直接 OOM,什么有效数据都没拿到。正确姿势是等比数列梯度 + 每档稳定观测

  1. 并发档位建议:1、4、8、16、24、32、48、64。
  2. 每档至少跑 2–3 分钟,让 vLLM 的调度器进入稳态(前 30 秒有冷启动和 KV Cache 预热噪声,数字会虚高)。
  3. 每档记录:平均 / P50 / P95 / P99 延迟、请求成功率、Token 吞吐、GPU 显存占用(用 nvidia-smi 或 DCGM 采样)。
  4. 用固定长度的输入/输出(如 input 512、output 256)先标定,再用真实分布复测。

这里有个关键细节:输出长度对延迟的影响远大于输入。生成 2048 token 比生成 256 token 慢一个数量级,因为 decode 阶段是逐 token 串行计算。所以压测的 output 长度必须贴近你业务的实际生成长度,否则数字毫无参考价值。如果你的业务是「短问答」(output 128),就别用 2048 输出压测;如果是「长文生成」(output 2k),就必须用 2k 输出,否则你会严重高估容量。

```mermaid pie title 单次请求延迟构成(示意量级) "Prefill 计算(输入处理)" : 15 "Decode 逐 token 生成" : 70 "排队与调度等待" : 10 "网络与流式开销" : 5 ```

怎么读显存:nvidia-smi 与 DCGM 看什么

光看延迟不够,显存才是先行指标。两个抓手:

  • nvidia-smi:重点看 memory.usedmemory.total 的比值。vLLM 启动后会预先向显存申请一块 KV Cache 池(由 --gpu-memory-utilization 决定占比),这块是常驻的;推理时实际占用会在此基础上浮动。你要观测的是:随并发升高,memory.used 是否逼近 memory.total——一旦到 95%+,下一档基本就是 OOM 或被迫丢弃请求。
  • DCGM(NVIDIA 数据中心 GPU 管理器):在 K8s 里通常用 dcgm-exporter + Prometheus 采集。重点计数器包括显存带宽利用率、SM 利用率、显存占用等。如果你的 SM 利用率长期低于 40% 但延迟却很高,说明瓶颈不在算力,而在调度或显存碎片——这是非常典型的「KV Cache 争抢」信号,下一步该调的是 --max-num-seqs 而非加卡。

我建议把 nvidia-smi 采样做成每 1 秒一次的旁路采集脚本,压测结束后拉出显存曲线叠加在延迟曲线上——显存触顶永远早于延迟崩塌,它是最可靠的预警线。

读数据:拐点在哪,一眼看出来

把每档的 P99 延迟和显存画成曲线,拐点非常明显:

  • 延迟拐点:P99 从平稳(如 1 秒)开始非线性抬升的那一档,就是并发上限。比如 16 路时 P99=1.1s,24 路时 P99=2.8s,32 路时 P99=9s——那么 24 路就是你要警惕的临界,16 路是舒适区。
  • 显存拐点nvidia-smi 显示显存占用在某一档后逼近 100%,且出现 requests 排队(vLLM 的 --max-num-seqs 决定并发排队上限)。显存先满,延迟才崩,所以显存是先行指标
  • 成功率拐点:出现 5xx / 超时 / 被拒请求的那一档,已经是红线。

我的经验公式:以「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)设置,能释放大量显存给并发。这是对「显存预算」影响最大的一个旋钮。
```mermaid flowchart LR A[拐点并发=24] --> B[max-num-seqs=32
超则排队而非 OOM] C[显存 85% 即满] --> D[gpu-mem-util=0.80
留安全垫] E[真实最长上下文=64k] --> F[max-model-len=65536
不预留 1M 空池] ```

多副本与横向扩展的边界

单卡单 Pod 到顶了怎么办?两条路:

  • 多副本 + Service 负载均衡:在一个 GPU 节点上起 2 个 Pod(时间切片共享),或跨节点扩副本,靠 K8s Service 把并发摊开。前提是请求无状态——vLLM 默认不跨请求共享 KV Cache(除非开启前缀缓存),所以多副本是「水平复制算力」,简单可靠。
  • Prefix Caching(前缀缓存):如果大量请求共享系统提示词(system prompt),开启 vLLM 的 prefix caching,让相同前缀的 KV Cache 复用,等效提升单 Pod 吞吐。这对「固定人设 + 用户追问」场景收益明显。

但要清醒:多副本是线性加算力的钱,真正的瓶颈常在显存和单卡算力,不是副本数。压测要分别测「单副本天花板」和「N 副本线性度」,确认加副本确实按比例提升吞吐,而不是被某处锁/网络拖成边际递减。如果 2 副本只换来 1.3 倍的吞吐,说明瓶颈在共享资源(如节点网络带宽、存储 I/O),加副本是浪费钱。

常见翻车与排查:压测数字不对劲时看哪

  • P99 飙但显存没满:通常是 KV Cache 争抢或前缀缓存未命中导致排队。查 --max-num-seqs 是否过小、是否该开 prefix caching。
  • 显存直接 OOM:下调 --gpu-memory-utilization--max-model-len;确认没有别的进程偷占显存。
  • 吞吐卡住但 GPU 利用率低:瓶颈在调度/网络/客户端并发能力不足,而非模型。先确认压测客户端本身能打满并发(别用单线程脚本测高并发)。
  • 首 Token 延迟(TTFT)极高、后续正常:prefill 阶段被长输入拖慢,考虑 chunked prefill 或限制单请求输入长度。

一套你可以直接复用的压测记录表

最后给你一个可直接填的模板(数字为示意量级,请以你的实测为准):

并发档 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。

```mermaid graph TD R[实测结论] --> S[单副本舒适并发 16
吞吐 ~760 tok/s] R --> T[临界 24 / 拐点 32] R --> U[生产水位=16~19 路
留 20%+ 余量] R --> V[扩容走副本
非顶单 Pod] ```

本节收尾:你带走了什么

读完这一节,你应该能用一句话说清:压测的本质是用梯度并发找到延迟/显存的饱和拐点,再在拐点下方留 20%+ 安全水位去规划生产容量。具体动作是:基准脚本标引擎上限、真实流量补业务形态、按等比数列梯度记录 P99 与显存、用显存先做先行指标、最后反推 --max-num-seqs / --gpu-memory-utilization / --max-model-len 三个旋钮。下一节,我们讲怎么把这些容量结论落成「灰度发布 + 生产化监控 + 回滚」的闭环。


发布者: 作者: 不智能的AI的小龙虾 转发
评论区 (0)
U