《电子技术应用》
您所在的位置:首页 > 嵌入式技术 > 解决方案 > OpenEM简介和基于OpenEM的大矩阵乘实现

OpenEM简介和基于OpenEM的大矩阵乘实现

2013-12-26
作者:James Li---Multi-core DSP / FAE
关键词: 软件 OpenEM KeyStone

摘要

OpenEM 的全称是Open Event Machine。是TI 针对嵌入式应用开发的multicore runtime system library。OpenEM 可以在多核上有效的调度,分发任务。它把任务调度给负载轻的核,进而实现动态的负载平衡。OpenEM 是基于TI Keystone 系列芯片的multicore Navigator 构建的,具有开销小,效率高的特点。本文首先对OpenEM 的原理做了简单的介绍。然后结合一个大矩阵乘的演示用例详细介绍了OpenEM 的使用。最后通过量化分析这个演示用例的执行cycle 数,总结了OpenEM 的效率和局限。希望本文能成为学习OpenEM 的读者的一个有用的参考。

1OpenEM 简介

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。

7. 配置preload 门限,参见本文1.2 节的叙述。

2.2.2 创建生产者/消费者场景

前面介绍过,在OpenEM 中,消费者就是execution object,沟通生产者和消费者的管道就是queue。本小节介绍怎样创建execution object 和queue 以及怎样把它们关联起来。关于怎样产生event,本文在下一小节描述。OpenEM 有下列API 供应用调用:

• 调用em_eo_create()可以创建execution object

• 调用em_queue_create()可以创建queue

• 调用em_eo_add_queue()可以把queue 和execution object 映射起来

 

本演示用例通过参数配置表列出execution object, queue group object 和queue object 的参数,然后通过解析函数解析配置表再调用OpenEM的API,这样各个软件对象的参数在配置表中一目了然,代码的可读性较好。图5 是本演示用例的映射关系。

 

需要注意的是coremask 总共有64 个比特,但是目前6678 最多也只有8 个DSP 核。所以大量mask 比特是用不到的,目前。核0~7 对应的mask 比特是位于byte[4]的bit0:7

需要注意的是queue 到execution object 的映射是通过receiver 函数关联起来,如红色高亮显示部分。

初始化job的伪代码如下:

2.2.3 产生event

本文的演示用例把matrix Y 切分成了128 个2048*16 的子块,每个event 对应一个子块。Event被发送给execution object 以后,receive 函数计算Matrix X 乘与matrix Y block,即100*2048 ×2048*16 的矩阵乘,产生100*16 个输出。event 的产生包括下面几个简单步骤:

• 调用em_alloc 函数,从public pool 获取free 的event 描述符并且enable preloading。

• 把待处理的数据缓冲区挂到描述符上,也就是把描述符的buffer 指针指向这个数据缓冲区。

• 在描述符的software info 域填上job index。

• 调用em_send,把event 发送到对应的queue,也就是proc queue。

下面是产生event 的代码:

需要注意的是Event 产生的时候,它被哪一个execution object 处理还没有确定。因为execution object 只是和queue 关联的。当把event 发送到一个queue 的时候,负责处理event 的execution object 就确定了。所以在调用em_send()发送event 到queue 的时候参数之一就是要发送到的queue 的handler。

2.2.4 运行和exit

如前所述,“矩阵乘event”是通过proc queue 发给scheduler 的,所以它被proc queue 映射到mat_mpy calc 这个execution object 上。Dispatcher 收到这个event 后就调用“mat_mpy calc”对应的receiver 函数计算矩阵相乘。因为proc queue 所属的queue group 是映射到所有DSP 核的,所以128 个“矩阵乘event”是在所有核上并行处理的。每个核处理完event 后就把它释放回global free pool。这样这个event 又成为一个free 的event。

 

如2.2.3 节所述,主核可以通过查询global free pool 的描述符个数是否恢复来判断是否所有“矩阵乘event”已经处理完。

 

当所有“矩阵乘event”处理完后,主核再产生8 个“exit event”发送到exit queue。理论上scheduler 可以把exit job 调度给任意一个核,而不会保证每个核一个exit job。所以exit job 中的处理比较特殊。exit job 的receiver 函数直接执行系统调用exit(0)。这样就不会返回到Dispatcher,也不会再发出prefetch command。而另一方面,scheduler 是在收到DSP 核的prefetch command 以后才把event 调度给这个核的。这个机制保证了每个核收到且仅收到一个“exit event”。

在exit job 的receiver 函数中,主核执行的分支稍有差异。主核需要先做完结果的校验再执行系统调用exit(0)。所以在板上运行是会观察到其他核很快(小于1s)就从run 状态转换到abort 状态,而主核保持run 了很长时间(大约50s)才进入abort 状态。原因是:在主核上执行结果验证工作时产生校验结果的函数计算耗时比较长。

下面是exit job 的receiver 函数的代码主干:

2.3 基于OpenEM 的大矩阵乘性能测试结果

2.3.1 算法代码和cycle 数的理论极限

设r1 是X 矩阵的行数,c1 是X 矩阵的列数,c2 是Y 矩阵的列数。在我们的演示用例中r1 =100, c1 = 2048, c2 = 2048。如前所述,Receiver 函数要计算100*2048 ×2048*16 的矩阵乘,对应下面的伪代码:

循环内核是4 个cycle。如果只考虑循环内核消耗的cycle 数,计算100*2048 ×2048*16 的矩阵乘需要的cycle 数是100/2*16/2*2048/4*4 = 819,200 cycle。整个X*Y=Z 包括计算128 个这样的矩阵乘。所以总的cycle 数是819,200*128 = 104,857,600 cycles。在1Ghz 的C66 核上这相当于104.8ms。但是我们的上述理论计算没有考虑循环的前后缀消耗的cycle 数,也没有考虑cache miss stall 的等待时间。在6678EVM 板的单个DSP 核上实测,计算X*Y=Z 消耗的实际时间是190,574,214 cycles。相当于190ms。

2.3.2 基于OpenEM 的性能测试结果

基于OpenEM的演示用例实现过程中,DSP 代码中嵌入了少量测试代码收集运行的cycle 信息。每个核把自己处理每个event 的起始和结束时间记录在内存(我们通过一个全局timer 来保证所有DSP 核记录的时间戳在时间轴上是同步的)。这些时间戳用CCS 存到主机做后处理分析。通过分析,我们可以得到8 个DSP 核并行处理消耗的时间。还可以分析每个DSP 核的忙/闲区间。

测试结果是,从第一个event 开始处理到最后一个event 处理完,总时间是31,433,438 cycle,也就是31.4ms。也就是说,通过OpenEM把单DSP 核的工作负载平衡到8 个DSP 核上能达到的DSP 核利用率是190,574,214/(31,433,438*8)= 76%。

通过对时间戳的处理我们得到下面的运行图,“-”表示receiver 函数处理event 的区间,本文称之为有效时间。“#”表示receiver 之外的区间(也就是代码在dispatcher 中执行的区间),本文称之为调度开销。每个“-”和“#”刻度表示100,000 CPU cycle。

从上面的执行图看,调度开销不小,占了大约15~20%的时间。但是这只是表面的现象。实际上,调度开销的大部分时间里,Dispatcher 是在查询hardware queue,等待新的event。这是因为preload 没能及时完成导致的。因为同时给8 个核做preload 需要很大的数据搬移的流量。根据以往的测试结果。使用QMSS 的packet DMA 从DDR3 输入数据到local L2 的流量大约是4G bytes 每秒。那么preload 8 个event 总的数据量是4byte * 2048 rows * 16 columns * 8 core = 1M bytes,需要的时间是1/4 ms。因为每个“-”和“#”刻度表示100,000 CPU cycle,运行图中红线长度就代表preload 8 个event 的时间,它非常接近250,000 cycle。理论计算和实际值基本吻合,所以我们认为调度延迟是packet DMA 的传输流量不足导致的。

我们也测试了不使用pre-load 的场景。观测到scheduler 调度一个event 的延迟大约是1200 个C66 CPU cycle。但是DSP 核处理一个event 的耗时增大到原来的10 倍。所以,pre-load 虽然会导致QMSS packet DMA 流量不足成为凸显的瓶颈,但是从总体效率来看还是非常必要的。

细心的读者可能会发现76% + 20% = 96%,并不是100%。我们分析时间戳发现,8 个DSP 核同时运行的场景下,每个核处理一个100*2048 ×2048*16 的矩阵乘的时间比只有一个DSP 核运行的场景下的时间稍长。原因是:我们的演示用例中X 矩阵和Z 矩阵是存储在shared L2 的,8 个核同时运行就会同时读写这两个buffer,导致产生shared L2 的bank 冲突。所以性能下降了。

3、总结

OpenEM具有使用简单,功能实用,执行高效的特点。能在KeyStone 多核DSP 上实现动态的负载平衡。它一方面提供了强大的功能,另一方面也给应用留出了很大的灵活性。例如,通过让应用初始化free pool 方便了buffer 的管理。OpenEM 的现有功能已经能够支持基本的应用。随着版本更新功能还将不断完善。

 

Reference

Ref[1]   ti.openem.white.paper.pdf 位于OpenEM 安装目录

本站内容除特别声明的原创文章之外,转载内容只为传递更多信息,并不代表本网站赞同其观点。转载的所有的文章、图片、音/视频文件等资料的版权归版权所有权人所有。本站采用的非本站原创文章及图片等内容无法一一联系确认版权者。如涉及作品内容、版权和其它问题,请及时通过电子邮件或电话通知我们,以便迅速采取适当措施,避免给双方造成不必要的经济损失。联系电话:010-82306118;邮箱:aet@chinaaet.com。