
这需求看起来简单但真正上手做的时候不少人会卡在“同时跑”这三个字上。原因很简单RK3588的NPU虽然标称6 TOPS算力但它不是一个可以无限并发的大池子它是3个NPU核心加共享内存带宽组成的异构单元。你在板子上同时加载三个模型然后各自开线程去推理结果往往是NPU利用率上不去、CPU被打满、内存带宽爆炸甚至直接报错崩溃。这篇文章记录的是我实际做完一版智慧园区边缘节点的完整思路和踩坑过程在一块RK3588主板上同时跑人员入侵检测、烟火检测、垃圾分类识别三个AI任务。不是拿OpenCV乱开线程那种“能用就行”的做法而是围绕模型转换、NPU调度、内存复用、实测调优这几个层面把算力分配清楚。1. 先摸清NPU的家底6 TOPS算力到底怎么分配才不会打架很多人拿到RK3588的第一反应是把它当成一颗大号GPU或者独立NPU来用理解成“有一个6 TOPS的加速卡随便塞模型”。但实际去看Rockchip公布的NPU架构和TRM手册你会发现它的NPU是三个独立的CORE组成的每个CORE的计算能力大概在2 TOPS左右三核合起来才是6 TOPS。这个结构决定了你在做多模型并发的时候可以选择把不同模型绑到不同的CORE上也可以选择让一个模型独占全部三核。1.1 三核NPU的拓扑与共享资源从硬件拓扑来看RK3588的NPU和CPU、GPU、编解码器都挂在同一套总线体系上它们共享DDR带宽和系统缓存。逻辑上NPU内部有三个CORE但实际上它们之间并没有独立的存储系统全部要通过DMA访问DDR。这意味着一个模型在推理的时候如果输入Tensor特别大、中间结果特别多它会占用掉大量的内存带宽其他模型哪怕是绑在另一个CORE上也可能因为等带宽而被拖慢。举个例子。我最初做的版本是三个模型各绑一个NPU CORE人员检测绑CORE_0烟火检测绑CORE_1垃圾分类绑CORE_2。表面上看资源绝对隔离了但实测帧率比“三个模型顺序跑在同一核上”还差。原因就是三个模型几乎同时发起对DDR的大量读写总线冲突严重单核行推理效率反而下降。后来改成一个时间片轮转的方式反而稳定了。1.2 算力需求评估三个任务不是对等的很多做项目的朋友容易犯一个错误给每个任务分配相同的资源。实际上三个任务的实时性要求和计算量差异非常大任务模型复杂度输入分辨率实时性要求实际需要的帧率人员入侵中/高1280x768高15~25 FPS烟火检测中640x640较低5~10 FPS垃圾分类低640x640低2~5 FPS这个表格不是随口写的而是根据项目需求推导的。人员入侵涉及安全告警帧率太低会导致漏人或者轨迹不连续所以必须保证实时性烟火检测虽然也是告警类但烟火不会在0.1秒内烧起来5~10 FPS足够覆盖垃圾分类更特殊它是静态场景识别一个垃圾丢进桶里你几秒钟内识别出来并提示分类结果就行跑个2~5 FPS完全没有问题。把不同实时性要求的任务放在同一块NPU上正确的思路不是“同时跑”而是“分时复用按需分配”。三个任务的计算量高峰错开比同时抢资源要高效得多。2. 模型选型与RKNN转换量化、尺寸和精度的三重取舍三个任务对模型的要求完全不同。RK3588的NPU对YOLO系列支持得最成熟所以模型层的选型基本可以在YOLOv5、YOLOv6、YOLOv8几个版本之间选。但真正决定能不能流畅跑的是后面的RKNN转换和量化配置。2.1 三路检测的模型选型思路人员入侵检测我选的是YOLOv5s输入分辨率设了1280x768。为什么不用YOLOv8从RKNN部署的成熟度来说YOLOv5依然是最稳的转出来的rknn模型对ONNX算子兼容性最好而且v5的后处理代码在板子上有好几种现成写法不用自己从头造轮子。1280x768是16:9监控画面比较常用的分辨率既保留了小目标的检测能力又不至于像1920x1080那样让NPU吞吐量断崖式下降。烟火检测我用的YOLOv5n输入分辨率640x640。烟火目标的特点是边界模糊、形态不固定火焰的颜色和纹理特征比形状更重要所以我对数据集做了专门增强大量加入暗光环境下的远距离小烟火样本。模型精度要求不能只看mAP更要看误报率——这个任务误报一次安保人员就要白跑一趟。垃圾分类选的是YOLOv5n输入分辨率640x640类别我压缩到12类。这些类别包括塑料瓶、易拉罐、纸箱、报纸、玻璃瓶、电池等常见垃圾。垃圾分类有一个天然优势目标基本是静止或低速运动的而且拍摄距离近、目标大所以模型可以做得非常小。2.2 RKNN转换的版本和工作模式RKNN转换用的是rknn-toolkit2这一点必须强调旧版rknntoolkit只支持瑞芯微上一代芯片RK3399Pro、RK1808等RK3588必须用toolkit2。转换流程上我分成三步第一步把PyTorch模型导出为ONNX注意必须固定batch size为1并且把模型的输入name改成一个方便记忆的名字比如images后续在板子上要用这个名字去填充输入Tensor。第二步用rknn-toolkit2加载ONNX模型设置target_platformrk3588。第三步量化。用的是PTQ训练后量化中的量化感知校准准备500~1000张覆盖不同场景的图片调用quantize接口生成量化表。量化这里有一个很容易被忽视的坑mean_values和std_values要与训练时一致。如果你在PyTorch里用的是Normalize(mean[0,0,0], std[1,1,1])也就是不归一化那RKNN转换时千万不能套用ImageNet的归一化参数。否则模型直接精度崩掉。# rknn-toolkit2 转换核心流程 from rknn.api import RKNN rknn RKNN(verboseTrue) rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], # 如果训练时输入是0~255就直接除255 target_platformrk3588, quantized_dtypeasymmetric_quantized-8 ) rknn.load_onnx(modelyolov5s_1280.onnx) rknn.build(do_quantizationTrue, datasetcalib_dataset.txt) rknn.export_rknn(yolov5s_1280.rknn)2.3 量化精度的验证方法量完之后不要直接上板先在PC上用模拟器或者连板模式跑一遍校验集看mAP掉点情况。我给自己定的标准是量化后mAP相对FP16下降不超过3%。如果超过这个值先排查是不是校准集分布偏差太大再排查是不是某些层对量化特别敏感——这种时候可以在config()里打开optimization_level0对比一下或者针对特定层配置混合量化让敏感层保持FP16。0~255的归一化方式是我在实际项目里试出来最稳定的。很多人喜欢把mean和std设置成ImageNet的值但自训练的三路检测模型它们的输入分布和ImageNet差得很远强行套用只会让量化校准的数值范围不合理。3. 多模型并发调度上下文、NPU核绑定与异步推理模型转好之后就是整个项目最核心的部分怎么让三个rknn模型在板子上一套系统里稳定并发运行。3.1 多上下文还是单上下文RKNN Runtime提供了两种加载模型的方式。一种是单上下文也就是一个rknn_app内只init一个模型另一种是在同一进程内创建多个RKNN对象也就是多上下文。我一开始想的是把三个模型全部init到同一个进程里这样共享内存池理论上资源利用率更高。但实际跑下来发现RKNN Runtime的上下文之间并不是完全线程安全的而且三个模型同时推理时C库内部的NPU请求排队机制会让某些帧等待时间不可控。最后采用的是“多进程多上下文”方案每个模型一个独立进程进程间通过共享内存或者消息队列通信。理由有三个某一模型崩溃不会拖垮整个系统NPU的上下文隔离性更好不容易出现算子冲突三个任务可以独立更新重启做OTA升级的时候不用全停。进程模型下每个进程内部再开两个线程一个负责图像采集和预处理一个负责NPU推理和后处理。线程之间用双缓冲队列衔接避免图像采集等待推理。3.2 NPU核绑定的代码写法rknn-toolkit2/rknn runtime在C接口中提供了rknn_set_core_mask可以指定当前上下文使用哪个NPU核心。Python接口中对应的是rknn_init后的rknn.bind_core之类的方法。// C接口伪码示意 rknn_context ctx; rknn_init(ctx, model_path, 0, 0, NULL); rknn_set_core_mask(ctx, RKNN_NPU_CORE_0); // 绑定到NPU CORE_0实际部署中我是用一个AI_Scheduler模块来管理三个模型的核绑定和推理周期。调度器的逻辑很简单根据各任务配置的目标帧率计算出一个调度时间片然后依次触发各模型的异步推理。# 调度主循环示意 import time tasks [ {name: intrusion, interval: 0.05, core: 0}, {name: fire, interval: 0.15, core: 1}, {name: garbage, interval: 0.30, core: 2}, ] while True: for task in tasks: if time.time() - task[last_run] task[interval]: task[last_run] time.time() submit_inference(task) time.sleep(0.001)这里有一个关键细节核绑定要避免全部绑死。实测发现如果三个模型分别锁死三个CORE一旦某个模型的计算量临时增大比如烟火检测来了一个复杂场景推理时间从30ms变成80ms另外两个模型不能借用多余的核资源整体响应就会变差。所以我的做法是人员入侵默认绑CORE_0烟火检测绑CORE_1垃圾分类自适应也就是不设固定核由runtime自动调度。这样既保证了高优任务的独占性又留出了一定的调度弹性。3.3 异步推理与阻塞陷阱RKNN推理本身是同步阻塞的rknn_run会一直等到NPU计算完才返回。如果你不做任何处理三个模型轮流调用就会出现“主线程序列化等待”的问题人员检测推理50ms烟火推理30ms垃圾分类推理20ms加起来100ms刚好10 FPS。这就是很多人说的“三个模型加起来帧率低得离谱”的根本原因。解法有两种。一种是在C/C层用多线程分别跑三个rknn模型另一种是同一个上下文内使用零拷贝接口加多重请求。无论哪种核心思路都是让NPU不要空等上一帧的后处理还没结束下一帧的输入已经提交进去了。我用的是Python多线程加重试机制。但要注意Python的GIL对rknn_run这种native调用影响有限因为NPU推理期间GIL会被释放。GIL真正影响的是预处理和后处理阶段所以这两个阶段我都用C扩展或者直接改成C写Python只做流程编排。4. 内存带宽才是最大的敌人解码、缩放、Tensor复用的全套优化芯片的NPU再快数据送不进DDR也是白搭。这个项目里最大的瓶颈不是算力而是LPDDR4/LPDDR5的内存带宽。四个核心的硬件单元CPU、GPU、NPU、VPU全部都要访问DDR一旦并发程度上来带宽立刻成为天花板。4.1 视频解码与影像流水线三路检测任务都需要实时视频帧。如果是网络摄像头通常拿到的是RTSP流这时候需要解码。很多人图省事直接调OpenCV的cv2.VideoCapture这会把CPU直接打满。更合理的方式是用RK3588自带的视频硬件解码单元MPP/VPU去解H.264/H.265码流然后通过RGARockchip的2D硬件加速模块做缩放和格式转换。我实际的路数RTSP取流用FFmpeg封装好的MPP硬解接口拿到的是NV12格式的视频帧。NV12经过RGA缩放并转成RGB后直接作为模型输入。这中间完全没有CPU参与像素搬运CPU负载能控制在比较低的水平。4.2 显式控制Tensor内存布局RKNN推理的输入Tensor默认是NHWC布局但很多模型转出来之后要求的输入布局是不同的。模型转换时要注意设置quantized_dtype和输入layout如果layout配错了输入数据在NPU里会被当作错误维度解释推理结果全是垃圾。另外一个容易忽略的是零拷贝接口。rknn-toolkit2在板端runtime中提供了rknn_create_mem和rknn_set_io_mem系列接口可以让你手动分配输入输出的内存避免每次推理时在用户态和内核态之间反复拷贝。rknn_tensor_mem *input_mems[3]; for (int i 0; i 3; i) { input_mems[i] rknn_create_mem(ctx, input_attrs[i].size_with_stride); rknn_set_io_mem(ctx, input_mems[i], input_attrs[i]); }创建好内存之后后续每帧只需要把解码缩放后的RGB数据搬运到这个固定的输入内存块里然后调用rknn_run。输出结果同理直接在创建的输出内存块里读取不要再做一次numpy.copy。4.3 多路视频流与模型缓冲复用如果是单路视频流同时做人员、烟火、垃圾识别那其实不需要开三个RTSP连接。正确做法是解码一次拿到原始帧然后分发给三个模型各自的预处理模块。三个模型需要的分辨率不同就用RGA分别缩放到目标尺寸。如果项目再放大一点比如4路摄像头同时接入每路都要做人员入侵和烟火检测那就不会为每路单独去加载四个模型实例了而是把同一个模型的输入Tensor做成batch。RKNN是支持batch推理的转换时把batch设为4运行时一次性提交4路帧NPU并行计算。实测下来4路640x640的YOLOv5n烟火模型batch推理总耗时比串行4次推理下降40%以上。这一块的优化空间非常大很多人宁可在CPU里跑4次相同的模型也不愿意改成batch白白浪费了NPU的并行能力。5. 实测数据与调优性能参数、风扇控制和那些让人抓狂的报错理论说得再多不如直接看实测数据。下面是我的测试环境RK3588开发板8GB LPDDR4X内存散热装的是主动风扇系统是Ubuntu 22.04rknn-toolkit2版本1.6.0板端runtime使用librknnrt 1.6.0。5.1 三模型并发实测数值指标单独运行三模型并发优化前三模型并发优化后人员入侵 FPS281222烟火检测 FPS351518垃圾分类 FPS422012CPU 整体占用25%78%41%NPU 整体占用30%95%72%DDR 带宽占用12%44%31%主板核心温度516958优化前的数据惨不忍睹问题根源就是我在第1节说的三个模型同时发起推理总线风暴导致每个模型都在互相等待。优化后其实是刻意降低了垃圾分类的推理频率把省下来的带宽让给人员入侵和烟火检测整体系统才算稳定下来。核心温度的数值不要小看。NPU长时间满负荷跑热量散不出去会触发降频一旦降频帧率会进一步恶化形成恶性循环。所以散热设计不是可有可无的直接决定系统能否7x24小时稳定运行。5.2 风扇与温度控制热词里有人搜“rk3588读取风扇转速”、“rk3588 pwm-fan”可见不是只有我一个人在纠结散热。RK3588主板的PWM风扇接口一般支持自动调速内核里有pwm-fan驱动。系统默认的温控策略往往太保守——温度到了55度才开低速65度全速这样NPU跑起来温度波动很大。我直接把调速曲线改成根据NPU的负载动态调整而不是只看CPU温度温度 45℃风扇转速 30% 45℃ ~ 60℃风扇转速 60% 60℃ ~ 70℃风扇转速 85% 温度 70℃风扇全速同时监听风扇转速是否正常如果转速低于预期值系统主动降级垃圾分类的帧率把NPU负载降下来。这套策略跑了两周没出现过热死机。5.3 常见报错与排查记录部署过程中踩过的坑比写业务代码还多。捡几个有代表性的记录在这里。报错一E RKNN: Cannot find suitable delayline。这个报错出现得很诡异同样的rknn模型在PC端模拟器上能跑到60FPS拷贝到板子上却直接初始化失败。排查到最后发现是GPU和NPU打架板子上有个OpenGL的显示程序占用了大量DMA buffer导致NPU申请连续内存失败出现delayline错误。把显示程序退出问题消失。报错二E RKNN: rknn_init fail, ret -7。这个一般是模型版本和runtime库不匹配。用rknn-toolkit2 1.6.0转换的模型板端librknnrt就必须是1.6.0对应的runtime。有人喜欢用pip install rknn-toolkit2的最新版去转换结果模型给到了老版本的板子就报这个错。解决方案很简单转换工具和板端runtime版本严格打齐。报错三miniloader.bin相关。有些用户在rknn-toolkit2的Linux PC上做仿真推理会遇到找不到miniloader.bin的情况。这纯属环境问题检查一下rknn-toolkit2/rknn/api目录下有没有对应的loader文件或者把板子的USB连接状态确认好。RK3588真正量产部署时一定是在板端用librknnrt直接推理PC仿真只是开发阶段的手段不要过度依赖。5.4 从demo到量产系统的几个收尾工作模型跑顺之后离真正的产品还差几步把三个模型的推理结果统一封装成标准事件结构打上时间戳和视频帧ID方便上层业务做联动告警逻辑。比如人员入侵目标框和烟火检测目标框重叠时优先上报烟火告警。日志系统里把每帧的NPU推理耗时记下来方便后期做慢帧分析。如果某一帧推理耗时突然翻倍八成是内存带宽被其他进程抢占了。开机自启动脚本里把CPU governor设为performance避免CPU频率波动影响预处理延迟。NPU本身不依赖CPU频率但图像预处理是CPU做的这块不能省。6. 最后再分享一点体会回看这个项目单块RK3588同时跑三个AI任务技术上完全可行但前提是不要把它当GPU来用也不能拿PC上的那套多线程思维直接搬过来。NPU调度、核绑定、输入尺寸、模型量化、内存复用每一个环节都是独立的坑而且经常互相牵连。改一个模型的分辨率DDR带宽占用立刻变改一下调度周期CPU占用率又上去了跑得正欢的时候一个异常帧传入内存没对齐就直接段错误重启。我的建议是先定好三个任务各自的帧率底线再反推模型大小和分辨率最后才动手写调度逻辑。顺序反了后面优化起来会非常痛苦。RK3588这块芯片的能力上限比很多人想象中要高但只要摸清了它的脾气它是一个非常可靠的多路AI边缘节点方案。