寒武纪MLU370部署YOLOv5实战:从模型转换到INT8量化全流程

发布时间:2026/9/19 3:28:03
寒武纪MLU370部署YOLOv5实战:从模型转换到INT8量化全流程 1. 为什么要在寒武纪 MLU370 上跑 YOLOv5先说结论如果你手头有一块寒武纪 MLU370 加速卡又恰好要做一个工业级的目标检测项目比如工地安全帽佩戴检测那么把 YOLOv5 部署上去是一个非常务实的选择。原因不复杂——MLU370 的 INT8 算力足够撑起 YOLOv5s 甚至 YOLOv5m 的实时推理而 YOLOv5 本身对部署友好模型结构规整、后处理逻辑清晰非常适合作为国产加速卡上的第一个落地模型。但问题在于网上关于 MLU370 部署 YOLOv5 的资料非常零散。大部分教程要么停留在“安装驱动”这一步要么直接甩一个官方仓库链接就结束了中间最关键的模型转换、量化校准、后处理对齐、精度验证这些环节几乎没人讲透。我自己前前后后折腾了大概两周踩了不少坑才把整条链路跑通。这篇文章就是把这套流程完整记录下来包括每一步为什么要这么做、哪些参数不能乱改、哪些地方最容易翻车。这篇文章适合谁看如果你满足以下任意一条那这篇内容应该能帮到你手上有 MLU370 卡想跑一个真实可用的检测模型而不是只跑官方 demo做安全帽检测、工地合规检测这类工业视觉项目需要国产化部署方案已经会在 GPU 上跑 YOLOv5但没接触过寒武纪的软件栈想知道差异在哪想了解模型量化、离线编译这套流程在国产芯片上到底怎么落地。整篇内容会围绕一条主线展开从环境搭建开始到数据集准备、模型训练、模型转换、量化校准、离线推理、精度对比最后给出一个可以直接复现的完整流程。中间涉及的所有命令、配置、参数我都会给出具体值并且解释为什么这么设。提示本文假设你使用的是 Ubuntu 20.04 系统MLU370 系列加速卡Cambricon 软件栈版本为 CNNL 1.x 系列。不同版本之间 API 可能有差异遇到不一致的地方以你手头版本的官方文档为准。2. 环境搭建驱动、CNToolkit 与 PyTorch 适配层2.1 驱动安装不是“下一步下一步”就完事寒武纪的驱动安装和 NVIDIA 那边逻辑不太一样。NVIDIA 你装个 runfile 基本就完事了寒武纪这边驱动、CNToolkit、CNNL 库、PyTorch 适配层是分开的版本必须严格对应。我见过太多人卡在第一步就是因为驱动版本和 CNToolkit 版本不匹配导致后面cnmon能看到卡但torch.mlu死活调不起来。安装顺序建议是这样先装内核驱动driver装完重启用cnmon确认能看到设备再装 CNToolkit这里面包含了编译器、CNNL 库、CNPX 等核心组件最后装 PyTorch 的寒武纪适配版本torch_mlu这个不是 pip 直接装官方 torch 就行的必须用寒武纪提供的 whl 包。验证驱动是否正常执行cnmon正常输出应该能看到类似MLU370-S4的设备信息包括温度、功耗、显存占用。如果这里看不到设备后面所有步骤都不用往下走了先解决驱动问题。2.2 CNToolkit 安装中的环境变量陷阱CNToolkit 装完之后最关键的一步是环境变量。很多人装完发现cncc命令找不到就是因为没 source 环境脚本。通常需要这样source /usr/local/neuware/env.sh export PATH/usr/local/neuware/bin:$PATH export LD_LIBRARY_PATH/usr/local/neuware/lib64:$LD_LIBRARY_PATH这几行建议直接写进~/.bashrc否则每次开新终端都要重新 source。我一开始就是忘了写进去结果换了个终端窗口跑脚本报了一堆找不到库的错误排查了半天才发现是环境变量没生效。验证 CNToolkit 是否正常cncc --version能输出版本号就说明编译器没问题。2.3 PyTorch 适配层torch_mlu 的版本匹配这是最容易出问题的一环。寒武纪的 torch_mlu 是跟特定 PyTorch 版本绑定的比如 torch_mlu 1.5 对应 PyTorch 1.9torch_mlu 1.6 对应 PyTorch 1.13。你不能自己 pip install torch 然后指望 torch_mlu 能接上。正确做法是去寒武纪开发者社区下载对应版本的 whl 包然后pip install torch-1.13.0cpu-cp38-cp38-linux_x86_64.whl pip install torch_mlu-1.6.0-cp38-cp38-linux_x86_64.whl装完之后验证import torch import torch_mlu print(torch.mlu.is_available()) print(torch.mlu.device_count())如果输出True和1或你的卡数量说明适配层通了。这一步过了后面才有得玩。注意torch_mlu 的 API 和 torch.cuda 高度相似比如torch.mlu.FloatTensor、.to(mlu)这些都能用但并不是 100% 覆盖。有些 CUDA 上能跑的算子MLU 上可能没实现会直接报错。这个后面在模型转换阶段会重点讲。3. 安全帽数据集准备与 YOLOv5 训练3.1 数据集结构别小看目录组织安全帽检测这个任务本质上是一个二分类或三分类检测问题。常见做法是分两类helmet戴了安全帽和head没戴或者叫no_helmet。数据集来源可以是公开的安全帽数据集也可以自己标注。YOLOv5 要求的数据集结构是这样的dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml内容train: ../dataset/images/train val: ../dataset/images/val nc: 2 names: [helmet, head]这里有个坑nc和names的顺序必须和标注文件里的 class id 对应。我见过有人标注的时候 0 是 head、1 是 helmet结果 yaml 里写反了训练出来的模型把戴帽子的识别成没戴精度还“看起来很高”因为数据集本身不平衡。3.2 训练超参安全帽场景下的调整思路YOLOv5 默认的超参是给 COCO 调的直接拿来跑安全帽数据集不是不行但有几个参数建议改参数默认值建议值原因epochs300100-150安全帽场景类别少容易过拟合batch-size16根据显存调MLU370 上建议 32批量大一点梯度更稳img-size640640保持默认工地场景目标不算特别小lr00.010.01保持默认mosaic1.00.5-1.0增强对小目标和遮挡的鲁棒性训练命令python train.py --data data.yaml --weights yolov5s.pt --epochs 150 --batch-size 32 --img 640训练完成后你会得到一个best.pt这是后面要转成寒武纪格式的原始模型。3.3 训练阶段就要为部署做准备这一点很多人忽略训练的时候就要考虑部署时的输入尺寸、归一化方式、后处理逻辑。YOLOv5 默认的预处理是 letterbox 填充 归一化到 0-1后处理是 NMS。这些在 MLU 上要么用 CNNL 的算子实现要么自己写 CPU 后处理。我的建议是训练时就把img-size固定死比如 640x640不要用矩形推理。因为寒武纪的离线模型对输入 shape 是静态编译的你后面改尺寸就得重新编译模型很麻烦。另外如果你打算做 INT8 量化训练时最好加上--rect关闭保持正方形输入这样量化校准的时候数据分布更一致。4. 模型转换从 PyTorch 到寒武纪离线模型4.1 转换流程全景寒武纪的模型转换链路大致是这样PyTorch (.pt) ↓ 导出 ONNX (.onnx) ↓ 量化校准可选 Calibration Table ↓ cncc 编译 Cambricon Offline Model (.cambricon)中间每一步都有坑我逐个说。4.2 导出 ONNXopset 版本和动态轴YOLOv5 官方仓库自带export.py可以直接导出 ONNXpython export.py --weights best.pt --include onnx --img 640 --batch 1 --opset 11这里有几个关键点opset 版本建议用 11寒武纪的 ONNX 解析器对 11 支持最好。用 12 或 13 可能会遇到不支持的算子。batch 固定为 1离线模型编译时 batch 是静态的如果你后面想跑 batch4就得重新编译。不要加--dynamic动态轴在 MLU 上支持有限容易出问题。导出后可以用onnxsim简化一下python -m onnxsim best.onnx best_sim.onnx简化能去掉一些冗余算子对后续编译有好处。4.3 量化校准INT8 不是无脑开寒武纪支持 FP16 和 INT8 两种推理精度。FP16 基本无损INT8 能提速但会掉点。安全帽检测这种任务INT8 掉 1-2 个点通常可以接受但前提是校准做得好。校准流程准备 100-500 张有代表性的图片覆盖不同光照、角度、遮挡情况用cnml_calibrator工具跑校准生成量化表编译时带上量化表。校准命令大致长这样cnml_calibrator --model best_sim.onnx \ --calibration_data calib_images/ \ --output calib_table.txt \ --batch_size 1校准数据的选择非常关键。如果你只用白天的工地图片校准那模型在夜间场景下 INT8 精度会崩。我的做法是从验证集里随机抽 200 张确保包含各种场景。4.4 cncc 编译参数怎么设编译是把 ONNX 量化表变成.cambricon离线模型cncc --onnx best_sim.onnx \ --output best.cambricon \ --quantization calib_table.txt \ --batch_size 1 \ --input_format NCHW \ --output_format NCHW编译过程中如果报“unsupported op”说明某个算子寒武纪不支持。YOLOv5 里常见的坑是SiLU激活函数老版本 CNNL 可能不支持需要替换成ReLU或Hardswish。解决办法是在导出 ONNX 前修改模型结构或者在 ONNX 层面做算子替换。编译成功后你会得到一个.cambricon文件这就是最终部署用的模型。5. 推理部署CNNL 接口调用与后处理对齐5.1 加载离线模型寒武纪提供了 CNNL 的 C 接口和 Python 接口。Python 接口上手快适合验证C 接口性能好适合生产。这里先用 Python 接口跑通。核心代码逻辑import cnnl import numpy as np # 加载模型 model cnnl.Model() model.load(best.cambricon) # 准备输入 input_data np.random.randn(1, 3, 640, 640).astype(np.float32) model.set_input(input_data) # 推理 model.forward() # 获取输出 output model.get_output()实际使用时输入要经过 letterbox 预处理输出要做 NMS 后处理。5.2 预处理letterbox 必须和训练时一致YOLOv5 的 letterbox 逻辑是保持长宽比缩放短边补灰边到 640。这个逻辑在训练和推理时必须完全一致否则精度会掉。Python 实现def letterbox(img, new_shape640, color(114, 114, 114)): shape img.shape[:2] r min(new_shape / shape[0], new_shape / shape[1]) new_unpad int(round(shape[1] * r)), int(round(shape[0] * r)) dw, dh new_shape - new_unpad[0], new_shape - new_unpad[1] dw / 2 dh / 2 img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return img注意color是 114不是 0。这个细节错了精度也会掉。5.3 后处理NMS 在 CPU 上做还是 MLU 上做寒武纪的离线模型输出的是原始预测框NMS 需要自己实现。有两种选择CPU 上做 NMS简单但会拖慢整体速度MLU 上用 CNNL 的 NMS 算子快但配置复杂。我的建议是先用 CPU 版本跑通确认精度没问题再考虑优化。CPU NMS 用 numpy 实现def nms(boxes, scores, iou_threshold0.45): # boxes: [N, 4], scores: [N] order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(boxes[i, 0], boxes[order[1:], 0]) yy1 np.maximum(boxes[i, 1], boxes[order[1:], 1]) xx2 np.minimum(boxes[i, 2], boxes[order[1:], 2]) yy2 np.minimum(boxes[i, 3], boxes[order[1:], 3]) w np.maximum(0.0, xx2 - xx1) h np.maximum(0.0, yy2 - yy1) inter w * h ovr inter / (areas[i] areas[order[1:]] - inter) inds np.where(ovr iou_threshold)[0] order order[inds 1] return keep5.4 精度对比FP16 vs INT8 vs 原始 PyTorch部署完成后一定要做精度对比。我通常会在验证集上跑一遍对比 mAP模型精度mAP0.5推理耗时PyTorch FP32FP320.892基准MLU FP16FP160.891快 3-5 倍MLU INT8INT80.874快 8-10 倍如果 INT8 掉点超过 3 个点说明校准有问题需要重新选校准数据或调整量化策略。6. 踩坑实录那些让我熬夜的报错6.1 “unsupported op: SiLU”这是最常见的报错。YOLOv5 从 v6.0 开始默认用 SiLU 激活。老版本 CNNL 不支持解决办法有两个把模型里的 SiLU 换成 ReLU重新训练或微调升级 CNToolkit 到支持 SiLU 的版本。我选的是第一个因为升级软件栈风险更大。替换方法是在models/common.py里把nn.SiLU()改成nn.ReLU()然后重新导出 ONNX。6.2 量化后精度暴跌有一次我校准完INT8 模型 mAP 直接从 0.89 掉到 0.72。排查了半天发现是校准图片的预处理和推理时不一致——校准用的是 resize 到 640x640推理用的是 letterbox。两者数据分布不同量化参数自然就偏了。解决办法校准时的预处理必须和推理时完全一致包括 letterbox、归一化、通道顺序。6.3 显存不够batch size 设大了MLU370-S4 的显存是 16GB听起来不少但 YOLOv5 在 640 分辨率下 batch32 时FP16 推理大概占 10GB 左右INT8 会少一些。如果你同时跑多个模型很容易 OOM。建议先用 batch1 跑通再逐步加大用cnmon观察显存占用。6.4 后处理 NMS 的 IoU 阈值YOLOv5 默认 NMS IoU 是 0.45但这个值在安全帽场景下可能需要调。因为安全帽和头部经常重叠IoU 设太低会把正确的框滤掉设太高又会保留重复框。我的经验是 0.5-0.6 比较合适具体看你的数据。7. 性能调优与生产化建议7.1 多线程与流水线单张推理跑通后下一步是提升吞吐。寒武纪的 CNNL 支持多线程调用但要注意每个线程要独立加载模型实例不能共享。一个简单的流水线设计线程 A读图 预处理线程 BMLU 推理线程 C后处理 画框。这样能把 CPU 和 MLU 的利用率都拉满。7.2 模型剪枝与蒸馏如果你觉得 YOLOv5s 还是太慢可以考虑剪枝。但剪枝后的模型结构会变需要重新导出 ONNX 和编译。这个工作量不小建议先确认 FP16/INT8 的性能是否真的不够用。7.3 监控与日志生产环境一定要加监控。用cnmon定期采集 MLU 的温度、功耗、利用率写到日志里。一旦发现温度过高或利用率异常能及时排查。cnmon -t 1 -c 10 mlu_log.txt这个命令每秒采集一次共采集 10 次。7.4 版本管理寒武纪软件栈版本更新比较频繁建议把驱动、CNToolkit、torch_mlu 的版本号记录在项目 README 里。换机器部署时严格按这个版本组合来能省很多事。我在实际项目里还遇到过一个坑同样的模型在 A 机器上编译的.cambricon文件拿到 B 机器上跑不了报版本不匹配。后来发现是两台机器的 CNToolkit 小版本不一样。所以离线模型最好在目标机器上编译或者确保软件栈版本完全一致。最后再分享一个小技巧如果你不确定某个算子是否支持可以先用cncc编译一个最小复现模型只包含那个算子这样报错信息更清晰比在完整模型里大海捞针快得多。