vLLM 性能监控要看哪些指标
vLLM 服务跑起来,只是第一步。
真正上线前,还有一些内容:首 token 慢不慢、生成过程稳不稳、并发上来以后吞吐能不能涨、P99 会不会突然炸、KV Cache 有没有逼近显存上限。
这些问题如果没有监控,只靠“我感觉还行”,上线以后一定会吃亏。
先把服务用可控参数启动
按场景分了四类:基础验证、高吞吐、低延迟、省显存。这种拆法很实用,因为不同目标下参数取舍完全不一样。
比如高吞吐配置会倾向于增大并发序列数、提高显存利用率、开启前缀缓存:
# 中文注释:高吞吐模式通过增大 batch 和并发序列数,提高 tokens/s
python -m vllm.entrypoints.openai.api_server \
--model /root/autodl-tmp/models/qwen/Qwen3-0.6B \
--port 8000 \
--dtype float16 \
--max-model-len 4096 \
--max-num-seqs 64 \
--max-num-batched-tokens 4096 \
--gpu-memory-utilization 0.92 \
--enable-prefix-caching \
--trust-remote-code
如果目标是低延迟,就会更关注 Chunked Prefill 和 batch 上限:
# 中文注释:低延迟模式限制批处理规模,并开启分块预填充降低长 prompt 阻塞
python -m vllm.entrypoints.openai.api_server \
--model /root/autodl-tmp/models/qwen/Qwen3-0.6B \
--port 8000 \
--dtype float16 \
--max-num-seqs 16 \
--max-num-batched-tokens 512 \
--enable-chunked-prefill \
--gpu-memory-utilization 0.90 \
--trust-remote-code
Chunked Prefill(分块预填充):把长 prompt 的 Prefill 计算拆成小块,让它和 decode 请求交错执行。它解决的是长 prompt 独占 GPU、导致其他请求首 token 变慢的问题。
Prefix Caching(前缀缓存):复用相同 prompt 前缀的 KV Cache。多轮对话、固定系统提示词、RAG 模板都很适合开启。
TTFT 和 TPOT 要分开测
我不建议只看端到端耗时,因为它会把不同阶段混在一起。
TTFT(Time To First Token):首 token 延迟,主要反映 Prefill 阶段、排队时间和冷启动影响。
TPOT(Time Per Output Token):相邻输出 token 的时间间隔,主要反映 Decode 阶段是否稳定。
代码里用流式输出测这两个指标,思路很直接:记录请求开始时间,再记录每个 chunk 到达的时间。
# 中文注释:发送流式请求,逐个接收 token,并记录每个 token 到达时间
stream = client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
stream=True,
max_tokens=max_tokens,
temperature=0.7,
)
# 中文注释:第一个 token 的时间点用于计算 TTFT,后续相邻时间差用于计算 TPOT
for chunk in stream:
if chunk.choices[0].delta.content:
now = time.perf_counter()
token_timestamps.append(now)
有一个很好的细节:短 prompt 的 TTFT 反而最高,达到 355ms;中 prompt 和长 prompt 只有 40ms、63ms。
这看起来违反直觉,但原因是冷启动。第一个请求时 CUDA kernel 预热、内存分配、运行时初始化都会增加额外延迟。生产环境里通常会发预热请求,避免首批真实用户承担这段成本。
并发压测要找拐点
并发压测的目的不是证明“并发越高越好”,而是找到吞吐和尾延迟的拐点。
代码里的压测逻辑是用线程池发起多个请求,然后统计 QPS、tokens/s 和延迟分位数:
# 中文注释:用线程池模拟指定并发度,同时向 vLLM OpenAI 接口发送请求
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
futures = [
executor.submit(send_chat_request, prompt, max_tokens)
for prompt in prompts
]
# 中文注释:每个请求完成后收集结果,用于后续计算延迟分位数和吞吐量
for future in concurrent.futures.as_completed(futures):
all_results.append(future.result())
教结果很适合做基准判断:
| 并发度 | QPS | 吞吐量 tok/s | P50 延迟 | P99 延迟 |
|---|---|---|---|---|
| 1 | 1.02 | 101.0 | 899.8ms | 1453.9ms |
| 2 | 2.02 | 201.6 | 985.1ms | 1048.2ms |
| 4 | 3.85 | 385.4 | 1018.2ms | 1128.3ms |
| 8 | 6.35 | 635.0 | 1037.3ms | 1101.5ms |
| 16 | 9.87 | 987.2 | 1520.6ms | 2085.3ms |
我会这样解读:并发从 1 到 8,吞吐提升明显,P50 基本稳定,这是 Continuous Batching 在发挥作用。并发到 16 时,吞吐还在涨,但 P50 和 P99 都明显上升,说明系统已经开始接近饱和。
如果这是线上服务,我不会只看最高 tokens/s。我会根据业务能接受的 P99,选择一个更稳的并发上限。
Prometheus 指标要接入常态监控
vLLM 可以暴露 Prometheus 格式的 /metrics。这比压测脚本的单次输出更适合长期观察。
Prometheus(普罗米修斯):常见的监控指标采集系统,服务暴露文本格式指标,Prometheus 定期抓取并存储,Grafana 可以基于这些数据画图。
代码里解析的关键指标包括:
# 中文注释:定义需要从 Prometheus 文本中提取的 vLLM 核心指标
patterns = {
"prompt_tokens_total": r"vllm:prompt_tokens_total[^\n]*?\s+([\d.]+)",
"generation_tokens_total": r"vllm:generation_tokens_total[^\n]*?\s+([\d.]+)",
"num_requests_running": r"vllm:num_requests_running\s+([\d.]+)",
"num_requests_waiting": r"vllm:num_requests_waiting\s+([\d.]+)",
"gpu_cache_usage_perc": r"vllm:gpu_cache_usage_perc\s+([\d.]+)",
}
我会重点看这些指标:
num_requests_running:当前正在执行的请求数。它能反映 batch 是否真正跑起来。
num_requests_waiting:排队等待请求数。如果长期增长,说明入口流量超过服务处理能力。
gpu_cache_usage_perc:GPU KV Cache 使用率。接近上限时,要警惕 swapping、OOM 或尾延迟上升。
prompt_tokens_total 和 generation_tokens_total:输入和输出 token 累计量。结合时间窗口可以估算吞吐。
time_to_first_token_seconds 和 time_per_output_token_seconds:分别对应 TTFT 和 TPOT 的分布。
负载前后对比显示,20 个并发请求让生成 token 从 21634 增加到 24634,增量 3000。持续监控时每 2 秒新增约 900 tokens,也就是实际吞吐约 450 tok/s。
这类时间序列比单次压测结果更有价值,因为它能看到负载期间是否抖动。
常见瓶颈可以按现象定位
如果 TTFT 过高,我会先看 prompt 长度、等待队列和 Prefill 是否阻塞 decode。
常见处理方式是开启 Chunked Prefill、开启 Prefix Caching,或者进一步做 PD 分离。
PD 分离(Prefill-Decode Disaggregation):把 Prefill 和 Decode 拆到不同资源或不同服务上执行。它解决的是长 prompt 的 Prefill 计算阻塞短请求 decode 的问题。
如果 TPOT 抖动大,我会看 batch size 是否波动太大、推测解码接受率是否下降、是否存在抢占或换出。
如果 GPU 利用率低,但显存占用很高,这通常不是“机器不努力”,而是 memory-bound。此时可以考虑增大 --max-num-seqs、使用 FP8 KV Cache、缩短上下文,或者在小 batch 场景启用推测解码。
FP8 KV Cache:用更低精度保存 KV Cache,目标是节省显存和提升吞吐。它通常会带来很小的精度影响,但生产上仍要结合业务结果验证。
小结
vLLM 调优不是一组固定参数,而是一套监控闭环。
我会按这个顺序做:
- 用基础配置确认服务能正常返回。
- 用流式请求拆开测 TTFT 和 TPOT。
- 用并发压测找到吞吐和 P99 的拐点。
- 用 Prometheus 长期观察队列、KV Cache、token 吞吐和延迟分布。
- 根据瓶颈选择高吞吐、低延迟、省显存或推测解码方案。
做到这一步,调优才不是玄学。每个参数为什么改、改完有没有收益,都能用指标说清楚。