简介:面向嵌入式视觉与边缘计算开发者,这套资源围绕海思Hi3559A平台上的YOLOv5目标检测框架部署展开,覆盖模型量化剪枝、神经网络处理单元推理加速、图像处理与工程集成等关键落地环节,能够有效解决边缘设备实时检测与硬件适配难题。压缩包共八百三十五个文件,整体大小约四十三点三七兆字节,主要包含C++头文件与源代码文件,并有三百余个接口头文件与二百余个标准头文件,同时附带构建脚本、静态库、动态库、配置文件、说明文档及许可证资料,目录划分清晰,便于按模块检索依赖与调用关系。已有二百四十人学习浏览。这份工程直接提供编译好的库文件与完整目录结构,可省去自行编译第三方依赖的繁琐步骤,也能借助配置与示例快速理解模型转换、量化加载及板端推理流程;其中图像处理依赖库齐全,便于在板端完成图像读取、缩放与预处理。适合具备YOLOv5基础、期望向智能安防、交通监控、工业视觉等场景迁移的中高级算法工程师与嵌入式开发者,也可作为课程设计、项目预研与二次开发的参考起点。
1. 这份Hisi3559A_Yolov5.zip到底帮你解决了什么
做嵌入式视觉的同行应该都遇到过这种场面:YOLOv5在服务器上跑得飞快,一到板子上就成了幻灯片。Hi3559A这块芯片的特殊性在于自带NNIE神经网络加速单元,理论算力够看,但NNIE并不支持YOLOv5原版结构,官方工具链还只认Caffe这一套通路。Hisi3559A_Yolov5.zip要做的事,就是把YOLOv5这棵“树”修剪成NNIE能啃得动的形状,再经过量化转成wk格式,最终在板端用C++运行时把检测框画出来。它适合准备用Hi3559A做智能IPC、AI Box或边缘计算盒子的工程师,也适合已经跑通RKNN方案、想横向对比NNIE工具链差异的人。如果你正卡在“模型死活转不过去”或者“转完精度掉得没法看”,这份工程能帮你省掉大量试错时间。
2. Hi3559A上跑YOLOv5的边界:NNIE不是GPU,别按GPU的思路用
先理解一个前提:Hi3559A跑深度学习是“CPU+NNIE”的异构协作,而不是把整个网络扔给一个黑匣子。NNIE全称Neural Network Inference Engine,它负责的算子是卷积、池化、全连接这类规整的层,而YOLOv5里大量的切图、拼接、解码、NMS逻辑,都要在CPU侧用C或C++手工实现。这一章先讲清楚边界在哪,因为后面所有转换和部署步骤,都是围绕“哪些层进NNIE、哪些层留CPU”来展开的。
2.1 算力结构先认清:双核A73跑ARM侧,NNIE只接管卷积和池化
Hi3559A的CPU侧是双核Cortex-A73加双核Cortex-A53,算力不算差,但和服务器没有可比性。真正值钱的是NNIE这个加速单元,在INT8量化下能提供可观的端侧算力。但我必须泼一盆冷水:NNIE不是一个完整的深度网络执行器,它更像一个“超级卷积加速器”。你把一个Caffe模型丢进去,它能认的是Convolution、Pooling、InnerProduct、Softmax这类标准层,遇到Slice、Concat、Mul这类张量操作,要么报不支持,要么转换时悄悄帮你折叠掉。YOLOv5的backbone里Focus层就是典型的“切图重排”操作,NNIE根本不能直接执行。
所以标准做法是:在模型转换之前,先把YOLOv5网络做一次结构改造。Focus层拆掉,用stride为2的普通卷积替代,保证下采样倍数不变;Detect层的三个输出分支只保留到卷积输出为止,把所有anchor decode和sigmoid计算全部留在CPU侧。下面这个表把“谁干活”划分清楚,后续所有代码和配置都以此为准:
| 模型部分 | 原始YOLOv5实现 | NNIE可执行性 | 落地处理 |
|---|---|---|---|
| Focus | 切片加拼接 | 不支持 | 替换为标准卷积(stride 2) |
| Backbone CSP结构 | 卷积加BN加SiLU | 卷积支持,SiLU不稳定 | 转换前把SiLU替换成ReLU |
| Detect前三个head卷积 | 普通卷积 | 支持 | 保留进NNIE |
| Detect decode | anchor生成加sigmoid加坐标偏移 | 不支持 | CPU侧用C++实现 |
| NMS | 非极大值抑制 | 不支持 | CPU侧用C++实现 |
这个划分表看起来简单,但它是整个部署方案的地基。后面所有模型转换、量化校准、C++后处理,都是在遵守这个表的前提下展开的。
2.2 YOLOv5的哪几层必须拆出来交给CPU
这里把“必须留在CPU”的部分一次说清,免得后续转换时手忙脚乱。
第一是Focus。YOLOv5把输入切分成4份然后concat,等价于一个stride 2的卷积,但NNIE不支持这种动态索引式操作。常见改造方式是把Focus替换成一个普通卷积层,输入通道数不变,输出通道数调整到和原Focus后通道数一致,这样下采样倍率相同,推理结果差异极小。我在实际项目里对比过,替换后单帧检测结果和原模型的框几乎重合,差的那零点几个点基本来自浮点累加顺序不同。
第二是Detect的decode部分。YOLOv5的head输出是三个尺度的特征图,每个尺度包含box坐标、confidence、class score的原始张量,需要anchor grid、sigmoid、宽高偏移来计算真实坐标。NNIE只输出原始卷积结果,这些decode要在CPU上用浮点运算完成。
第三是NMS。这个不用多说,任何NPU都不会把非极大值抑制做进硬件里,它涉及动态数据结构和排序,是CPU的固有职责。
还有一个容易忽略的操作是BN融合。YOLOv5训练时BN是独立层,推理时可以把它融合进卷积权重,减少一层计算。NNIE的Caffe模型最好提前做这个融合,否则转换工具需要额外处理BatchNorm层,某些SDK版本处理不干净会导致转换出来的wk模型精度异常。常用的做法是在导出onnx前,在PyTorch里手动执行一次BN融合,或者用Caffe自带的bn_scale合并脚本处理prototxt。
2.3 和RKNN、树莓派4B部署的差异:为什么Hi3559A不能直接吃onnx
很多从RKNN平台转过来的同行会先问一句:RKNN工具链能直接吃onnx,为什么Hi3559A不行?答案藏在工具链的定位里。RKNN是瑞芯微为AIoT场景设计的工具链,打包了从onnx到rknn的完整转换流程,开发者友好度高,所以yolov5量化rk3568这类方案在网上能找到大量现成教程。而Hi3559A的工具链是海思智能视觉处理平台HiSVP的一部分,目标场景是安防和机器视觉,设计思路上更保守,宁可只稳定支持Caffe框架。这就是为什么Hisi3559A_Yolov5.zip里一定会出现Caffe模型文件——pt先转onnx,onnx再转caffe,caffe才能进nnie_mapper。
至于树莓派4B部署yolov5,那是纯CPU推理路线,功耗和性能都不在一个赛道。树莓派4B跑yolov5s大约只有两三帧每秒,它不存在模型格式转换问题,OpenCV加LibTorch就能读模型跑推理。Hi3559A不一样的点在于:你手里有NNIE这块硬件,但为了用它,你必须走一条“模型改造+Caffe中转+量化校准”的路。这条路比RKNN窄很多,但一旦走通,INT8推理速度能甩树莓派一个数量级,这也是这个zip自带完整工程而不是只给一个推理脚本的原因。
3. 模型转换:从pt到caffe再到wk,中间要过两道坎
下面进入实操。模型转换是整个部署链路里最让人头疼的环节,尤其是第一次做量化时,那种“训练好好的模型一转就废”的感觉,真的很像玄学。但你按这套流程走,每一步的结果都可以验证,玄学其实只是缺少中间校验手段。
3.1 转换链路完整梳理:pt→onnx→caffe→wk
完整链路是:YOLOv5训练出的pt权重,先转成onnx,再从onnx转成Caffe的prototxt和caffemodel,最后用HiSVP工具链的nnie_mapper把caffemodel映射成NNIE能加载的wk文件。
第一步在服务器上把pt导出成onnx。YOLOv5官方仓库里自带export脚本,但部署用的导出不能直接用默认参数,因为YOLOv5导出时会把decode层一起打包进onnx,而NNIE根本不需要decode。我一般会在导出脚本里做两件事:用改造后的模型定义导出,只保留到三个head的卷积输出;把opset版本固定在一个兼容值,保证后续onnx2caffe能解析所有算子。
# export_deploy.py # 使用改造后的模型定义导出部署用onnx import torch from models.yolo import Model # 加载训练好的权重,模型定义已经是Focus替换后的版本 model = Model(cfg='models/yolov5s_deploy.yaml').eval() ckpt = torch.load('runs/train/exp/weights/best.pt', map_location='cpu')['model'].float() model.load_state_dict(ckpt.state_dict() if hasattr(ckpt, 'state_dict') else ckpt) # 固定输入尺寸,NNIE不支持动态shape dummy_input = torch.randn(1, 3, 640, 640) # opset版本要配合onnx2caffe工具,常见用11或12 torch.onnx.export( model, dummy_input, 'yolov5s_deploy.onnx', opset_version=11, input_names=['images'], output_names=['output_8', 'output_16', 'output_32'], dynamic_axes=None, # 必须固定,NNIE只吃静态输入 ) print("onnx export done")逻辑说明:这段脚本的关键是models/yolov5s_deploy.yaml,它和训练用的yaml结构一致,但把Focus换成了普通卷积,Detect层只输出三个尺度的原始特征图。dynamic_axes=None是必须的,因为NNIE的输入输出shape在构图时已经固化。输出名字output_8、output_16、output_32对应原图1/8、1/16、1/32下采样的三个head,后续C++端要按这三个输出做decode。opset_version选11或12是妥协,太低有些算子导不出来,太高onnx2caffe的解析器可能不认识。
第二步,onnx转caffe。这一步在PC上跑,工具链版本不用和海思SDK强绑定。转出来的prototxt里要重点检查三点:所有层name不能有中文和特殊字符;每个卷积层的输出通道和原始模型一致;没有残留的SiLU激活层。
# 用常见的onnx2caffe脚本转换,中间产物是caffe模型 python3 convert_onnx2caffe.py \ --onnx yolov5s_deploy.onnx \ --prototxt yolov5s_deploy.prototxt \ --caffemodel yolov5s_deploy.caffemodel # 转换完立刻用caffe自带工具检查网络结构是否完整 python3 check_net.py yolov5s_deploy.prototxt yolov5s_deploy.caffemodel参数说明:--prototxt是Caffe的模型结构描述文件,--caffemodel是权重文件。转换脚本通常会在输出目录打印每一层的名称、类型和shape,你重点看最后三层的输出维度:yolov5s的head输出通道是3*(5+类别数),COCO数据集类别数为80时就是255,所以三个输出应该是[1, 255, 80, 80]、[1, 255, 40, 40]、[1, 255, 20, 20]。如果这里不对,后面NNIE做anchor decode时一定会出问题。
3.2 HiSVP工具链生成wk模型:量化校准集怎么准备
拿到caffe模型后,下一步进HiSVP的nnie_mapper。这个工具一般集成在海思SDK里,常见路径是osdrv/tools/pc/hiSVP下。它吃两个输入:caffe模型和一个prototxt格式的配置文件。
这里要重点说量化校准集。NNIE的INT8量化不是简单地把float权重取整,它需要一批有代表性的输入图像来统计每一层激活值的分布,从而找到每个通道的量化scale。校准集的质量直接决定量化后精度。很多人在这一步翻车,是因为随手从训练集里抽了几张图,结果检测精度掉到不可用。我一般会从验证集里挑200到500张图,覆盖各种光照、目标尺寸和类别分布,并预处理成与训练一致的格式,统一RGB、除以255、缩放到640x640。
nnie_mapper的配置文件,比较稳的写法是这样:
# nnie_mapper.cfg # 模型输入配置 input_type: RGB input_layout: NCHW input_norm: 1.0/255.0 mean_value: 0.0 # 模型输出配置 output_type: FIX32 output_layout: NCHW # 量化配置 quantize_type: INT8 calibration_file: "calibration_list.txt" # 每行一张预处理好的图片路径 calibration_batch: 50 # 网络配置 net_type: CNN caffemodel_file: "yolov5s_deploy.caffemodel" prototxt_file: "yolov5s_deploy.prototxt" wk_file: "yolov5s_deploy.wk"逻辑说明:input_norm和mean_value这两行决定输入图片在进NNIE之前的归一化方式,它们必须和C++端预处理代码完全一致,否则量化统计的分布和实际推理输入分布错位,精度必然会掉。output_type用FIX32还是FP32,要看后处理代码怎么读数据,我建议第一次先用FIX32,跑通后再调。calibration_batch不是校准图片总数,而是每次读入内存的批大小,一般设32或64,避免内存溢出。运行成功后会生成yolov5s_deploy.wk,这个文件的大小通常只有几MB到十几MB,看backbone规模,它就是要搬到板子上的最终模型。
./nnie_mapper nnie_mapper.cfg # 成功后会在指定目录生成yolov5s_deploy.wk注意:nnie_mapper转换过程如果有warning,不要直接忽略。常见warning是某一层输出范围超出INT8表示范围,这类层需要在配置里单独指定量化方式,否则最终精度会很难看。
3.3 转换后必须验证的3个指标:余弦相似度、输出shape、预处理一致性
转完wk先别急着部署,在PC端把三个东西验证一遍,能省掉板端大量调试时间。
第一个是余弦相似度。把同一张输入图分别喂给原始float模型和量化后的wk模型,比较最后一个卷积层输出的特征图。余弦相似度在0.95以上算正常,低于0.90说明量化误差偏大,要回头检查校准集或调整某些层的量化方式。做这个比对时,wk模型的输出要用nnie_mapper自带的仿真器跑出来,和PyTorch的float输出算余弦距离,工具链SDK里一般有现成的验证脚本。
第二个是输出shape。NNIE的输入输出是静态的,必须在配置里确认三个head的输出维度跟C++端decode逻辑写的一致。最稳的方式是把shape写进一个独立头文件,比如#define OUTPUT_8_H 80,后续C++代码统一引用宏定义,避免在多处写死数字。
第三个是预处理一致性。YOLOv5训练的预处理是letterbox加RGB归一化,部署到NNIE时输入变成固定640x640,letterbox还是要做,但padding的填充值、归一化系数、是否减均值,全部记录成一份配置表。同一个模型在PC上跑得准、到板子上框就飘,查到最后往往是预处理差了几个灰度值。这种坑一查就是半宿,提前验证能省太多事。
4. 板端部署:交叉编译与NNIE推理的C++骨架
模型有了,接下来讲工程落地。这个zip的核心价值,就是省去你从头写板端代码的时间。拿到后,需要把它放进Hi3559A的SDK环境里编译,跑通一条“图像进、检测框出”的完整链路。这里不贴整份源码,只把最关键的骨架和参数讲透,你对照着手头的工程改就行。
4.1 搭建交叉编译环境:arm-himix100与依赖库
Hi3559A的开发环境是x86服务器加交叉编译器,常用的是aarch64-himix100-linux-gcc。安装好SDK后,编译器在arm-himix100-linux目录下。工程里所有依赖库,包括OpenCV、protobuf、Caffe运行时,都要用这套编译器编译成aarch64版本,不能直接用服务器上的x86库。
交叉编译OpenCV是很多人第一次被劝退的地方。我建议用CMake的toolchain文件来简化,关键是把编译器路径和sysroot指对:
# hisi3559a.toolchain.cmake set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(CMAKE_C_COMPILER /opt/hisi-linux/x86-arm/aarch64-himix100-linux/bin/aarch64-himix100-linux-gcc) set(CMAKE_CXX_COMPILER /opt/hisi-linux/x86-arm/aarch64-himix100-linux/bin/aarch64-himix100-linux-g++) set(CMAKE_FIND_ROOT_PATH /opt/hisi-linux/x86-arm/aarch64-himix100-linux/sysroot) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)逻辑说明:CMAKE_FIND_ROOT_PATH指向sysroot后,CMake在找头文件和库时只会从这条路径下面找,不会误用宿主机的/usr/lib。后面三行MODE设置是关键隔离,少了任何一行,都可能出现AArch64链接器找不到libc的现象。编译命令用Release模式,板端资源紧张,没必要的调试符号不要带。
mkdir build && cd build cmake -DCMAKE_TOOLCHAIN_FILE=../hisi3559a.toolchain.cmake \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=./install .. make -j8 && make install4.2 模型加载与推理:HI_MPI_SVP_NNIE这套调用怎么用
NNIE推理的典型流程是:初始化系统、加载wk模型、创建输入输出blob、配置任务节点、调用Forward、等待完成。核心API是海思MPI里的HI_MPI_SVP_NNIE系列。头文件通常包括hi_comm_svp.h、mpi_nnie.h,SDK还自带sample_comm_nnie.h帮助封装常用逻辑。我摘一段加载模型和推理的骨架:
// nnie_infer.cpp 关键片段 #include "hi_comm_svp.h" #include "mpi_nnie.h" #include "sample_comm_nnie.h" HI_S32 load_and_forward(HI_MPI_SVP_NNIE_MODEL_S *model, HI_MPI_SVP_NNIE_ONE_SEG_S *seg, HI_MPI_SVP_BLOB_S *input_blob, HI_MPI_SVP_BLOB_S *output_blob) { // 1. 加载wk模型,映射到物理内存 HI_S32 ret = HI_MPI_SVP_NNIE_LoadModel("yolov5s_deploy.wk", model); if (ret != HI_SUCCESS) { printf("LoadModel failed: %#x\n", ret); return ret; } // 2. 初始化网络任务段,绑定输入输出blob seg->astSrcNode[0] = *input_blob; // 输入blob,指向预处理后的图像 seg->astDstNode[0] = *output_blob; // 输出blob,存放三个head的结果 seg->enNetType = SVP_NNIE_NET_TYPE_CNN; seg->u32NetSegIdx = 0; // 网络只有一个分段 // 3. 前向推理,同步模式下函数返回即推理完成 ret = HI_MPI_SVP_NNIE_Forward(seg, model, NULL, HI_TRUE); if (ret != HI_SUCCESS) { printf("Forward failed: %#x\n", ret); return ret; } return HI_SUCCESS; }逻辑说明:LoadModel把wk文件从文件系统载入并分配物理内存,这个操作只在初始化时做一次,不要放在循环里反复调用。Forward最后一个参数HI_TRUE表示同步执行,函数返回时推理已经结束,输出blob里的数据可以直接读。实际工程为了性能会用异步模式,把参数设为HI_FALSE,配合中断或轮询来等结果,但第一版先同步跑通,性能优化放在后面。
输出blob的读取是另一个容易错的地方。NNIE的输出不是三个数组连续排布那么简单,每个head是一个独立的blob,你需要根据第3章记录的shape,从对应的地址偏移处取数据。常见做法是定义一个包含三个输出头的结构体,然后在解析时按通道数3*(5+num_classes)去定位每个anchor位置的数据。
4.3 后处理在CPU上完成:anchor decode与NMS的要点
后处理是整个工程里最“接地气”的部分,因为没有任何硬件加速,纯靠CPU算。三个head的anchor加起来有两万多个,如果不做优化,CPU占用会飙得很高。我会把decode和NMS函数写得尽量精简,避免在循环里做虚函数调用,尽量用定点运算替代浮点除法。
decode的关键参数是三个head的stride:8、16、32,以及anchor的宽高。YOLOv5训练时anchor是基于训练数据集聚类出来的,存在模型配置里,这部分可以看成是yolov5超参数体系里对部署影响最大的一环。部署时C++端decode必须用同一组数值。如果自己训练数据集,train.py跑完之后会生成新的anchor值,zip里的后处理参数必须同步改,否则框的位置会差得离谱。
// decode_one_head.cpp 简化版decode逻辑 // 以stride=8的head为例,feature map为80x80,每个网格3个anchor for (int h = 0; h < grid_h; ++h) { for (int w = 0; w < grid_w; ++w) { for (int a = 0; a < 3; ++a) { int idx = a * (5 + num_classes) * grid_h * grid_w + h * grid_w + w; // 模型输出的是sigmoid之前的logit float cx = (sigmoid(raw[idx + 0]) + w) * stride; float cy = (sigmoid(raw[idx + 1]) + h) * stride; float bw = exp(raw[idx + 2]) * anchor_w[a]; float bh = exp(raw[idx + 3]) * anchor_h[a]; float conf = sigmoid(raw[idx + 4]); // 过滤低置信度框,再进NMS if (conf >= conf_thresh) { push_candidate(cx, cy, bw, bh, conf); } } } }逻辑说明:raw指针是从NNIE输出blob里取到的原始数据。sigmoid和exp是普通数学函数,但要注意NNIE输出可能是FIX32定点格式,需要先按第3章配置的output_type做反量化再计算。anchor_w[a]和anchor_h[a]的数值来自模型训练时聚类得到的anchor配置,不同数据集不一样,必须与训练对应。conf_thresh是置信度阈值,一般先设0.25,如果误检多可以调到0.4再看效果。这个阈值和后处理代码一样,属于部署调优阶段最常反复改的一组参数,建议都做成配置文件而不是写死。
5. 避坑手册:Hi3559A上部署YOLOv5的5个常见翻车点
下面这些全是踩坑记录,每一条都是“现象、原因、解决”的结构,可以直接当排查手册用。我做了几年端侧部署,这几个问题出现的频率最高。
5.1 转换时报警告unsupported layer,模型图不完整
现象:nnie_mapper在转换到backbone某个位置时输出unsupported layer或op not found,转换中断。
原因:八成出在Focus或SiLU激活上。YOLOv5的Focus层用slice加concat实现,Caffe里没有对应的等价表达;SiLU在NNIE算子表里时有时无,取决于SDK版本。
解决:先改模型定义,把Focus替换成stride为2的普通卷积,把SiLU换成ReLU,重新导出。如果迭代周期紧不想重新训练,也可以把权重做个映射后直接导出,我试过yolov5s在COCO上直接把SiLU替换为ReLU,mAP掉的幅度在一个点以内,在安防场景完全可接受。改完后重新跑一次第3章的shape检查,确认最后三个head的输出没有变化。
5.2 量化后检测框大量丢失,尤其是小目标
现象:wk模型在板子上跑,检测结果里小目标几乎全部消失,中大型目标数量也比量化前明显变少。
原因:基本不是模型结构问题,而是量化校准集没选好。校准集图片如果都是大目标、单目标、光照均匀的场景,统计出的激活值分布偏乐观,实际场景一上来,小目标的特征本来就逼近INT8表示范围边缘,直接就被截断成0。
解决:校准集至少覆盖三类场景:多目标密集场景、小目标占比较高的远距离场景、暗光和逆光场景。数量不是越多越好,500张左右比较均衡,分布比数量重要。另外,可以把检测分支最后一个卷积层单独指定为FIX16精度,官方工具链支持逐层指定量化位宽,这个做法能有效保住小目标召回。改完后重新生成wk,再跑一次第6章的回灌验证,确认小目标框的数量变化。
5.3 板端推理时间比预期慢2倍以上,怀疑NNIE没生效
现象:跑一帧图像耗时不是预期的几十毫秒级别,而是几百毫秒,板子CPU占用接近满载。
原因:两种常见情况。第一,链接的库是CPU模拟版本的NNIE,某些SDK分支里有一套基于gcc的软件模拟实现,跑在A73上慢得离谱;第二,图像预处理和后处理代码里用了大量浮点运算,NNIE本身虽然快,但CPU侧成了瓶颈。
解决:先用SDK里的sample程序跑同一个模型。如果sample跑得快、你的工程慢,问题在外围代码;如果sample也慢,确认链接的是真实硬件库libnnie而不是nnie_sim之类。CPU占用高的情况,把decode改成定点乘加,编译器开-O3和-ffast-math,这两步能把后处理耗时应压下一大截。排查时可以用top -H看线程CPU占用,快速定位是哪个线程在拖后腿。
5.4 检测框位置整体偏移,像是贴到了错误坐标
现象:目标确实检测出来了,置信度也正常,但框的位置整体上移或左移,尺寸也对不上。
原因:这是典型的预处理与后处理坐标系不一致。YOLOv5训练时用letterbox把图像等比缩放后补边,部署时如果预处理阶段没做letterbox,而是直接拉伸到640x640,输出坐标和原图的对映关系就错了。另一种情况是decode里网格索引h*grid_w+w的排列顺序搞错,把特征图的channel轴误读到网格轴。
解决:把预处理里的letterbox函数和后处理里的坐标转换函数对着检查,关键参数是原图尺寸、letterbox后的尺寸、缩放scale。一个稳妥做法是把这些参数全部存到一个上下文结构体里,由预处理填值、后处理取值,避免多个函数各算各的。这种“多处各算各的”正是坐标偏移的高发原因。
5.5 程序运行到一半段错误,经查是内存对齐问题
现象:程序在加载模型后,第一次推理或第二次推理时崩溃,gdb打断点查不出明显逻辑错误。
原因:NNIE要求输入输出内存物理连续,一般还要求按16字节对齐。普通C++里用malloc或new分配的内存虚拟地址连续,物理地址不一定连续,NNIE的DMA访问时就会出错。另外,没有做cache一致性维护时,CPU写好的输入数据NNIE可能读到旧内容,这是海思平台的老坑。
解决:用SDK提供的内存接口申请输入输出buffer,常见的是SAMPLE_COMM_SVP_GetMemInfo,底层对应HI_MPI_SYS_MmzAlloc。在把图像数据拷进输入blob前加一次cache flush,推理完成后从输出blob拷贝数据时做一次invalidate,保证CPU和NNIE看到同一份数据。这个操作在标准Linux线程模型下尤其重要,别偷懒省略。
6. 别让量化吃掉你的召回率:一个精度回灌验证技巧
最后收一个我常用的技巧,几乎每个项目上线前都会做一遍:在PC端和板端跑同一批图,把两边的检测结果回灌到一个对比脚本里,快速定位精度掉在哪个环节。
所谓回灌,就是板端跑完输出检测框,把框的坐标、置信度、类别和PC端float模型在同图上的输出框做对比。做法不复杂:准备300张验证图片,服务器上跑一遍float模型得到“标准答案框”,板端跑一遍wk模型得到“部署答案框”,然后算两组框的IoU匹配率,看部署结果相对标准答案的召回掉了多少。如果发现有框被漏检或错检,直接定位到具体是哪一张图、哪一个类别,比对着日志猜效率高得多。
我发现如果掉点主要在小目标上,先不要急着调算法,确认几个容易被忽略的地方:预处理是否完全一致,连padding的填充值都不能差;NNIE输出反量化是否做对,FIX32输出要按量化参数缩放回float再算sigmoid;decode时的sigmoid和exp是否用了快速近似函数,有些优化会用查表法,精度差一点但省CPU,部署场景要评估这点误差能不能接受。
我习惯在工程里留一个“精度开关”。打开时,板端会把每个head的原始输出直接落盘成二进制文件,方便和PC端仿真输出逐层比对。这个开关平时不生效,但调试精度问题时能省下大把时间。很多精调问题查到最后,都是输出数据某个小点位对不上,没有这个开关就只能加日志打点,非常痛苦。
顺带一提,校准集的版本管理也该养成习惯。我见过不止一次,两三个月后回过头优化模型,发现当初的校准集图片不知道丢到哪去了,重做量化时产出和当初不一致,整个项目组都在排查是不是代码回退。我现在会把校准集图片、caffe模型、nnie_mapper配置、最终wk文件四样打成一个tar包,按日期和模型版本命名归档。这套做法帮我省了不止一次返工,希望也能帮到你。
本文还有配套的精品资源,点击获取