博客算法工具链把模型扔上开发板①:先拿 J5 开刀,PyTorch 模型离真正上板还有多远?

把模型扔上开发板①:先拿 J5 开刀,PyTorch 模型离真正上板还有多远?

hailong2026-08-28
28
0

最近开始重新梳理端侧模型部署这件事。作为一个平时主要和 PyTorch、YOLO、MobileNet、ONNX 打交道的算法工程师,我最开始对“把模型部署到开发板”的理解其实很简单:

模型训练好 → 导出 ONNX → 工具链转换 → 扔到板子上跑。

真正开始折腾 地平线 J5 系列开发板以后,会发现这几个箭头虽然看起来很短,但里面藏着的问题一点不少。
于是有了这个系列——《把模型扔上开发板》

这个系列不会单纯复述工具链手册,也不会把文章写成几十条命令组成的安装教程,而是选择具体开发板、具体模型和具体问题,把一个算法模型真正从 GPU 环境一路“扔”到开发板上。

第一块板,就从 J5 开始。

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

只看最终结果,两边都是 floor,似乎完全没有问题。

但如果后面再进行 INT8 量化:

模型

置信度

PyTorch FP32

0.91

ONNX FP32

0.83

INT8

0.72

当最终精度下降时,我们很容易第一时间怀疑:

“是不是 J5 量化把模型搞坏了?”

实际上问题可能在进入工具链之前就已经出现。

所以我现在把:

看成真正端侧部署的第一道关。


3. 最简单的办法:让 PyTorch 和 ONNX 打一架

验证方式其实不用设计得特别复杂。

完全相同的一份输入分别送给 PyTorch 和 ONNX:

然后直接比较输出:

我通常主要关注:

  • 输出 Shape 是否一致;

  • 最大误差是否异常;

  • 平均误差是否异常;

  • 最终预测是否一致;

  • 完整测试集上的 Accuracy / mAP 是否明显变化。

这里有一个经验:

单张图片适合 Debug,完整测试集才适合下结论。

一张图片结果相同,并不能证明模型真的没有发生变化。


4. 最容易浪费半天时间的,可能只是 RGB 和 BGR

刚开始排查 ONNX 推理差异时,我通常会自然地怀疑:

  • Opset 版本;

  • Dynamic Shape;

  • ONNX 算子;

  • 模型结构;

  • 推理框架。

但实际工程中,一个更加常见的“罪魁祸首”是:

前处理。

例如:

这些地方随便错一个,程序通常都不会崩。

这反而是最麻烦的。

比如训练时使用 RGB,而推理程序直接把 OpenCV 读取出来的 BGR 图像送进模型:

但是精度已经悄悄发生变化。

这类问题特别符合我对端侧部署的一个感受:

最可怕的 Bug 不是程序不能运行,而是程序运行得非常正常。

所以现在做模型一致性验证时,我更倾向于让 PyTorch 和 ONNX 共用同一套前处理

至少先排除人为因素,再去怀疑模型。


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 以后。

所以这个系列就干一件事:

找一个模型,找一块开发板,然后把它扔上去看看。

第一块板,从 地平线 J5 系列开发板开始。
算法工具链
技术深度解析征程5
评论0
0/600