背景
上周给客户部署一个本地知识库问答系统,用的是我自建的双卡服务器(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 8690rx/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 字段里。