博客算法工具链征程6上多任务模型打包:把检测+分割+深度塞进一个hbm的实战经验

征程6上多任务模型打包:把检测+分割+深度塞进一个hbm的实战经验

默认265282026-08-30
34
0

接手一个室内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传输可能出错。

算法工具链
社区征文征程6
评论0
0/600