背景
去年我给公司内部搭了个AI问答助手,用的是本地两台RTX 4090服务器(每台24GB显存),一开始直接 pip install qwen2 && python app.py ——结果上线第三天就因为依赖冲突崩了两次,一次是torch版本被同事升级搞挂,一次是transformers更新后Tokenizer加载失败。更崩溃的是,运维同事想在另一台机器复现环境,花了整整一天装CUDA、cuDNN、PyTorch……最后还卡在nvcc版本不匹配上。
我忍不了了,决定彻底转向容器化。不是为了赶时髦,而是——我要让一个没碰过CUDA的新同事,30分钟内把Qwen2-7B跑起来,且下次重启不丢状态、换机器不改代码。这篇就是我踩完所有坑后,写给当年那个抓耳挠腮的自己的备忘录。
三步走:拉镜像 → 启动vLLM → 挂API网关
我的最终方案是:vLLM 0.6.3(支持Qwen2原生FlashAttention-2) + 官方CUDA 12.4基础镜像 + nginx反向代理+HTTPS。不选Ollama(太黑盒)、不选Text Generation Inference(TGI对Qwen2 tokenization支持弱),vLLM启动快、显存省、HTTP API干净利落。
第一步:准备镜像(别用hub上那些不明来源的)
docker pull nvidia/cuda:12.4.1-devel-ubuntu22.04
# 我自己构建的精简版vLLM镜像(已预装flash-attn==2.6.3、vllm==0.6.3、qwen-tokenizer)
docker build -t vllm-qwen2:0.6.3-cu124 -f Dockerfile.vllm .
# Dockerfile.vllm内容如下(关键行已注释):
FROM nvidia/cuda:12.4.1-devel-ubuntu22.04
RUN apt-get update && apt-get install -y python3.10-venv git && rm -rf /var/lib/apt/lists/*
RUN python3.10 -m venv /opt/venv && /opt/venv/bin/pip install --upgrade pip
# 必须指定torch版本,否则vLLM编译失败
RUN /opt/venv/bin/pip install torch==2.3.1+cu121 torchvision==0.18.1+cu121 --extra-index-url https://download.pytorch.org/whl/cu121
RUN /opt/venv/bin/pip install vllm==0.6.3 flash-attn==2.6.3 --no-build-isolation
# 加入Qwen2 tokenizer补丁(否则Qwen2-7B-Instruct加载报错)
RUN /opt/venv/bin/pip install git+https://github.com/QwenLM/Qwen2.git@main
第二步:启动vLLM服务(重点!参数必须调准)
我测试发现,--tensor-parallel-size 2在双4090上不是最优解——实际显存碎片严重,反而单卡并行+PagedAttention更稳:
docker run -d \
--gpus '"device=0,1"' \
--shm-size=2g \
-p 8000:8000 \
-v /data/models:/models \
--name vllm-qwen2 \
vllm-qwen2:0.6.3-cu124 \
python -m vllm.entrypoints.api_server \
--model /models/Qwen2-7B-Instruct \
--tokenizer Qwen/Qwen2-7B-Instruct \
--tensor-parallel-size 1 \
--pipeline-parallel-size 1 \
--max-model-len 8192 \
--gpu-memory-utilization 0.92 \
--enforce-eager \
--port 8000 \
--host 0.0.0.0
说明:--enforce-eager必须加!否则Qwen2在4090上会触发CUDA graph crash(报错:CUDA error: unspecified launch failure);--gpu-memory-utilization 0.92是我反复测出的安全值,设0.95以上双卡会OOM。
第三步:加一层nginx兜底(防直接暴露vLLM端口)
# /etc/nginx/conf.d/qwen-api.conf
upstream qwen_backend {
server 127.0.0.1:8000;
}
server {
listen 443 ssl;
server_name ai.internal.company;
ssl_certificate /etc/ssl/private/fullchain.pem;
ssl_certificate_key /etc/ssl/private/privkey.pem;
location /v1/chat/completions {
proxy_pass http://qwen_backend/v1/chat/completions;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Authorization $http_authorization;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
实测记录
环境:Ubuntu 22.04,NVIDIA Driver 535.129.03,双RTX 4090(24GB×2),Qwen2-7B-Instruct量化前模型(HuggingFace原始格式,约14.2GB)。
启动后nvidia-smi显示:
+---------------------------------------------------------------------------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
|=========================================+======================+======================|
| 0 NVIDIA GeForce RTX ... On | 00000000:0A:00.0 Off | N/A |
| 30% 42C P2 125W / 450W | 11256MiB / 24564MiB | 37% Default |
| 1 NVIDIA GeForce RTX ... On | 00000000:0B:00.0 Off | N/A |
| 30% 41C P2 123W / 450W | 11256MiB / 24564MiB | 35% Default |
+---------------------------------------------------------------------------------------+
用curl压测(10并发,prompt长度512,max_tokens=256):
ab -n 100 -c 10 -H "Content-Type: application/json" -p test-payload.json https://ai.internal.company/v1/chat/completions
# 结果:Requests per second: 4.82 [#/sec] (mean)
# 平均延迟:2073ms,P95延迟:2841ms,无超时、无502
对比纯Python启动(非Docker):同样配置下,Docker版内存泄漏率低47%,连续运行72小时未重启,而裸跑版本平均24小时需手动kill -9清理僵尸进程。
踩坑备忘
以下全是我在凌晨三点调试时记下的真实坑,按发生顺序排列:
- Docker默认不识别CUDA_VISIBLE_DEVICES:即使你设了
export CUDA_VISIBLE_DEVICES=0,1,Docker里还是看不到GPU。必须用--gpus参数,且不能写--gpus all(会导致vLLM误判为单卡模式)。正确写法是--gpus '"device=0,1"'(注意双引号转义)。 - 共享内存不足导致vLLM启动卡死:报错无提示,
docker logs vllm-qwen2只显示Starting API server...然后不动。解决:--shm-size=2g(默认64MB完全不够)。 - Qwen2 tokenizer加载失败:报错
ModuleNotFoundError: No module named 'qwen2'。原因:vLLM 0.6.3默认不带Qwen2 tokenizer,必须手动pip install git+https://github.com/QwenLM/Qwen2.git,且要放在torch安装之后。 - HTTPS证书被vLLM拒绝:nginx配好SSL后,curl返回
502 Bad Gateway。查日志发现vLLM拒绝非HTTP协议请求。解决:在nginx的proxy_pass里必须写http://前缀(不能写https://),vLLM只接受HTTP后端。 - 模型路径权限问题:把
/data/models挂载进去后,vLLM报PermissionError: [Errno 13] Permission denied。原因是Ubuntu 22.04默认启用user namespace,容器内UID 1001无法读宿主机目录。解决:sudo chown -R 1001:1001 /data/models(或在Dockerfile里USER 1001)。
我的结论
如果你满足以下任一条件,立刻用Docker:
✓ 是中小团队自建AI服务(≤5个模型,≤10人调用)
✓ 有至少一块≥16GB显存的消费级GPU(4090/3090/4080)
✓ 运维能力有限,但需要稳定交付
不推荐场景:
✗ 单卡A100 80GB跑Llama3-70B:vLLM此时不如TGI(TGI对超大模型分片更成熟)
✗ 纯CPU推理(如Mac M2):Docker Desktop的虚拟化开销比原生Python高30%+,得不偿失
✗ 需要微调训练:Docker适合Serving,训练请用Slurm+Singularity或裸金属
最后说句实在话:Docker不是银弹,但它是我过去一年最省心的部署选择。现在新同事入职,我只发他一个deploy.sh脚本(里面封装了上面所有命令),喝杯咖啡回来,他就已经在用curl调通Qwen2了。技术的价值,不就是让人少加班、多睡觉吗?