摘要
OpenEM 的全稱是Open Event Machine。是TI 針對嵌入式應(yīng)用開發(fā)的multicore runtime system library。OpenEM 可以在多核上有效的調(diào)度,分發(fā)任務(wù)。它把任務(wù)調(diào)度給負(fù)載輕的核,進(jìn)而實(shí)現(xiàn)動態(tài)的負(fù)載平衡。OpenEM 是基于TI Keystone 系列芯片的multicore Navigator 構(gòu)建的,具有開銷小,效率高的特點(diǎn)。本文首先對OpenEM 的原理做了簡單的介紹。然后結(jié)合一個大矩陣乘的演示用例詳細(xì)介紹了OpenEM 的使用。最后通過量化分析這個演示用例的執(zhí)行cycle 數(shù),總結(jié)了OpenEM 的效率和局限。希望本文能成為學(xué)習(xí)OpenEM 的讀者的一個有用的參考。
1、OpenEM 簡介
OpenEM 的全稱是Open Event Machine。它是TI 開發(fā)的可應(yīng)用于Keystone 多核DSP 的multicore runtime system library。OpenEM 的目的是在多核上有效的調(diào)度,分發(fā)任務(wù),實(shí)現(xiàn)動態(tài)的負(fù)載平衡?;贠penEM,用戶可以很容易的把原來的單核應(yīng)用移植到Keystone 多核芯片。需要注意的是OpenEM 目前只能把任務(wù)調(diào)度分發(fā)到同一個DSP 的多個核上,不能跨DSP 調(diào)度分發(fā)。OpenEM不依賴于BIOS。它可以在芯片上裸跑,代碼精簡,效率高。而且,OpenEM不同于業(yè)界已經(jīng)有OpenMP 和OpenCL 等開放式的multi-core runtime systems。它是針對嵌入式系統(tǒng)的設(shè)計(jì),更能滿足嵌入式設(shè)計(jì)的實(shí)時性要求。TI 的keystone 架構(gòu)多核芯片中有Multicore Navigator。它由Queue Manager(簡稱為QMSS)和一系列Packet DMA engine 構(gòu)成。OpenEM就是基于這套硬件系統(tǒng)構(gòu)建的。例如,OpenEM 的scheduler 是運(yùn)行在QMSS 的PDSP(QMSS內(nèi)部的RISC 處理器)上的。OpenEM的preload 功能是通過QMSS 的packet DMA 實(shí)現(xiàn)的。熟悉QMSS 的編程對學(xué)習(xí)OpenEM 很有幫助。OpenEM 是MCSDK 的一個組件。它還在不斷的發(fā)展改進(jìn)中。本文對OpenEM 的介紹以及演示用例都是基于BIOS MCSDK 2.01.02 的OpenEM 1.0.0.2。
1.1 OpenEM 的軟件對象
下面通過列表和圖示介紹了OpenEM的主要軟件對象。表1 是OpenEM 的主要軟件對象的列表。
需要注意的是,本文介紹的OpenEM 的運(yùn)行模式是:Scheduler 運(yùn)行在PDSP,Dispatcher 是“run to completion ”模式。
圖1 是一個軟件對象關(guān)系圖,顯示出了表1 中列舉的軟件對象。定義了2 個queue group,5 個queue 和3 個execution object。Queue group1 的core mask 對應(yīng)核0 和1。所以來自queue1,2,3,4 的event 只能在核0 和核1 上執(zhí)行,因?yàn)檫@些queue 屬于queue group1。Queue group2 的core mask 對應(yīng)核2 和3。所以來自queue5 的event 只能在核2 和核3 上執(zhí)行,因?yàn)閝ueue5 屬于queue group2。execution object 1 和queue 1,2,3 映射關(guān)聯(lián)。execution object 2 和queue 4 映射關(guān)聯(lián)。execution object 3 和queue 5 映射關(guān)聯(lián)。圖中的藍(lán)線表示了event 的行徑,紅線表示command 的行徑。圖中的SD queue 是hardware queue,它不是一個軟件對象而是OpenEM內(nèi)部的組件。
1.2 OpenEM 的兩個重要概念
OpenEM中有兩個容易混淆的重要概念:prefetch 和preload。
• Prefetch 是指每個DSP 核向scheduler 發(fā)命令,告訴scheduler“本核已經(jīng)空閑了,可以分配新的工作給本核了”。只有收到一個核的prefetch 命令,scheduler 才會調(diào)度新的event 給這個核。如果DSP 核不發(fā)出prefetch 命令,它就不會被分派任務(wù)。這是OpenEM 的scheduler的基本調(diào)度原則。
• Preload 和event 的屬性有關(guān)。通常,event 的數(shù)據(jù)是位于DDR 的。如果DSP 核直接訪問DDR 效率會比較低。所以,OpenEM 可以把event 的數(shù)據(jù)通過QMSS 的packet DMA 搬到DSP 核的local L2。這個搬移的過程就是preload。每個event 的數(shù)據(jù)是否做preload 是可配的。每個event 在創(chuàng)建的時候都可以指定一個preload 屬性。Event 的preload 屬性可以是:
– Preload disable, 即不做預(yù)搬移
– Preload up to sizeA,即做預(yù)搬移,但是最多只搬sizeA bytes
– Preload up to sizeB,即做預(yù)搬移,但是最多只搬sizeB bytes
– Preload up to sizeC,即做預(yù)搬移,但是最多只搬sizeC bytes
– 其中SizeA,SizeB 和SizeC 是常數(shù),在OpenEM 初始化的時候可以配置。
1.3 OpenEM 的常用API cycle 數(shù)
OpenEM的附帶開銷是應(yīng)用最關(guān)注特性之一。所以我們實(shí)測了OpenEM 常用API 的cycle 數(shù)如表2。需要注意的是:由于OpenEM會負(fù)責(zé)cache 一致性的維護(hù),而有些API 的處理過程中含有cache 一致性的維護(hù)操作。所以這些API 的調(diào)用cycle 數(shù)很大程度上取決于它對多大的數(shù)據(jù)緩沖區(qū)做了cache 一致性的維護(hù)。本文測試這些cycle 的場景使用的數(shù)據(jù)緩沖區(qū)的大小是是4096 words(32bit)。
2、基于OpenEM 的大矩陣乘實(shí)現(xiàn)
大矩陣相乘的目的是計(jì)算X*Y = Z
矩陣X 是(100 ×2048 )的浮點(diǎn)實(shí)數(shù)矩陣。
矩陣Y 是(2048 ×2048 )的浮點(diǎn)實(shí)數(shù)矩陣。
矩陣Z 是(100 ×2048 )的浮點(diǎn)實(shí)數(shù)矩陣。
由于矩陣Y 的數(shù)據(jù)量很大,所以在多核DSP 上可以把它拆分成多個子塊,交給多個DSP 核并行計(jì)算。如圖2 所示。
2.1 基于OpenEM 的大矩陣乘方案設(shè)計(jì)
2.1.1 Memory 使用
Shannon DSP (6678)的內(nèi)存系統(tǒng)包括片內(nèi)的LL2(local L2)和SL2(shared L2)。加上片外的DDR。LL2 的size 是512 Kbytes,每個核有一份LL2。SL2 的size 是4Mbytes,8 個核共享SL2。DDR size 和硬件板卡設(shè)計(jì)有關(guān),一般在1G bytes 以上。C66x 核對LL2 的訪問效率最高,對SL2 的訪問效率稍差,對DDR 的訪問效率最低?;诙喾N存儲區(qū)間的不同特性,我們對數(shù)據(jù)存儲位置按如下規(guī)劃(參見圖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 數(shù)據(jù)(通常位于DDR)搬到被調(diào)度core 的LL2。所以DSP 核運(yùn)行的時候不直接從DDR 取數(shù)。這保證了DSP 核的數(shù)據(jù)訪問效率。
2.1.2 處理流程
OpenEM中要有一個DSP 核作為主核,其他核就是從核,主核要完成的工作較多。本文的演示用例中,核0 是主核,核1~7 是從核。主從核的分工差異如圖4:
1. 初始化QMSS 和free pool。
2. OpenEM 的global 初始化和local 初始化。global 初始化是主核執(zhí)行。local 初始化是每個核各自執(zhí)行。Local 初始化要等global 初始化完成才能開始。所以,中間需要加一個barrier。Barrier 可以理解成一個同步點(diǎn),所有DSP 核在這個點(diǎn)完成一次同步再繼續(xù)向下執(zhí)行。本演示用例的Barrier 是通過共享內(nèi)存的軟件信號量實(shí)現(xiàn)的。
3. 主核構(gòu)造生產(chǎn)者/消費(fèi)者場景并產(chǎn)生待處理的event。生產(chǎn)者在OpenEM 中不是一個軟件對象。我們可以把產(chǎn)生event 并發(fā)送到queue 的函數(shù)認(rèn)為是生產(chǎn)者。消費(fèi)者就是execution object,溝通生產(chǎn)者和消費(fèi)者的管道就是queue。構(gòu)造生產(chǎn)者/消費(fèi)者場景就是創(chuàng)建execution object 和queue 并且把它們關(guān)聯(lián)起來。
4. 主核和從核進(jìn)入event 處理的過程。
5. 主核檢測到所有event 都處理完成后為每個DSP 核(包括它自己)產(chǎn)一個exit job。
6. 主核和從核處理exit job。從核直接調(diào)用exit(0)退出。主核先做結(jié)果驗(yàn)證然后調(diào)用exit(0)退出。
本文演示用例實(shí)現(xiàn)的幾個特點(diǎn)是:
• OpenEM 的free pool 是由用戶初始化的。在初始化free pool 的時候event 描述符不指向數(shù)據(jù)緩沖區(qū)。等分配了一個event 的時候再在這個event 對應(yīng)的描述符上掛數(shù)據(jù)緩沖區(qū)。這樣可以避免不必要的數(shù)據(jù)拷貝(從global buffer 拷貝到event buffer)。
• 主核通過查詢free pool 中的event 個數(shù)是否恢復(fù)回初始值來判斷是否所有“矩陣乘event”都處理。因?yàn)椋?/p>
– Free pool 在初始化以后有N 個free event,
– 從中分配了若干個event 后,free event 就減少了相應(yīng)的個數(shù),
– 每個core 每處理完一個event 就把這個event 回收到free pool,free pool 的event 個數(shù)就加一。當(dāng)free pool 的event 個數(shù)恢復(fù)回N,就說明所有event 都處理完了。
2.2 基于OpenEM 的大矩陣乘實(shí)現(xiàn)
在初始化OpenEM之前首先要做multicore Navigator 的初始化。包括:PDSP firmware 的download,Link RAM 的初始化,Memory region 的初始化還有free pool (也就是free descriptorqueue)的初始化。這不屬于本文介紹的范疇,本文直接介紹OpenEM的初始化。
2.2.1 OpenEM 的Global 初始化
OpenEM的global 初始化通過調(diào)用API 函數(shù)ti_em_init_global()完成的。這個API 的入?yún)⑹窍旅嫠镜慕Y(jié)構(gòu)體。其中所列的參數(shù)是本文的演示用例使用的配置參數(shù)。本文針對每個參數(shù)的作用做了注釋。了解了參數(shù)了含義,就能了解OpenEM 的global 初始化的大致做了些什么。
注釋:
1. OpenEM要使用hardware queue 資源。hw_queue_base_idx 用來指定OpenEM 從哪個hardware queue 開始可用。
2. OpenEM 的少量操作需要多DSP 核訪問共享的數(shù)據(jù)結(jié)構(gòu)。是通過hardware semaphore 實(shí)現(xiàn)多核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 對應(yīng)起來。QMSS 有32 個TX queue,索引是800~831。對應(yīng)QMSS packet DMA 的TX channel 0~31。所以,如果前面配置的dma_idx 是0,那么這里配置的dma_queue_base_idx 應(yīng)該是800。
5. 指定OpenEM local free pool 對應(yīng)的free queue index。Local free pool 是和preload 相關(guān)的。local free pool 在物理上是一個free descriptor queue。里面存儲著2 個host 描述符。每個描述符對應(yīng)一個local L2 buffer。如果發(fā)生preload,packet DMA 就從free descriptor queue pop 描述符,然后把數(shù)據(jù)傳到描述符指向的local L2 buffer。每個DSP 核有一個local free pool。例如,在我們的演示用例中core0~7 對應(yīng)的free descriptor queue 索引是2050~2057。
6. 指定OpenEM global free pool 的個數(shù)。每個global free pool 包括4 個初始化參數(shù),例如{ globalFreePoolFdqIdx, TI_EM_COH_MODE_ON,TI_EM_BUF_MODE_GLOBAL_TIGHT,0}。參數(shù)1是這個global free pool 對應(yīng)的free queue index。接下來幾項(xiàng)是這個pool 中的buffer 的屬性。Global free pool 是用來從中分配free event 的。調(diào)用em_alloc()的入?yún)⒅痪褪莊ree pool index。
7. 配置preload 門限,參見本文1.2 節(jié)的敘述。
2.2.2 創(chuàng)建生產(chǎn)者/消費(fèi)者場景
前面介紹過,在OpenEM 中,消費(fèi)者就是execution object,溝通生產(chǎn)者和消費(fèi)者的管道就是queue。本小節(jié)介紹怎樣創(chuàng)建execution object 和queue 以及怎樣把它們關(guān)聯(lián)起來。關(guān)于怎樣產(chǎn)生event,本文在下一小節(jié)描述。OpenEM 有下列API 供應(yīng)用調(diào)用:
• 調(diào)用em_eo_create()可以創(chuàng)建execution object
• 調(diào)用em_queue_create()可以創(chuàng)建queue
• 調(diào)用em_eo_add_queue()可以把queue 和execution object 映射起來
本演示用例通過參數(shù)配置表列出execution object, queue group object 和queue object 的參數(shù),然后通過解析函數(shù)解析配置表再調(diào)用OpenEM的API,這樣各個軟件對象的參數(shù)在配置表中一目了然,代碼的可讀性較好。圖5 是本演示用例的映射關(guān)系。
需要注意的是coremask 總共有64 個比特,但是目前6678 最多也只有8 個DSP 核。所以大量mask 比特是用不到的,目前。核0~7 對應(yīng)的mask 比特是位于byte[4]的bit0:7
需要注意的是queue 到execution object 的映射是通過receiver 函數(shù)關(guān)聯(lián)起來,如紅色高亮顯示部分。
初始化job的偽代碼如下:
2.2.3 產(chǎn)生event
本文的演示用例把matrix Y 切分成了128 個2048*16 的子塊,每個event 對應(yīng)一個子塊。Event被發(fā)送給execution object 以后,receive 函數(shù)計(jì)算Matrix X 乘與matrix Y block,即100*2048 ×2048*16 的矩陣乘,產(chǎn)生100*16 個輸出。event 的產(chǎn)生包括下面幾個簡單步驟:
• 調(diào)用em_alloc 函數(shù),從public pool 獲取free 的event 描述符并且enable preloading。
• 把待處理的數(shù)據(jù)緩沖區(qū)掛到描述符上,也就是把描述符的buffer 指針指向這個數(shù)據(jù)緩沖區(qū)。
• 在描述符的software info 域填上job index。
• 調(diào)用em_send,把event 發(fā)送到對應(yīng)的queue,也就是proc queue。
下面是產(chǎn)生event 的代碼:
需要注意的是Event 產(chǎn)生的時候,它被哪一個execution object 處理還沒有確定。因?yàn)閑xecution object 只是和queue 關(guān)聯(lián)的。當(dāng)把event 發(fā)送到一個queue 的時候,負(fù)責(zé)處理event 的execution object 就確定了。所以在調(diào)用em_send()發(fā)送event 到queue 的時候參數(shù)之一就是要發(fā)送到的queue 的handler。
2.2.4 運(yùn)行和exit
如前所述,“矩陣乘event”是通過proc queue 發(fā)給scheduler 的,所以它被proc queue 映射到mat_mpy calc 這個execution object 上。Dispatcher 收到這個event 后就調(diào)用“mat_mpy calc”對應(yīng)的receiver 函數(shù)計(jì)算矩陣相乘。因?yàn)閜roc queue 所屬的queue group 是映射到所有DSP 核的,所以128 個“矩陣乘event”是在所有核上并行處理的。每個核處理完event 后就把它釋放回global free pool。這樣這個event 又成為一個free 的event。
如2.2.3 節(jié)所述,主核可以通過查詢global free pool 的描述符個數(shù)是否恢復(fù)來判斷是否所有“矩陣乘event”已經(jīng)處理完。
當(dāng)所有“矩陣乘event”處理完后,主核再產(chǎn)生8 個“exit event”發(fā)送到exit queue。理論上scheduler 可以把exit job 調(diào)度給任意一個核,而不會保證每個核一個exit job。所以exit job 中的處理比較特殊。exit job 的receiver 函數(shù)直接執(zhí)行系統(tǒng)調(diào)用exit(0)。這樣就不會返回到Dispatcher,也不會再發(fā)出prefetch command。而另一方面,scheduler 是在收到DSP 核的prefetch command 以后才把event 調(diào)度給這個核的。這個機(jī)制保證了每個核收到且僅收到一個“exit event”。
在exit job 的receiver 函數(shù)中,主核執(zhí)行的分支稍有差異。主核需要先做完結(jié)果的校驗(yàn)再執(zhí)行系統(tǒng)調(diào)用exit(0)。所以在板上運(yùn)行是會觀察到其他核很快(小于1s)就從run 狀態(tài)轉(zhuǎn)換到abort 狀態(tài),而主核保持run 了很長時間(大約50s)才進(jìn)入abort 狀態(tài)。原因是:在主核上執(zhí)行結(jié)果驗(yàn)證工作時產(chǎn)生校驗(yàn)結(jié)果的函數(shù)計(jì)算耗時比較長。
下面是exit job 的receiver 函數(shù)的代碼主干:
2.3 基于OpenEM 的大矩陣乘性能測試結(jié)果
2.3.1 算法代碼和cycle 數(shù)的理論極限
設(shè)r1 是X 矩陣的行數(shù),c1 是X 矩陣的列數(shù),c2 是Y 矩陣的列數(shù)。在我們的演示用例中r1 =100, c1 = 2048, c2 = 2048。如前所述,Receiver 函數(shù)要計(jì)算100*2048 ×2048*16 的矩陣乘,對應(yīng)下面的偽代碼:
循環(huán)內(nèi)核是4 個cycle。如果只考慮循環(huán)內(nèi)核消耗的cycle 數(shù),計(jì)算100*2048 ×2048*16 的矩陣乘需要的cycle 數(shù)是100/2*16/2*2048/4*4 = 819,200 cycle。整個X*Y=Z 包括計(jì)算128 個這樣的矩陣乘。所以總的cycle 數(shù)是819,200*128 = 104,857,600 cycles。在1Ghz 的C66 核上這相當(dāng)于104.8ms。但是我們的上述理論計(jì)算沒有考慮循環(huán)的前后綴消耗的cycle 數(shù),也沒有考慮cache miss stall 的等待時間。在6678EVM 板的單個DSP 核上實(shí)測,計(jì)算X*Y=Z 消耗的實(shí)際時間是190,574,214 cycles。相當(dāng)于190ms。
2.3.2 基于OpenEM 的性能測試結(jié)果
基于OpenEM的演示用例實(shí)現(xiàn)過程中,DSP 代碼中嵌入了少量測試代碼收集運(yùn)行的cycle 信息。每個核把自己處理每個event 的起始和結(jié)束時間記錄在內(nèi)存(我們通過一個全局timer 來保證所有DSP 核記錄的時間戳在時間軸上是同步的)。這些時間戳用CCS 存到主機(jī)做后處理分析。通過分析,我們可以得到8 個DSP 核并行處理消耗的時間。還可以分析每個DSP 核的忙/閑區(qū)間。
測試結(jié)果是,從第一個event 開始處理到最后一個event 處理完,總時間是31,433,438 cycle,也就是31.4ms。也就是說,通過OpenEM把單DSP 核的工作負(fù)載平衡到8 個DSP 核上能達(dá)到的DSP 核利用率是190,574,214/(31,433,438*8)= 76%。
通過對時間戳的處理我們得到下面的運(yùn)行圖,“-”表示receiver 函數(shù)處理event 的區(qū)間,本文稱之為有效時間。“#”表示receiver 之外的區(qū)間(也就是代碼在dispatcher 中執(zhí)行的區(qū)間),本文稱之為調(diào)度開銷。每個“-”和“#”刻度表示100,000 CPU cycle。
從上面的執(zhí)行圖看,調(diào)度開銷不小,占了大約15~20%的時間。但是這只是表面的現(xiàn)象。實(shí)際上,調(diào)度開銷的大部分時間里,Dispatcher 是在查詢hardware queue,等待新的event。這是因?yàn)閜reload 沒能及時完成導(dǎo)致的。因?yàn)橥瑫r給8 個核做preload 需要很大的數(shù)據(jù)搬移的流量。根據(jù)以往的測試結(jié)果。使用QMSS 的packet DMA 從DDR3 輸入數(shù)據(jù)到local L2 的流量大約是4G bytes 每秒。那么preload 8 個event 總的數(shù)據(jù)量是4byte * 2048 rows * 16 columns * 8 core = 1M bytes,需要的時間是1/4 ms。因?yàn)槊總€“-”和“#”刻度表示100,000 CPU cycle,運(yùn)行圖中紅線長度就代表preload 8 個event 的時間,它非常接近250,000 cycle。理論計(jì)算和實(shí)際值基本吻合,所以我們認(rèn)為調(diào)度延遲是packet DMA 的傳輸流量不足導(dǎo)致的。
我們也測試了不使用pre-load 的場景。觀測到scheduler 調(diào)度一個event 的延遲大約是1200 個C66 CPU cycle。但是DSP 核處理一個event 的耗時增大到原來的10 倍。所以,pre-load 雖然會導(dǎo)致QMSS packet DMA 流量不足成為凸顯的瓶頸,但是從總體效率來看還是非常必要的。
細(xì)心的讀者可能會發(fā)現(xiàn)76% + 20% = 96%,并不是100%。我們分析時間戳發(fā)現(xiàn),8 個DSP 核同時運(yùn)行的場景下,每個核處理一個100*2048 ×2048*16 的矩陣乘的時間比只有一個DSP 核運(yùn)行的場景下的時間稍長。原因是:我們的演示用例中X 矩陣和Z 矩陣是存儲在shared L2 的,8 個核同時運(yùn)行就會同時讀寫這兩個buffer,導(dǎo)致產(chǎn)生shared L2 的bank 沖突。所以性能下降了。
3、總結(jié)
OpenEM具有使用簡單,功能實(shí)用,執(zhí)行高效的特點(diǎn)。能在KeyStone 多核DSP 上實(shí)現(xiàn)動態(tài)的負(fù)載平衡。它一方面提供了強(qiáng)大的功能,另一方面也給應(yīng)用留出了很大的靈活性。例如,通過讓應(yīng)用初始化free pool 方便了buffer 的管理。OpenEM 的現(xiàn)有功能已經(jīng)能夠支持基本的應(yīng)用。隨著版本更新功能還將不斷完善。
Reference
Ref[1] ti.openem.white.paper.pdf 位于OpenEM 安裝目錄