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.9,torch_mlu 1.6 对应 PyTorch 1.13。你不能自己 pip install torch 然后指望 torch_mlu 能接上。
正确做法是去寒武纪开发者社区下载对应版本的 whl 包,然后:
pip install torch-1.13.0+cpu-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 调的,直接拿来跑安全帽数据集不是不行,但有几个参数建议改:
| 参数 | 默认值 | 建议值 | 原因 |
|---|---|---|---|
| epochs | 300 | 100-150 | 安全帽场景类别少,容易过拟合 |
| batch-size | 16 | 根据显存调,MLU370 上建议 32 | 批量大一点梯度更稳 |
| img-size | 640 | 640 | 保持默认,工地场景目标不算特别小 |
| lr0 | 0.01 | 0.01 | 保持默认 |
| mosaic | 1.0 | 0.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 导出 ONNX:opset 版本和动态轴
YOLOv5 官方仓库自带export.py,可以直接导出 ONNX:
python export.py --weights best.pt --include onnx --img 640 --batch 1 --opset 11这里有几个关键点:
- opset 版本:建议用 11,寒武纪的 ONNX 解析器对 11 支持最好。用 12 或 13 可能会遇到不支持的算子。
- batch 固定为 1:离线模型编译时 batch 是静态的,如果你后面想跑 batch=4,就得重新编译。
- 不要加
--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_shape=640, 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, interpolation=cv2.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, value=color) 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_threshold=0.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:
| 模型 | 精度 | mAP@0.5 | 推理耗时 |
|---|---|---|---|
| PyTorch FP32 | FP32 | 0.892 | 基准 |
| MLU FP16 | FP16 | 0.891 | 快 3-5 倍 |
| MLU INT8 | INT8 | 0.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 分辨率下 batch=32 时,FP16 推理大概占 10GB 左右,INT8 会少一些。如果你同时跑多个模型,很容易 OOM。
建议先用 batch=1 跑通,再逐步加大,用cnmon观察显存占用。
6.4 后处理 NMS 的 IoU 阈值
YOLOv5 默认 NMS IoU 是 0.45,但这个值在安全帽场景下可能需要调。因为安全帽和头部经常重叠,IoU 设太低会把正确的框滤掉,设太高又会保留重复框。我的经验是 0.5-0.6 比较合适,具体看你的数据。
7. 性能调优与生产化建议
7.1 多线程与流水线
单张推理跑通后,下一步是提升吞吐。寒武纪的 CNNL 支持多线程调用,但要注意每个线程要独立加载模型实例,不能共享。
一个简单的流水线设计:
- 线程 A:读图 + 预处理;
- 线程 B:MLU 推理;
- 线程 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编译一个最小复现模型,只包含那个算子,这样报错信息更清晰,比在完整模型里大海捞针快得多。