RK3588同源多任务调度:NPU/GPU/CPU协同,让多路视觉流稳跑不掉帧

发布时间:2026/9/5 5:48:03
RK3588同源多任务调度:NPU/GPU/CPU协同,让多路视觉流稳跑不掉帧 1. “同源”这两个字才是RK3588多任务调度的核心我最早看到“同源多任务调度”这个词的时候第一反应是这不就是多线程吗RK3588有8个CPU核再配上NPU、GPU、RGA这些协处理器把任务丢给不同的核去跑就是调度有什么好单独拿出来讲的直到真正在板子上做视觉应用被NPU算力瓶颈、GPU管线冲突、以及多路视频流同时进来时的资源争抢反复打脸之后我才明白“同源”这两个字的份量。先解释一下我理解的“同源”。在RK3588上做边缘AI视觉同一个模型文件比如yolov8的rknn版本可以被加载到三种不同的执行后端NPU、GPU、CPU。所谓同源指的是模型权重、模型结构来自同一个来源但推理任务可以分发到不同的算力单元上执行。这跟“异源”是两回事异源往往是不同模型跑在不同芯片上比如用NPU跑检测、用GPU跑分割、用CPU跑分类而同源是同一份rknn模型在同一个系统里被多路任务共用NPU跑不过来的时候GPU兜底CPU再兜底并且这些推理请求由一个统一的调度器来分发。这里就有一个很多人会忽略的问题RK3588的NPU虽然是6 TOPS算力但它不是无限并行的。NPU内部有3个核心Core官方文档里写的“3 TOPs per core”但在实际使用中3个核心不是随便就能同时被不同进程打满的尤其是用了RKNN Toolkit生成的模型默认情况下一个应用只会占用一个NPU core。如果同时跑多路YOLOv8检测NPU core 0可能已经100%满载core 1和core 2还在闲着但你的代码就是没让它们干活。这时候同源多任务调度要解决的第一个问题就是怎么把模型的一路推理请求分发到不同的NPU核心上并让它们真正并发执行。我在开发板上做过一次很直接的对比同一份yolov8s.rknn用单核连续跑4路RTSP视频流每路画面对应一个独立线程结果NPU占用率直接顶到100%GPU和CPU几乎没参与平均每帧推理耗时从16ms飙到45ms视频开始卡顿改成同源调度把4路推理请求通过调度器拆到NPU双核 GPUCPU做预处理兜底同样4路视频流每帧推理耗时稳定在20ms左右虽然单帧没有单核单路那么快但整机吞吐量高了一倍以上。这就是我写这篇文章的起因。这篇文章面向的是已经跑通过RK3588基础环境、准备做多路视觉任务的开发者或者正在为“模型在开发板上跑得太慢”“多路视频流一来就掉帧”头疼的朋友。我会把同源多任务调度的完整思路、调度器设计、以及我在RK3588上实测的数据和踩过的坑都整理出来重点讲清楚三个问题为什么同源能提高整体吞吐调度器的优先级和队列怎么设计以及双核NPU并发和GPU接入时最容易被忽略的坑。2. 先把四路算力盘清楚NPU双核、GPU与CPU的实际承担角色在写调度代码之前必须先把RK3588的算力资源摸清楚。我见过不少人犯同一个错误把所有AI推理全部丢给NPU然后把NPU跑满之后剩下的活儿丢给CPUGPU完全闲置。这样不算调度只能叫“让NPU硬扛”。2.1 RK3588算力架构里真正能跑模型的单元有哪些RK3588的关键算力单元是这几块6 TOPS NPU3核单核约2 TOPSMali-G610 MP4 GPU4核支持部分通用计算但生态不如NPU成熟4个Cortex-A76大核 4个Cortex-A55小核2个RGA图像处理加速单元可以做缩放、格式转换、镜像等这里要特别说明一下RGA。它不是跑AI模型的但它承担了预处理图像resize、crop、颜色空间转换这些脏活。我们做视觉任务时模型输入往往是640x640或320x320而摄像头输入是1920x1080或2688x1520如果不经过RGA直接用CPU做resizeCPU占用率会高得离谱。同源多任务调度中我把RGA当作独立的“第5路算力”来规划它不参与推理但参与整个pipeline调度器必须给RGA任务预留带宽。GPU在RK3588上跑AI模型的选择不多。RKNN Toolkit生成的模型可以直接跑NPU但GPU推理需要走OpenCL。我实测过用GPU跑yolov8s的rknn转onnx再转OpenCL的方案单帧推理时延大概在35~50ms比NPU单核16~25ms慢不少但在NPU满载时GPU可以作为次级执行端把积压的任务消化掉。这里要提醒一点GPU在RK3588上和显示共用一套资源如果上面还跑着QT界面或HDMI输出GPU做AI推理会拖累显示性能所以在调度器里GPU执行端的优先级必须低于NPU并且要设置任务超时。CPU的A76大核就更好理解了。RKNN推理在CPU模式下其实不是直接跑模型而是用RKNN的模拟层去执行速度很慢我实测yolov8s在A76上单帧推理要150~250ms基本只能做兜底。但CPU在调度器里的角色不止推理它还负责视频解码、图像预处理、调度逻辑、以及部分后处理比如NMS、目标跟踪关联。所以调度器给CPU分配任务时要区分“推理任务”和“非推理任务”。2.2 算力分工的指导原则慢设备不能拖垮快设备设计调度器之前我给自己定了一条原则每一类任务都有一个“主执行端”和一个“备执行端”主执行端跑不过来时才降级到备执行端。这就像一条高速公路NPU是主车道GPU是应急车道CPU是辅路不能一上来就把所有车都堵在辅路上。我的分配策略如下任务类型主执行端备执行端适用场景YOLOv5s/v8s检测NPU Core 0/1GPU / CPU多路RTSP实时检测轻量分类模型小于20MBNPU Core 2CPU A76人体属性、车辆颜色图像缩放/格式转换RGACPU NEON所有视频帧预处理后处理NMS/跟踪CPU A76CPU A55所有检测结果过滤分割类模型GPUOpenCLNPU Core 2慢速场景不要求实时我一再强调“同源”就是因为同一个模型在NPU、GPU、CPU上都有一份执行上下文调度器才能在这三者之间自由切换任务。如果模型只部署到NPU那GPU和CPU根本没资格接任务说什么调度都是空话。3. 一个真正能跑起来的同源调度器请求队列、优先级与线程池我最初想得很简单写一个全局任务队列NPU空闲就丢给NPUGPU空闲就丢给GPU。但跑起来之后发现判断“空闲”本身就很困难而且RKNN推理是阻塞式API一个线程在调用rknn_run的时候其他线程看不到它的“忙闲”状态只能通过互斥锁硬等。这会导致一个严重问题一个线程在NPU上跑推理时另一个线程即使想用NPU的另一个核也被同一把锁挡住了。3.1 RLS调度器的核心设计后来我设计了一个轻量级的RLSRequest-Level Scheduler模块它不修改RKNN底层驱动只在应用层做请求分发。核心数据结构是这样typedef struct rk_aiot_request { uint32_t req_id; // 请求ID自增 const char *model_name; // 模型标识同源模型共用 int task_type; // 0detect 1classify 2segment int priority; // 0最高3最低 uint64_t arrival_ts; // 请求到达时间 uint64_t deadline_us; // 期望完成的时间上限 int exec_target; // 分配结果0NPU0 1NPU1 2GPU 3CPU void *input_data; // 输入帧指针 void *private_ctx; // 模型上下文指针 struct rk_aiot_request *next; } rk_aiot_request_t;每个请求从视频流线程进来之后先进入全局请求队列调度线程每隔5ms做一次批量分发。这里的优化点是批量分发比逐个分发吞吐高得多。我实测过调度线程每5ms轮询一次4路视频流ping-pong式发请求比每来一帧就立刻调度一次CPU占用率降低了12%因为减少了线程切换和锁竞争。分发逻辑不复杂但优先级策略值得展开说priority 0人机交互类比如触摸屏点击框选目标必须立刻响应直接插队到NPU Core 0。priority 1实时检测类比如视频流里的入侵检测要求帧率大于15fps分配到当前负载最低的执行端。priority 2上报类比如每隔几秒做一次车流统计不要求实时放到低优先级队列等NPU或GPU闲下来再执行。priority 3后台离线分析比如抓拍图片入库前的属性分析直接放到CPU A55执行几乎不影响主流程。3.2 任务动态分发其实最难的是“下次别走错门”分发到哪个执行端不能只看当前负载还得看“上一次这个模型在哪个端执行”。这里面有个性能陷阱RKNN模型上下文在NPU上需要8~30ms的初始化时间但上下文建好之后同一份上下文的重复推理非常快。如果你的调度器每次都把一个任务分发到不同的执行端那就等于每次都要重新做上下文初始化性能不升反降。所以我在调度器里加了亲和性策略每个模型维护一个“最近执行端”的字段新请求到达时优先尝试最近执行端只有当最近执行端的待处理队列超过阈值比如队列里积压了超过3个请求才把新请求分发给其他执行端。这就好像一个人干活手里的活儿没堆积完就别打断他换了别人来又要重新熟悉工具。我在4路视频流上实测时这个亲和性策略对吞吐的贡献非常大。没有亲和性时系统每秒钟能处理约20帧4路每路5fps加上亲和性每秒能处理28~32帧。原因就是上下文初始化的开销被摊薄了NPU执行端大部分时间在做纯推理而不是重建上下文。4. 双核NPU并发与RKNN异步标志跑yolov8实测的数据变化要说RK3588同源多任务调度里最核心的实战环节就是把同一份yolov8模型加载到NPU的两个核上并发执行。RKNN官方SDK从RKNN Toolkit 1.6开始支持NPU Core的分配控制很多人不知道这个特性把两个NPU核当成一颗用等于浪费了一半算力。4.1 RKNN_FLAG_ASYNC_MASK 与 zero-copy 的配合加载模型时需要设置RKNN的flag和核心掩码。下面是我在C环境里用的关键配置// 创建RKNN上下文 rknn_context ctx_npu0; rknn_context ctx_npu1; // 分别加载同一份rknn模型到两个上下文 rknn_init(ctx_npu0, model_data, model_size, RKNN_FLAG_ASYNC_MASK, nullptr); rknn_init(ctx_npu1, model_data, model_size, RKNN_FLAG_ASYNC_MASK, nullptr); // 通过set_core_mask把这两个上下文绑定到不同NPU核 uint32_t core_mask_0 1; // core 0 uint32_t core_mask_1 2; // core 1 rknn_set_core_mask(ctx_npu0, core_mask_0); rknn_set_core_mask(ctx_npu1, core_mask_1); // 推理时使用异步标志 rknn_run(ctx_npu0, inputs, outputs, RKNN_FLAG_ASYNC_MASK); // 同步等待结果 rknn_wait(ctx_npu0, outputs);注意这里有个大坑rknn_init同一个模型文件两次等于在NPU上创建了两份权重副本。如果你的模型文件是80MB两份就是160MBDDR要能扛得住。yolov8s的rknn模型大约40~50MB两份大概100MB对RK3588的8GB内存版本没问题但4GB版本就会紧张。后面我会单独讲权重复用的问题。4.2 yolov8s在四种执行模式下的实测数据我使用baseline rknn的yolov8s模型输入640x640做4路RTSP视频流的人体检测对比了四种执行方案。这里把实测数据放出来执行方案平均单帧推理耗时4路总帧率NPU占用率整机CPU占用率备注单上下文单核串行42ms约10fpscore0 100%35%最容易写的代码但掉帧严重双上下文双核无调度器26ms约15fpscore0/core1各60~70%48%提升明显但有任务积压双上下文双核调度器20ms约20fpscore0/core1各75%41%调度器把负载压得非常平双核NPU GPU兜底18ms约24fpscore0/core1各80%GPU 30%52%整体吞吐最高但功耗也最高需要说明这里的“平均单帧推理耗时”是4路视频流里的平均单帧延迟不是单路独占时的推理延迟。单路独占时yolov8s在RK3588 NPU上大约16~18ms这个大家应该都比较熟悉。从数据能看出双核并发不是简单地把单核翻倍因为还有输入排队、内存拷贝、后处理这些开销。再加上调度器之后收益的一部分来自负载均衡另一部分来自亲和性策略减少了上下文切换。GPU兜底带来的吞吐提升只多了4fps但CPU占用率直接从41%涨到52%因为OpenCL路径需要CPU做大量内存搬运。于是我在实际部署时默认关掉GPU兜底只在NPU过载时动态开启。4.3 一个容易误判的现象异步标志不是开了就“真异步”RKNN的RKNN_FLAG_ASYNC_MASK这个标志名字里有“ASYNC”但我用了很久才摸清它的脾气。它解决的不是“调用线程不阻塞”而是“NPU执行过程中CPU可以继续做别的事”。如果你在rknn_run之后立刻调用rknn_wait那其实和同步执行没区别CPU还是会等着。真正的异步用法是发起rknn_run之后CPU马上去处理上一批已经完成的推理结果然后再回来rknn_wait当前这一批。我在调度器里把这种“流水线”思路落实成三块缓冲区缓冲A正在NPU上推理缓冲B正在做后处理缓冲C正在被视频流线程填充输入数据。三个区互不重叠CPU、NPU、解码器才能并行起来。这也是我觉得“同源多任务调度”里最有工程价值的部分不是调度器本身多复杂而是它把RK3588的每个算力单元都当成一个独立的流水线级来用同一份模型在算力链路上从头到尾没有真正的空闲等待。5. 上下文复用时最容易踩的坑权重拷贝、NPU句柄与CPU线程绑定这部分是实打实的经验汇总。同源多任务调度最大的优势是“同一份模型多端复用”但也正因为复用会踩到一些单模型部署时永远不会出现的问题。5.1 两次rknn_init等于双倍权重拷贝4GB板子怎么办如果你的开发板内存是4GB版本一定要谨慎使用“创建两套上下文”的方式。我在8GB板子上做双核NPU并发没觉得内存紧张后来换到4GB板子测试一个yolov8s.rknn加载两份系统可用内存直接从900MB掉到300MB再叠加4路视频流的缓冲直接触发OOM进程被杀掉重启。解决这个问题有两个方向。第一个方向是改用RKNN Toolkit Framework的Zero-Copy功能让两个上下文尽可能共享模型权重段但RKNN底层对零拷贝的支持和模型结构相关不是所有模型都能吃到红利。第二个方向更通用4GB板子上不要做“两份NPU上下文”改成一个上下文调度器强制串行另一个模型跑GPU或CPU。实验数据告诉我4GB板子上双NPU并发的内存压力远大于吞吐收益不如混合执行端方案划算。5.2 npu句柄的线程安全问题别跨线程乱调rknn_apiRKNN官方文档不太强调这件事但我把血泪史写在这里同一个rknn_context的rknn_run和rknn_wait不能在不同的线程里并发调用。我最初想用“一个线程把输入丢进rknn_run另一个线程立刻rknn_wait”的方式提高吞吐结果跑了一个多小时系统随机报RKNN_ERR_PARAM_INVALID有时候直接段错误。后来查了SDK源码才发现rknn_context内部的命令缓冲区不是线程安全的。正确做法是每个执行上下文绑定一个专属工作线程比如NPU0上下文只被线程A调用NPU1上下文只被线程B调用调度器通过队列把请求投递给对应线程而不是直接跨线程调API。这样一来RKNN调用全部单线程化吞吐靠多个执行端并行撑起来而不是靠单执行端内多线程并发。5.3 CPU侧的线程绑核实时性提升比想象中大我在跑调度器时把后处理线程绑到A76大核CPU 4~7把视频解码线程绑到A55小核CPU 0~3这样大核不会被解码中断打搅小核也不会因为跑后处理而卡顿。用pthread_setaffinity_np做绑核代码很简单cpu_set_t cpuset; CPU_ZERO(cpuset); CPU_SET(4, cpuset); // 绑定到CPU4A76大核 pthread_setaffinity_np(pthread_self(), sizeof(cpuset), cpuset);实测效果调度线程的响应波动从±8ms下降到±2ms。这个优化其实不复杂但在多任务调度系统里特别管用因为调度器如果自身抖动过大后面所有执行端的负载估计都会失真。6. 路由策略与内存占用同源多任务比“一模型一进程”到底省在哪最后聊一个很多人忽略的问题既然多任务调度这么麻烦我能不能简单粗暴一点每路视频流起一个独立进程每个进程独立加载一份模型互不干扰答案是可以但代价非常大。我做过对比测试4路视频流每路一个进程每个进程各加载一份yolov8s.rknn总内存占用从单进程调度方案的约650MB涨到约1.6GB。多出来的这1GB就是四个进程各自持有的模型权重、RKNN内部缓冲区、以及重复的预处理中间结果。在同源多任务调度方案里模型权重只保留一份公共副本各路任务通过调度器共享同一份模型上下文只有输入输出缓冲区和后处理结果是各自独立的。这在大内存场景下不算致命但在边缘设备上内存每多出1GB就意味着散热、功耗、稳定性全面受影响。调度器的路由时机也非常重要。我的原则是调度决策要在预处理之前而不是推理之前。帧从摄像头抓进来后先根据当前各执行端的负载决定这帧交给NPU、GPU还是CPU然后再做对应的输入格式转换。如果等预处理完再决定一旦发现NPU忙要转给GPU就得重新做一次格式转换白费功夫。这个细节让我的整体pipeline延迟降了约3ms/帧。再补充一个我自己觉得很有用的收尾技巧把调度器的状态导出来通过串口或者日志定期打印一张“各执行端任务分布图”包含每个端已经处理的总请求数、当前队列积压数、平均处理时延。初期调优时我在RK3588上同时跑两路模型任务一张4路视频流的调度拓扑图就能很快看出哪一路在偷懒、哪一路过载。这类诊断信息用RKNN自己的profiler很难直接拿到因为profiler只关注单模型的NPU执行细节不管应用层的请求分发。7. 关于RK3588调试流程和固件版本的一些提醒调度器写完能不能跑其实还取决于你板子上的环境和固件。我调试RK3588的过程中遇到过几次特别困惑的事代码没问题但NPU推理结果一直不对后来发现是固件里的NPU驱动版本太老和RKNN Toolkit生成的模型版本不匹配。这类问题很难靠看日志定位因为各家开发板的系统镜像版本五花八门有时候同一个RKNN版本在不同板子上表现都不一样。我现在的做法是上车第一件事先确认四样东西NPU驱动版本dmesg | grep rknpu或者直接cat /sys/kernel/debug/rknpu/versionRKNN Toolkit版本跟模型转换环境严格对应不要在PC上用了新版转换板子上还跑老版runtime系统镜像版本强烈建议用开发板厂商提供的最新官方固件自己裁剪的内核有时会把RKNPU的dts节点配置错内存带宽压力跑业务时用perf或top盯着CPU使用率如果整机CPU一直处于80%以上先查是不是解码和RGA挤占了NPU的DDR带宽电源也是个大坑。RK3588在NPU满载GPU满载4路视频解码同时工作时功耗上得很快。我用的开发板在12V 2A供电下整机高负载跑10分钟就会触发降频NPU频率从1.0GHz掉到0.7GHz推理时延大幅增加。后来换成12V 3A电源稳态功耗才稳住。电源不足导致的“假性卡顿”容易被误判成调度器问题排查时要先排除。从我的经验看RK3588作为一块边缘AI开发板性能和灵活性在同类设备里是第一梯队。但“算力”和“实际能用的算力”之间隔着系统、驱动、调度器、内存管理这一堆工程问题。同源多任务调度不是银弹它解决的是多路任务共享同一份模型资源时的调度效率问题如果你的业务只有一路视频流老老实实单核NPU跑就是最优解。但只要有超过两路的视觉任务进来这个调度器思路的价值就会立刻体现出来。希望这篇文章能帮你少走我之前走过的弯路。