ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YOLOv4在KV260上的完整部署链路:训练、量化、编译与推理实践

YOLOv4在KV260上的完整部署链路:训练、量化、编译与推理实践

从拿到 KV260 这块板子到现在,我前后踩了不少坑,特别是在把 YOLOv4 从 GPU 上的训练环境搬到板子上的时候。训练、量化、编译、部署这四步,每一步单独拎出来看都有教程,但串在一起的时候,各种版本不匹配、算子不支持、精度掉点的问题全来了。这篇文章就把我实际跑通的完整链路拆开讲,包括配置参数、踩坑记录和排查思路,希望能帮你少走点弯路。

先说下这套流程适合谁:手里有 KV260 或者类似边缘计算板卡,想把目标检测模型真正跑在板上而不是只停留在训练阶段的开发者。默认你已经有基础的目标检测训练经验,知道 YOLOv4 的网络结构大概是什么样,也知道怎么在自己的环境里训出来一个能用的模型。如果你还没有 KV260,只是想提前看看部署流程长什么样,这篇文章也能帮你建立一个整体认知,后面真上手时不会两眼一抹黑。

整个链路里我最想强调的一点是:训练出来的模型和部署能用的模型之间,隔着一道很深的鸿沟。很多人觉得训出模型就万事大吉,结果在量化和编译阶段被各种报错折磨得怀疑人生。所以这篇文章会按“训练→量化→编译→部署”的顺序,把每一步的输入、输出、关键参数和容易出错的地方都讲清楚。

1. 整体链路拆解:为什么是YOLOv4,如何在KV260上跑起来

1.1 KV260的硬件定位与算力特征

KV260 这块板子的核心价值在于,它不是用 CPU 或者 GPU 去跑神经网络,而是用板上的 FPGA 资源去实现一个专用的深度学习处理器单元(DPU)。这个 DPU 是板卡官方工具链里提供的可编程 IP,相当于你在 FPGA 里“画”出来一个专门跑卷积的加速器。它的算力不像独立显卡那么夸张,但好在功耗低、时延稳定,做成一个边缘检测设备非常合适。

这就带来一个很实际的限制:模型的算力需求不能太大,否则 DPU 上跑不满或者根本跑不动。我一开始尝试过把比较大尺寸输入的 YOLOv4 直接编译上去,结果发现 DPU 的资源利用率和内存占用都很难看。后来把输入尺寸控制在一个合适范围,比如 416×416,整个推理时间就变得可以接受了。YOLOv4 相比更新的 YOLO 版本,结构虽然没有那么花哨,但每个算子都很“正统”,卷积、池化、上采样、拼接这些基本操作都能被 DPU 工具链完整覆盖,这是它在这个平台上特别吃香的原因。

1.2 从训练到部署的完整环节

整个流程可以拆成五个环节,我建议你把它当成一条流水线来理解:

  • 数据准备:整理标注数据,转换成训练框架要求的格式。
  • 模型训练:在 GPU 上用 Darknet 训练出 FP32 精度的权重文件。
  • 模型量化:把 FP32 权重量化成 INT8,同时通过校准数据集降低精度损失。
  • 模型编译:用官方编译工具把量化后的模型转换成 DPU 能执行的 ELF 文件。
  • 应用部署:在 KV260 的 Linux 系统里写推理程序,加载 ELF,做预处理和后处理。

每个环节的产物都会变成下一个环节的输入,任何一个环节的“小改动”都可能让后面全部重来。比如训练时改了输入尺寸,量化环节就要重新校准,编译环节也要重新指定输入张量形状。所以我强烈建议你在训练之前就定好部署时的输入尺寸,别中途频繁改。

1.3 为什么这条链路能成功

我见过很多人在这个平台上去适配各种新模型,最后发现许多新颖的模块在编译时根本不被支持。YOLOv4 的好处就在于它足够的“标准”:主干是 CSPDarknet53,特征融合是 PANet,还有 SPP 模块,这些结构在 DPU 工具链的算子支持列表里基本都能一一对应。你可以通俗地理解为,DPU 更像一个“挑食”的处理器,只爱吃结构规整的菜,YOLOv4 恰好就是这么一盘标准菜。这也是我在反复折腾之后,最终认定这个方案的核心原因。

2. 模型训练:在GPU上把一个可部署的YOLOv4训出来

2.1 数据集准备与标注格式转换

模型的起点是数据。YOLOv4 原生的训练框架是 Darknet,它期望的标注格式是每个图片对应一个同名 txt 文件,每行内容为:

class_id x_center y_center width height

这里的 x_center、y_center、width、height 都是相对于图片宽高的归一化数值,用小数表示。如果你手里的是常见的 VOC 格式 XML 或者 COCO 格式 JSON,要先写个脚本做转换。

举个例子,假设一张图片的宽是 1920,高是 1080,某个目标的 bounding box 在像素坐标系下是 (x_min=500, y_min=400, x_max=900, y_max=700),那么转成 Darknet 格式就是:

0 0.3646 0.5093 0.2083 0.2778

计算过程不复杂,中心点 x 坐标是 (500+900)/2=700,除以 1920 得到 0.3646;中心点 y 坐标是 (400+700)/2=550,除以 1080 得到 0.5093;宽度是 400,除以 1920 得到 0.2083;高度是 300,除以 1080 得到 0.2778。

我在实际做的时候,习惯把训练集、验证集、测试集分开三个目录,并且生成对应的 train.txt、val.txt、test.txt 文件,文件内容就是图片的绝对路径列表。Darknet 训练时会根据这个列表去读取图片和对应的标注文件。有一条经验是:路径最好是绝对路径,别用相对路径,否则换机器后容易莫名读不到数据。

2.2 修改配置:类别数、Anchor与输入尺寸

网络结构定义在 cfg 文件里。这一步是训练前最容易出错的地方,改错一个数字,训练出来的网络结构就是错的。主要需要处理三类配置:

第一个是每个 yolo 层前面的卷积层 filters。YOLOv4 有 3 个 yolo 层,每个 yolo 层前面会有 1×1 卷积做输出通道变换。filters 的计算公式是:

filters = (classes + 5) * 3

如果你的检测目标有 5 个类别,那么 filters = (5+5)*3 = 30。这里面的 5 表示每个锚框要预测 4 个位置参数加 1 个目标置信度,3 表示每个网格点有 3 个锚框。

第二个是 yolo 层里的 classes 参数,直接改成你的类别数。

第三个是输入尺寸,也就是 cfg 文件中的 width 和 height。我在这个项目里用的是 608×608 还是 416×416,需要根据 DPU 的输入约束来定。考虑到板子的资源有限,我最后选择了 416×416。这里要提醒你,改输入尺寸不只是改两个数字,Anchor 也需要根据新尺寸重新聚类。

Anchor 的重新计算可以用 Darknet 自带的 k-means 工具,把训练集标注框的宽高全部提取出来,跑一遍聚类,得到 9 个预选框。很多人忽略这一步,直接沿用 COCO 上的默认 Anchor,导致训练收敛慢、精度上不去。

2.3 训练参数与迁移学习策略

训练 YOLOv4 最省力的方式是加载预训练权重做迁移学习。流程分两步:先用很小的学习率冻结主干网络训练,让检测头先适应你的数据集;然后再放开全部层,用小学习率微调。

我用的参数和踩坑后的调整如下:

  • batch=64,subdivisions=16 或 32。subdivisions 的作用是把一个 batch 拆成若干小份,防止显存溢出。显存不够时就调大 subdivisions,效果等同减小 batch 但不影响最终梯度。
  • learning_rate=0.001,burn_in=1000。前 1000 个迭代步里学习率会从 0 慢慢爬到设定值,这一步是为了让训练更稳定。
  • policy=steps,steps=40000,50000,scales=.1,.1。意思是训练到 40000 步时学习率降为原来的 0.1,到 50000 步时再降为原来的 0.1。
  • momentum=0.949,decay=0.0005,这两个是常规值,一般不用动。

迁移学习时我给训练命令加上-stop_backward或者先冻结主干权重,具体方式取决于你用的 Darknet 版本。如果不冻结直接全量训练,对于小数据集很容易发生过拟合,尤其是数据量只有几千张时,训练集 loss 降得很低,但验证集 mAP 很惨。

2.4 训练过程监控与精度评估

Darknet 训练时的命令行日志会持续输出 loss 值,我一般关注两点:一是 loss 是否在稳步下降,二是平均召回率 Recall 和平均查准率 Precision 的走势。很多人一看到 loss 不降就焦虑,其实前几千步都在做热身,只要整体趋势向下就不用慌。

训练结束后,用下面的命令测试模型在验证集上的 mAP:

./darknet detector map data/obj.data cfg/yolov4.cfg backup/yolov4_final.weights

这一步产出的精度值要记录下来,因为后面量化环节会拿它做对比基准。如果说训练完的 mAP 是 0.82,量化后掉到 0.70,那就要警觉,说明量化参数没调好;如果本身就只有 0.50,那问题大概率出在训练环节。

我还想提醒一个容易被忽视的细节:训练时的数据增强会影响模型的最终表现。Darknet 默认开启了随机翻转、随机缩放、马赛克等增强手段。如果你的部署环境里很多图片是固定角度、固定光照,建议训练时适当减少数据增强强度,否则模型在真实场景里可能表现反而变差。

3. 模型量化:把FP32压成INT8,保住mAP

3.1 内存带宽与算力账:为什么量化是必选项

模型训练完是 FP32 精度,也就是每个权重占 4 字节。YOLOv4 的权重文件大约有 244MB,听起来不是很大,但在嵌入式平台上,这个体积带来的内存带宽压力非常可观。如果直接把 FP32 模型拿到 KV260 上跑,DPU 需要频繁从内存里搬运权重到计算单元,带宽会成为严重的瓶颈,推理速度会很慢。

换成 INT8 之后,每个权重只占 1 字节,模型体积直接缩小到原来的四分之一,内存带宽压力大幅下降。同时,DPU 的 INT8 运算在硬件上做了深度优化,单位时钟周期内能完成更多操作。你可以把它理解成一条本来只能并排跑 2 辆卡车的路,现在每辆车都缩小了,能并排跑 8 辆。

3.2 量化原理:scale与校准数据集

量化的核心是把浮点数映射到 0 到 255 的整数区间。对每个张量(权重或激活值),需要计算两个关键参数:scale 和 zero point。scale 决定浮点数值如何缩放成整数,zero point 决定浮点零值映射到哪个整数。

这个映射必然带来精度损失,所以量化工具需要你提供一个“校准数据集”,用来统计模型中间激活值的分布范围,从而为每个张量选取合适的截断阈值。校准数据集的选择会直接影响量化后的精度,不是随便拿几张图就行的。

我在实际项目里总结了校准数据集的三条选择标准:

  • 图片数量在 100 到 500 张之间,太少统计不充分,太多校准时间过长。
  • 覆盖所有检测类别,并且每类的样本数量尽量均衡。
  • 图片的尺寸、光照、角度要和真实部署场景接近,最好从验证集中随机抽取。

3.3 量化实操步骤

量化环节我用的是板卡官方的量化工具链,输入端是训练好的 FP32 权重和 cfg 文件,输出端是量化后的模型。整个操作分四步走:

第一步,准备校准数据集目录,把选好的图片放进去,写一个图片列表文件。

第二步,修改一个数据加载配置文件,指定输入图片的目录、图片大小、batch 数等信息。这个配置文件在量化工具的说明文档里会有模板,直接照着改路径就行。

第三步,执行量化命令,指定权重文件、网络结构和校准配置。量化完成后,工具会输出一个量化后的模型文件和一份精度评估报告。

第四步,用工具自带的评估功能,在验证集上计算量化模型的 mAP,和训练时的 mAP 对比。

我在这一步遇到过的最大问题是“校准图片的输入尺寸和训练尺寸不一致”,导致量化过程中网络输入张量形状错乱。后来统一成 416×416 之后就顺利了。所以务必检查好配置文件里的尺寸参数。

3.4 精度下降排查思路

量化后 mAP 掉点是正常现象,通常掉 1 到 3 个百分点问题不大,但如果掉了 5 个点以上,就需要排查了。排查顺序我一般是这样:

先看校准数据集是否合理。如果校准图片和验证集差异太大,比如校准集里全是白天晴天的图、验证集里全是夜晚的图,精度掉点会很严重。

再看是否有个别层对量化特别敏感。官方量化工具一般支持设置“不量化层”的白名单,也就是让某些层继续保留 FP32 精度。你可以用二分法定位:先让网络后半部分保持 FP32,量化前半部分,评估精度;再反过来试,定位到敏感层之后,把那一层加进白名单。

还有一种容易忽略的情况是:训练时用了某种特殊的数据增强,导致激活值分布和实际部署时差异很大。这种问题很难在量化环节解决,只能回到训练环节关闭增强重新训。

4. 编译与ELF生成:让DPU真正认识你的模型

4.1 编译这一步到底在干什么

量化完成之后,模型还是一个框架层的描述,DPU 看不懂。编译工具负责把这个模型翻译成 DPU 能直接执行的指令流,同时把权重参数打包成适合 DPU 读取的二进制格式,最终输出一个 ELF 文件。

你可以把 ELF 文件理解成专门为这块板子定制包装好的“模型可执行包”。它在推理时被应用程序加载到 DPU 上,DPU 按照里面的指令流完成每一层卷积、池化、激活等操作。应用层不需要再关心模型结构,只需要管好输入输出张量就行。

所以编译环节的重要性不亚于训练:如果网络结构里有 DPU 不支持的算子,编译会直接失败,或者即使编译通过,实际运行也可能出错。

4.2 编译参数配置与操作流程

在编译之前,你需要了解自己的 DPU 配置。板卡官方工具链会提供一个描述 DPU 硬件结构的网表文件和对应的配置文件,编译时要指定这些文件。

编译命令的大致形式如下:

vai_c_xir --xmodel /path/to/quantized_model.xmodel \ --arch /path/to/DPU.json \ --net_name yolov4 \ --output_dir /path/to/output

不同版本的工具链命令可能略有差异,但核心参数就几项:输入模型路径、DPU 配置路径、输出网络名称和输出目录。编译成功后,在输出目录里会生成 .elf 文件和 .json 文件,后者记录了模型输入输出的张量信息,部署时写代码要用。

我在编译时踩过一个很具体的坑:早期无数算子拼接的应用中,某一层用了工具链支持列表之外的 resize 参数,编译卡了很久,最后报了一个非常难懂的底层错误。排查了半天才发现是上采样倍率设置成了 DPU 不支持的值。后来我养成了一个习惯:在编译前先用工具链自带的一个算力预估功能,提前检查模型的算子覆盖情况,能省很多时间。

4.3 算子不支持时的处理思路

如果编译时报“unsupported op”之类的错误,通常有三种处理思路:

第一种是替换算子。比如某些自定义激活函数在 DPU 上不支持,可以把它们换成 ReLU 或者 LeakyReLU,YOLOv4 里主要就是 LeakyReLU,问题不大。

第二种是调整网络结构。如果某个特殊模块编译不过,可以考虑把它从网络中拆掉。比如 SPP 模块里的某些池化组合如果报错,可以尝试改成普通的最大池化。当然这会损失一些精度,需要权衡。

第三种是把不支持的算子挪到 CPU 端处理。这个思路在部署阶段实现,网络里能跑 DPU 的部分尽量跑 DPU,不能跑的部分在预处理或后处理阶段用 CPU 代码补上。比如 Focus 模块如果在你的工具链里不支持,就可以在预处理里用切片操作先处理完,再把结果喂给 DPU。

5. 部署与推理:从ELF文件到能看到检测框

5.1 应用软件框架设计

编译出 ELF 文件只是一个开始,真正的产品是把推理程序跑在板子上。我在 KV260 上部署 YOLOv4 时,程序结构大致分如下几个模块:

  • DPU 驱动初始化模块:负责打开 DPU 设备,加载 ELF 文件,创建推理任务。
  • 图像读取与预处理模块:负责从文件或摄像头读取图像,并转换成 DPU 需要的输入格式。
  • 模型推理模块:调 DPU 接口执行推理,获取原始输出张量。
  • 后处理模块:对输出张量解码,还原边界框坐标,执行 NMS 过滤。
  • 结果可视化模块:把检测框画到原图上并输出。

这个框架本身没什么神秘的,但每一块都有细节。

5.2 预处理与后处理的关键细节

先说预处理。YOLOv4 在训练时通常使用 BGR 三通道输入,并且做了归一化。但在 KV260 的 DPU 工具链里,输入张量的通道顺序取决于你训练时的配置。如果训练时用的是 BGR,部署时又用 OpenCV 的默认 BGR,那没问题;但有一个非常常见的错误是,OpenCV 读出来是 BGR,而 DPU 期望的是 RGB,导致检测完全失效。

怎么确认?当你的模型在部署后检测率极低、框的位置乱飘时,第一个要怀疑的对象就是颜色通道顺序。处理办法也很简单,在预处理阶段调用cv2.cvtColor(img, cv2.COLOR_BGR2RGB)做一次转换即可。

另一个关键点是缩放方式。DPU 对输入尺寸有要求,通常需要缩放到固定尺寸。我建议用等比例缩放(letterbox),也就是把图片缩放到 416×416 的尺寸,保持宽高比,空余部分用灰色填充(通常填充值取 128)。如果直接强行拉伸到方形,目标会变形,检测精度会明显下降。

后处理环节,主要是从 DPU 的原始输出中解码出目标框。YOLOv4 的输出分为三个尺度,每个尺度的张量形状通常是(1, 255, 13, 13)或者类似形式,255 就是(5 + classes) * 3。后处理时,先对每个网格点的输出做 sigmoid 激活,然后根据预先设定的 Anchor 尺寸解码出中心坐标、宽高,再按置信度阈值过滤一遍,最后做 NMS 去掉重叠框。

这里有个我踩过的坑:DPU 输出张量的数据排布是NCHW还是NHWC,不同工具链输出不一样。如果你的后处理代码假设 NHWC,但实际输出是 NCHW,解码结果会完全错乱。解决办法是提前打印输出张量的 shape,确认排布方式后再写解码逻辑。

5.3 运行性能与多核并发

我实测下来,在 KV260 上跑 YOLOv4 416×416 输入,单次推理大约在几十毫秒量级,具体数值和 DPU 配置、时钟频率、内存带宽都有关系。

如果想进一步提升吞吐量,可以启用多线程并发推理。DPU 本身支持同时创建多个任务,你在应用里可以开多个线程,每个线程独立完成“预处理→推理→后处理”的流水线。我在一个项目里用两个线程并发跑,整体帧率比单线程提升了不少。

不过并发也有代价:线程太多时,预处理和后处理的 CPU 占用会明显上升,如果 CPU 被占满,反而会拖慢整体速度。所以并发线程数需要根据实际情况做权衡,一般 2 到 4 个线程是一个比较稳妥的选择。

6. 经验汇总:排查清单与几条实操建议

6.1 常见问题速查表

问题现象可能原因解决办法
编译过程报不支持算子网络中包含了 DPU 不支持的操作替换算子、调整网络结构或把该层挪到 CPU 处理
量化后 mAP 掉点严重校准数据集与真实场景差异大,或某层对量化敏感重新选择校准数据,用白名单保留敏感层为 FP32
部署后检测率极低颜色通道顺序错误或预处理方式不正确确认训练时的通道顺序,检查缩放到尺寸的方式
检测框位置错乱输出张量排布方式和后处理代码假设不一致打印输出 shape,调整 reshape 顺序
DPU 推理时间不稳定内存带宽受限或 CPU 端预处理占用过高优化预处理逻辑,控制并发线程数
ELF 文件加载失败DPU 配置和编译时使用的配置不一致确认编译时指定的 DPU 网表与板子实际配置一致

这张表只是开头,实际遇到的问题往往更隐蔽。比如有一个问题是我在换了一块不同的板卡映像后,DPU 驱动版本变了,旧版编译出来的 ELF 直接加载失败。后来我重新编译一遍就正常了。所以如果你换了系统版本,建议把所有模型文件重新走一遍编译流程。

另一个值得注意的点是:备份你的编译配置文件和量化校准配置。很多时候问题不是在当前步骤,而是你改了配置但没留底,后面出问题时无从排查。我在项目里会为每个模型单独建一个目录,放好训练时的 cfg、权重、量化配置和编译日志,方便回溯。

6.2 我的个人调试顺序建议

如果让我重新做一次这个项目,我的调试顺序会是这样的:

先跑通官方自带的 YOLOv4 示例。这一步的目的是验证工具链环境、DPU 驱动、部署代码框架是否正常,不要用自己的模型去试探环境有没有问题。

其次,用官方示例的权重替代一个简单图片验证预处理和后处理代码。保证在一张图上能看到正确的检测框。

再然后,才换成自己训练的权重。如果这一步出现精度问题,就可以确信是模型本身或量化环节的问题,而不是部署框架的问题。

最后,把单张图片测试扩展到视频流或摄像头实时推理,并做性能调优。

这套顺序帮我省下了大量排查时间。否则一旦出错,你会陷入“到底是模型的问题、量化的问题、还是部署代码的问题”这样的泥潭。

我个人在实际操作中还有一个很深的体会:这类模型部署项目,真正花在写代码上的时间其实只占一小部分,大部分时间都耗在“环境对齐”这件事上。训练环境的 PyTorch 版本、量化工具版本、编译工具版本、板卡系统版本,任何一环对不上,都可能让你卡上半天。所以从一开始就做好版本记录和环境隔离,是让你后面省心的关键。如果你正在被某个奇怪的报错折磨,不用太怀疑自己,先把版本对齐检查一遍,很多时候答案就在那里。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进