
做边缘视觉盒子的人迟早会碰到这种“既要又要”的诉求一个小区周界要防人员入侵楼栋角落要盯烟火隐患垃圾分类点还要识别混投行为。采购方就给一个设备预算明摆着不想装三块板卡。我当时的判断是与其堆硬件不如把算力利用率卷上去选一块RK3588用它的NPU同时扛住这三个模型。这期就把整个部署链路拆开讲清楚包括为什么选RK3588、模型怎么裁剪、三个任务怎么共享NPU以及量化转换和并发调度里那些文档没写透的坑。这篇文章适合正在做边缘计算盒子、安防一体机或者AIoT设备的工程师也适合刚接触RKNN工具链、想搞懂NPU多模型并发的朋友。如果你手头有一块RK3588开发板照着文中的思路可以把“入侵检测 烟火识别 垃圾分类”三套检测同时跑起来整体帧率控制在可用的范围内。1. 整体设计与思路拆解1.1 为什么是RK3588而不是Jetson或直接上云先回答一个被问得最多的问题既然NPU算力有限为什么不用NVIDIA Jetson Orin Nano或者干脆把所有帧传到云端识别Jetson生态确实成熟CUDA好用但价格和供货在2025年的行情下依然不稳定。Orin Nano 8GB模块的价格足以买两三块RK3588整板遇到项目需要批量出货成本差距会直接决定方案能不能落地。云端推理依赖网络传输一个1080p码流哪怕只抽帧上传一个月流量成本也够呛更别说园区内网不稳定的情况。RK3588的优势在于单芯片集成8核CPU4×A76 4×A55、Mali-G610 GPU、6 TOPS算力的NPUINT8以及强大的视频编解码单元外设接口齐全价格却控制在几百元级别整体BOM成本非常友好。单说NPU算力6 TOPS看着不高。但注意这是三个神经网络在同时跑不是每路视频都跑最大模型。我们用YOLOv8n这一档的轻量模型做检测单个模型在NPU上的推理时间可以压到20~40毫秒三个模型就算最坏情况串行跑也能凑出接近10帧每秒的总吞吐。如果再做多核并行和抽帧调度实际体验会好很多。这个性能对于安防场景完全够用人员入侵不需要30fps那么夸张烟火检测甚至1秒一次都算高频率了。从工程软件栈来看RKNN工具链在2024年后明显成熟了。rknn-toolkit2支持PyTorch、ONNX、TensorFlow等主流通用格式YOLOv8系列官方导出的ONNX可以直接转换。驱动层librknnrt.so提供比较完整的C/C和Python API还支持显式指定NPU核心数来做并行调度这些能力为单板多模型打下了基础。1.2 三个检测任务同时跑的核心矛盾单块NPU上跑三个模型听起来像把三条流水线硬塞进一条巷子里核心矛盾就三个算力怎么分、内存怎么省、调度怎么排。第一是算力分配。RK3588的NPU由3个核心组成官方叫NPU Core0/Core1/Core2。多数模型在转换时默认使用全部核心做单模型加速相当于一条任务独占三车道。但如果三个模型同时要跑就得分车道。好在RKNN在模型转换和运行时都支持core_mask设置我们可以指定A模型用Core0、B模型用Core1、C模型用Core2实现真正意义上的物理并行。难点在于模型转换时就要规划好core分配运行时的调度策略要和转换参数对齐。第二是内存占用。三个模型同时驻留DDR每个YOLOv8n量化后的RKNN模型文件大约16~25MB但推理时的中间特征图和工作缓冲区会远大于文件体积。实测三个模型叠加NPU相关内存占用约600~800MB这在RK3588搭配4GB或8GB内存的配置下是可以接受的但如果还挂着内存盘、跑着数据库服务就该掂量掂量。所以模型设计阶段要做剪枝压缩部署阶段还要关闭不需要的调试打印。第三是调度策略。安防场景的三类任务对帧率要求其实不一样。人员入侵关系安全等级最好每秒跑2~4次烟火检测需要尽早发现但不用连续追踪1秒1次足够垃圾分类属于事件型检测垃圾桶旁边有人且处于投放时间段才需要识别可以设计成“人形触发后再检测”。所以没必要让三个模型抢同一份实时视频流而是各自按节奏从共享帧队列里抽帧。这样整体开销从“3倍全帧率”变成了“入侵全帧率 烟火低频 分类触发式”NPU压力一下子小了很多。2. 核心细节解析与实操要点2.1 模型选型不是精度越高越好是“刚好够用”才好模型选型是很多从服务器端转过来的工程师容易失手的地方。习惯了服务器上跑YOLOv8m、YOLOv8l一上边缘设备就看不上n版本。但RK3588的6 TOPS算力摆在那里选模型的基本原则是在满足业务阈值的前提下把推理时延压到可控范围。人员入侵检测我用YOLOv8n或YOLOv5su只保留person一个类别。输出分辨率640×640输入INT8量化后单次推理在RK3588上大约20~35ms。这里有个细节RKNN版本的YOLO模型输出层建议做裁图优化转换ONNX时把模型后处理中的NMS留在CPU端做NPU只负责特征提取和预测框解码这样NPU耗时能减少约10%。另外入侵检测通常不需要识别具体是谁所以只输出person的置信度和框避免多余类别拖慢分类分支。烟火检测的难点是小目标和形态多样性。火苗可能只有几十个像素烟雾是半透明且没有明显边缘。很多人直接套通用模型效果很差。我建议用专门的火焰烟雾公开数据集从YOLOv8n微调一遍把类别固定为fire和smoke两类。输入分辨率可以保持640但测试时如果场景固定尝试把输入缩放到416或480会显著提升小目标命中率。原因是分辨率降低后小目标虽然更小但模型的感受野和锚点匹配会变得更合适看起来矛盾实测下来往往比强行跑640更好。垃圾分类检测相对简单常见设定是识别瓶子、易拉罐、纸箱、塑料袋、厨余垃圾等几类。可以用轻量分类模型如MobileNetV3-Small也可以继续用YOLOv8n做检测。从工程统一性考虑我建议三个任务如果都走YOLOv8n工具链和推理代码可以复用同一套。垃圾分类模型不追求实时检测它是被入侵检测模型“敲门”后才触发的所以整体算力开销最小。2.2 RKNN-Toolkit2 模型转换的关键操作模型转换是整套部署中最容易出问题的地方90%的“NPU跑不起来”都发生在这一步。基础流程是PyTorch模型导出ONNX - ONNX转RKNN - INT8量化。但实际操作中有很多细节决定成败。先导出ONNX。YOLOv8官方仓库自带export.py直接执行就能导出带NMS的ONNX但那种ONNX里包含大量后处理节点转RKNN时容易失败。正确做法是导出不带NMS的尾椎模型或者手动修改模型结构把Decode分支也尽量简化。推荐直接用ultralytics源码里YOLOv8的self.model.model部分导出确保输出三个feature map的raw张量。导出后用onnxsim简化一次去掉冗余transpose和reshape节点。然后是RKNN转换脚本。我用的是rknn-toolkit2的python接口核心代码如下from rknn.api import RKNN rknn RKNN() rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588 ) if rknn.load_onnx(modelyolov8n_person.onnx) ! 0: raise RuntimeError(load onnx failed) # 加速量化校准数据建议准备300~500张真实场景图 ret rknn.build(do_quantizationTrue, datasetdataset.txt) if ret ! 0: raise RuntimeError(build failed) ret rknn.export_rknn(person.rknn)先说mean和std。上面写法把输入做了0~255归一化即mean0std255这个对齐了YOLOv8官方训练的归一化方式。如果你的模型在训练时用了自定义mean和std转换脚本必须同步修改否则精度会掉得很离谱。dataset.txt里每行一个图片路径这些图片最好从真实部署场景中采集而不是用训练集或网络下那种千篇一律的风景图。量化校准集的分布如果和实际场景偏差太大INT8量化后的精度损失会非常明显。另一个细节是target_platform。不要随手填rk3588要填你板子实际对应的NPU平台。RK3588对应的是rk3588。如果你用的是3588s则对应rk3588s。填错平台虽然也能生成rknn但运行时可能报不匹配错误。量化环节如果INT8精度不够可以先检查是不是校准集太少或背景杂乱。不要把校准图片用来做增强尽量保持raw原图。如果还是不行可以用混合量化只把某些敏感层保留为FP16代价是速度变慢。实测下来YOLOv8n的INT8和FP32在mAP上的差距通常在0.5~2%以内对安防场景完全够用。2.3 多模型并发推理的底层机制和调度策略RK3588的NPU在运行时允许创建多个context每个context加载一个RKNN模型通过core_mask参数指定使用哪些核心。默认值是RKNN_NPU_CORE_AUTO也就是让驱动自动调度所有核心如果要并行跑三个模型就必须显式指定不同核心。我实测过三种调度方式完全串行、核心绑定、混合调度。完全串行最简单三个模型按优先级排队跑同一个core_mask总耗时是三个模型耗时之和比如35322592ms等效帧率约11fps。核心绑定则让三个模型分别跑在Core0、Core1、Core2上耗时不由加法决定而是取决于最慢的那个模型由于NPU核心之间的争抢较小实际总耗时比串行能提升50%以上。关键是API怎么用。创建RKNN对象时模型加载方式不变。推理时指定coreMask代码大致如下# 注意这里用rknn-toolkit-lite2或librknnrt的Python绑定 # rknn init runtime env ret rknn.init_runtime(targetNone, device_idNone) # 推理时指定core mask core_mask 1 # Core0 outputs rknn.inference(inputs[img], data_formatnhwc, core_maskcore_mask)如果用的是C API则调用rknn_run时设置rknn_run_extend结构体里的core_mask字段。需要说明的是RKNN在单模型时默认会尝试利用全部核心如果你不显式指定三个模型共享三个核会导致严重的cache抖动最终性能反而不如各占一核。调度策略上我建议开三个线程每个线程固定处理一个模型的帧队列。帧队列由一个全局的视频采集线程填充采集线程从RTSP或USB摄像头拉流每隔若干毫秒把最新帧放入队列。每个推理线程按自己的节奏消费帧不关心其他线程的进度。这样天然实现了按需抽帧也避免了多线程同时读取摄像头造成的资源浪费。这里要特别提醒一点RKNN的inference接口不是线程安全的。不同线程操作各自独立的RKNN对象是可以并行的但绝对不能让两个线程共用同一个RKNN对象。否则轻则报错重则直接段错误。我刚开始图省事想用锁去串行化同一个模型对象结果NPU并行优势全丢了。正确做法是N个模型就创建N个RKNN实例每个实例绑定各自的core_mask。3. 实操过程与核心环节实现3.1 环境搭建与工具链版本选择环境分两块PC上的模型转换工具链板子上的运行时环境。PC端建议使用x86_64 Ubuntu 20.04或22.04安装rknn-toolkit2对应版本的Python包。官方提供conda环境配置推荐Python 3.8~3.10。安装时注意版本对应关系rknn-toolkit2的版本和RKNN Runtime版本要匹配否则转换出的rknn在板子上可能加载不了。我踩过的坑是用2.0.0版本的转换工具生成rknn板子上的runtime还是1.5.x结果init_runtime直接报找不到对应版本的librknnrt.so。解决方法是保持两边版本一致或者用兼容方式转换时在config里关掉平台校验但那样通常也不靠谱最好还是统一版本。板端环境有两种选择一种是安装rknn-toolkit-lite2可以在板子上直接调用Python接口做推理适合原型验证另一种是交叉编译librknnrt.so后通过C/C API调用适合产品化。我建议原型阶段用librknnrt的C API写一个简单的封装虽然写起来慢但性能和内存控制比Python好很多。Python接口在每次inference时都有额外封装开销对于要求极致吞吐的场景不太合适。拿到开发板后第一步是烧录官方Ubuntu固件或Debian固件然后确认NPU驱动正常工作。可以用设备节点来判断ls /dev/rknpu如果输出中有rknpu相关设备节点说明驱动已经加载。再通过sysfs查看NPU工作状态cat /sys/kernel/debug/rknpu/load这个文件会显示当前NPU的负载百分比。跑推理前这个值应该是0或接近0如果一上电就是100%大概率是残留程序或驱动异常。温度监控同样重要RK3588的NPU长时间工作在80度以上会触发降频导致推理速度突然变慢。用以下命令查看cat /sys/class/thermal/thermal_zone0/temp单位为毫摄氏度。我在项目中设置超过75度就降低推理线程优先级或者临时把烟火检测帧率从每秒1次降到每2秒1次这样可以有效控温。3.2 模型转换与性能基线测试这一步是最枯燥但最关键的。把三个模型的转换脚本统一放一个目录写好日志和校验逻辑。转换完成后先单独加载每个rknn模型用同一张测试图做推理记录推理耗时、内存占用和输出是否正常。我建议每个模型转换后都跑一次性能基线。所谓基线是单模型使用全部NPU核心时的最快推理时间。记录这个值有两个作用一是评估后续并行调度的衰减程度二是确认模型本身是否优化到位。如果基线时间远高于同类模型正常水平优先检查ONNX导出时是否有过多的动态shapeRKNN对动态shape的支持有限动态维度会显著增加推理耗时。以我手上的数据为例YOLOv8n int8640×640单模型全核推理约22ms烟火模型416×416约18ms垃圾分类分类模型用YOLOv8n-cls224×224约6ms。这些数据可以作为参考但不同模型结构会略有差异。实际测试基线时建议每张图跑50次以上取中位数而不是一次跑完就记时间。第一次inference会有模型解析和内核初始化开销往往比后续推理慢很多。用中位数能排除这个冷启动噪声。同时在反复推理后记录内存是否持续上涨如果内存一路涨大概率是推理循环里创建了过多临时对象这在Python接口下尤其常见。3.3 多线程推理框架的代码实现下面给出一套简化但可运行的多线程框架。它分成三个线程采集线程、入侵检测线程、烟火分类调度线程。实际项目我会根据优先级把三个任务拆成三个线程但为了演示调度思路这里用一个管理类来组织。先定义帧队列用Python标准库的queue.Queue即可。采集线程从RTSP拉流不断把最新帧放入队列。如果队列满了说明消费端跑不过来直接丢弃最旧帧保证实时性。import queue import threading import cv2 frame_queue queue.Queue(maxsize1) stop_event threading.Event() def capture_worker(rtsp_url): cap cv2.VideoCapture(rtsp_url, cv2.CAP_FFMPEG) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) while not stop_event.is_set(): ret, frame cap.read() if not ret: continue if frame_queue.full(): try: frame_queue.get_nowait() except queue.Empty: pass frame_queue.put(frame) cap.release()这段代码注意maxsize1故意只保留最新帧。如果推理线程处理速度跟不上就直接跳过旧帧保证拿到的永远是最新画面避免延迟积压。CAP_PROP_BUFFERSIZE设为1也是同样的目的减少FFmpeg内部缓存带来的额外延迟。接下来是推理线程。每个模型一个线程各自持有独立的RKNN对象推理前从帧队列取帧做必要的预处理。class InferThread(threading.Thread): def __init__(self, rknn_model, core_mask, need_nv12False): super().__init__() self.rknn rknn_model self.core_mask core_mask self.daemon True def run(self): while not stop_event.is_set(): try: frame frame_queue.get(timeout0.5) except queue.Empty: continue img cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img cv2.resize(img, (640, 640)) # 进入推理 outputs self.rknn.inference( inputs[img], data_formatnhwc, core_maskself.core_mask ) # 后处理解码框、NMS、业务逻辑 self.post_process(outputs, frame)继续细化在实际项目中不同模型需要不同的预处理参数有人侵模型需要对画面做等比缩放加letterbox烟火模型可能直接resize分类模型还要做中心裁剪。这些细节会影响精度不能一把梭。我建议把每个模型的预处理封装成独立函数代码里通过函数指针注入避免在一个线程里反复判断。3.4 帧率调度与优先级控制纯靠多线程并不能保证系统整体帧率最优。阻塞在NPU上的线程之间没有协作可能导致低优先级任务抢占高优先级任务的带宽。我在项目中引入了一个简单的优先级调度器核心逻辑是入侵检测具有最高优先级只要它有帧要处理NPU资源优先满足烟火检测次之垃圾分类优先级最低只有入侵检测线程检测到人形目标时才会触发。具体实现不需要复杂的中间件用一个aggressive的event标志足够了。入侵线程每完成一次检测如果发现画面中存在person就把一个全局标志置为True垃圾分类线程只有在看到该标志时才消费一帧并执行推理完成后再把标志清零。这样直接避免了垃圾分类模型空转。烟火检测的调度策略则是“固定帧率 失败退避”。固定帧率定为每秒1次。如果连续多次检测都未发现烟火把检测间隔加大到2秒一旦检测到可疑目标立刻把间隔缩小到0.5秒并以告警方式推送结果。这种自适应策略能在不降低安全性的前提下大幅降低NPU的平均负载。实际运行中我用了一个监控定时器统计三个线程的推理次数和平均耗时并计算NPU整体利用率。如果利用率持续超过85%说明配置过于激进需要调低某个低频任务的频率如果低于40%说明还有余量可以把入侵检测的分辨率从640提到768提升小目标召回率。这个经验公式我屡试不爽。4. 常见问题与排查技巧实录4.1 训练和转换阶段的问题速查我整理了一份实际项目中遇到过的问题清单比官方FAQ更贴近业务场景分享出来供参考。现象原因解决方案转换时提示“could not find suitable delayline”ONNX解析失败通常是网络结构有不支持的算子先跑onnxsim简化再逐个定位不支持算子替换为不支持的等效结构板端init_runtime报“load model failed”rknn模型版本和runtime版本不匹配升级或降级板端runtime到转换工具对应版本推理结果全黑或全零输入数据格式或归一化参数不对检查data_format是nhwc还是nchw确认mean/std与训练时一致推理速度忽快忽慢NPU温控降频查thermal_zone温度增加散热片降低推理频率多个模型同时跑时出现段错误多个线程共用了同一个RKNN对象确保每个线程独立持有RKNN实例不要用全局共享对象量化后召回率骤降校准集太少或场景单一增大校准集到300张以上尽量覆盖实际部署场景的亮度、季节、天气这些里面我觉得量化后召回率下降是实际影响最大的一个。很多人训练出来的模型在PC上跑mAP不错一量化就糟糕下意识以为是模型太大、网络复杂、对量化不友好。但很多时候只是校准图的分布根本没覆盖夜晚或背光场景。我在一个园区项目里白天量化校准的模型到了晚上烟火识别几乎失效后来把夜间红外灯开启后的画面加入校准集问题立刻解决。校准集比量化算法本身更影响最终效果很难多强调。4.2 NPU内存与硬件排查经验排查NPU问题不能只盯着算法。硬件层面的坑往往更隐蔽。RK3588的NPU驱动如果和固件不匹配会出现在/dev下看不到rknpu设备或者sysfs入口缺失的情况。这时先检查固件版本再检查内核dmesg里是否有rknpu相关的错误信息dmesg | grep -i npu如果驱动正常但推理过程中系统内存被大量占用可以用以下命令实时观察NPU进程的内存增长watch -n 1 cat /proc/meminfo | grep -E MemFree|MemAvailable同时查看是否有异常的page allocation failure如果有说明物理内存碎片化严重。此时可以把系统交换分区加大或者调整NPU运行时的内存分配策略。RKNN C API里有个接口是修改推理临时缓冲区的分配方式实验时可以先采用一次性分配的方式避免每个推理周期都动态申请内存。另外RK3588的NPU核心在执行不同模型时对DDR带宽的争抢比想象中大。我用四个模型同时跑时即使core_mask各自分离DDR带宽也会成为瓶颈尤其是在输入分辨率都调得比较大的情况下。解决方案是降低高分输入数量或者把模型输入改成半精度的同时缩小输入尺寸。经验值是把三个模型的总输入像素控制在1280×1280以内DDR争抢基本不会影响整体帧率。4.3 部署后的稳定性与长时间运行记录实际部署时盒子7×24小时运行最容易暴露问题是内存泄漏和延迟漂移。我做的第一个版本跑了三天之后内存占用从开机时的800MB涨到了2.1GB最终系统卡死。排查下来是Fireware解码线程每帧都new了一个缓冲区没有释放。解决方式很简单——复用已分配的帧缓冲区减少GC压力。Python端的GIL在这里也有影响。虽然多线程可以并行调用NPU但后处理的NMS部分如果写得不小心会占据大量GIL时间。考虑到推理线程竞争我建议把核心后处理逻辑用Cython或者直接用C扩展实现或者更简单地把后处理移到独立进程中。由于推理数据量不大进程间通信的开销可以接受。长时间运行时我还习惯在每个推理循环里统计耗时并把最近100次的平均帧率周期性写入日志一旦发现连续多帧耗时超过阈值就自动触发保护逻辑比如降低烟火检测的分辨率或暂停垃圾分类任务。这个机制在户外高温环境下尤其重要。夏季午后设备被晒到70度NPU如果还保持全负荷运行很容易进入降频保护导致所有任务同时变慢。有策略地主动降低边缘任务的资源占用反而能保住核心的入侵检测帧率。5. 项目后续可以这样扩展最后再说一个我最近在试的方向把这三个模型合并成一个多任务模型或者至少把人员入侵和烟火检测合并到同一个YOLO模型里让分类模型单独跑。这样在RK3588上可以进一步压缩NPU开销。多任务模型需要自定义数据集和loss结构工作量会翻倍但带来的帧率提升很直接。如果不想动模型结构也可以利用RK3588的RGA硬件做缩放、格式转换。我实测把1080p BGR图像用RGA转成640×640 RGB相比OpenCV版能节省3~5毫秒CPU时间对NPU推理没有直接影响但三个模型都省一次CPU占用就降下来了整体系统更稳。至于推送告警部分别把告警服务放在推理线程里。三个模型都会有结果产出如果直接在推理线程里做HTTP推送网络抖动会反过来拖慢检测。我的做法是每个模型维护一个结果队列结果队列由独立的推送线程消费推理线程只往队列里丢数据不关心网络状态。这样即使公网断掉NPU推理也不会受影响。RK3588这块板子的潜力比账面算力看起来大得多。你不一定非得用它跑超大模型把任务拆小、按需调度、精确分配NPU核心三个模型同时跑完全可行。希望这次分享能让大家少踩几个我在项目里踩过的坑。从我自己实际使用的感受来说最后悔的是前期没重视量化校准集导致烟火模型上线后第一周就误报连连。如果你只能从我这里带走一句话我希望是NPU部署的瓶颈往往不是算力而是对任务节奏和内存调度的理解。后者想通了RK3588能干的事真的比你想象中多。