RK3588 NPU多模型并发部署:同一芯片跑三个AI模型的优化实践 手里有一块 RK3588 开发板想把“人员入侵”、“烟火检测”、“垃圾分类”三个模型同时部署到 NPU 上跑听着就头大毕竟这块板子的 NPU 算力是 6 TOPS说多不多说少不少怎么安排才能不打架、还能保证实时性这是有讲究的。先说结论单块 RK3588 的 NPU 完全能扛得住这三个任务关键是模型选型和调度策略。我前前后后调了一两周踩了不少坑把硬件加速、多模型并发、内存复用这些环节理清楚之后三个模型同时跑并不拥挤最多可以做到入侵检测 20ms 一帧、烟火检测 25ms 一帧、垃圾分类 30ms 一帧全部跑在 NPU 上CPU 占用也就百分之十几。这篇文章就是把这套方案从零到一完整拆给你看包括模型转换、NPU 上下文管理、量化踩坑、常见掉帧和报错排查直接抄作业就行。1. 整体设计与可行性拆解1.1 为什么单块 RK3588 NPU 能扛住三个模型很多人一听“三个 AI 模型同时跑”就觉得要上集群其实到板卡级别完全不需要。RK3588 的 NPU 是 3 核架构拥有独立的 6 TOPS 算力INT8 精度下。这个算力不是 CPU 那种分时抢占的模式而是可以给多个模型分时复用、也可以给每个核单独分配任务的。先说我的总体评估结论人员入侵检测推荐用 YOLOv5s 或 YOLOv8s输入分辨率 640x640单帧 INT8 推理约 15-30ms烟火检测推荐用 GhostNet-YOLOv5n 或者 YOLOv5n输入分辨率 640x640单帧约 20-35ms垃圾分类推荐用 ResNet18 或 MobileNetV3 这种轻量级分类网络输入 224x224单帧 5-10ms。三者合计理论耗时在 40-75ms换算成帧率就是 13-25 FPS。看起来不算高但对于安防和边缘智能场景入侵和烟火这种实时性高的检查15FPS 完全可用垃圾分类本来就不需要实时出结果做到 3-5 秒更新一次即可。这就是关键思路不追求三个模型每帧都全速跑而是按需求分配帧率和触发频率。还有一点能省不少算力的是模型结构本身的精简。RK3588 的 NPU 不做 FP32 高精度推理是浪费一定要走 INT8 量化。量化之后不仅速度翻倍内存带宽压力也会大幅下降这对三个模型同跑是至关重要的一步。1.2 NPU 并发调度的三种可选方案在正式写代码之前必须先想清楚调度逻辑。RK3588 的 NPU 驱动层RKNPU支持多种并发模式我实测下来有这几种方案 A串行推理简单但浪费算力方案 B单进程多线程每个线程各持有一个 RKNN 上下文NPU 内部自动分时调度方案 C单上下文内多模型复用把所有模型加载到同一个 RKNN 上下文中喂数据时通过不同的 input 索引区分。方案 A 不推荐总延时是三个模型之和CPU 到 NPU 的数据拷贝也会互相阻塞帧率低到没法看。方案 B 是大多数人选择的方案代码逻辑最简单一个线程管一个模型缺点是多上下文之间无法做优先级控制NPU 驱动用默认 round-robin 调度低延迟需求的任务可能会被其他任务卡住。方案 C 更高级多模型共享同一个上下文和权重内存内存占用更小调度上也可以更细粒度地控制但复杂度高一些对新手不太友好。我自己用的是“方案 B 为主 手动控制帧率”的组合三个线程分别跑入侵、烟火、垃圾分类但不让它们死循环抢占而是在主循环里用定时器或逻辑帧控制触发频率这样既简单又可控。1.3 我的整体架构图核心思路可以这样概括USB 摄像头或 RTSP 拉流作为输入分发到三个独立的推理线程每个线程持有独立的 RKNN 上下文输入图像先用 RGA 硬件缩放模块转到模型需要的尺寸再拷贝到 NPU 可访问的内存推理结果分别做后处理NMS、softmax后给到业务层/报警模块。这里要强调一个点图像缩放、颜色空间转换千万不要用 CPU 去做。RK3588 自带的 RGARaster Graphic Acceleration硬件单元可以零拷贝完成 resize、cvtColor 这些操作如果三个模型都用 CPU 去 resizeCPU 占用会飙升到 70% 以上NPU 倒是空着但整体处理速度被拉下来了。2. 模型转换与 RKNN 工具链实操2.1 RKNN-Toolkit2 环境准备和模型导出要把 PyTorch 或 ONNX 模型跑在 RK3588 的 NPU 上第一步就是通过 RKNN-Toolkit2 转换成 .rknn 格式。这里有几个常见的坑我一个个说。第一是环境版本。RKNN-Toolkit2 跟 RK3588 的板端 runtime 库严格对应rknn-toolkit2 1.5.0 配 1.5.0 的 librknnrt.so混用经常报错。建议开发环境用 Docker 跑 rknn-toolkit2避免 Python 依赖冲突。板端的 runtime 库文件librknnrt.so从 RKNN 官方 GitHub 仓库的 rknpu2 目录下拿放进 /usr/lib 下并做软链接。第二是模型输入尺寸。别一股脑把 640x640 的输入搬上去。实测下来人员检测 640x640 没问题但烟火检测因为是小目标烟雾边缘、火焰尖角稍微降低到 480x480 或 512x512 会损失不少召回率。我最后保留 640x640。垃圾分类因为是分类网络224x224 是标配。第三是算子兼容性。YOLOv8 的很多后处理算子DFL、softmax在 RKNN 里不一定完全支持。我的经验是ONNX 导出时把后处理全部砍掉只保留 backbone neck 的输出NMS 和 decode 全在板端用 CPU 做。这样虽然 CPU 会多一点工作量但可以规避大量 NPU 算子兼容性问题。导出 ONNX 的关键代码import torch from models.experimental import attempt_load model attempt_load(yolov5s.pt, map_locationcpu) model.eval() dummy_input torch.randn(1, 3, 640, 640) torch.onnx.export(model, dummy_input, yolov5s.onnx, opset_version12, input_names[images], output_names[output]) print(ONNX export done)在把 ONNX 喂给 rknn-toolkit2 之前我习惯用 Netron 打开看一眼输出节点确认没有多余的 DFL 分支。2.2 量化校准数据集和精度参数的取舍量化这个地方才是真正决定“单块 NPU 能不能扛住三个模型”的核心点之一。RK3588 的 NPU 对 INT8 的支持并不是简单把权重四舍五入而是会做一个逐层校准校准集的选取直接影响最终精度。我之前走过弯路随便拿一部分视频帧做校准集结果模型在白天光线正常时表现不错一到晚上或逆光场景就狂出漏检。后来改成从真实场景里均匀采集 200-300 张图像覆盖白天、夜晚、强光、背光、雨天量化后的精度损失明显减少。量化核心配置config 文件参考mean_values: [[0, 0, 0]] std_values: [[255, 255, 255]] quantized_dtype: asymmetric_quantized-8 quantized_algorithm: normal quantized_method: layer target_platform: rk3588注意 mean_values 和 std_values 必须与你训练模型时的归一化方式一致如果训练时用 ImageNet 均值这里就要对应填进去否则量化后特征分布直接偏掉。量化方式上我选了 per-layer 而非 per-channel因为实测下来 per-channel 在某些 NPU 版本上有加速但精度波动大per-layer 保守但稳。如果你要追求极致速度再慢慢调 per-channel放在多模型同时跑的场景里稳定性优先。2.3 生成 .rknn 模型的完整脚本模型转换这一步我写了一个独立脚本方便换模型时直接改参数。from rknn.api import RKNN rknn RKNN() rknn.config(mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588) # 加载 ONNX 模型 ret rknn.load_onnx(model./yolov5s.onnx) assert ret 0, Load ONNX failed # 构建模型 ret rknn.build(do_quantizationTrue, dataset./dataset.txt) assert ret 0, Build failed # 导出 rknn ret rknn.export_rknn(./yolov5s.rknn) assert ret 0, Export failed rknn.release()dataset.txt 是校准图像路径列表每行一个路径建议放 100-300 张。构建完之后我还喜欢在 PC 上先做一遍推理模拟rknn.init_runtime(targetNone)走模拟器模式看看输出形状对不对避免到了板端才暴露问题。如果你手头已经有板子用targetrk3588走板端联调更快。这里还要提醒每个模型都需要一个独立的 .rknn 文件不要尝试把三个模型合并进同一个文件里的不同通道。RKNN 格式本身不支持多模型合并但支持运行时在一个上下文里加载多个 .rknn。3. 板端部署多线程调度与内存管理的核心实现3.1 初始化多个 RKNN 上下文板端代码我用的是 C 和 Python 混合方案核心部分用 C因为 Python 的 GIL 会在多线程推理时拖后腿。虽然 rknn-toolkit2 的 Python API 也可以支持多线程但实际压测下来 GIL 对 NPU 推理等待的阻塞非常明显C 版本性能至少提升 20%-30%。初始化三个模型上下文的 C 伪代码如下#include rknn_api.h rknn_context ctx_person, ctx_fire, ctx_garbage; // 加载模型 ret rknn_init(ctx_person, ./yolov5s_person.rknn, 0, 0, NULL); ret rknn_init(ctx_fire, ./yolov5n_fire.rknn, 0, 0, NULL); ret rknn_init(ctx_garbage, ./mobilenetv3_garbage.rknn, 0, 0, NULL); // 查询输入输出信息 rknn_input_output_num io_num_person; rknn_query(ctx_person, RKNN_QUERY_IN_OUT_NUM, io_num_person, sizeof(io_num_person)); rknn_tensor_attr input_attrs_person[io_num_person.n_input]; rknn_tensor_attr output_attrs_person[io_num_person.n_output]; // ... 依次对三个模型 query // 设置输入 rknn_input inputs_person[1]; inputs_person[0].index 0; inputs_person[0].type RKNN_TENSOR_UINT8; inputs_person[0].size width * height * 3; inputs_person[0].fmt RKNN_TENSOR_NHWC; inputs_person[0].buf img_data;这里有一个很关键的细节rknn_init的时候第三个参数是 flag默认填 0 就行。但如果你想用零拷贝模式需要填上RKNN_FLAG_MEM_ALLOC_OUTSIDE在初始化时就指定外部内存。这个我留到后面的优化章节细说。三个上下文同时存在不冲突底层 NPU 会临时调度分时计算。不过不要让三个线程同时调用同一个上下文那是未定义行为一定会崩溃所有线程之间的共享应该通过消息队列或图像指针互斥来实现。3.2 图像采集与 RGA 硬件缩放的配合每个模型需要不同的输入分辨率如果我们直接对原始摄像头帧分别做 resize那 CPU 或者 NPU 都会白白多干活。正确做法是用 RGA 硬件做缩放和格式转换把 YUV 转成 RGB、再缩放成模型输入尺寸。在 Linux 下操作 RGA 的常见方式是 librga也可以通过 Rockchip 的 MP 或零拷贝接口。我这里提供一个简易 C 封装的调用逻辑#include im2d.h #include rga.h // 将 NV12 转成 RGB888 并缩放 int rga_resize_nv12_to_rgb(int src_fd, int src_w, int src_h, int dst_fd, int dst_w, int dst_h) { rga_buffer_t src wrapbuffer_fd_t(src_fd, src_w, src_h, RK_FORMAT_YCbCr_420_SP); rga_buffer_t dst wrapbuffer_fd_t(dst_fd, dst_w, dst_h, RK_FORMAT_RGB_888); im_rect src_rect {0, 0, src_w, src_h}; im_rect dst_rect {0, 0, dst_w, dst_h}; return imcvtresize(src, dst, src_rect, dst_rect, 0, 0, 0); }把采集线程和推理线程解耦采集线程从 V4L2 或 RTSP 拉到一帧后通过 RGA 分别生成三份不同分辨率的 RGB 数据存到三个环形缓冲区里推理线程再从自己的缓冲区里取数据送 NPU。这样 RGA 只做一次三个模型拿到的都是现成的输入。实测这个方案下CPU 的 resize 占用从 35% 降到 5% 以下3 路视频流的 1080p 实时拉流也能轻松应对。3.3 推理线程与帧率控制逻辑多模型同跑的另一个痛点是无序竞争。如果不去管直接三个线程各自 while(1) 死循环推理NPU 的调度器会让三个任务抢时间片最终表现是三个任务都不稳定入侵检测本来要 20ms 一帧被拖到 40ms。解决方法是给每个推理线程加一个帧率上限。入侵检测要求最高我目标 15-20 FPS烟火检测 10-15 FPS垃圾分类 2-5 FPS 足够。用 C 的std::this_thread::sleep_for实现节奏控制代码如下void person_thread() { auto interval std::chrono::milliseconds(50); // 20 FPS while (running) { auto start std::chrono::high_resolution_clock::now(); process_person_inference(); auto end std::chrono::high_resolution_clock::now(); auto elapsed std::chrono::duration_caststd::chrono::milliseconds(end - start); if (elapsed interval) { std::this_thread::sleep_for(interval - elapsed); } } } void fire_thread() { auto interval std::chrono::milliseconds(80); // 12.5 FPS // ... 同结构 }实际压测中加入帧率控制之后NPU 的空闲时间片被充分利用整体帧率反而更稳定。这里想表达的核心是单块 NPU 同时跑多任务真正要管理的不只是算力还有任务的节奏。3.4 内存规划和零拷贝优化多模型同跑内存是一个容易被忽视的瓶颈。每个模型的输入、输出、中间张量都会占用内存如果各自独立申请三个模型叠加可能把板子的 4GB/8GB 内存吃紧。优化方案是复用内存池。我这里用 Android 的 AHardwareBuffer 或者 Linux 的 DMA-BUF 来分配 NPU 可访问的内存三个模型统一从一个内存池中拿 buffer用完之后归还。零拷贝模式下RGA 把图像数据直接写入 NPU 能访问的内存rknn_inputs的 type 设置为RKNN_TENSOR_UINT8然后让 NPU 直接从这块内存读数据省掉一次 memcpy。初始化 NPU 外部内存的大致流程rknn_tensor_mem* input_mem_person; rknn_tensor_mem* input_mem_fire; rknn_tensor_mem* input_mem_garbage; input_mem_person rknn_create_mem(ctx_person, input_size_person); input_mem_fire rknn_create_mem(ctx_fire, input_size_fire); input_mem_garbage rknn_create_mem(ctx_garbage, input_size_garbage); rknn_set_io_mem(ctx_person, input_mem_person, input_attrs_person[0]); rknn_set_io_mem(ctx_fire, input_mem_fire, input_attrs_fire[0]); rknn_set_io_mem(ctx_garbage, input_mem_garbage, input_attrs_garbage[0]); // 然后每次推理前只需把图像数据 memcpy 或 dma_buf_map 到 input_mem 的虚拟地址实际工程里我推荐直接把 RGA 的输出目标地址设成 input_mem 的地址这样 Camera - RGA - NPU 这条链路完全没有 CPU 拷贝。这个优化做完后单个模型推理延时虽然没变但整体端到端帧率涨了大约 18%同时 CPU 占用大幅降低。4. 后处理与业务联调4.1 人员入侵检测的后处理人员检测我用的是 YOLOv5s 的 decode NMS 后处理。由于导出 ONNX 时把后处理砍掉了板端需要自己写 decode 逻辑。这里要注意 RKNN 输出的排布格式三个输出层分别是 80x80、40x40、20x20 的 feature map每个网格点有 3 个 anchor每个 anchor 有 85 个维度cx, cy, w, h, obj_conf, 80 个类别概率。C 代码里先做 sigmoid 和 decode再把所有候选框收集起来做 NMS。这里我把 NMS IoU 阈值设成 0.5置信度阈值 0.25。跑实测时因为 RK3588 NPU 的输出本身经过量化反量化数值和 FP32 模型会有细微差异所以阈值不能设太严否则小目标容易漏检。有个经验数据同样的超参数NPU 量化版和 FP32 GPU 版相比mAP 大约下降 1%-2%但如果你把置信度阈值提太高比如 0.5召回率会掉 10% 甚至更多。恰当的做法是保持阈值不高于 0.35再靠业务层的连续帧确认机制去消抖。4.2 烟火检测的难点与后处理技巧烟火检测和普通目标检测最大的区别是火焰和烟雾没有清晰的边缘目标尺度变化极大而且通常出现在画面的中远距离。这种小目标检测在 NPU 量化后更容易丢细节。我做了几件事来弥补检测类别只保留 fire 和 smoke 两类把模型容量集中到这两个类上输入分辨率保持 640x640不用 416 或 320针对小目标后处理时把低置信度阈值放宽到 0.15再用帧间连续性过滤误报。帧间连续性用的是一个非常简单的方法如果连续 3 帧在同一坐标附近检测到烟火才上报告警如果只是偶发一帧检测到认为是噪声。这个逻辑写在后处理模块里只影响业务层不增加 NPU 负担。烟火检测的小目标 NMS 还可以加一个 trick不同尺度 feature map 的候选框分别做 NMS最后再合并做一次 NMS。这个做法在 RK3588 NPU 上有效抑制了跨层重复框对低置信度小目标有显著帮助。4.3 垃圾分类的后处理与模型替换垃圾分类我用的是 MobileNetV3 作为 backbone输出维度是类别数比如 4 类可回收、有害、厨余、其他后处理就是一个 softmax argmax几乎不耗时。但分类任务有个问题如果直接对整帧图像做分类画面里多个垃圾桶、多个物体混在一起时结果会很差。正确做法是先通过人员入侵模型检测到人手中的物体区域或者用一个独立的检测框把垃圾桶区域裁出来再送进分类模型。这个联动逻辑可以共用已经跑出来的人员检测结果不需要额外增加一次检测。在代码里我会把垃圾分类线程设计成“等待触发”模式// 垃圾线程变量 std::shared_ptrcv::Mat garbage_roi nullptr; // 业务线程检测到投放动作时设置 ROI void set_garbage_roi(cv::Mat roi) { std::lock_guardstd::mutex lock(garbage_mutex); garbage_roi std::make_sharedcv::Mat(roi); } void garbage_thread() { while (running) { cv::Mat roi; { std::lock_guardstd::mutex lock(garbage_mutex); if (!garbage_roi) { std::this_thread::sleep_for(std::chrono::milliseconds(30)); continue; } roi garbage_roi-clone(); garbage_roi.reset(); } process_garbage_classify(roi); } }这样垃圾分类不是每帧都跑只在检测到投放行为时执行NPU 的压力又小了一大截。5. 性能实测数据与调优记录5.1 三模型并行压测结果我把三个模型的规格和实测耗时汇总成一个表方便你直接对照参考模型输入分辨率量化精度单帧 NPU 耗时ms帧率限制YOLOv5s 人员入侵640x640INT818-2520 FPSYOLOv5n 烟火检测640x640INT820-3012 FPSMobileNetV3 垃圾分类224x224INT86-10触发式这里的耗时是纯 NPU 推理时间不包括 RGA 缩放和数据拷贝。全流程端到端耗时从摄像头取帧到拿到检测结果在 1080p 输入下大约为人员入侵45ms烟火检测52ms垃圾分类35ms不含等待触发时间。系统总 CPU 占用三个线程 RGA 主业务稳定在 18% 左右内存峰值约 900MB4GB 板子还有很大余量。这个数据是在 RK3588 标准频率、无主动降频的情况下测的。如果你加了过热保护或者跑在低功耗模式推理时间会再涨 20%-30%建议做好散热。5.2 影响性能的三个隐藏因素这三个因素在官方文档里写得很少但直接影响帧率我单独拎出来说第一NPU 频率策略。RK3588 的 NPU 默认是自动调频但有时候驱动会保守地跑在低频率上。可以通过/sys/class/devfreq/fdab0000.npu/下的接口查看当前频率如果一直上不去可以考虑设置为 performance 模式echo performance /sys/class/devfreq/fdab0000.npu/governor实测 performance 模式下 NPU 推理时间稳定降低 10%-20%代价是功耗和温度上升。在无风扇被动散热的环境中要特别注意温度墙超过 85℃ 会触发降频反而更卡。第二CPU 到 NPU 的数据排队。如果你用普通指针传入图像rknn_run内部会先做一次内存映射或拷贝。多次迭代下来这个拷贝时间可以占到单帧总耗时的 10% 以上。零拷贝模式解决的就是这个。第三中断和线程优先级。Linux 默认调度策略可能让 NPU 推理线程被 RTSP 拉流线程抢占。我建议把所有推理线程用pthread_setschedparam设为 SCHED_RR 实时优先级优先级数值从 20 设到 30。struct sched_param param; param.sched_priority 30; pthread_setschedparam(pthread_self(), SCHED_RR, param);注意别把采集线程也设太高否则会导致帧缓冲堆积、内存飙升。5.3 各任务帧率与告警响应时间的取舍不同任务对实时性的要求差别很大。人员入侵告警要求秒级响应烟火检测可以放 2-3 秒垃圾分类则完全不需要实时。所以三者的帧率分配不能平摊。我在架构里是这样设置的人员入侵20 FPS每帧都做 NMS一旦出现框且中心点进入设定区域立即触发告警烟火检测12 FPS连续 3 帧命中才告警告警响应时间大约 250ms垃圾分类不是固定帧率而是由业务事件触发通常检测到“投放行为”后 200ms 内给出结果。三者相加的 NPU 总负载只占用了整体算力的 60%-70%剩余算力还可以留给未来的新模型或者跑 OpenCV 的某些加速算子。这套设计的可扩展性非常强。6. 常见问题与排查技巧实录6.1 多模型同时跑崩了NPU 上下文冲突我最开始做多线程推理时遇到过一个很典型的问题单模型跑都正常两个模型同时跑几十秒到几分钟后板子直接 hang 死串口报错类似rknpu: fatal error。排查过程费了很大劲最后定位到根因两个线程同时调用了rknn_run但用来存放输出结果的内存共用了一个 buffer导致数据竞争。虽然各自的上下文是独立的但输出内存的申请和释放如果不在同一层级驱动内部做 cache 操作时可能会互相踩踏。解决方式是给每个上下文单独申请输入输出内存并把内存生命周期绑定到上下文生命周期不要在主循环里频繁 malloc 和 free。如果你用rknn_create_mem创建的内存记得在回调或析构函数里用rknn_destroy_mem释放。另外一个类似问题是两个上下文使用同一个 .rknn 文件即两个模型权重完全相同。这种情况下 NPU 内部某些版本的驱动会尝试共享权重内存出现冲突。避免办法就是给模型文件做一次重命名或者拷贝两份到不同路径。6.2 推理速度突然下降一半这个问题出现在长时间运行场景原因大概率是 NPU 触发降频或 CPU 被其他进程占满。先看温度cat /sys/class/thermal/thermal_zone0/temp温度超过 75℃ 时就该考虑加强散热了。另外检查一下是否打开了太多调试终端或日志日志输出本身也会抢 CPU。我习惯把所有 printf 都加一个#ifdef DEBUG开关线上运行全关。如果温度正常再看 NPU 频率cat /sys/kernel/debug/rknpu/freq如果频率低于 800MHz说明驱动在省电模式把 governor 改成 performance 即可。6.3 检测结果偏框、置信度低量化后模型精度下降导致的偏框很多人第一反应是重新训练模型其实不用。优先检查两件事检查校准集如果校准集全是室内场景室外场景检测自然变差。把图片分布扩充分布到目标场景检查 color formatRKNN 默认输入是 RGB 还是 BGR 取决于你转换时指定的fmt。如果 RKNN 模型训练时用 RGB但你输入了 BGR那检测效果会直接崩。常见错误是 OpenCV 默认读图是 BGR直接往从 RGA 出来的 RGB 数据上套通道就反了。通道检查可以用一张固定颜色的简单测试图做推理看输出张量分布2 分钟就能定位。6.4 编译和链接问题找不到 rknn_api.h如果你在板端用 GCC 编译 C 代码最常遇到的问题就是头文件路径和动态库路径没配对。从官网仓库拿 rknpu2 之后把include/rknn_api.h放到/usr/local/include把librknnrt.so放到/usr/local/lib或/usr/lib然后更新 LD_LIBRARY_PATHsudo cp librknnrt.so /usr/lib/ sudo ldconfig g main.cpp -o app -lrknnrt -I/usr/local/include如果是 Python 环境直接用 pip 装rknn-toolkit2配套的 Python 包不要手动拷贝 .so 到 site-packages容易版本不匹配。6.5 帧率抖动严重如果你的端到端帧率忽高忽低多半不是因为 NPU 算力不够而是帧缓冲队列太长。某些拉流库默认会缓存几十帧推理速度跟不上的时候缓冲区堆满表现就是画面延迟越来越大。解决方式是主动丢帧只保留最新一帧旧帧直接丢弃。线程每次取帧时清空队列或者用双 buffer 标志位的方式。还有一个容易被忽略的地方GStreamer 管线的 queue size 也要限制。gst-launch-1.0 rtspsrc location$URL latency0 ! queue max-size-buffers1 ! ...把 latency 设成 0把 queue 缓冲设为 1延迟会明显下降帧率也更平稳。7. 下一步可以做的扩展这套三模型方案跑通之后如果你想继续榨干 RK3588 的潜力可以从这几个方向扩展。第一个方向是叠加更多视觉模型。因为当前 NPU 占用只有 60%-70%还有 30%-40% 算力余量可以再加一个车牌识别或者安全帽佩戴检测。新增一个 YOLOv5n 级别的模型也就增加 15ms 的 NPU 推理时间只要帧率控制得当不会和现有任务冲突。第二个方向是把推理结果上云或者接入 IoT 平台。RK3588 自带千兆网口可以用 MQTT 把告警事件推到远端平台。注意推流和推理线程不要放在同一个线程里用消息队列异步发避免网络波动拖累检测。第三个方向是做视频流路数扩展。单模型处理单路视频流的方法已经理顺了如果要做 4 路甚至 8 路视频流核心变化是每个流的模型中加一个 stream_id 参数并在后处理时保留路数信息。NPU 算力不变但每路帧率必须降下来比如 8 路入侵检测每路只跑 5 FPS总耗时仍能控制在 200ms 以内。我个人在实际项目里有个很深的体会RK3588 这块板子并不缺算力缺的是合理的资源编排。很多人拿到板子第一件事就是把一个模型跑到最高帧率等真正做多任务时就发现 CPU 爆了、内存不够、线程互相抢占然后怀疑硬件不行。其实把这几个环节理顺把模型量化做扎实把帧率策略设计好单块 NPU 同时跑四五个模型都是很从容的事情。最后再分享一个小技巧三模型同跑时每加一个新模型前我习惯先用 30 帧图片快速压测一下单帧耗时把新任务的期望帧率乘以单耗时算出来再把所有任务的总时间占比加起来看是否超过 70%。不超过就放心跑超过了就必须砍掉一些帧率或降低输入分辨率不要等上了板发现问题再返工。