使用 Docker 部署 AI 服务:新手也能看懂

使用 Docker 部署 AI 服务:新手也能看懂

背景

去年我给公司内部搭了个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清理僵尸进程。

踩坑备忘

以下全是我在凌晨三点调试时记下的真实坑,按发生顺序排列:

  1. Docker默认不识别CUDA_VISIBLE_DEVICES:即使你设了export CUDA_VISIBLE_DEVICES=0,1,Docker里还是看不到GPU。必须用--gpus参数,且不能写--gpus all(会导致vLLM误判为单卡模式)。正确写法是--gpus '"device=0,1"'(注意双引号转义)。
  2. 共享内存不足导致vLLM启动卡死:报错无提示,docker logs vllm-qwen2只显示Starting API server...然后不动。解决:--shm-size=2g(默认64MB完全不够)。
  3. Qwen2 tokenizer加载失败:报错ModuleNotFoundError: No module named 'qwen2'。原因:vLLM 0.6.3默认不带Qwen2 tokenizer,必须手动pip install git+https://github.com/QwenLM/Qwen2.git,且要放在torch安装之后。
  4. HTTPS证书被vLLM拒绝:nginx配好SSL后,curl返回502 Bad Gateway。查日志发现vLLM拒绝非HTTP协议请求。解决:在nginx的proxy_pass里必须写http://前缀(不能写https://),vLLM只接受HTTP后端。
  5. 模型路径权限问题:把/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了。技术的价值,不就是让人少加班、多睡觉吗?