做 YOLO 训练的人,最崩溃的往往不是模型不收敛,而是手里同时压着三十个训练任务,没人说得清哪个实验用了哪份数据、改过哪些参数、权重存在哪个目录。我们折腾大半年的 YOLO 训练管理平台,经历就是从这种混乱里长出来的。平台本身不算复杂,本质上是把数据版本、训练调度、实验追踪、模型交付这几环串成一条流水线。今天想把这 23 个实践教训摊开聊,适合正在搭内部训练平台、或者打算把 YOLO 流程规范化的团队参考。
1. 先说结论:YOLO 训练的痛点根本不在"训练"
1.1 平台到底解决了什么问题
很多人一开始会把训练管理平台理解成"远程跑训练任务的网页"。实际上,单纯的远程执行连锦上添花都算不上,因为 YOLO 本身已经能通过命令行在任意机器上跑起来。真正让团队崩溃的是上下文丢失:每个实验用了哪份数据集、哪个 commit 的代码、哪组超参、best.pt 放在哪个目录,这些信息散落在聊天记录和文件夹命名里。今天跑出一个 mAP 不错的模型,两周后想复现,连当时验证集是从哪份数据里切出来的都查不到。
所以平台解决的不是"能不能训练",而是"每次训练能不能留下足够上下文,让结果可以被解释和复现"。我们做的 YOLO 训练管理平台,核心组件只有四个:数据集版本服务、训练任务调度器、实验追踪服务、模型注册表。前端页面和可视化只是外壳,真正重要的是背后这条链路。这决定了后面所有功能优先级,也直接引发了 23 个教训里的很多条。
1.2 23 个教训从哪来
这些教训不是从架构设计文档里推出来的,而是从事故复盘、on-call 记录和代码 review 里筛出来的。我们自己内部做过一次统计,把 YOLO 相关项目里所有"训练结果对不上""实验白跑""模型上线后表现异常"的问题汇总,发现高频原因集中在五类:数据管理、任务调度、实验追踪、模型部署落地、平台工程化。每个方向最终都沉淀出几条硬教训,总数正好 23。
为什么强调这个数字?因为前 20 条是开发阶段就踩过的,后面 3 条是平台上线后被团队实际使用逼出来的。这 3 条跟算法关系不大,却决定了平台能不能活下去。后面正文按这五类展开,每条都是拿真金白银的 GPU 时长换的。
2. 数据管理:五个血泪教训
2.1 教训01:标签格式不统一,必须在源头拦截
YOLO 训练的标准标签是每张图对应一个 txt 文件,每行是class_id cx cy w h,坐标已经归一化到 0~1。但标注组给的数据经常不是这个格式,可能是 COCO 的 JSON、VOC 的 XML,或者某个标注工具导出的x1 y1 x2 y2像素坐标。如果你在训练脚本里做格式兼容,代码里就会堆满if format == "coco"这样的分支,最后某个分支出错,训练出来的模型类别错位还不自知。
平台必须定义自己的中间格式,把所有来源统一转换后再入库。导入阶段就要校验:类别 id 是否从 0 开始连续编号?坐标有没有越界?归一化值是不是在合理范围?多边形标注是否闭合?任何一项不通过就直接拒收,并把错误样本列出来反馈给标注方,而不是等训练时再报错。我们上线这个校验后,数据引起的训练事故少了一半以上。
注意:类别 id 连续性尤其容易被忽略。YOLO 系列模型通常把类别 id 当成索引去处理,如果数据里出现空洞,比如只有类别 0 和 2,没有 1,模型的分类头会对应错位,但 loss 不一定炸,只是指标悄悄变差。
2.2 教训02:数据集版本号交给哈希,别信文件名
数据集目录命名是所有团队都会做但都做不好的事。dataset_v3_final_20240215这种名字,既可以指这份目录,也可以指改名之前的旧目录,没有任何办法证明内容没变过。文件内容被重新压缩、旋转、裁剪后再放进去,文件夹名字还是一样,但训练结果已经完全变了。
我们后来改成基于内容哈希做版本号:每个数据集生成一份 manifest,记录目录下每个文件的哈希值,再把 manifest 本身聚合成一个版本字符串。训练任务提交时,只接受这个版本号,后端根据版本号去取数据快照。这样哪怕目录在对象存储里被搬来搬去,只要内容没变,版本号就一致;内容一变,版本号必然变,谁也糊弄不过去。
对于几百 GB 的图像数据集,全量哈希刚开始跑起来稍慢,但数据集导入是一次性成本。真正常见的坑反而是有人手动改了标签后没有重新生成 manifest,导致版本号没有更新。建议在平台里把"重新生成 manifest"做成导入流程的必选步骤,数据上传成功自动触发。
2.3 教训03:固定随机种子,别让评测集天天变
YOLO 训练通常会把数据按比例切成 train/val,如果每次都重新随机切分,同一份数据、同一个超参组合跑两遍,结果可能差出一个量级。最典型的表现是:今天用某组参数实验,mAP 比 baseline 高 2 个点,明天原样复跑却低了 1 个点,原因是验证集完全不同。
平台要把random_seed和val_ratio作为训练配置的必填字段,数据划分阶段固定使用这两个值。代码里不能只在开头写一句torch.manual_seed(0)就结束,还要保证 DataLoader 的 shuffle 行为、分布式 Sampler、甚至 OpenCV 里可能用到的随机操作都依赖同一个 seed。即便这样,不同机器上的划分也可能因为库版本差异不完全一致,所以最稳妥的做法是:切分完成后把 train/val 的文件路径列表存成 JSON 快照,后续训练只认这份快照。
2.4 教训04:缓存增强后的样本,而不是缓存增强代码
YOLO 训练默认会做大量在线增强,比如 mosaic、随机仿射、HSV 扰动。这些增强给模型带来泛化能力,也给复现带来麻烦:同样的代码,依赖库升级一个小版本,增强结果可能就变了。我们早期把"增强逻辑"固定下来,以为就够了,结果发现 Albumentations 从 1.3 升到 1.4 后,同一张图生成的结果有了细微差异,实验对比的纯度直接被污染。
平台的做法是支持缓存增强后的样本。对训练集做一轮预增强,把生成结果缓存到本地磁盘或 LMDB 里,训练时直接读取。这样既加快了 IO,又保证多次实验用到的实际图片内容一致。缓存成本是磁盘占用会成倍增加,但换来的是"实验结果可复现"这个底线。我们只对热数据集开启缓存,冷门数据集可以手动选择跳过,免得磁盘爆炸。
2.5 教训05:给数据质量加自动探针
大部分训练平台只管数据有没有,不管数据好不好。YOLO 对数据质量极其敏感,一类样本只有几十张,或者标注框普遍比真实目标大一圈,这些都会让 mAP 曲线变得非常难解释。所以我们的数据导入流程里内置了一套质量探针,统计几项硬指标:空标签图占比、目标面积分布、宽高比分布、类别样本数、重复框数量、坐标越界样本数。
这些指标每次训练前都会自动计算一次,生成一份数据质量报告。如果某类样本数低于阈值,平台会明确标注"类别不均衡告警",但不强制阻止训练,因为有时候小样本类别就是冷门目标,用户有权选择继续。我们最值钱的一次告警是发现验证集里混进了一批从论文里截出来的示意图,和业务真实场景差异巨大,导致线上表现远低于离线指标。没有探针的话,这个问题可能整个季度都发现不了。
3. 任务调度与资源管理:六个容易踩的坑
3.1 教训06:GPU 调度要按显存和拓扑分池
GPU 不等价,这是调度器最容易忽略的问题。8 卡 A100 和 4 卡 4090 放在同一个资源池里,表面看都是"有 GPU 可用",实际上训练任务对机器拓扑、显存、驱动版本都有要求。如果任务需要多卡分布式训练,调度器必须知道机器是单机 8 卡还是多机组合,跨机的通信走 RDMA 还是普通千兆网,不同的拓扑直接影响训练速度。
我们把资源池拆成几类:单卡调试池、单机多卡池、多机训练池。提交任务时用户声明需要的 GPU 数量和显存下限,调度器只从对应池子分配。这里有个细节:不能用nvidia-smi看到的显存数值做资源判断,因为 MIG 切分、其他进程占用的统一内存、以及驱动版本兼容性都会影响实际可用资源。调度器还需要定期对每个 GPU 做连通性探测,把 ECC 报错或训练总在中途挂掉的卡自动剔除,否则任务会被反复分发到坏卡上重试,浪费大量时间。
3.2 教训07:训练中断恢复是上线前必须完成的功能
YOLO 训练动辄十几个小时,中断一次从头来过,用户对平台的信任就崩塌一次。我们第一版只做了宕机自动重启,结果一个 20 小时的任务因为网络抖动断了 3 次,每次都是从第 0 epoch 开始,研究员直接在工作群里吐槽到半夜。后来把 checkpoint 恢复做成了硬性要求,才把这个坑填上。
具体做法是训练脚本固定周期保存 checkpoint,至少每 2 个 epoch 保存一次,并同步到对象存储。checkpoint 里除了模型权重,还要有 optimizer、scheduler、EMA 权重、torch.cuda.amp.GradScaler 的状态,有条件的话连随机数生成器的状态也存下来。调度器发现任务失败后,自动定位最新的 checkpoint 从那里恢复,同时把数据集分片索引恢复好。注意训练代码必须保证恢复的幂等性,否则可能出现同一个 epoch 重复读数据的问题。
3.3 教训08:超参配置要 schema 化
YOLO 的配置文件分散得很厉害:模型结构参数写在 yaml 里,训练超参一部分在命令行里,一部分在代码默认值里,还有一部分藏在数据增强配置里。这种分散导致"同一个配置"在不同人手里可能跑出完全不一样的结果。平台把超参建模成严格的 schema,每个参数声明类型、允许范围、默认值,并且做参数间的依赖校验。比如mosaic关闭时,跟 mosaic 相关的增强系数就不应该出现在配置里。
配置提交时自动计算一个哈希,作为实验配置的唯一标识。页面展示里,两个实验如果配置哈希完全相同,就可以放心对比;如果哈希不同,点击 diff 能看到每个参数的具体差异。这个功能看起来不起眼,但极大减少了"我什么都没改,为什么结果不同"这类问题,因为"什么都没改"最后会被配置哈希直接打脸。
3.4 教训09:早停逻辑必须基于 mAP 滑动窗口
拿 val loss 做早停是很多 YOLO 项目的通病。YOLO 的总 loss 是多个损失的加权组合,跟最终的 mAP 并不是严格同方向变化。有时候 loss 还在下降,但 mAP 已经过拟合到训练集的分布上开始下滑;有时候 loss 在高位震荡,mAP 反而还在涨,这跟数据增强强度、验证集大小都有关系。
平台内置的早停策略改成基于 mAP50-95 的滑动窗口:只有当连续 N 个 epoch 都没能刷新历史最佳 mAP 时,才判定为可以早停,并且自动保存历史最佳模型。N 的取值要根据验证集大小来,验证集大、噪声小可以设小一点;验证集小,mAP 波动大,窗口至少要 10~20 个 epoch,否则很容易提前误停。
3.5 教训10:batch size 不是随便填的数字
batch size 影响的不只是显存,还包括 BN 统计、学习率缩放、分布式训练的同步成本。很多用户把 batch size 从 16 调到 64,学习率不调,结果 loss 直接飞掉。平台要帮用户算清楚一个公式:有效学习率通常随 batch size 线性或平方根缩放,具体用哪种要看优化器和训练阶段,但无论如何不能只改 batch 不改 lr。
另一个易错点是多卡训练时 batch size 到底算总样本数还是单卡样本数。Ultralytics 系列工具在部分版本里把 batch 理解成总 batch,然后自动除以 GPU 数,但并没有在所有版本里保持一致。平台在提交表单里强制用户声明这一点,并把"per-GPU batch"和"total batch"分开展示,避免理解偏差。还可以基于模型参数量和输入分辨率做一个显存估算,提前提示用户某个 batch 在当前 GPU 上跑不跑得动。
3.6 教训11:日志要结构化,别让前端解析纯文本
训练日志如果只是 stdout 里的print("epoch 10 loss 0.5"),前端要解析这种文本会非常痛苦,更别说做报警和趋势图。我们把日志统一成 JSON Lines 格式,每条日志至少包含:时间戳、epoch、step、总 loss、box_loss、cls_loss、dfl_loss、当前学习率、GPU 利用率、显存占用。所有训练脚本都通过平台的日志 SDK 输出,不允许直接写print。
结构化日志带来的额外收益是调试走查方便。任务失败时可以根据 trace id 把同一次训练的所有日志串起来,包括调度器日志、训练容器日志、数据加载日志。前端只需要直接解析 JSON 渲染曲线,不需要写正则去抠数字。这个问题越早定越好,后面想改造就很麻烦。
4. 实验追踪:五个教训让对比不再失真
4.1 教训12:复现条件是实验的及格线
一份实验报告如果没有记录代码版本、数据版本、超参配置、依赖库版本,那这份报告就没有复现价值。YOLO 项目经常因为 CUDA、PyTorch、Ultralytics 的版本差异导致结果浮动,哪怕同一个人的机器,两个月后环境变了都跑不出原来结果。平台在提交训练任务时自动快照一组上下文:git commit、requirements.lock、conda env export、数据版本、配置哈希、GPU 驱动版本。
快照不是给用户看的表单,而是写入实验记录元数据里。后续任何人都能一键重建这个实验环境,平台把依赖装到一个隔离的容器里跑。这个能力一开始被人嫌重,真正用起来之后,大家才发现以前大部分"结果对不上"的问题都是环境差异造成的,跟算法改动没有任何关系。
4.2 教训13:曲线保留原始点和平滑线
TensorBoard 的 smoothing 参数很容易让曲线看起来"很干净",但它会掩盖关键信息。我见过一个实验的 mAP 在第 30 个 epoch 突然掉了一大截,但平滑后的曲线只是轻微波动,几乎看不出这次跳变。训练曲线的真相比平均值更复杂,尤其是小验证集上,每个 epoch 的 mAP 波动本来就大。
平台的图表默认同时展示原始散点和 EMA 平滑线,散点颜色浅一点,平滑线颜色深一点。如果想进一步了解波动源,还可以按验证集分片画每片的指标分布,而不是只画一个总平均。这些细节决定了用户能不能快速发现"第 30 轮数据加载坏了"这类偶发问题。
4.3 教训14:损失函数要拆开跟踪每个分量
YOLOv8 的训练损失至少包含 box_loss、cls_loss、dfl_loss 三个分量,总 loss 是它们的加权和。只看总 loss 会有很强的迷惑性:可能 cls_loss 降了,但 dfl_loss 在涨,回归精度在悄悄退化,总 loss 却因为权重关系看起来还在下降。平台在训练过程中分别保存每个 loss 分量的数值,并且在同一个图表里展示。
为了对比方便,还要支持对 loss 分量做归一化或切换到不同坐标轴,因为不同 loss 的量纲差异可以很大。拆开看之后,我们多次在 dfl_loss 异常上升之前就发现问题,避免了训练到最后才发现的回归崩坏。
4.4 教训15:对比实验必须锁死数据和预处理
实验对比的基本要求是"单变量控制"。可数据版本不一致是 YOLO 项目里最常见的隐性变量。今天对比两个模型结构,一个用 v1.2 的数据,一个用 v1.3 的数据,就算结果显示某个模型高 1 个点,也不能说明模型结构更好。平台在创建对比组时会自动检查参与对比的实验数据版本是否一致,不一致就高亮提示。
预处理层面的坑更隐蔽。同样的数据,如果离线和在线训练用了不同的 letterbox 参数、不同的归一化方式、不同的 HSV 增强范围,结果差异会大到让人误判。所以平台把预处理版本也纳入配置哈希,数据增强代码的每次改动都会改变配置哈希,对比时如果预处理不同,页面会明确标出来。
4.5 教训16:模型权重是资产,不是文件
模型权重文件的命名和存放是训练管理平台最容易失控的一环。best.pt、last.pt这类名字会在不同实验目录下反复出现,过一个月没人知道哪个对应哪个实验,哪个指标好哪个不能上线。我们把模型权重纳入注册表管理,每个权重分配一个全局唯一 ID,并记录它的来源训练任务、数据版本、配置哈希、关键指标、导出状态。
注册表还有一个好处:部署系统拿到的是一个稳定的 ID,而不是一个文件路径。后续模型上线、回滚、对比,都通过注册表操作。模型文件本身可以存在对象存储里,但平台对外只暴露注册表 ID。这样即使有人误删了对象存储里的文件,只要注册表还在,也能通过哈希校验及时发现问题。
5. 优化与部署:模型交付不止是"导出 onnx"
5.1 教训17:导出前先冻结结构,避免动态算子埋雷
从 .pt 导出 ONNX 不是点一下按钮就完事的。YOLO 训练过程中如果用了 EMA,导出的最优权重往往来自 EMA 模型,但有些自定义结构在导出时仍然保留训练阶段的 BatchNorm 状态,导致推理结果和训练结果不一致。更常见的是动态 shape 问题:导出时开了 dynamic 维度,后续部署框架对动态尺寸支持不好,推理直接报错。
平台在模型导出阶段自动执行一套冻结流程:把 BatchNorm 的 running_mean/running_var 固定,设置明确的 opset 版本,默认导出静态 shape,只有显式选择时才启用动态维度。导出完成后还要对 ONNX 做一次完整性检查,用随机输入跑一遍,对比导出的输出和 PyTorch 原始模型的输出差异,差异超过阈值就阻止发布。
5.2 教训18:量化必须跑完整评估集做精度对比
INT8 量化不是免费的午餐,尤其对于小目标检测,量化掉点可能比分类任务严重得多。很多团队的流程是量化完拿几张验证图看一眼就上线了,这是非常危险的做法。平台要求量化模型必须跑完整评估集,输出与 FP32 模型的 mAP 差异、每个类别的 AP 差异,再决定是否上线。
校准集的选取也很关键。如果校准集和训练集分布严重不同,量化后模型可能在白天场景正常、夜间场景表现崩坏,但离线指标完全看不出来。平台允许给数据集打"场景标签",校准集尽量覆盖所有场景。量化对比结果存入模型注册表,当差异超过预设阈值时,模型状态自动标记为"需人工确认",不会默许上线。
5.3 教训19:输入尺寸和预处理参数写进模型元数据
模型文件一旦导出,就不再是"训练时的那个模型",而是一个需要在特定输入条件下运行的计算图。部署端必须知道输入尺寸是多少、要不要 letterbox、padding 用什么值、像素要不要归一化、RGB 还是 BGR。这些信息如果只存在于训练代码的某个 yaml 文件里,部署团队很难拿到。
平台在模型注册表里固化一份部署元数据:img_size、stride、pad_value、normalize、swapRB、opset、export_version。推理服务启动时从注册表拉取这份元数据,而不是让开发人员手动读代码。我们曾经因为推理端用了 640 输入尺寸,而训练时用的是 1280,离线 mAP 很高,线上效果差到被用户投诉,后来查了很久才发现是输入尺寸不一致。
5.4 教训20:推理服务要做请求级超时和排队,而不是裸并发
GPU 推理服务和普通的 HTTP 服务不一样,它不是线程越多越好。显存和算力有限,当大量请求同时打进来,如果每个请求都创建一个推理上下文,显存会迅速爆掉,已进入的请求也互相拖慢。平台在推理服务层加入请求队列和动态批处理,把短时间到达的多个请求聚合到一个 batch 里,减少单次推理开销。
队列不是万能的。队列过长会导致延迟无限上升,所以还要配套请求级超时和队列长度上限。超时的请求直接返回错误,客户端按退避策略重试而不是无限等。平台监控里要区分 queue delay、inference time、postprocess time 三段耗时,这样才能定位是排队问题还是模型本身慢。
5.5 教训21:不要只盯 mAP,还要看 P99 延迟
mAP 只能回答"模型质量怎么样",回答不了"上线后用户体验怎么样"。模型推理延迟受 batch 策略、输入分辨率、硬件型号、TensorRT 版本影响很大。换一个 engine 版本,mAP 可能完全不变,但 P99 延迟翻倍。平台把模型上线后的性能监控回写到注册表,记录 P50/P99 延迟、吞吐、GPU 显存占用,并和离线 benchmark 做交叉验证。
我们在一次例行升级中把 TensorRT 引擎换了新版本,离线指标几乎没变,结果线上 P99 从 40ms 涨到 90ms,如果不是监控报警,这个问题会拖到用户抱怨才被发现。训练管理平台如果只管到 best.pt 就结束,那它最多算一个实验管理工具,还不算完整的模型交付链路。
6. 平台工程化:三个让团队"用得下去"的关键
6.1 教训22:权限设计要克制,RBAC 最小集够用就够
平台刚上线时,我们参考企业级系统做了一整套细粒度权限:项目级隔离、数据集级权限、模型级审批、训练任务可见范围等等。结果研究员之间互相看不到对方的实验,想复现一个同事的实验要反复申请权限,找模型更是要层层审批,效率断崖式下降。权限越复杂,用户越不愿意用平台,最后大家都回去用共享目录加记事本。
教训是平台第一版只需要三个角色:管理员、成员、只读访客。数据隔离可以先用项目字段做逻辑区分,不要一开始就按 namespace 物理隔离。细粒度权限等团队真正形成规范之后再加,而且加的时候要给用户一个缓冲期,别让已有流程一夜之间失效。
6.2 教训23:CLI/SDK 比网页表单更受欢迎
我们原本把大量精力花在网页表单上,结果研究员们还是习惯在终端里写脚本。网页表单有天然的局限:参数太多,校验要到提交以后才报错,体验很差;而且没法被自动化脚本调用。平台后来补了一个 Python SDK,核心接口只有几个:注册数据集、提交训练、获取实验指标、拉取模型、导出 ONNX。CLI 只是对这个 SDK 的薄封装。
这套接口出来之后,平台的使用量反而涨了。研究员可以把自己常用的几个配置组合封装成函数,一键提交一系列实验;CI/CD 流程也能在代码合并后自动触发训练。网页端最终定位成查询和可视化工具,而不是唯一的交互入口。如果一开始就有这个意识,能省掉至少一个月的开发量。
6.3 团队抵制平台的三个真实原因
再好的技术方案,如果团队不用就一文不值。我们复盘时发现团队抵制平台的原因通常有三个:一是平台增加了额外负担,提交一次训练要填一堆必填字段,比原来敲命令行还慢;二是平台展示的指标和研究员习惯的 wandb 页面不一致,感觉不权威;三是审批或者权限流程太重,拖慢了节奏。
解决办法是把平台做成"默认顺手"而不是"强制规范"。必填项只保留数据版本、配置哈希、运行环境,其他全部给默认值。展示层面兼容 wandb 的常见指标名称,甚至可以把 wandb 日志同步进平台,让用户不改变原有习惯。最重要的是提供迁移期,允许用户先在平台外跑实验,通过 SDK 把结果导入注册表,让他们感受到平台的查询和复现能力是真实有用的。
7. 常见翻车现场与排查思路
7.1 训练卡死但 GPU 利用率很低
表现是任务还在跑,日志也不报错,但是nvidia-smi看到 GPU 利用率长期在 0%~10%。这不是用户代码问题,就是平台调度问题。先看数据加载链路:ps -eo pid,stat,comm如果看到训练进程处于 D 状态,大概率在等磁盘 IO 或者网络存储。检查数据是否第一次从对象存储拉取,本地缓存是否没命中,num_workers是不是设成了 0。再查 NCCL 相关:多卡训练时通信初始化卡住也会表现为 GPU 低利用率,这时候要看是否有进程在 etcd 或共享存储上等待。
平台侧可以做两件事:一是调度器在训练启动前预热数据,把数据集拉到本地盘;二是日志系统把 dataloader 的吞吐指标也打出来,如果每秒处理样本数远低于理论值,就直接告警,不用等用户来反馈。
7.2 验证集 mAP 忽高忽低,该信哪一次
验证集小、seed 不固定、数据划分没快照,都会导致 mAP 在几次训练之间跳来跳去。最典型的场景是同一份数据同一个配置,第一次跑 mAP 是 0.52,第二次变 0.57,差 5 个点,这已经超过很多算法改动的收益。不要急着调模型,先确认数据版本哈希一致、划分 seed 一致、训练配置哈希一致。如果这些都一致,那就要看验证集本身是否大到足以稳定评估,或者用多次采样取平均。
平台把这些问题都收敛成了硬约束:同一数据集同一配置哈希再次提交训练时,系统会弹窗提示"两次实验数据版本相同,但 seed 相同的话结果应该可复现,请确认是否希望重置随机种子制造新结果"。这个小提示极大减少了"假复现"的争论。
7.3 多卡训练结果和单卡结果对不上
多卡训练和单卡训练结果不一致,多数情况下不是平台 bug,而是 batch size 和学习率的换算出了问题。比如单卡 batch=16,8 卡训练如果总 batch 还是 16,那每张卡实际只看到 2 张图,BN 统计完全改变,结果自然对不上。平台在配置表单里明确区分 per-GPU batch 和 total batch,并在提交时自动换算。还有一个容易忽略的点:多卡时必须固定 distributed sampler 的 seed,否则每个 epoch 的数据分配顺序不同,结果会有细微偏差。
如果确认 batch 和 seed 都没问题,检查 SyncBN 是否开启,以及 GPU 之间通信的 all-reduce 顺序是否一致。这些信息都要写进实验元数据,排查时可以直接拉出来对比,而不是靠记忆猜。
7.4 最后一点个人体会
如果现在让我重做这套平台,我会把前两周时间全部花在数据质量探针和结构化日志上,而不是把页面做得更好看。YOLO 本身已经足够成熟,训练跑不通的问题很少,真正让团队消耗精力的永远是"这个结果到底能不能信"。
平台后续最值得扩展的方向是自动标注和主动学习,把人工标注成本降下来,再往闭环里加一个数据迭代的反馈机制。那是另一个很长的故事,但前提仍然是今天写的这些基础能力都过关。