ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RK3588边缘AI多任务调度实战:同源共享串行架构,帧率翻倍

RK3588边缘AI多任务调度实战:同源共享串行架构,帧率翻倍 开篇先讲个我自己的真实经历。去年接了一个边缘AI视觉盒子的项目硬件选型定了RK35888核A76A556 TOPS NPU板子看起来性能很充裕。需求也不复杂一路摄像头进来要同时做目标检测、人脸检测、车牌识别、区域入侵报警最多再加上一个属性分类。最开始我按最直觉的方式做每个任务独立开一个线程各自拉流、各自预处理、各自调NPU、各自后处理。结果跑起来之后CPU占用直接飙到70%以上NPU经常撞车内存峰值逼近系统上限整体帧率从预期的25帧掉到不到12帧而且高负载的时候还偶发卡顿、丢帧。后来我把问题翻来覆去捋了一圈发现根子不在单模型性能而在于多个任务之间完全没有协作各抢各的资源。RK3588再强也架不住这么内耗。最后我重新设计了一套调度框架核心思路就六个字同源、共享、串行。同一路视频源所有视觉任务吃同一份数据预处理只做一次NPU推理按优先级排队后处理异步分发。改完之后帧率稳定在25帧以上CPU占用降到35%左右整个系统跑一周不重启也不崩。这篇文章就把这套方案的设计思路、代码骨架和踩坑记录完整写出来给正在RK3588上做边缘视觉多任务的朋友一个可以直接参考的路线。1. 同源多任务调度解决的是哪一类问题先说清楚“同源多任务调度”到底是个什么东西。它不是一个算法也不是某个固定框架而是一套针对边缘视觉设备的多任务资源组织方式。核心前提是多个视觉任务的数据来源是同一个输入源比如同一路摄像头、同一个视频文件、同一条RTSP流。所谓“同源”就是所有任务共享这次输入的数据和中间结果“多任务调度”则是对这些共享数据的任务做统一仲裁决定谁先跑、谁后跑、谁可以少跑、谁的结果最紧急。1.1 边缘视觉项目里最常见的“各跑各的”陷阱很多第一次在RK3588上做多任务视觉的人包括当时的我都会不自觉地陷入一个设计误区把模块当水管子一根进水多头出水。每个业务模块为了保持“高内聚低耦合”各自抓取视频流、各自解码、各自缩放、各自算推理。比如一个盒子要同时做人形检测和车牌识别你可能会写成视觉库A只管检测人视觉库B只管识别车牌两个库互不通信各开各的线程各调各的模型。这样做的优点看起来是代码隔离好、职责清晰但在RK3588这类嵌入式平台上代价非常致命。首先是CPU和DDR带宽被预处理操作反复轰炸。一路1080p视频解码出来是YUV原始帧每个任务都要对它做一次resize、一次cvtColor、一次归一化。四个任务就是四套一模一样的操作每帧多出几十毫秒的纯CPU开销和几倍的DDR流量。其次是NPU资源被无谓地争抢。RK3588的NPU只有一个同一时刻只能跑一个推理请求严格说支持多核并发但单模型串行更稳多个线程同时往里塞任务底层驱动只能排队等待或者直接报错。第三个问题更隐蔽不同任务对延迟的容忍度不一样——区域入侵必须毫秒级响应定期统计人数可以慢一点但如果你把所有任务同等对待最终结果就是轻量任务被重型检测拖后腿整个系统帧率被最慢的任务拉低。1.2 同源调度和传统方案的本质区别传统多路独占式的思路是把有限资源切分成多份每个任务觉得自己“拥有一块专属资源”。而同源多任务调度恰恰相反它强调从数据到算力的全链路复用和统一切换。二者对比看这张表维度各跑各的多任务同源多任务调度数据入口每任务独立拉流解码统一取流单路分发预处理每任务重复resize/归一化一次预处理全任务共享NPU使用多线程抢占随机排队调度器统一排队按优先级推理内存占用每份输入各存一份副本共享帧缓冲零拷贝提供延迟稳定性高负载时抖动严重优先级保证关键任务延迟可控代码耦合任务间完全独立调度器居中协调业务仍可解耦从工程角度看同源调度的本质是引入了“共享层”。数据共享、预处理共享、推理时序共享三层共享叠加的效果是同样的硬件配置能承载的视觉任务数量差不多能翻一倍。而且它并不牺牲任务的独立性——业务模块之间依然可以零通信只是它们不再各自抢占底层资源而是统一向调度器申请执行窗口。1.3 RK3588的资源底账能省的必须省的在动手写调度代码之前必须把RK3588的资源底账算明白。这芯片的CPU部分有4个Cortex-A76大核和4个Cortex-A55小核大核主频能跑到2.4GHz小核1.8GHzNPU是6 TOPS算力加上2个RGA 2D硬件加速器、8K VPU编解码一个Mali-G610 GPU。看起来资源很豪华但边缘视觉的真实约束从来不是“单点性能”而是“持续多任务下的带宽与功耗”。我在实际项目中抓到的主要瓶颈有三个。第一个是NPU的串行特性rknn的推理请求一次只跑一个多核并行看起来美实际上调度复杂度高、内存翻倍实用场景里很少能稳定跑满两核。第二个是DDR带宽4K/1080p图像数据动不动就是几十MB一帧所有任务共享同一条内存总线任何重复搬数据的行为都是巨量浪费。第三个是CPU与NPU的协同NPU跑推理的时候CPU并不能完全闲着它要做前处理的喂数据和后处理的取结果这两段开销如果叠加在关键路径上CPU占用率立刻上去。同源调度的所有设计本质上都是在绕开这三个瓶颈。2. 调度器整体架构与核心设计现在进入正题直接讲我最终落地的调度器结构。这个方案不是凭空拍脑袋想的是我在RK3588上反复压测、断断续续调了一个多月才稳定下来的踩过的坑我会在后面的章节专门列出来。这里先把框架讲清楚大家可以拿它当蓝图直接改造。2.1 数据入口统一从多路采集到单路分发同源调度的第一步是砍掉所有重复的取流逻辑。不管你是用V4L2抓USB摄像头还是用海康/大华的SDK跑RTSP或者读取本地视频文件都只保留一路采集线程。采集线程取到一帧原始数据后编码成一个Frame对象里面包含了图像数据指针、时间戳、帧序号、分辨率信息然后投递到调度器的输入队列。这一步看起来简单但有一个很关键的效率优化点图像数据指针必须是“零拷贝”流转。含义是采集线程把帧数据放到一块预先分配好的内存池里后面的所有任务都只能引用这块内存谁也不能私自再copy一份。RK3588上内存带宽是稀缺资源1080p NV12数据一帧大概3MB如果每个任务都复制一份4个任务就是12MB一秒25帧就是300MB的无谓搬运。我见过最夸张的案例有人在任务线程里用cv::Mat浅拷贝理解错了写成了clone()导致dma压力直接拉满系统内存带宽成了瓶颈NPU反而在空转。用代码来描述这个统一入口大概是这样的struct Frame { uint8_t* data; // 指向内存池中的图像数据 size_t size; // 数据大小 int width; // 图像宽 int height; // 图像高 int format; // 像素格式一般是V4L2_PIX_FMT_NV12 uint64_t timestamp; // 采集时间戳 uint32_t seq; // 帧序号 };所有下游任务拿到的不是“自己的帧”而是这个Frame的只读引用。从设计上就杜绝了重复拷贝和灾备不一致。2.2 预处理结果复用一次RGA缩放多个任务共用统一数据入口只是第一步接下来步入成本最高的环节——预处理。很多视觉任务对输入尺寸的要求不一样目标检测模型可能吃640x640人脸识别模型要吃112x112车牌识别要吃416x416。如果按老思路每个任务自己去缩放就等于同一帧画面被RGA或CPU反复缩放多次纯属浪费。同源调度的做法是预先把“常用尺寸”的缩放结果一次算好缓存起来。比如输入是1080p任务要求640和416调度器就在收到原帧后立刻用RGA硬件加速器做两次缩放把640和416两个分辨率的图像各备一份挂在Frame的附加数据区里。后续所有需要640输入的任务直接从Frame上拿现成的缩放图不需要再触发任何缩放操作。这里有一个参数选择的小技巧。不要为每个任务单独生成一个独有的分辨率而是“就近合并”把任务要求的分辨率聚合成几档通用尺寸。例如多个模型都要求608x608到672x672之间的输入统一生成640x640一份就够了每个模型推理前只做极轻量的中心裁剪或letterbox补齐。这样能用最少的缩放次数覆盖最多的任务。RGA是瑞芯微平台很重要的硬件加速器你把缩放操作从CPU上挪到RGA上CPU的占用率能立刻降下来一大截。在常规情况下1080p缩放到640x640用RGA大概只需要1-2ms而用CPU的opencv resize大概要8-12ms。这个差距在20帧以上的实时系统里就是天壤之别。2.3 任务注册与调度策略设计数据层准备好之后就到了调度器的核心任务管理与调度仲裁。我是把每一个视觉任务抽象成一个TASK节点它只向调度器暴露几个固定接口采集输入尺寸、执行的优先级、模型推理函数、后处理回调。调度器不关心具体任务内部跑了什么算法只负责给它分配执行时机和算力配额。优先级的定义要严格按照业务容忍度来定。我给三类典型任务定义了三个等级L0实时告警类如区域入侵、跌倒检测、火焰识别必须在50ms内响应优先级最高每帧都推理。L1常规识别类如车牌识别、人脸抓拍要求100-200ms内出结果可以每帧推理但允许排队。L2统计类如人流量统计、聚集检测300ms以上延迟无所谓可以每隔3-5帧才推理一次大幅降低算力消耗。调度器维护一个“就绪任务链表”按优先级排序每收到一帧信号就从高到低依次触发任务。高优先级任务立刻执行低优先级任务如果检测到NPU忙可以选择跳帧skip frame而不是阻塞。这就是“同源调度”在时序层面的核心思路不是每个任务都跑满帧率而是保证关键任务实时、非关键任务尽力而为。对于L2统计类任务我还会做一个动态抽帧策略。比如人流量统计25帧/秒的视频流实际上每5帧执行一次检测就已经绰绰有余捕获率误差小于2%。抽帧逻辑很简单调度器维护一个计数器只有当计数对某个任务设定的步长取模为0时才投递该任务到执行队列。这个策略在后面的性能数据里是省NPU算力的大功臣。2.4 后处理异步化别让解码和识别堵住采集最后一个重要设计是后处理异步化。什么是后处理就是模型推理输出张量之后你要做的NMS解码、坐标映射、分类打分、结果推送这些逻辑全部走CPU。如果这些操作都放在推理线程里同步执行CPU的峰值负载会非常高而且一旦NMS逻辑写得重下一帧的采集就会被堵住。我的做法是把后处理从推理线程里完全拆出去放到一个独立的“结果工作池”里。调度器一旦拿到模型输出张量就把张量和对应的Frame信息封装成ResultTask扔进工作池。工作池里有2-4个线程并行消费各自处理NMS、映射坐标、推送到业务回调。这样NPU和CPU能并行工作NPU在跑第N帧的推理CPU在同步处理第N-1帧的后处理。这个拆分的收益很大。实测在没有异步化之前单帧端到端时延大约是65ms其中后处理占了20ms异步化之后单帧端到端时延降到45ms而且CPU峰值的毛刺明显减少。你要是后处理里还有深度学习分类器比如对检测框二次做属性分类异步化的收益会更大。3. RK3588上的调度器落地实现架构层面的设计说完了下面进入硬核实操环节。我会上真实的代码骨架覆盖调度循环、RKNN模型初始化、预处理共享三个关键部分。代码我尽量精简去掉业务细节只保留调度框架本身的逻辑大家可以直接抄到自己的项目里改。3.1 代码结构与关键模块先看整个工程怎么组织。我的建议是哪怕你用的是Python快速原型也按这个模块边界来拆后面换C或者加功能会顺畅很多。edge_ai_scheduler/ ├── capture/ # 采集模块统一取流 │ ├── v4l2_capture.c │ └── rtsp_capture.c ├── common/ # 公共数据结构和工具 │ ├── frame.h │ ├── ring_buffer.h │ └── thread_pool.h ├── preprocess/ # 预处理模块RGA缩放封装 │ └── rga_preprocess.c ├── scheduler/ # 核心调度器 │ ├── scheduler.h │ ├── scheduler.c │ └── task_api.h ├── tasks/ # 具体业务任务 │ ├── detect_task.c │ ├── face_task.c │ └── plate_task.c └── main.c核心的调度循环在scheduler.c里整个循环是单线程的所有任务只由这一个循环触发生效。为什么用单线程因为多线程再加锁会导致逻辑复杂而且难以排查时序问题RK3588的大核跑单线程调度循环开销很低瓶颈本来就不在调度器本身。3.2 调度循环核心代码与分析下面这个片段是调度循环最核心的部分完整逻辑包括取帧、更新共享缩放、执行任务、回收帧引用。void scheduler_run(Scheduler* sch) { while (sch-running) { // 1. 采集线程取一帧阻塞最多100ms Frame* frame capture_acquire_frame(sch-cap, 100); if (!frame) continue; // 2. 标记帧被使用禁止缓存释放 frame_retain(frame); // 3. 按需生成共享缩放结果 preprocess_generate_common_sizes(frame); // 4. 遍历就绪任务链表依次调度 uint64_t now get_time_ms(); for (int i 0; i sch-task_count; i) { Task* task sch-tasks[i]; // 抽帧策略 if (task-frame_skip 0 (frame-seq % task-frame_skip) ! 0) { continue; } // 超时保护低优先级任务不能在上一帧拖太久 if (now - task-last_start task-timeout_ms) { continue; } // 执行推理并异步后处理 if (task-is_ready) { task-last_start now; task-async_infer(task, frame); } } // 5. 释放帧引用归还内存池 frame_release(frame); } }这段代码看起来平淡但我用上了几个在真实场景里救过命的细节。第一帧序号seq用于抽帧和丢帧判断它是全局单调递增的这保证了就算推理时间抖动抽帧节奏也不会乱。第二每个任务维护一个last_start时间戳这是“延后调和”的关键如果某个低优先级任务上一轮已经占用了大量时间调度器会主动跳过它当前轮次避免影响高优先级任务的下一轮。第三task-async_infer是异步接口推理请求发出后立即返回真正的等待发生在内部维护的NPU请求队列里这样调度循环本身不会被单个推理卡死。3.3 RKNN模型初始化与共享推理RKNN模型怎么初始化和共享推理是RK3588上最需要讲究的一环。我这里直接用C API给出核心代码。首先要初始化多个模型必须在同一个rknn_context上下文里依次加载而不是每个任务自己凭空创建一个context。// 初始化阶段一次性加载所有模型到上下文 rknn_context ctx; rknn_init(ctx, model1_path, 0, 0, NULL); rknn_load_rknn(ctx, model2_path, 0, 0, NULL); rknn_load_rknn(ctx, model3_path, 0, 0, NULL); // 对每个模型设置输入 for (int i 0; i nn_model_count; i) { rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, io_num[i], sizeof(io_num[i])); rknn_set_io_mem(ctx, input_mem[i], input_attr[i]); // 每个模型独立输入 }实际推理时共享同一个context的最大好处是底层驱动可以对多个模型的推理请求做更好的合并与排序而不是直接打架。代码上每个任务的推理函数大致长这样int task_detect_infer(Task* task, Frame* frame) { // 从frame的共享缩放区直接拿640x640输入 void* input_data frame_get_scaled(frame, 640); memcpy(task-input_buf, input_data, task-input_size); // 封装输入输出 rknn_input inputs[1]; inputs[0].buf task-input_buf; inputs[0].size task-input_size; inputs[0].pass_through 0; inputs[0].type RKNN_TENSOR_UINT8; rknn_inputs_set(task-ctx, 1, inputs); // 查询输出 rknn_output outputs[task-output_count]; memset(outputs, 0, sizeof(outputs)); for (int i 0; i task-output_count; i) { outputs[i].want_float 0; } rknn_outputs_get(task-ctx, 1, outputs, NULL); // 封装成异步后处理任务投递到工作池 post_process_submit(task, outputs, frame-timestamp, frame-seq); return 0; }看到没有同一个ctx的不同模型每个模型有自己独立的rknn_tensor_mem输入输出缓冲区但要确保输入缓冲区在init阶段就已经分配好。RKNN的底层的输入输出内存管理很讲究你要是频繁在推理循环中分配和释放内存DDR压力会非常大。所以我的做法是在模型初始化时一次性为每个模型分配好输入输出缓冲区之后循环复用推理期间只做memcpy和数据搬运。这里还要提一点对于多个模型共享同一个context实测中要注意的是算子兼容性。如果某个模型包含context中其它模型不支持的算子初始化就可能失败或者推理结果错误。因此我建议所有的模型先单独用rknn-toolkit2的精度工具验证一遍确保每个模型都能在RK3588上通过再合入同一个context。3.4 实测数据与瓶颈分析直接放一组我在真实项目里跑出来的数据。测试环境是RK3588 8GB内存开发板一路1080p RTSP摄像机输入同一时间跑四个任务YOLOv8s行人检测L0、人脸关键点检测L1、车牌识别L1、人流量统计L2每5帧推理一次。模型全部转为RKNN格式INT8量化。方案端到端总时延CPU占用内存峰值备注各跑各的4路独立78ms68%1.9GB帧率只能到13fps偶发卡顿同源调度CPU预处理51ms48%1.2GB帧率20fpsCPU仍然偏高同源调度RGA预处理36ms33%0.9GB帧率稳定27fps同源调度RGA异步后处理32ms28%0.8GB帧率稳定30fpsCPU无毛刺这个表是我多次压测后取的中位数。从中可以看出压力优化是逐层累积的共享数据省掉了重复拷贝RGA把缩放分流到硬件异步后处理把CPU的毛刺抹平了。到最后一档配置系统的CPU占用只有28%还能剩出不少资源给业务逻辑和网络通信用。4. 平台调优与常见坑做RK3588边缘视觉光有调度器还不够平台层面的坑和调优占据了一半工作量。这一节专门整理我在RK3588实测中遇到的问题和解决办法有些是RKNN工具链的有些是系统层面的。4.1 NPU、CPU、RGA的协同配置调度器搭起来之后还有一个重要工作是把线程绑定到合适的CPU核心上。RK3588的4个大核是Cortex-A76小核是A55。调度循环和采集线程要绑大核后处理工作池可以绑大核但优先级降低而一些低负载任务比如内存回收、日志上传可以绑小核。具体用pthread设置亲和性pthread_t thread pthread_self(); cpu_set_t cpuset; CPU_ZERO(cpuset); CPU_SET(3, cpuset); // 绑定到第4个大核 pthread_setaffinity_np(thread, sizeof(cpu_set_t), cpuset);这种固定绑核能有效避免系统自身的调度器把脑子搞乱。尤其在NPU和CPU一起高负载运行时如果线程不断在大小核之间迁移缓存命中率会掉得厉害你经常会看到CPU占用没变但推理时延加了10ms就是迁移导致的cache miss。另外RGA的调用次数要控制好。RGA2虽然快但它和NPU、DDR控制器共享带宽如果你在一个帧周期内调用RGA超过3次带宽可能就会成为瓶颈。所以我在前面提到“共享尺寸聚合”一定要做好把多个任务的分辨率合并成1-2档让RGA一帧最多压两次。4.2 高频报错与排查速查表下面是几个我在开发过程中频繁遇到的报错每个都是真实踩过的坑。报错信息/现象根因分析解决办法rknn_inputs_set failed: EINVAL输入缓冲区大小和模型要求的size不匹配检查input_attr给出的size分配后用memset清零再memcpyEACCES: Cannot open rknn model模型路径不对或者运行用户没有文件读取权限用完整路径并检查文件存在确保运行在root或有读权限的用户下Inference result all zeros输入图像的通道顺序/色彩空间和模型训练时不一致检查training时用的输入顺序使用cvtColor统一转成模型期望格式cant find suitable delayline这是显示模块的报错如果你用无头模式跑SDK不影响推理如果你接HDMI做调试换一个支持的分辨率或刷新率不接屏可忽略推理偶尔超时一帧卡40ms内存动态分配导致DDR高负载或heap碎片化在初始化时预分配所有大块内存不要在推理循环里new/malloc系统运行几个小时后内存逐渐上涨某些任务缓存了输出结果或RGA分配的内存未释放排查frame_release和RGA buffer释放逻辑可以用malloc_trim或监控/proc/meminfo这里面最坑的是“all zeros”问题。有一次我把BGR/RGB顺序写反了模型输出的所有类别分数都是0而且不会报任何错误你只能通过打印输出张量数值才能察觉。所以调试推理结果时第一时间把输出张量的stats打出来哪怕只是一个最小值最大值也能省半天时间。4.3 长时间运行的稳定性与散热联动边缘视觉设备往往是7x24小时连续运行的不像开发板玩一会儿就关机稳定性要求天然高。我在做完功能后专门做了72小时压测发现了一个很隐蔽的问题高负载持续半小时后RK3588会触发温控降频。如果不处理NPU和CPU的主频掉下来端到端时延会逐渐变大从32ms一路涨到50ms以上看起来像是系统“变慢了”。排查下来发现需要做两件事。第一是散热要跟上我是加了一个5010风扇通过PWM控制。第二是软件层面读取芯片温度做联动当温度超过65度风扇全速转低于50度风扇低速。这个逻辑很基础但必须在项目里落地。读取温度很简单标准sysfs接口就行cat /sys/class/thermal/thermal_zone0/temp # 单位是毫摄氏度比如63000表示63度风扇转速可以通过PWM风扇的hwmon节点读取例如cat /sys/class/hwmon/hwmon1/fan1_input如果你的板子风扇不支持转速反馈也可以用PWM占空比估算转速但最好还是选带测速线的三线风扇。长时间运行的另一个重点是内存碎片化。RGA和RKNN驱动在内核态会分配连续物理内存如果用户态代码频繁分配释放大块内存碎片化会导致某些内存分配失败表现就是跑着跑着突然推理失败。第二章节里说到的“内存池帧复用”就是针对这个问题的核心解法务必在工程里落地。4.4 周边小坑摄像头掉线、时间戳与日志策略除了核心算力调度周边细节也会影响整体稳定性。摄像头掉线是边缘设备最常见的故障尤其是IPC通过RTSP取流时。我的做法是采集线程增加自动重连逻辑如果连续3秒取不到新帧就释放旧连接重新建立RTSP会话。同时调度器必须感知到输入源异常暂停所有推理任务防止NPU空转和无效计算。时间戳也很重要。边缘视觉设备经常要回传事件给服务端如果事件的时间戳是按“系统获取时间”而不是帧采集时间在系统时钟因为NTP校时而发生跳变时会产生逻辑混乱。我在Frame结构里强制使用单调时钟CLOCK_MONOTONIC标记采集时间同时记录一个wall clock时间对应关系业务事件统一用采集时间戳。最后是日志。调度器在运行过程中我建议把关键事件打点比如每次NPU推理的耗时分布、延迟超时次数、抽帧率等用ring buffer缓存起来统一由低优先级线程异步写盘。绝不能把日志写在推理关键路径上否则日志IO本身会变成性能瓶颈。5. 这套方案还能往哪个方向扩展这套同源多任务调度框架在我自己的项目里已经稳定运行了几个月。如果后续想在它基础上继续迭代有几个方向非常值得做。第一个是引入动态模型切换。现在所有的任务模型都是静态加载的运行时不可变。实际业务中经常有“夜晚切换红外检测模型”、“白天切换遮阳帽检测模型”这类需求。改动思路是调度器增加模型热插拔接口当一个新模型被加载进context后任务节点自动切换到新模型的输入输出缓冲区。注意热切换前要暂停该任务一轮推理等当前推理释放后才能替换否则会读写到旧内存造成崩溃。第二个是异构算力分摊。RK3588上除了NPUMali GPU的通用计算能力也可以被利用起来。像NMS这类后处理逻辑本质上就是大量并行比较操作很适合用GPU跑。目前后处理还占着CPU资源如果能把NMS移到GPU或者RGA上做CPU占用还能再降5%到10%。不过这需要引入OpenCL开发工作量会增加不少。第三个方向是模型压缩蒸馏。同源调度框架解决的是“算力分配”问题但如果你把单个模型换成更小的蒸馏版本整体推理时延还能进一步下降。比如行人检测原本用YOLOv8s换成蒸馏后的YOLOv6-tiny或者定制的小head模型量化后推理时间可以从18ms降到8ms。省下来的算力又可以支撑更多同源任务边际收益很可观。回到事先给出的话——我在这个项目里最大的体会是边缘AI视觉的系统设计真正拉开差距的往往不是某个模型有多厉害而是你如何把这些模型组合进一个完整的、有序的流水线里。单点再强资源打架就全完了单点平平但调度得当系统反而能稳定高效地跑下去。所以如果你正在RK3588上做多任务视觉先别急着优化模型精度把同源多任务调度这层地基打扎实你的回报率比调优任何单个模型都高。
返回列表