摘要
OpenEM 的全称是 Open Event Machine。是 TI 针对嵌入式应用开发的 multicore runtime system library。OpenEM 可以在多核上有效的调度,分发任务。它把任务调度给负载轻的核,进而实现动态的负载平衡。OpenEM 是基于 TI Keystone 系列芯片的 multicore Navigator 构建的,具有开销小,效率高的特点。本文首先对 OpenEM 的原理做了简单的介绍。然后结合一个大矩阵乘的演示用例详细介绍了 OpenEM 的使用。最后通过量化分析这个演示用例的执行cycle 数,总结了 OpenEM 的效率和局限。希望本文能成为学习 OpenEM 的读者的一个有用的参考。
1、OpenEM 简介
OpenEM 的全称是 Open Event Machine。它是 TI 开发的可应用于 Keystone 多核 DSP 的multicore runtime system library。OpenEM 的目的是在多核上有效的调度,分发任务,实现动态的负载平衡。基于 OpenEM,用户可以很容易的把原来的单核应用移植到 Keystone 多核芯片。需要注意的是 OpenEM 目前只能把任务调度分发到同一个 DSP 的多个核上,不能跨 DSP 调度分发。 OpenEM不依赖于 BIOS。它可以在芯片上裸跑,代码精简,效率高。而且,OpenEM不同于业界已经有 OpenMP 和 OpenCL 等开放式的 multi-core runtime systems。它是针对嵌入式系统的设计,更能满足嵌入式设计的实时性要求。TI 的 keystone 架构多核芯片中有 Multicore Navigator。它由 Queue Manager(简称为 QMSS)和一系列 Packet DMA engine 构成。OpenEM就是基于这套硬件系统构建的。例如,OpenEM 的 scheduler 是运行在 QMSS 的 PDSP(QMSS内部的 RISC 处理器)上的。OpenEM的 preload 功能是通过 QMSS 的 packet DMA 实现的。熟悉QMSS 的编程对学习 OpenEM 很有帮助。OpenEM 是 MCSDK 的一个组件。它还在不断的发展改进中。本文对 OpenEM 的介绍以及演示用例都是基于 BIOS MCSDK 2.01.02 的 OpenEM 1.0.0.2。
1.1 OpenEM 的软件对象
下面通过列表和图示介绍了 OpenEM的主要软件对象。表 1 是 OpenEM 的主要软件对象的列表。

需要注意的是,本文介绍的 OpenEM 的运行模式是:Scheduler 运行在 PDSP,Dispatcher 是“run to completion ”模式。

图 1 是一个软件对象关系图,显示出了表 1 中列举的软件对象。定义了 2 个 queue group,5 个queue 和 3 个 execution object。Queue group1 的 core mask 对应核 0 和 1。所以来自 queue1,2,3,4 的 event 只能在核 0 和核 1 上执行,因为这些 queue 属于 queue group1。Queue group2 的 core mask 对应核 2 和 3。所以来自 queue5 的 event 只能在核 2 和核 3 上执行,因为queue5 属于 queue group2。execution object 1 和 queue 1,2,3 映射关联。execution object 2 和queue 4 映射关联。execution object 3 和 queue 5 映射关联。图中的蓝线表示了 event 的行径,红线表示 command 的行径。图中的 SD queue 是 hardware queue,它不是一个软件对象而是OpenEM内部的组件。
1.2 OpenEM 的两个重要概念
OpenEM中有两个容易混淆的重要概念:prefetch 和 preload。
• Prefetch 是指每个 DSP 核向 scheduler 发命令,告诉 scheduler“本核已经空闲了,可以分配新的工作给本核了”。只有收到一个核的 prefetch 命令,scheduler 才会调度新的 event 给这个核。如果 DSP 核不发出 prefetch 命令,它就不会被分派任务。这是 OpenEM 的 scheduler的基本调度原则。
• Preload 和 event 的属性有关。通常,event 的数据是位于 DDR 的。如果 DSP 核直接访问DDR 效率会比较低。所以,OpenEM 可以把 event 的数据通过 QMSS 的 packet DMA 搬到DSP 核的 local L2。这个搬移的过程就是 preload。每个 event 的数据是否做 preload 是可配的。每个 event 在创建的时候都可以指定一个 preload 属性。Event 的 preload 属性可以是:
– Preload disable, 即不做预搬移
– Preload up to sizeA,即做预搬移,但是最多只搬 sizeA bytes
– Preload up to sizeB,即做预搬移,但是最多只搬 sizeB bytes
– Preload up to sizeC,即做预搬移,但是最多只搬 sizeC bytes
– 其中 SizeA,SizeB 和 SizeC 是常数,在 OpenEM 初始化的时候可以配置。
1. 3 OpenEM 的常用 API cycle 数
OpenEM的附带开销是应用最关注特性之一。所以我们实测了 OpenEM 常用 API 的 cycle 数如表2。需要注意的是:由于 OpenEM会负责 cache 一致性的维护,而有些 API 的处理过程中含有cache 一致性的维护操作。所以这些 API 的调用 cycle 数很大程度上取决于它对多大的数据缓冲区做了 cache 一致性的维护。本文测试这些 cycle 的场景使用的数据缓冲区的大小是是 4096 words(32bit)。

2、基于 OpenEM 的大矩阵乘实现
大矩阵相乘的目的是计算 X*Y = Z
矩阵 X 是(100 × 2048 )的浮点实数矩阵。
矩阵 Y 是(2048 × 2048 )的浮点实数矩阵。
矩阵 Z 是(100 × 2048 )的浮点实数矩阵。
由于矩阵 Y 的数据量很大,所以在多核 DSP 上可以把它拆分成多个子块,交给多个 DSP 核并行计算。如图 2 所示。

2.1 基于 OpenEM 的大矩阵乘方案设计
2.1.1 Memory 使用
Shannon DSP (6678)的内存系统包括片内的 LL2(local L2)和 SL2(shared L2)。加上片外的 DDR。LL2 的 size 是 512 Kbytes,每个核有一份 LL2。 SL2 的 size 是 4Mbytes,8 个核共享 SL2。DDR size 和硬件板卡设计有关,一般在 1G bytes 以上。 C66x 核对 LL2 的访问效率最高,对 SL2 的访问效率稍差,对 DDR 的访问效率最低。基于多种存储区间的不同特性,我们对数据存储位置按如下规划(参见图 3):
– 矩阵 X 的 size 是 800 Kbytes,存储是 shared L2
– 矩阵 Y 的 size 是 16 Mbytes,存储是 DDR
– 矩阵 Z 的 size 是 800 Kbytes,存储是 shared L2
虽然矩阵 Y 存储在 DDR,但是我们启用了 OpenEM 的 preload 功能。Preload 就是通过 QMSS 的 packet DMA 把待处理的 event 数据(通常位于 DDR)搬到被调度 core 的 LL2。所以 DSP 核运行的时候不直接从 DDR 取数。这保证了 DSP 核的数据访问效率。

2.1.2 处理流程
OpenEM中要有一个 DSP 核作为主核,其他核就是从核,主核要完成的工作较多。本文的演示用例中,核 0 是主核,核 1~7 是从核。主从核的分工差异如图 4:

1. 初始化 QMSS 和 free pool。
2. OpenEM 的 global 初始化和 local 初始化。global 初始化是主核执行。local 初始化是每个核各自执行。Local 初始化要等 global 初始化完成才能开始。所以,中间需要加一个barrier。Barrier 可以理解成一个同步点,所有 DSP 核在这个点完成一次同步再继续向下执行。本演示用例的 Barrier 是通过共享内存的软件信号量实现的。
3. 主核构造生产者/消费者场景并产生待处理的 event。生产者在 OpenEM 中不是一个软件对象。我们可以把产生 event 并发送到 queue 的函数认为是生产者。消费者就是 execution object,沟通生产者和消费者的管道就是 queue。构造生产者/消费者场景就是创建execution object 和 queue 并且把它们关联起来。
4. 主核和从核进入 event 处理的过程。
5. 主核检测到所有 event 都处理完成后为每个 DSP 核(包括它自己)产一个 exit job。
6. 主核和从核处理 exit job。从核直接调用 exit(0)退出。主核先做结果验证然后调用 exit(0)退出。
本文演示用例实现的几个特点是:
• OpenEM 的 free pool 是由用户初始化的。在初始化 free pool 的时候 event 描述符不指向数据缓冲区。等分配了一个 event 的时候再在这个 event 对应的描述符上挂数据缓冲区。这样可以避免不必要的数据拷贝(从 global buffer 拷贝到 event buffer)。
• 主核通过查询 free pool 中的 event 个数是否恢复回初始值来判断是否所有“矩阵乘 event”都处理。因为:
– Free pool 在初始化以后有 N 个 free event,
– 从中分配了若干个 event 后,free event 就减少了相应的个数,
– 每个 core 每处理完一个 event 就把这个 event 回收到 free pool,free pool 的 event 个数就加一。当 free pool 的 event 个数恢复回 N,就说明所有 event 都处理完了。
2.2 基于 OpenEM 的大矩阵乘实现
在初始化 OpenEM之前首先要做 multicore Navigator 的初始化。包括:PDSP firmware 的download, Link RAM 的初始化, Memory region 的初始化还有 free pool (也就是 free descriptorqueue)的初始化。这不属于本文介绍的范畴,本文直接介绍 OpenEM的初始化。
2.2.1 OpenEM 的 Global 初始化
OpenEM的 global 初始化通过调用 API 函数 ti_em_init_global()完成的。这个 API 的入参是下面所示的结构体。其中所列的参数是本文的演示用例使用的配置参数。本文针对每个参数的作用做了注释。了解了参数了含义,就能了解 OpenEM 的 global 初始化的大致做了些什么。

注释:
1. OpenEM要使用 hardware queue 资源。hw_queue_base_idx 用来指定 OpenEM 从哪个hardware queue 开始可用。
2. OpenEM 的少量操作需要多 DSP 核访问共享的数据结构。是通过 hardware semaphore 实现多核lock/unclock 的。所以通过 hw_sem_idx 告诉 OpenEM该使用哪一个 hardware semaphore。
3. 指定 preload 使用的 QMSS packet DMA 的通道的起始索引。QMSS packet DMA 有 32 个 RX/TX channel。在 OpenEM 中,每个 DSP core 要占用一个 TX/RX channel。
4. 指定 preload 使用的 QMSS Tx queues 的起始索引。要和 dma_idx 对应起来。QMSS 有 32 个 TX queue,索引是 800~831。对应 QMSS packet DMA 的 TX channel 0~31。所以,如果前面配置的 dma_idx 是 0,那么这里配置的 dma_queue_base_idx 应该是 800。
5. 指定 OpenEM local free pool 对应的 free queue index。Local free pool 是和 preload 相关的。local free pool 在物理上是一个 free descriptor queue。里面存储着 2 个 host 描述符。每个描述符对应一个 local L2 buffer。如果发生 preload,packet DMA 就从 free descriptor queue pop 描述符,然后把数据传到描述符指向的 local L2 buffer。每个 DSP 核有一个 local free pool。例如,在我们的演示用例中 core0~7 对应的 free descriptor queue 索引是 2050~2057。
6. 指定 OpenEM global free pool 的个数。每个 global free pool 包括 4 个初始化参数,例如{ globalFreePoolFdqIdx, TI_EM_COH_MODE_ON,TI_EM_BUF_MODE_GLOBAL_TIGHT,0}。参数 1是这个 global free pool 对应的 free queue index。接下来几项是这个 pool 中的 buffer 的属性。Global free pool 是用来从中分配 free event 的。调用 em_alloc()的入参之一就是 free pool index。