
1. 从“一路视频多个模型”说起搞嵌入式AI的朋友应该都有这种感觉RK3588这颗芯片在边缘端真算得上“六边形战士”。8核CPU、Mali-G610 GPU、6TOPS算力的NPU还有完善的ISP和硬件编解码单元一颗SoC几乎能把视觉相关的活儿全包了。但资源丰富是一回事真正把资源用起来、用出效率又是另一回事。我最近在做的项目正好卡在这个点上一块RK3588板子上接了一路RTSP摄像头同时又接了USB摄像头和本地视频文件作为输入源。业务侧的需求是同一路视频流要同时跑yolov8检测、车辆颜色分类、还有一个人形分割模型。如果按常规做法——每个模型开一个独立线程、各拉各的流、各占各的NPU内存——用不了多久板子就会卡死或者NPU OOM。于是就有了这篇文章的核心主题RK3588边缘AI视觉中的“同源多任务调度”。所谓“同源”指的是多个AI推理任务共享同一个或同一组视频输入源在这个前提下做统一的任务调度和NPU资源管理。这篇内容主要写给两类人一类是把yolov8往RK3588上部署、但发现多模型同时跑就跑不动的开发者另一类是正在做边缘视频监控、多路视觉业务集成的朋友。我会把我在实际项目中调通的方案、踩过的坑、排过的错尽量系统地讲清楚希望能让你少走几步弯路。2. 整体设计与调度思路拆解2.1 为什么需要“同源多任务”而不是“各自为战”你可能会想既然RK3588算力够强每个任务独立拉流、独立推理不就行了如果业务简单比如就一路摄像头跑一个yolov8确实可以这么干。但一旦任务变多问题就来了。首先是内存问题。RK3588的NPU内存通常在系统内存里划分可用的CMA区域或者ION buffer是有限的。每个模型加载进去都要占用固定的权重空间和输入输出buffer。yolov8s的权重大约50MB分类模型小一点分割模型又大一截。三个模型各加载一份同时还要给推理session留足动态内存内存直接就紧张了。其次是编解码资源冲突。RK3588的VPU视频编解码单元能力有一定上限当多路视频同时硬解码时会有通道数的限制和多路抢占的问题。如果每个线程都从摄像头拉流、各自硬解很容易出现“谁都能跑但谁都跑不顺”的情况。第三是CPU调度抖动。每个模型推理前的预处理resize、归一化、颜色空间转换都是CPU密集操作。三个线程同时预处理系统平均负载飙升可能导致推理任务没有得到及时调度帧率波动剧烈。同源多任务的核心价值就是把这些重复的拉流、解码、预处理工作合并成一份让不同模型共用同一份输入数据再把NPU的推理请求统一排队、统一调度。数据只有一份但可以喂给不同的模型NPU只有一份但可以通过任务队列合理分配让每个模型都有稳定的推理机会。2.2 调度器的三层结构设计在设计“同源多任务调度”的时候我参考了快递分拣中心的工作方式视频流入库相当于快递进场解码模块就是卸货口每个模型就是不同的分拣线。卸下来的包裹放在传送带上分拣线按需取件而传送带的控制中心统一调配流量。这里的核心调度结构分为三层第一层是数据生产层。负责统一拉取视频流、解码、抽帧把图像帧放到一个循环缓冲区中。这个缓冲区是整个系统最关键的公共资源所有模型共享同一份帧数据。缓冲区大小需要根据推理速度和业务实时性要求来设计我后面会详细讲参数选择。第二层是任务管理层。维护一个“推理请求队列”每个模型根据自己的运行频率和当前负载向队列提交推理请求。请求中包含模型ID、帧ID、优先级、期望的最晚推理完成时间。调度器根据优先级和截止时间决定先处理哪个请求。第三层是NPU执行层。RK3588的NPU支持同步和异步两种推理模式。同步模式简单但串行异步模式可以重叠预处理和推理吞吐量更高。执行层把队列中的请求一次一个或分批交给NPU执行并在推理完成后把结果返回给对应的业务线程。这三层各司其职数据生产层解决“数据重复”的问题任务管理层解决“如何分配”的问题NPU执行层解决“效率最大化”的问题。2.3 选型对比为什么不直接用多线程 同步推理最开始我的想法很简单每个模型一个std::thread都在主循环里同步调用RKNN的rknn_run不就行了但实际跑起来之后发现三个问题。第一个是排队严重。NPU的rknn_run是串行执行的三个线程同时调用底层会阻塞等待。由于NPU执行时间不等yolov8s在RK3588上大约30-50ms分类模型5-10ms分割模型80-100ms如果分割模型先占用了NPU检测模型的实时性就完全没法保证。第二个是帧数据拷贝开销。每个模型虽然共享同一个视频源但如果各写各的预处理逻辑每个模型都需要一份RGB数据。如果输入源是1080p一帧RGB888就是约6MB三个模型同时拷贝就是18MB的搬运量这还不算在NPU输入格式转换上的CPU计算开销。数据显示在RK3588上1080p图像从NV12转RGB并做letterbox大约要耗费15-25ms的CPU时间——三个模型各自做一遍CPU就没时间干别的了。第三个是优先级完全不可控。检测模型需要实时响应分类模型可以稍慢一点分割模型即使延迟200ms也没关系。但多线程同步推理没有“优先级”的概念全是操作系统的线程调度说了算这显然不符合实际业务需求。所以最终我放弃了“各跑各的”方案改成共享输入源、集中调度、异步执行的架构。这个改造直接解决了NPU冲突和CPU超额开销的问题帧率稳定性也从“忽高忽低”变成了“平平稳稳”。3. 核心实现细节与调度策略3.1 循环缓冲区多模型共享帧数据的关键循环缓冲区是整个调度系统的心脏。它存储的是经过解码后、尚未做模型特定预处理的原始帧NV12格式或BGR格式以及对应的帧元信息时间戳、帧序号、来源通道ID等。缓冲区的大小直接决定了系统的延迟和丢帧策略。如果缓冲区太大视频源产生的帧堆积过多业务取到的帧就是“过时”的帧实时性变差如果太小模型处理不过来时就会频繁丢帧漏掉目标出现的瞬间。我的经验值是缓冲区深度 最大单模型推理耗时 / 视频帧间隔 × 1.5。比如1080p25fps的摄像头帧间隔是40ms。假设最慢的分割模型推理耗时100ms那么缓冲区深度 100/40 × 1.5 ≈ 4帧。预留1.5倍是为了应对瞬时抖动避免缓冲区直接打满导致生产者阻塞。这里有一个取舍细节缓冲区深度的“帧”是整个视频帧的引用计数不是拷贝。也就是说每个模型从缓冲区取帧时并不是复制一份完整的图像数据而是增加一次引用计数等到所有模型都用完了这帧才真正释放内存。这样无论有多少个模型共享同一帧内存中只有一份图像数据。我在实现时用了一个简单的shared_ptr 引用计数器实测在1080p分辨率下3个模型同时取帧内存占用几乎没有额外增长。3.2 任务队列与调度策略时间片轮转还是优先级抢占任务队列的设计是整个系统的调度中枢。这个队列中的每个元素代表一次具体的推理请求。请求的结构体大致如下struct InferenceRequest { int model_id; // 哪个模型来执行推理 uint64_t frame_id; // 对应哪一帧视频数据 int priority; // 0-255数值越高优先级越高 uint64_t deadline_us; // 截止时间超过这个时间结果就没意义了 void* input_data; // 输入数据指针已由调用方完成预处理 RKNN_OUTPUT* output; // 推理结果存放位置 };调度策略方面我对比过两种方案。第一是固定优先级抢占每个模型分配一个固定优先级调度器始终执行优先级最高的请求。这个方案实现简单但坏处是低优先级任务可能长期得不到执行出现“饿死”现象。比如检测模型的优先级高一直有请求进来分割模型就永远排不上队。第二是最早截止时间优先调度器从请求队列中取 deadline_us 最小的请求优先执行。这个方案的好处是每个模型都能根据自己的最大容忍延迟设置截止时间长期得不到执行的任务会因为截止时间逼近而获得更高调度权重。我最终采用的是“优先级 截止时间”的混合策略。在队列头部先按优先级排序优先级相同的请求内部再按截止时间排序。同时设置一个上限如果某个请求的 deadline_us 距离当前时间小于某个阈值比如20ms则“强制插队”即使它的优先级不是最高。这样可以避免极端情况下的饿死问题。具体来说RK3588上的yolov8检测设置了最高优先级因为检测结果要驱动后续业务逻辑车辆分类稍微低一些因为它可以在检测框确定之后再快速执行分割模型的最低只要保证每秒2-3次推理即可。3.3 RKNN推理的异步模式把等待时间变成处理时间在RK3588上跑RKNN模型官方提供了两种接口同步模式rknn_run和异步模式rknn_run_async。同步模式好理解调用rknn_run之后线程阻塞等待NN输出。这个模式编码简单但CPU在等待期间完全闲着非常浪费。异步模式则不同rknn_run_async提交任务后立即返回系统内部会把推理任务交给NPU执行CPU线程可以去做别的事情比如下一个模型的预处理。当NPU执行完成后再通过查询或回调方式获取结果。在“同源多任务调度”这个场景下异步模式几乎是必须的。因为多个模型共享一个NPU如果第一个模型用同步模式老老实实等NPU跑完第二个模型即使已经预处理完毕也只能排在后面干等。而异步模式下第二个模型可以在第一个模型NPU执行的这段时间里继续做自己的预处理把CPU的空闲时间利用起来。我的实现方式是这样的// 执行循环运行在独立的推理线程中 while (running) { auto req scheduler-dequeue(); // 获取最高优先级请求 if (!req) { std::this_thread::sleep_for(std::chrono::milliseconds(2)); continue; } // 异步提交推理 rknn_run_async(ctx, req-input, req-output); // 不等NPU先处理后续的预处理任务 auto preprocess_task scheduler-prepare_next(); if (preprocess_task) { do_preprocess(preprocess_task-frame, preprocess_task-dst); } // 等待NPU完成 rknn_wait(ctx, req-output); // 把结果通知对应模型线程 notify_complete(req-model_id, req-frame_id, req-output); }这个循环的精髓在于把NPU执行时间和CPU预处理时间重叠起来。在等待NPU完成的同时当前线程先去做下一个请求的预处理等NPU跑完了再回来取结果。实测下来综合吞吐量比同步模式提升了30%左右。3.4 多路输入源的管理与时间戳同步在RK3588上接多个输入源是一个很常见但不简单的需求。我在项目中同时接了RTSP网络相机、USB摄像头和一个本地视频文件。三种输入源的帧率、分辨率、时间基准都不一样如果直接混在一起调度器的“帧”概念就会混乱。RTSP相机我用的是FFmpeg硬解码RK3588的VPU支持H264/H265硬解USB摄像头走V4L2读取YUYV格式再转成NV12本地视频文件也用FFmpeg解出来。三路源的解码线程共享同一个“数字时钟”这个时钟以RTSP相机的PTS显示时间戳为主基准其他路源在数据到达时与主时钟对齐打上统一的frame_id。frame_id采用32位递增计数器从0到UINT32_MAX循环。调度器看到frame_id就能知道哪一帧先到、哪一帧后到即使不同源的帧率不同也能保证模型拿到的输入帧顺序一致不会出现“检测的是第100帧分类的是第101帧”的错位情况。4. 实操过程在RK3588上一步步实现和验证4.1 环境与基础配置这块内容需要你手头有一块RK3588开发板我这边用的是正点原子的RK3588开发板系统是Debian 11内核版本5.10。NPU驱动和RKNN Toolkit版本要匹配我用的是rknn-toolkit2 1.5.2版本配套的librknnrt.so。环境准备的关键步骤确认NPU驱动正确加载执行ls /dev/rknpu如果输出rknpu设备节点说明驱动正常。确认librknnrt版本strings /usr/lib/librknnrt.so | grep version确认版本与你的RKNN模型匹配。版本不匹配经常导致推理报错或结果不对。设置NPU工作模式RK3588的NPU支持0低功耗、1平衡、2性能三种模式在业务代码里可以通过rknn_set_npu_core_mask或系统节点/sys/kernel/debug/rknpu/freq调整。我的项目因为要长时间稳定跑设在模式1NPU频率默认温控和性能平衡得比较好。如果只求性能可以切到模式2但要注意散热。说到散热这里顺手提一句我在RK3588上的经验性能模式下NPU长时间满载核心温度会快速上升到85℃以上板载风扇会全速转。板子上的PWM风扇转速可以通过读取/sys/class/hwmon/hwmon*/fan1_input这种节点拿到数值。我做个一个简单的监控告警当风扇转速持续超过5000RPM且温度超过75℃时主动降低非关键模型分割模型的推理频率让算力回归到主要任务上。这个策略对设备长期稳定运行很有帮助。4.2 模型转换yolov8和分割模型部署到RK3588模型转换是RKNN部署里最容易出幺蛾子的环节。yolov8官方是PyTorch格式要跑在RK3588上必须先转成ONNX再通过RKNN Toolkit转换为RKNN格式。转换流程如下第一步导出ONNX模型。用yolov8官方仓库的export.py脚本导出时加上--opset 12参数并且要固定输入尺寸为640x640。不要用动态shapeRKNN对动态shape支持不好。python export.py --weights yolov8s.pt --include onnx --opset 12 --imgsz 640第二步用RKNN Toolkit写转换脚本。以下是我在项目中使用的转换配置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./yolov8s.onnx) assert ret 0, load_onnx failed # 配置量化yolov8s我用int8量化精度损失在2%以内 ret rknn.build( do_quantizationTrue, dataset./dataset.txt, # 量化校准图片列表 pre_compileFalse ) assert ret 0, build failed # 导出RKNN ret rknn.export_rknn(./yolov8s.rknn) assert ret 0, export failed这个过程有两个容易踩的坑。第一个是量化校准数据集。dataset.txt里要准备好几十张覆盖不同场景的图片光照变化、目标大小变化必须是模型训练时类似的分布。我一开始图省事只放了10张图片量化后的模型在夜间场景下漏检严重后来扩展到100张各类场景的图片问题明显改善。第二个是某些算子兼容性问题。yolov8的检测头中包含一些自定义算子如DFL结构RKNN在转换时可能不完全支持需要手动改模型结构或替换算子。我用的是yolov8官方仓库导出配合rknn-toolkit2 1.5.2不存在算子问题。但如果你用的是其他框架或改动过的网络结构转换时就要留意报错信息必要时需要把不支持的算子抽出来改用CPU实现。分割模型我用的是一个轻量级的人形分割模型基于PP-HumanSeg的TensorFlow版本转ONNX后转RKNN的流程与yolov8类似唯一区别是输入尺寸是192x192量化比特为int8。分类模型体积更小用的是ResNet18的timm版本输入224x224转换零压力。4.3 调度器代码核心实现调度器的代码不算太长但几个关键细节都集中在这里。我先把核心部分放出来再逐个解释。class TaskScheduler { public: explicit TaskScheduler(size_t queue_size) : capacity(queue_size) {} bool enqueue(InferenceRequest req) { std::lock_guardstd::mutex lock(mtx); if (req_queue.size() capacity) { return false; // 队列满拒绝请求 } req_queue.push_back(std::move(req)); cv.notify_one(); return true; } InferenceRequest dequeue() { std::unique_lockstd::mutex lock(mtx); cv.wait(lock, [this] { return !req_queue.empty() || stop_flag; }); if (req_queue.empty() stop_flag) { return {}; // 返回空对象作为终止信号 } // 找到最高优先级的请求 auto it std::max_element( req_queue.begin(), req_queue.end(), [](const InferenceRequest a, const InferenceRequest b) { if (a.priority ! b.priority) return a.priority b.priority; return a.deadline_us b.deadline_us; } ); InferenceRequest req std::move(*it); req_queue.erase(it); return req; } // 判断是否可以立即执行用于异步模式下的预处理准备 bool has_next() { std::lock_guardstd::mutex lock(mtx); return !req_queue.empty(); } private: std::mutex mtx; std::condition_variable cv; std::vectorInferenceRequest req_queue; size_t capacity; bool stop_flag false; };这段代码有两个细节值得说明。第一为什么用std::vector而不是std::priority_queue。因为在实际实现中排队条件不只是优先级还有截止时间的逼近强制插队逻辑以及在队内移除某个特定请求的需求比如某帧数据在其他模型处理中失效。std::priority_queue不支持遍历和删除特定元素而业务逻辑中经常需要单独处理某个请求所以用vector做容器每次取max_element即可。队列长度一般不超过十几项遍历开销可以忽略。第二dequeue返回后调用方推理线程就拿到了最高优先级的请求。但如果CPU此刻需要做预处理而NPU又在忙怎么处理我的做法是推理线程先调用rknn_run_async提交请求然后在NPU执行期间从调度器再拿一批请求做预处理调用预处理函数不调用推理。这样可以充分利用NPU等待时间。具体实现代码在上文3.3节已给出。4.4 多模型加载与NPU内存分配策略在调度器跑起来之前各个模型的RKNN context要先创建好。RK3588的NPU内存分配有个值得注意的点每个模型创建独立的context但NPU内存池是共享的。我在项目中先加载yolov8s再加载分类模型最后加载分割模型。加载顺序会影响内存碎片——大模型先加载再加载小模型内存利用率最高。如果先加载了分割模型占内存大再加载yolov8也大容易出现NPU内存分配失败。每个模型的context用rknn_init创建创建时指定RKNN_FLAG_PRIOR_HIGH之类的标志可以设置NPU优先级。我的分配策略是yolov8s优先级最高RKNN_FLAG_PRIOR_HIGH分类模型中等不设或RKNN_FLAG_PRIOR_MEDIUM分割模型最低RKNN_FLAG_PRIOR_LOW。这样即使调度器队列为空、模型自行调用rknn_run也不会出现低优先级模型长期霸占NPU的情况。加载完成后每个模型需要预先分配输入输出buffer。RKNN提供了rknn_create_mem来分配内部连续内存建议每个模型都只分配一次不要在推理循环中反复创建释放。反复创建释放会导致内存碎片化长时间运行后NPU分配失败的概率会大幅上升。4.5 各任务结果回传与业务集成调度器处理完一个请求后需要把结果高效地回传给对应的业务模块。我的实现方式是每个模型维护一个独立的结果队列推理线程完成NPU执行后把解析好的结果检测框数组、分类标签、分割掩码放入对应模型的队列中由业务线程按需取用。在解析yolov8输出时我遇到一个和RK3588强相关的问题RKNN的输出数据排列。Yolov8的检测头输出是一个 (1, 84, 8400) 的tensor8400是三个尺度80x8040x4020x20的总预测框数。但RKNN在int8量化后输出数据可能与PyTorch里的顺序略有不同需要做一次维度重排否则解析出来的框坐标全是乱的。我的解析策略是这样的void parse_yolov8_output(int8_t* output, float scale, int num_classes, int img_w, int img_h, float conf_thresh, std::vectorDetBox boxes) { // 重点是输出数据的排布RKNN的int8输出是NHWC格式 // 这里把8400个候选框遍历一遍筛选置信度高的框 for (int i 0; i 8400; i) { float conf sigmoid(output[i * (4 num_classes) 4]); // 第一类的置信度 if (conf conf_thresh) continue; // 反量化、解析box坐标这里省略坐标解码细节 float cx (output[i * (4 num_classes) 0] - zero_point) * scale; float cy (output[i * (4 num_classes) 1] - zero_point) * scale; float w (output[i * (4 num_classes) 2] - zero_point) * scale; float h (output[i * (4 num_classes) 3] - zero_point) * scale; // 得到检测框 boxes.push_back({cx - w/2, cy - h/2, cx w/2, cy h/2, conf, 0}); } // 最后做NMS nms(boxes, 0.45); }分割模型的结果解析类似但输出是一个(1, 2, 192, 192)的argmax结果我直接按像素取最大概率类生成一张二值掩码图缩放回原图分辨率后用于后续的人形区域裁剪处理。4.6 实测效果与性能数据整套系统跑通之后我对性能做了几组对比测试。测试条件是RK3588开发板Debian 11NPU模式1平衡风扇常转。视频源1080p25fps RTSP摄像头 USB摄像头 本地1080p文件三个模型同时共用视频帧。测试数据如下测试场景检测模型帧率分类模型帧率分割模型帧率CPU占用率内存占用单模型独立跑25 fps25 fps15 fps60%1.2GB多线程独立跑未调度8 fps10 fps4 fps95%1.8GB且偶有OOM同源调度同步模式18 fps18 fps6 fps70%1.4GB同源调度异步模式22 fps20 fps8 fps65%1.4GB可以看到同源调度在稳定性上的提升是明显的。多线程独立跑时所有模型的帧率都被拖到很低的水平CPU几乎被打满还时不时报NPU内存不足。切换成同源调度后CPU占用大幅下降检测模型的帧率从8fps提升到18fps以上异步模式还能再进一步。需要说明的是分割模型帧率只有8fps是因为分割模型的输入分辨率较小192x192但在RK3588上int8量化后的推理本来就要大约80-100ms能达到8fps已经接近硬件极限。如果想提高分割模型帧率可以降低分辨率或改用更轻量的模型这个要根据业务需要来权衡。5. 常见问题与排查技巧实录5.1 NPU内存分配失败的问题这是我在多模型加载阶段遇到的最典型问题。当三个模型依次加载时有时在加载第三个模型时会出现rknn_init返回-1或类似“get memory failed”的错误。排查过程大概是这样先用dmesg查看内核日志确实看到了NPU内存申请失败的记录。然后用free -m查看系统内存发现可用内存还有但系统CMA区域已经满了。RK3588的NPU内存默认从Linux核的CMA区分配CMA区域默认大小是256MB左右而我的三个模型权重加输入输出buffer总共需要约200MB。解决思路有几个方向修改内核启动参数把CMA区域调大。在/boot/uEnv.txt或对应引导文件中追加coherent_pool8M cma512M重启后生效。这个方法可以一劳永逸地解决NPU内存不足的问题。确认每个模型的NPU内存不要重复申请。比如某些模型如果输入尺寸固定可以在初始化时就分配好不要在推理时反复动态申请。调整模型加载顺序。大模型先加载小模型后加载能显著减少内存碎片。踩过坑之后我现在都会先打印每个模型rknn_query获得的内存占用信息再决定加载顺序。5.2 推理结果错乱RKNN输出格式的坑yolov8s在RK3588上部署后检测框位置完全是乱的框和物体对不上坐标严重偏大或偏小。这个问题排查了很久最后发现锚点不在模型结构而在输出解析。RKNN的int8量化输出的数据不是浮点数而是int8整数需要反量化换算成真实的浮点值。我当时解析时漏掉了反量化步骤把int8数据直接当float用了结果自然是全错。反量化公式是float_value (int8_value - zero_point) * scale其中zero_point和scale可以在rknn_query(RKNN_QUERY_OUTPUT_ATTR)中查询到。另外一个坑是int8量化的类别置信度偏移。yolov8的输出中置信度是sigmoid后的值但在量化过程中网络的输出层可能没经过sigmoid量化模型为了精度保留线性输出需要在解析时自己加一层sigmoid。这个细节在RKNN官方示例里有时不会特别强调如果你发现置信度普遍偏高或偏低可以检查一下这个点。5.3 多模型同时推理导致的NPU性能下降即使加了调度有时仍然感觉NPU执行时间变长。yolov8s单独跑时推理时间约35ms但三个模型一起跑时yolov8s的推理时间会涨到45ms甚至50ms。我排查这个问题的时候一度以为是调度器逻辑问题。后来用RKNN提供的profiling工具在build模型时设置rknn.config(optimization_level3)或通过环境变量开启NPU profiling才发现NPU在多模型并发时会动态调整频点。当系统负载高、温度升高时NPU频率会自动降频保护。解决方法是控制NPU并发度。同时只有一个模型在NPU上执行不要同时提交两个rknn_run_async。我的调度器天然满足这个条件但如果你的代码里模型A和模型B各自独立提交推理就可能出现并发反而导致性能更差。降低非关键模型的推理频率。比如分割模型本来要求5fps就够了可以主动做“跳帧”不要每个视频帧都提交分割任务隔几帧提交一次。这样能减少NPU总负载保持关键模型的速度。优化散热让温度稳定在75℃以下。温度过高导致降频是RK3588上很常见的问题尤其在密闭的工控箱里使用时。这里又得提一下PWM风扇的重要性了——实测在风扇从3000RPM提升到6000RPM后NPU频率能保持在高档位推理时间稳定。读取风扇节点的方法我上文提到过这也是我为什么建议在边缘设备上保留风扇转速读取和调速功能的原因。5.4 多路RTSP拉流卡顿与延时问题RK3588拉多路RTSP流时经常出现画面卡顿、延时逐渐增大。排查后发现问题主要集中在两个环节。一是解码线程的CPU繁忙度。RK3588的VPU虽然能硬解H264但FFmpeg的demux和packet拷贝仍然占用CPU。如果解码线程和预处理线程绑定在同一个CPU核互相抢资源就可能导致解码不及时。解决办法是设置线程亲和性pthread_setaffinity_np把解码线程绑定到CPU 4-5大核把预处理线程绑定到CPU 6-7大核避免争抢。二是RTSP的接收buffer设置。有些网络摄像头码流波动大如果FFmpeg内部的socket接收buffer太小高码率段会丢包花屏。通过设置av_dict_set(opts, buffer_size, 2048000, 0)和av_dict_set(opts, rtsp_transport, tcp, 0)可以有效缓解。TCP传输虽然延迟略高于UDP但稳定性要好很多在局域网边缘场景下我一般都推荐TCP。5.5 常见问题速查表现象可能原因解决办法rknn_init 失败NPU内存不足 / CMA区太小调大cma到512M调整模型加载顺序推理结果坐标乱int8反量化遗漏或zero_point/scale设置错误用rknn_query读取正确的zero_point和scale置信度普遍不靠谱网络输出层未做sigmoid解析时手动加sigmoid多模型一起跑NPU变慢温度过高降频优化散热控制并发降低非关键模型频率RTSP拉流卡顿解码线程和预处理线程抢CPU设置线程亲和性大核分开放长时间运行后NPU报错内存碎片化推理buffer只分配一次不要在循环中反复创建风扇转速不可调PWM风扇驱动未配置确认 /sys/class/hwmon/hwmon*/pwm1 节点设置手动模式多模型结果帧不同步各模型独立拉流统一数据源使用公共缓冲区 frame_id6. 调度系统的扩展方向这套“同源多任务调度”的方案目前解决了三个模型共享输入源的问题。但实际业务往往比这个更复杂我这里再聊聊几个扩展方向。第一个方向是多路视频输入 分区域处理。如果一路摄像头画面里有多个感兴趣区域可以对不同区域跑不同的模型。比如在园区监控场景画面左半区跑车辆检测右半区跑人形分割。这个时候可以在数据生产层做一次ROI裁剪把裁剪区域作为“虚拟源”再交给调度器统一调度。好处是底层的帧缓冲、任务队列、NPU执行层都不需要改动只需在预处理环节根据区域ID做一次裁剪即可。第二个方向是按需动态加载/卸载模型。有些边缘场景业务规则不是固定的比如白天跑检测分类晚上切到低照度增强检测。如果所有模型都常驻NPU内存内存压力很大。可以考虑在调度器中加入“动态模型注册/注销”的机制业务切换时卸载不用的模型加载新模型释放NPU内存。卸载前需要确保没有正在执行的推理请求。第三个方向是负载感知的动态帧率调节。调度器可以根据当前NPU队列长度和最近推理耗时动态调整各模型的推理频率。比如检测任务历史平均耗时为40ms那么它可以被分配25fps的最大频率分割任务历史平均耗时为100ms但业务上只需要2fps的实时性那么它就隔40帧提交一次。这种动态调节在系统负载变化较大的场景很实用。第四个方向是结果融合与多模型协同。同源多任务调度的天然优势是多个模型处理的是同一帧数据所以模型结果之间天然具有时间一致性。可以做到先用检测模型框出目标再把目标区域送给分类模型或分割模型去细化——这就是经典的“两阶段级联”架构。由于所有模型使用的是同一帧缓冲区的数据不存在帧偏移问题级联精度比各模型独立处理要高。我在项目中就用了这个思路yolov8先检测出人形目标框然后分割模型只对目标框区域做前背景分割而不是对全图做分割。这样分割模型可以跑更小的输入尺寸比如64x64推理时间从100ms降到25ms左右而整个系统保持了对全图“隐式”的感知能力。这个优化不改变调度器的核心逻辑只需要在预处理环节添加一个ROI提取步骤收益却非常显著。7. 一些实际操作中的体会写到最后再分享几个我在这套系统开发过程中的个人体会。第一调度器本身不复杂复杂的是把“业务节奏”和“NPU节奏”对齐。单纯做好任务队列、优先级、异步推理只能保证系统能跑真正跑得好需要你花时间了解每个模型在不同输入下的真实耗时分布并在调度策略中针对性地调整参数。我的做法是在代码里加入一个轻量级的统计模块记录每个模型的推理耗时、队列等待时间、丢帧率运行一段时间后导出看一下哪边是瓶颈基本一目了然。第二RK3588的硬编解码能力相当强大但使用时要提前规划好通道和分辨率。H264/H265硬编码一路1080p视频对VPU的占用并不高但如果你同时要硬解多路RTSP流并做硬编码输出就需要留意VPU的总通道数限制。RK3588的VPU在某些固件版本下多路并发存在限制我在开发过程中就遇到过VPU资源争抢导致画面花屏的问题后来降低了解码路数或分辨率才解决。第三不要忽视温度对NPU性能的巨大影响。我在RK3588上遇到过很多“莫名其妙的性能下降”最后排查下来多半是温度墙。在部署边缘AI设备时散热设计一定要做足风扇PID策略要合理。在软件层面用pwm-fan节点控制风扇转速再配合温度监测动态降频是保证设备长期稳定运行的必备手段。第四如果你在做正式的边缘AI项目建议从第一天就把“监控和日志”引入系统中。我在这套调度器里加入了每个请求的耗时统计、帧率上报、以及NPU内存/温度/风扇转速采集定期输出到本地日志。这样即使在客户现场出了问题也能快速定位是哪个环节产生了瓶颈而不用像以前一样靠猜。大概就写这么多。这套同源多任务调度方案并不是什么黑科技本质上就是把数据流的管理和资源调度做好但在RK3588这样的边缘设备上这种合理的设计往往能带来30%-50%的综合性能提升。希望这篇内容包括代码和踩坑记录能在你做RK3588边缘视觉项目时帮上一点忙。