4. 性能分析工具:采集vLLM吞吐瓶颈的第一手数据
在定位吞吐瓶颈时,最忌讳的就是“拍脑袋”猜测。GPU利用率低,究竟是显存带宽受限、计算单元闲置,还是CPU来不及喂数据?KV Cache命中率低,是调度策略问题还是显存容量不足?这些问题必须依靠分层采集的性能数据来回答。本章围绕vLLM内置监控、Profiling工具和系统级工具三个层次,讲解如何建立一套完整的性能数据采集体系。
4.1 核心原理:三层数据采集模型
vLLM推理系统的性能瓶颈可能出现在GPU计算层、显存管理层、CPU调度层或网络通信层。单一工具只能看到局部,必须分层采集、交叉验证。下图展示了三层工具的分工与配合关系:
三层工具并非互相替代,而是从不同时间粒度和抽象层级回答不同问题。vLLM内置监控告诉你“服务有多慢”,Profiling告诉你“GPU在忙什么”,系统级工具告诉你“CPU和GPU之间谁在拖后腿”。
4.2 方法流程:从粗粒度到细粒度的诊断路径
第一步:vLLM内置监控——建立吞吐基线
启动vLLM服务时,通过 --metrics-port 暴露Prometheus格式的监控端点。核心指标包括:
- vllm:num_requests_running:当前正在执行的请求数,反映GPU实际负载
- vllm:cache_usage:KV Cache使用率,接近1.0说明显存吃紧,可能触发swap
- vllm:time_to_first_token_seconds:TTFT直方图,反映prefill阶段延迟
- vllm:time_per_output_token_seconds:TPOT直方图,反映decode阶段吞吐
- vllm:e2e_request_latency_seconds:端到端延迟,包含排队等待时间
采集命令示例:
# 启动vLLM时开启metrics
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3.1-8B-Instruct \
--gpu-memory-utilization 0.9 \
--max-num-seqs 256 \
--metrics-port 8001
# 从另一终端采集指标
curl -s http://localhost:8001/metrics | grep -E "vllm:(num_requests_running|cache_usage|time_per_output_token)"
num_requests_running 长期低于 max-num-seqs,说明服务端没有足够的并发请求来填满GPU;如果 cache_usage 持续接近1.0,则说明KV Cache容量是限制吞吐的瓶颈,需要调整 --max-num-seqs 或 --gpu-memory-utilization。
第二步:Profiling工具——定位算子级瓶颈
当内置监控确认GPU利用率不高但吞吐上不去时,需要用Profiling工具查看GPU内部到底在做什么。推荐使用PyTorch Profiler进行快速采样:
import torch
from torch.profiler import profile, ProfilerActivity
# 在vLLM的推理循环中包裹关键步骤
with profile(
activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
record_shapes=True,
profile_memory=True,
with_stack=True
) as prof:
# 执行一次batch推理(模拟vLLM的prefill+decode)
output = model.generate(input_ids, max_new_tokens=128)
# 按CUDA时间排序,输出Top 10耗时算子
print(prof.key_averages().table(
sort_by="cuda_time_total",
row_limit=10,
top_level_events_only=True
))
输出中重点关注两类算子:
- 访存密集型算子(如
reshape、copy、concat):如果这些算子占比高,说明数据搬运开销大,可能由KV Cache碎片化或连续张量拼接引起 - 计算密集型算子(如
matmul、attention):如果这些算子占比高但GPU利用率低,可能是batch size太小,未充分占用SM
第三步:系统级工具——排除CPU与I/O瓶颈
使用 nvidia-smi 和 perf 检查GPU与CPU的协同状态:
# 每0.5秒刷新一次GPU状态
watch -n 0.5 nvidia-smi --query-gpu=utilization.gpu,utilization.memory,power.draw,temperature.gpu,pcie.link.gen.current,pcie.link.width.current --format=csv
# 采集CPU热点(需要root权限)
sudo perf top -p $(pgrep -f vllm) --sort comm,dso
# 检查Python GIL竞争(如果使用py-spy)
py-spy record -p $(pgrep -f vllm) -o cpu_profile.svg --duration 30
关键判断逻辑:
| 现象 | 可能瓶颈 | 下一步动作 |
|---|---|---|
| GPU利用率高(>95%)但吞吐低 | 计算kernel本身效率低或访存模式差 | 用Nsight Compute分析kernel的memory throughput |
| GPU利用率波动大(30%~80%跳变) | CPU来不及准备batch数据 | 检查CPU线程数、--num-scheduler-steps是否过小 |
| GPU利用率低(<30%)且显存占用高 | KV Cache碎片化或等待swap | 查看vLLM日志中的swap次数,调整block大小 |
| PCIe吞吐持续打满 | CPU与GPU间数据拷贝频繁 | 检查是否启用了zero-copy或pinned memory |
4.3 案例:一次真实的多层诊断过程
某团队部署Llama-3.1-70B服务,目标吞吐500 tokens/s,实际仅达到180 tokens/s。他们按以下流程排查:
- vLLM内置监控:发现
num_requests_running平均只有32(max-num-seqs设为128),cache_usage仅0.4。说明GPU远未满载,且KV Cache还有大量空闲。 - PyTorch Profiler:发现
aten::copy_算子占CUDA总时间的28%,reshape占12%。大量时间花在张量搬运而非计算上。 - nvidia-smi:GPU利用率仅35%,但显存带宽利用率达到82%。说明kernel是访存受限而非计算受限。
- perf:CPU侧发现
memcpy系统调用频繁,且vLLM的Python调度线程占用单核100%。
最终定位:vLLM默认的block大小为16个token,当请求长度差异大时,KV Cache碎片化严重,导致每次attention都要做大量非连续内存的拷贝。将 --block-size 调整为32,并开启 --enable-prefix-caching 后,吞吐提升至420 tokens/s。
4.4 误区与复盘:数据采集中的常见陷阱
nvidia-smi 中的 utilization.memory 和Profiling中的 memory throughput 综合判断。
torch.profiler 的 schedule 参数设置on/off周期,减少干扰。
top 或 perf),并关注 vllm:num_requests_waiting 指标——如果等待队列长期非空,说明CPU调度速度跟不上GPU消费速度。
掌握三层工具的使用,等于为vLLM装上了“CT机”。下一层的数据回答“是什么”,上一层的数据回答“为什么”,交叉验证后,吞吐瓶颈的根因往往无处遁形。在实际调优中,建议按照“内置监控 → Profiling → 系统工具”的顺序逐层深入,每层都记录数据后再决定是否进入更细粒度的分析,避免一上来就陷入算子级排查的泥潭。