0. 前言
在进行嵌入式AI算法迁移时,我们经常会遇到"PC端推理正常,板端结果不对"的情况。最近在 Hi3519DV500 平台上部署一个定制化的 3 类别 YOLOv5 模型时,遇到了一个极其隐蔽的坑:模型能正常检出第1类(Person)和第2类(Face),但第3类(Hand)目标完全消失。
经过深度的白盒分析,最终定位到这竟然是一个由 Memory Stride( 内存 步长) 引起的"血案"。
1. 现象描述
-
模型配置:YOLOv5n,3个类别(0:Person, 1:Face, 2:Hand)。
-
硬件方案 :开启 RPN 硬化加速(Dual-Input 模式),第二输入为
rpn_paras(用于动态调整阈值)。 -
异常表现:
-
单输入模式(走 CPU 后处理)下,三个类别全部正常。
-
双输入(硬件 RPN)模式下,无论如何调整手部阈值,手部目标检出数为 0。
-
通过打印发现,画面中明明有手,但 RPN 算子的输出 Tensor 里,手部类别的计数始终为 0。
-
2. 白盒追踪:日志里的蛛丝马迹
查看板端运行模型时的元数据信息,发现如下关键属性:
[rpn_data] index:2, shape:[4 3], stride:16, bufferSize:64
深度解读:
-
Logical Shape [4, 3]:逻辑上是 4 行(Score阈值、NMS阈值、最小高度、最小宽度)和 3 列(对应 3 个类别)。总共 4 × 3 = 12 个 float 参数。
-
Physical Stride: 16 :这是硬件层面的强制要求。它意味着每一行数据在 物理内存 中必须占据 16 字节(即 4 个 float 的空间)。
3. 核心矛盾:逻辑形状 vs 物理布局
为什么官方 Sample 的 80 个类(COCO)没出问题,而我们的 3 个类出事了?
80 类的"巧合":
-
80 个 float = 320 字节。
-
320 \ 16 = 20(完美整除)。
-
因此,80 类的逻辑结尾就是物理步长的结尾,平铺数组(Flat Array)不会产生位移偏差。
3 类的"陷阱":
-
3 个 float = 12 字节。
-
12 \ 16 无法整除。
-
硬件为了效率,会在每一行的末尾强制预留 4 字节(1个 float 的位置)作为填充。
内存错位白盒图示:
如果你在 C++ 中直接传一个 float rpn[12] 的平铺数组,硬件读取时的视角是这样的:
|-----------|--------------|---------------------------|----------------------------|
| 行号 | 硬件想读的地址 | 实际读到的内容 | 结果 |
| Row 0 | ptr + 0 | rpn[0], rpn[1], rpn[2] | 正确(Score 读对了) |
| Row 1 | ptr + 16 | rpn[4], rpn[5], rpn[6] | 错位(NMS阈值读成了 Row 2 的数据) |
| Row 2 | ptr + 32 | rpn[8], rpn[9], rpn[10] | 错位(最小高度读成了 Row 3 的数据) |
| Row 3 | ptr + 48 | 内存 越界/垃圾值 | 毁灭(最小宽度读到了数组外的随机数) |
结论 :由于错位,第三类(Hand)的置信度阈值读成了第7位"1"上面去了,所有的手部框都被过滤掉。且"最小宽度"阈值读到了内存之外的随机大数(例如 32768.0),导致硬件算子认为所有的"手"都太小了,在 RPN 内部直接进行了硬截断过滤。
4. 解决方案:补齐 16 字节步长
解决办法很简单:将逻辑上的 [4, 3] 矩阵在 物理内存 中补齐为 [4, 4]。
C++ 修改示例:
cpp
// 以前的做法:直接传 12 个数 (错误) // float rpn_data[12] = { ... }; // 正确做法:考虑 Stride 16,按 4x4 布局传 16 个数 float rpn_aligned[16] = { 0.5f, 0.5f, 0.1f, 0.0f, // Row 0: Score Threshold (手部调低阈值) 0.35f, 0.35f, 0.35f, 0.0f, // Row 1: NMS Threshold 1.0f, 1.0f, 1.0f, 0.0f, // Row 2: Min Height 1.0f, 1.0f, 1.0f, 0.0f // Row 3: Min Width (补齐后,硬件读到正确的 1.0) }; // 拷贝大小改为 64 字节 (16 * 4) aclrtMemcpy(deviceAddr, 64, rpn_aligned, 64, ACL_MEMCPY_HOST_TO_DEVICE);
5. 经验总结
-
重视 Stride :在嵌入式开发中,永远不要假设内存是紧凑排列的。看到
stride属性一定要警觉。 -
打印 Output [0]:对于带 RPN 的模型,第一个输出通常是各类别计数。如果计数为 0,说明问题出在过滤阶段(阈值、对齐、TopK)。
-
白盒思维:遇到问题不要盲目调参,通过 Netron 观察结构、通过日志分析布局,才能找到最底层的真因。
技术交流关键词:海思 Hi3519, 昇腾 ATC, RPN硬化, 内存对齐, Stride.
本文为作者原创,转载请注明出处。