如果你的工作涉及边缘视觉部署大概率遇到过这个问题训练服务器上跑得好好的yolo26 best.pt一旦要搬到手头的边缘盒子上就变得又慢又不听话。慢还好说更麻烦的是压根跑不起来。这里说的“边缘盒子”通常是一块带TPU加速器的板卡它上边能直接执行的推理格式是bmodel而不是 PyTorch 的 pt 权重。我最近正好把一个预训练好的 yolo26 权重完整走了一遍best.pt - bmodel的转换流程。这个过程中踩了不少坑也把很多命令行参数的含义彻底搞明白了。按照网上的碎片教程一步步复制粘贴最后八成会卡在“转换报错”或者“转换成功但推理结果全乱”这些地方。这篇文章就围绕这个转换完整链路来写把我实际用过的命令、改过的配置文件、校准数据集的准备方式都放出来给接下来要做同样事情的你提供一个可以照着做的范本。这整条链路不涉及重新训练模型也不涉及改网络结构纯粹是把已有权重从 PyTorch 生态迁移到边缘芯片生态。适合的对象是算法工程师、部署工程师、以及那些自己手里有 YOLO 权重但又没接触过边缘部署的开发者。如果你连 ONNX 是什么都还不太清楚这篇文章也能帮你把链路捋顺。1. 为什么服务器上的best.pt到了边缘盒子就不香了1.1 pt、onnx、bmodel三者到底是什么关系这三个格式之间的关系我习惯用一个不太严谨但很直观的类比来说明。PyTorch 训练出来的best.pt本质上是一个“大礼包”里面既有网络结构定义也有每一层的权重参数还包括优化器状态、训练步数、类别标签映射等一堆训练现场信息。以torch.save的方式序列化成文件。这个东西适合做训练、做实验、做断点续训但它不适合直接放到嵌入式设备里跑。bmodel则是 TPU 芯片看得懂的原生格式。它把计算图重新编排过把算子映射到芯片上对应的硬件计算单元甚至把卷积、归一化、激活函数这些常用结构融合成更高效的执行单元。更关键的是bmodel 里可以直接携带量化参数让模型以 INT8 精度运行从而显著降低内存带宽压力和计算延迟。ONNX 是中间桥梁。PyTorch 的模型可以先导出成 ONNX因为 ONNX 是一种通用的计算图描述格式不绑定训练框架。拿到 ONNX 之后平台自带的 MLIR 编译器会对它做解析、优化、量化最终生成 bmodel。所以整条路线在概念上非常清楚best.pt - ONNX - MLIR 中间表示 - 量化 - bmodel。每一步都有对应的工具缺一步都不行但每一步里也有很多容易出问题的细节。1.2 这个转换任务到底卡在哪里很多第一次接触的人以为“转换就是点一下按钮”。实际上没那么简单真正的困难集中在这几处第一PyTorch 导出的 ONNX 图上常常藏着很多冗余节点这些节点在训练时有意义在推理时纯粹是拖累。比如一些动态 shape 相关的Reshape、Expand、Shape操作TPU 编译器解析起来很费劲甚至直接报错。需要先对 ONNX 做“简化”。第二模型的预处理和后处理都需要人工显式处理。训练时用的数据增强、letterbox、归一化操作并不会自动进入推理图。需要决定哪些放到模型内部哪些放到上板程序里一旦两边设置不一致推理结果就是乱的。第三浮点模型转成 INT8 定点模型后精度或多或少会有损失。如果校准数据集选得不好、或者某些对量化敏感的层没有处理好可能出现精度断崖式下降平时 map 0.9 的模型量化完只有 0.5 的可用性。这一块需要反复迭代相当磨人。第四目标平台的工具链版本必须正确。不同芯片型号、不同 SDK 版本之间编译选项和算子支持范围有差异版本错一个号编译出来的 bmodel 就可能是废的。理解了这些卡点再去看后面每一步的命令和参数你就能反过来理解每个命令背后的目的是什么为什么必须这样做。2. 从best.pt导出干净ONNX这一步直接决定后面顺不顺利这一步是整个转换链条的源头。ONNX 文件如果导出得有问题后面所有工作都是在给一个坏基础做装修最后不是报错就是精度崩坏。所以值得多花一些时间把这一节看完。2.1 导出代码里的几个隐蔽设置导出 ONNX 的实际代码并不长。以 yolo26 训练框架里常用的导出脚本为例核心部分类似于import torch from models.experimental import get_model # 按你的实际项目路径调整 model get_model(yolo26.yaml, weightsbest.pt) model.eval() dummy_input torch.randn(1, 3, 640, 640) torch.onnx.export( model, dummy_input, best.onnx, opset_version13, do_constant_foldingTrue, input_names[images], output_names[output], dynamic_axesNone, # 强烈建议导固定shape )里面有三个地方必须注意。第一个是model.eval()。几乎每个导出教程都会提这一句但很多人不知道为什么。eval()模式会关掉 Dropout 和 BatchNorm 在训练时依赖的 batch 统计量行为把 BN 层切换成使用训练阶段累积好的 running_mean 和 running_var。对于 YOLO 这种结构来说不切换到 eval 模式导出的计算图和实际推理行为不一致轻则结果有偏差重则在量化阶段直接崩掉。第二个是dynamic_axesNone。TPU 编译器对动态 shape 的支持能力有限虽然部分算子和场景支持一定范围的动态输入但在实际部署中固定输入尺寸能省掉大量麻烦。边缘部署一般输入都是固定的比如 640x640所以直接导出固定 batch 1、固定分辨率。如果训练时的输入通道是三通道 RGB 或 BGR那个维度也不能变。第三个是opset_version13。这个值不能选得太低太低了像Mul、Sub这些基础算子可能会以旧版本语义导出后续简化工具识别不充分。也不能选得太新太新的算子集在 MLIR 工具链里可能还没有完全覆盖。据我测试下来opset 13 是兼容性很好的版本opset 14、15 也没有大问题但 13 最稳。2.2 把NMS和重参数化结构提前拆掉这一步极容易被忽略但直接影响转换成败。很多 YOLO 官方的源码或第三方修改版里在模型 forward 函数的最后会自动接上非极大值抑制NMS模块有的版本还会把 Decoupled Head、Detect 层输出形状转换封装在一起。导出 ONNX 时如果你不加区分地把整个模型端到端导出得到的 ONNX 图上会包含:类别置信度解码sigmoid、阈值过滤、TopK 选择候选框解码从 grid 坐标到原图坐标的转换NMS 操作一堆NonMaxSuppression算子以及后面跟着的Gather、Slice、Concat等辅助操作。这些算子中的相当一部分在 TPU 编译器里是没有对应硬件加速单元的强行放进 bmodel 会导致编译失败或者即便编译成功运行效率也极低。NMS 这种带有循环和动态分支的操作在硬件上做几乎是灾难。正确的做法是只导出网络主干和检测头后处理全部留给上板程序在 CPU 侧完成。也就是说best.onnx 这张图只负责输出原始特征图上的解码前结果——通常是各个尺度下的(1, 4 num_classes, grid_h * grid_w)特征向量或者在某些版本里输出多个尺度的原始特征张量一切解码操作都在外部完成。怎么判断自己的 ONNX 有没有把后处理带进去最简单的方式是在 PyTorch 里用一个torch.jit.trace或者其他方式检查模型 forward 的返回内容。如果返回的是 torchvision 的 NMS 结果、或者是已经过滤后的框坐标列表那就说明导出时把后处理打包进去了需要把模型 forward 中相关代码摘除或者换用导出专用分支。2.3 用onnx-simplifier做一次“大扫除”不管导出时怎么小心ONNX 图里还是会存在很多训练框架特有的冗余结构。比如多余的Identity节点只是用来做 shape 广播的Reshape、Expend节点导出的常量节点在推理时根本没有实际作用。这些节点虽然不影响运算正确性但会给后续的 MLIR 解析带来额外负担。有些编译器甚至会因为某个不支持的Slice用法而直接终止转换。推荐在导出后立刻跑一次onnx-simplifierpip install onnx-simplifier python -m onnxsim best.onnx best_sim.onnx跑完之后再用onnxruntime对原模型和简化后的模型各推理一次对比输出结果。如果两者输出几乎一致允许很小的浮点误差就说明简化过程没有破坏语义。这里还有一个经验导出并简化后的模型先不要直接丢给转换工具。先用一个小脚本随机生成一张图过一遍简化后的 ONNX打印输出的 shape。对于 YOLO 系列来说通常能看到类似(1, 84, 8400)这样的输出84 4框坐标 80类别数8400 表示不同尺度特征图上的候选位置总数。如果你的输出 shape 有 num_classes 参与计算就对上了。这一步能在源头上把很多问题拦下来。3. TPU-MLIR转换环境搭建版本和依赖一个都不能乱ONNX 准备干净之后就该搭转换环境了。这个环节表面上只是安装工具但根据我的经验很多人一上来就在环境问题上耗掉大半天。这里我把完整的流程和最容易出错的点写出来。3.1 设备端工具链的环境选择bmodel 的转换依赖目标芯片对应的 TPU-MLIR 编译器。不同芯片有不同的工具链版本转换环境的第一步是确认目标平台的芯片型号。以目前边缘市场上常见的平台为例端侧设备上的推理框架加载 bmodel 时必须和工具链编译时期选择的芯片型号保持一致型号对不上会直接加载失败。环境的安装方式通常是两种一种是直接用官方发布的 Docker 镜像在容器里完成全部转换工作另一种是下载 CPU 侧的发布包配合 Python 环境安装。我比较推荐 Docker 方式原因很现实工具链依赖的库版本跟宿主机环境经常冲突尤其是一些老版本工具链依赖特定版本的 glibc 和 Python官方镜像里通常已经预装好了依赖库省去大量手动排查同一个项目后期如果要在不同芯片型号之间来回验证容器可以轻松做到环境隔离。工具链装好后需要激活环境变量。以最常见的发布包为例source ${TPU_MLIR_PATH}/envsetup.sh这个脚本会把model_transform.py、model_deploy.py、run_calibration.py等工具加入 PATH。激活后可以用which model_transform.py确认路径是否正常。3.2 先跑一个官方demo验证环境每次搭完环境我都建议不要直接拿自己的模型去试而是先用工具链自带的一个官方示例模型把整个流程跑通。比如官方包里通常带有 resnet18 等经典分类模型的 demo直接按照 demo 文档里的命令走一遍model_transform.py --model_name resnet18 \ --model_def resnet18.onnx \ --input_shapes [1,3,224,224] \ --pixel_format rgb \ --mean 0,0,0 \ --scale 0.00392156862745098 \ --mlir resnet18.mlir如果这一步能顺利生成.mlir文件说明环境基础是好的。接着再走一遍部署命令model_deploy.py --mlir resnet18.mlir \ --quantize INT8 \ --calibration_table resnet18_cali_table \ --chip bm1684x \ --model resnet18_int8.bmodeldemo 跑通之后再切换到自己的模型心理会踏实很多。如果 demo 都失败那基本可以断定是环境问题而不是模型问题。这个顺序能帮你快速区分问题归属。3.3 工作目录的规划转换过程会产生多类中间文件包括原始 ONNX、简化后的 ONNX、MLIR 中间表示、校准表、量化后的 bmodel、以及各种 log。一股脑全放在同一层文件夹里后期迭代几次就会乱成一锅粥。我习惯的目录结构是这样的work/ ├── onnx/ │ └── best_sim.onnx ├── mlir/ │ └── yolo26.mlir ├── calibration/ │ ├── cali_images.txt │ └── images/ └── bmodel/ └── yolo26_int8.bmodel这样做的好处是当你要调试某个环节时能明确知道需要检查哪个目录。尤其是在反复调整校准集、反复编译新版本 bmodel 的过程中目录清晰能够省下不少时间。也算是一个小习惯但对整个流程推进挺重要。4. fp32模型到INT8量化校准集、阈值表与编译参数实测环境没问题ONNX 也干净了接下来就是整个链路中最核心也是最有技术含量的一段把浮点模型转成 MLIR再通过校准完成 INT8 量化最终生成 bmodel。4.1 模型结构和预处理参数必须和训练时严格一致第一步是把 ONNX 转换成 MLIR 中间表示。这个命令乍看参数很多但其实每个参数背后都有明确含义值得逐一说清楚。我实际在 yolo26 上跑通的命令如下model_transform.py \ --model_name yolo26 \ --model_def best_sim.onnx \ --input_shapes [1,3,640,640] \ --pixel_format bgr \ --mean 0,0,0 \ --scale 0.00392156862745098 \ --keep_aspect_ratio \ --pad_value 114 \ --mlir yolo26.mlir这里有几个关键参数必须理解透彻。--pixel_format bgr对应的是训练时图像通道的顺序。如果你的训练数据来自 OpenCV 的imread()那么输入图像本身就是 BGR 顺序这里就写bgr。如果你训练时用的是一套会先做RGB转换的流程那这里必须跟着改成rgb。一旦这里和训练不一致相当于给模型喂了通道错乱的图像推理结果必然不对。--mean 0,0,0和--scale 0.00392156862745098代表归一化参数。之所以 mean 全为 0是因为 YOLO 训练时通常用的是img / 255.0这种简单归一化也就是把 [0, 255] 的像素值映射到 [0, 1]对应 scale 约等于 0.0039。如果你的训练代码里有自己的均值/方差参数那就填训练时真正在用的数值不要照抄这组。这里的scale 0.00392156862745098其实就是 1/255 的浮点表示看到这个数就应该反应过来它对应的是最经典的除以 255 归一化。--keep_aspect_ratio和--pad_value 114对应的是 YOLO 训练和推理时常用的 letterbox 预处理。所谓 letterbox就是在缩放图像的同时保持宽高比不变再把空白区域用固定像素值填充。YOLOv5 系列以来这个 padding 值默认就是 114。如果你的模型在训练时 letterbox 用的不是 114而是别的数值也需要改掉。这一步常见误区是--keep_aspect_ratio只加一个参数看起来没啥但在后续上板程序的预处理里还要再做一次 letterbox两者必须严格对应否则图形会被拉伸或裁剪效果很差。4.2 校准数据集的准备转换完 MLIR 之后还是一个 fp32 模型。要让 bmodel 以 INT8 精度运行必须先在校准数据集上跑一遍推理收集各层激活值的分布范围由此计算出每一层合适的量化阈值。校准集的质量直接决定 INT8 模型的最终精度。校准数据集的准备有几个原则数量通常在 100 到 500 张之间。太少的话统计出来的激活分布不够稳定阈值偏差大太多的话耗时变长收益并不明显。图片内容要覆盖实际应用场景中的各种情况。比如你要做的是工业零件检测校准集里就该多放不同光照、不同角度、不同零件类别的图而不是随便从网上下载一些风景图。不应当包含大量模糊、极暗、极亮或者有明显标注噪声的图。这些异常图会污染激活值的统计分布导致最后某些层的阈值偏移。图片越小越好只要能跑通推理就行。因为校准过程只关心激活值分布不需要像训练那样做数据增强。准备好图片目录之后生成一个文件列表cali_images/0001.jpg cali_images/0002.jpg cali_images/0003.jpg ...每一行是一张图片的路径。然后执行校准run_calibration.py yolo26.mlir \ --dataset cali_images.txt \ --input_num 200 \ -o yolo26_cali_table--input_num指定实际参与校准的图片数量。我建议先选 100-200 张跑完看一眼精度如果量化掉点不明显就用这个数量如果掉点严重再适当增加图片数量重新尝试。校准表文件生成之后后面每次部署都用同一份校准表这样多次编译结果之间是可比的。4.3 生成INT8 bmodel有了校准表最后一步就是生成 bmodelmodel_deploy.py \ --mlir yolo26.mlir \ --quantize INT8 \ --calibration_table yolo26_cali_table \ --chip bm1684x \ --model yolo26_int8.bmodel--chip参数必须和目标设备型号一致。如果你手里的板卡是另一款型号这个参数也要相应修改。这一步执行完成后如果一切顺利工作目录里会出现yolo26_int8.bmodel文件。可以顺便看一眼文件大小它会比原始的 fp32 模型小不少这也是量化之后参数从 fp32 变成 int8 的直接体现。到这里模型层面的转换已经全部完成。趁热打铁补充一个我在实际项目中常用的经验刚生成的 bmodel 不要直接丢到板子上跑先在服务器上花两分钟做一次快速验证确认模型能被加载、输出 shape 是否符合预期。如果这一步通过再去考虑预处理对齐和端到端推理的问题排错效率会高很多。5. 上板验证bmrt_test、预处理对齐与精度对比bmodel 生成之后真正的挑战才开始。模型能不能编译通过是一回事上板之后能不能输出正确结果是另一回事。很多人在转换环节没有遇到任何报错结果在板子上跑出来的框全部乱飘或者置信度全为负值这时才开始回头排查。这节内容就是帮你把这个阶段快速过掉。5.1 用bmrt_test做一次冒烟测试工具链通常会自带一个模型推理测试工具名字一般是bmrt_test或类似名称。它的作用很简单加载一个 bmodel生成随机假输入跑一次完整推理检查能不能成功出结果。这一步不验证精度只验证模型在芯片上能不能真正跑起来。用 bmodel 的测试工具跑一下生成的 int8 模型bmrt_test --bmodel yolo26_int8.bmodel如果模型有多个输入还需要准备对应的随机输入文件具体格式以工具链文档为准。输出正常的话你会看到模型成功加载、推理完成、输出张量的信息打印出来。看到这一步说明算子映射和计算图编排都没问题。如果这一步就报错最常见的两个原因一是编译时的--chip和实际板卡的芯片型号不一致重新确认芯片型号再编译二是模型里的某些算子在当前工具链版本上不支持这种情况通常需要升级工具链或者对 ONNX 做进一步简化。5.2 输入预处理细节mean/scale、padding通道值如果在板子上能跑通但结果不对下一步就该检查预处理这是最常见的出错点。训练时的预处理流程会决定模型实际接收的输入数据分布。把训练代码里的预处理和上板代码里的预处理对照着看重点检查三个地方通道顺序。上板程序读取图片后如果没有在推理前做BGR - RGB转换而模型训练时用的是 BGR那模型就是在错误通道顺序的数据上推理。归一化方式。模型转换时如果写死了scale0.00392156862745098且 mean 全为 0那上板程序就不能再额外减一个其他数值的 mean否则输入分布完全错掉。letterbox 行为。转换时的--keep_aspect_ratio只影响编译器对模型输入的理解。真正把一张任意分辨率的图片处理成 640x640 的 letterbox 逻辑在上板程序里。如果上板程序用简单的强制拉伸 resize而不是保持宽高比的 letterbox检测目标就会变形小目标直接丢失。我把训练、转换、上板三端的参数列成一个对照关系方便你自查环节通道顺序归一化图像缩放方式padding 值训练BGR/255.0letterbox114转换命令bgrscale0.0039, mean0keep_aspect_ratio114上板程序BGR不做额外处理letterbox 114 填充114这三端必须完全对齐任何一个参数不一致体现在推理结果上就是框的位置系统性偏移、置信度整体偏低、或者小目标召回率暴跌。5.3 后处理的取舍NMS留在外面做还记得之前在导出 ONNX 时特意把 NMS 摘掉的操作吗在上板阶段这一点依然成立。bmodel 只做网络的推理部分输出原始的框坐标和类别置信度NMS 必须在 CPU 侧完成。上板程序的推理流程一般是这样的用解码器读取图像做 letterbox 预处理把图像变成[1, 3, 640, 640]把预处理后的图像数据喂给 bmodel 推理拿到原始输出特征图后在 CPU 上做解码把特征图映射回原图坐标做置信度过滤和 NMS得到最终检测结果。也就是说bmodel 的输出不能直接作为最终结果暴露出来。如果你手头的后处理代码是之前在 PyTorch 里写的那套逻辑是可以复用过来的只需要把输入从 PyTorch Tensor 换成原始的特征图数据即可。这里有个实操小技巧在调后处理时先用训练好的 PyTorch 模型在服务器上对同一张图推理一次打印输出特征再在板子上用 bmodel 对同一张预处理后的图推理对比两者输出的特征值。如果两者接近量化和浮点的差异通常很小说明模型本身没问题后处理应该能直接对上如果差异巨大说明前端的预处理或者 bmodel 的输入理解出了问题。6. 转换过程中最容易翻车的几个场景复盘最后这部分我把转换过程中实际遇到的高频问题整理出来。这些都是我在不同项目里真实碰到的整理成场景复盘的形式方便你遇到类似现象时快速定位。6.1 场景一模型推理结果全是框在图像边缘现象检测框大量集中在图像四周边界区域或者整张图的置信度都很低几乎没有有效检测。这个现象我在第一次把 yolo26 部署到板子上时遇到过。排查到最后问题出在 letterbox 上转换模型时设置了--keep_aspect_ratio编译器认为模型的输入是已经 padding 过的 letterbox 图但上板程序直接做了强制拉伸 resize等于把一张被拉伸变形的图喂给了模型。模型训练时学到的目标形状特征完全错位检测结果自然全乱。解决办法上板程序改为先做保持宽高比的缩放再用 114 填充空白区域或者反过来转换时去掉--keep_aspect_ratio上板程序改成直接拉伸 resize。两条路只能二选一不能混着来。6.2 场景二量化后精度跌得没法看现象fp32 模型在服务器上 map 0.88量化成 int8 bmodel 后 map 掉到 0.6 以下某些类别几乎完全无法检测。这种掉点通常不是单点问题需要从校准数据入手逐个排查。第一个原因是校准集太单一。比如你打算在户外场景部署但校准集用的全是室内图片激活值分布偏差过大。这时不需要动模型直接替换校准图片重新生成校准表再编译一次往往能救回来不少。第二个原因是某些层对量化特别敏感。YOLO 的检测头通常比主干更容易掉点因为输出特征图的动态范围大且稀疏。如果你在校准集调整后仍然掉点严重可以参考工具链文档里的混合精度选项把检测头部分的关键层保留为 fp32其余部分继续走 int8。混合精度能明显缓解量化掉点。第三个原因是校准集里存在异常图。我踩过的坑是校准集中混入了几张带网格注释线的图片导致某些层的激活值统计被污染。去掉这些异常图之后精度恢复明显。6.3 场景三编译报错算子不支持现象model_deploy.py执行时提示某个 ONNX 算子没有对应的实现转换终止。这种报错需要区分是算子真的不支持还是 ONNX 图里有冗余结构导致的误解。第一种处理方式回看 ONNX 简化这一步。如果onnx-simplifier没有启用先启用它很多报错会直接消失。第二种处理方式换 opset 版本重新导出。有些算子在不同 opset 版本下的语义表示不同13 不行就试 14 或 15。第三种处理方式升级工具链版本。遇到新增的激活函数或不太常用的算子老版本工具链确实不支持这时候升级工具链通常能覆盖到更多算子。如果升级之后依然报错就需要考虑在 PyTorch 端把模型结构微调一下用基础算子组合替代那些不支持的复杂算子。不过这种情况在 YOLO 系列中比较少见多数报错都是冗余节点引起的。6.4 场景四bmodel加载失败或者异常卡死这个场景多发生在环境版本不匹配上。工具链编译时选定的芯片型号和上板设备的实际型号不一致或者工具链版本和板卡上的运行时驱动版本跨度太大导致 bmodel 内部依赖的运行时接口对不上。遇到这问题先核对编译时--chip参数是否和设备完全一致工具链版本是否在设备 SDK 官方兼容列表里。这两个问题排除后如果还存在加载异常可以换个思路先用官方 demo 编译出来的 bmodel 上板验证确认板卡基础链路正常再回来怀疑自己的模型。写在最后的一点体会把 yolo26 best.pt 转成 bmodel 这件事单看命令可能不到十行但真正顺畅跑通一个项目需要对整条链路有整体理解。我从这次经历里得到的最深体会是转换工作要尽早介入不要等模型训练完全冻结了才开始考虑部署。如果在导出 ONNX 之前就能确定好输入分辨率、预处理方式和是否保留后处理模块后续的转换会顺利得多。另外一个操作建议是把每一次转换使用的命令、校准集路径、模型版本和最终精度记录到一个简单的表格里。转换过程不像训练那样可以靠损失函数曲线来判断好坏对比表格能帮你快速找出最优的那一次编译结果。这篇文章里写到的命令和参数都是基于我当前项目所用工具链的实际操作。不同版本的 TPU-MLIR 在个别参数名上可能略有差异但整体的pt - onnx - mlir - 校准 - bmodel这条链路是通用的。你按照这个思路排查即使遇到具体报错也能更快定位到问题所在的环节。
阅读完成 · 觉得有帮助?