OpenEM 简介和基于 OpenEM 的大矩阵乘实现
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。
评论