ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Docker容器化部署大模型推理服务:从Ollama验证到vLLM上线

Docker容器化部署大模型推理服务:从Ollama验证到vLLM上线 最近刚把一个微调过的开源模型接到业务系统里从模型文件到能被前端直接调用的 HTTP 接口整个过程踩了不少坑。真正动手后你会发现最麻烦的不是模型效果好不好而是“怎么把模型稳定地变成一个随时能被人调用的服务”。最后我选择用 Docker 把这整条链路打包起来从模型依赖、推理框架到 API 统一向外暴露前后折腾了差不多一周才把一套可以照着走的交付路径固定下来。这篇文章就把这条路径完整写出来为什么要用 Docker、推理框架怎么选、Dockerfile 怎么写、GPU 容器怎么启动、OpenAI 兼容接口怎么暴露、出问题怎么排查。命令和配置都是我自己在真实环境里跑过的不是那种只能“看着很美”的示例。适合看这篇文章的人模型训练完不知道怎么交付给业务方的算法工程师想让本地模型接入现有工具链或 AI Agent 的开发者以及准备从裸机部署转向容器化部署的运维同学。1. 部署之前的几个关键认知模型容器化到底难在哪1.1 推理服务和普通后端服务有本质区别很多人第一次把模型包装成 API 的时候下意识会把它当成普通 Web 服务来处理用 Flask 或者 FastAPI 起一个进程把模型加载进来然后写几个路由。这个思路本身没错但如果你没有提前意识到推理服务的一些特殊性后面一定会吃苦头。普通后端服务比如订单系统或者用户中心进程一启动代码就跑起来了依赖的是数据库和缓存。推理服务不一样它最重的部分是模型权重动辄几个 GB 到几十个 GB模型加载进 GPU 显存需要几十秒甚至几分钟而且加载完成后就“驻场”在那里不能被随意卸载。这意味着你不能像普通服务那样随便重启重启一次就是几分钟不可用。除了加载时间长推理服务还需要考虑并发、批处理、显存限制、精度转换甚至要处理模型输出格式的稳定性问题。用生活里的例子类比普通后端像一个快餐柜台点单就做做完就出餐。推理服务更像一个大型中央厨房早上开工要把整套设备预热到指定温度预热完成后才能批量出菜中途设备不能随便关。Docker 在这里最大的价值就是帮你把这个“中央厨房”连锅带灶、连带厨师手册一起封起来换一台机器也能原样启动。1.2 推理框架选型我的“快速验证 生产部署”组合部署 AI 模型之前先别急着写代码先选框架。市面上常见的思路有几个直接用 Transformers 写推理逻辑再包一层 FastAPI使用 vLLM使用 Text Generation Inference还有用 Ollama。很多人一上来就选难度最高的路线结果光依赖问题就折腾好几天这个没必要。我的建议是分成两条路径来看。第一条是“快速验证路径”用 Ollama。它把模型下载、量化、推理框架全部封装好了一条 Docker 命令就能跑起来而且默认自带 OpenAI 兼容 API。我可以先在 CPU 或者单卡环境上快速跑通业务逻辑验证模型的调用方式、输入输出格式、跟现有系统的兼容性这个阶段不需要写太多代码。第二条是“生产部署路径”用 vLLM。vLLM 在推理性能上优势非常明显支持 PagedAttention 和连续批处理吞吐量比原生 Transformers 高出一大截而且同样自带 OpenAI 兼容 API。团队手里如果已经有一块 NVIDIA 的卡比如 A10、3090、4090 或者 A100那 vLLM 基本是绕不开的选择。我把几个主流方案放在一起对比过方便你按场景选推理框架适合场景启动速度是否支持 OpenAI 兼容 API一句话评价Ollama本地开发、快速验证、个人电脑快一条命令支持最省心的选择适合先跑通流程vLLM生产环境、高并发访问中需要加载模型支持性能和吞吐量最好适合正式上线TGIHugging Face 出品生产环境中支持周边生态好但部分功能要订阅企业版FastAPI Transformers深度自定义需求慢依赖自己管理需要自己写协议层灵活但容易陷入工程细节还有一个隐藏的好处是如果你正准备把本地模型接入 Agent 工具链比如让 Codex 或者自己的 AI 代理工具去调用本地模型那么推理服务是否兼容 OpenAI 格式就显得特别重要。选 Ollama 和 vLLM 都能省掉大量适配工作。1.3 API 协议先行都按 OpenAI 格式来做在开始写容器之前先统一一个共识对外暴露的 API 尽量遵守 OpenAI 的接口格式。不管最后选什么推理框架对外接口都走/v1/chat/completions、/v1/embeddings这类路径。这样做的理由很直接市面上几乎所有大模型工具链都默认兼容 OpenAI 格式无论是 LangChain、LlamaIndex 还是 OpenAI Python SDK设置一个base_url指向你本地部署的地址就能直接用不需要中间再写一层翻译协议。很多人会忽略这一点觉得接口格式无伤大雅。但实际上一旦你开始对接 Agent、自动化工具或者第三方系统接口格式是否统一是决定工作量多少的核心因素。我自己有一次踩过坑前期图省事直接用 Transformers 自定义了 JSON 返回格式结果后面接入一个现成客户端时因为缺少choices字段客户端直接解析失败白白改了半小时代码。2. Docker 化模型服务的镜像与依赖处理2.1 两种镜像来源路径官方镜像和自建镜像确定好框架和协议之后接下来就是 Docker 化。这一步先要做个选择用官方镜像还是在基础镜像上自己构建。官方镜像的好处是省心。比如 vLLM 官方提供了vllm/vllm-openai镜像里面已经内置了 CUDA 依赖和模型服务代码Ollama 也提供了ollama/ollama官方镜像。你拉下来就能直接跑不需要管 CUDA 版本、Python 依赖、驱动底层的兼容性问题非常适合团队里没有专门 Docker 经验的人。自建镜像的好处是可控。当你的模型服务不仅包含推理还包含一套自定义的前后处理逻辑时官方镜像就不够用了。你需要写自己的 Dockerfile把模型加载代码、预处理脚本、业务逻辑全部打进镜像里然后用同一份镜像在不同环境之间迁移。我实际的建议是快速验证阶段用官方镜像切换到大模型时如果业务逻辑确实复杂再迁移到自建镜像不要一开始就过度设计。2.2 多阶段构建 Dockerfile一个可参考的最小示例下面给一个自建镜像的 Dockerfile 示例。这个示例的场景是加载一个 Hugging Face 的模型用 FastAPI 暴露一个自定义的推理 API。为了构建体积和依赖隔离我用了多阶段构建。# 阶段一安装依赖 FROM python:3.11-slim AS build ENV PIP_DISABLE_PIP_VERSION_CHECK1 \ PIP_NO_CACHE_DIR1 WORKDIR /app COPY requirements.txt . RUN pip install --prefix/install \ torch --index-url https://download.pytorch.org/whl/cpu \ fastapi \ uvicorn[standard] \ transformers \ sentencepiece # 阶段二运行镜像 FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04 ENV PYTHONUNBUFFERED1 \ HF_HOME/models/cache \ HF_HUB_OFFLINE0 WORKDIR /app COPY --frombuild /install /usr/local COPY app/ /app/app RUN useradd -m deploy mkdir -p /models/cache chown -R deploy:deploy /models USER deploy EXPOSE 8000 CMD [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8000]第一阶段的重点是用python:3.11-slim作为构建环境把所有 Python 依赖安装到一个独立的/install目录第二阶段使用 CUDA 运行时镜像作为基底再把依赖从第一阶段复制过来。这样最终的镜像不会残留构建时产生的缓存和临时文件体积会小不少。需要注意生产环境大多数情况下不需要那么完整的 PyTorch GPU 依赖可以根据模型类型选择性安装。如果是纯 CPU 环境上面 torch 的 index-url 可以换回默认源如果模型要跑在 ARM 架构的 Mac 上还得用 Metal 那套运行时不是简单的 N 卡方案。2.3 模型文件不要打进镜像里这里要强调一个非常容易被忽略的设计原则模型权重不要作为镜像的一层而是挂在外部卷volume里。如果你把模型文件直接 COPY 到镜像里构建一次要等十几分钟镜像动辄十几二十个 GB团队迁移、拉取镜像都会非常痛苦。改用外部挂载后模型文件只需要下载一次容器重启、销毁、重建都不会影响模型文件N 个容器可以共享同一份模型文件。常见做法是把模型文件放到宿主机的一个固定目录比如/data/models然后启动容器时用-v /data/models:/models挂载进去。同理Hugging Face 的默认缓存目录~/.cache/huggingface也可以挂载到持久化磁盘避免每次容器重建都重新从网上下载模型。很多模型第一次下载会卡很长时间这个设计在断网维护或者网络不稳定时就是救命稻草。2.4 用 Docker Compose 把模型服务编排起来当你的系统不只有一个模型服务时Docker Compose 几乎是必备的。我最常用到的场景是起一个模型推理服务再起一个配套的业务服务两者通过内网地址互通。Compose 文件里统一管理镜像、端口、GPU 资源和环境变量比一条条 docker run 命令清晰得多。services: vllm: image: vllm/vllm-openai:latest command: [ --model, /models/Qwen/Qwen2.5-7B-Instruct, --served-model-name, qwen, --port, 8000 ] volumes: - /data/models:/models ports: - 8000:8000 runtime: nvidia environment: - NVIDIA_VISIBLE_DEVICES0 deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] business-api: build: ./business-api ports: - 8080:8080 depends_on: - vllm这个 Compose 文件里有两个服务vllm是模型推理服务business-api是业务层 API。业务层通过http://vllm:8000/v1这个 Docker 内网地址访问模型服务。在 Compose 网络模式下服务名就是主机名所以不需要去记 Docker 宿主机的 IP这个细节能让后续联调省很多事。3. 实操路线从 Ollama 验证到 vLLM 上线3.1 先把 Docker 环境准备好在跑模型之前Docker 环境本身就可能卡住很多人。我在 Ubuntu 上一般走官方安装脚本然后手动把当前用户加入 docker 组不然每次都要 sudo 会很烦。# 安装 Docker curl -fsSL https://get.docker.com | sh # 把当前用户加入 docker 组避免每次用 sudo sudo usermod -aG docker $USER # 重新登录终端后验证 docker version docker info | grep Docker Root Dir如果是在 Windows 上用 Docker Desktop常见的一个坑是启动时提示virtualization support wasnt detected。这个和 WSL2 后端有关需要在 BIOS 里开启虚拟化Intel 一般是 VT-xAMD 是 SVM然后在 Windows 功能里勾选“适用于 Linux 的 Windows 子系统”和“虚拟机平台”。改完 BIOS 设置之后要重启电脑不要只重启 Docker Desktop我之前就是只重启应用结果一直报同样的错误。3.2 NVIDIA 容器支持让容器里真正用上 GPU模型要跑在 GPU 上不是装好 Docker 就可以的。宿主机上需要装好 NVIDIA 驱动然后安装nvidia-container-toolkitDocker 才能把 GPU 设备映射进容器。# 确认宿主机能看到显卡 nvidia-smi # 安装 nvidia-container-toolkitUbuntu 示例 sudo apt-get update sudo apt-get install -y nvidia-container-toolkit # 重启 Docker sudo systemctl restart docker装完之后用一条简单的容器命令验证 GPU 是否映射成功docker run --rm --gpus all nvidia/cuda:12.2.0-base-ubuntu22.04 nvidia-smi如果容器里能正常输出显卡信息说明 GPU 映射没问题。在这种验证上花五分钟能避免后面跑大模型时各种 “CUDA error: no kernel image is available” 的报错那类问题排查起来会非常浪费时间。3.3 快速验证路径Ollama 一条命令跑起来验证阶段建议直接用 Ollama。拉取镜像后启动容器然后在容器里拉模型。# 启动 Ollama 容器 docker run -d --name ollama \ --gpus all \ -v ollama_data:/root/.ollama \ -p 11434:11434 \ ollama/ollama:latest # 拉取 Qwen2.5 7B 模型 docker exec -it ollama ollama pull qwen2.5:7b # 测试调用 curl http://localhost:11434/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen2.5:7b, messages: [{role: user, content: 你好用一句话介绍你自己}] }这里要留意Ollama 在 Docker 环境下拉取模型要进容器内部执行不是在宿主机直接执行ollama pull。不过就算搞混了也不致命只是模型会下载到宿主机目录而不是容器挂载卷导致下次重建容器又要重新下载。Ollama 的 API 本身兼容 OpenAI 格式所以你可以直接用 OpenAI Python SDK 测试from openai import OpenAI client OpenAI( base_urlhttp://localhost:11434/v1, api_keyEMPTY ) resp client.chat.completions.create( modelqwen2.5:7b, messages[{role: user, content: 写一段 K8s 部署的注意点}] ) print(resp.choices[0].message.content)这个阶段的核心目的就是把链路跑通确认模型本身可以响应确认输入输出格式没问题。等你确定要上生产环境再切到 vLLM 也不迟。3.4 生产部署路径vLLM 提供高吞吐推理切到 vLLM 之后你会发现它的命令行参数很直白官方镜像内置了 OpenAI 兼容 API跑起来非常规整docker run -d --name vllm \ --gpus all \ --shm-size8g \ -p 8000:8000 \ -v /data/models:/models \ vllm/vllm-openai:latest \ --model /models/Qwen/Qwen2.5-7B-Instruct \ --served-model-name qwen \ --max-model-len 8192--served-model-name qwen这个参数尤其重要它决定了 API 调用时的 model 名。如果不设置默认会用完整的路径名调用的时候写起来又长又容易出错。--shm-size8g是我在实际使用中为了防止某些服务出现共享内存不足而加的参数vLLM 在容器内会使用共享内存做数据交换太小会导致运行不稳定。启动后验证一下curl http://localhost:8000/v1/models这个接口会返回当前服务支持的模型列表。如果返回结果里有id: qwen说明模型加载成功可以开始用 OpenAI SDK 去调了。vLLM 对显存有明确要求。拿 Qwen2.5-7B-Instruct 来说半精度权重大约占 16GB 显存加上推理时的 KV Cache一张 24GB 显存的卡勉强能跑短上下文如果想把上下文长度拉长到 32K 以上建议直接上 48GB 或 80GB 的卡或者选 3B/4B 小模型。3.5 容器内模型文件的预置策略还有一个值得提前规划的点模型文件从哪里来。理论上你可以让 vLLM 或者 Transformers 在容器启动时自动从模型仓库下载但这样会面临两个问题一是首次下载需要较长时间容器启动会被卡住二是在网络不稳定或者内网环境下下载失败会让容器直接崩溃。我的做法是先把模型文件完整下载到宿主机的一个目录比如/data/models/Qwen/Qwen2.5-7B-Instruct然后通过-v /data/models:/models挂载进容器。如果模型文件已经在别的机器上下载过直接用 rsync 或者移动硬盘复制过去比在容器里重新下载靠谱得多。如果你用的模型来自 Hugging Face可以先用 transformers 的预下载脚本把模型拉到本地缓存然后把对应目录挂载进容器。这样容器镜像本身不包含庞大的权重文件多个环境共享同一份模型文件构建和部署速度都会有质的变化。4. 把模型容器变成稳定的 API调用协议、日志与运维4.1 统一用 OpenAI 协议业务接入几乎零成本不管是 Ollama 还是 vLLM它们启动的服务都已经兼容 OpenAI API 格式所以业务端的接入代码写起来很统一。下面这个 Python 示例是我在项目里实际用过的调用方式base_url 指向部署的容器服务地址from openai import OpenAI client OpenAI( base_urlhttp://your-server:8000/v1, api_keyANYTHING, # 本地服务一般不校验但字段不能为空 ) response client.chat.completions.create( modelqwen, messages[ {role: system, content: 你是一个专业的运维助手。}, {role: user, content: 解释一下 Docker 镜像和容器有什么区别} ], temperature0.7, max_tokens512 ) print(response.choices[0].message.content)这里的核心是base_url指向本地推理服务的/v1路径。api_key随便填一个非空字符串即可因为自建服务通常不做鉴权但 OpenAI SDK 要求这个参数存在。如果你的业务方已经写好了调用 OpenAI 的代码只需要把 base_url 换成本地地址其他逻辑完全不用动。这就是统一协议带来的最直接好处。4.2 日志与监控别等用户报告问题才去看容器模型服务上线后日志和监控是很多人容易轻视的部分。Docker 本身的docker logs能拿到服务日志但模型推理服务的日志往往又长又难读尤其是 vLLM启动时会打印大量模型配置和参数信息。我的做法是先用docker logs --tail 200 vllm看最近的日志重点看有没有ERROR或者WARNING然后重点关注两个指标跟推理相关的耗时指标比如每秒输出 token 数、首 token 延迟以及资源指标比如容器内存和显存占用。docker stats是 Docker 自带的最轻量监控命令可以实时查看容器 CPU 和内存占用docker stats --no-stream vllm如果要更精细地监控 GPU 使用情况建议在宿主机上定期执行nvidia-smi并记录日志或者部署 Prometheus nvidia_gpu_exporter。对大部分团队来说第一步做到docker statsnvidia-smi已经能覆盖 80% 的排障需求没必要一开始就上完整的监控全家桶。日志格式上建议在编写模型服务代码时就用 JSON 格式输出比如{timestamp: ..., level: INFO, message: ...}。这样后续接入日志收集系统时解析成本会低很多。我自己因为早期图省事直接 print 文本后面接日志平台时吃了不少苦头改一次日志格式比想象中麻烦。4.3 资源限制和并发控制别让模型把整台机器拖死模型服务没做资源限制的话可能会出现一个极端情况某个请求的长上下文把显存打满后续请求全部排队甚至整个容器 OOM 被 Docker Kill 掉。vLLM 的--max-model-len参数就是对上下文长度做限制防止单请求消耗过多显存。如果不设置vLLM 会尝试根据显存自动估算但估算不总是符合你的预期我建议手动指定一个业务用不到的上限比如 8192 或 4096不要贪大。如果想让并发更平滑vLLM 还有--max-num-seqs参数可以限制同时处理的序列数量超出之后新的请求会排队。比如单张 24GB 显存的卡跑 7B 模型时把--max-num-seqs设为 8 或 16能明显降低单请求抖动带来的影响。不要把并发数设得太高。vLLM 的 continuous batching 已经会在内部做批处理它之所以快很大程度上就是从多个请求里拼成一个 batch 一起计算。如果你强行限制并发数为 1等于把它的批处理能力关了性能会腰斩。所以这里要做的是找到业务的合理并发区间而不是越大越好。4.4 安全小提醒容器内的秘密管理部署模型服务时团队经常会把 API Key、数据库连接串直接写进 Dockerfile 环境变量。这在开源模型和内部系统联调时风险不大但一旦镜像被推到公共仓库问题就大了。镜像的每一层历史都可以被翻出来里面有敏感信息就相当于裸奔。我的习惯是环境变量尽量通过 docker run 的-e参数或者 Kubernetes 的 Secret 注入不写进 Dockerfile.dockerignore文件里排除.env、密钥证书这类文件。如果一定要在镜像构建时使用敏感信息至少要用 Docker 的 BuildKit 密文注入特性不要直接在RUN命令里明文写。5. 高频问题排查与避坑速查表这一节把我实际部署中遇到过的、以及跟朋友交流时高频出现的问题整理成一张速查表。遇到问题先对照表格定位往往比重新翻日志高效得多。报错/现象常见原因解决办法docker: permission denied while trying to connect to the Docker daemon socket当前用户不在 docker 组sudo usermod -aG docker $USER后重新登录或临时使用 sudoDocker Desktop 提示virtualization support wasnt detectedBIOS 虚拟化未开启或 WSL2 功能未启用进 BIOS 开启 VT-x/SVMWindows 功能里勾选 WSL2 和虚拟机平台重启系统容器启动后日志报CUDA error: no kernel image is availableNVIDIA 驱动版本和容器 CUDA 版本不匹配或 nvidia-container-toolkit 未安装宿主机执行nvidia-smi确认驱动安装 nvidia-container-toolkit 并用--gpus all启动容器模型服务启动很慢卡在Loading checkpoint shards模型文件大且在加载或首次从网上下载权重耐心等二次部署建议挂载本地模型目录避免重复下载容器启动后马上退出docker logs显示 OOM内存或显存不足查看docker stats和nvidia-smi减少模型大小或缩短上下文长度Port is already allocated宿主机端口被占用用docker ps -a查看旧容器删除或换端口映射vLLM 频繁报ValueError: invalid dtype模型权重精度和参数不匹配比如在只支持 fp16 的环境传了 bf16检查 GPU 架构手动指定--dtypeAPI 返回 404/v1/models正常但chat/completions404框架不兼容当前 API 路径或服务没注册 chat 模型确认框架版本支持 OpenAI 协议检查模型配置比如 vLLM 是否支持该模型类型请求偶尔超时但服务没有明显报错并发过高请求排队严重调低--max-num-seqs或缩小--max-model-len必要时加卡这些问题里有几个藏得很深的细节值得单独说。第一CUDA 相关的报错不要一上来就怀疑模型代码。先用nvidia-smi确认宿主机显卡正常再跑一个最简单的 CUDA 基础镜像容器验证 Docker 到 GPU 的链路通不通。如果基础镜像都跑不起来问题一定出在驱动或 toolkit 层面。第二OOM 并不总是显存问题。有时候是容器内存限制Docker 默认不限制内存但如果你用了--memory参数要记得同时考虑模型加载时的内存峰值。模型加载阶段内存占用通常会比稳态推理高出不少给得太紧容易启动即挂。第三使用 vLLM 时如果发现请求响应很慢先去查是不是--max-model-len设置得过大。这个值越大KV Cache 预留的显存就越多实际计算时反而会因为显存不够而频繁做内存换入换出性能大打折扣。我自己就犯过这个错7B 模型设成 32K 上下文结果显存直接被预留占满一个普通请求等了几十秒。6. 最后再说几句我自己的部署习惯其实整个部署链路走通之后回头去看最大的体会是不要把部署想象成模型训练之后的“最后一步小事”它和训练一样需要设计。先定协议再选框架再定 Docker 化方案每一步都往前想一步后面才会省心。如果让我给一个刚准备入手的团队提建议第一件事不是去研究 Dockerfile 怎么写而是先把模型和推理框架跑在命令行里用最原始的方式确认模型能出结果然后立刻用 Ollama 把 API 链路验证一遍最后再切到 vLLM 做性能和并发测试。很多团队一上来就让运维直接上 vLLM结果模型还没验证完先被环境问题搞到崩溃这个顺序是反的。模型文件单独用卷挂载这件事看起来没什么技术含量实际却是最能提升部署体验的细节之一。我有一次为了赶进度把模型直接 COPY 进镜像构建一次用了二十分钟镜像 20 多个 GB传输到测试服务器又花了大半个小时。后来改成挂载方式构建时间变成两三分钟镜像体积缩到几个 GB。从那以后我再没把大模型文件塞进镜像层里。最后模型的 API 服务上线以后一定要留一个最基础的健康检查接口比如/health或者直接复用/v1/models。Kubernetes 或者 Docker 重启策略都可以通过探测这个接口来判断服务是否真正可用而不是只看进程在不在。否则模型加载失败但进程还挂着整个服务会处于一种“看似活着其实什么都干不了”的假死状态这种情况如果你遇到过一定会对健康检查有深刻的认同感。
返回列表