最近开始重新梳理端侧模型部署这件事。作为一个平时主要和 PyTorch、YOLO、MobileNet、ONNX 打交道的算法工程师,我最开始对“把模型部署到开发板”的理解其实很简单:
模型训练好 → 导出 ONNX → 工具链转换 → 扔到板子上跑。
这个系列不会单纯复述工具链手册,也不会把文章写成几十条命令组成的安装教程,而是选择具体开发板、具体模型和具体问题,把一个算法模型真正从 GPU 环境一路“扔”到开发板上。
1. 开发板可能是模型最好的“照妖镜”
模型还在 GPU 上训练的时候,我们通常最关心几个指标:
指标 | 关注内容 |
|---|---|
Accuracy / mAP | 精度够不够 |
Loss | 训练是否收敛 |
Params | 模型是不是足够轻 |
FLOPs | 理论计算量大不大 |
但模型真正进入端侧以后,问题会突然变多:
这时候除了精度,还会开始关心 Latency、内存、算子支持、前后处理、量化误差以及模型到底有多少计算真正跑在 BPU 上。
这也是我逐渐发现的一件很有意思的事情:
GPU 上表现漂亮的模型,并不一定就是最适合开发板的模型。
很多平时训练阶段看不到的问题,只有真正把模型扔到板子上才会暴露出来。
2. 第一道关甚至还没到 J5:先看 ONNX
假设现在已经训练好了一个视觉模型:
然后顺利导出了:
使用 ONNX Runtime 跑一下:
程序不报错,输出 Shape 也正确。
是不是可以直接开始 J5 模型转换了?
我现在的答案是:
先别急。
因为:
ONNX 能运行,只代表计算图能够执行,并不代表它和 PyTorch 模型完全一致。
举一个很简单的例子:
模型 | 预测类别 | 置信度 |
PyTorch | floor | 0.91 |
ONNX | floor | 0.83 |
但如果后面再进行 INT8 量化:
模型 | 置信度 |
PyTorch FP32 | 0.91 |
ONNX FP32 | 0.83 |
INT8 | 0.72 |
当最终精度下降时,我们很容易第一时间怀疑:
“是不是 J5 量化把模型搞坏了?”
实际上问题可能在进入工具链之前就已经出现。
所以我现在把:
看成真正端侧部署的第一道关。
3. 最简单的办法:让 PyTorch 和 ONNX 打一架
验证方式其实不用设计得特别复杂。
然后直接比较输出:
我通常主要关注:
输出 Shape 是否一致;
最大误差是否异常;
平均误差是否异常;
最终预测是否一致;
完整测试集上的 Accuracy / mAP 是否明显变化。
这里有一个经验:
单张图片适合 Debug,完整测试集才适合下结论。
一张图片结果相同,并不能证明模型真的没有发生变化。
4. 最容易浪费半天时间的,可能只是 RGB 和 BGR
刚开始排查 ONNX 推理差异时,我通常会自然地怀疑:
Opset 版本;
Dynamic Shape;
ONNX 算子;
模型结构;
推理框架。
但实际工程中,一个更加常见的“罪魁祸首”是:
前处理。
例如:
这些地方随便错一个,程序通常都不会崩。
这反而是最麻烦的。
比如训练时使用 RGB,而推理程序直接把 OpenCV 读取出来的 BGR 图像送进模型:
但是精度已经悄悄发生变化。
这类问题特别符合我对端侧部署的一个感受:
最可怕的 Bug 不是程序不能运行,而是程序运行得非常正常。
至少先排除人为因素,再去怀疑模型。
5. YOLO 这种检测模型还会再“骗”你一次
分类模型相对简单,输出通常比较直接。
检测模型就不同了。
一个 YOLO 类模型最终产生检测框之前,一般还会经历:
这意味着:
即使 PyTorch 和 ONNX 的模型原始输出已经有一些差异,经过 Confidence 和 NMS 之后,最终画出来的框仍然可能几乎一样。
例如:
肉眼看两边都有框,感觉没有问题。
继续量化:
假设阈值设置成:
结果就变成:
模型 | 检测结果 |
PyTorch | √ |
ONNX | √ |
INT8 | × |
最后看到的现象是:
INT8 模型漏检。
但仔细往前追会发现,这个目标其实从:
阶段就已经开始发生变化。
所以对于检测模型,我现在不会只看最终画框效果。模型真正准备进入 J5 工具链之前,最好已经把浮点模型本身检查干净。
6. 为什么我不喜欢一上来就研究量化参数
J5 工具链后面真正有意思的部分当然是模型量化和编译。
但量化本身又会引入很多新的变量:
如果再进一步进入 QAT,还会遇到 FakeQuant、Observer 等新的东西。
如果前面的浮点模型还没有确认一致,就直接进入量化阶段,最后很容易出现一种经典场景:
所以我现在更加倾向于采用一个比较“笨”的办法:
一关一关过。
阶段 | 需要先回答的问题 |
PyTorch | 原始模型是否正常? |
ONNX | 导出后结果是否一致? |
Calibration | INT8 精度损失多少? |
Quantized Model | 定点模型是否满足要求? |
Compile | 模型能否正确编译? |
J5 BPU | 板端精度和性能怎么样? |
这样最终如果:
至少可以比较有底气地把问题集中到量化阶段。
而不是重新从训练代码开始猜。
7. J5 真正有意思的地方,是模型开始离开 GPU
J5 的算法工具链 OpenExplorer 本身就是为了把训练侧的浮点模型进一步量化、转换并部署到地平线计算平台。真正上板时,运行在 BPU 上的是经过工具链处理后的部署模型,而不是简单把原始 ONNX 文件复制到开发板执行。
这也是我觉得端侧部署开始变得有意思的地方。
在 GPU 上,我们很容易形成一些直觉:
参数量少 = 更快。
FLOPs 低 = 延迟低。
网络更小 = 更适合端侧。
真正到了 J5 上,这些判断就未必永远成立。
因为还会受到算子类型、计算图结构、BPU 支持情况、CPU/BPU 划分、访存等因素影响。
例如社区里就有一个很典型的问题:模型虽然能够导成 ONNX,但由于输入类型以及算子约束的问题,工具链判断没有可运行在 BPU 上的节点。最后修改输入 Tensor 类型后才通过模型检查。
这类问题在训练 GPU 模型的时候几乎不会特别考虑。
但到了开发板,它突然就变成了核心问题。
所以我觉得:
开发板其实就是一个很好的模型“照妖镜”。
论文里的 FLOPs、训练服务器上的 FPS、PyTorch Profiler 给出的结果都只能作为参考。
真正想知道模型适不适合端侧:
扔上板子看看。
8. 一个模型真正扔到 J5 上以后,我会开始关注什么?
以前评估模型,我可能主要维护这样一张表:
Model | Accuracy | Params | FLOPs |
Model A | 92.5% | 5M | 1.2G |
Model B | 92.1% | 3M | 0.8G |
进入端侧以后,我觉得这张表至少应该扩展成:
Model | Accuracy | Params | FLOPs | INT8 Accuracy | J5 Latency |
Model A | - | - | - | - | - |
Model B | - | - | - | - | - |
如果继续深入,甚至还可以增加:
到这个阶段以后,模型选型就从:
“哪个模型精度最高?”
逐渐变成:
“哪个模型在满足精度要求的情况下,更适合当前硬件?”
这其实是两个完全不同的问题。
9. 这次没有准备把文章写成“一键复现教程”
这篇更多是整个系列的开场,所以我没有把 OpenExplorer 环境、工具链命令、模型转换参数全部贴出来。
一方面,不同 J5 开发环境、OpenExplorer 版本、模型输入和网络结构本身就存在差异;另一方面,我觉得相比把几十条命令复制出来,更重要的是先把模型部署过程中的几个检查点建立起来。
真正自己复现时,大概率还是会遇到一些“小惊喜”:
但这反而是整个过程比较有意思的地方。
如果一条命令从 PyTorch 直接生成最终板端模型,那大概也就没有什么值得写的了。
10. 写在最后
第一次认真把模型从训练环境往 J5 上迁移以后,我最大的变化不是学会了某一条转换命令,而是开始把一个 AI 模型看成完整的工程链路:
以前主要盯着:
现在还会多看:
一个模型在 GPU 上训练完成,并不意味着它已经完成了自己的使命。
有时候,真正有意思的故事,恰恰发生在它离开 GPU 以后。
所以这个系列就干一件事:
找一个模型,找一块开发板,然后把它扔上去看看。
