接手一个室内AGV的视觉检测项目,要求在征程6M上跑YOLOv5m,目标帧率30fps,mAP不低于80%。前前后后折腾了两周,从环境配置到精度恢复再到性能压榨,几乎把能踩的坑踩了个遍。最后帧率干到41fps,mAP 81.2%。这篇把全流程记下来,后面做类似项目的可以直接抄作业。
一、环境配置:版本对不齐是一切噩梦的开端
拿到板子第一件事是确认OE包版本。我习惯性直接docker pull openexplorer/horizon_j6_open:latest,结果编译出来的hbm在板端加载报HB_DNN_LAYOUT_MISMATCH,查了一下午日志发现Docker里的hb_mapper是3.6.2,板端刷的是3.7.0,差一个小版本都不行。
正确的做法是先查板端版本再拉镜像:
```bash
# 板端执行
cat /opt/horizon/version
# 输出:3.7.0-20250815
# Docker里拉对应版本
docker pull openexplorer/horizon_j6_open:v3.7.0
# 进去双重确认
hb_mapper --version
# 输出:3.7.0-release
```
顺便提一嘴,hb_mapper 3.7.0相比3.6.x在编译优化上改进不少,O3模式下activation复用率平均提升了8%,建议能用新版本就别守着旧版本。
另一个坑是march参数。征程5的代号是bernoulli,征程6改成了nash-e,对应BPU纳什架构。我第一次写成了bernoulli2,编译没报错,板端加载直接segfault,连报错信息都没有。后来翻OE包里的文档才发现对照表,这种低级错误浪费了我整整一个下午。
二、模型准备:别被动态shape坑死
YOLOv5m的Detect头里有torch.cat和F.interpolate,这两个在ONNX导出时会生成动态axis。hb_mapper目前只支持静态shape,所以必须把所有输入尺寸固定死。
我一开始的导出代码:
```python
torch.onnx.export(model, dummy, 'model.onnx',
input_names=['images'],
output_names=['output'],
dynamic_axes={'images': {0: 'batch', 2: 'height', 3: 'width'}} # 错误!
)
```
编译时报错:[ERROR] ONNX model contains dynamic shapes。解决方式很简单,去掉dynamic_axes:
```python
dummy = torch.randn(1, 3, 640, 640)
torch.onnx.export(model, dummy, 'yolov5m.onnx',
opset_version=11,
input_names=['images'],
output_names=['p3', 'p4', 'p5'],
do_constant_folding=True,
)
```
Detect头里的torch.max和torch.topk在opset 11不支持,需要手动把后处理拆出来。我的做法是把模型拆成两部分:ONNX里只包含backbone+neck+三个检测头的卷积层,后处理(decode+NMS)在板端用C++写。这样ONNX干净很多,编译也更快。
一个小技巧:导出前用torch.jit.trace先trace一遍,可以暴露一些隐藏的控制流问题:
```python
model_jit = torch.jit.trace(model, dummy)
torch.onnx.export(model_jit, dummy, 'yolov5m.onnx', ...)
```
三、量化校准:数据量直接决定精度生死
量化校准是我掉坑最深的地方。一开始随手从训练集抽了200张图,mAP从82.5%掉到71.3%。当时差点怀疑人生,以为是模型不适合量化。
后来逐步排查发现三个问题:
问题1:数据量不够
200张根本不够覆盖数据分布。我试了500张、1000张、1500张、2000张,精度随数据量变化如下:
校准数据量 mAP 相比浮点损失
200张 71.3% -11.2%
500张 76.8% -5.7%
1000张 79.4% -3.1%
1500张 80.9% -1.6%
2000张 81.0% -1.5%
1500张之后收益递减,我最后定了1500张作为标准流程。
问题2:预处理不一致
校准代码里resize用的是INTER_LINEAR,训练代码用的是INTER_CUBIC。就这一个差异,第一层输出的cosine similarity只有0.97。用hb_model_verifier逐层对比才发现问题。
修正后的校准预处理:
```python
def preprocess(img_path, size=640):
img = cv2.imread(img_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (size, size), interpolation=cv2.INTER_CUBIC) # 必须一致
img = img.astype(np.float32) / 255.0
img = (img - np.array([0.485, 0.456, 0.406])) / np.array([0.229, 0.224, 0.225])
return np.transpose(img, (2, 0, 1)).astype(np.float32)
```
问题3:用了训练集做校准
训练集经过数据增强(随机裁剪、颜色抖动、mosaic等),分布和真实场景有偏差。校准数据应该来自验证集,或者专门收集的校准集,不能直接从训练集抽。
我重新用验证集的1500张图做校准,mAP从76.8%回升到了80.9%。
四、混合精度策略:小目标检测头的抢救
默认int8量化对小目标检测头(P3,负责80x80特征图)伤害很大。我查了不少资料,发现可以用mix策略,对敏感层手动提升精度。
我的quant_config.yaml:
```yaml
calibration_parameters:
cal_data_dir: ./calibration_data
calibration_type: mix
layer_config:
# P3检测头:小目标最敏感,升int16
- layer_name: "model\\.24\\.m\\.0"
precision_mode: int16
- layer_name: "model\\.24\\.m\\.1"
precision_mode: int16
# P4检测头:中等目标,int8够用
- layer_name: "model\\.24\\.m\\.2"
precision_mode: int8
# P5检测头:大目标,int8够用
- layer_name: "model\\.24\\.m\\.3"
precision_mode: int8
```
效果对比:
策略 mAP 延迟 帧率
全int8 76.5% 19ms 52.6fps
mix(P3升int16) 81.2% 22ms 45.5fps
mix(P3+P4升int16) 82.1% 26ms 38.5fps
产品要求mAP≥80%,帧率≥30fps,所以选mix(P3升int16),81.2% mAP + 45.5fps,两个指标都满足。
顺便提一嘴,P3升int16之后延迟只增加了3ms,但精度回升了4.7个百分点,这个trade-off非常划算。如果P4也升int16,精度再涨0.9%,但延迟涨4ms,性价比就不高了。
五、编译优化:O3和双核必须用
compile_config.yaml:
```yaml
model_parameters:
onnx_model: ./quantized/yolov5m_pre.onnx
march: nash-e
output_model: ./yolov5m.hbm
input_parameters:
input_name: images
input_type_rt: rgb
input_layout_rt: NCHW
norm_type: data_mean_and_scale
mean_value: 123.675 116.28 103.53
scale_value: 0.0171247 0.017507 0.017429
calibration_parameters:
cal_data_dir: ./calibration_data
calibration_type: mix
compiler_parameters:
optimize_level: O3
compile_mode: latency
core_num: 2 # 必须写!不写默认单核,性能腰斩
```
core_num: 2这个参数我第一次漏掉了,测出来帧率只有28fps,排查了半天才发现只用了单核BPU。加上之后帧率直接翻倍到45fps。
O3优化比O2在activation复用和算子融合上好很多。我对比过:
优化级别 延迟 内存峰值
O2 26ms 156MB
O3 22ms 128MB
O3不仅延迟低了4ms,内存还省了28MB,双赢。
六、板端验证:结果对不上怎么排查
hbm放到板端跑,最容易出的问题是推理结果和PyTorch不一致。我的情况是检测框坐标前四位一样,但最后两位差0.3-0.5。
排查三板斧:
第一斧:预处理逐像素对比
板端C++代码的resize插值、normalize顺序、padding方式必须和Python校准代码完全一致。我用hb_model_verifier逐层对比,发现板端代码resize之后少了一次astype(float32),导致后续除法精度丢失。
第二斧:后处理阈值对齐
NMS的IoU阈值、置信度阈值必须和训练评估时完全一致。我板端默认NMS阈值设成0.5,训练评估用的是0.45,这个差异导致检测结果数量不一样。
第三斧:hb_evaluator跑精度
```bash
hb_evaluator --model yolov5m.hbm \
--input-dir ./val_images \
--annotation ./annotations.json \
--output-result ./eval_result.json
```
输出每个类别的mAP,帮助定位是整体精度不行还是某个特定类别不行。
七、性能压榨:从26ms到22ms
用hb_perf分析性能瓶颈:
```bash
hb_perf --model yolov5m.hbm --output perf_report.html
```
发现两个主要瓶颈:
1. Focus层fallback到CPU:YOLOv5的Focus层底层是slice+concat,BPU不支持。改成stride=2的6x6卷积后,这部分从CPU挪到BPU,省了7ms。
2. 内存带宽瓶颈:中间层activation占用大量DDR带宽。O3的activation复用优化省了4ms。
最终优化结果:
指标 优化前 优化后
延迟 30ms 22ms
帧率 33fps 45.5fps
mAP 76.5% 81.2%
内存峰值 156MB 128MB
八、注意事项总结
1. 版本对齐:Docker工具链版本和板端OE包版本必须一致,差0.1都可能出问题。
2. march参数:征程6用nash-e,不是bernoulli2,写错板端直接segfault。
3. 校准数据量:至少1500张,用验证集不用训练集,覆盖所有场景。
4. 预处理一致性:校准、训练、板端C++三者的resize插值、normalize顺序必须完全一致。
5. 混合精度:小目标检测头对量化敏感,P3层升int16,精度回升明显。
6. 双核编译:core_num=2必须显式指定,否则性能腰斩。
7. 动态shape:ONNX导出时固定所有输入尺寸,后处理拆出来不放进ONNX。
8. Focus层改造:用stride=2的6x6卷积替代Focus,避免CPU fallback。
9. 性能测试看P99:不要只看平均延迟,P99延迟才是产品稳定性的关键。
10. 板端内存对齐:申请tensor内存用posix_memalign,16字节对齐,否则DMA传输可能出错。
