3、vLLM镜像构建:编写Dockerfile、选择基础镜像、安装依赖、配置入口脚本
镜像构建这件事,说白了就是给vLLM搭一个「家」。这个家要舒服、稳定,还得能随时搬家到K8s集群里。我刚开始搞vLLM部署时,图省事直接用了官方镜像,结果发现定制化需求一上来,改起来特别痛苦。后来我学乖了——自己写Dockerfile,从基础镜像开始一点点搭。
3.1 基础镜像怎么选?
选基础镜像,核心就三个字:匹配。你的GPU是什么架构,CUDA版本是多少,PyTorch需要什么版本,这三者必须对齐。
我个人习惯用 nvidia/cuda:12.1.0-runtime-ubuntu22.04 作为基础镜像。为什么是runtime而不是devel?因为devel镜像里带了很多编译工具链,体积大得吓人。生产环境用runtime就够了,省下来的空间能多装好几个模型。
举个例子,如果你用的是A100或H100,CUDA 12.x是首选。如果是V100或T4,CUDA 11.8也完全够用。我曾在项目里踩过一个坑——用了CUDA 11.4的镜像,结果vLLM的某些算子编译不过去。后来查文档才发现,vLLM对CUDA版本有硬性要求。
| GPU型号 | 推荐CUDA版本 | 基础镜像示例 |
|---|---|---|
| A100 / H100 | 12.1+ | nvidia/cuda:12.1.0-runtime-ubuntu22.04 |
| V100 / T4 | 11.8 | nvidia/cuda:11.8.0-runtime-ubuntu22.04 |
| L40S / A10 | 12.0+ | nvidia/cuda:12.0.0-runtime-ubuntu22.04 |
3.2 编写Dockerfile:一步一步来
好,基础镜像选好了,接下来写Dockerfile。我习惯把Dockerfile分成三段:基础环境、Python依赖、vLLM安装。这样分层清晰,出了问题也好排查。
先看一个完整的例子:
# 第一阶段:基础环境
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
# 设置非交互模式,避免安装时卡住
ENV DEBIAN_FRONTEND=noninteractive
# 安装系统依赖
RUN apt-get update && apt-get install -y \
python3.10 \
python3-pip \
git \
wget \
&& rm -rf /var/lib/apt/lists/*
# 第二阶段:Python依赖
RUN pip3 install --no-cache-dir \
torch==2.1.0 \
transformers \
accelerate
# 第三阶段:安装vLLM
RUN pip3 install vllm==0.4.0
# 配置入口脚本
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
嗯,这里要注意几点:
- 版本锁定:PyTorch和vLLM的版本一定要锁死。我曾经因为没锁版本,镜像构建时自动装了最新版,结果和CUDA不兼容,推理时直接报错。
- 清理缓存:
rm -rf /var/lib/apt/lists/*这行不能省。不清理的话,镜像体积能大出200MB。 - 非交互模式:
DEBIAN_FRONTEND=noninteractive是必须的,否则apt-get安装时可能卡在时区选择上。
3.3 安装vLLM依赖:那些容易忽略的细节
vLLM的依赖其实不多,但有几个坑。我一个个说。
第一个坑:PyTorch版本
vLLM对PyTorch版本有明确要求。以vLLM 0.4.0为例,它需要PyTorch 2.1.x。如果你装了2.0.x或2.2.x,编译时就会报错。怎么确认?去vLLM的GitHub页面看release note,上面写得清清楚楚。
第二个坑:CUDA运行时库
有些基础镜像只带了CUDA驱动,没带运行时库。vLLM在推理时需要调用CUDA的运行时API,少了就会报 CUDA error: no kernel image is available。解决办法很简单——用runtime镜像,别用base镜像。
第三个坑:内存和磁盘空间
构建镜像时,vLLM会编译一些自定义算子,这个过程很吃内存。我建议构建机器的内存至少16GB,磁盘空间留10GB以上。否则编译到一半OOM,那叫一个崩溃。
--progress=plain 参数看日志。我试过一次,输出刷了上万行,眼睛都看花了。除非你在调试,否则用默认的进度条模式就行。
3.4 配置入口脚本:让容器跑起来
入口脚本是容器的「大脑」。vLLM的启动命令、模型加载参数、端口配置,全写在这里。
我写了一个通用的入口脚本,你可以直接拿来用:
#!/bin/bash
set -e
# 默认参数
MODEL_PATH=${MODEL_PATH:-"/models/llama-2-7b"}
PORT=${PORT:-8000}
GPU_MEMORY_UTILIZATION=${GPU_MEMORY_UTILIZATION:-0.9}
echo "Starting vLLM server..."
echo "Model: $MODEL_PATH"
echo "Port: $PORT"
# 启动vLLM
python3 -m vllm.entrypoints.openai.api_server \
--model $MODEL_PATH \
--port $PORT \
--gpu-memory-utilization $GPU_MEMORY_UTILIZATION \
--max-model-len 4096 \
--dtype auto
这个脚本有几个设计点:
- 环境变量驱动:模型路径、端口、GPU内存利用率都通过环境变量传入。这样在K8s的YAML里可以直接配置,不用改镜像。
- set -e:脚本遇到错误就退出。别小看这行,没有它,vLLM启动失败时容器可能还在运行,K8s检测不到异常。
- 默认值:给每个参数都设了默认值。这样即使忘了传环境变量,容器也能启动,不至于直接挂掉。
/health 端点。K8s的liveness probe和readiness probe都需要这个。我见过有人没加,结果Pod显示Running,但模型根本没加载完,流量一进来全超时。
3.5 构建并推送镜像到私有仓库
镜像写好了,接下来构建和推送。这部分其实不难,但有几个小技巧能省不少时间。
构建命令:
docker build -t registry.example.com/vllm-server:0.4.0 .
这里我习惯把镜像名写成 仓库地址/镜像名:版本号 的格式。这样推送时直接 docker push 就行,不用再打tag。
多阶段构建:
如果镜像体积太大,可以考虑多阶段构建。比如第一阶段用devel镜像编译vLLM的算子,第二阶段用runtime镜像只拷贝编译好的文件。这样最终镜像能小30%左右。
举个例子:
# 第一阶段:编译
FROM nvidia/cuda:12.1.0-devel-ubuntu22.04 AS builder
RUN pip3 install vllm==0.4.0
# 第二阶段:运行
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
COPY --from=builder /usr/local/lib/python3.10/dist-packages /usr/local/lib/python3.10/dist-packages
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
推送命令:
docker push registry.example.com/vllm-server:0.4.0
推送前记得先登录:docker login registry.example.com。我刚开始时老忘记这步,每次推送都报权限错误,后来干脆写了个脚本自动登录。
3.6 本章核心逻辑图
下面这张图展示了镜像构建的完整流程,从基础镜像选择到最终推送仓库,每一步都环环相扣。
这张图把整个流程串起来了。你从左上角开始,按箭头方向走一遍,就能理解镜像构建的全貌。我个人觉得,最关键的还是基础镜像的选择和版本锁定——这两步做好了,后面基本不会出大问题。
好了,镜像构建这部分就讲到这里。下一章我们会聊怎么把镜像部署到K8s集群里,包括Pod配置、Service暴露、以及如何用Ingress做流量管理。到时候见。