ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Keras模型Linux部署全指南:从环境配置到TensorFlow Serving

Keras模型Linux部署全指南:从环境配置到TensorFlow Serving 在 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 tensorflow2.15.0安装完成后用这一句验证python -c import tensorflow as tf; print(tf.__version__)如果你用的训练环境是纯 keras 3.x就补装一个指定版本pip install keras3.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_formath5)但加载时我更推荐使用 SavedModelmodel 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(titleKeras 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_modelPredictResponse) def predict(req: PredictRequest): try: data np.array(req.instances, dtypenp.float32) # 这里可以加入与训练流程一致的预处理例如归一化、resize result model(data).numpy().tolist() return PredictResponse(predictionsresult) except Exception as e: raise HTTPException(status_code500, detailstr(e))这段代码请根据自己模型的实际输入结构调整图片分类可能要先把 base64 解码再 resize文本分类可能要做 tokenization序列模型可能要做 padding。但核心思想不变HTTP 层只负责数据格式转换真正计算全在 TensorFlow 框架里。不要把业务规则写进推理接口否则后面维护接口的人会非常痛苦。注意开发时本地起服务测试没问题但部署到生产环境时千万别用uvicorn app:app --host 0.0.0.0 --port 8000这种单进程模式。单进程意味着同一时刻只能利用一个 CPU 核并发稍高就会出现请求排队。正确做法是用 Gunicorn 起多个 workergunicorn app:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 --timeout 120每个 worker 会独立加载一份模型。如果你的模型特别大比如超过 2GB4 个 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 typebind,source$(pwd)/models,target/models \ -e MODEL_NAMEmy_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 --frombuilder /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 typebind,source$(pwd)/models,target/models \ -e MODEL_NAMEmy_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_limit4096)] )更简单的方式是设置环境变量TF_GPU_ALLOCATORcuda_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 延迟优化三板斧如果服务上线后延迟偏高先从这三个方向排查。第一是不是每次请求重复加载模型这个问题我遇到太多次了模型加载写在请求处理函数里一次请求加载一次模型单看一次才几秒钟但并发一上来就直接雪崩。正确做法是像前面例子那样把模型加载放到模块顶层或者应用启动阶段进程启动时加载一次。第二有没有做输入批处理对小请求来说单条推理的固定开销占比很高。如果业务允许排队就尽量让多个请求凑成一个 batchTensorFlow Serving 的动态批处理就是干这个用的。如果你用的是 FastAPI也可以在应用层攒 batch但这会引入额外复杂度我建议先评估是否真的有必要再加。第三模型本身有没有量化或优化。对 CPU 部署float16 量化通常能带来可观的延迟收益对 GPU 部署还要看是否使用了 XLA。TensorFlow 2.x 里可以用tf.function(jit_compileTrue)对部分算子做 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不要一开始就追求最重的架构。
返回列表