
1. 项目概述与需求拆解1.1 项目标题想解决什么问题先说结论单块 RK3588 的 NPU 同时跑人员入侵检测、烟火检测、垃圾分类这三个模型在算力和工程上完全可行但绝对不是把三个模型文件一股脑塞进去就能跑起来的事。这个项目核心是在一块开发板上完成多路视频流的接入、三个AI模型的并发调度、检测结果的业务联动以及长时间稳定运行的性能调优。先说 RK3588 的算力底子。这块芯片的 NPU 标称 6 TOPS 算力实际可用大概在 3~4 TOPS 左右INT8 量化后。三个模型同时跑听起来好像很紧张但如果你对模型尺寸、输入分辨率、推理帧率做合理分配余量其实比想象中充裕。我见过不少项目把三个模型全用 YOLOv8s 跑 640x640 输入结果帧率掉到个位数这不是 NPU 不行是设计阶段没有做算力规划。再看三个任务的特性差异人员入侵检测是实时性要求最高的任务漏检代价大通常需要跑到 15~25 FPS 以上才能作为安防告警使用烟火检测对实时性要求没那么极端但需要高召回率而且烟火目标往往比较小输入分辨率低了容易漏垃圾分类则完全不一样它通常是人员主动触发的操作比如把垃圾放到识别区再拍照对实时性要求最低但对分类精度要求高而且数据集类别多模型体积自然偏大。这三种任务混在一个系统里最忌讳的就是“一刀切”——统一用一个大模型、统一分辨率、统一帧率。实际上应该各自独立设计再在调度层面统一管理。1.2 适合谁来参考这个方案适合正在做 RK3588 边缘计算盒子、智慧安防一体机、社区/园区综合巡检设备的同学。如果只是想在 RK3588 上跑通单个模型这篇文章里模型转换部分对你有用如果你要做的项目本身就是多模型并发那从第 2 章开始到第 5 章的内容都可以直接抄作业。我假定你已经做过 RK3588 的基础环境搭建烧录、连接、跑通 RKNN demo如果还没到这一步建议先去把官方的 yolov8 示例跑一遍再回来。2. 整体设计与方案选型思路2.1 模型选型策略三个模型不能一个套路在 RK3588 上部署多模型第一原则是按任务特性选模型而不是按统一标准选模型。这是整个项目最关键的决策点直接决定后面所有的性能表现。人员入侵检测我建议用YOLOv8n 或 YOLOv8s输入分辨率 640x640。YOLOv8n 量化后单帧推理耗时大约 25~35msYOLOv8s 大约 45~60ms。如果只有一个摄像头接入YOLOv8s 完全够用如果同时接 3 路以上的视频流建议上 YOLOv8n牺牲一点 mAP 换并发能力。烟火检测这个任务的关键不在模型大小而在输入分辨率和训练数据。烟火目标在远距离监控画面里经常只占几十个像素用 640x640 输入很容易漏检。建议用YOLOv8s输入分辨率提高到 960x960 或 1280x1280。代价是推理耗时翻倍但烟火检测对帧率要求低5~10 FPS 足够所以算力上是划算的。垃圾分类这里有个路线选择用目标检测模型直接检垃圾类别还是先用检测模型定位再裁剪分类。我的经验是不要用检测模型直接分类因为垃圾分类数据集类别太多常见的有 40~100 类检测模型在边缘设备上扛不住这么多类的复杂度和算力消耗。更稳妥的做法是用一个小型检测模型YOLOv8n负责定位垃圾位置再配合一个轻量分类模型MobileNetV3 或 EfficientNet-Lite做类别判断。这样两个模型都很小但效果反而更好。如果一定只用一个模型那也得用带分类头的检测模型且类别控制在 20 类以内。2.2 算力分配6 TOPS 到底够不够我们来算一笔账。RK3588 NPU 标称 6 TOPS实际连续运行时候的稳定算力大约在 3.5~4.5 TOPS 之间取决于散热条件和 NPU 频率设置后面第 5 章会细说。以 INT8 量化为基准几个常用配置的单帧耗时参考如下实测值散热良好、NPU 1.0GHz 频率下模型配置输入分辨率量化类型单帧推理耗时等效 FPSYOLOv8n640x640INT8约 28ms约 35YOLOv8s640x640INT8约 52ms约 19YOLOv8s960x960INT8约 105ms约 9.5MobileNetV3-Large224x224INT8约 6ms约 160我们按人员入侵 20 FPS、烟火检测 8 FPS、垃圾分类 2 FPS 的目标来规划人员入侵YOLOv8n640x640目标 20 FPS → 单帧 50ms 以内NPU 占用约 50%烟火检测YOLOv8s960x960目标 8 FPS → 单帧约 105ms按 8 FPS 算 NPU 占用约 84%垃圾分类MobileNetV3224x224目标 2 FPS → NPU 占用约 2%等一下这里加起来已经超过 100% 了。所以我说“算力分配”是关键不是随便选完模型就完事。实际上烟火检测不需要一直全速跑。烟火特征是持续存在的火苗、烟雾会在画面里停留几秒甚至更久你可以用 3 FPS 的间隔去检测有疑似目标再提升到 8 FPS 连续确认。同时垃圾分类是事件触发式的平时完全不做推理只有人员触发识别时才启动。这样平均 NPU 占用可以控制在 60~70% 以内。2.3 多模型并发执行架构NPU 怎么同时跑三个模型很多人以为 RK3588 的 NPU 要同时跑多个模型需要像 GPU 一样做 CUDA Stream 并行。实际上 RK3588 的 NPU 是时分复用架构虽然在驱动层面支持多个上下文Context交替执行但本质上还是一个引擎在排队干活。真正能并行的硬件资源是三个独立的 NPU 核心RK3588 NPU 内部有 3 个 coreRKNN Toolkit 会自动把一个模型的计算图切分到多个核上执行但对于多个模型同时推理不同模型会被分配到不同的核上这个由驱动调度。实操上我建议给每个模型单独创建一个 RKNN 上下文rknn.Context每个上下文一个线程让驱动自己调度。这种方式最简单也最稳定。不要试图自己手动绑核或做复杂的流水线调度RKNN 的驱动调度已经很成熟手动干预反而容易出问题。多线程框架如下线程 A视频采集 人员入侵推理线程 B视频采集 烟火推理可单独用一个 RTSP 流或和 A 共用一路流但不同帧线程 C垃圾分类推理事件驱动只在触发时工作三个线程之间没有共享数据互不阻塞。共享的是硬件资源驱动层面会做排队调度。实测下来这种方式比“共用上下文串行推理”的吞吐量要高得多。2.4 模型转换流程ONNX 到 RKNN不管用什么框架训练的模型上板子之前都要转成 RKNN 格式。推荐用 RKNN-Toolkit2 完成转换流程固定加载 ONNX 模型 → 量化 → 导出 RKNN。安装环境我用的是 Docker 方式避免污染宿主机 Python 环境# 拉取 RKNN2 的 docker 镜像 docker pull rknn-toolkit2:latest # 宿主机挂载模型目录后进入容器 docker run -it -v $(pwd):/workspace rknn-toolkit2:latest /bin/bash进入容器后写一个转换脚本下面是我在项目里实际用过的版本以 YOLOv8s 烟火模型为例from rknn.api import RKNN rknn RKNN() # 配置量化相关参数 rknn.config( mean_values[[0, 0, 0]], # 和训练时保持一致 std_values[[255, 255, 255]], # 归一化到 [0,1] target_platformrk3588, # 目标平台 quantized_dtypew8a8, # 权重和激活都量化成 INT8 quantized_algorithmnormal, # 量化算法用 normal更快 optimization_level3 # 打开全部优化 ) # 加载 ONNX 模型 ret rknn.load_onnx(modelyolov8s_smoke.onnx) if ret ! 0: print(load onnx failed) exit(1) # 执行量化校准需要准备校准集图像列表 ret rknn.build(do_quantizationTrue, datasetdataset.txt) if ret ! 0: print(build failed) exit(1) # 导出 RKNN 模型 ret rknn.export_rknn(yolov8s_smoke.rknn) if ret ! 0: print(export failed) exit(1) print(convert success)这里面有几个关键细节值得说。dataset.txt文件里每行是一张用于量化校准的图片路径。这些图片不要从训练集里随机抽我踩过最大的坑就是量化后的模型精度暴跌最后发现是校准集图片选得不对。正确做法是从真实业务场景里抽 100~300 张有代表性的图覆盖各种光照、角度、目标大小、复杂背景。量化校准的本质是统计激活值的分布范围如果你的校准图分布和真实场景差异太大量化后的精度会出问题。quantized_dtype我用了w8a8这个参数是 RKNN-Toolkit2 新版本支持的权重和激活都做 INT8 量化。如果你对精度要求特别敏感比如烟火检测本来就难可以尝试w8a16——权重 INT8激活保留 INT16。代价是推理速度会慢 15~25%内存用量也涨一些但精度恢复明显。实测烟火检测模型用w8a16后 mAP0.5 比w8a8高 2.5 个百分点左右。optimization_level3是默认开启全套优化一般不用动。转换完的.rknn文件拷到板子上注意 RKNN-Toolkit2 转换出来的模型版本必须和板子上的rknn-toolkit-lite2对应版本一致版本不匹配会直接加载失败或推理结果全乱。2.5 YOLO 模型后处理RKNN 输出的特殊处理转换完 RKNN 模型后有个很多新手都会忽略的坑RKNN 的 YOLO 模型输出格式和 PyTorch 原始输出不完全一样。如果你直接在板子上用原始 YOLO 后处理代码去解析输出大概率什么都检不出来。RKNN 在转换时会把 YOLO 的检测头输出重排成三个尺度的特征图通常按 stride 从大到小排列各个输出层的 shape 是[1, num_anchors, num_classes4, H, W]或者[1, num_boxes, num_classes4]具体取决于 RKNN-Toolkit2 的版本和你是否开启了 RKNN 的 YOLO 优化。更稳的处理方式是用官方 RKNN Model Zoo 里提供的 YOLO 后处理代码它已经处理好了 RKNN 输出的排布差异。如果你的模型输出布局确实不符合可以在转换时通过rknn.config里的outputs参数手动指定输出节点或者关闭 YOLO 优化让输出保持原始布局rknn.config( # ... 其他配置 custom_rgb2grayTrue, # YOLO 相关配置 targets_without_sigmoidFalse, # 是否已在模型内做 sigmoid )targets_without_sigmoid这个参数尤其重要。有些版本的 YOLOv8 ONNX 导出会把 sigmoid 融合进前面的卷积层有些不会。如果你在 ONNX 里已经带了 sigmoid但 RKNN 配置里还按需要做 sigmoid 来解析结果就会重复计算导致置信度全部异常。实操时最简单的方式是导出 ONNX 时加opset12且建议simplifyTrue转换后先用单张图在板子上跑一遍测试代码确认输出的置信度范围在 0~1 之间再进入正轨。3. 板端部署与多线程调度实现3.1 开发环境准备与依赖安装板子端需要装的依赖不多核心就是rknn-toolkit-lite2和 Python 的推理绑定库。推荐直接用 Python 调 RKNN 接口开发效率高性能损失可以忽略推理本身的耗时占大头Python 调用开销占比很小。# 进入板端后执行 pip install rknn-toolkit-lite2 pip install numpy opencv-python如果发现 pip 装不上去 RK 官方仓库下载对应芯片版本的.whl文件安装。系统镜像建议用官方的 Ubuntu 20.04 或 Debian 11 版本自带 NPU 驱动不需要额外装。这里有一个很多人没注意到的性能点RK3588 的 CPU 默认可能工作在低频率或者保守调频策略下。如果板子的 CPU 都在powersave模式下跑视频解码、预处理这些 CPU 密集操作会拖后腿。建议把 CPU 调频策略改成performance或schedutil# 检查当前调频策略 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 临时切换为 performance echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor # 永久生效可以写进 rc.local 或 systemd 服务实测在powersave和performance两种模式下多路 RTSP 视频解码的延迟差异可以达到 30% 以上。这个优化往往被忽视但对整体系统吞吐量的影响很大。3.2 多线程推理代码骨架直接可用的参考实现下面我给出一个可以直接跑起来的 Python 多线程部署框架你把它按自己的业务逻辑改一改就能用。import threading import queue import time import cv2 import numpy as np from rknnlite.api import RKNNLite class InferenceThread(threading.Thread): def __init__(self, rknn_model_path, input_queue, output_queue, conf_thres0.5, nms_thres0.45, max_fps20): super().__init__() self.model_path rknn_model_path self.input_queue input_queue self.output_queue output_queue self.conf_thres conf_thres self.nms_thres nms_thres self.max_fps max_fps self.rknn None self.stop_flag False def run(self): self.rknn RKNNLite() # load_rknn 后面要加 per_obtain 参数多模型共享 NPU 时需要 ret self.rknn.load_rknn(pathself.model_path) if ret ! 0: print(f[{self.model_path}] load rknn failed) return ret self.rknn.init_runtime(core_maskRKNNLite.NPU_CORE_AUTO) if ret ! 0: print(f[{self.model_path}] init runtime failed) return frame_interval 1.0 / self.max_fps while not self.stop_flag: start_time time.time() try: frame self.input_queue.get(timeout0.5) except queue.Empty: continue # 预处理resize 到模型输入尺寸 归一化 input_data self.preprocess(frame) # NPU 推理 outputs self.rknn.inference(inputs[input_data]) # 后处理NMS 坐标解析 detections self.postprocess(outputs) # 把结果送到业务线程 self.output_queue.put(detections) elapsed time.time() - start_time sleep_time frame_interval - elapsed if sleep_time 0: time.sleep(sleep_time) self.rknn.release() def stop(self): self.stop_flag True def preprocess(self, frame): # 实际使用时按模型输入尺寸和预处理要求实现 img cv2.resize(frame, (640, 640)) img img.astype(np.float32) / 255.0 # RKNN Lite 默认输入格式是 NCHW img np.transpose(img, (2, 0, 1)) img np.expand_dims(img, axis0) return img def postprocess(self, outputs): # 这部分要按模型输出格式实现 # 建议复用 rknn_model_zoo 里 YOLO 的后处理代码 pass在main函数里把三个模型分别实例化def main(): # 三个推理线程各自独立的队列 person_queue_in queue.Queue(maxsize5) person_queue_out queue.Queue() smoke_queue_in queue.Queue(maxsize5) smoke_queue_out queue.Queue() trash_queue_in queue.Queue(maxsize2) trash_queue_out queue.Queue() person_thread InferenceThread( yolov8n_person.rknn, person_queue_in, person_queue_out, conf_thres0.6, max_fps20 ) smoke_thread InferenceThread( yolov8s_smoke.rknn, smoke_queue_in, smoke_queue_out, conf_thres0.35, max_fps8 ) trash_thread InferenceThread( mobilenetv3_trash.rknn, trash_queue_in, trash_queue_out, max_fps2 ) person_thread.start() smoke_thread.start() trash_thread.start() # 视频流采集线程可以这样做 def capture_loop(cap, q): while True: ret, frame cap.read() if not ret: continue if q.full(): # 队列已满时丢帧保证实时性 try: q.get_nowait() except queue.Empty: pass q.put(frame) # 拿摄像头/RTSP 流初始化 cap_person cv2.VideoCapture(rtsp://your_person_stream) t1 threading.Thread(targetcapture_loop, args(cap_person, person_queue_in), daemonTrue) t1.start() # smoke 和 trash 的视频源类似不重复写 # 业务处理主循环读取三个输出队列做告警联动 while True: if not person_queue_out.empty(): dets person_queue_out.get() # 有人入侵则联动告警 if not smoke_queue_out.empty(): dets smoke_queue_out.get() # 有烟火则联动告警 if not trash_queue_out.empty(): dets trash_queue_out.get() # 垃圾分类结果回调 time.sleep(0.01)3.3 队列设计原则满则丢帧不要积压上面的代码里我对输入队列设置了maxsize这是一个非常重要的设计决策。在边缘设备上做多路视频AI最忌讳的就是队列塞满导致延迟无限增大。假设人员入侵线程处理不过来视频帧在队列里排队等排到的时候画面已经是好几秒前的了这个延迟对安防告警来说是致命的。所以队列策略应该是宁丢帧不积压。每路视频队列只保留最近的 1~3 帧满了就丢弃最老的帧保证推理线程拿到的永远是最新的画面。这个策略在计算机视觉里叫“最新帧优先”在智能安防项目里几乎是标配。3.4 NPU 核心分配切还是自动RKNNLite 的init_runtime接口有个core_mask参数可选NPU_CORE_0,NPU_CORE_1,NPU_CORE_2分别只用一个核或NPU_CORE_AUTO三个核自动调度。我实测的情况是在不同线程里各自设置NPU_CORE_AUTO时驱动会尽量让不同模型跑在不同的核上基本能达到接近硬件并行的效果。但是如果你对实时性有更严格的要求可以手动指定人员入侵用NPU_CORE_0单独占一个核不受其他模型干扰烟火检测用NPU_CORE_1 NPU_CORE_2AI 自动调度这两个核垃圾分类用NPU_CORE_AUTO反正触发频率低这样做的好处是最高优先级的任务人员入侵永远不会被烟火模型的长耗时推理拖住延迟更可预测。我试过单独给入侵检测绑一个核P99 延迟从 60ms 降到 35ms 左右比完全自动模式稳定得多。缺点是只绑一个核时单个模型的推理耗时可能比三个核自动跑慢 20~40%。但因为人员入侵模型本身很小YOLOv8n单核也跑得动所以换成延迟优势是划算的。代码写法# 人员入侵单独占用核心 0 ret self.rknn.init_runtime(core_maskRKNNLite.NPU_CORE_0) # 烟火检测占用核心 1 和 2 ret self.rknn.init_runtime(core_maskRKNNLite.NPU_CORE_1 | RKNNLite.NPU_CORE_2)注意是init_runtime里配置不是在模型加载时配置。三个线程如果用的是同一个RKNNLite实例“同时”推理时会阻塞等待所以必须每个线程各持有一个RKNNLite实例每个实例加载同一个.rknn文件也不会浪费内存模型权重在内存中是共享的。3.5 视频流接入与硬件解码如果只有一个 USB 摄像头接进来直接用 OpenCV 的VideoCapture就够了。但实际项目里更常见的是接多个 RTSP 网络摄像头。这里有一个性能坑OpenCV 的 RTSP 拉流默认走软件解码CPU 占用很高。三路 1080P 的 RTSP 流同时软解CPU 可能已经跑满 60% 了留给 AI 检测的 CPU 资源就紧张了。更好的方案是复用 RK3588 的硬件解码器Mpp。RK3588 的 VPU 支持 H264/H265 硬解CPU 占用可以忽略。但硬解接入有两个麻烦第一OpenCV 默认不支持走 Mpp 硬解拉流第二GStreamer 配置麻烦。实际项目里我优先用带 h264 硬解的 GStreamer 管道来接 RTSPcap cv2.VideoCapture( rtspsrc locationrtsp://your_stream latency0 ! rtph264depay ! h264parse ! mppvideodec ! videoconvert ! video/x-raw,formatBGR ! appsink )核心是mppvideodec这个 GStreamer 插件由 Rockchip 提供走的是 Mpp 硬件解码。实测三路 1080P 拉流硬解CPU 占用率只有 5~8%几乎可以忽略。如果摄像头输出的是 H265把rtph264depay换成rtph265depay即可。延迟方面给rtspsrc加上latency0会把延迟压到最低适用于局域网摄像头。3.6 业务联动的设计告警频率抑制与置信度策略三个模型并跑业务联动逻辑也要提前设计好不然检测结果出来之后现场管理会一塌糊涂。人员入侵告警最怕的是误报和重复告警。一个目标在画面里站了 10 秒如果你每帧都触发告警后端会收到几百条重复事件。我一般用两个手段抑制一是置信度阈值拉高一些0.6 左右二是加一个“持续确认”机制——连续 3 帧都检测到入侵目标才触发告警只要中间有一帧没有目标就重置计数。if dets: person_hit_count 1 else: person_hit_count 0 if person_hit_count 3 and not alert_sent: trigger_person_alert() # 发送告警 alert_sent True烟火检测则是倒过来最怕漏报。置信度阈值要低一些0.3 左右而且建议做“跨帧确认”——单帧检测到有火苗可能不准但 3 秒内连续多帧都检测到疑似目标基本可以确认真实烟火。因为烟火是持续存在的连续确认不会造成多大延迟但能大幅降低误报率。垃圾分类的联动取决于场景。如果是社区垃圾房的识别亭一般是“人员靠近垃圾箱 → 触发拍照 → 启动分类模型 → 输出投放建议”。这种业务逻辑是事件驱动推理线程平时不发数据只有收到触发信号才从队列里取最新一帧做推理。4. 性能调优与精度损失控制4.1 实测性能数字记住这些参考值我以手里的 RK3588 板子8GB 内存版本官方散热风扇室温 25℃跑过的实际数据为参考给你一组可预期的数字模型输入尺寸量化单帧耗时单线程极限 FPSYOLOv8n人员入侵640x640INT827~32ms约 32YOLOv8s烟火960x960INT8100~115ms约 9YOLOv8s烟火960x960INT16w8a16125~140ms约 7.5MobileNetV3-Large分类224x224INT85~7ms约 150三个模型同时启动且人员入侵 20 FPS、烟火 8 FPS、垃圾分类 2 FPS 一起跑NPU 占用大概在 60~80%CPU 占用 30~50%内存整体占用 3~4GB。对于 RK3588 8GB 版本这个余量是健康的。如果你想在同样配置下把三路视频流都接满建议把人员入侵模型换成 YOLOv8n 且只绑一个 NPU 核把另外两个核分给烟火检测这样整体调度更稳。4.2 量化精度损失的排查思路很多同学做完 INT8 量化后发现模型在板子上的检测效果明显变差常见表现是小目标检测不到、置信度整体偏低、边界框偏移。按这个顺序排查第一量化校准集。这是最常见的原因。校准集必须是真实场景图而且要和运行时输入分布一致。如果你用训练集那批纯目标大特写去做校准上板子跑真实监控画面大概率出问题。我用过一个土办法先在板子上用原始 ONNX 模型非量化跑一段真实视频截取 200 帧代表性画面作为校准集效果比随机抽训练集好很多。第二输入预处理一致性。RKNN 的mean_values和std_values必须与你训练时的一致。如果你训练时用normalize0/255即直接除以 255那 RKNN 里就写std_values[[255,255,255]]且mean_values[[0,0,0]]。如果你训练时用了 ImageNet 的 mean/std那 RKNN 也要对应写 sklearn 风格的值而且要注意是按通道归一化还是全局归一化搞错了检测效果直接崩。第三通道顺序。RKNN 默认输入是 NHWC 布局height, width, channel但模型内部可能期望 RGB 或 BGR。OpenCV 读进来的图是 BGR如果你的模型训练时用的是 RGB你必须在预处理阶段做cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)否则模型会严重误检。这个坑我至少碰见过 3 次每次都是排查半天才想起来是通道顺序问题。第四conf_thres 缩放。量化模型输出的置信度分布和原始浮点模型有差异整体会偏低一些。你原来在浮点模型上用的 0.5 阈值量化后直接套用可能一个框都不出。建议在板子上先用多张测试图打印出原始置信度分布再依据分布调整阈值通常量化后阈值要下调 0.05~0.15。4.3 温控与 NPU 降频不重视会翻车RK3588 的 NPU 满载连续跑 10 分钟以上芯片温度能到 75~85℃。如果板子散热不佳比如装在密闭的铝壳里但没有导热垫温度超过 85℃ 左右系统会自动降频保护NPU 性能和 CPU 性能断崖式下跌。所以做长时间运行的部署方案散热设计必须提前考虑。至少要做到芯片表面加散热片 导热硅脂机箱有通风孔或装风扇软件层面监控/sys/class/thermal/thermal_zone0/temp温度超过阈值时主动降负载也可以把 NPU 频率从默认的1.0GHz调到0.8GHz来降低功耗和发热性能损失大约 15%但温度能压下去 5~10℃。实测 0.8GHz 下三个模型并跑性能依然够用系统更稳定。# 查看 NPU 频率 cat /sys/kernel/debug/rknpu/freq # 临时切换 NPU 频率 echo 800000000 /sys/kernel/debug/rknpu/freq如果/sys/kernel/debug/rknpu不存在先挂载 debugfsmount -t debugfs none /sys/kernel/debug4.4 内存占用控制RK3588 的内存有 8GB 和 16GB 版本建议用 8GB 以上的因为三个模型 多路视频帧 系统本身很容易吃掉 4~5GB。几个省内存的技巧Python 多线程之间共享视频帧时用引用传递即可别copy这个开销很惊人队列最大长度限制在合理范围别让视频帧在队列里堆积检测结果里不需要的字段比如所有类别全量的 box及时删除只保留要用的如果内存紧张垃圾分类模型可以不用常驻内存第一次触发时再加载不过首次加载要 1~2 秒看场景能不能接受4.5 日志与运行状态监控多模型长期跑的项目一定要做运行状态监控不然问题出现了都不知道是哪一环出的。我习惯用一个独立线程周期性比如 10 秒一次采集系统状态def monitor_loop(): while True: # CPU 占用 cpu_percent get_cpu_usage() # 内存占用 mem_percent get_memory_usage() # 芯片温度 temp read_temp() # NPU 频率 npu_freq read_npu_freq() # 各线程的实时帧率可以用帧计数差值算 person_fps get_fps(person_queue_out) smoke_fps get_fps(smoke_queue_out) log_json json.dumps({ cpu: cpu_percent, mem: mem_percent, temp: temp, npu_freq: npu_freq, person_fps: person_fps, smoke_fps: smoke_fps }) write_log(log_json) time.sleep(10)这些监控数据不仅用来观测还可以做成简单的告警CPU 持续 90% 以上、温度超过 80℃、某个模型连续 30 秒没有输出——都说明系统异常需要提前介入调整。5. 常见问题与排查技巧实录5.1 模型加载失败或推理结果全乱这个问题的原因大概率是RKNN 工具链版本不一致。现象原因解决load_rknn返回错误PC 端转换用的 RKNN-Toolkit2 版本和板端 RKNNLite 版本不匹配尽量用同一版本的工具链重转模型inference返回的 shape 不对转换时定义的输出节点数量/顺序和预期不符在转换脚本里指定outputs参数或在板上打印 outputs shape 逐一核对检测结果全部为 0预处理通道顺序错误或归一化参数不对检查 RGB/BGR、mean/std、数值范围检测框位置偏移输入 resize 方式不一致letterbox vs 直接拉伸后处理时按和预处理相同的坐标变换逆向映射尤其是最后一行坐标偏移的问题很隐蔽。如果训练前处理用了 letterbox保持宽高比缩放填充而板端预处理直接cv2.resize拉伸到 640x640模型推理出的坐标肯定对不上。解决办法有两个要么板端也实现一套 letterbox 预处理要么在转换 ONNX 时就把 letterbox 的逻辑固化进计算图。我建议后者——用比较简单的做法在导出 ONNX 前写一个带 letterbox 的 wrapper 模块把resize pad作为模型的第一层写进图里。这样板端只需要喂原始帧进去后处理坐标也不需要额外变换省了一笔麻烦。5.2 多线程下推理被阻塞为什么总有一个模型特别慢现象描述三个模型分线程跑发现人员入侵偶尔卡顿帧率忽高忽低。这是很正常的情况。RK3588 的 NPU 硬件层面是三个核不假但驱动调度粒度是“一个模型的推理请求”如果烟火模型用了全核自动模式一次inference就占满三个核运行 100ms 期间其他模型的请求只能在队列里等着。解决方式就是我前面说的给高优先级的任务单独分配核心。如果必须全部用NPU_CORE_AUTO就要接受吞吐量互相挤压的现实这也是正常的多租户资源竞争。另一种特殊情况如果你的某个模型内部算子特别小众比如用了自定义上采样、某些特殊的注意力模块RKNN 编译器编译不了运行时可能回退到 CPU 计算RKNN 支持混合执行。CPU 推理会抢占系统资源而且非常慢。遇到这种情况要检查编译日志里有没有fallback to CPU之类的警告有则说明模型的某些算子没被 NPU 支持需要修改模型结构或者算子实现。5.3 垃圾分类小模型单独跑没问题三个一起跑就掉精度这个现象非常有意思很多人会误以为是量化导致的精度下降实际上是CPU 资源竞争导致预处理/后处理延迟抖动。垃圾分类识别流程里检测模型的预处理和后处理都有大量直接内存拷贝和浮点运算比如做透视矫正、裁剪、resize、归一化。当 CPU 被视频解码和其他线程抢占时这些操作的耗时会出现明显抖动用户体验就是“识别慢”或“偶尔识别结果不对”。对策是把耗 CPU 的预处理/后处理尽量放到推理线程里利用队列天然串行执行同时把视频解码尽量压到硬件 Mpp 上前面说的 GStreamer 方案把 CPU 让给 AI 全链路。5.4 RTSP 流断线重连长期运行的必修课网络摄像头没断过线的基本不存在长期稳定运行必须处理 RTSP 断线重连。OpenCV 的VideoCapture一旦断开不会自动恢复要么手动重连要么整个线程重启。我常用的重连策略是采集线程检测到cap.read()返回 False就关闭当前 VideoCapture等待 2 秒后重新连接。重连最长尝试 5 次如果还是失败就发告警通知运维避免线程卡死。def capture_loop(url, q, stop_event): while not stop_event.is_set(): cap cv2.VideoCapture(url) if not cap.isOpened(): time.sleep(3) continue while not stop_event.is_set(): ret, frame cap.read() if not ret: break # 丢帧策略同上 ... cap.release() time.sleep(2) # 延后重连另外记得在重连时清空旧的队列不然旧视频帧会和新视频帧混在一起导致推理线程处理的是过期画面。5.5 RKNN 版本升级带来的输出布局不兼容RKNN 工具链升级频率不低而且偶尔会调整输出 tensor 的排布方式。如果项目跑得好好的某天你把板端运行库升级了模型输出格式变了后处理解析全乱了不要奇怪。建议板端运行库版本一旦稳定就不要随意升级。PC 端转换模型用的 RKNN-Toolkit2 版本要固定记录在项目文档里每次重新转模型都用同一个版本。团队协作时最好在容器里固化工具链版本避免每个人本地的版本不一样转出的模型行为不一致。5.6 /sys/kernel/debug/rknpu 不可写NPU 频率调不了我看到网上不少人反映 RK3588 板子上设置 NPU 频率时报权限错误或路径不存在。原因通常有两个一是 hexo 版本的内核没启用 debugfs或者路径不对。先检查 debugfs 有没有挂载mount -t debugfs none /sys/kernel/debug ls /sys/kernel/debug/rknpu/二是部分板商的内核里没开 NPU 调频的 debug 接口那就只能去改设备树或者在驱动层配置对应用开发来说成本太高。这种情况下我建议放弃手动调频靠良好散热让系统自动跑默认频率就好。补充一点小技巧如果你想让 NPU 频率开机自动设为 800MHz可以写一个 systemd 服务开机后延时执行频率设置命令。实测 800MHz 对于这个三模型的负载来说已经够用而且温度和功耗表现更好适合日夜不间断运行的边缘盒子。6. 我踩过的一些坑与最后心得做 RK3588 多模型并发部署这个项目最让我意外的不是 NPU 算力本身而是整个系统的认知门槛其实在“调度”而不是“推理”——算力分配、线程模型、队列策略、散热管理、版本一致性随便一个环节掉链子整个系统就崩给你看。几个印象深刻的经验模型体积不是问题看着三个模型加起来一两百 MB但 RKNN 的模型权重在内存里是可以共享的实际内存增量比想象中小。真正吃内存的是多路视频帧的拷贝这个才是大头。给高优先级任务分配专用 NPU 核换来的是延迟稳定性的大幅提升。如果只追求吞吐量全自动模式更简单但延迟会抖动得很厉害。取舍标准只有一个你的业务需要可预见的延迟还是极致的吞吐。校准集的质量永远比数量重要。100 张分布得当的真实场景图比 1000 张训练集里的废图效果好得多。这个经验对任何 RKNN 量化模型都适用不只是这三个任务。最后再分享一个调试小技巧板子上留一个单独的“省电模式”运行参数平时开发调试用全速模式现场长期运行切到降频模式。这样既能保证开发效率又能保证设备长期稳定不降频。这个方案后续还可以按需求扩展更多模型只要遵循“按任务特性选模型、按优先级分配核心、队列满则丢帧、监控温度防降频”这几条原则5 个、6 个模型并跑的原理都是一样的。