在 Linux 上部署 Keras 模型,这件事说大不大,说小不小。我见过太多项目在本地 notebook 里跑得顺顺当当,一到服务器上就原形毕露:要么 CUDA 版本对不上,要么模型加载就崩,要么并发一上来延迟飙到十几秒。所以这篇指南想解决的就是一件事:把你在 Keras 下训练好的模型,干净利落地部署到 Linux 服务器上,让它可以稳定对外提供预测服务。不管你是刚接触模型部署的新手,还是已经会跑训练但没上过生产环境的算法工程师,这篇文章都会给你一套可以照抄的流程。这里面的很多坑都是我自己踩过的,写出来的目的就是让你少走一点弯路,最好一次跑通。
1. 环境准备:把地基打牢
1.1 系统与 Python 版本怎么选
在 Linux 上部署 Keras 模型,第一步不是急着 pip install,而是先确认你的操作系统和 Python 版本。绝大多数生产服务器是 Ubuntu 20.04/22.04 LTS、Debian 11/12,或者 CentOS Stream 9 这类发行版。我个人的建议是,优先用 Ubuntu 22.04 或 Debian 12,因为 TensorFlow 官方提供 Linux 预编译 wheel,对 glibc 版本有明确要求,老系统的 libc 太旧会导致 import tensorflow 直接报错,那种报错信息往往很抽象,新手喜欢从权限和路径上找问题,结果根本不是那回事。
Python 版本这里要尤其小心。TensorFlow 2.10 到 2.15 时代,官方 wheel 对 Python 3.8 到 3.11 支持得最好;到 TensorFlow 2.16 之后才逐步支持 3.12,但我仍不建议在生产环境用最新版本的 Python,除非你确认依赖链全都跟上了。核心理由是:你部署的是模型,不是体验新特性,稳定压倒一切。所以如果你问我,我会回答:Python 3.10 加 TensorFlow 2.15 是最不折腾的组合,网上遇到过的部署报错案例里,绝大多数都能用这套组合绕开。
如果系统自带 Python 版本太老,可以用 apt 安装 python3.10、python3.10-venv,或者直接用 Miniconda 管理。对于团队协作,我更喜欢 conda 环境,因为它可以精确锁定 Python 小版本,换机器也方便复制环境。下面的操作以系统 python3 加 venv 为例,因为这样软件依赖足够干净,日后排查问题也更容易定位。基础命令就四条:
sudo apt update sudo apt install python3-venv python3-pip python3 -m venv keras-deploy source keras-deploy/bin/activate激活之后用python3 --version确认版本,如果版本不对就不要再往下走了,先花十分钟把环境调对,这十分钟绝对值得。这里有个很多人忽略的小细节:venv 目录建在项目目录里,不要在根目录或者 /opt 下乱建,不然以后迁移环境时路径很容易写乱。
1.2 安装 TensorFlow 前必须知道的一件事
Keras 与 TensorFlow 的关系,在安装之前必须搞清楚。Keras 自 2.4 版本以后被并进 TensorFlow 成为 tf.keras,而新的 Keras 3 在 2.16 之后的 TensorFlow 里又变成了独立包。大部分训练好的模型,只要你历史代码里用的是from tensorflow import keras,那部署时就只需要安装 tensorflow 这一个包就够了,keras 包没必要单独装。如果你的训练环境是独立的 keras 3.x,安装时就要让 keras 与 tensorflow 版本尽量对齐,否则容易出现加载模型时 node 找不到或者属性缺失的诡异问题。
所以我的建议是:部署机上直接固定安装与训练环境一致的 tensorflow 版本。先装 CPU 版本,验证模型加载和推理逻辑没问题后,再考虑 GPU 版本。很多第一次部署的人一上来就装 GPU 版,然后被 CUDA 驱动折腾到崩溃,其实完全没这个必要。推理阶段的模型通常体积不大,在 CPU 上跑几个毫秒到几十毫秒完全可接受,CPU 部署成功之后再回头处理 GPU 加速,那个时候你至少知道问题出在硬件适配还是代码逻辑上。
安装命令一句话:
pip install tensorflow==2.15.0安装完成后用这一句验证:
python -c "import tensorflow as tf; print(tf.__version__)"如果你用的训练环境是纯 keras 3.x,就补装一个指定版本:
pip install keras==3.3.3这里还有一个比较隐蔽的点:如果你用的是树莓派、Jetson 这类 arm64 设备,TensorFlow 官方 wheel 不一定有对应版本,需要下载第三方构建版或者直接用 TensorFlow Lite 的运行时,安装方式差别比较大,我就不过多展开了。
2. 模型文件:从 notebook 到可部署格式
2.1 为什么我不推荐只保存 .h5
开发时把模型存成 model.h5 很常见,但生产部署时我却强烈建议改成 SavedModel 目录格式。h5 是单文件,方便传输,但它把结构、权重、优化器状态都塞在一个文件里,TensorFlow Serving、TFLite 这些工具对 h5 的支持不如 SavedModel 自然。SavedModel 是一个目录,包含 assets、variables 和 saved_model.pb,不仅记录了网络结构与权重,还能附带签名信息和对应版本号,后续做灰度发布或回滚都很方便。即便你自己写推理服务,SavedModel 也能让模型与推理代码解耦得更干净。
保存代码非常简单,训练完之后直接写:
import tensorflow as tf model.save("saved_model/my_model")如果一定要用 h5,也得注意写法:
model.save("my_model.h5", save_format="h5")但加载时我更推荐使用 SavedModel:
model = tf.keras.models.load_model("saved_model/my_model")这里有个小坑:如果模型里有自定义层或者自定义 loss,加载时必须把自定义类传入 load_model 的 custom_objects 参数。很多人部署时图省事直接 load,然后报错找不到某个类,其实背后就是因为序列化时没法还原自定义对象。这个问题在 Keras 3 中会更加明显,因为新的序列化格式对自定义代码的依赖更严格。建议训练阶段就把自定义对象封装成独立模块,部署时直接 import 进代码,这样最省心。
2.2 ONNX 和 TFLite 转换流程
如果下游系统不使用 TensorFlow 生态,比如需要跨框架用 ONNX Runtime 推理,或者模型要跑到移动端,那 Keras 模型就要转成 ONNX 或 TFLite。转换前必须保证输入 shape 是固定的,动态维度会让后续固定 batch 的部署麻烦不断。安装转换工具就两行命令:
pip install tf2onnx onnx onnxruntime转换命令可以写成:
python -m tf2onnx.convert --saved-model saved_model/my_model --output my_model.onnx --opset 13转换完成后,最要紧的事不是看输出文件大小,而是做一致性验证。我习惯用一小批真实输入分别跑原始 Keras 模型和 ONNX Runtime,比较输出的最大相对误差。如果误差超过 1e-4,说明转换过程出问题了,不要上线。验证脚本大致是:
import numpy as np import onnxruntime as ort import tensorflow as tf model = tf.keras.models.load_model("saved_model/my_model") dummy = np.random.rand(1, 224, 224, 3).astype(np.float32) keras_out = model(dummy).numpy() sess = ort.InferenceSession("my_model.onnx") ort_out = sess.run(None, {sess.get_inputs()[0].name: dummy})[0] print(np.max(np.abs(keras_out - ort_out)))如果目标环境是移动端或者嵌入式设备,比如树莓派、Jetson 这种低算力设备,建议同时产出 TFLite 格式。转换代码很简洁:
converter = tf.lite.TFLiteConverter.from_saved_model("saved_model/my_model") converter.optimizations = [tf.lite.Optimize.DEFAULT] tflite_model = converter.convert() with open("my_model.tflite", "wb") as f: f.write(tflite_model)float16 量化一般精度损失很小,int8 量化则需要提供校准数据集,不要贸然使用,否则精度崩了排查起来会非常痛苦。我见过团队为了极致体积上 int8,结果线上召回率掉了一截,最后发现是量化时没有做代表性数据校准,白白折腾了一周。
3. 自己包一个 Web 服务
3.1 为什么选 FastAPI
如果只是内部简单调用,一个 Flask 加 Gunicorn 倒也够用;但要接生产环境,我更推荐 FastAPI。原因很简单:原生异步、自带请求字段校验和 OpenAPI 文档,性能和开发体验都优于传统的 Flask/Werkzeug。部署模型推理服务的核心矛盾是:单个请求的预处理和后处理可能很快,但遇到并发时容易因为 Python GIL 导致 CPU 资源没有吃满。FastAPI 配合 Uvicorn 的多 worker 方案,可以在不改业务逻辑的情况下把模型加载到每个 worker,用进程数来摊平 GIL 的影响。至少在我的实践中,同样一台 8 核机器,FastAPI 加 4 个 worker 能扛住的 QPS 比单机 Flask 高出不少。
安装依赖只需要一行:
pip install fastapi uvicorn gunicorn代码结构上,我建议建立这样一个目录:
models/ saved_model/ app.py requirements.txt这套结构看着简单,但在后面接监控和日志时你会感谢自己当初没把代码全堆在 notebook 里。很多团队把推理逻辑写在一个巨大的 ipynb 里,部署时再从里面一段段抠代码,简直是要命的事情。
3.2 一个可以直接抄的推理服务
下面这个骨架我几乎每个项目都在用。要点有三个:模型在模块加载时初始化一次;请求体的校验交给 Pydantic;预测函数里只做最少的预处理和后处理逻辑。代码:
import numpy as np import tensorflow as tf from fastapi import FastAPI, HTTPException from pydantic import BaseModel MODEL_PATH = "saved_model/my_model" model = tf.keras.models.load_model(MODEL_PATH) app = FastAPI(title="Keras Model Service") class PredictRequest(BaseModel): instances: list class PredictResponse(BaseModel): predictions: list @app.get("/healthz") def healthz(): return {"status": "ok"} @app.post("/v1/predict", response_model=PredictResponse) def predict(req: PredictRequest): try: data = np.array(req.instances, dtype=np.float32) # 这里可以加入与训练流程一致的预处理,例如归一化、resize result = model(data).numpy().tolist() return PredictResponse(predictions=result) except Exception as e: raise HTTPException(status_code=500, detail=str(e))这段代码请根据自己模型的实际输入结构调整:图片分类可能要先把 base64 解码再 resize,文本分类可能要做 tokenization,序列模型可能要做 padding。但核心思想不变:HTTP 层只负责数据格式转换,真正计算全在 TensorFlow 框架里。不要把业务规则写进推理接口,否则后面维护接口的人会非常痛苦。
注意,开发时本地起服务测试没问题,但部署到生产环境时,千万别用uvicorn app:app --host 0.0.0.0 --port 8000这种单进程模式。单进程意味着同一时刻只能利用一个 CPU 核,并发稍高就会出现请求排队。正确做法是用 Gunicorn 起多个 worker:
gunicorn app:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 --timeout 120每个 worker 会独立加载一份模型。如果你的模型特别大,比如超过 2GB,4 个 worker 就意味着 8GB 内存起步,这时候就要根据机器配置权衡 worker 数量,或者改用第四部分讲的 TensorFlow Serving。我见过有人在一台 8GB 机器上起了 8 个 worker,结果服务启动到第三个直接 OOM,这属于典型的没算好内存账。
3.3 反向代理与访问控制
生产环境不要直接把 8000 端口暴露给外部。Nginx 是成熟的选择,简单配置如下:
server { listen 80; server_name your_server_domain; location /v1/predict { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }设置proxy_read_timeout时要考虑到模型推理慢的情况,默认 60 秒可能不够,我一般调到 120 秒以上,具体看你的模型延迟。接口访问控制方面,至少加一层 API Key 校验,最简单的做法是在 FastAPI 里写一个依赖函数检查请求头。如果你们的平台对安全要求比较高,建议再接一层身份认证网关,不要在业务代码里单独实现完整鉴权逻辑。
4. 上生产:TensorFlow Serving 与容器化
4.1 TensorFlow Serving 能省多少事
自己写 FastAPI 服务优点灵活,但要处理版本管理、批处理、指标监控时,就轮到 TensorFlow Serving 登场了。它原生支持 SavedModel 格式,目录名就是版本号,例如models/my_model/1/、models/my_model/2/,它会自动加载最新版本并支持平滑切换。更重要的是,Serving 内置了动态批处理,多个推理请求到达后可以拼成一个 batch 喂给 GPU 或 CPU,吞吐量与硬件利用率都会更高。这点在 GPU 推理时尤其明显,单请求一个 batch 往往只能吃满 GPU 很小一部分,动态批处理能明显压低单次推理的固定调度开销。
当然,TensorFlow Serving 也不是银弹。它主要解决的是“模型服务”这一层,如果你还需要在请求里做复杂的鉴权、动态路由、多模型编排,那仍然要在前面套一层自己的 API 网关。但底层的预测能力交给它,稳定性会比从零写要好很多。
4.2 用 Docker 跑起 Serving
用官方镜像 tensorflow/serving 可以直接把模型目录挂载进去:
docker run -p 8501:8501 \ --mount type=bind,source=$(pwd)/models,target=/models \ -e MODEL_NAME=my_model \ -t tensorflow/serving启动之后,REST 接口默认在 8501 端口,gRPC 接口在 8500 端口。REST 调用的请求体风格和前面 FastAPI 很像:
curl -X POST http://localhost:8501/v1/models/my_model:predict \ -H "Content-Type: application/json" \ -d '{"instances": [[1.0, 2.0, ...]]}'gRPC 调用则更高效,适合内部服务之间调用。注意 Serving 默认会在启动时扫描 /models 目录下所有子目录,因此模型版本变动不用重启容器,新增目录就会自动加载。想要多模型共存,可以用 config 文件启动:
model_config_list: { config: { name: "my_model", base_path: "/models/my_model", model_platform: "tensorflow" } }这里有个常见的目录结构误区,很多人直接把模型文件扔在 /models 下,结果 Serving 启动时找不到有效模型。正确结构必须是/models/模型名/版本号/saved_model.pb这种三层结构,模型名和启动参数 MODEL_NAME 保持一致才能加载成功。
4.3 自己写 Dockerfile 的坑
如果你希望把推理服务和业务逻辑打包进一个镜像,多阶段构建是很常见的做法。下面这个 Dockerfile 是我在生产里用过的精简版:
FROM python:3.10-slim as builder WORKDIR /app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt FROM python:3.10-slim WORKDIR /app COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages COPY app.py ./ COPY saved_model ./saved_model EXPOSE 8000 CMD ["gunicorn", "app:app", "-w", "2", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]这里有一个非常容易踩的坑:tensorflow 的完整安装包超过 500MB,如果直接把虚拟环境拷贝到镜像里,镜像体积会大得惊人,推送到私有仓库时就很痛苦。多阶段构建能有效控制最终层的大小,但即便如此,基础镜像也建议选 slim。CPU 推理的镜像不需要装 cuda 相关库,别把训练环境的依赖一股脑全复制进来。镜像体积直接影响到上线速度,我见过一个项目镜像 3 个多 GB,每次发布都要等好几分钟,后来删掉一堆无用依赖,体积降到不到 2GB,发布效率明显提升。
4.4 GPU 部署的环境细节
如果模型推理需要 GPU,部署前先确认宿主机装了 NVIDIA 驱动,并安装了 nvidia-container-toolkit。然后容器要加--gpus all参数:
docker run --gpus all -p 8501:8501 \ --mount type=bind,source=$(pwd)/models,target=/models \ -e MODEL_NAME=my_model \ -t tensorflow/serving:2.15.0-gpu千万别忘了在代码或者环境变量里配置显存增长,否则模型一启动就会预占全部显存,导致同一张卡上没法跑其他任务。TensorFlow 里可以这样设置:
import tensorflow as tf gpus = tf.config.list_physical_devices('GPU') if gpus: tf.config.set_logical_device_configuration( gpus[0], [tf.config.LogicalDeviceConfiguration(memory_limit=4096)] )更简单的方式是设置环境变量TF_GPU_ALLOCATOR=cuda_malloc_async,在 Serving 容器里也能生效。GPU 部署的前提是版本匹配,驱动版本、CUDA 版本、cuDNN 版本和 TensorFlow 版本必须一一对应,不然报错信息会非常抽象,有时候你甚至分不清是驱动问题还是容器问题。我的建议是尽量用官方镜像,把环境匹配的复杂度交给镜像维护者。
5. 监控、调优和掉坑记录
5.1 加指标才能知道模型服务有没有挂
很多部署上线的项目,只有收到报警才知道服务挂了,这是不对的。至少要在接口里暴露几个核心指标:QPS、平均延迟、P95 和 P99 延迟、当前 worker 数、错误数。prometheus_client 加 FastAPI 的 middleware 就能实现。例如:
from prometheus_client import Counter, Histogram REQUEST_COUNT = Counter("request_count", "Total request count") REQUEST_TIME = Histogram("request_time_seconds", "Request latency")然后在请求进来时计数加一,并用观测器记录耗时。Uvicorn 本身也自带访问日志,但业务侧的耗时统计更直观。日志我建议统一输出为 JSON 格式,便于在 ELK 或 Loki 里做检索。别小看这些东西,出了线上事故时,没有日志和指标,你只能靠瞎猜,而瞎猜的代价往往是深夜加班。
5.2 延迟优化三板斧
如果服务上线后延迟偏高,先从这三个方向排查。
第一,是不是每次请求重复加载模型?这个问题我遇到太多次了,模型加载写在请求处理函数里,一次请求加载一次模型,单看一次才几秒钟,但并发一上来就直接雪崩。正确做法是像前面例子那样把模型加载放到模块顶层或者应用启动阶段,进程启动时加载一次。
第二,有没有做输入批处理?对小请求来说,单条推理的固定开销占比很高。如果业务允许排队,就尽量让多个请求凑成一个 batch,TensorFlow Serving 的动态批处理就是干这个用的。如果你用的是 FastAPI,也可以在应用层攒 batch,但这会引入额外复杂度,我建议先评估是否真的有必要再加。
第三,模型本身有没有量化或优化。对 CPU 部署,float16 量化通常能带来可观的延迟收益;对 GPU 部署,还要看是否使用了 XLA。TensorFlow 2.x 里可以用tf.function(jit_compile=True)对部分算子做 XLA 编译,但要注意有些自定义算子不支持,可能会直接报错。调优时记得每次只改一个变量,同时对比优化前后的延迟分布,不要一股脑把所有手段都上,出了问题没法定位是哪一步带来的收益或损失。
5.3 高频问题速查表
| 现象 | 常见原因 | 解决办法 |
|---|---|---|
| import tensorflow 报 libcudart.so 找不到 | 系统缺少 CUDA/cuDNN,或版本不匹配 | 按官方版本矩阵安装对应 CUDA 和 cuDNN,推荐用 Docker 镜像 |
| 加载模型报 Unknown layer/object | 自定义层或损失函数没有传 custom_objects | 加载时传入自定义类 |
| 接口偶发 500,日志里 OutOfMemory | 模型被重复加载,或显存设置不当 | 确保模型全局加载一次;配置显存增长 |
| 并发升高后延迟成倍增加 | worker 数量太少或模型推理阻塞 | 增加 Gunicorn worker;考虑 TensorFlow Serving 批处理 |
| 预测结果和训练时不一致 | 输入预处理不一致,或模型量化后精度变化 | 核对预处理流程;量化前做校准 |
| 容器启动后立刻退出 | 模型路径挂载错误,或模型名称不一致 | 检查 /models 目录结构,确认 MODEL_NAME 与目录名匹配 |
5.4 部署完毕后,建议再做一遍端到端回归
部署上线前,我会准备一组带标签的测试样本,在本地先跑出基准结果,然后在部署环境通过 HTTP 接口跑同样的输入,比较输出差异。不要只测一个正常样本,至少要覆盖边界情况,比如空列表、超大数值、字符串类型、缺失字段等。这是成本最低的事故预防手段。建议把这组样本和脚本存到代码仓库里,每次改模型或升级环境后都执行一遍。我见过太多人只验证一个结果,然后上线当天被用户各种奇形怪状的输入打挂,最后白白熬夜排查到天亮。
我在实际部署里的一个体会是:Keras 模型部署的难点,大多不在模型本身,而在工程链路。真正稳的服务,都是在环境、模型格式、服务框架、监控运维这些看似不起眼的环节上下了功夫的。上面这套流程,我在 CPU 和 GPU 机器上都跑过,稳定程度比我预想的高很多。最后再给一个小建议:如果你还拿不准该选哪种部署方式,从 FastAPI 起步最合适,等流量大了再逐步迁移到 TensorFlow Serving,不要一开始就追求最重的架构。