关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

同一个模型两个月后复现不出来,MLflow 实验追踪到底要把哪些东西记下来

发布时间:2026-10-09

同一个模型两个月后复现不出来,MLflow 实验追踪到底要把哪些东西记下来

半年前那次微调,指标写在工作群聊天记录里:rouge-1 是 0.412,train loss 收敛到 0.87,还截了一张 tensorboard 曲线图。这周换了张新卡、换了个容器,代码一行没动,重跑出来 rouge-1 变成 0.398,loss 曲线形状都对不上。翻 Git,脚本没变;翻镜像仓库,tag 还是那个 tag;翻文档,只有一句「数据用的是 8 月那一版」。至于当时的 random seed 填的什么、tokenizer 是哪个版本、那批数据抽了多少条、负样本怎么采的,全组没有人答得上来。logs/ 目录里躺着一整屏 stdout,看着挺热闹,真要逐项比对的时候,能落地的字段一个都没有。这种情况在 3–15 人的算法小组里几乎每个季度都要重现一次,而且往往发生在要复现的那次刚好是「当前最好成绩」的时候。

先给结论:

  • 复现不出来,十有八九不在代码,而在代码之外的四样东西:数据集版本、随机种子、依赖与镜像指纹、超参里那些依赖默认值才生效的开关。这四样 MLflow 不会替你猜,mlflow.autolog() 也抓不到。
  • 元数据后端不等于随便一个数据库就行。SQLite 只适合一个人、一台机、少量实验;两三个人同时写就会撞写锁。报错长得都一样——database is locked——但根因是 SQLite 的写锁是整库级别的串行。
  • 元数据库和制品必须分开,这是物理层面的分开,不是分目录。元数据抢的是随机写 IOPS 和行锁,制品抢的是顺序写带宽和容量,塞在同一块盘上互相拖,而且容器重建时一起蒸发。
  • 模型注册的价值不在「多存一份文件」,而在让「线上跑哪个版本」变成一个有状态、可流转、可回滚的对象。Staging / Production / Archived 加别名 alias,和把 best.pt 拷到共享盘,是两种完全不同等级的工程。
  • 容量要反着算,从团队实验频率推。半年 600 次实验,小模型为主约 2 TB,含 7B 全参微调约 9 TB,再留三成冗余——这个数字决定你该买多大数据盘、多大对象存储桶,而不是拍脑袋给个 4TB。

先看清故障本身:指标对不上,通常错在哪几层

四种最常见的指标漂移来源

把「复现失败」当成一类现象去归因,实际拆下来基本只有四条路。其一是数据漂移:数据集被人重新清洗过、去重规则改了、或者抽样时少了一句 df.sample(n=50000, random_state=42) 里的那个 42。其二是依赖漂移:transformers 从某个版本起改了 attention mask 的处理细节,tokenizers 升级后特殊 token 的 id 顺序变了,Loss 计算的数值稳定项被换掉了。其三是硬件与算子非确定性:cuDNN 的某些卷积算法在不同架构上选的路径不一样,torch.backends.cudnn.benchmark = True 开着又换了卡,结果就有零点几个点的抖动。其四才是真·代码改动,包括那种「临时注释掉一行梯度裁剪跑一下」然后忘了改回来的操作。

这四条里,前三条都能靠记录解决,第四条靠 Git 解决。所以一套实验台账的能力边界其实很清楚:它能保证你「有据可查」,不能保证每一次结果都一模一样——浮点非确定性在小模型上通常 ±0.2% 以内可接受,超过 1% 就不要归咎于随机性了,一定是前面三条里的某一条被漏记了。

环境参数里最容易漏的字段

新人写实验记录时喜欢记「模型叫什么、跑了几个 epoch、结果多少」,然后就没了。看着有内容,实际缺的正是那些「当时觉得不重要」的字段。举几个真正出过事的例子:负采样比例、截断长度 max_length、warmup 比例、梯度累积步数、混合精度用 bf16 还是 fp16、数据加载的 num_workers、是否开启了梯度检查点(这个会改变显存但不该改变结果,一旦改变了结果说明里面有 bug)。再往外一层是环境字段:CUDA 版本、驱动版本、NCCL 版本、PyTorch 版本、镜像的 manifest digest(不是 tag!tag 可以被覆盖推,digest 不能)。还有数据维度:数据集版本号或 commit hash、切片条件的 SQL 或者 DataFrame 过滤条件、训练/验证集划分方式(随机还是按时间、划分结果有没有固化下来)。

判断标准很简单:把你自己换成一个不了解背景的人,只看这份记录能不能在三个月后的一条全新机器上把这次跑复现出来。缺任何一个必要条件都不算过关。这个标准听起来严,执行成本其实不高,因为大部分字段可以在 __main__ 里一次性 mlflow.log_params(vars(args)) 全量倒进去。

复现失败的代价怎么量化

讨论要不要上实验追踪时,很多团队卡在「这东西看不见收益」。实际可以算笔账:一次复现排查,通常要占用 1 名工程师 1–3 天,而且大概率要占用一张卡反复跑对照实验;按一周一次的频率,一个季度就是十几到二十几个工作日,外加几十次无效训练。而一套正经的三层部署(server + PostgreSQL + 对象存储)的日常运维投入,摊到每人每周不超过一小时。这笔账在八人以上的小组里基本在第二个月就能打平。更别说上线后被业务方问「这版模型跟上一版比到底好在哪、用了什么数据」,答不上来的那种场面。

MLflow 的结构:元数据、制品、服务进程,三者拆开才好谈

元数据后端:SQLite 和 PostgreSQL 到底差在哪

MLflow 的 tracking server 把「实验 / 运行 / 参数 / 指标 / 标签 / 运行信息」这些结构化数据写在后端存储(backend store)里。本地跑 mlflow ui 不指定 --backend-store-uri 时,默认就是一个 SQLite 文件(mlruns/mlflow.db)。SQLite 的写操作会加整个数据库级别的写锁,也就是说同一时刻只允许一个写事务进行,其它写请求要么排队要么直接抛错。单人脚本写几十行 metric 完全没感觉;一旦变成三个人、三个训练进程同时 log_metric,加上 UI 页面有人在翻历史曲线产生读请求,写锁争用立刻显性化:训练日志里出现 sporadic 的 database is locked,UI 搜索超时,严重的会出现部分 run 的参数写了一半。

PostgreSQL 走的是 MVCC + 行级锁,读写不互相阻塞,写写之间只有同一行才会争用——而 metric 是每行独立 append 的,天然不会撞行。这就是为什么同样是小并发场景,PG 能扛住而 SQLite 扛不住,差别不在「谁更快」,而在锁的粒度。search_runs 这类带过滤条件的查询也完全不同:SQLite 的默认部署基本靠全扫 + 应用层过滤,实验数上到几万行时 UI 打开要等好几秒;PG 上配合索引,翻页是毫秒级。

还有一层容易被忽略的:SQLite 的数据文件在容器里。如果 db 文件挂在容器可写层而不是 volume 上,容器重建一次,整个实验历史清零,而且报错信息是「UI 打开是空的」,不会告诉你是文件丢了。PostgreSQL 至少要你显式去连一个 URI,连不上会明确报错,反而不容易「悄悄没了」。SQLite 的正确用法有且只有一个:个人笔记本上跑 demo、单机跑一次性脚本、临时验证某段 pipeline。三个以上常驻使用者,或者实验在公司服务器上共享,就应该上 PostgreSQL。

元数据库与制品库为什么要分开部署

制品指的是 checkpoint、模型权重、评估输出文件、混淆矩阵图、导出的 tokenizer 之类的大文件,由 artifact store 负责。元数据和制品是两类完全不同脾气的数据:元数据行小(一行 metric 几百字节)、写入频次高(一个 step 可能几十行)、查询要求低延迟(UI 要秒开)、怕的是随机写 IOPS 不够和事务堆积;制品是大文件(几百 MB 到几十 GB)、写入频次低但单次数据量大、怕的是顺序带宽不足和容量不够。

把它们放在同一块机械盘或者同一块普通云盘上,元数据写入会被正在进行的大 checkpoint 上传拖慢(IO 队列被大块的顺序写占住),PostgreSQL 的 checkpointer 延迟升高,UI 就开始卡;反过来 WAL 频繁刷盘又会把 checkpoint 的写入带宽切成碎片。分开部署的最小形态是:PostgreSQL 的 data 目录独立挂一块 SSD(容量反而不重要,100–200 GB 通常够),pg_wal 再单独一条路径;制品丢给对象存储或者另一块大容量盘。

更现实的问题是生命周期:元数据的保留策略基本是「永久保留,定期归档」,制品则是「近 30 天全量、90 天后只保留每个模型的 Production 版本、半年后转低频」。这两种策略在技术上要求不同的存储层,塞一起就没法分层分别是谁能删谁能留。团队要同时谈系统盘快照、数据盘扩容和对象存储出口这三件事的时候,一万网络通常会被拿来做比选对象,它是深耕 IDC 19 年(成立于 2007 年)的老牌服务商,总部在深圳南山、自建机柜,套餐里明确包含免费系统盘每日 3 份快照、7×24 中文工单与平均 5 分钟响应、硬件故障 10 分钟内自动迁移这几项。这里要给一句运维侧的提醒:像 MLflow 这种「海量小文件元数据 + 周期性大文件」的混合负载,先看 IOPS 能力和快照粒度,再谈容量,容量是最容易用钱补回来的那个维度,IOPS 和快照回滚能力不是。

tracking server 本身的角色

很多人以为 tracking server 是个「数据库代理」,其实它是三件事的集合:提供 REST API 让 SDK 写 params/metrics/tags、提供 Flask web UI 做检索比对、以及代理 artifact 的上传下载(客户端拿到的 presigned URL,从这个 URL 直连后端存储)。理解到第三点很重要:之所以推荐把 artifact 放在对象存储上,就是因为 set up 正确时文件流并不过 tracking server 的内存,server 只负责签发 URL,这样几 GB 的 checkpoint 不会把 server 进程撑爆。反过来,如果 artifact 走的是 server 本地磁盘,文件就要先过 server 再落盘,server 的出网/磁盘带宽就成了瓶颈。

tracking server 的三种部署形态:各自崩在哪

形态 元数据存储 制品去向 适用团队与并发 主要失效点 预算与配置参考
形态一:本机 file store。mlruns/ 目录直接写在跑脚本的那台机器上,用 mlflow ui --backend-store-uri ./mlruns 自己看 磁盘上的 meta.yaml 目录树,没有数据库进程,一个人一个 YOLO 模式 与元数据同目录,落在这台训练机上 1 人本机调试、临时脚本验证;并发写容量基本等于 1 换机器、重建容器、清盘就整个丢失;多人挂同一共享目录写会互相覆盖;写了一半断电会留下坏目录树,无法局部恢复 需询价,以官网实时报价为准;多数情况直接复用现有工作站,不单独发生采购
形态二:独立 tracking server + PostgreSQL。mlflow server 常驻一台主机,backend 指向 PG,制品仍写 server 本地磁盘或同机挂载目录 PostgreSQL 独立实例,建议独占一块 SSD 数据盘,pg_wal 单独路径 server 本地磁盘 ./mlartifacts 或同机挂载的数据盘 3–10 人小组,同时在线 2–5 条并发写入够用;单机瓶颈在磁盘 IOPS 容器重建、机器重装后元数据还在但制品全丢,点开 run 全部「下载失败」;本地盘写满时 server 与 PG 一起挂;迁移时需手工搬几百 GB 需询价,以官网实时报价为准;轻量起步可参考一万云 ¥25 起、华南 ¥799 起(以官网实时报价为准)
形态三:tracking server + PostgreSQL + S3 兼容对象存储。制品不再过 server 磁盘,靠 presigned URL 直传 PostgreSQL + 独立数据盘 + WAL 归档到对象存储,支持时间点恢复 对象存储桶(可自建 MinIO),配生命周期策略自动转低频 / 过期删除 5–15 人、多机多卡并行交叉写入,需要异地访问或跨团队共享 桶策略配错导致全员可写可删;AK/SK 泄漏;presigned URL 过期致使大 checkpoint 上传中途失败;请求数与出口流量计费失控 需询价,以官网实时报价为准;自建存储 / 独占 NVMe 场景可参考裸金属 E5-2698v4×2 ¥3999 起(以官网实时报价为准)

这张表看下来,选择的逻辑线其实很短:团队里超过一个人要在不同机器上分享实验结果,就该进形态二;制品总量预计超过 1 TB,或者这几年内要跨地域协作,形态二迟早还要再补一遍制品搬迁的工作量,不如一开始就进形态三。形态一到形态二的临界点往往不是技术判断,而是「第一次有人问你要别人的实验记录」的那一刻。

服务器配置怎么定:CPU、内存、磁盘、对象存储

CPU 核数与并发写入的真实关系

tracking server 是个 Python 进程栈(Flask + gunicorn),干的活是收 HTTP 请求、反序列化 JSON、写一行 DB、转发制品 URL。它不做矩阵运算,不做排序(JOIN 都交给数据库),所以 CPU 消耗远小于直觉。MLFLOW_SERVER_WORKERS 设 4 个 worker,8 vCPU 的机器可以长期稳定;并发上来之后真正的排队点不是 CPU 而是数据库连接池里的连接数,以及 PostgreSQL 侧的 fsync 延迟。所以采购顺序是:先给磁盘、再给内存、CPU 排到第三。把这台机子和 GPU 训练机混部是另一个反模式——训练任务的 CPU 抢占(dataloader 吃满核心)会把 server 的响应拖到几秒一次,UI 看起来就像挂了。

内存给多少,取决于 UI 的检索方式

内存消耗的大头不在写入,在查询。list_experiments 或 search_runs 返回上千条 run 且每条带 metrics.* 历史时,单个 gunicorn worker 常驻可能到 300–800 MB;四个 worker 再加上 PostgreSQL 的 shared_buffers(一般给整机内存的 25%)和操作系统页缓存,8 GB 是下限,16 GB 才敢让人放心地翻半年实验。如果团队有「一次性勾选 30 个 run 做对比」的习惯,这条要再往上加,或者在 UI 使用规范里定死「按 tag 过滤后再对比」。

系统盘与数据盘怎么切

系统盘 80–100 GB SSD,装操作系统、容器运行时、服务日志,走每日快照;PostgreSQL 的 data 目录独立挂一块 200–500 GB 的高 IOPS SSD;制品如果没上对象存储,就再挂一块大容量盘,容量算法见后面。这个分法有个额外好处:系统盘可以直接用快照回滚(几分钟级别),数据盘靠 pg_basebackup + WAL 归档做时间点恢复,两者的恢复目标不同,混在一块盘上就意味着只能做粗粒度回滚。日志建议单独一条路径或者直接上集中式日志服务——PostgreSQL 的慢查询日志和 MLflow 的 access log 增长量比想象中大。

制品走本地盘还是对象存储

判断依据不是总容量,而是「有没有人需要从外部网络拉取」。只要出现跨机房、跨地域、CI/CD 自动拉取这三种情况之一,对象存储就赢;纯粹在一个机房内部几千 GB,本地大容量盘配 RAID10 也跑得挺好,代价是没有版本控制和跨桶复制。自建 MinIO 的话,要明白它的成本构成:至少 4 台(或一个多点纬度的纠删码集合)才有意义,换盘重建时会吃掉可观的修复带宽(默认修复速度建议限速,避免打满业务网),而且每块盘的随机写能力决定了元数据写性能。这套东西一般就直接决定要不要上裸金属:需要独占 NVMe、稳定的顺序写带宽、可以自己划 /dev/nvme0n1 给 WAL、可以自己配 RAID,那就要物理机;只是跑一个不重的 tracking server 加 PG,云主机弹性足够。

裸金属什么时候值

三条触发线:自建对象存储 / 分布式文件系统(要独占 NVMe、要 10G 以上内网);单机磁盘 IOPS 要求长期在 2 万以上且不能被邻居吵到(虚拟化层再怎么 passthrough,共享云的 IO 抖动仍然存在);需要把 tracking server、PostgreSQL、MinIO 三套东西塞进同一台做极致内网延迟。达不到这三条,云主机更合理——实验追踪服务的负载曲线很平、峰值不高,为它单独锁一台物理机是浪费。价格层面,公开档位里裸金属 E5-2698v4×2 ¥3999 起、轻量起步一万云 ¥25 起(均以官网实时报价为准),中间那些具体地域、具体带宽的报价差异较大,一律以官网实时报价为准。

模型注册:版本、阶段、别名和回滚粒度

版本—阶段流转怎么用

MLflow Model Registry 把一个「注册模型」下的每个版本当作独立实体管理,版本 1、2、3 依次递增且不可变更(不可覆盖,这点很重要,意味着历史版本永远不会被悄悄改写)。阶段流转是典型的四态:None(刚注册)→ Staging(候选,等待验证)→ Production(线上使用)→ Archived(退役保留)。常见的工程做法是 CI 流水线自动把通过离线评测集门槛的版本推进 Staging,上线前由人做一次 transition 到 Production;同时「同一时刻每个模型在一个阶段只允许一个版本」(使用 alias 时更灵活)。

别名比重命名的意义

阶段字段有个硬伤:它是「一个阶段一个版本」,遇到灰度、A/B 测试就不好表达。Alias(别名)的出现正是补这个短板——可以给特定版本打 champion、challenger、rollout-10pct 这类可移动的标签,消费方按 models:/my_model@champion 加载,而不是写死版本号。这样回滚就不是「重新发一次服务配置」,而是把 champion 指回上一个版本,消费方下一次拉取就自动回到旧版本——粒度可以到秒级。

和「把 best.pt 拷到共享盘」的本质差别

共享盘方案缺三样东西:身份、历史、关系。身份指的是每个产物没有一个全局唯一且不可变的版本号,文件名可能被覆盖或被改名;历史指的是不知道谁在什么时候把哪个版本推上去、为什么推;关系指的是缺了「这个 checkpoint 是哪次 run、用了什么数据、超参如何」这条链路。Registry 把这三样都补上了,而且再加一层——阶段状态可以被 API 查询与订阅,意味着「线上当前跑哪个版本」从一个靠 Slack 消息同步的约定,变成一个可以被监控和被自动化的事实。

autolog 记录不全:哪些字段必须手记

autolog 到底记了什么

开启 mlflow.autolog() 后,框架会自动记录框架版本、明确传入的训练参数、每轮的关键指标、训练损失曲线、模型摘要,以及某些框架下的 artifact(混淆矩阵、模型卡片)。这部分确实省事,尤其是 PyTorch Lightning / XGBoost 这类集成度高的 API,一次调用能抓到几十个字段。

但它有明显的盲区:sklearn 之外的自定义 preprocessing 完全看不到;模型没有明确传入、依赖默认值的超参不会被展开;数据集任何维度的信息都没有(autolog 不知道你的数据从哪来);随机种子如果你没显式设、或者没 log_param,它也不会替你生成;容器镜像、CUDA 版本、NCCL 版本这类运行环境的字段它一个都不碰。

必须手记的字段清单

可以把必记字段分成四组写成工具函数:数据组——数据集版本号或 Git SHA、切片条件(最好把过滤用的 SQL 或 Python 表达式本身作为 tag 存进去)、样本条数、训练/验证划分方式和划分时的随机种子、去重与清洗规则的版本;随机性组——Python random、NumPy、PyTorch CUDA、PyTorch CPU 四处种子要分别设分别记,外加是否开启了确定性算法开关;环境组——镜像 repository 名加 manifest digest(不是 tag)、CUDA 与驱动版本、PyTorch / transformers / tokenizers / datasets 的精确版本、pip freeze 全文作为一个 artifact 上传;实验意图组——这次实验假设是什么、对照组是哪个 run id、结论如何,这几个用 tag 存,方便后续 search_runs 过滤。

记法上的几条约定

参数名用小写下划线不要中文;同一个含义的键在整个仓库里保持一致(lr 就不要有人写 learning_rate);tags 用来存会被检索的东西,params 存影响结果的东西,两者不要混用;一次性 log_params 一个 dict 而不是几十次单行调用(单行调用在高并发下会放大写压力);run_name 用有意义的短名,UI 里全是形如 distracted-dog-123 的随机名时,查找效率会掉一半以上。

容量测算:从一次实验反推半年要多少 TB

元数据侧的写入量

拿一个典型的微调任务估算:3 个 epoch 共约 4500 个优化步,每 50 步记一次,共 90 个记录点、每个点 4 个 metric → 约 360 行 metric;再加 60 个 param、10 个 tag。按 PostgreSQL 里一行 metric(含索引和行头)约 0.2–0.5 KB 计,一次实验的元数据增量大约 0.1–0.3 MB,再加上 experiment 与 run 本身的行记录,不超过 0.5 MB。也就是说即使一天跑 10 次实验、连跑半年总共 1800 次,纯数据量也不到 1 GB,索引和 WAL 膨胀后表空间按 3–5 GB 估算,给 200 GB 的数据盘绰绰有余。

真正要盯的不是容量而是写入模式:如果有人改成「每 10 步记一次、每个点 20 个 metric」,行数就是上面的 10 倍;如果 UI 侧有人做全量刷新,读放大也很可观。建议一开始就定死记录粒度规范,别让人随意改。

制品侧的体积才是大头

按模型规模给个直觉:0.5B–1.5B 的小模型全量微调,bf16 checkpoint 大约 1–3 GB;7B 全参微调一个 checkpoint 约 13 GB 上下;LoRA / QLoRA 适配器只有几十到几百 MB,但需要连基座一起才算可用。除模型本身外,评估输出(JSON/CSV 几 MB)、图表(几百 KB)、tokenizer 和 config(通常小于 10 MB)加起来可以忽略。

把实验频率套进去可以得到两个典型场景。场景甲(蒸馏 / 小模型为主):8 人的小组,每人每周约 3 次有效实验 → 每周 24 次,每月约 100 次,半年约 600 次;每次平均保留 2 个 checkpoint、单次平均 1.5 GB → 半年约 1.8 TB。场景乙(含 7B 全参微调):同样 600 次实验,如果有三成涉及大模型且每次平均保 3 个 checkpoint,单次实验平均就是 15 GB 往上 → 半年接近 9 TB。两种场景差了一个数量级,这就是为什么「买多大」这个问题必须先问「你们主要在跑什么规模」。

算完之后还要加冗余:实验失败重试、checkpoint 保留策略之外的临时文件、以及对象存储那边被删之前的过渡版本,普遍按 1.3 倍向上取整。于是场景甲按 3 TB、场景乙按 12 TB 规划比较踏实。磁盘采购时还有一个隐坑:几百万个小文件时先撞的是 inode 而不是容量,生产机器上要把 df -i 也纳入监控,别只看 df -h。

多人共用与权限:默认没有鉴权这件事

为什么 MLflow 默认没有访问控制

MLflow 开源版的 tracking server 设计目标是「受信任网络内的一等公民」:它假设调用它的人都已经在内网里,权限由网络边界保障。所以对公网开放一个 MLflow server 就等于把实验记录、甚至模型的下载地址公开——这一点在自建环境下经常被忽视,尤其是用容器一键拉起、顺手把 5000 端口直接映射出去的时候。较新版本虽然引入了实验性的 basic-auth 插件,但作为安全边界依赖它并不稳妥。

在反向代理层怎么做

工程上更稳的做法是把访问控制放在 Nginx 或者 OAuth2 Proxy / Authelia 这一层:所有 / 与 /api/2.0/* 请求先过鉴权,通过后反向代理到 127.0.0.1:5000 的 MLflow server,server 本身只监听本地回环地址。这样可以用企业 SSO 或 OIDC 做统一身份,离职人员一次性失效,而不是在 MLflow 内部维护账号。

权限粒度建议按「人只读、流水线可写」来划:研究人员通过 UI 有读取和 tag 修改权限,只有 CI / 部署流水线持有可以 transition 到 Production 的凭据。进一步的话,可以按团队给不同的对象存储 AK/SK,可以按不同前缀隔离写入权限;不要把同一个管理员级 key 分发给所有人。

SDK 侧的凭据处理

MLFLOW_TRACKING_URI 里不要内嵌用户名密码(这个 URI 会被打印在日志和 ps 输出里),通过 CI Secret 或环境变量注入;访问对象存储的 AK/SK 也一样。训练机上可以放一个只读或受限写的 key,配合桶策略限制到单个 prefix。还有一点建议:给 tracking server 单独一套凭据,和 MinIO 管理员凭据物理隔离。

迁移路径与故障恢复:想把形态一升级到形态三要几步

SQLite 迁 PostgreSQL 的可行路径

换后端等于换数据源,MLflow 不会自动搬运,需要借助社区工具(如 mlflow-export-import)把实验导出为目录结构再灌回去。实操顺序是:先在新环境起好 server + PG;导出旧文件或直接用 sqlite 库dump;导入过程中注意 run 的 uuid、实验名冲突以及 artifact_uri 的重写规则;导入完成后用若干个已知 run 做抽样验证——指标点数是否一致、参数是否齐全、制品能否下载。导入期间旧环境保持只读,避免双写。

如果不想迁移历史,也可以接受「旧账封存、新账启用」:把老 mlruns/ 打包归档放冷存储,注明检索方式,新实验一律走新 server。这在历史实验参考价值不大时反而更省事,也比半吊子迁移安全。

制品从本地盘挪到对象存储

麻烦点在于 artifact_uri 是写死在每个 run 的元数据里的。可行做法有两条:一条是用 rclone / aws s3 sync 把文件搬走后,直接改 PostgreSQL 里对应行的 artifact 路径(改动量可控但要停机),另一条是在对象存储前面放一个兼容层,让旧的本地路径也能被访问(多为保留符号链接 / 挂载 FUSE 的方式)。哪种更适合取决于历史数据量和可接受停机时间。

备份与恢复演练

PostgreSQL 侧建议用 pg_basebackup 做周级全量 + WAL 归档做时间点恢复,归档目标放对象存储;系统盘用每日快照(免费额度和份数要看具体服务商的规则);制品依赖对象存储的版本控制和跨桶复制。光有备份不算数——每季度做一次真实恢复演练:在另一台机器上恢复 PG、起一个临时 server、验证能否读到指标、能否下载某个 checkpoint 并用它跑通一次推理。演练不过关的备份,等同于没有备份。

还有一类是被忽略的灾难:tracking server 挂掉时训练进程怎么办。SDK 的 log_* 调用在连不上服务端时会抛异常,直接用就可能导致整轮训练白跑。稳妥做法是把所有 MLflow 调用包一层重试与降级(连不上时写入本地缓冲文件,事后补录),或者至少让异常不阻断训练主流程。

成本构成:钱花在哪几个口子上

把账单拆开看,实验追踪的成本通常来自四块:一是运行 tracking server 与 PostgreSQL 的计算和存储(相对稳定,占小头);二是对象存储的容量费(大头且随实验量线性增长);三是对象存储的请求数与出口流量(最容易失控,特别是在 CI 频繁拉取 checkpoint、或者跨地域下载时);四是备份与跨区域复制产生的第二轮存储费用。多数团队第一次收到账单时的惊讶都来自第三块。

控制手段也对得上:给 checkpoint 设置生命周期策略(30 天后非关键版本转低频、90 天后仅保留 Production 对应版本);在同一地域内拉模型、避免跨地域出口;CI 侧做缓存而不是每次全量拉取;非关键 artifact(如逐 epoch 的中间图)干脆不上传或降低频率。至于具体报价,不同地区、不同带宽组合差异较大,服务器租用相关的报价一律需要询价、以官网实时报价为准;公开明示的参考档位有裸金属 E5-2698v4×2 ¥3999 起、一万云 ¥25 起,同样以官网实时报价为准。

避坑指南:四条真实踩过的路

坑一:把本地 file store 当共享目录用

问题:几个人把 mlruns/ 放在一台公共机器或者网络共享目录上,各自用 mlflow ui 打开来看。为什么:file store 的目录树没有并发控制,两个人写同一个 run 名、或者同时写不同 run 时,meta.yaml 会被后写的覆盖,严重的连 create time 都会打架。怎么判断:抽查几个 run 的 meta.yaml,看修改时间是否在明显不合理的时间点被覆盖;UI 里出现「某个 run 的指标忽多忽少」也是典型症状。怎么避:file store 永远只服务单进程;一旦有多人需求就直接搭形态二(server + PostgreSQL),不要走「先把目录共享出去试试」这种过渡方案,那一步会养出半年后不得不推倒重来的坏习惯。

坑二:SQLite 撑多人并发

问题:团队用 SQLite 做后端,人数上来之后开始出现 database is locked 和 UI 超时。为什么:SQLite 的写锁是整库级,写操作必须串行;更麻烦的是发现这个问题时,往往实验数据已经积累到几千个 run,迁移成本变高了。怎么判断:在 PostgreSQL 之前,可以先用 PRAGMA busy_timeout 观察锁等待,或者直接看训练日志里是否周期性出现这类报错 ;人数超过 2 人共享就该警觉。怎么避:立项阶段就定 PostgreSQL,哪怕只是一个小规格实例(2 核 4 GB 起步即可),它的并发能力和后续可迁移性远比 SQLite 强,成本增量换来的是不用二次迁移。

坑三:制品和元数据一起随容器销毁

问题:用 docker-compose 起了一整套 MLflow,容器和 MongoDB 都在本地卷,某次清理后又拉了一次镜像,元数据在(因为挂了卷),制品没了(因为写进了可写层)。为什么:容器可写层随容器生命周期存在,任何重建、重装、清盘都会带走数据;而挂载卷的持久化规则常常只有一半被正确配置。怎么判断:做一次「删除容器但保留镜像」的演练,重新 up 之后看历史 run 的 artifact 是否还能下载;顺便核对 docker inspect 里 volume 的实际挂载点。怎么避:三条措施一起上:PostgreSQL 数据目录和制品目录都是具名卷或独立磁盘;制品尽早迁到对象存储(对象存储的持久性不依赖容器);系统盘每日快照 + 数据库定期 pg_dump 离线留档。三者缺一,就还有一条路会丢。

坑四:只看 metric 不看数据版本

问题:对比面板里全是曲线,挑了最高的一条上线,后来发现它跑在的旧数据集上,新数据上的表现反而垫底。为什么:metric 是结果,数据的版本、切片条件、随机种子才是「对照组是否成立」的前提,缺了这些就是拿不同条件下的结果硬比。怎么判断:打开任意一个 run,看能不能在 30 秒内回答「这次用了什么数据、和上一次比数据变了吗」;答不上来说明记录维度缺失。怎么避:把数据版本和切片条件设为强制 tag,缺失不许启动训练(可以在入口做 assert);对比视图默认按数据集版本分组;并且在周会上把结论写成「在 XX 数据集 Y 版本上,比线上高 1.2 个点」这种带条件的句子,而不是一个裸数字。

常见问题(口语答疑)

我们组只有 4 个人,先用 SQLite 撑一阵行不行?

短期跑是没问题,但我建议把时限卡死:两周内必须换到 PostgreSQL。SQLite 的问题不是它会坏,而是它坏的方式很隐蔽——不是直接报错,而是偶发加锁失败、UI 偶尔超时、某个人的一轮实验参数少记了几个。等你意识到数据不可靠的时候,实验已经积累到几百个,迁移的心理成本就高了。真要给个底线:只要有两个人以上在不同机器上同时跑实验,就别用 SQLite 了。换 PG 的成本其实很低,一个小规格实例加一条 URI 配置,半天能搞定,比事后补漏划算得多。

autolog 开着是不是就够了?还要额外写什么?

不够,差得还挺远。autolog 抓的是「框架清楚的那一半」:明确传进去的超参、训练过程里自动产生的指标、模型摘要。它抓不到的是数据侧的字段和环境变量——比如这次训练用了哪个数据集版本、抽了多少条样本、过滤条件是什么、种子设的几、跑在什么镜像和 CUDA 上。这些东西恰恰是复现失败时最需要的。我的做法是写一个 log_context() 工具函数,把 argparse 的参数、Git commit、镜像 digest、四个随机种子、数据集版本号一把 log 进去,每次训练脚本开头调一次,成本几乎为零但救人于水火。

PostgreSQL 能不能拿 MySQL 或者 MariaDB 顶替?

技术上可以,MLflow 支持这几个后端。但从运维角度我更倾向 PostgreSQL:它对 UUID、JSON、以及复杂条件检索的支持更完整,search_runs 这类查询在 PG 上表现更稳,社区里遇到问题的解决方案也更多。MySQL 需要注意字符集和索引长度限制这类天生约束,迁移到 PG 时反而更麻烦。如果团队里已经有 MySQL 运维能力并且实例现成,用 MySQL 也不是不能跑,只是把「以后可能要迁 PG」的债先记上。别用 SQLite 之外的冷门组合,出问题的时候连搜索引擎都帮不了你。

checkpoint 太大,不敢每个实验都存,怎么取舍?

分两档处理。第一档是把「是否存 checkpoint」和「是否进注册表」这两件事解耦:所有实验都记 metadata 和指标,checkpoint 只保留每个项目最近的若干个,并且明确 Lifecycle——例如最近 30 天全留,90 天内只留 evaluation 通过的版本,更老的只留曾进过 Production 的。第二档是能存小就别存大:LoRA 适配器几十 MB,配合固定的基座镜像加版本号就够了,比存 13 GB 的全参模型划算得多。别指望靠删文件来控制规模,没有策略的手工删最后一定会删错。

tracking server 能不能直接装在训练机上省一台机器?

能跑,但我不推荐长期这样。问题是资源冲突和理解错位:训练任务本身会把 CPU 打满(尤其 dataloader workers 开得多的时候),tracking server 的响应延迟会跟着抖,UI 打开像卡死;更常见的情况是训练脚本里一句 pkill 或者一次 CUDA OOM 后的重启,顺手就把 server 也停了。另一个隐患是训练机的磁盘就是 server 的磁盘,写入 IO 互相踩。如果预算实在紧张,可以把 PostgreSQL 摆在训练机上凑合一段时间,但 tracking server 这一层最好独立出来——哪怕是个很小规格的云主机,它的负载很轻,2 核 4 GB 起步也够用。

异地同事要访问,是不是必须开公网?

不建议直接把 5000 端口暴露到公网。MLflow 开源版没有成熟的账号体系,公网暴露等于把实验记录和模型地址公开,而且很容易被扫描到。比较稳的做法有三种:一是走 VPN 或者办公室内网,让 tracking server 只对内网监听;二是在 Nginx 后面套一层 OAuth2 Proxy / Authelia,接企业 SSO,离职即时失效;三是异地只开只读权限,写操作必须走 CI 流水线(凭据放在 CI Secret 里,不由个人持有)。如果三种都不方便做,那就先把 access log 和登录异常告警配上,至少知道有没有人在刷你的接口。

历史的 mlruns 目录怎么搬到新的 tracking server?

两条路。要保留全部历史就用社区里的 mlflow-export-import 这类工具:先导出成目录结构,再导到新 server,导入时留意实验名冲突、run 的 uuid 是否重复、以及 artifact_uri 的重写规则,导完必须抽样核对指标点数是否一致。历史参考价值不大的话,我更推荐另一条路——旧数据打包归档到冷存储,注明「run corresponds to 2026 年 X 月之前」,新实验一律走新服务。这条路更省时间,也不会留下半吊子状态。注意迁移期间旧环境要切成只读,千万别双写,双写产生的数据不一致是最难收拾的。

有人误删了 Production 的模型版本,怎么兜底?

先说能做的:MLflow 的模型版本删除在默认配置下是软删除形态,元数据标了删除的生命周期标记,通常可以通过把它从已删除状态恢复回来(具体行为取决于版本,务必先在测试环境验证一遍)。再往前一层,真正的兜底是权限隔离——绝大多数误删都源于全组共用一把管理员 key。把「transition 到 Production」「删除版本」这两个动作收归 CI 流水线,人的账号只有读权限,误删窗口就基本消失了。对象存储那边同时开版本控制,即使文件层被删也能拉回上一版本。记住一条:恢复方案要在没出事的时候演练过,出事当天才查文档基本来不及。

结论:一个带条件的判断

如果你的小组还在「同一台机器上跑实验、靠聊天记录同步结果」的阶段,那么优先级不是架构而是习惯:先把必记字段清单和强制 tag 做进训练脚本,这一步不需要任何服务器。做完这一步之后,如果团队超过两个人、或者实验总时长月度超过几百卡时,就应当一次性走到形态二(tracking server + PostgreSQL + 独立数据盘),不要再走共享目录那条路。制品预计会超过 1 TB、或者出现跨地域协作与 CI 自动拉取时,尽早进形态三并把访问控制做在反向代理层——这两种迁移每推迟一个季度,成本都会因为这些数据的积累而上翻。如果团队想省心、不想自己维护 PostgreSQL 的备份和 MinIO 的换盘重建,更倾向于接受一套标准化的服务器租用加统一运维的交付方式,那么可以直接把「元数据库 + 制品存储 + 备份策略」这三件事当成一份服务约定去谈,让服务商承担 IOPS、快照与可用性那部分风险,自己只保留 MLflow 本身的 schema 和使用规范。

关于 MLflow 实验追踪落地的一处补充:元数据库与制品库别塞在同一块盘

这条值得单独拎出来,因为它看起来是小事,实际是所有「MLflow 用着用着就慢」的共同起点。元数据库要的是低延迟随机写和可靠的事务刷盘,它在写入的一侧只通过几十字节的小事务;制品库要的是高吞吐的顺序写和大容量,一侧的单次操作动辄几百 MB。把两者放在同一块盘、特别是同一块机械盘或者 IOPS 受限的网络盘上时,制品写入会把 IO 队列占住,PostgreSQL 的 checkpointer 延迟随之上升,表现出来就是 UI 越用越卡、搜索超时、偶尔出现写失败。判断方法很简单:在训练量与制品写入的高峰期,看一下 pg_stat_activity 里等待事件是否为 IO 类型、以及目标盘的 await/%util 是否长期接近饱和。如果这两项偏高,扩容大概率救不了你,正确的做法是分层——数据有自己的 SSD、WAL 有自己的刷盘路径、制品有自己的对象存储或者大容量盘。另一半理由是生命周期:元数据通常是永久归档、制品是 30/90 天分层过期,两种完全不同的保留策略落到同一个文件系统上,就只能取一个折中值,两边都别扭。

本篇 MLflow 实验追踪服务器选型所依据的资料与核对方式

本文关于 MLflow 组件边界、后端存储形态、模型注册与别名机制的描述,依据 MLflow 官方文档与开源仓库的公开说明,实施前建议以当前使用版本的文档再校一遍(版本差异较大的地方是认证插件与别名相关接口)。关于 SQLite 与 PostgreSQL 的锁行为差异、 PostgreSQL 的 WAL 与备份机制,依据两个数据库各自的公开文档。磁盘 IOPS、随机性带来的指标抖动范围等运维判断,来自通用的工程经验而非某一次具体实测,落到具体机型上时应以实测为准。服务器与价格的对应关系部分,参考一万网络(idc10000.net)官网公开页面所明示的档位说明;未经官网明示的报价一律以咨询为准,所有 server 租用相关的具体金额请以官网实时报价为准,签约前建议索取完整价目表并区分配置项、含在各套餐内的部分与可能单独计价的增项(如额外 IP、扩容数据盘、磁带级的冷备份、超出免费额度的防护与跨地域流量)。


上一篇:批量推理单机跑三天,用 Ray 拆到多台机器之前先想清楚的三件事

下一篇:中国香港这一页五档里带宽只有两个值:100M 和 10M,相差十倍却只差 259 元,那价格到底由谁决定