4、多核NPU调度原理:RKNN Runtime的多核调度机制、核心绑定、负载均衡策略、同步与异步执行模型
RK3588 搭载的三核 NPU(NPU0/NPU1/NPU2)并非简单的算力叠加,其实际性能上限取决于 RKNN Runtime 如何将模型的计算图切分、映射并调度到多个 NPU 核心上。理解多核调度原理,是进行多核性能调优的前提。本章将深入剖析 RKNN Runtime 的多核调度机制,包括核心绑定、负载均衡策略以及同步/异步执行模型,帮助开发者从“能用”走向“会用”。
上图展示了从 Host CPU 到 NPU 核心集群的完整调度链路。RKNN Runtime 位于中间层,负责将上层应用的计算请求转换为 NPU 可执行的指令序列,并根据核心绑定配置和负载均衡策略,将任务分发到不同的 NPU 核心上执行。下面我们将从核心原理、操作流程、实战案例和常见误区四个维度展开。
4.1 核心原理:从计算图到多核任务映射
RKNN Runtime 的多核调度本质上是计算图切分与任务映射的过程。当模型通过 rknn_init() 加载后,Runtime 会将模型解析为内部计算图(Graph),图中的每个节点代表一个算子(如 Conv、ReLU、Pooling 等)。多核调度的第一步是决定如何将这些算子分配到不同的 NPU 核心上执行。
RK3588 的 NPU 采用 MIMD(多指令多数据) 架构,每个 NPU 核心可以独立执行不同的算子。Runtime 支持两种切分粒度:
- 算子级切分:将不同的算子分配到不同的核心。例如,将 Conv1 分配给 NPU0,将 Conv2 分配给 NPU1。这种方式的优点是实现简单,但核心间需要频繁同步,通信开销较大。
- 层内切分:将单个大算子(如大尺寸卷积)按通道或空间维度切分到多个核心并行执行。这种方式能充分利用多核算力,但对内存带宽要求较高。
实际调度中,Runtime 会根据算子的计算量、数据依赖关系和内存访问模式,自动选择最优的切分策略。开发者可以通过 rknn_query 接口查询模型在目标平台上的实际调度信息。
4.2 核心绑定机制:控制任务落核
核心绑定(Core Binding)是 RKNN Runtime 提供的一种手动干预手段,允许开发者指定模型在哪些 NPU 核心上运行。通过设置 rknn_core_mask 参数,可以控制任务执行的核集合。
核心绑定的典型应用场景包括:
- 性能隔离:当多个模型同时运行时,将不同模型绑定到不同的核心,避免相互干扰。
- 功耗优化:对于轻量级模型,只绑定一个核心,降低整体功耗。
- 延迟敏感任务:将关键任务绑定到固定核心,减少调度抖动。
核心绑定的配置方式如下:
// 设置核心掩码,例如绑定 NPU0 和 NPU1
rknn_core_mask core_mask = RKNN_NPU_CORE_0 | RKNN_NPU_CORE_1;
ret = rknn_set_core_mask(ctx, core_mask);
if (ret < 0) {
printf("设置核心掩码失败: %d\n", ret);
}
需要注意的是,核心绑定是全局设置,会影响该上下文(Context)中所有后续的推理调用。如果需要在不同推理请求之间切换绑定策略,需要创建多个 Context 或动态调整。
| 核心掩码值 | 含义 | 适用场景 |
|---|---|---|
RKNN_NPU_CORE_0 |
仅使用 NPU0 | 低功耗、轻量模型 |
RKNN_NPU_CORE_0 | RKNN_NPU_CORE_1 |
使用 NPU0 和 NPU1 | 中等算力需求 |
RKNN_NPU_CORE_0 | RKNN_NPU_CORE_1 | RKNN_NPU_CORE_2 |
使用全部三个核心 | 高性能、大模型 |
RKNN_NPU_CORE_AUTO |
由 Runtime 自动决定 | 默认模式,推荐优先使用 |
4.3 负载均衡策略:静态分配与动态调度
负载均衡决定了多个核心之间的任务分配比例。RKNN Runtime 提供了两种策略:
静态负载均衡:在模型加载时,根据算子的计算量(MACs)和内存访问量,预先计算每个核心的任务分配方案。这种方式的优点是调度开销小,适合计算量分布均匀的模型。缺点是如果模型输入尺寸动态变化(如不同分辨率的视频帧),静态分配可能不再最优。
动态负载均衡:在推理过程中,Runtime 根据每个核心的实际执行时间和队列长度,动态调整任务分配。这种方式能适应输入变化,但会引入额外的调度开销。RKNN Runtime 默认采用静态均衡,但在某些场景下(如连续推理不同尺寸的输入),可以尝试启用动态模式。
rknn_run 的异步模式配合自定义任务队列实现。
4.4 同步与异步执行模型
RKNN Runtime 支持两种推理执行方式:
- 同步执行(阻塞模式):调用
rknn_run()后,当前线程阻塞,直到推理完成并返回结果。这种方式编程简单,但 CPU 在推理期间无法执行其他任务,导致 CPU 利用率低。 - 异步执行(非阻塞模式):调用
rknn_run_async()后立即返回,推理在后台进行。开发者可以通过rknn_wait()等待推理完成,或通过回调函数获取完成通知。异步模式允许 CPU 在推理期间处理其他任务(如视频采集、图像预处理),显著提升系统吞吐量。
异步执行的核心是事件机制。Runtime 内部维护一个事件队列,每个推理请求对应一个事件。当推理完成时,Runtime 会触发相应的事件,唤醒等待的线程或调用回调函数。
// 异步推理示例
ret = rknn_run_async(ctx, &input_attrs, &output_attrs);
if (ret < 0) {
printf("异步推理启动失败: %d\n", ret);
return -1;
}
// 执行其他任务...
// 等待推理完成
ret = rknn_wait(ctx, &output_attrs);
if (ret < 0) {
printf("等待推理完成失败: %d\n", ret);
return -1;
}
4.5 实战案例:多路视频流场景下的多核调度优化
下面通过一个实际案例来演示多核调度优化的完整流程。
背景与约束:某安防项目需要在 RK3588 上同时处理 4 路 1080p 视频流,每路视频流运行一个行人检测模型(YOLOv5s)。硬件平台为 RK3588 开发板,内存 8GB,NPU 三核。初始方案中,4 路视频流共用一个 RKNN Context,使用默认的 RKNN_NPU_CORE_AUTO 配置。
初始现象:系统运行后,整体帧率仅为 12 FPS(期望 25 FPS),且 CPU 占用率高达 80%。通过 rknn_query 查询发现,三个 NPU 核心的利用率分别为 95%、60%、30%,负载严重不均。
分析过程:
- 检查模型计算图,发现模型包含大量小算子(如激活函数、池化),这些算子计算量小但调度开销大。
- 使用
perf工具分析发现,Runtime 的调度线程成为瓶颈,频繁的核心间同步导致大量时间浪费在等待上。 - 进一步分析内存带宽,发现 4 路视频流共享同一内存带宽,当多个核心同时访问内存时,带宽竞争严重。
优化方案:
- 多 Context 隔离:为每路视频流创建独立的 RKNN Context,并将每个 Context 绑定到不同的 NPU 核心(Context0→NPU0,Context1→NPU1,Context2→NPU2,Context3→NPU0+NPU1)。这样避免了核心间的频繁同步。
- 异步执行:将推理调用改为
rknn_run_async(),在等待推理完成期间,CPU 执行下一帧的图像缩放和格式转换。 - 内存优化:使用
rknn_create_mem()为每个 Context 分配独立的输入输出缓冲区,减少内存访问冲突。
结果对比:
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 整体帧率 | 12 FPS | 24 FPS | 100% |
| CPU 占用率 | 80% | 45% | 降低 43% |
| NPU 核心利用率 | 95%/60%/30% | 85%/82%/78% | 负载更均衡 |
风险与代价:多 Context 方案会增加约 30MB 的内存开销(每个 Context 的模型副本),且模型加载时间从 200ms 增加到 800ms(4 个 Context 串行加载)。但考虑到推理性能的大幅提升,这些代价是可接受的。
复盘经验:多核调优的核心不是“让所有核心都跑满”,而是“让每个核心做合适的事”。在本案例中,将 4 路视频流拆分为 4 个独立 Context 并绑定到不同核心,避免了核心间的调度竞争,同时通过异步执行隐藏了 CPU 预处理延迟,最终实现了性能翻倍。
4.6 常见误区与总结
RKNN_NPU_CORE_AUTO 测试,再根据实际利用率决定是否手动绑定。
rknn_run() 和 rknn_run_async(),这会导致内部状态混乱。应统一使用一种模式,并在异步模式下确保每次 rknn_run_async() 后都有对应的 rknn_wait()。
总结:多核 NPU 调度优化是一个系统工程,需要从计算图切分、核心绑定、负载均衡和执行模型四个维度综合考虑。核心原则是:先测量,后优化。通过 rknn_query 获取核心利用率、执行时间等关键指标,针对瓶颈采取对应的优化措施。在实际项目中,建议优先使用 Runtime 的自动调度能力,仅在自动调度无法满足性能要求时,再考虑手动干预。