ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

RK3588边缘计算实战:单NPU高效并行部署三路AI检测

RK3588边缘计算实战:单NPU高效并行部署三路AI检测 1. 项目概述手头正好有一块RK3588的开发板项目需求是在一个边缘计算盒子里同时搞定三件事人员入侵检测、烟火识别、垃圾分类。最开始我也犯嘀咕这三件事分开跑都见过同时塞进一块板子、一个NPU里还要保证实时性确实是个有挑战的活。但折腾完之后发现RK3588的6 TOPS NPU其实比想象中能扛关键看你怎么分配算力、怎么调度模型。这个项目的核心是把三路视频流分别接入三个不同的AI检测任务在同一块RK3588的NPU上并行推理同时保证CPU侧能处理RTSP拉流、视频解码、画框显示等杂活。人员入侵用的是目标检测模型烟火检测用的是轻量化分类加检测的组合垃圾分类则是端到端的检测加分类。三个模型交替或分时占用NPU最终在HDMI输出上实时显示三路结果。适合谁来参考呢如果你手里也有一块RK3588的板子正头疼怎么把多个模型同时部署上去或者你刚接触RKNN工具链想知道模型转换、量化、板端推理的完整流程——这篇文章就是给你写的。即便是小白只要照着步骤走也能把三个模型跑起来。2. 单块NPU如何高效承载三个模型任务2.1 6 TOPS算力够不够用先算一笔账RK3588的NPU标称6 TOPS算力INT8精度。这是什么概念做一个粗略估算YOLOv8s的INT8版本在RK3588上单帧推理大约需要30到50毫秒算下来每秒能跑20到30帧。如果换成更轻量的模型比如YOLOv8n或者定制的小模型单帧推理可以压缩到15到25毫秒这样一秒能腾出40帧左右的余量。三个模型同时跑最直观的方案是每个模型各占三分之一算力也就是每个模型分到约2 TOPS。对于人员入侵检测2 TOPS足够跑一个YOLOv8n级别的模型帧率能到15到20FPS。烟火检测如果是用分类网络加区域判断2 TOPS跑一个MobileNetV3级别的分类器绰绰有余。垃圾分类模型稍微重一点因为要识别几十个类别但同样用轻量化检测头2 TOPS也能维持10到15FPS。算了一下总账三路任务同时跑NPU占用率大约在60%到75%之间CPU还有富余做编解码和业务逻辑。这个余量很重要因为NPU不是唯一瓶颈内存带宽和CPU调度也会影响整体表现。2.2 NPU的时间片轮转机制RK3588的NPU架构是三个核心的MACC阵列组底层调度由rockchip的驱动统一管理。在应用层NPU一次只能执行一个推理任务所谓“同时跑三个模型”实际上是靠快速切换实现的——当一个模型在NPU上执行卷积计算时另一个模型的数据已经在内存里排队等待了。这个机制跟操作系统的进程调度很像。每个推理请求被封装成一个任务通过rknn_init、rknn_run这类API提交给NPU驱动。驱动按照先进先出或者优先级策略给任务分配计算资源。在实际工程里我用的是权重轮转法三个推理线程各自独立每个线程在while循环里执行rknn_run因为NPU驱动本身是线程安全的多线程提交任务时驱动会自动排队。需要注意一个坑如果三个线程同时提交任务NPU驱动虽然有锁保护但任务切换有开销频繁抢占会导致推理时间抖动。我的解法是给三个任务设置不同的优先级人员入侵优先级最高烟火检测次之垃圾分类最低这样在NPU繁忙时高优先级的任务能更快插队执行。2.3 模型选型的核心思路小模型大模型的组合拳模型选型直接决定了NPU的承载能力。三个任务里人员入侵和烟火检测都有一个共同特点目标在画面中的尺寸变化很大。入侵者可能贴近摄像头也可能在远处烟火更是有大有小这种场景如果只用一个固定感受野的模型很容易漏检。我的做法是用一个baseline模型做全图检测同时加一个额外的分类头做二次确认。具体来说人员入侵用YOLOv8n作为baseline输出检测框和置信度烟火检测用MobileNetV3-SSD这样的轻量检测结构垃圾分类则是一个单阶段的分类网络输入224x224的RGB图输出几十个类别的置信度分布。三个模型全部用INT8量化。为什么要量化INT8相比FP16能减少一半内存占用推理速度提升30%到50%。更重要的是RK3588的NPU对INT8有专门的硬件加速单元不量化就等于自废武功。量化过程中遇到的最大问题是某些层对量化敏感尤其是检测头的最后一层我的处理方式是对这些层做channel-wise量化而不是整个tensor用同一套scale。3. 三路视频流接入和解码的工程实践3.1 RTSP拉流与多路解码方案三个任务的输入来源不一样人员入侵接的是IPC摄像头的RTSP流烟火检测接的是另一个摄像头垃圾分类则是本地USB摄像头或者图片文件。统一处理这三路输入是个容易翻车的地方尤其是RTSP流的不稳定性。RK3588的硬解能力很强支持H.264和H.265的硬解码。我用的方案是调用rockchip的mpp解码库通过FFmpeg拉流后把裸流喂给mpp这样CPU占用就能控制在很低的水平。刚开始偷懒直接用FFmpeg软解发现CPU瞬间被吃满NPU还在干活CPU早就成了瓶颈。解码后的帧数据是NV12格式RKNN的输入要求是RGB或者BGR这里需要进行格式转换。直接用CPU转一次640x480的转换大约耗时3到5毫秒三路同时转就到10毫秒以上了有点尴尬。后来看到有rga硬件加速模块就改用rga做格式转换和缩放转换时间直接压缩到1毫秒以内CPU几乎无感。3.2 前处理的硬件加速技巧RK3588的RGARaster Graphic Acceleration模块可以用来做图像的缩放、格式转换、旋转等操作。这个模块平时大家不太关注但在多路视频处理里能省下不少CPU资源。我的做法是用rockchip的librga库调用im2d接口实现NV12转RGB和缩放。有一个细节:rga处理完的buffer是连续内存,但RKNN的输入需要按模型的channel顺序排布。rknn的api提供了rknn_create_mem接口申请dma buffer把rga的输出直接映射到这个dma buffer上就不再需要额外的memcpy操作。这一步优化对性能影响非常明显我实测下来省掉了大约30%的拷贝耗时。首帧和尾帧的处理也要注意。RTSP流刚接入时解码器需要积累几帧才能输出完整画面直接跑推理会出现前几帧花屏或者黑屏。我的处理是等待解码器输出稳定后再启动推理线程或者在推理前做一个简单的帧间隔合理性判断——如果帧间隔异常大说明解码可能丢帧了这时候主动丢弃这一帧避免画面上出现残影。4. RKNN模型的转换、量化与板端部署4.1 从PyTorch/ONNX到rknn的完整流程模型训练完成后要把模型转换成RKNN格式才能在NPU上跑。整个流程分为三步先将PyTorch模型导出成ONNX再用RKNN-Toolkit2把ONNX转成rknn格式最后在板端加载rknn模型执行推理。ONNX导出这里有个容易出错的点。PyTorch里有些算子ONNX不支持比如某些自定义的检测头结构需要在导出时用torch.onnx的symbolic函数做映射。我遇到过一次DCN可变形卷积算子导出失败的问题最后只能把可变形卷积替换成普通卷积精度掉了1个百分点左右换来的是能正常部署。用RKNN-Toolkit2转换时有几个关键参数需要格外留意。量化数据集的选择直接影响INT8精度建议从真实使用场景中截取几百到上千张图片作为校准集而不是随便从网上下一个数据集。量化方式上我用的是static quantization把normalize层和通道顺序都融合进模型里减少板端的前处理代码。4.2 量化踩坑实录识别率掉了怎么办量化最头疼的问题是精度下降。我第一次量化YOLOv8n做人员入侵检测时mAP从FP16的0.72掉到了0.65这个跌幅不能接受。排查后发现两个问题第一是校准集里的图片正样本太少了。500张图里只有80张包含人员目标导致量化时对人员这个类别的激活范围估计不准确。解决方法是专门采集包含各种距离、姿态、光照条件下人员的图片把人员类别的比例提升到60%以上。第二是模型输出层对量化过于敏感。YOLO的检测头包含多个卷积层特别是最后的1x1卷积输出的值范围跨度很大。解决方案是对输出层做per-channel量化并且把几个关键层的量化粒度从per-tensor改成per-channel精度恢复到了0.70基本上在可接受范围内。4.3 板端推理代码的三段式结构在板端加载RKNN模型并执行推理代码结构相对固定。我用C写了一个通用的推理封装类三个模型共用同一个类只是传入的模型路径和输入输出尺寸不同。// 推理封装类核心接口 class RKNNModel { public: int Init(const std::string model_path); int Infer(cv::Mat input_image, std::vectorDetectResult results); void Release(); private: rknn_context ctx_; rknn_input_output_num io_num_; rknn_tensor_attr* input_attrs_; rknn_tensor_attr* output_attrs_; };Init函数里做这些事rknn_init创建上下文、rknn_query查询输入输出信息、rknn_create_mem申请输入输出buffer。Infer函数里做这些事输入数据拷贝到DMA buffer、调用rknn_run执行推理、从输出buffer解析检测结果。Release时调用rknn_destroy释放资源。实际跑起来发现一个性能细节如果每帧都用rknn_inputs_set设置输入数据驱动内部需要做一次数据拷贝这个开销在低帧率时不明显但高帧率下会累积。我的优化方式是在Init阶段就用rknn_create_mem申请固定大小的DMA buffer之后每帧只需要把图像数据memcpy到这个固定buffer里省掉了一层拷贝。5. 多线程调度与NPU任务分时协同5.1 三线程独立推理资源竞争如何控制三个任务如果各跑一个线程每个线程里循环调用rknn_run从驱动的角度来说它们是并行提交任务的。但NPU本身只有一个执行单元同时只能跑一个推理所以真正执行是串行的。这样做的好处是每个线程的逻辑清晰坏处是任务切换频繁上下文切换开销不可忽略。我的调度策略是给三个线程设置不同的nice值配合pthread的优先级控制让人员入侵的推理线程优先得到CPU时间片。这样即使NPU在同时处理三个任务人员入侵的推理请求也能更快到达NPU驱动队列的前端。不过这里要提醒一个问题RKNN的上下文是有内存开销的每个模型都单独rknn_init三个模型占用的内存加起来可能超过500MB。RK3588板子如果内存只有4GB还要跑系统和其他应用内存会吃紧。我的做法是在AI推理子系统中关掉不必要的后台服务给NPU和推理应用留足内存。5.2 帧率调节策略动态跳过和降级多任务并跑时NPU的处理能力是固定的如果每路视频都要求30FPS三路就是90FPS的推理量明显超出算力。我的方案是动态调节每路帧率人员入侵保持20FPS烟火检测15FPS垃圾分类10FPS加起来45FPS刚好在NPU的能力范围内。如果发现NPU占用率超过80%就会触发降级机制。降级策略分三级第一级跳过垃圾分类的推理只保留人员入侵和烟火检测第二级把人员入侵的推理分辨率从640x640降到448x448第三级直接暂停烟火检测只保留人员入侵。这种分级降级策略保证了核心任务人员入侵在任何情况下都不会中断。帧率调节用了一个简单的PID控制思路通过rknn_run的实际耗时和预期耗时之差动态调整推理线程里的sleep时间。实测下来这种反馈调节比固定sleep更稳定尤其是RTSP流的帧间隔本身就不均匀时能有效避免推理任务堆积。5.3 共享内存池避免重复分配每路视频流在解码、前处理、推理、后处理这几个环节都要传递数据。如果每个环节都重新分配内存malloc和free的操作会在高频调用下成为性能瓶颈。我实现了一个共享内存池在初始化阶段预分配三个任务所需的所有buffer之后整个生命周期内不再做动态内存分配。内存池的实现参考了Android的GraphicBuffer思路核心是引用计数加复用队列。帧数据在解码环节写入buffer_1前处理环节从buffer_1读取并输出到buffer_2推理环节从buffer_2读取检测结果写入result结构体。整个链路里没有一次malloc实测内存碎片几乎为零内存占用稳定在600MB左右。6. 在RK3588上实现三级AI任务的技术细节与实现6.1 对照表三种任务的模型架构与参数为了让你对三路任务的资源占比有直观印象我整理了一份模型参数和性能的对照表。任务模型输入尺寸参数量单帧耗时目标帧率人员入侵YOLOv8n640x640约310M约28ms20FPS烟火检测MobileNetV3-SSD320x320约90M约10ms15FPS垃圾分类ResNet18改装224x224约110M约6ms10FPS人员入侵模型单帧耗时最高占NPU资源最多所以把它放在优先级最高同时用20FPS的目标帧率限制避免占用过多算力。烟火检测用320x320输入检测精度有所下降但胜在速度快0.5米以上的烟火目标基本都能识别。垃圾分类因为输入尺寸小在NPU上的耗时只有6毫秒左右属于“顺便做掉”的任务。实际运行下来三路任务同时对NPU发起推理请求时总耗时在44毫秒到50毫秒之间波动换算成聚合帧率在20到22FPS之间。这个数字看起来不高但要注意这是三路并行推理的结果单看任一路的帧率其实都达到了目标值以上。6.2 动态线程池实现代码示例线程池的伪代码是这样的核心是控制三个推理任务的执行周期和优先级// 动态线程池控制示例 class InferenceScheduler { public: void Run() { while (running_) { // 根据NPU占用率动态调整任务帧率 float npu_load GetNPULoad(); AdjustFrameRate(npu_load); // 提交任务到不同优先级队列 SubmitTask(INTRUSION, 0); // 最高优先级 SubmitTask(FIRE_SMOKE, 1); SubmitTask(GARBAGE, 2); // 等待一个调度周期 sleep_ms(50); } } private: void SubmitTask(AITaskType type, int priority); void AdjustFrameRate(float npu_load); };这个调度器在项目里实际运行了大半个月整体稳定。唯一一次崩溃是因为RTSP流断开后解码线程试图写入一个已经释放的buffer加了buffer有效性检查后问题解决。如果你参考这个代码建议运行的每个线程里都加上异常捕获和资源释放逻辑避免线程崩了之后死锁。6.3 推理结果聚合和可视化三路任务的结果要同时显示在同一个画面上我用的是SDL叠加的方式。把HDMI输出的画面分成三个区域左上显示人员入侵检测的摄像头画面和框右上显示烟火检测的画面下方显示垃圾分类的结果和置信度。画框用的是OpenCV的rectangle和putTextCPU开销很小。不过在叠加画面时发现一个奇怪的现象如果直接往HDMI输出的buffer里写数据偶尔会花屏。排查了半天发现是HDMI的输出buffer和NPU的DMA buffer在内存地址上有冲突。解决办法是用memcpy把结果先拷贝到普通内存再从普通内存写HDMI输出问题就消失了。垃圾分类的结果展示比较特殊因为它不是画框而是显示一句文本比如“可回收物塑料瓶 置信度0.87”。我用中文字体渲染最开始用freetype库结合ttf文件渲染中文发现加载字库很耗时后面改成启动时预加载字库位图后续直接拷贝位图速度快了不少。7. 部署后踩过的坑与排查方法7.1 NPU初始化失败模型文件不匹配三个模型转换完后我把rknn模型文件拷贝到板子上跑发现烟火检测的模型加载失败rknn_init返回错误码。排查过程花了整整一天最后发现问题出在模型转换时指定的目标平台参数不对。RKNN-Toolkit2转换时需要指定target_platform如果设置成rk3588的NPU版本不匹配就会出现init失败。解决方法是在转换脚本里指定正确的target_platform并且确保板端的RKNN驱动版本和PC端的RKNN-Toolkit2版本兼容。用rknn-toolkit2的query接口查询板端NPU驱动版本后重新在PC端匹配了对应版本的toolkit问题就解决了。7.2 推理帧率骤降CPU频率和散热限制有一段时间部署完成后白天和晚上的帧率差别特别大。晚上温度低帧率正常白天温度一高帧率就明显下降。查了板子的温度日志发现NPU核心温度到了85摄氏度以上触发了温控降频。解决方法是给板子加了主动散热风扇并在应用层监控温度当温度超过75摄氏度时主动降低垃圾分类任务的分辨率。降分辨率的效果非常明显NPU负载降了差不多15%温度就能稳定在70度左右。改进后昼夜帧率差异基本控制在3FPS以内。7.3 内存泄漏排查经验三期跑下来发现内存占用一直在缓慢增长。用valgrind检测caffe模型推理的部分没发现问题。后来自己写了一个监控脚本定时记录/proc/self/status的VmRSS值发现每次检测到烟火目标并画框后内存会跳增几MB但不会释放回系统。定位了几个画框和字体渲染相关的代码终于找到根源每次调用freetype渲染文字都会realloc一个新的位图buffer但没有及时释放。修复后内存曲线变得平稳跑一整天占用维持在580MB到620MB之间与一开始基本持平。7.4 排查技巧速查表现象可能原因排查方向模型加载失败平台参数不匹配、驱动版本不匹配检查rknn_init错误码核对target_platform推理帧率跳动大NPU被其他任务抢占、散热不足监控NPU占用率和核心温度调整优先级花屏/残影内存地址冲突、RGB顺序错误用memcpy避开DMA buffer检查RGB/BGR内存持续增长动态内存未释放、字体缓存未清理监控VmRSS定位泄漏代码位置首帧黑屏解码器未稳定输出增加解码稳定等待丢掉前几帧8. 从板级到边缘盒子的产品化经验8.1 系统裁剪与服务化部署开发完功能后如果要落地成产品还有一堆工程化的事情要处理。首先是把系统裁剪到最小去掉桌面环境、蓝牙、不必要的网络服务只保留AI推理必需的系统组件。裁剪后的Ubuntu系统内存占用从1.2GB降到了450MB左右给AI推理留出了充足空间。推理服务我用systemd托管配置了开机自启动和自动重启策略。如果推理进程因为异常崩溃systemd会自动拉起保证边缘盒子的长时间稳定运行。日志统一写到syslog通过logrotate定期清理不会把磁盘写满。8.2 模型热更新与远程运维产品落地后模型需要持续优化所以远程升级能力很关键。我设计了一套简单的OTA方案把rknn模型文件打包成tar.gz通过MQTT下发到设备设备下载完后校验MD5然后切换模型文件路径并重启推理服务。这套方案在实验环境里验证了两周升级成功率和稳定性都没问题。需要注意的点是模型升级时要保留上一版本作为回退一旦新版本推理准确率异常可以通过MQTT指令回滚旧版本。实际操作中我把新旧版本放在两个目录软链接指向当前版本切换时改软链接就好非常方便。8.3 功耗和散热的长期验证边缘盒子的设计目标是7x24小时不间断运行所以功耗和散热必须做长期验证。整板功耗在AI推理满载时大约是8到10瓦散热设计需要保证NPU和CPU在连续高负载下温度不超过80度。散热方案上我用铝制外壳加导热硅垫再加一个小风扇做主动散热。实测环境温度35度时外壳表面温度稳定在52度左右NPU核心温度在65到70度之间距离温控降频阈值还有余量。如果环境温度更高可以进一步降低垃圾分类任务的分辨率或者减少目标帧率来降低功耗这是系统调度策略里已经预留的冗余手段。
返回列表