做视觉项目这几年,被问得最多的一类问题不是"模型怎么训",而是"模型训完了,怎么塞进那块板子里跑起来"。训练环境里一行推理代码就能出框,搬到只有几百兆内存、算力按TOPS计、工具链还可能只认特定算子集的嵌入式板卡上,中间隔着的坑能装满一箩筐。这次要聊的就是YOLOv8 目标检测算法移植这件事——把一个在通用框架里训练好的检测模型,完整地搬到边缘计算板卡上,做到精度不掉、速度够用、长时间跑不崩。这套流程适用的场景很广:产线质检、园区安防、车载辅助、无人机巡检,基本只要是"前端出图、本地出结果"的项目都用得上。读者对象我按两类来写,一类是刚接触模型部署、手里有块板子不知道怎么下手的同学,另一类是把模型跑通了但精度或帧率总差一口气、卡在调优阶段的老手。下面的内容不会只给命令,更多是讲清楚每一步为什么这么做、不这么做会出什么问题。
1. 移植的整体设计与路线取舍
1.1 从训练环境到目标板卡,中间到底隔了几道坎
很多人对移植的理解停留在"转个格式"这一步,实际上从训练完的权重到板上稳定出框,中间至少有四道独立的坎,每一道都能单独把你卡住。第一道是图结构坎:训练框架里的模型是一张动态图,带着大量只在训练时才有意义的节点,比如辅助分支、各种条件判断、动态 shape 操作,目标板卡的推理引擎往往只接受静态图,节点一多就报"不支持"。第二道是算子坎:板卡上的加速单元通常只硬实现了有限的一批算子,像变形卷积、某些模式的插值、动态切片,落到不支持列表里就得自己想办法替换或拆解。第三道是数据类型坎:训练用 FP32,板上为了速度和内存大量使用 INT8,量化过程一旦校准不当,精度掉个十几二十个点很常见。第四道是前后处理坎:这部分最容易被人忽略,因为模型本身转得好好的,但板端写的 letterbox 缩放比例和训练时不一致、NMS 的坐标还原方式差一点点,框就整体偏移或者尺寸不对,你还以为是模型坏了。
我在一个工业质检类项目里就吃过这个亏。当时模型在 PC 上验证 mAP 有 0.78,转到板子上跑同一批图,mAP 直接掉到 0.41,第一反应是量化出了问题,折腾了两天校准集、换了三套量化策略,纹丝不动。最后把板端输出的原始张量 dump 出来和 PC 端逐元素对比,发现主干网络输出几乎完全一致,问题出在后处理的 anchor 解码上——板端代码里 stride 的取值是硬编码的数组,而模型导出时因为输入尺寸被改过,三个特征层的实际 stride 和数组对不上,框的中心点整体偏移了半个格子。你看,这就是典型的"以为模型有问题,其实是胶水代码有问题"。
所以我给移植这件事定了个基本顺序:先把链路打通,再保精度,最后压性能。顺序反了会非常痛苦,因为你永远分不清掉点是模型转换造成的还是自己代码写错了造成的。打通链路的标准是:板端能加载模型、能吐出和 PC 端形状一致的原始张量、单个张量的数值误差在可接受范围内(FP32 路线一般要求相对误差小于 1e-3)。只有这个基线建立了,后面所有的调优才有参照物。
1.2 三条主流路线怎么选:我为什么倾向 ONNX 中转
实际可选的路线大致三条,各有各的适用面,我列个表对比一下,心里有个数。
| 路线 | 中间格式 | 优点 | 代价 | 适用情况 |
|---|---|---|---|---|
| 框架直转 | 训练框架直接导出目标格式 | 少了中间环节,理论上误差最小 | 强绑定特定工具链版本,模型结构稍改就得重来 | 项目结构稳定、工具链成熟且不再升级 |
| ONNX 中转 | 导出 ONNX 再转目标格式 | 中间态可视化、可逐层比对、生态最全 | 多一次转换,可能引入额外误差,opset 版本要挑 | 绝大多数场景,尤其是需要排查问题的 |
| 纯手写重实现 | 用目标平台的原生接口重搭网络 | 性能上限最高,可以针对性优化 | 工作量大、易出错、维护成本高 | 模型结构简单,或者对性能有极端要求 |
我基本都走 ONNX 中转这条路,原因很实在:ONNX 给了我一个可以逐层对齐的中间态。前面说的那个 stride 问题,就是因为有 ONNX 这个参照物才定位得那么快——我可以在 PC 上用 ONNX 推理一遍,把每层输出存成 npy,再和板端输出逐个比对,误差从哪一层开始放大一目了然。如果走框架直转,出了问题你只能对着黑盒猜。
选 ONNX 也有讲究。opset 版本不是越高越好,太新的 opset 里某些算子目标工具链还没跟上,太老的又表达不了新结构。YOLOv8 这类结构,opset 12 到 13 是比较稳的区间,我一般先用 12,遇到转换报错再往上试。另外导出时一定要开图简化和常量折叠,能砍掉不少冗余节点。还有一点,导出后务必用可视化工具把图看一遍,重点看有没有残留的If、Loop、NonZero这类动态控制流节点,有的话在静态图引擎上基本必炸。
关于输入输出的形状,这里提前定死:输入固定为1×3×640×640,三个检测头的输出在导出时会被合并成一个1×84×8400的张量(80 类 COCO 为例,4 个回归值加 80 个类别分数,注意 YOLOv8 已经去掉了 objectness 分支)。这个合并是好事,板端只需要处理一个输出张量,省心。但如果你的类别数不一样,通道数要跟着变,4 + 类别数,别照抄。
2. 模型导出与图结构梳理
2.1 导出前的三件必做事
导出这一步看着简单,一行代码的事,但准备工作没做好,后面全是返工。我总结了三件必须在导出前确认的事。
第一件,确认推理模式下的模型行为。训练框架里模型有 train 和 eval 两种状态,导出前一定要切到 eval,否则 BN 层用的还是 batch 统计量,结果会飘。这个坑不新鲜,但每年还是有人踩。第二件,确认输入尺寸和是否动态。边缘部署我强烈建议固定输入尺寸,动态轴在多数板卡工具链上要么不支持,要么性能极差。YOLOv8 默认支持矩形输入,但固定成正方形640×640对板端最友好,内存分配好算,padding 逻辑也简单。第三件,确认后处理是否要一起导出。这里有个选择:把 NMS 也塞进图里,还是留在外面用 CPU 做。
我的经验是不要导出 NMS。原因很直接,NMS 里有排序和动态循环,这两个东西在静态图引擎上要么不支持,要么实现得极其低效,有些工具链甚至会在转换时把它悄悄降级成 CPU 实现,然后你在板上看到推理耗时莫名其妙多出几十毫秒。把原始检测头输出直接吐出来,NMS 在 CPU 上用 C++ 手写,可控性高得多,而且可以和其他逻辑一起做流水线优化。至于 DFL 那部分解码,也是同理,放在外面。
导出命令大概长这样,参数我标注了理由:
from ultralytics import YOLO model = YOLO("weights/best.pt") model.export( format="onnx", imgsz=(640, 640), # 固定正方形,板端好算 opset=12, # 保守起步,报错再升 simplify=True, # 开图简化,砍冗余节点 dynamic=False, # 静态图,性能最优 batch=1, # 边缘场景基本不需要多 batch )导出完成先别急着转,拿张测试图跑一下 ONNX,看看输出的 shape 和数值是不是符合预期。有个小细节:simplify依赖的简化库版本有时会和导出工具打架,如果简化后精度莫名变差,先关掉简化再试一次,能快速判断问题出在简化环节还是导出环节。
2.2 用可视化把图翻一遍,重点盯这几类节点
导出后的图我一般会过一遍,不是走形式,而是提前发现雷。重点关注四类东西。
第一类是动态 shape 相关的节点,比如Reshape的目标形状来自另一个张量计算、Slice的边界是动态的、NonZero之类。这些在静态引擎上要么直接不支持,要么需要工具链做特殊处理。YOLOv8 官方导出一般不会有这问题,但如果你改过检测头的结构,或者用了自定义模块,就得留神。
第二类是Resize和Interpolate。上采样在 YOLO 结构里很常见,而不同平台对插值模式的支持差别很大,有的只支持最近邻,有的支持双线性但不支持 align_corners 的某些组合。如果工具链报错说 Resize 不支持,通常的解法是把上采样换成"先最近邻放大再卷积"的等价结构,精度影响很小。
第三类是大规模的Concat。YOLOv8 的 neck 部分有不少通道拼接,拼接本身不贵,但如果拼接前的两个张量在内存里的布局不一样,某些平台会强制插入内存重排操作,性能直接掉一截。这个在图上不一定看得出来,需要结合工具链的性能分析报告看。
第四类是输出前的Sigmoid和Softmax。YOLOv8 的类别分数和 DFL 权重都各自带激活,这些激活函数能不能在加速单元上执行、会不会被降级到 CPU,直接影响最后一段的耗时。有些平台允许你把这几层摘出去自己算,摘出去之后模型输出的是 logits,你在后处理里加一次 sigmoid 就好,这样能规避掉不支持的问题。
这里给个实操建议:导出后写个小脚本,把 ONNX 的节点类型统计出来,对着板卡工具链的支持列表逐个勾,勾不上的提前规划替代方案,别等转换报错了再临时想。
import onnx from collections import Counter m = onnx.load("yolov8n.onnx") ops = Counter(n.op_type for n in m.graph.node) for k, v in ops.most_common(): print(f"{k:20s}{v}")跑完你会看到Conv、Concat、Resize、Sigmoid、Softmax、Split这些熟面孔。数量多不可怕,可怕的是某个冷门算子只出现一次,往往就是它导致整个图转换失败。
3. 前后处理对齐:精度掉点的重灾区
3.1 letterbox 的三个参数,差一个都不行
模型训练时用的 letterbox 逻辑是:按长边等比缩放到目标尺寸,短边用固定灰度值填充,保证原图比例不变形。这套逻辑看起来简单,但在板端重写的时候,有三个参数必须和训练时完全一致,否则框的位置一定偏。
第一个是填充值。训练框架默认用的是 114 这个灰度值,你在板端如果图省事填 0(黑边),虽然影响不大,但在边界目标的检测上会有可测量的差异,因为模型见过的边界上下文变了。这个不是玄学,我实测过,在密集小目标场景下,用 0 填充会导致靠近图像边缘的目标漏检率上升几个百分点。
第二个是缩放系数的计算方式。正确做法是先算scale = min(640/w, 640/h),然后用这个统一系数同时缩放宽高,再算实际缩放后的尺寸,最后算需要填充的量:pad_w = (640 - new_w) / 2,pad_h = (640 - new_h) / 2。填充量通常要对齐到偶数或者 32 的倍数,有些平台对输入对齐有要求,对齐之后再算实际的偏移量。这里最常见的错误是宽高各算各的缩放系数,结果图像被拉伸变形,模型输出自然一塌糊涂,而你在 PC 上验证时用的又是正确的逻辑,两边对不上,排查起来极其折磨。
第三个是坐标还原的偏移量。板上得到的框坐标是在 640×640 的 letterbox 图上的,要映射回原图,得先减去 pad 再除以 scale。减和除的顺序不能反,也不能漏掉某一边的 pad。这一步我用单元测试的方式保底:准备一张已知目标位置的合成图,在 PC 和板端各跑一遍,把还原后的框坐标打出来对比,误差超过 1 像素就说明有问题。
提示:建议把 letterbox 的参数(scale、pad_w、pad_h)随每帧一起传递,而不是在多个函数里各自重算一遍。重算次数一多,总有一处用的不是同一组数。
3.2 DFL 解码和 NMS:手写的部分要逐行对
YOLOv8 的回归分支用的是分布式焦点损失(DFL)的思路,输出通道是4 × reg_max,reg_max默认 16。解码过程是:把每个方向的 16 个值做 softmax 得到权重,再和0..15做加权求和,得到一个期望距离,四个方向合起来就是框到中心点的距离,最后结合 anchor 点的坐标还原成绝对坐标。这个过程如果板端实现错了,症状很典型:框的大小整体偏大或偏小、框长宽比异常、或者框全都挤在图像中心。
我的做法是先在 PC 上用 numpy 写一版参考实现,逐行注释清楚每个张量的含义,然后照着这版翻译成板端的 C++。翻译过程中最容易错的两处:一是 softmax 的轴选错了,应该是沿着通道维度做,不是沿着 anchor 维度;二是stride的映射,三个特征层的 stride 分别是 8、16、32,对应的特征图尺寸是 80×80、40×40、20×20,加起来正好 8400 个点。拼接顺序也必须和导出时一致,通常是按 stride 从小到大排。
NMS 部分相对独立,但有两个参数要和训练/验证时保持一致:置信度阈值和IoU 阈值。PC 上验证的时候用的是哪组值,板端就用同一组,否则你没法判断差异是模型造成的还是阈值造成的。另外,NMS 之前要先按类别做分数筛选,再做框的抑制,顺序不能反,反了会导致不同类别的框互相抑制,出现"两个挨着的不同物体只检出一个"的怪现象。
还有个实用技巧:如果目标类别少、场景固定,可以把 NMS 的输入数量卡死,比如每层只保留分数最高的前 100 个候选,再做抑制。这样能把 NMS 的耗时从几毫秒压到一毫秒以内,精度损失微乎其微。
4. 量化与算子兼容的实操处理
4.1 量化校准集怎么选,决定了你掉几个点
板端跑 INT8 几乎是绕不开的,FP32 在那个算力级别上根本达不到实时。量化的核心是校准,校准集选得好,精度基本能保住;选得差,掉十几个点很正常。我踩过的坑和总结出来的规则有这么几条。
校准集必须覆盖真实分布。我见过有人图省事拿 ImageNet 的几百张图去校准一个工业缺陷检测模型,结果模型在缺陷区域的特征被量化得很粗糙,小目标几乎全丢。正确做法是从实际业务数据里抽,数量不用太多,500 到 1000 张足够,但一定要覆盖各种光照、各种场景、各种目标尺度。如果某类目标很稀有,校准集里也得按比例塞进去一些,不能让稀有类在量化时被"平均掉"。
校准算法优先选基于 KL 散度的,比起简单的 min-max,它对长尾分布更友好。如果工具链同时提供多种,我的选择顺序是:KL 散度 > 百分位截断 > min-max。百分位截断的截断比例一般设 99.9%,效果和 KL 接近但速度快不少,工程上很实用。
输出层尽量保持高精度。检测头的输出是框坐标和分类分数,这两个对量化误差极其敏感,尤其是回归分支,一点点误差经过 DFL 加权放大后,框的位置就偏了。多数工具链支持把某些层设为 FP16 或者保持 FP32,我的惯例是把检测头前面的几层和输出层都设为高精度,主干网络全 INT8。这样速度损失很小(检测头计算量占比低),但精度能救回来一大半。
量化前后必须做逐层对比。不要只看最终 mAP,要拿同一个输入,把量化模型和浮点模型的中间层输出都 dump 出来,算余弦相似度或者相对误差,找到误差突增的那一层。定位到具体层之后,要么把它排除出量化范围,要么检查是不是这一层的输入分布有异常值(比如某些激活值特别大),针对性调整校准策略。
| 量化配置 | 相对速度 | mAP 变化(某质检数据集实测) | 适用建议 |
|---|---|---|---|
| 全 FP32 | 1.0x | 基准 | 只用于精度验证 |
| 主干 INT8,检测头 FP16 | 3.6x | -0.4 个点 | 首选方案 |
| 全 INT8,KL 校准 | 4.2x | -1.8 个点 | 精度要求不苛刻时 |
| 全 INT8,min-max 校准 | 4.2x | -4.5 个点 | 不推荐 |
这张表的数字来自一个具体项目,不同数据集、不同平台肯定不一样,但趋势是稳定的:检测头保精度这件事性价比极高。
4.2 碰到不支持算子,四种处理思路
算子不支持是移植路上的常态,工具链的支持列表永远追不上学术界的脑洞。处理思路我按优先级排一下。
第一种,改模型结构避开它。这是最干净的方案。比如某个平台的插值只支持最近邻,那就把上采样换成"最近邻 + 3×3 卷积",改动很小,重新训练几个 epoch 精度就回来了。再比如某些平台不支持大 kernel 的深度可分离卷积,可以拆成普通卷积加通道裁剪。改结构的前提是你还掌握训练流程,能重训,如果只有权重没有训练代码,这条路就走不通。
第二种,算子分解。把一个大算子拆成几个小算子的组合。变形卷积是典型例子,很多平台不支持,但可以拆成"常规卷积预测偏移 + 网格采样 + 加权求和"三步,其中网格采样再拆成整数索引和双线性插值。麻烦是麻烦,但能用。分解的代价通常是性能下降,需要评估是否可接受。
第三种,搬回 CPU 执行。多数工具链允许把不支持的节点标记为 CPU 执行,图会在那一层断开,数据在主存和加速器之间来回搬。适合那种只出现一两次、计算量不大的节点。如果节点在主干深处、每帧都要执行,来回搬运的开销可能比计算本身还大,这时候就得慎重。
第四种,自己写自定义算子。平台一般提供了扩展接口,允许你用底层语言实现一个算子并注册进去。这是最重的方式,但如果某个算子性能关键又确实不支持,这是唯一的路。写自定义算子要注意对齐数据布局和量化参数,这两处错了会导致结果完全无法解释。
注意:任何算子替换或分解之后,都要重新做一次精度验证,不能因为"数学上等价"就跳过。浮点运算的舍入误差累积起来,有时候真的会让最终结果变差。
5. 板上实测与性能调优
5.1 首轮跑通怎么验证,别一上来就看帧率
第一次在板上跑通,最忌讳的就是直接看帧率然后开始调优。这个时候你连结果对不对都不知道,调性能没有意义。我的验证流程分三层。
第一层,单张图的原始输出比对。准备一张固定的测试图,PC 端和板端各跑一次,把检测头输出的1×84×8400张量存下来逐元素对比。FP32 路线要求最大绝对误差小于 1e-3,INT8 路线放宽到 1e-2 以内。超过这个量级就说明模型转换有问题,先回去查图。
第二层,后处理结果的框级比对。同样的图,PC 和板端各自跑完整流程,输出的框坐标、类别、分数做匹配比对。允许因为量化造成的轻微差异,但框的数量应该基本一致,位置偏移应该在几个像素以内。这一层能抓到 letterbox 和解码的问题。
第三层,批量数据集的指标比对。拿验证集跑一遍,算 mAP 和召回率,和 PC 的结果对比。这一层是最终验收,掉点超过预期就得回头找原因。我的经验是 INT8 加检测头保精度的配置,mAP 掉点在 1 个点以内算正常,超过 2 个点就值得深挖。
三层都过了,才开始看性能。
5.2 性能优化:先找瓶颈,再动手
性能优化最怕瞎调。我一般的做法是先做时间分解,把一帧的处理拆成采集、预处理、推理、后处理、渲染五段,各自计时,找出占比最大的那段。
实测下来,多数项目的初步版本里,推理占 40% 到 60%,预处理和后处理加起来能占 30% 到 40%,剩下是采集和渲染。很多人只盯着推理优化,结果把后处理从 20 毫秒优化到 2 毫秒之后,整体帧率反而翻了一倍——因为后处理才是真正的瓶颈。
推理侧的优化手段主要是多线程流水线和零拷贝。流水线的思路是把上面五段拆到不同线程,用队列串起来,让推理在跑的时候预处理已经在准备下一帧。注意队列深度不要设太大,2 到 3 就够,设大了延迟会增加,对实时性要求高的场景反而不好。零拷贝则是让预处理直接把数据写进推理引擎要求的缓冲区,省掉一次内存搬运,这个在 4K 输入场景下能省好几毫秒。
预处理侧最大的优化点是别在 CPU 上做浮点归一化。如果平台支持在推理引擎内部做归一化(把减均值除方差作为模型的一部分),就把它塞进模型里,CPU 只做 resize 和格式转换。如果平台不支持,可以在 resize 的时候用定点运算近似,或者用 SIMD 指令加速。我见过一个项目,光是把预处理里的三次内存拷贝合并成一次,帧率就涨了 15%。
后处理侧的优化前面提过了,卡住候选数量是最简单的。另外,NMS 的 IoU 计算可以用并查集或者分块的方式加速,类别多的时候效果明显。
| 优化项 | 典型收益 | 实施难度 | 备注 |
|---|---|---|---|
| 后处理候选数量限制 | 帧率 +20% 到 +50% | 低 | 精度几乎无损 |
| 预处理合并内存拷贝 | 帧率 +10% 到 +20% | 中 | 需要改数据流 |
| 三线程流水线 | 帧率 +30% 到 +80% | 中 | 注意队列深度控制延迟 |
| 检测头外提 CPU | 帧率 +5% 到 +15% | 中 | 同时利于精度 |
| 零拷贝输入 | 大分辨率下收益明显 | 高 | 依赖平台内存接口 |
5.3 长时间运行的稳定性,比峰值帧率更重要
这一点我在实际项目里吃亏吃出来的。实验室里跑十分钟帧率很漂亮,装到现场跑八小时,第三天开始偶发卡顿,第五天直接崩了。查下来是内存泄漏——每帧分配的输出张量没有释放,几十万帧之后内存耗尽。
所以我现在做移植,压力测试是必做项。连续跑 24 小时,监控三样东西:内存占用曲线、帧率曲线、温度曲线。内存曲线应该是平的,缓慢上升就是有泄漏;帧率曲线应该是稳的,周期性下跌说明有 GC 或者队列积压;温度曲线要关注是否触发降频,很多板子跑满半小时后因为过热降频,帧率掉 30% 很常见。针对降频,要么加散热,要么在软件上做动态限帧,把负载压在热设计功耗以内。
还有一个容易被忽略的点:异常输入的容错。摄像头断流、图像全黑、分辨率突变,这些在现场都会发生。板端代码如果不做判断,一次异常输入就可能让整个推理流程崩溃。我通常会在采集和推理之间加一层校验,尺寸不对就丢弃并重置状态机,全黑图像直接跳过推理省算力,重启采集的间隔要做退避,别一秒钟重连八百次。
6. 常见问题速查与踩坑记录
6.1 精度类问题:从现象快速定位
精度问题最难查,因为现象都很像。我整理了一张对照表,按现象找原因,能省不少时间。
| 现象 | 最可能的原因 | 排查动作 |
|---|---|---|
| 框整体偏移固定像素 | letterbox 的 pad 没减或减错边 | 打印 scale 和 pad 值,和 PC 端对比 |
| 框尺寸系统性偏大/偏小 | DFL 解码的加权系数范围错 | 检查 reg_max 是否用了 16,权重是否 0 到 15 |
| 框长宽比明显异常 | 宽高缩放系数分别算了 | 确认用统一 scale |
| 全部框挤在图像中心 | stride 映射错或 anchor 坐标未乘 stride | 打印各层 stride 和特征图尺寸 |
| 类别全错但框正确 | 类别分数的通道偏移错(漏了 4 个回归通道) | 确认类别从第 5 个通道开始取 |
| 小目标大量漏检 | 校准集不含小目标,或输入分辨率被压缩 | 换校准集,检查输入尺寸 |
| 边界目标漏检 | 填充值用了 0 而非训练时的灰度 | 改成一致的填充值 |
| 只有部分类别掉点 | 校准集类别不均衡 | 补充稀有类样本重新校准 |
这张表里的每一条我基本都亲自踩过,尤其是"类别通道偏移"那条,症状特别迷惑——框画得漂漂亮亮,标签全是错的,看着像模型坏了,其实就是data[4:]写成了data[3:]之类的小事。
6.2 性能与稳定性:几个不常见的坑
除了内存泄漏和过热降频,还有几个坑值得单独说。
第一个是首次推理特别慢。有些平台在第一次推理时会做算子编译或者内存预分配,耗时能达到正常帧的十几倍。这本身不是问题,但如果你把这帧算进平均帧率,或者把它算进启动超时,就会误判。处理办法很简单,启动时先用一张空白图做一次预热推理,把这次耗时排除在统计之外。
第二个是多线程下的结果错乱。为了提帧率开了多线程,结果发现偶尔出现框和画面对不上。这通常是共享缓冲区没有做保护,线程 A 在写这块内存,线程 B 已经在读了。解决办法要么是每线程独立缓冲,要么用双缓冲加原子交换。我倾向于双缓冲,内存占用可控,交换开销也小。
第三个是输入源的格式差异。PC 上验证用的是 JPEG 解码后的 RGB 图,板端摄像头出来的可能是 YUV,中间还要做一次色彩空间转换。这一步如果放错位置(比如在 resize 之后才转),计算结果会有细微差异,累积起来能影响到小数点后第二位。正确顺序是:拿到原始 YUV,先转 RGB,再 resize,再归一化。顺序错了不仅精度有影响,性能也会变差,因为 YUV 转 RGB 在原始分辨率上做比在缩放后做要慢。
第四个是模型文件加载失败但没有报错。有些工具链在模型文件损坏或者版本不匹配时不会抛异常,而是返回一个空句柄,然后推理时输出全零。你在板上看到的是"模型跑了但一个框都没有",很容易误判成后处理阈值太高。建议加载完模型之后立刻做一次结构校验,打印输入输出的形状,形状不对就直接报错退出,别带着病跑。
6.3 我个人的几条实操习惯
最后分享几个养成的习惯,都是被坑出来的。
导出模型的时候,文件名带上日期和关键参数,比如yolov8n_640_int8_20240612。板上跑的模型和 PC 上验证的模型对不上号,这种低级错误在多人协作时特别容易发生,命名规范能省掉大量扯皮。
每一版转换都保留中间产物。ONNX 留一份,量化前后的模型各留一份,配置文件一起归档。出问题的时候可以快速回滚,也能做版本间的 A/B 对比。我一般用一个简单的目录结构:export/放 ONNX,quant/放量化模型,logs/放每次验证的指标记录,一目了然。
写一个最小的复现脚本。就是那种只依赖模型文件和一张测试图、几十行代码就能跑出结果的脚本,PC 端和板端各一份,逻辑完全对应。任何精度问题先用它复现,能排除掉业务代码里 90% 的干扰因素。这个东西看起来是额外工作,实际上每次排查都能省下几个小时。
别信"理论上等价"。所有优化、替换、分解,只要动了模型或前后处理,就必须重新验证。我见过太多次"我只是换了个等价的实现"结果精度掉了两个点的案例,包括我自己。数学上等价和浮点上等价是两回事,量化之后更是两回事。
把验证指标固化成脚本。每次改动跑一遍,输出一张对比表,和上一版比。人眼看不出的细微退化,表格里一目了然。这套流程搭起来要一两个小时,但后面每一次迭代都会感谢当初的自己。
这套移植流程我从最早的检测模型一直用到现在的 YOLOv8,中间换过几块不同的板卡、几套不同的工具链,方法论基本没变:先对齐中间态,再保精度,最后压性能,每一步都有可量化的验收标准。真正花时间的从来不是那几条转换命令,而是转换失败之后怎么快速定位——而这恰恰是最考验对模型结构理解程度的地方。