
系列第一篇写了怎么准备数据集和选型第二篇讲的是从Darknet到Caffe的转换流程这一篇是对NNIE平台部署yolov4-tiny的收尾小结。所谓小结不是“终于跑通了”的庆祝帖而是把整个过程中没写进前两篇的问题、取舍和补救办法集中交代一次。如果你手头也有一块嵌入式开发板准备把yolov4-tiny放上去做实时目标检测这篇内容基本覆盖了从模型适配到端侧推理的所有关键卡点。我会把能公开的细节、踩过的坑、还有那些文档里没写清楚但实际躲不开的东西尽量一次说透。1. 为什么是yolov4-tiny NNIE这个组合1.1 嵌入式平台上的目标检测需求这两年边缘端的目标检测需求越来越具体不是“能不能跑AI”的问题而是“在有限功耗、有限内存、固定帧率下能不能稳定跑检测”。x86服务器上随便调GPU模型精度高但功耗和体积摆在那很多场景根本上不了。我手里的板子算力、内存、带宽都属于“够用但必须精打细算”的档次所以模型选型时基本锁定轻量级检测网络。yolov4-tiny就是在这样的前提下进入视野的。它相比yolov4完整版参数量和计算量都小了一大截换来的精度损失在可接受范围内。尤其对安防、工业质检、无人机这类固定场景检测目标类别通常有限yolov4-tiny足够完成任务。再加上Darknet生态里有很多现成的预训练权重省去了从头训练的时间成本直接做迁移学习就能快速拿到可用的模型。当时也对比过yolov5s、移动端SSD之类的方案但综合部署难度、硬件加速适配、以及后续量化支持yolov4-tiny反而最省事。模型结构清晰主干部分基本是卷积、BN、LeakyReLU的组合没有特别怪异的算子。这在后面做NNIE离线转换时帮了大忙。1.2 NNIE能做什么、不能做什么NNIE全称是Neural Network Inference Engine可以理解成一块专门跑CNN计算的硬件加速器和GPU、NPU是一类东西但它更专一。它不支持通用计算也不跑训练只做前向推理。模型在PC端离线转换成一个带权重的指令文件运行时交给NNIE加载执行。NNIE的优点很突出功耗低、确定性高。模型一旦转换成功跑一次推理的时间和耗电基本是稳定的不会像CPU那样受系统负载影响。这对生产环境很重要。缺点也同样明显算子支持列表有限数据格式跟常见深度学习框架不一样量化精度控制需要手动调。所以NNIE部署的典型姿势是能用硬件加速器算的层交给NNIE算不了的层在CPU上补输出结果之后的后处理——比如sigmoid、解码、NMS——也基本是CPU活。这个分工思路要是一开始就明确后面不会绕太多弯路。2. 模型适配yolov4-tiny在NNIE上要过的第一关2.1 算子支持现状与冲突点NNIE的算子支持范围以CNN基础算子为主。常见的卷积、池化、全连接、softmax、concat、eltwise都在支持列表里激活函数支持ReLU、LeakyReLU、PReLU这些。看着覆盖面还行但实际拿yolov4-tiny一比对问题立刻冒出来。yolov4-tiny网络结构里有几个关键组成部分CSP块、shortcut残差连接、route分支、maxpool下采样、最近邻上采样、yolo输出层。前面几个都好说CSP块拆开就是卷积加route加concatNNIE能处理。shortcut和route在Caffe里用Eltwise和Concat表达也没问题。maxpool是NNIE的老朋友稳。真正的冲突在两点一个是Upsample层NNIE对最近邻上采样的支持不友好至少在我用的工具链版本里直接丢一个Upsample层进去要么报错要么效果不对另一个是Darknet的yolo层这个层本质上承担的是检测结果的解码和loss计算NNIE根本不会去算它转换时得想办法绕开。2.2 把Darknet网络改造成Caffe结构所以模型适配的第一步是把Darknet的yolov4-tiny转成Caffe的prototxt和caffemodel。这一步我前一篇已经详细写过这里只挑重点说。首先是结构翻译。Darknet里一个卷积块通常是Conv2d BN LeakyReLU翻译到Caffe里就是Convolution BatchNorm Scale ReLU其中ReLU要配negative_slope参数模拟LeakyReLU。这一步顺手把BN融合进卷积也做了。BN融合的意思是把BN层的缩放系数、平移系数、均值、方差全部折算到前一卷积层的权重和偏置里推理时就不用再单独算BN了。好处有两个减少计算量同时避免NNIE转换时某些版本对BN层处理不好导致精度异常。UpSample层的处理要灵活些。我们当时的做法是趁早把上采样挪出NNIE执行范围。也就是说转换后的Caffe模型只保留到两个特征图输出一个13x13一个26x26中间如果需要做特征融合就把上采样和concat操作移到端侧CPU上处理。代价是推理链路变成了“NNIE算卷积主干 → CPU做上采样 → concat → 再送NNIE或直接输出”。实际工程里更简单的做法是只取两个尺度的输出上采样在CPU端做完解码也在CPU端一次搞定链路短调试也方便。代价是硬件加速的优势少发挥了一截但对yolov4-tiny这种小模型来说CPU多干点活影响不大。yolo输出层则直接截断。NNIE不负责解码所以Caffe模型里不需要有Region或YOLO层只要让网络在两个特征图处结束就行。特征图的数据出来以后由我们自己写解码函数把tx、ty、tw、th映射成真实坐标框。2.3 融合和参数校验结构改造完之后一定要做一步参数校验。最简单的办法是把转换后的Caffe模型放到CPU上用Caffe或兼容框架跑一遍对比Darknet原模型的输出特征图。如果两层之间的数值差距在可接受范围内说明权重和结构都翻译对了如果差距很大基本可以判定是某一层的参数顺序、维度或激活函数配错了。我这里遇到过两个典型错误。一是BN融合后卷积层的偏置忘记更新结果所有通道的输出整体偏移检测框全偏二是Caffe里卷积的权重排布是(out_ch, in_ch, kh, kw)而Darknet排布是(out_ch, kh, kw, in_ch)我一开始没做重排导致第一个卷积层出来就是一团噪声。这类错误用“逐层数值对比”的方式排查特别快每层输出都打印出来和Darknet对比定位到第一个不一致的层就行。3. 离线转换与量化.wk文件里藏着的那些门道3.1 RuyiStudio工程配置的关键项Caffe模型准备好之后就轮到官方工具链登场了。把模型转换成NNIE能运行的.wk文件主流程是在RuyiStudio里新建一个模型转换工程导入prototxt和caffemodel配置输入图像、均值、量化类型然后点击转换。听起来简单但配置项里有几个地方不认真填后面准出问题。第一个是输入图像格式。yolov4-tiny训练时用的是RGB顺序还是BGR顺序工具链默认按BGR处理还是按RGB处理这些得先搞清楚。我们训练的模型从数据加载到预处理都是BGR如果工具链里用默认RGB配置推理结果会直接错乱。这个错误在模拟器阶段不会报警但如果发现检测效果莫名其妙地差优先查这里。第二个是输入尺寸。yolov4-tiny常用输入是416x416但NNIE对feature map的空间尺寸有对齐要求一般宽高都要是16的整数倍。416本身满足但如果你为了提速想压到320或288也要确认对齐规则否则转换能过运行阶段数据布局会错位。第三个是均值减除和归一化方式。Darknet官方的预处理是除以255之后再做简单归一化。工具链里的mean和scale参数要严格对应训练时的预处理。我见过有人把mean填成0.5scale填成1/255结果检测精度掉了不少还找不到原因。最好自己写一个小脚本把同一张图分别按训练预处理和工具链预处理走一遍对比输入数组确认完全一致。3.2 量化校准怎么做才不掉点模型转换到NNIE的部署阶段几乎绕不开量化。NNIE支持INT8和INT16两种量化类型INT8快且省内存但精度损失明显INT16精度好一些但占用资源更大。yolov4-tiny这种小模型如果细节处理不好INT8量化后检测小目标的能力会损失得比较厉害。我采用的策略是先用INT16跑通全流程验证功能和精度没问题之后再换INT8优化性能。这样做的好处是能把“模型本身问题”和“量化精度问题”分开排查。如果INT16精度正常、INT8一塌糊涂那就只针对量化调优如果INT16本身就差那说明模型适配或预处理还是有bug先别急着赖量化。量化的时候还需要提供校准图片一般是几百张覆盖真实场景的图而不是随便拿训练集里抽几张。因为量化的本质是统计每一层的数值范围统计得准量化误差就小统计偏了激活值的分布没覆盖住后面层全都跟着遭殃。我们的做法是从实际部署现场抽了大约300张图涵盖不同光照、不同目标距离再混合少量训练集数据让分布更平滑。3.3 转换失败时的log排查思路用工具链转换模型报错是家常便饭。我建议拿到一个陌生的失败log先看算子支持错误再看shape对齐错误最后才看量化相关错误。算子不支持的错误通常会指名是哪一层哪个算子直接在prototxt里替换即可。shape错误多半是中间层输出的尺寸没有满足对齐要求常见于concat和eltwise合并了两个不同stride的特征图。还有一类坑比较隐蔽工具链的log里报“layer xxx unsupported”或者“parse error”但实际原因不是算子本身而是上一层的输出在一个奇怪的尺寸上把后续层的维度推导搞崩了。这时候把模型断点逐层往前推找到第一个输出尺寸不对的层基本就定位了。量化错误同样容易迷惑人表现为log显示转换成功校准过程也能跑完但.wk文件加载到板子上推理结果全乱。这种问题我只能说回到INT16重新转换如果结果正常那基本确定是量化参数配置不对或校准图分布太偏。4. 端侧部署从拿到输出特征图到画出检测框4.1 NNIE输出的数据布局和读取方式模型在端侧跑起来之后第一个要理解透彻的就是NNIE输出的数据布局。它和标准NCHW不一样内部用的是NC1HWC0格式。简单理解就是NNIE会把通道方向的数据掐成一段一段每段固定长度然后按空间维度和段去排列。这意味着你不能像在Caffe或Darknet里那样直接按NCHW顺序去读内存。假设网络某一层输出是N1C32H26W26。NNIE输出时通道会被分成C12组每组C016段。内存里排布方式是第一组的16个通道对应H*W个像素的16维向量依次排开放完第一组所有空间位置再放第二组的16个通道。看起来不复杂但如果你在CPU端直接按C32连续取出一个像素的所有通道数据是错乱的因为同一个空间位置的32个通道并不连续存放跨了两个C0组中间还夹了一大段别的空间位置数据。所以端侧代码里必须按NC1HWC0的索引规则去取数。我写了一个工具函数输入是NNIE给出的数据指针和特征图的N、C、H、W输出是重排后的NCHW数据。虽然多一次内存拷贝但后续解码逻辑可以用标准方式写不容易出错。等到全部跑通之后如果你追求极致性能再针对特定的H、W、C做内联优化。4.2 YOLO解码与后处理拿到NNIE输出的特征图之后后面的活就是纯CPU的。yolov4-tiny有两个检测尺度13x13和26x26每个网格预测3个anchor两种尺度合计9352个候选框。解码时先把特征图每个cell上的原始数值按anchor维度拆分。每个anchor对应85个值前4个是tx、ty、tw、th第5个是objectness后面80个是类别概率。需要特别注意的是NNIE输出的这些数值是卷积输出后的原始logit并没有做过sigmoid。如果你直接把0.5当成阈值去筛选会发现所有框都消不掉因为还没有归一化。解码公式我用的是下面的方式bx (sigmoid(tx) grid_x) * stride by (sigmoid(ty) grid_y) * stride bw anchor_w * exp(tw) bh anchor_h * exp(th)这里的stride是416除以当前特征图边长13x13对应3226x26对应16。anchor_w和anchor_h是yolov4-tiny官方配置里预先设定好的值13x13尺度用(10,14)、(23,27)、(37,58)26x26尺度用(81,82)、(135,169)、(344,319)。千万别把anchor配反了配反的典型症状是检测框全跑到角落或者尺寸完全错乱。sigmoid建议用查表法或者fast sigmoid函数因为一张图有900多个候选框每个框要算多次sigmoid纯用exp函数在CPU上付出的代价不小。我们实测下来浮点的exp版本和查表版本在后处理总耗时上能差出5到8毫秒对目标帧率有实际影响。4.3 怎么让后处理不拖后腿后处理流程里最耗时的其实是NMS。候选框最多框之间的IoU计算量和候选框数量的平方成正比。第一次实现我就老实做双重循环一张图处理下来后处理耗时20多毫秒帧率直接被卡死。优化思路有两个。第一个是提前过滤物体置信度低于0.25的框直接丢掉大多数场景下这一步能筛掉八成以上候选框。第二个是类别分组按类别分别做NMS相同类别之间的框才计算IoU避免跨类别误抑制。这两个改动合起来把后处理耗时降到了5毫秒以内效果非常明显。还有一个小技巧如果你用C做端侧实现可以在解码阶段就直接把置信度和类别索引存成结构体数组只保留分数最高的前若干个框进入NMS排序而不是把所有候选框都完整保留。内存占用小排序也快。需要避免的坑是NMS里忘记做“同类别比较”。我第一次写NMS时没按类别分组结果不同类别物体重叠时一个框被另一个类别的强框抑制掉检测结果偶尔会缺目标。按类别分组之后这个问题就消失了。5. 性能优化与常见坑5.1 多核并行、帧率瓶颈与内存管理性能调优的第一个坎就是确认帧率瓶颈到底在NNIE侧还是CPU侧。NNIE推理的时间可以通过打印时间戳拿到后处理和解码的时间也分别统计。如果NNIE推理耗时占比已经很高优先优化模型尺寸和量化类型如果CPU后处理是瓶颈优先优化解码和NMS。NNIE支持多核并行推理但前提是你得把任务切分得够细。实际场景中常见做法是设置batch把多张图一次性发给NNIE充分利用多核流水线。不过要注意batch变大后单帧内存占用也会同步上涨嵌入式设备的内存预算要提前算清楚。我们用batch2跑过帧率提升大约30%但内存涨了不少后来在生产里还是回到batch1用多线程挂多个模型实例的方式稳定跑。内存管理的核心原则是启动时一次性把NNIE需要的输入、输出、中间buffer都申请好避免推理过程中反复malloc。嵌入式系统的内存分配器不像PC那么高效频繁分配会造成碎片跑几个小时后系统变慢甚至内存不足。当时我为了省事在每一帧推理前临时申请一个输出buffer结果运行半小时后系统内存碎片化严重检测频率肉眼可见地下降。改成启动时预分配后问题彻底消失。5.2 常见问题速查表下面这个表是我在NNIE部署yolov4-tiny过程中反复遇到并确认过解决方法的高频问题基本覆盖了从模型转换到端侧稳定运行的主要环节。问题现象可能原因排查方法解决办法转换成功但检测结果完全错乱BGR/RGB通道顺序不一致打印预处理前后图像数组对比调整工具链或预处理中的通道顺序小目标检测不到大目标正常INT8量化精度损失集中在小目标上换INT16验证对比精度用INT16运行或增加带小目标的量化校准图检测框整体偏移尺寸不对anchor配置与特征图尺度不对应核对cfg中的mask和anchor按13x13/26x26分别对应正确anchor组后处理NMS耗时高未做低置信度过滤和类别分组打印NMS前后耗时先粗筛再按类别分组执行NMS内存随时间变高甚至卡死推理过程中频繁动态分配观察内存曲线启动时一次申请所有buffer并复用特征图读取数据错乱未按NC1HWC0布局读取打印某通道某位置的数值与CPU层输出对比按C0分组索引方式读取并重排每条我都实际踩过不少问题看起来像是“模型不行”最后定位下来都是工程实现细节。所以遇到异常先别急着换网络结构先把工具链、数据布局、解码逻辑这三层基础检查做扎实。5.3 回头看的一些体会与建议NNIE这套东西如果只跑一个demo一两天就能出效果如果要做长期稳定运行需要耐着性子把工具链的每一个细节吃透。我再分享几条自己总结的经验。第一模型转换阶段的自动化脚本一定要留好。不管是Darknet权重转Caffe还是Caffe模型做逐层校验脚本化之后你可以随时复盘和复用。项目做到一半换分辨率、换类别数量时脚本能帮你快速重新生成模型避免手工改prototxt改到怀疑人生。第二日志和数据打印是定位问题的利器。NNIE推理结果出错时我会把某一层的输出全部导出和CPU上的相同模型对比。通过逐层对比能迅速把问题锁定到某个具体环节。很多时候你以为的玄学问题其实只是某个维度顺序搞反了。第三量化相关的调优要设定一个精度验证集。不要只看训练集mAP应该准备一些真实的、复杂的场景图跑一遍检测看输出框的位置和置信度是否符合预期。用肉眼判断往往比指标更直观因为mAP无法告诉你“是不是某个特定类型的目标全丢了”。另外一定要重视工具链版本。不同版本对算子的支持、量化策略和对齐要求可能不一样升级工具链后务必重新验证原有模型不能盲目沿用旧模型和旧配置。回头再看整个yolov4-tiny的NNIE部署过程真正花时间的不是跑通流程而是把每个环节的“为什么”搞清楚。无论是模型适配时为什么要融合BN量化时为什么要选校准图还是端侧为什么要按NC1HWC0读数据背后都有明确的硬件和工程原因。把这些问题弄明白之后换一个模型、换一个硬件平台你也能快速上手而不是反复试错。最后说一个我最近实践下来的小改进把NMS里的IoU计算从浮点改成定点近似在保持精度不变的前提下又省了一点点耗时。帧率敏感的嵌入式场景里每一毫秒都值得抠。这个内容之后如果继续深挖我会把端侧整个推理链路的耗时分解图和优化工具做成独立的一篇方便大家对照着做性能预算。