【2023 · CANN訓(xùn)練營第一季】課堂筆記2-算子編程范式及算子實(shí)現(xiàn)
【2023 · CANN訓(xùn)練營第一季】課堂筆記2-算子編程范式及算子實(shí)現(xiàn)
一、算子編程范式
1.簡介:
快速開發(fā)編程的固定步驟
統(tǒng)一代碼框架的開發(fā)捷徑
使用者總結(jié)出的開發(fā)經(jīng)驗(yàn)
面向特定場景的編程思想
定制化的方法論開發(fā)體驗(yàn)
TIK C++編程范式把算子內(nèi)部的處理程序,分成多個(gè)流水任務(wù)(Stage),以張量(Tensor)為數(shù)據(jù)載體,以隊(duì)列(Queue)進(jìn)行任務(wù)之間的通信與同步,以內(nèi)存管理模塊(Pipe)管理任務(wù)間的通信內(nèi)存。
2.流水任務(wù):
流水任務(wù)(Stage)指的是單核處理程序中主程序調(diào)度的并行任務(wù)。在核函數(shù)內(nèi)部,可以通過流水任務(wù)實(shí)現(xiàn)數(shù)據(jù)的并行處理來提升性能。
單核處理程序的功能可以被拆分成3個(gè)流水任務(wù):Stage1、Stage2、Stage3,每個(gè)任務(wù)專注于完成單一功能;需要處理的數(shù)據(jù)被切分成n片,使用Progress1~n表示,每個(gè)任務(wù)需要依次完成n個(gè)數(shù)據(jù)切片的處理。Stage間的箭頭表達(dá)數(shù)據(jù)間的依賴關(guān)系,比如Stage1處理完P(guān)rogress1之后,Stage2才能對Progress1進(jìn)行處理

若Progress的n=3,待處理的數(shù)據(jù)被切分成3片,對于同一片數(shù)據(jù),Stage1、Stage2、Stage3之間的處理具有依賴關(guān)系,需要串行處理;不同的數(shù)據(jù)切片,同一時(shí)間點(diǎn),可以有多個(gè)流水任務(wù)Stage在并行處理,由此達(dá)到任務(wù)并行、提升性能的目的

流水的三大基本任務(wù): CopyIn:負(fù)責(zé)數(shù)據(jù)搬入操作---->輸入數(shù)據(jù)從Global內(nèi)存搬移到Local內(nèi)存
Compute:負(fù)責(zé)矢量計(jì)算操作---->使用local內(nèi)存進(jìn)行計(jì)算
CopyOut:負(fù)責(zé)數(shù)據(jù)搬出操作---->輸入數(shù)據(jù)從Local內(nèi)存搬移到Global內(nèi)存
3.任務(wù)間的通信與同步:
不同的流水任務(wù)之間存在數(shù)據(jù)依賴,需要進(jìn)行數(shù)據(jù)傳遞 TIK C++中使用Queue隊(duì)列完成任務(wù)之間的數(shù)據(jù)通信和同步,Queue提供了EnQue、DeQue等基礎(chǔ)API Queue隊(duì)列管理NPU上不同層級的物理內(nèi)存時(shí),用一種抽象的邏輯位置(QuePosition)來表達(dá)各個(gè)級別的存儲(Storage Scope),代替了片上物理存儲的概念,開發(fā)者無需感知硬件架構(gòu) 矢量編程中Queue類型(邏輯位置)包括:VECIN、VECOUT
上文提到的Local內(nèi)存和Global內(nèi)存,是數(shù)據(jù)的載體,他們分別使用GlobalTensor和LocalTensor作為數(shù)據(jù)的基本操作單元,它是各種指令A(yù)PI直接調(diào)用的對象
矢量編程介紹:
矢量編程中有兩個(gè)邏輯位置(QuePosition):
搬入數(shù)據(jù)的存放位置:VECIN
搬出數(shù)據(jù)的存放位置:VECOUT
其操作也是可以按照流水任務(wù)的三個(gè)stage對應(yīng)其三個(gè)主要流水任務(wù)
Stage1:CopyIn任務(wù)
1.使用DataCopy接口將GlobalTensor拷貝到LocalTensor。
2.使用EnQue將LocalTensor放入VECIN的Queue中
Stage2:Compute任務(wù)
1.使用DeQue從VECIN中取出LocalTensor
2.使用TIK C++指令A(yù)PI完成矢量計(jì)算:Add 3.使用EnQue將結(jié)果LocalTensor放入VECOUT的Queue中
Stage3:CopyOut任務(wù)
1.使用DeQue接口從VECOUT的Queue中取出LocalTensor
2.使用DataCopy接口將LocalTensor拷貝到GlobalTensor

4.內(nèi)存管理
任務(wù)間數(shù)據(jù)傳遞使用到的內(nèi)存統(tǒng)一由內(nèi)存管理模塊Pipe進(jìn)行管理 Pipe作為片上內(nèi)存管理者,通過InitBuffer接口對外提供Queue內(nèi)存初始化功能,開發(fā)者可以通過該接口為指定的Queue分配內(nèi)存 Queue隊(duì)列內(nèi)存初始化完成后,需要使用內(nèi)存時(shí),通過調(diào)用AllocTensor來為LocalTensor分配內(nèi)存給Tensor,當(dāng)創(chuàng)建的LocalTensor完成相關(guān)計(jì)算無需再使用時(shí),再調(diào)用FreeTensor來回收LocalTensor的內(nèi)存

5.臨時(shí)變量的內(nèi)存管理
編程過程中使用到的臨時(shí)變量內(nèi)存同樣通過Pipe進(jìn)行管理。臨時(shí)變量可以使用TBuf數(shù)據(jù)結(jié)構(gòu)來申請指定QuePosition上的存儲空間,并使用Get()來將分配到的存儲空間分配給新的LocalTensor
從TBuf上獲取全部長度,或者獲取指定長度的LocalTensor
ps:使用TBuf申請的內(nèi)存空間只能參與計(jì)算,無法執(zhí)行Queue隊(duì)列的入隊(duì)出隊(duì)操作
二、算子開發(fā)流程--矢量算子的編程
快速TIK C++算子開發(fā)流程:
完成算子核函數(shù)的開發(fā)
基于內(nèi)核調(diào)用符方式進(jìn)行算子運(yùn)行驗(yàn)證

標(biāo)準(zhǔn)TIK C++算子開發(fā)流程:
完成算子核函數(shù)的開發(fā)
完成單算子網(wǎng)絡(luò)應(yīng)用程序的開發(fā)
基于ACL單算子調(diào)用方式進(jìn)行算子運(yùn)行驗(yàn)證

標(biāo)準(zhǔn)和快速開發(fā)兩種方式的對比

三大步驟:
算子分析:分析算子的數(shù)學(xué)表達(dá)式、輸入、輸出以及計(jì)算邏輯的實(shí)現(xiàn),明確需要調(diào)用的TIK C++接口。?核函數(shù)定義:定義TIK C++算子入口函數(shù)。?根據(jù)矢量編程范式實(shí)現(xiàn)算子類:完成核函數(shù)的內(nèi)部實(shí)現(xiàn)。
(1)算子分析:
明確算子的數(shù)學(xué)表達(dá)式及計(jì)算邏輯
明確輸入和輸出
確定核函數(shù)名稱和參數(shù)
確定算子實(shí)現(xiàn)所需接口
(2)核函數(shù)定義:
1.完成內(nèi)存初始化:
2.完成核心邏輯實(shí)現(xiàn)
3.對于核函數(shù)的調(diào)用,使用內(nèi)置宏__CCE_KT_TEST__來標(biāo)識<<<…>>>僅在NPU模式下才會編譯到(CPU模式g++沒有<<<…>>>的表達(dá)),對核函數(shù)的調(diào)用進(jìn)行封裝,
(3)算子類的實(shí)現(xiàn)
流水任務(wù):
CopyIn任務(wù):將Global Memory上的輸入Tensor xGm和yGm搬運(yùn)至Local Memory,分別存儲在xLocal, yLocal
Compute任務(wù):對xLocal, yLocal執(zhí)行加法操作,計(jì)算結(jié)果存儲在zLocal中
CopyOut任務(wù):將輸出數(shù)據(jù)從zLocal搬運(yùn)至Global Memory上的輸出Tensor zGm中
各任務(wù)的通信方式:
CopyIn,Compute任務(wù)間通過VECIN隊(duì)列inQueueX,inQueueY進(jìn)行通信和同步
Compute,CopyOut任務(wù)間通過VECOUT隊(duì)列outQueueZ進(jìn)行通信和同步
pipe內(nèi)存管理對象對任務(wù)間交互使用到的內(nèi)存、臨時(shí)變量使用到的內(nèi)存統(tǒng)一進(jìn)行管理

Init函數(shù)的實(shí)現(xiàn)
這里老師講解的例子是一個(gè)add算子,shape是(8,2048)的,打算將其放入8個(gè)核進(jìn)行計(jì)算,這里老師的init代碼如下:

使用多核并行計(jì)算的方法:
需要將數(shù)據(jù)切片,獲取到每個(gè)核實(shí)際需要處理的在Global Memory上的內(nèi)存偏移地址 數(shù)據(jù)整體長度TOTAL_LENGTH為8* 2048,平均分配到8個(gè)核上運(yùn)行,每個(gè)核上處理的數(shù)據(jù)大小BLOCK_LENGTH為2048。block_idx為核的邏輯ID,(gm?half*)x + block_idx * BLOCK_LENGTH即索引為block_idx的核的輸入數(shù)據(jù)在Global Memory上的內(nèi)存偏移地址
使用單核處理數(shù)據(jù)的方法:
可以進(jìn)行數(shù)據(jù)切塊(Tiling),將數(shù)據(jù)切分成8塊。切分后的每個(gè)數(shù)據(jù)塊再次切分成BUFFER_NUM=2塊,可開啟double buffer,實(shí)現(xiàn)流水線之間的并行 單核需要處理的2048個(gè)數(shù)被切分成16塊,每塊TILE_LENGTH=128個(gè)數(shù)據(jù)。Pipe為inQueueX分配了BUFFER_NUM塊大小為TILE_LENGTH * sizeof(half)個(gè)字節(jié)的內(nèi)存塊,每個(gè)內(nèi)存塊能容納TILE_LENGTH=128個(gè)half類型數(shù)據(jù)
Process()函數(shù)實(shí)現(xiàn)
處理函數(shù)的實(shí)現(xiàn)主要就是三大流水任務(wù):
copyIn:

Compute:

copyOut:

double buffer機(jī)制
double buffer通過將數(shù)據(jù)搬運(yùn)與矢量計(jì)算并行執(zhí)行以隱藏?cái)?shù)據(jù)搬運(yùn)時(shí)間并降低矢量指令的等待時(shí)間,最終提高矢量計(jì)算單元的利用效率 1個(gè)Tensor同一時(shí)間只能進(jìn)行搬入、計(jì)算和搬出三個(gè)流水任務(wù)中的一個(gè),其他兩個(gè)流水任務(wù)涉及的硬件單元?jiǎng)t處于Idle狀態(tài) 如果將待處理的數(shù)據(jù)一分為二,比如Tensor1、Tensor2
當(dāng)矢量計(jì)算單元對Tensor1進(jìn)行Compute時(shí),Tensor2可以執(zhí)行CopyIn的任務(wù)
當(dāng)矢量計(jì)算單元對Tensor2進(jìn)行Compute時(shí),Tensor1可以執(zhí)行CopyOut的任務(wù)
當(dāng)矢量計(jì)算單元對Tensor2進(jìn)行CopyOut時(shí),Tensor1可以執(zhí)行CopyIn的任務(wù)
由此,數(shù)據(jù)的進(jìn)出搬運(yùn)和矢量計(jì)算之間實(shí)現(xiàn)并行,硬件單元閑置問題得以有效緩解

ps:該文僅是為了記錄CANN訓(xùn)練營的學(xué)習(xí)過程所用,不參與任何商業(yè)用途