4. vLLM容器化:Dockerfile编写、镜像构建、模型权重挂载策略

好,咱们进入第四章。这一章我打算聊聊vLLM的容器化。

说实话,容器化这件事,在AI推理部署里太关键了。你想想看,一个推理服务要跑在K8s上,不打包成镜像怎么行?但怎么打包、怎么挂模型权重,这里头门道不少。我踩过的坑,今天一并说清楚。

4.1 为什么非要容器化?

有人问我:直接在宿主机上跑vLLM不行吗?

行,当然行。但你要考虑几个问题:

  • 环境一致性:开发环境是Python 3.10 + CUDA 12.1,生产环境呢?万一版本对不上,推理结果都可能不一样。
  • 依赖隔离:vLLM依赖的torch版本、flash-attention版本,跟其他服务冲突怎么办?
  • 快速扩缩容:K8s调度Pod,镜像一拉就起来。你手动装环境?那黄花菜都凉了。

所以,容器化是必选项,不是可选项。

核心原则:镜像要小、启动要快、权重要分离。

4.2 Dockerfile编写:我的最佳实践

先上代码,再解释。

# 基础镜像:用官方CUDA镜像,别自己装驱动
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04

# 设置环境变量,避免交互式安装
ENV DEBIAN_FRONTEND=noninteractive \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

# 安装系统依赖
RUN apt-get update && apt-get install -y \
    python3.10 \
    python3-pip \
    git \
    && rm -rf /var/lib/apt/lists/*

# 设置工作目录
WORKDIR /app

# 先复制requirements.txt,利用Docker缓存
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制vLLM源码(或者直接pip安装)
COPY . .
RUN pip install --no-cache-dir -e .

# 暴露端口
EXPOSE 8000

# 默认启动命令
CMD ["python3", "-m", "vllm.entrypoints.openai.api_server", \
     "--model", "/models", \
     "--port", "8000"]

嗯,这里要注意几个点:

  • 基础镜像选型:我习惯用nvidia/cuda:12.1.0-runtime-ubuntu22.04。为什么不用devel版本?因为runtime更小,推理不需要编译环境。devel版本动辄几个G,没必要。
  • 依赖安装顺序:先装requirements.txt,再复制源码。这样如果依赖没变,Docker能复用缓存层,构建速度快很多。我在项目中遇到过,每次改一行代码都要重装torch,那叫一个痛苦。
  • 多阶段构建:如果追求极致镜像大小,可以用多阶段。但vLLM本身依赖复杂,我建议单阶段就够了,别为了省100MB搞出兼容性问题。

小技巧:在requirements.txt里固定版本号,比如vllm==0.4.0。别用vllm>=0.4.0,否则哪天升级了,你的推理结果可能就变了。

4.3 镜像构建:自动化与版本管理

Dockerfile写好了,怎么构建?

我个人习惯用Makefile来管理:

# Makefile
IMAGE_NAME = my-registry/vllm-inference
IMAGE_TAG = $(shell git rev-parse --short HEAD)

build:
    docker build -t $(IMAGE_NAME):$(IMAGE_TAG) .
    docker tag $(IMAGE_NAME):$(IMAGE_TAG) $(IMAGE_NAME):latest

push:
    docker push $(IMAGE_NAME):$(IMAGE_TAG)
    docker push $(IMAGE_NAME):latest

为什么用git commit hash做tag?

  • 可追溯:每个镜像对应一个代码版本。
  • 避免冲突:不会出现两个人都打v1.0的情况。
  • 回滚方便:出问题了,直接切回上一个hash的镜像。

我曾经见过一个团队,所有镜像都打latest标签。结果某天更新了代码,推理结果全错了,回滚都不知道回滚到哪个版本。嗯,那场面,挺混乱的。

4.4 模型权重挂载策略:核心中的核心

这是本章的重头戏。模型权重怎么挂?

先说结论:永远不要把模型权重打包进镜像

为什么?

  • 模型权重动辄几十GB,镜像会变得巨大无比。
  • 权重更新频繁,每次更新都要重新构建镜像,太慢了。
  • 多个模型共享权重?镜像里只能有一份。

那怎么办?用外部存储挂载

4.4.1 挂载方式对比

方式 优点 缺点 适用场景
HostPath 简单、性能好 节点绑定、不灵活 单机测试、开发环境
PVC (PersistentVolumeClaim) 可迁移、支持多节点 需要配置存储后端 生产环境、多副本部署
对象存储 (S3/MinIO) 弹性、成本低 延迟高、需要额外工具 冷备、模型分发

我个人强烈推荐PVC + NFS/GPFS的方案。为什么呢?

  • PVC是K8s原生资源,调度器能自动处理。
  • NFS部署简单,性能对于推理来说够用。
  • 多个Pod可以共享同一个PVC,节省存储空间。

4.4.2 实战:PVC挂载模型权重

假设你有一个NFS服务器,导出了/data/models目录,里面放了Llama-2-7b-chat-hf

第一步:创建PV和PVC

# pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: models-pv
spec:
  capacity:
    storage: 500Gi
  accessModes:
    - ReadOnlyMany
  nfs:
    server: 192.168.1.100
    path: /data/models
---
# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: models-pvc
spec:
  accessModes:
    - ReadOnlyMany
  resources:
    requests:
      storage: 500Gi

第二步:在Deployment中挂载

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-inference
spec:
  replicas: 2
  template:
    spec:
      containers:
      - name: vllm
        image: my-registry/vllm-inference:abc1234
        volumeMounts:
        - name: models
          mountPath: /models
        env:
        - name: MODEL_NAME
          value: "Llama-2-7b-chat-hf"
      volumes:
      - name: models
        persistentVolumeClaim:
          claimName: models-pvc

注意:这里accessModes用了ReadOnlyMany。推理时模型权重是只读的,没必要给写权限。万一哪个Pod误写了权重,所有推理服务都会受影响。我曾经遇到过,一个测试脚本把模型文件覆盖了,整个集群的推理都挂了,排查了半天才发现。

4.4.3 模型权重预热

模型权重挂载后,vLLM启动时会加载到GPU显存。这个过程很慢,尤其是大模型。

怎么优化?

  • 预加载:在Pod启动前,用initContainer把权重从NFS拷贝到本地SSD。虽然多了一步,但后续加载速度快很多。
  • 共享内存:多个Pod共享同一个PVC时,操作系统会缓存文件。第二个Pod启动时,直接从缓存读,速度翻倍。
  • 模型分片:如果模型太大,一张GPU放不下,可以用vLLM的tensor parallelism。但要注意,分片后的权重文件要提前准备好。

4.5 本章知识体系

下面这张图,是我梳理的vLLM容器化核心逻辑。你看一眼,应该能快速理解整体流程。

vLLM容器化核心流程 Dockerfile编写 基础镜像选择 依赖安装顺序 镜像构建 Makefile自动化 Git Hash版本管理 权重挂载策略 PVC/NFS方案 ReadOnlyMany模式 关键决策:权重不打包进镜像,使用外部存储挂载 HostPath 简单但节点绑定 仅限开发测试 PVC + NFS 生产推荐方案 支持多副本共享 对象存储 延迟高 适合冷备分发 优化技巧:预加载 + 共享内存 + 模型分片 提升启动速度,降低推理延迟

4.6 避坑指南

最后,分享几个我踩过的坑:

  • 权限问题:NFS挂载后,容器内用户可能没有读权限。记得在Dockerfile里设置USER root,或者调整NFS导出权限。
  • 网络延迟:如果NFS服务器和K8s节点不在同一个机房,模型加载会非常慢。我建议把NFS部署在同一个集群内,或者用GPFS这类高性能文件系统。
  • 版本兼容:vLLM不同版本对模型格式要求不同。比如v0.3.0支持safetensors,v0.2.0只支持bin。升级vLLM时,记得检查模型文件格式。

好了,这一章就到这里。容器化是K8s调度的基础,把这一步做扎实了,后面的部署才能顺风顺水。


专注资料整理