ARTICLE DETAIL

资讯详情

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

MLflow+BentoML实现模型全生命周期管理:从实验到容器化部署

MLflow+BentoML实现模型全生命周期管理:从实验到容器化部署 我做了三年多算法也带过模型上线的小组最大的感受是模型本身从来不是瓶颈瓶颈在于“模型从训练完到真正提供服务”这段路。多少人遇到过这种场景同事把一个模型文件起名叫model_v2_final_0815(2).pkl放进网盘参数记在一个共享Excel里过了两周谁都不知道这个“最终版本”到底是哪棵树的参数、哪个数据集训练出来的。到了上线前环境一换依赖库版本对不上模型直接报错现场崩溃。这些问题的本质不是算法不行而是缺一套全生命周期的模型管理方案。今天我要讲的就是用 MLflow 和 BentoML 这两个开源框架把“实验记录、模型注册、版本管理、服务化打包、容器化发布”串成一条完整流水线。这套做法我实际在多个项目里落地过适合各种规模的算法团队哪怕你只是一个人维护模型也值得照着搭一次。1. 为什么选这套组合而不是自己搭一套1.1 瞎管模型版本迟早要出事故先聊聊我见过的一个典型案例。某个业务线的同学训练了一个分类模型效果不错然后他把模型文件发到群里让后端同事拿去部署。后端问这个模型对应哪个preprocess脚本特征顺序是什么样的原模型用的是什么版本依赖一问三不知只能重新翻代码、对特征、分析参数。折腾了两天终于部署上去结果运营发现线上效果不对想回滚到上一个版本不好意思上一个版本早就被覆盖了。这就是典型的“模型只管训练、不管发布”的后果。正常情况下一个模型要想被可靠地上线使用至少要满足三个条件它的训练参数、指标、数据依赖都能被追溯。它有明确的版本号并且能随时回滚到任意历史版本。它的运行环境Python版本、依赖库版本是可以复现的。这三个需求单靠 Git 和手工记录根本扛不住。Git 管的是代码但模型文件、指标、环境之间的关联Git 是不管的。1.2 MLflow和BentoML各自擅长什么MLflow 和 BentoML 是两套定位完全不同的工具但它们刚好互补。MLflow 解决的是“模型怎么管”的问题。它提供四块能力实验跟踪Tracking、模型注册Model Registry、项目打包Projects、模型格式Models。其中最核心的是 Tracking 和 Model Registry。Tracking 负责把每次训练的参数、指标、artifact模型文件、图片等自动记录下来Model Registry 则在 Tracking 之上增加了模型版本和阶段管理让一个模型可以从 Staging 流转到 Production。BentoML 解决的是“模型怎么发”的问题。它把训练好的模型、推理代码、依赖环境、服务配置打包成一个标准产物这个产物叫 Bento。Bento 可以本地起服务、可以做性能测试、可以打成容器镜像扔到 K8s 里。它自带的模型 serving 层处理了请求解析、并发调度、资源隔离这些工程细节不需要你再写一堆 web 框架胶水代码。两者合在一起就是一条从“跑实验”到“跑服务”的完整链路。能力项MLflowBentoML实验指标记录专业自动记录参数/指标不负责模型文件归档负责作为 artifact 存储不负责模型版本管理Model Registry 支持版本阶段内部有 model store启动 HTTP 推理服务不擅长需配合其他工具核心能力并发推理与资源管理不擅长核心能力容器化发布不提供bentoml containerize一条命令与现有 CI/CD 集成一般方便1.3 这套组合最终解决什么问题从结果倒推这套组合最终要解决三个问题第一训练过程不再是一堆随机事件。每次跑实验参数、指标、模型文件自动入库UI 上像看股票K线一样看模型效果变化哪次效果最好一目了然。第二模型上线是一个可重复的软件交付动作。拿 MLflow 里注册好的某个版本导入 BentoML打包成 Bento再容器化发布。这个过程和“用代码开发、git打标签、CI构建镜像”的节奏完全一致。第三回滚变得非常廉价。旧版本的 Bento 和镜像还在线上出了问题用旧镜像重新发布就行不需要重新训练、重新调参。这套组合并不是银弹但它足够轻。你不需要招聘专门的 MLOps 工程师只需要算法同学花半天时间把流程跑通之后每次训练和上线都按这个规范走效率提升是肉眼可见的。2. 环境准备先把MLflow服务跑起来2.1 本地起一个轻量Tracking ServerMLflow 的 Tracking Server 是整个流程的“账房先生”。所有实验的指标和模型 artifact 都会汇总到这里。最简单的起步方式是在本地用 SQLite 做后端存储artifact 存本地磁盘。我先给出常用命令再解释每个参数。mkdir -p ~/mlflow_data mlflow server \ --backend-store-uri sqlite:///~/mlflow_data/mlflow.db \ --default-artifact-root ~/mlflow_data/artifacts \ --host 0.0.0.0 \ --port 5000--backend-store-uri元数据存哪里比如 run 的参数、指标、时间、状态都用 SQLite 记录。生产环境建议换成 PostgreSQL。--default-artifact-root模型文件、指标图等大文件存哪里。本地测试用目录就行生产建议挂 S3 或者 NAS让训练机和部署机都能访问到。--host 0.0.0.0允许远程访问。如果只是本机玩用127.0.0.1更安全。--port 5000MLflow UI 默认端口。注意这个端口经常被 macOS 的 AirPlay 接收器占用如果起不来换个端口比如--port 8080。启动后浏览器打开http://localhost:5000会看到 MLflow 的 UI 首页。这个时候还没有任何实验记录页面是空的接下来我们要做的事情就是让它变得丰富起来。2.2 训练代码接入MLflow Tracking要让实验被自动记录核心改造点就一个在训练代码里调用 MLflow 的 API。我用一个非常简单的 Iris 分类模型做演示。假设你有一个训练脚本train.pyimport mlflow from sklearn.datasets import load_iris from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split mlflow.set_tracking_uri(http://localhost:5000) mlflow.set_experiment(iris-demo) # 同一个实验下的多次 run 会放到一个分组里 with mlflow.start_run(run_namerf-v1): X, y load_iris(return_X_yTrue) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) params {n_estimators: 100, max_depth: 5} model RandomForestClassifier(**params) model.fit(X_train, y_train) score model.score(X_test, y_test) # 记录参数和指标 mlflow.log_params(params) mlflow.log_metric(accuracy, score) # 记录模型文件并注册到 Model Registry后面细说 mlflow.sklearn.log_model( model, artifact_pathmodel, registered_model_nameiris_model, ) # 把特征列名和依赖文件存成 artifact方便回溯 with open(feature_columns.txt, w) as f: f.write(sepal_length,sepal_width,petal_length,petal_width) mlflow.log_artifact(feature_columns.txt)运行完这段代码打开 MLflow UI你会看到一个iris-demo实验里面有一条名为rf-v1的记录。点进去能看到 n_estimators、max_depth 这些参数accuracy 指标还有 model 这个 artifact。这里有几个细节值得注意mlflow.sklearn.log_model()不只是把 sklearn 模型 dump 成 pkl。它会生成一个标准的 MLflow Model 目录里面包含 flavor 信息比如是 sklearn 还是 pyfunc、依赖库版本、加载函数。后续其他工具加载这个模型时不需要关心它原本是什么框架训练的MLflow 会负责用正确的方式还原。mlflow.set_experiment()建议按照业务线或者项目来取名。比如funnel-ctr-v2、recommend-warm-start这样在 UI 里检索起来非常直观。对于 PyTorch 用户mlflow.pytorch.log_model()是类似用法。TensorFlow 则用mlflow.tensorflow.log_model()。如果你图省事MLflow 还提供了 autolog。在代码最前面加上mlflow.sklearn.autolog()然后正常写训练代码MLflow 会自动把模型参数、评估指标、模型文件全部记录下来。这个功能在跑 baseline 或者探索阶段特别好用。2.3 实验跑完如何从UI快速定位最优模型之前没有这套工具的时候我们都是靠记忆力找最优模型而实际上最靠不住的就是记忆力。现在有了 Tracking做法就变了。在 MLflow UI 里选择某个 experiment直接点指标列的英文名比如 accuracy就能排序最优的那次 run 一眼就能看到。点进去拿到 run id 和 artifact 路径代码里可以直接用runs:/run_id/model这个 URI 加载模型。我个人的习惯是每次实验结束会在 UI 里对比几次关键 run。筛选出效果最好的那次后给它加个 description比如“用了新增的 uuid 特征验证集 auc 提升 2%”。这个习惯坚持半年之后你回头看任何一次历史实验都能快速定位当时的决策逻辑。3. 模型注册与版本状态管理3.1 注册模型的正确姿势Tracking 记录的是“过程”Model Registry 管理的是“交接物”。你可以把它理解成 Git 的 tag跑完实验后选定一个 run 发布成正式模型版本打上版本号进入模型的版本管理流程。我在前面的训练代码里已经用了registered_model_nameiris_model所以在 MLflow UI 左侧导航的 Models 菜单里能看到一个名为iris_model的注册模型版本号默认是 1。如果之前没注册也可以事后手动注册。在 UI 里进入某次 run 的详情页找到 model artifact点击右侧的“Register Model”按钮输入模型名称即可。命令行方式也支持from mlflow.tracking import MlflowClient client MlflowClient() result client.create_model_version( nameiris_model, sourceruns:/RUN_ID/model, run_idRUN_ID, descriptionrandom forest with 100 trees, acc 0.9667 ) print(fcreated version: {result.version})注意source是runs:/run_id/model不是本地文件路径。这样做的好处是任何时刻你都能从 MLflow 追溯到这次模型对应的完整训练记录不会出现“模型文件还在但没人知道它怎么来的”的尴尬。3.2 Staging/Production就绪流程设计模型注册之后的另一个核心概念是 stage阶段。MLflow 内置了三个阶段Staging、Production、Archived。一般流程是训练完成后注册为模型的某个新版本。先在 Staging 阶段做离线评测、联调测试。通过后 transition 到 Production。旧版本在 transition 时自动进入 Archived。用客户端代码切换阶段from mlflow.tracking import MlflowClient client MlflowClient() client.transition_model_version_stage( nameiris_model, version1, stageProduction, archive_existing_versionsTrue, )这里的archive_existing_versionsTrue很重要它会在把当前版本置为 Production 的同时把之前处于 Production 的旧版本自动归档。这么做是防止线上同时出现两个“生产版本”的混乱状态。你可能觉得“阶段”这个概念多余但在实际协作中它非常有用。同一份模型算法团队在 Staging 上继续优化平台团队在 Production 上用稳定版本提供服务两边互不干扰。如果只是“最新版本”一个维度很难做到这种隔离。3.3 用models URI替代硬编码路径MLflow 里有一个特殊的 URI 协议models:/。它不关心模型实际存放在哪个磁盘而是通过“模型名 版本/阶段”来定位模型。比如models:/iris_model/1固定加载版本 1。models:/iris_model/Production加载当前处于 Production 阶段的那一版。models:/iris_model/latest加载最近注册的版本。这个协议最大的价值是解耦。你的训练脚本、评估脚本、推理服务都不需要知道模型文件的绝对路径只需要传递一个models:/iris_model/Production这样的逻辑地址。当线上效果需要回滚时不需要改代码只需要在 MLflow 里把旧版本 transition 到 Production服务下次加载模型时自然就会拿到正确的版本。这一点在后端和算法协作时特别省心后端同学不用等着你发来新路径他自己去 Model Registry 里取即可。4. BentoML打包从模型到可服务化Bento4.1 从MLflow导入模型MLflow 管好了模型版本接下来要把它变成能跑的服务。这里 BentoML 出场。BentoML 的做法是先把模型导入自己的 model store。它支持从 MLflow 导入一条命令就能完成import bentoml bentoml.mlflow.import_model( iris_model_svc, model_urimodels:/iris_model/Production, )执行后BentoML 会从 MLflow 拉取模型文件、元数据和依赖环境信息存到本地 model store。后续构建 Bento 时不需要再连接 MLflow这在部署环境下非常有用——部署机不一定要能访问训练环境。这段代码里我用了bentoml.mlflow.import_model在目前常见的 BentoML 1.3/2.x 版本里都可用。这里想提醒一点model_uri是 MLflow 侧的逻辑地址用Production这个阶段而不是某个具体版本号会让导入操作每次拿的都是当时的稳定版本。但实际项目里我建议在 CI 中构建镜像时使用具体的版本号避免“这次打包出的服务和上次服务逻辑相同但模型不同”的情况。4.2 编写service.py导入模型后编写一个service.py定义推理服务和 HTTP API。一个最小示例import numpy as np import bentoml from bentoml.io import JSON # 从 BentoML model store 取模型并转成 runner model_runner bentoml.mlflow.get(iris_model_svc:latest).to_runner() # 创建服务声明使用上面这个 runner svc bentoml.Service(iris-classifier, runners[model_runner]) svc.api(inputJSON(), outputJSON()) def predict(input_data: dict) - dict: # input_data: {data: [5.1, 3.5, 1.4, 0.2]} features np.asarray(input_data[data], dtypenp.float32).reshape(1, -1) result model_runner.run(features) # 返回类似 [0] 的类别标签 return {species_id: int(result[0])}bentoml.mlflow.get(iris_model_svc:latest)从 BentoML 本地 model store 里加载之前导入的模型。to_runner()把模型包装成 runner。runner 是 BentoML 对推理计算的抽象它负责加载模型到内存并对并发请求做批量和资源调度。业务逻辑要尽量写在svc.api装饰的函数里输入输出用 pydantic 模型或 JSON 等类型做声明式处理。服务定义好后本地启动bentoml serve service.py --reload --port 3000启动日志里会出现 Uvicorn 运行的地址。默认 BentoML 服务端口是 3000跟 MLflow 的 5000 不同别搞混。用 curl 测一下curl -X POST http://localhost:3000/predict \ -H Content-Type: application/json \ -d {data: [5.1, 3.5, 1.4, 0.2]}返回结果类似{species_id: 0}。这说明整个推理链路已经通了。4.3 bentofile.yaml的正确打开方式本地跑通只是第一步真正要交付的是 Bento。Bento 的构建规则写在bentofile.yaml里。一个典型配置service: service.py:svc labels: owner: ml-platform-team stage: staging python: packages: - scikit-learn1.3.2 - numpy1.24.3 - mlflow2.8.0 - bentoml1.3.7 models: - iris_model_svc:latest重点看几个字段service声明服务的入口文件和应用对象。python.packages和普通 requirements.txt 很像BentoML 在构建 Bento 时会根据这些包生成隔离的运行环境。这里强烈建议锁定具体版本号不要写scikit-learn这种裸包名。我曾经因为没锁版本构建出来的镜像里 sklearn 版本不一致推理结果和本地测试差得离谱。models声明这个 Bento 要打进去哪些模型。这里填的是 BentoML model store 里的名字加 tag。构建的时候BentoML 会读取这个文件并把 service.py、模型、依赖环境一起打包bentoml build构建完成后bentoml list可以看到生成的 Bento 列表每个 Bento 有一个类似iris-classifier:abc123的 tag。这里有一个很重要的特性Bento 是自包含的。你把这个 Bento 复制到任何一台机器只要装好 Docker就能把服务跑起来不再需要折腾 Python 环境和依赖安装。这在多人协作、多环境部署的场景下简直是救命级的便利。4.4 本地构建与调试我实际调试流程一般是三步。第一步用bentoml serve . --reload在本地带热重载地跑起来调 API 业务逻辑确认特征解析、异常处理没问题。第二步执行bentoml build构建正式 Bento然后bentoml serve iris-classifier:latest用构建出的 Bento 跑一次验证打包是否完整。这一步能提前暴露“代码能跑但依赖没打进 Bento”的问题。第三步用bentoml containerize打成容器镜像本地用 docker run 跑起来再测一次接口。到这一步基本就和生产环境一致了。每次调试遇到问题我会先去看构建日志BentoML 通常会明确指出是模型缺失、依赖解析失败还是构建目录不对。这种排查思路比瞎猜高效率很多。5. 容器化发布上生产前最后的工程化动作5.1 构建可移植镜像Bento 已经是一个热部署产物但实际生产环境往往要求的是 OCI 容器镜像。BentoML 提供了一个命令直接完成转换bentoml containerize iris-classifier:latest \ -t myregistry.example.com/ml/iris-classifier:1.0.0最终生成的镜像里自带运行环境、健康检查、日志采集配置。推到镜像仓库后直接用 K8s Deployment 或任何容器编排平台部署即可。这里有一点值得留意如果部署机需要访问私有镜像仓库记得先在部署环境里配置好拉取凭证。我第一次在生产环境发布时就是因为 K8s 节点拉不到私有仓库里的镜像折腾了半天才发现是 imagePullSecret 没配。5.2 接入CI/CD让发布变成流水线动作手动执行上面的命令当然也行但容易漏步骤。最好把 Bento 构建和镜像发布放进 CI。以 GitHub Actions 为例大致流程是name: build-and-push-model on: push: tags: - model-v* jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.10 - name: Install dependencies run: pip install bentoml mlflow - name: Import model from MLflow env: MLFLOW_TRACKING_URI: ${{ secrets.MLFLOW_TRACKING_URI }} run: | python -c import bentoml; bentoml.mlflow.import_model(iris_model_svc, model_urimodels:/iris_model/${{ github.ref_name }}) - name: Build Bento run: bentoml build - name: Containerize and push run: | bentoml containerize iris-classifier:latest \ -t ghcr.io/myorg/iris-classifier:${{ github.sha }} \ --push触发条件我习惯用 git tag比如model-v1.0.0这样每次发布都对应一个可追踪的版本。CI 从仓库拉取代码、从 MLflow 导入指定模型版本、构建 Bento、推镜像整个过程只要几分钟。算法同学不需要拥有生产集群的权限也能完成一次标准发布。5.3 部署之后的模型版本与回滚策略这套流程上线后回滚操作就变得非常清晰线上镜像基于哪个 Bento 构建Bento 里固化了哪个模型版本都有据可查。一旦服务出问题首选操作是重新发布上一个健康版本的镜像而不是在运行中的容器里手动替换模型文件。手动替换的破坏性很大因为模型、依赖、代码可能不匹配而且容器重建后状态又会丢失。我见过一些团队图方便把模型文件挂载到 K8s 的 emptyDir 里上线时直接把新权重文件拷进去。这样看起来很快但版本管理彻底失效回滚时连“原来的模型是谁”都说不清。正确的做法是模型版本变化就重新构建 Bento 和镜像走一次标准发布流程。模型管理和代码管理一样必须走“不可变产物”这条路。6. 常见问题与排查技巧实录6.1 MLflow端口与启动问题MLflow 默认监听 5000 端口这是个高频踩坑点。我在 Mac 上经常遇到“5000端口已被占用”的提示查到最后往往是 macOS 自带的 AirPlay 接收器。解决方案也很简单启动时指定一个高位端口mlflow server --backend-store-uri sqlite:///mlflow.db --default-artifact-root ./mlartifacts --port 8080另外如果训练机和 MLflow server 不在同一台机器记得设置MLFLOW_TRACKING_URI环境变量或者在代码里调用mlflow.set_tracking_uri(http://server-ip:8080)。忘了这一步的话最典型的报错是连接被拒绝或者模型注册失败。而且在云服务器上启动时需要把--host设为0.0.0.0才能被外部访问。6.2 模型找不到、阶段不对使用bentoml.mlflow.import_model()时如果报“Model version does not exist”之类的错误先检查三件事MLflow 里注册模型名称是否和代码里一致。model_uri里的阶段是否真实存在。很多用户注册完模型后忘记把版本 transition 到 Production结果models:/iris_model/Production找不到任何版本。MLflow server 地址是否填写正确远程模式下 URI 传错也会导致读取失败。还有一个容易忽略的坑MLflow 阶段转换不是注册时的默认状态新注册模型版本的 stage 是 None。需要手动 transition 或者确认。6.3 BentoML服务启动失败排查bentoml serve启动失败常见原因有两类。一是模型缺失。构建出来的 Bento 里没有包含模型启动时 runner 加载失败。解决办法是检查bentofile.yaml的models字段然后用bentoml list确认本地的模型 tag。二是依赖冲突。最常见的情况是本地环境 Python 版本和 bentofile 里声明的依赖版本不一致。比如本机 Python 3.8 用了 scikit-learn 1.3而 bentofile 里锁定的 scikit-learn 只能在 Python 3.9 上安装。构建阶段可能不报错启动加载模型时才开始哭。建议在写 bentofile.yaml 之前先查清楚模型训练时用到的各核心库版本尽量保持一致。6.4 版本敲定、依赖冲突这些隐形坑模型管理里最难排查的往往不是代码 bug而是“隐性版本漂移”。举个例子训练时用了 scikit-learn 1.2.0但某次本地环境升级到了 1.3.0你再次加载旧模型做推理结果可能出现 warning甚至预测结果不一致。这正是为什么我一直强调要锁定依赖版本的原因。生产项目里我都会在训练脚本里显式记录核心库版本。MLflow 的 log_model 会自动生成requirements.txt但在自定义记录时我也会手动补充一句mlflow.log_params({sklearn_version: 1.2.0, python_version: 3.9.17})这些元数据在回溯问题时非常有价值。BentoML 的 bentofile.yaml 里则应该使用精确版本号不要用大于号、星号这类模糊约束。另外如果镜像构建时下载 pip 依赖太慢可以在构建环境里配置 pip 镜像源。BentoML 在构建镜像时支持设置环境变量比如export PIP_INDEX_URLhttps://pypi.tuna.tsinghua.edu.cn/simple bentoml containerize iris-classifier:latest -t myregistry.example.com/ml/iris-classifier:1.0.0这样能明显加快构建速度。6.5 常见问题速查表现象可能原因排查与处理MLflow UI 打不开端口被占用host 配置不对换端口确认 bind 地址为 0.0.0.0检查防火墙训练代码连接 MLflow 失败tracking URI 未设置或填错设置 MLFLOW_TRACKING_URI 环境变量确认 server 地址可达模型注册成功但没有版本只创建了 registered model没创建 version用 log_model 或 create_model_version 生成版本import_model 找不到 Production 版本没有 transition 到 Production在 UI 或代码里将模型版本 transition 到指定阶段BentoML 启动报模型缺失bentofile.yaml 的 models 字段没写好或本地 model store 里没有对应 tag用bentoml list检查模型重新 import 并 build镜像构建时 pip 下载超时网络问题依赖源不稳定配置 PIP_INDEX_URL 镜像源重试线上结果和本地不一致依赖版本不一致或模型文件被替换核对 bentofile.yaml 和训练时的依赖版本避免手动改模型服务并发一高就超时runner 资源配置不足在 service 中配置 resources给 runner 分配更多 CPU/内存这些坑我基本都踩过一遍。每次排查到最后发现大多数问题不是出在模型本身而是出在“版本信息模糊”和“环境不一致”这两个根因上。用管代码的思路去管模型很多问题从一开始就不会发生。最后再分享一个小经验千万别一上来就追求大而全的机器学习平台。先把 MLflow 的 Tracking 用起来让实验记录自动化再接入 Model Registry 管好正式模型最后用 BentoML 把发布流程标准化。三步走完你已经拥有一套够用的全生命周期管理系统了而整个过程不需要自研一行平台代码。这套流程我在项目里跑了很久实测下来模型从训练完成到上线时间从之前的“按天算”压缩到了“按小时算”而且回滚再也不是靠翻聊天记录找模型文件了。这对算法交付来说已经是一次很值得的工程化升级。
返回列表