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容器化核心逻辑。你看一眼,应该能快速理解整体流程。
4.6 避坑指南
最后,分享几个我踩过的坑:
- 权限问题:NFS挂载后,容器内用户可能没有读权限。记得在Dockerfile里设置
USER root,或者调整NFS导出权限。 - 网络延迟:如果NFS服务器和K8s节点不在同一个机房,模型加载会非常慢。我建议把NFS部署在同一个集群内,或者用GPFS这类高性能文件系统。
- 版本兼容:vLLM不同版本对模型格式要求不同。比如v0.3.0支持safetensors,v0.2.0只支持bin。升级vLLM时,记得检查模型文件格式。
好了,这一章就到这里。容器化是K8s调度的基础,把这一步做扎实了,后面的部署才能顺风顺水。