1. 从一次“翻车”说起:为什么要做训练管理平台
去年秋天,我接手了一个目标检测的迭代项目,团队里三个人,前后跑了大概四十多轮训练。最开始大家各跑各的,命令行参数靠微信群里发,权重文件命名靠自觉,结果第三周就出了事:一个同事以为best.pt是最新版本,直接拿去做了导出和部署,实际上那是五天前的旧权重,mAP 差了将近 6 个点。等发现的时候,下游已经基于错误模型做了一轮标注回流,白白浪费了两天。
那次之后我就下定决心,得把训练这件事“管起来”。所谓 YOLO 训练管理平台,说白了就是把数据版本、训练配置、权重产物、评估指标、导出记录这几件事,用一个统一的界面和一套统一的规则串起来,让每一次训练都可追溯、可复现、可对比。它解决的不是“模型能不能训出来”的问题,而是“训出来之后你还找不找得到、敢不敢用、能不能复现”的问题。适合谁来参考?我的判断是:只要你手上有超过两个人在跑训练,或者你自己一个月要跑十轮以上,这个平台就有价值;如果你只是偶尔跑个 demo,那确实没必要上这套东西。
下面这 23 条教训,是我从零把这个平台搭起来、又迭代了四个大版本之后攒下来的。有些是架构层面的,有些是特别琐碎的坑,但每一条都真实地让我加过班。
2. 平台整体设计与技术选型思路
2.1 先想清楚“管理”的边界在哪
我见过不少人一上来就想做一个“大而全”的 MLOps 平台,结果三个月过去连训练都还没跑通。我的建议是先把边界划死:训练管理平台只负责“训练生命周期”,不负责数据标注工具本身,也不负责线上推理服务。它要做的是接收一个已经标注好的数据集版本,接收一份训练配置,调度一次训练任务,收集日志和指标,产出可追溯的权重,最后支持一键导出。
这个边界一旦定下来,技术选型就清晰了。我用的是 FastAPI 做后端,因为训练任务的元数据读写非常频繁,异步 IO 比同步框架舒服太多;前端用 React 加 Ant Design,主要是表格和表单多,组件库能省大量时间;任务调度没有上 K8s,而是用了简单的进程池加队列,因为我们的机器就四台,上 K8s 属于杀鸡用牛刀。这里第一条教训就来了:不要为了技术先进性选型,要为了运维成本选型。K8s 很香,但你得有人维护它。
2.2 数据库表结构决定了平台能走多远
我踩过最大的一个坑,是最开始把训练配置直接存成一个 JSON 字符串塞进一张表里。前两个月很爽,写进去读出来就行。等到要做“对比两次训练的差异”功能时,我傻眼了——得把两个 JSON 解析出来做 diff,还没法用 SQL 过滤。后来重构,拆成了experiment、run、config、metric、artifact五张核心表。
| 表名 | 作用 | 关键字段 |
|---|---|---|
| experiment | 一次实验的顶层分组 | id, name, dataset_version, owner |
| run | 一次具体的训练执行 | id, experiment_id, status, start_time |
| config | 训练超参与结构配置 | run_id, key, value |
| metric | 逐 epoch 的指标记录 | run_id, epoch, map50, loss |
| artifact | 权重、日志、导出文件 | run_id, type, path, hash |
这个拆法的好处是,任何一次训练都能通过run_id把所有东西串起来,而且指标可以按 epoch 做趋势查询,配置可以做精确匹配。教训二:元数据一定要结构化,别偷懒存 JSON。你省下的那点开发时间,后面会以十倍的代价还回来。
2.3 权重文件的管理策略
权重文件是平台里最占空间、也最容易乱的东西。我的做法是:所有权重统一存到一个对象存储目录下,路径规则是{experiment_id}/{run_id}/{epoch}_{metric}.pt,同时在数据库里记录文件的 SHA256。这样有两个好处,一是重名问题彻底解决,二是可以通过 hash 判断两个权重是不是同一个文件,避免重复存储。
这里有个细节值得说:YOLO 训练过程中会不断产生last.pt和best.pt,如果每个 epoch 都存一份,磁盘很快就爆了。我的策略是只保留last.pt、best.pt,以及每 10 个 epoch 存一个快照。教训三:权重保留策略要提前定,别等磁盘满了再删。我们第一次磁盘告警的时候,清理脚本还没写,只能手动删,删错了一个还被同事骂了半天。
3. 核心功能模块的实操要点
3.1 训练配置的模板化与继承
如果每次训练都让用户从头填一遍超参,那这个平台就是失败的。我的做法是配置模板加继承:先定义几个基础模板,比如yolov8n_base、yolov8s_finetune,用户创建 run 的时候选一个模板,然后只改需要改的字段。模板本身也是存在数据库里的,可以版本化。
具体实现上,我用了一个config_template表,存模板的默认值,然后config表只存和模板的差异(diff)。这样查询的时候做一次 merge 就行。好处是,当我想批量调整某个基础学习率时,只需要改模板,所有引用它的 run 在展示时都会反映出来——当然,已经跑完的 run 要冻结,不能受影响。教训四:模板要支持冻结,历史 run 的配置不能被后续修改污染。这个坑我踩过,改了一次模板,结果历史对比全乱了。
3.2 任务调度与资源隔离
四台机器,每台两张卡,一共八张卡。最开始我没有做资源隔离,谁先提交谁先跑,结果经常出现两张卡被同一个任务占满、另一个任务排队等半天的情况。后来加了一个简单的调度器:每个任务声明需要的 GPU 数量,调度器维护一个 GPU 占用表,有空闲才分配。
这里的关键是要处理任务失败后的资源释放。我遇到过一次,任务进程崩了,但调度器里的占用记录没清,导致那张卡一直显示“占用中”,实际上早就空了。解决办法是加一个心跳机制,任务进程每隔 30 秒往数据库写一次心跳,调度器定期扫描超时的心跳,把对应资源标记为释放。教训五:任何资源分配都必须有超时回收机制,这是分布式系统的铁律,哪怕你只有四台机器。
3.3 日志与指标的实时采集
YOLO 训练的输出是标准输出流,里面混杂着进度条、loss、mAP 等信息。我一开始想直接解析 stdout,后来发现格式不稳定,不同版本输出还不一样。最后改成了两条路:一条是让训练脚本在每个 epoch 结束时,主动往一个 JSONL 文件里追加一行结构化指标;另一条是平台定期读取这个文件,解析后入库。
# 训练脚本里每个 epoch 结束时调用 import json def log_epoch(run_id, epoch, metrics): record = {"run_id": run_id, "epoch": epoch, **metrics} with open(f"logs/{run_id}.jsonl", "a") as f: f.write(json.dumps(record) + "\n")这样做的好处是,指标格式由我们自己控制,不依赖 YOLO 的输出格式。教训六:不要解析第三方工具的标准输出,让它主动输出结构化数据。这个原则适用于所有训练框架,不只是 YOLO。
3.4 一键导出与格式转换
训练完之后,最常见的需求是导出 ONNX 或者 TensorRT。我的平台里做了一个导出队列,用户选一个权重,选目标格式,提交后后台异步执行导出,完成后通知。导出脚本用的是 YOLO 官方的export接口,但加了一层封装,把导出参数也记录进数据库。
这里有个细节:导出 ONNX 的时候,opset版本和dynamic参数会直接影响后续推理的兼容性。我默认用opset=12、dynamic=False,因为大部分部署环境对动态 shape 支持不好。教训七:导出参数要显式记录,不能只存一个“已导出”状态。有一次同事问某个 ONNX 是怎么导的,我完全想不起来,只能重新导一遍。
4. 那些让我加班的坑与排查实录
4.1 数据集版本错乱导致的“幽灵掉点”
有一次,一个同事报告说同一个配置、同一个数据集,两次训练 mAP 差了 4 个点。我查了半天,最后发现是数据集版本的问题:平台里的“数据集版本”只是一个标签,实际数据目录是共享的,有人在两次训练之间往目录里补了一批图。教训八:数据集版本必须是不可变的快照,不能是一个可变目录的别名。后来我改成了每次注册数据集版本时,计算整个目录的 hash,训练时校验 hash,不一致就拒绝启动。
4.2 显存泄漏与 OOM 的排查
YOLO 训练跑久了偶尔会 OOM,尤其是在做实例分割的时候。排查下来有两个原因:一是 dataloader 的num_workers设太大,每个 worker 都占一份显存;二是验证阶段没有torch.no_grad(),导致计算图一直累积。教训九:验证和推理阶段一定要加torch.no_grad(),这是最容易被忽略的显存杀手。另外,num_workers我一般设成 4 到 8,再大收益递减还容易出问题。
4.3 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 训练启动即失败 | 配置字段缺失或类型错误 | 检查 config 表与模板 merge 结果 |
| 指标不更新 | JSONL 文件路径不对或权限问题 | 检查训练脚本写入路径 |
| 权重找不到 | artifact 记录与磁盘不一致 | 校验 SHA256,检查清理脚本 |
| 导出失败 | opset 或 dynamic 参数不兼容 | 换 opset,关闭 dynamic |
| 任务卡在排队 | 心跳超时未回收 | 检查调度器扫描日志 |
教训十:把常见问题做成速查表,比写文档有用得多。文档没人看,速查表贴在群里,大家遇到问题第一反应就是查表。
4.4 关于“一键部署脚本”的教训
我一开始写了一个一键部署脚本,想把平台本身也一键部署到新机器上。结果脚本越写越长,最后有 800 多行,里面全是各种环境判断。教训十一:一键脚本的复杂度会随环境数量指数增长,超过 200 行就该考虑容器化。后来我把平台本身打成了 Docker 镜像,部署脚本缩减到 30 行,世界清净了。
5. 从“能用”到“好用”的进阶优化
5.1 训练对比视图的设计
平台跑起来之后,最受欢迎的功能是训练对比。用户选两个 run,平台把两条 mAP 曲线画在同一张图上,下面列出配置差异。这个功能看起来简单,但要做好不容易。我的实现是:指标从metric表按 epoch 查出来,配置差异用 diff 算法算,前端用 ECharts 画图。
这里有个体验细节:对比的时候要允许用户选择对齐方式,是按 epoch 对齐还是按时间对齐。按 epoch 对齐适合比较收敛速度,按时间对齐适合比较训练效率。教训十二:对比功能要提供多种对齐维度,单一维度满足不了真实需求。
5.2 多模态分析场景下的平台扩展
后来我们接了一个智慧交通的项目,需要在目标检测基础上叠加多模态分析,比如结合文本描述做事故判断。这对平台提出了新要求:训练产物不再只是一个检测模型,还可能包含一个文本编码器。我的做法是在artifact表里加了一个modality字段,区分vision、text、fusion,导出的时候按 modality 分组。教训十三:平台的数据模型要预留扩展字段,别把结构写死。这个字段加得很及时,否则又得重构。
5.3 边缘设备部署的适配
有一个项目要在 AGX Orin 上跑,需要把模型转成 TensorRT。这个过程比想象中麻烦:CUDA 版本、TensorRT 版本、PyTorch 版本三者必须匹配,错一个就报错。我在平台里加了一个“目标环境”配置,记录目标设备的 CUDA 和 TensorRT 版本,导出时自动选择对应的参数。教训十四:导出不是终点,目标环境的兼容性才是。建议在平台里维护一个环境矩阵表。
5.4 学生专注度检测这类场景的特殊处理
有个教育方向的同事用平台跑学生专注度检测,用的是 YOLOv8。他的数据特点是类别极度不均衡,专注的样本远多于走神的样本。平台本身不解决算法问题,但我加了一个功能:在数据集注册时统计类别分布,训练前给出警告。教训十五:平台要在训练前做数据体检,把问题暴露在启动之前。这个功能帮他省了很多次无效训练。
6. 工程化与团队协作的经验
6.1 权限与操作审计
平台上线第三个月,出了一个问题:有人误删了一个正在用的实验。虽然权重还在,但元数据没了。从那以后我加了软删除和操作审计,所有删除操作只是标记deleted_at,并且记录操作人和时间。教训十六:删除必须是软删除,物理删除只能由管理员在确认无引用后执行。
6.2 训练脚本的版本管理
训练脚本本身也是代码,也会变。我要求所有训练脚本必须提交到 Git,平台在启动任务时记录 commit hash。这样任何一次训练都能对应到确切的代码版本。教训十七:训练脚本的版本要和 run 绑定,否则复现就是空谈。
6.3 通知与协作
训练跑完要通知,失败也要通知。我一开始用邮件,后来发现大家都不看邮件。改成了企业微信机器人,训练完成或失败直接推到群里。教训十八:通知要发到大家真正在看的地方,这个道理很简单,但很多人做平台时会忽略。
6.4 成本核算
GPU 是花钱的。我在平台里加了一个简单的成本统计:每个 run 记录 GPU 占用时长,乘以单价,算出这次训练花了多少钱。这个功能上线后,大家提交任务明显谨慎了。教训十九:把成本可视化,比任何管理制度都管用。
7. 踩过的那些“小”坑
7.1 时区问题
训练日志的时间戳用的是 UTC,前端展示用的是本地时间,结果对比的时候差了 8 小时,一度以为是数据错乱。教训二十:全链路统一时区,存储用 UTC,展示时转换。
7.2 文件名里的特殊字符
有一次用户上传的数据集里有个文件名带空格和中文,导致训练脚本读取失败。教训二十一:所有路径处理都要做转义和校验,别假设文件名是干净的。
7.3 大文件上传
数据集动辄几个 G,用普通表单上传经常超时。后来改成了分片上传加断点续传。教训二十二:大文件传输必须分片,这是基本工程常识,但第一次做的时候确实没想到。
7.4 日志轮转
训练日志不轮转,几个月就堆了几十 G。教训二十三:日志必须配置轮转和清理策略,按大小或按时间都行,就是不能不管。
8. 我个人在实际操作中的几点体会
这 23 条教训里,如果只能留三条,我会留:元数据要结构化、资源分配要有超时回收、删除要软删除。这三条是真正让我少加班的。平台这个东西,功能多不多是次要的,稳不稳、可不可追溯才是核心。我见过太多团队把精力花在炫酷的可视化上,结果底层数据一团糟,最后没人敢用。
另外分享一个小技巧:平台上线初期,我强制要求所有训练必须通过平台提交,哪怕平台当时还很简陋。这个“强制”很关键,因为只有真实使用才能暴露问题。如果允许“特殊情况走命令行”,那平台永远长不大。等平台稳定了,再开放命令行入口也不迟。
后续如果还要扩展,我会优先做两件事:一是把数据集版本和训练 run 的关联做得更细,支持按类别统计;二是接入模型评估的自动化,训练完自动跑一遍验证集并生成报告。这两件事对实际项目的帮助,比再加十个可视化图表都大。