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
小提示:别用最新的CUDA版本。我见过有人追新用了CUDA 12.4,结果PyTorch还没适配,折腾了两天才降级。选稳定版,比如12.1或11.8,准没错。

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检测不到异常。
  • 默认值:给每个参数都设了默认值。这样即使忘了传环境变量,容器也能启动,不至于直接挂掉。
我的经验:入口脚本里最好加一个健康检查接口。比如在启动vLLM后,用curl检查 /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。我刚开始时老忘记这步,每次推送都报权限错误,后来干脆写了个脚本自动登录。

注意:私有仓库如果用了HTTPS,一定要配好证书。否则Docker客户端会拒绝推送。我曾在测试环境用了自签名证书,结果折腾了半天才发现是证书问题。生产环境建议用正规CA签发的证书,省心。

3.6 本章核心逻辑图

下面这张图展示了镜像构建的完整流程,从基础镜像选择到最终推送仓库,每一步都环环相扣。

vLLM镜像构建核心流程 选择基础镜像 CUDA 12.1 / 11.8 安装系统依赖 Python3、pip、git 安装Python依赖 PyTorch、transformers 安装vLLM vllm==0.4.0 配置入口脚本 entrypoint.sh 构建镜像 docker build -t ... 推送私有仓库 docker push 关键注意事项 • 版本锁定:PyTorch和vLLM版本必须匹配 • 清理缓存:apt-get后删除lists目录 • 非交互模式:设置DEBIAN_FRONTEND • 多阶段构建:减小镜像体积 • 健康检查:入口脚本中加/health端点

这张图把整个流程串起来了。你从左上角开始,按箭头方向走一遍,就能理解镜像构建的全貌。我个人觉得,最关键的还是基础镜像的选择和版本锁定——这两步做好了,后面基本不会出大问题。


好了,镜像构建这部分就讲到这里。下一章我们会聊怎么把镜像部署到K8s集群里,包括Pod配置、Service暴露、以及如何用Ingress做流量管理。到时候见。