ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RK3588边缘AI同源多任务调度实战:令牌桶策略与性能调优

RK3588边缘AI同源多任务调度实战:令牌桶策略与性能调优 做边缘AI视觉的项目做到第三期手头这块RK3588平台的板子已经跑了差不多三个月。前两期分别聊了画质采集和模型转换这次想认真聊聊一个特别容易被忽视、但上线后必炸的问题同源多任务调度。什么叫“同源多任务”说白了就是同一路视频流或者同一个相机传感器出来的画质数据同时喂给多个AI任务去处理。比如我这边一个边缘盒子接了四路IPC每一路画质如果都拿去跑YOLOv8检测、再跑一个人脸质量评估、再抽帧送OCR那就是典型的同源多任务。如果不做调度结果就是NPU利用率忽高忽低、CPU被打满、帧率乱跳甚至任务之间互相挤爆内存——这些问题我都踩过。这篇文章就把我这几个月在RK3588上做同源多任务调度的完整思路、方案选型和落地代码讲清楚。适合正在用RK3588做边缘视觉盒子、多路视频分析、多模型级联推理的朋友参考已经踩过坑的也可以对照看看有没有更好的解法。1. 先把需求想透同源任务到底卡在哪里1.1 “同源”的三层含义先说清楚“同源”这个词。它不只是指“同一路视频流”在实际工程里有三层意思。第一层是数据同源。多个AI任务消费的是同一帧画质数据比如同一路1080p图像即要跑检测又要跑分类。这就带来一个基本问题是每个任务各自去解码一份还是解码一次、共享一份答案显然是后者但实现起来却没那么简单因为有些模型要640×640输入有些要320×320有的要做归一化有的要做BGR/RGB转换。如果每个任务都在自己的线程里把原图重新缩放、转换一遍CPU和内存带宽就白白浪费了。第二层是模型同源。这里的“同源”不是指同一个模型文件而是指所有模型都来自同一套训练体系、用同一个工具链rknn-toolkit2导出、跑在同一个RKNN运行时上。模型同源的好处是格式统一、量化方式一致、runtime错误处理可以复用但同时也意味着它们抢的是同一块NPU资源。第三层是框架同源。所有的AI任务都在同一个进程里用同一套采集、推理、发布框架来跑。这样做的好处是资源池可以统一管理坏处是一旦某个任务出问题可能会把整个进程拖垮所以调度器必须要有任务隔离能力。三层同源叠加结论就是这类项目本质上不是“把N个单任务拼在一起”而是要做一个任务调度系统把共享的数据、模型、算力统一管起来。1.2 RK3588的算力底账做调度之前得先知道手里有多少资源。RK3588的NPU号称6 TOPS算力这个数字听着不小但要注意两个细节。第一6 TOPS是INT8算力。如果模型是FP16跑的实际算力打对折如果用了FP32基本只能当玩具。做边缘视觉部署模型几乎全部要INT8量化所以这6 TOPS是可用的理论上限。第二这6 TOPS不是每帧都吃满的。实际推理效率取决于模型结构、输入分辨率、NPU算子支持和batch大小。我实测过YOLOv8s转成RKNN后INT8推理640×640输入在RK3588 NPU上大约需要12~18ms一帧换算下来单模型峰值也就跑个55到80帧。听着还行吧但如果你要同时跑三个模型再加上解码、缩放、后处理的开销帧率很快就会掉到一个不可接受的水平。所以做调度的核心不是把NPU“榨干”而是在有限的算力里合理分配资源让每个任务都拿到它该有的帧率。这里的关键词是“该有的”——不是所有任务都需要满帧跑。为了直观我列一下我这边的资源预算资源项实际可用量说明CPU 核心4×A76 4×A55可以绑核隔离但大核不能全给推理NPU6 TOPS INT8实际效率大约85%~90%内存视板卡配置而定多路1080p解码H264编码会吃掉大量带宽编解码器VPU支持多种格式和NPU互相独立可以并行1.3 真正稀缺的其实是内存带宽这一节我想单独拎出来说。很多人做多任务调度的时候只盯着NPU利用率结果发现CPU还有余量、NPU也没吃满帧率却上不去。这时候问题大概率出在内存带宽。RK3588虽然是旗舰芯片但内存控制器是共享的。当四路视频流同时解码、每帧数据又要做缩放和格式转换、NPU又要频繁读写权重和中间特征图、编码器还要取帧去编码——所有数据流都在抢总线带宽。带宽一旦打满各模块的等待时间会急剧上升表现出来的就是“CPU不高NPU不高但就是卡”。这也是我最终决定做统一调度的根本原因所有任务对数据的访问必须被管理起来不能任由每个模块自己去抢内存。具体做法后面的代码部分再展开这里先记住一个结论调度的第一优化目标是数据复用和带宽控制而不是单纯地“让NPU多干活”。2. 调度方案选型为什么不能一股脑开多线程2.1 多线程硬跑会遇到的三座大山一开始我图省事想直接用多线程解决。每个任务起一个线程每帧图像各处理各的代码也简单。结果上线一压测就翻车了主要遇到三座大山。第一座是RKNN推理的线程安全问题。RKNN Runtime的rknn_run接口不是线程安全的多个线程同时往同一个context里塞推理请求轻则报错重则直接段错误崩溃。当然你可以一个任务开一个独立的rknn_context但这样模型权重在内存里就要放多份本身就造成了内存浪费。第二座是CPU后处理抢占。YOLOv8有解码decode、NMS这些后处理RKNPU只负责卷积计算后处理是在CPU上跑的。多个任务同时做后处理CPU就频繁切换线程上下文调度开销很大而且A76大核全被占满后视频解码线程反而抢不到CPU画面就开始掉帧。第三座是帧率漂移。每个任务的处理时间不是一个恒定值模型输入尺寸不同、画面内容复杂度不同、NPU当前还有没有排队都会让每一帧的处理耗时在几分之一毫秒到几十毫秒之间波动。如果没有调度就会出现“任务A跑了50帧任务B只跑了20帧”这种严重不均衡的情况。2.2 几套调度方案的对比多线程硬跑行不通那就得选调度方案。我调研和实测了几个方向。第一种是手写轮询调度。最朴素所有任务排队按固定顺序依次推理。实现简单但短板明显如果低优先级任务占用了一个耗时的推理请求高优先级任务就得在后面干等时延不可控。第二种是优先级抢占。让高优先级任务可以插队低优先级任务靠后。这个在理论上不错但RKNN的推理请求一旦提交给NPU中途是没办法打断的所以“抢占”只能发生在任务还没提交之前。当一个推理请求要跑100ms、高优先级任务需要等它结束才能提交的时候时延一样不可控。第三种是时间片轮转。和操作系统的调度类似给每个任务分配时间片到时间就切换。但这个很难适配AI推理——推理是一个不可切分的原子操作你不可能让NPU算到一半去算另一个模型。第四种是令牌桶/带宽配额调度。这是我最终选用的方案。核心思路是把NPU当成一个带吞吐上限的资源池每个任务按权重拿“推理令牌”没有令牌就等着。这个方案不要求推理可打断只需要控制每个任务的提交频率实现起来简单而且非常符合边缘视觉的实际场景——因为绝大多数的视觉任务对单帧时延不敏感但对整体吞吐和帧率稳定性有要求。2.3 为什么令牌桶适合边缘视觉我一直觉得边缘AI调度不应该做成通用操作系统调度那种复杂模型而应该贴近业务。视觉任务有一个天然特点有周期性。每一帧视频数据到达的时间是相对均匀的任务对每一帧的处理也有固定的“兴趣”——有的任务希望每帧都处理比如主检测有的任务每隔几帧处理一次就行比如人脸质量评估。令牌桶这种方案正好就是围绕“周期配额”设计的。它允许我为每个任务定义一个目标帧率和一个优先级权重。目标帧率决定它“要多少”权重决定它在资源紧张时“优先拿到多少”。当NPU空闲时所有任务都能超过目标帧率跑当NPU繁忙时资源按照权重重新分配。这样得到的不是理论最优而是工程上最稳的。3. 调度器核心实现从RKNN模型加载到帧级调度3.1 模型准备rknn-toolkit2转换与关键参数调度器的地基是模型本身。RK3588上的RKNN模型要用rknn-toolkit2来转换这里有两个参数我建议反复确认。第一个是target_platform必须设为rk3588。如果设成别的平台模型也能跑但算子优化会用通用策略老平台拿不到RK3588特有的算子融合能力推理性能打折扣。第二个是量化方式。实测下来quantized_dtypew8a8配合optimization_level3是这个平台上稳定性和性能平衡得最好的组合。如果你的模型里有对精度特别敏感的层可以在转换时报错后手动加custom_op指定混合精度但尽量在训练端就做好量化感知训练而不是全靠转换时硬压。转换后的RKNN模型文件每个任务一份。加载的时候有一点要特别注意如果多个任务共用同一个模型文件比如多路视频跑同一个检测模型建议只加载一次然后用同一个rknn_context配合多个rknn_input对象交替推理。实测下来这比加载多份副本要省将近一倍的模型内存对多路场景特别重要。3.2 调度框架的总体结构我最终实现的调度器是一个三线程模型加一个帧池采集线程负责从摄像头或视频流取帧统一解码做一次预处理缩放、格式转换然后放到帧池里。调度线程从帧池拿帧根据每个任务的配额和优先级决定“这一帧要不要喂给任务A”“要不要给任务B”然后按顺序调用RKNN推理。后处理线程池推理完成后把原始的RKNN输出丢给后处理线程池由多个线程并行做解码、NMS、逻辑判断。这样画的合理性在于采集、调度、后处理之间的数据依赖被解耦了每个环节的耗时波动不会直接传导给其他环节。帧池起到“缓冲”作用短期峰值可以被吸收掉。帧池我用的核心数据结构是带引用计数的共享帧对象。每个任务拿到帧后只做读取不拷贝只有当需要送RGA做缩放时才会申请新内存。引用计数归零就把帧回收到空闲队列不重复申请内存。3.3 令牌桶调度器代码调度器本身用Python实现会比较慢我最终的版本是C写的但核心逻辑用Python描述反而更清晰。先看框架代码class TaskItem: def __init__(self, name, context, target_fps, priority_weight): self.name name self.context context self.target_fps target_fps self.priority_weight priority_weight self.token_pool 0.0 self.last_submit_time time.time() def refill_tokens(self, now): # 按目标帧率补充令牌最多累积2帧的量防止积压 elapsed now - self.last_submit_time self.token_pool min(2.0, self.token_pool elapsed * self.target_fps) self.last_submit_time now class TokenBucketScheduler: def __init__(self, tasks): self.tasks tasks self.total_weight sum(t.priority_weight for t in tasks) def select_next_task(self): now time.time() for t in self.tasks: t.refill_tokens(now) # 找出令牌够且权重比例最高的任务 best None best_score float(-inf) for t in self.tasks: if t.token_pool 1.0: score t.priority_weight / self.total_weight if score best_score: best t best_score score return best这个算法的关键在于target_fps决定了令牌补充速度priority_weight决定了竞争时的优先顺序。当NPU空闲时每个任务都能拿到令牌跑超过目标帧率当NPU繁忙时权重高的任务会优先抢到推理机会。实际使用时我把权重做了归一化处理比如三路任务权重分别是5:1:1那么在NPU饱和情况下任务A能拿到的推理次数大约是任务B和任务C的5倍。这个比例可以根据业务重要性灵活调整。3.4 帧同步多任务结果怎么合并同源多任务还有一个容易忽视的问题多个任务处理同一帧画质数据后结果要合并到一条结构化记录里供上层业务使用。我的做法是给每一帧挂一个FrameContext对象里面有一个task_results字典。调度器把帧交给某个任务推理后任务完成时把结果写入这个字典当所有预定任务都完成或者超时就回调一个on_frame_completed函数把整帧的结果推给业务层。这里要特别处理“迟到”的情况。假设任务是每隔三帧才做一次人脸质量评估那它可能在下一帧检测任务都完成之后才出结果。这时不能傻等我的策略是回调只在主检测任务完成后被触发其他任务的结果以“附注”形式在下一个批次写入。换句话说慢任务的结果永远不会阻塞主流程它只会“迟到”。3.5 RKNN推理的排队和隔离RKNN推理不能直接从多个线程并发往同一个context塞数据所以我在调度线程里维护了一个推理队列。调度线程选出下一帧要跑的任务后把输入整理好调用rknn_run。因为所有rknn_run都是从同一个调度线程发起的就天然避开了线程安全问题。如果模型之间差异很大有的量化、有的不量化有的输入是NV12有的要RGB那就必须在提交推理前由调度线程统一做好格式转换。这个转换我有意识地在RGA硬件里做而不是CPU软转避免吃掉大量CPU带宽。RGA操作的开销大约每帧0.1~0.3ms比CPU软转的1~2ms好太多。4. 性能调优与实测数据4.1 四路视频流三个任务的实测结果把调度器写完以后我在实际场景里跑了压测。平台配置是RK35888GB内存四路1080p/30fps视频流输入。任务配置如下任务输入尺寸模型类型目标帧率权重主检测640×640YOLOv8s INT8255人脸质量评估320×320MobileFaceNet51车辆颜色分类224×224ResNet18 INT851压测30分钟采集到的平均数据指标数值主检测实际帧率28.3 FPS人脸质量评估实际帧率5.1 FPS车辆颜色分类实际帧率4.9 FPSNPU 平均利用率84%CPU 平均占用率4大核62%丢帧率0.4%这个结果基本符合预期。主检测超过了目标帧率说明NPU当时其实还有余量人脸和分类卡在目标帧率附近说明权重设置和配额设置起到作用了。另外这组数据里丢帧率只有0.4%主要集中在网络抖动导致的采集停顿调度器本身没有造成明显丢帧。4.2 几个关键的调优细节第一个是输入格式。RKNN推理时输入格式要从rknn_input里设置fmtNV12转RGB可以直接用RGA推荐fmt RKNN_TENSOR_NHWC而不是NCHW。虽然RKNN内部会处理但NHWC对RGA的输出更友好少一次内存转置。第二个是后处理的CPU绑核。我把YOLOv8的decode和NMS线程绑定到A76大核上视频解码线程绑定到A55小核上这样两个CPU密集模块互相不干扰。实测绑核后后处理延迟从平均6ms降到了4ms左右。第三个是NPU的batch设置。如果你的任务模型是单帧输入batch设为1即可。RK3588的NPU支持batch推理但多batch的模型加载时间和首帧延迟都会上升在一路视频连续推理的场景里并不划算。4.3 散热和风扇转速控制不可忽视的一环跑多任务调度之后NPU长时间高负载RK3588的发热量会迅速上来。我最初没管散热结果跑了一个小时之后发现帧率悄悄掉了20%——板子过热降频了。这里就牵扯到风扇控制。RK3588本身有硬件PWM风扇接口Linux内核里有pwm-fan驱动。我直接通过/sys/class/hwmon/hwmon0/fan1_input读取风扇转速并通过/sys/class/thermal下的温度节点做PID调速。实测下来的经验是别用固定的高转速那样噪音大也没必要。我在板子上做了个三级策略温度低于60度时风扇低速3000转60~70度中速4500转超过70度直接满转。这样既保证了散热又控制噪音。关于风扇调速有个小坑调PWM占空比之前一定要确认内核的设备树里启用了pwm-fan节点否则你写了pwm1节点半天没反应排查老半天。5. 常见问题排查与避坑记录5.1 多个rknn_context切换导致首帧卡顿用多个模型的时候必然要创建多个rknn_context。我踩过的一个问题是交替调用两个不同context的推理时每个模型第一次推理都特别慢有的要200ms以上。后来发现这是NPU在做权重换入换出特别是模型比较大的时候换入换出一个context需要几十毫秒。解决办法有两个方向一是调整调度策略尽量把帧按批次喂给同一个模型减少context切换次数二是如果几个模型确实需要频繁交替那就得评估是否值得把它们融合成一个多输出模型。比如我的检测人脸质量评估本来是两个模型后来我把人脸质量评估的输入特征直接从检测模型的backbone中间层接出来顺带还省了一次全图前向计算。当然这种模型融合方式对算法能力有要求不一定适合所有人但确实是一个可以思考的方向。5.2 decode后处理一直报“suitable delayline”这个报错是在调RGA和VOP视频输出时遇到的一开始以为是视频解码出问题后来发现和VPU、RGA的时钟同步有关报错文本里直接写着cant find suitable delayline。如果你在调试过程中看到这种跟时钟相移相关的报错先检查内核设备树里各模块的时钟配置特别是VPU和VOP的父时钟是否已经正确配置。另外一个笨但有效的办法是升级内核或使用官方发布的SDK BSP因为你不能确定裁剪过的内核是否保留完整的时钟树配置。5.3 模型加载时报错miniloader.binRKNN模型加载时报miniloader.bin load failed的错本质是runtime找不到配套的loader。常见原因有两种一是系统里RKNN runtime的版本和rknn-toolkit2的版本不一致模型虽然是新版导出的但板子上的librknnmrt还是旧版二是动态库安装路径有问题。我的建议是直接用官方SDK里配套好的runtime版本不要自己从网上散装下载版本组合不对排查起来非常耗费时间。5.4 队列积压导致内存越用越大调度器跑久了内存一直上涨多半是某个任务的消费速度跟不上生产速度导致帧池里的共享帧对象越积越多。排查思路是给每个任务队列加上积压上限比如主检测任务最多排队5帧人脸评估最多排队2帧超过了直接丢帧。丢帧总比内存耗尽崩溃好。另外记得在帧对象回收的时候把其中所有引用计数归零不然内存泄漏会非常隐蔽。5.5 优先级反转比看起来更可怕所谓优先级反转就是低优先级任务占了高优先级任务需要的资源导致高优先级任务反而被拖慢。在AI调度场景里最容易出现的优先级反转发生在帧池低优先级的后处理任务占着帧对象的引用高优先级的新帧没法复用这块缓冲只能重新申请内存。时间一长内存分配次数变多整个系统性能会慢慢下降。我的解决办法是给帧池做两级缓冲高优先级任务使用静态预分配缓冲低优先级任务使用动态缓冲。这样即使低优先级任务把动态缓冲耗光了也不会影响高优先级任务的性能表现。写在最后用RK3588做边缘AI视觉的同源多任务调度本质上是在算力、时延、稳定性这三者之间找平衡。我在这套方案上花的时间不多但踩的坑不少最核心的心得是不要一开始就想做一个完美的通用调度器而是先把业务场景拆清楚设定好目标帧率和优先级权重然后让调度器简单可信地执行。令牌桶策略在工程落地里最大的优点就是好理解、好调参、出问题容易排查——这比花里胡哨的算法重要得多。如果你现在正在做类似的项目建议从四路相机两个小模型开始跑先把调度框架验证通过再逐步加任务。RK3588的潜力其实比很多项目里流露出的“勉强够用”要大只要调度做对了一个盒子同时扛住检测、识别、分类基本不是问题。后面我还会继续更新边缘AI视觉系列聊聊把这一套调度器从板端裸机迁移到容器化部署的实践有兴趣可以持续关注。
返回列表