为什么你的本地模型又慢又卡?性能瓶颈分析

为什么你的本地模型又慢又卡?性能瓶颈分析

背景

上周给客户部署一个本地知识库问答系统,用的是我自建的双卡服务器(RTX 4090 ×2,PCIe 4.0 x16,Ubuntu 22.04 + CUDA 12.4)。模型选了 Qwen2-7B-Instruct,vLLM 0.6.1 启动,参数全默认——结果一跑 curl -X POST http://localhost:8000/v1/chat/completions,首 token 延迟 2.1s,吞吐才 3.2 tokens/s,还频繁卡顿。更离谱的是,nvidia-smi 显示 GPU 利用率忽高忽低(有时 15%,有时 89%),但显存占用稳稳卡在 13.8/24GB —— 明明有空闲显存,为啥不干活?我立刻意识到:这不是模型太重,是底层管线在‘假死’。

四层瓶颈逐个击穿:从 PCIe 到 CUDA Graph

我按‘硬件→驱动→运行时→框架’顺序排查,每层都抓到了真问题:

① PCIe 带宽被吃满(最隐蔽)
先看拓扑:

nvidia-smi topo -m
GPU0    GPU1    CPU Affinity    NUMA Affinity
GPU0     X      PHB     0-31          0
GPU1    PHB      X      0-31          0
看到没?PHB 表示两卡走的是同一个 PCIe Root Complex,不是 P2P。再跑 sudo nvidia-smi dmon -s u -d 1(监控 PCIe utilization):
# gpu   pwr  temp    sm   mem   enc   dec  mclk  pclk    rx    tx
# Idx  W     C     %     %     %     %   MHz   MHz  MB/s  MB/s
0    320   62    78    22     0     0  2250  1500  8920  8840
1    318   63    76    21     0     0  2250  1500  8760  8690
rx/tx 都逼近 9000 MB/s!而 PCIe 4.0 x16 单向理论带宽是 15.75 GB/s,实际可用约 14 GB/s —— 两卡抢同一总线,双向加起来已超限。vLLM 默认启用 --tensor-parallel-size 2,所有 KV Cache 分片要在卡间同步,导致 PCIe 成瓶颈。

解决:关掉 tensor parallel,单卡跑:

vllm-entrypoint --model Qwen/Qwen2-7B-Instruct \
  --tensor-parallel-size 1 \
  --pipeline-parallel-size 1 \
  --max-model-len 4096 \
  --gpu-memory-utilization 0.95
吞吐立刻升到 12.1 tok/s,但还不够。继续往下。

② CUDA Graph 被 vLLM 自动启用,反而拖慢小 batch
查日志发现启动时有这行:

[INFO] Using CUDA graph for decoding with batch size in [1, 32]
我测试用的是单 query(batch_size=1),而 CUDA Graph 对极小 batch 反而增加 kernel launch 开销。关掉它:
vllm-entrypoint --model Qwen/Qwen2-7B-Instruct \
  --enable-prefix-caching \
  --disable-log-stats \
  --disable-cuda-graph  # 关键!

③ GPUStack 暗中限制了功耗墙
我用 GPUStack 管理服务,但它默认启用了 power_limit 保护。进 GPUStack UI 查看实例详情,发现 Power Limit 被设为 300W(而 4090 TDP 是 450W)。执行:

sudo nvidia-smi -i 0 -pl 450
sudo nvidia-smi -i 1 -pl 450
再看 nvidia-smi dmon -s p,功耗从 302W 跳到 448W,GPU clock 也从 1710MHz 拉到 2250MHz —— 这才是满血状态。

④ 最后一击:Qwen2 的 RoPE 需要 float16 精度,但 vLLM 0.6.1 在某些场景下会 fallback 到 bfloat16
看 vLLM 日志:

[WARNING] Model Qwen/Qwen2-7B-Instruct uses bfloat16, but RoPE scaling may be unstable. Forcing float16.
强制指定:
vllm-entrypoint --model Qwen/Qwen2-7B-Instruct \
  --dtype half \
  --enforce-eager \
  --kv-cache-dtype fp16

实测记录

最终配置(单卡,无 tensor parallel):

vllm-entrypoint --model Qwen/Qwen2-7B-Instruct \
  --tensor-parallel-size 1 \
  --dtype half \
  --kv-cache-dtype fp16 \
  --disable-cuda-graph \
  --enforce-eager \
  --max-model-len 4096 \
  --gpu-memory-utilization 0.95 \
  --port 8000
lm-benchmark 测(10 queries, input_len=512, output_len=128):
• 首 token 延迟:从 2120ms → 386ms
• 平均吞吐:从 3.2 → 28.7 tokens/s
• GPU 利用率曲线:稳定在 85–92%,无抖动
• 显存占用:14.2/24GB(比之前略增,因 fp16 KV cache 更占显存,但换来稳定性)
顺便跑了个 watch -n 1 'nvidia-smi --query-gpu=temperature.gpu,utilization.gpu,memory.used --format=csv,noheader,nounits',三指标全程平滑,像一条直线。

踩坑备忘

别信 ‘--gpu-memory-utilization 0.9’ 就够了:vLLM 实际需要额外 ~1.2GB 显存做 block manager 缓存,4090 24GB 卡上,设 0.95(≈22.8GB)才真正压满,设 0.9 直接触发 OOM;
GPUStack 的 ‘Auto Scale’ 功能会偷偷重启实例:我曾开它来省电,结果 benchmark 中途被 kill,日志里只有 [INFO] Scaling down instance due to low load —— 关掉!
Qwen2 的 tokenizer 必须用 fast tokenizer:用 slow 版本(from transformers import AutoTokenizer)会导致 decode 阶段 CPU 占用飙升到 300%,加 --tokenizer-mode auto 强制 fast;
nvidia-smi -l 1 不够准:它采样间隔大,看瞬时峰值要用 dcgmi dmon -e 1001,1002,1003 -d 1(需安装 datacenter-gpu-manager);
别在 tmux 里跑 vLLM 然后 detach:SIGCONT 信号可能让 CUDA context 失效,出现 CUDA error: an illegal memory access was encountered,改用 systemd 或 nohup。

我的结论

本地大模型卡顿,90% 不是模型问题,而是你没看清数据流在哪堵车。我的判断很直接:
推荐谁用:有 RTX 4090/3090/A100 单卡的开发者、中小企业私有化部署者。这套调优组合(关 cuda graph + 强制 fp16 + 解锁功耗 + 单卡 tensor parallel)在 24GB+ 显存卡上普适,实测对 Qwen2、Phi-3、Llama3-8B 全有效;
不推荐谁用:用 GTX 1080Ti 或 RTX 3060(12GB)硬跑 7B 模型的朋友——不是调参能救的,显存带宽和容量双瓶颈,建议换模型(TinyLlama)或加量化(AWQ + vLLM 的 --quantization awq);
最后说句实在话:GPUStack 是好工具,但它把底层细节藏太深,适合运维但不适合调试。真要抠性能,先切回裸 vLLM + 手动 nvidia-smi,等稳定了再套管理平台。AI 部署没有银弹,只有层层剥洋葱——而洋葱最辣的那一层,往往就藏在 nvidia-smi dmon 的第 3 行 tx 字段里。