【2023 · CANN訓(xùn)練營(yíng)第一季】——Ascend C算子背后的魔法

前言:TIK C++,2023年CANN的一個(gè)神奇魔法,得益于TIK C++算子的孿生調(diào)試技術(shù),我們可以了解到更多的技術(shù)細(xì)節(jié),本文試圖對(duì)隱藏在多核并行,流水計(jì)算、dobule buffer背后的CANN Ascend C算子魔法進(jìn)行摸索和理解,是什么樣的技術(shù)讓用戶(hù)編寫(xiě)的簡(jiǎn)單代碼可以先實(shí)現(xiàn)上述神奇的功能。本文沒(méi)有請(qǐng)專(zhuān)業(yè)人士審查,分析的結(jié)果未必正確,只是個(gè)人的一種理解,如有錯(cuò)漏,歡迎大家指正?。?!

一、硬件基礎(chǔ)——向量單元,矩陣單元,數(shù)據(jù)搬運(yùn)DMA并行是基礎(chǔ)?

????????昇騰AI處理器(NPU),可以實(shí)現(xiàn)大規(guī)模神經(jīng)網(wǎng)絡(luò)的計(jì)算加速,如下圖所示,昇騰AI處理器的計(jì)算核心是1個(gè)或若干個(gè)AI Core,負(fù)責(zé)執(zhí)行矩陣、向量、標(biāo)量計(jì)算。TIC C++算子就是運(yùn)行在AiCore上的。

一)、AICore的架構(gòu)

????????如下圖所示,單個(gè)AICore內(nèi),包括:標(biāo)量計(jì)算單元、向量計(jì)算單元、矩陣計(jì)算單元、本地?cái)?shù)據(jù)存儲(chǔ)單元Local Memory,以及負(fù)責(zé)數(shù)據(jù)搬運(yùn)的DMA單元。

????????標(biāo)量計(jì)算單元任務(wù)是進(jìn)行計(jì)算流程控制和地址計(jì)算,向量計(jì)算單元負(fù)責(zé)執(zhí)行向量運(yùn)算,矩陣計(jì)算單元負(fù)責(zé)執(zhí)行矩陣運(yùn)算;運(yùn)算單元計(jì)算的輸入數(shù)據(jù),是DMA從Global Mem上搬運(yùn)到Local Mem上;計(jì)算結(jié)果也存儲(chǔ)在Local Mem上,并由DMA負(fù)責(zé)將計(jì)算結(jié)果搬運(yùn)到Global Mem上。

二)AICore計(jì)算能力

????????AICore的三個(gè)核心計(jì)算單元:

????????1、標(biāo)量計(jì)算單元:執(zhí)行標(biāo)量計(jì)算

????????2、標(biāo)量計(jì)算單元:1cycle可以執(zhí)行128個(gè)FP16的加法;

????????3、向量計(jì)算單元:1cycle可以完成兩個(gè)16*16 FP16的矩陣乘。

二、技術(shù)分析

????????課上,老師講述的一個(gè)ADD算子的實(shí)現(xiàn)。其定義如下:

????????數(shù)據(jù)整體長(zhǎng)度TOTAL_LENGTH為8 * 2048,定義邏輯核熟練USE_CORE_NUM=8(即8個(gè)邏輯核),每個(gè)核上處理的數(shù)據(jù)大小BLOCK_LENGTH為2048。

????????從硬件部分知識(shí),我們知道,單核的向量計(jì)算單元一個(gè)指令周期能處理128個(gè)FP16,所以還需要對(duì)單核數(shù)據(jù)進(jìn)行切分,切分成16塊,每塊128個(gè)FP16。

????????然后通過(guò)TPIPE流水線范式,簡(jiǎn)單的代碼就可以讓CopyIn、Compute、CopyOut這三個(gè)階段實(shí)現(xiàn)流水線工作。

????????還可以啟動(dòng)double buffer,進(jìn)行優(yōu)化,提供流水線的執(zhí)行效率。

????????開(kāi)發(fā)者只需要編寫(xiě)簡(jiǎn)單的代碼,就可以的這樣高效、并行的能力。背后的黑科技令人神往,下面將結(jié)合代碼和CPU側(cè)調(diào)試獲取的信息,做技術(shù)分析。

一)、多核并行:

1、開(kāi)發(fā)者需要編寫(xiě)的代碼:

????????在核函數(shù)定義里,通過(guò)block_idx,計(jì)算出輸入、輸出數(shù)據(jù)在Global Memory上的偏移地址。

2、背后的黑科技:

????????2023年昇騰開(kāi)發(fā)者峰會(huì)上,CANN首席架構(gòu)師閆老師說(shuō):“芯片內(nèi)部有多個(gè)AI核,AI核是同構(gòu)的。寫(xiě)一個(gè)AI核的計(jì)算程序,實(shí)際部署時(shí),會(huì)啟動(dòng)多個(gè)AI核實(shí)例,每個(gè)實(shí)例稱(chēng)之為一個(gè)block,所有block運(yùn)行的程序代碼端是完全一樣的,入?yún)⒁彩峭耆粯拥摹Nㄒ坏膮^(qū)別是block_id不同,block_id是個(gè)內(nèi)嵌變量,不是入?yún)?。block_id對(duì)應(yīng)邏輯核的數(shù)量,按0,1,2...的順序去排,因此實(shí)現(xiàn)了對(duì)數(shù)據(jù)的切分?!?/p>

? ? ? ?本例在CPU調(diào)試模式下,會(huì)生成8個(gè)CCE文件,代表1個(gè)邏輯核的實(shí)際指令,我們可以看到2048個(gè)fp16,對(duì)應(yīng)4096字節(jié)(十六進(jìn)制0x1000),可以看出相鄰2個(gè)CCE文件的偏移地址正好是0x1000(4096字節(jié))。



二)、單核流水線:

1、開(kāi)發(fā)者需要編寫(xiě)的代碼:

????????僅一個(gè)循環(huán),加3行代碼實(shí)現(xiàn)了流水線。

2、背后的魔法:

????????昇騰AI處理器使用set_flag/wait_flag兩條指令組成的指令對(duì),保證隊(duì)列內(nèi)部以及隊(duì)列之間按照邏輯關(guān)系執(zhí)行。set_flag/wait_flag為兩條指令,在set_flag/wait_flag的指令中,可以指定一對(duì)指令隊(duì)列的關(guān)系,表示兩個(gè)隊(duì)列之間完成一組“鎖”機(jī)制,其作用方式為:

set_flag:當(dāng)前序指令的所有讀寫(xiě)操作都完成之后,當(dāng)前指令開(kāi)始執(zhí)行,并將硬件中的對(duì)應(yīng)標(biāo)志位設(shè)置為1。

wait_flag:當(dāng)執(zhí)行到該指令時(shí),如果發(fā)現(xiàn)對(duì)應(yīng)標(biāo)志位為0,該隊(duì)列的后續(xù)指令將一直被阻塞;如果發(fā)現(xiàn)對(duì)應(yīng)標(biāo)志位為1,則將對(duì)應(yīng)標(biāo)志位設(shè)置為0,同時(shí)后續(xù)指令開(kāi)始執(zhí)行。

???????COPYIN單元和COMPUTE單元,通過(guò)set_flag/和wait_flag指令對(duì),如上圖的綠色箭頭,確保COPYIN單元數(shù)據(jù)拷貝完成后,再由COMPUTE單元執(zhí)行運(yùn)算。

????????COPYOUT單元和COMPUTE單元,通過(guò)set_flag/和wait_flag指令對(duì),如上圖的綠色箭頭,確保COMPUTE單元運(yùn)算完成后,再由COPYOUT單元搬運(yùn)計(jì)算結(jié)果。

? ? ? ?這個(gè)機(jī)制,實(shí)現(xiàn)了流水線操作,前序操作完成后,自動(dòng)執(zhí)行有關(guān)聯(lián)的后續(xù)操作。

????? ?不同分塊數(shù)據(jù)(i)之間也是通過(guò)set_flag/和wait_flag指令對(duì),實(shí)現(xiàn)流水操作的。如下圖所示,右側(cè)為不使用double buffer的情況,我們看到在做完第i次ADD后,通過(guò)set_flag操作,可以觸發(fā)第i+1次的COPYIN操作。完成第i次ADD操作后,第i次的輸入數(shù)已經(jīng)不再需要了,可以執(zhí)行第i+1次的COPYIN操作,把數(shù)據(jù)拷貝入Local Memory。下圖,左側(cè)部分是有double bu

ffer的情況,基本原理類(lèi)似,double buffer背后的技術(shù)實(shí)現(xiàn),在下一章節(jié)描述。

三)、Double Buffer

?1、開(kāi)發(fā)者需要編寫(xiě)的代碼:

????????開(kāi)啟dobule buffer,只需要引入BUFFER_NUM變量,并修改2處代碼即可,見(jiàn)下圖。

2、背后的魔法:

???????“話越少,事越大”,double buffer這個(gè)概念困惑了我很久,直到在2023年昇騰開(kāi)發(fā)者峰會(huì)上看到下面的兩張截圖。這兩張圖很好的講述了double buffer的作用,先上圖,后面再分析。

截圖于2023年昇騰AI開(kāi)發(fā)者峰會(huì)算子演示
截圖于2023年昇騰AI開(kāi)發(fā)者峰會(huì)算子演示

????????我們知道,數(shù)據(jù)先要從global memory搬運(yùn)到local memory里(buff1或buff2)里,然后計(jì)算單元會(huì)對(duì)數(shù)據(jù)進(jìn)行計(jì)算,計(jì)算結(jié)果存到local memory,然后再把結(jié)果搬出到global memory里。我們需要注意到,當(dāng)計(jì)算單元完成計(jì)算后,輸入數(shù)據(jù)buff的數(shù)據(jù)已經(jīng)沒(méi)有用了,此時(shí)搬運(yùn)單元會(huì)將下一個(gè)數(shù)據(jù)搬入buff,這是流水線的原理,不是double buffer的魔法。這個(gè)數(shù)據(jù)搬入,然后計(jì)算的過(guò)程,我在第2副圖的用藍(lán)色的線進(jìn)行表示。

????????從上面的分析我們知道,搬運(yùn)單元需要等待計(jì)算完成后,才能搬入下一組數(shù)據(jù)。而Double Buffer要優(yōu)化的是數(shù)據(jù)搬入單元在等待當(dāng)前數(shù)據(jù)計(jì)算完成的時(shí)間。做法就是在開(kāi)辟一個(gè)存儲(chǔ)單元buffer2,當(dāng)運(yùn)算單元計(jì)算當(dāng)前buffer1數(shù)據(jù)時(shí),往buffer2里搬運(yùn)數(shù)據(jù),等計(jì)算單元完成buffer1數(shù)據(jù)計(jì)算時(shí),等buffer2數(shù)據(jù)搬運(yùn)完成后,開(kāi)始計(jì)算buffer2的數(shù)據(jù),此時(shí)由于buffer1的數(shù)據(jù)已經(jīng)完成計(jì)算,所以搬運(yùn)單元又可以立即往buffer1中搬運(yùn)新的數(shù)據(jù)。?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容