作者:洪超 | 曠視科技 MegEngine 架構(gòu)師
前言
Cadence 的 Vision P6/Q6/Q7 系列 DSP 在很多的 ISP (“Image Signal Processor”) 芯片中都有部署瞬浓,可以在圖像處理場(chǎng)景補(bǔ)充甚至碾壓 CPU 算力僻爽。而且 Cadence 官方提供了一個(gè)比較全的基礎(chǔ)算子庫(kù) libxi,很多標(biāo)準(zhǔn)算子在 libxi 中都有特定參數(shù)組合下的參考實(shí)現(xiàn)来破。但是鑒于 Cadence DSP 開發(fā)群體比較小敲董,網(wǎng)絡(luò)上能找到的中文資源幾乎沒(méi)有惠猿,從零進(jìn)入開發(fā)狀態(tài)的門檻還是不低的颓鲜。本文梳理了一些 Cadence DSP 算子開發(fā)中的重點(diǎn)孽水,希望可以給對(duì) Cadence DSP 開發(fā)有興趣的同學(xué)帶來(lái)幫助。
DSP 架構(gòu)特點(diǎn)
首先匣屡,以 Cadence 的 Q7 為例涩拙,介紹一下 DSP 架構(gòu)上的特性。下圖是 Q7 硬件架構(gòu)的簡(jiǎn)化耸采。
從圖中可以直觀的得到 DSP 處理器的算力、寄存器等信息工育,注意 DSP 上有兩塊 data ram(簡(jiǎn)稱 dram)虾宇,每一塊 dram 又分為兩個(gè)寬為 512bit 的 bank。同時(shí)如绸,DSP 上有兩個(gè) Load/Store 單元嘱朽,Load/Store 模塊訪問(wèn) dram 的帶寬都是 512bit旭贬,所以理論上的訪存帶寬是 1024bit/cycle,而獨(dú)立于 Load/Store 的 SuperGather 模塊是為了支持 DSP 上高效的 gather/scatter 操作搪泳。另外稀轨,可以看到 DSP 還有一個(gè) dma 模塊,該模塊用于片外空間和 dram 之間的數(shù)據(jù)傳輸岸军。
為了充分利用算力和訪存能力奋刽,Cadence DSP 支持了 SIMD(Single Instruction, Multiple Data) 和 VLIW(Very Long Insruction Word) 兩種特性。前者支持 64lanes * 8bit 或 32lanes * 16bit 等總位寬為 512bit 的向量訪存和向量計(jì)算艰赞,后者是一種謀求指令級(jí)并行 (ILP, instruction level parallelism) 的技術(shù)佣谐。VLIW 可以將多個(gè)指令打包后在一起同時(shí)發(fā)射,從而獲取指令級(jí)的并行度方妖。與超標(biāo)量狭魂、亂序執(zhí)行等其他 ILP 技術(shù)不同的是,VLIW 的并行指令排布是在編譯期就確定好的党觅,而不需要 CPU 進(jìn)行復(fù)雜的運(yùn)行時(shí)調(diào)度雌澄。VLIW 使得 DSP 處理器在不需要大幅增加硬件復(fù)雜度的情況下,就可以獲取 ILP 的加速收益杯瞻。
還要補(bǔ)充一點(diǎn)镐牺,Cadence DSP 是哈弗架構(gòu),其指令和數(shù)據(jù)獨(dú)立編址又兵,具體的編址規(guī)格由 LSP(Linker Support Package) 決定任柜,而用戶可以通過(guò)名為 memmap.xmm 的內(nèi)存配置文件來(lái)定義和修改 LSP。截取了一段 xmm 文件的內(nèi)容沛厨,簡(jiǎn)單注釋如下:
// 存指令的地址段
BEGIN iram0
0xe000000: instRam : iram0 : 0x8000 : executable宙地,writable ;
iram0_0 : F : 0xe000000 - 0xe007fff : .iram0.literal .iram0.text ...
END iram0
// 256k 的 dram0
BEGIN dram0
0xe080000: dataRam : dram0 : 0x40000 : writable ;
dram0_0 : C : 0xe080000 - 0xe0bffff : .dram0.rodata .dram0.data .dram0.bss;
END dram0
// 240k 的 dram1
BEGIN dram1
0xe0c0000: dataRam : dram1 : 0x3c000 : writable ;
dram1_0 : C : 0xe0c0000 - 0xe0fbfff : .dram1.rodata .dram1.data .dram1.bss;
END dram1
// 16k 的棧空間逆皮,創(chuàng)建在 dram1 的尾巴后面
BEGIN dram1_stack
0xe0fc000: dataRam : dram1_stack : 0x4000 : writable ;
dram1_stack : C : 0xe0fc000 - 0xe0fffff : STACK : ;
END dram1_stack
// 存 os 相關(guān)的地址段
BEGIN sram0
0x10000000: instRam : sram0 : 0x2000000 : executable宅粥,writable ;
sram0 : F : 0x10000000 - 0x11ffffff: HEAP : .sram.rodata .rtos.data
END sram0
從注釋中我們可以看出,xmm 文件規(guī)定了運(yùn)行時(shí)的數(shù)據(jù)电谣、指令秽梅、棧、os 等各部分的地址范圍剿牺。
算子調(diào)用流程
有了上一節(jié)的背景知識(shí)企垦,我們來(lái)感性地了解下一個(gè) DSP 算子是如何被調(diào)起來(lái)的。
我們從 CPU 側(cè)發(fā)起調(diào)用晒来,通過(guò) rpc 協(xié)議調(diào)起 DSP 側(cè)提供的服務(wù)钞诡,將 CPU 側(cè)程序稱為 rpc_host,而 DSP 側(cè)程序稱為 rpc_dsp。rpc_dsp 負(fù)責(zé)起一個(gè)線程監(jiān)聽來(lái)自 rpc_host 的 message荧降,并從 message 解析出需要進(jìn)行的動(dòng)作接箫,并在執(zhí)行完該動(dòng)作后回復(fù) rpc_host 一個(gè) message。我們需要預(yù)先將 rpc_dsp 編譯成可執(zhí)行程序朵诫,再將可執(zhí)行程序 dump 成 bin 文件辛友,這里稱為 dsp_bin(包含 iram.bin 和 sram.bin)。而 CPU 側(cè)負(fù)責(zé)準(zhǔn)備算子調(diào)用的所有輸入剪返,并裝載編譯好的 dsp_bin 到 DSP 的 dram 中(前文介紹 LSP 的部分有說(shuō)明應(yīng)該如何進(jìn)行內(nèi)存映射)废累,同時(shí)把 rpc_dsp 側(cè)的監(jiān)聽線程 run 起來(lái),最后 rpc_host 發(fā)起 rpc 調(diào)用并等待 rpc 返回随夸。
需要說(shuō)明一點(diǎn)九默,CPU 和 DSP 之間一般會(huì)使用 IPCM(核間通信模塊)實(shí)現(xiàn)對(duì)一段 ddr 地址空間的共享。但是 DSP 直接訪問(wèn)這段 ddr 的延遲是遠(yuǎn)大于訪問(wèn) dram 的延遲宾毒,所以對(duì)于算子執(zhí)行過(guò)程中需要頻繁訪問(wèn)的 ddr 數(shù)據(jù)驼修,一般是先使用 dma 將其搬運(yùn)到 dram 上,算子執(zhí)行結(jié)束后诈铛,計(jì)算的輸出再通過(guò) dma 搬回到 ddr乙各。
以上就是算子調(diào)用流程的概述,搭配了一張時(shí)序圖幢竹,圖中用虛線框標(biāo)出了具有時(shí)序關(guān)系的若干步驟耳峦,如下所示:
工具鏈介紹
Cadence 為 DSP 開發(fā)者提供了 Xtensa 開發(fā)包,里面包含了一整套編譯焕毫、鏈接蹲坷、執(zhí)行、調(diào)試等相關(guān)的命令行工具邑飒。這些命令用法上很類似 GUN 的標(biāo)準(zhǔn)工具循签,而 Cadence 主要是加強(qiáng)了編譯的部分,因?yàn)榍懊嫣岬?Cadence DSP 使用 VLIW 進(jìn)行加速疙咸,而 VLIW 技術(shù)要求編譯器做更多的事情县匠,來(lái)盡可能獲得一個(gè)更優(yōu)的編譯期指令排布。
上一節(jié)講述的調(diào)用流程是在 DSP 硬件上跑算子的流程撒轮,看上去不是很友好乞旦。好在 Xtensa 工具包里還提供了 Cadence DSP 的模擬器,使用 xt-run 命令就可以在模擬器中執(zhí)行算子题山,從而使得開發(fā)驗(yàn)證兰粉、性能調(diào)試都可以脫離真實(shí)的硬件。
下面就以"hello world"為例顶瞳,介紹一下命令行工具的使用:
// file: hello_world.c
#include <stdio.h>
int main() {
printf("hello world\n");
return 0;
}
編譯:
xt-xcc hello_world.c -o hello_world.bin
不帶內(nèi)存模型執(zhí)行亲桦,用于算子初版實(shí)現(xiàn)崖蜜,不模擬訪存延遲:
xt-run ./hello_world.bin
帶內(nèi)存模型執(zhí)行,仿真性能非常逼近 DSP 硬件上的速度:
xt-run --mem_model ./hello_world.bin
帶--summary 選項(xiàng)執(zhí)行客峭,可以對(duì) cycle 分布有一個(gè)統(tǒng)計(jì)結(jié)果,比如 retaired inrstuction抡柿、branch delay舔琅、cache_miss 等各部分的 cycle 占比:
xt-run --summary ./hello_world.bin
如果需要 gdb 調(diào)試的話,可以用 xt-gdb:
xt-gdb ./hello_world.bin
如果需要 profiling 的話洲劣,需要先在執(zhí)行期加--client_cmds="profile --all gmon.out 選項(xiàng)备蚓,用于在當(dāng)前目錄下生成各種 profiling 文件,包括 gmon.out.cyc, gmon.out.bdelay, gmon.out.interlock 等囱稽,然后使用 xt-gprof 工具查看上一步生成的 profiling 文件郊尝,比如執(zhí)行下面兩行命令就可以查看函數(shù)級(jí)別的 cycle 分布:
xt-run --client_cmds="profile --all gmon.out" ./hello_world.bin
xt-gprof ./hello_world.bin ./gmon.out.cyc > hello_world_cyc.txt
分塊計(jì)算
Cadence DSP 主要應(yīng)用場(chǎng)景是圖像處理,現(xiàn)實(shí)的業(yè)務(wù)中圖片尺寸經(jīng)常都是 1080P 甚至 4K 的分辨率战惊,而 DSP 的 dram 容量雖然可配置流昏,但是通常都是 200KB 左右的級(jí)別(壕配十幾兆 dram 的是例外),根本放不下一張大圖吞获,這就是導(dǎo)致了我們的算子必須分塊計(jì)算况凉。通過(guò)將大圖分成一個(gè)個(gè)小塊(tile), 每次通過(guò) dma 從 ddr 搬運(yùn)一個(gè) src_tile 到 dram 上,執(zhí)行算子得到一個(gè) dst_tile, 再通過(guò) dma 把 dst_tile 搬到 ddr 上各拷。
認(rèn)識(shí) tile
拿一張圖說(shuō)明一下 tile 的具體參數(shù):
可以看到 tile 分兩層刁绒,里層的紅色區(qū)域是原始數(shù)據(jù)區(qū)域,尺寸即 tile_width*tile_height, 外層是一圈 edge, 因?yàn)橛行┧阕硬僮骺臼颍热?filter2d知市,計(jì)算的時(shí)候需要 padding,edge 的尺寸即為 padding 的大小速蕊。也正是因?yàn)?edge 的存在嫂丙,才有了 pData 和 pBuffer 的區(qū)分。
dram 內(nèi)存管理
tile 是分配在 dram 上的互例,就是 xmm 文件中的 dram0 和 dram1 段奢入,dram 是我們自由使用的,所以就需要一個(gè)內(nèi)存管理的邏輯媳叨。
首先定義一個(gè)數(shù)據(jù)結(jié)構(gòu) DramCtrl:
struct DramCtrl {
char* dram_start; // xmm 文件中 dram0/1 的起始地址
char* dram_end; // xmm 文件中 dram0/1 的終止地址
char* dram_cst_use; // 算子開發(fā)中可以自由使用的起始地址
char* dram_free; // 當(dāng)前尚未分配區(qū)域的起始地址
char* dram_idx; // 區(qū)分不同 dram 段的索引
};
其中 dram_cst_use 參數(shù)的存在是因?yàn)橛行┳兞勘仨毞峙湓?dram 上腥光,但是在調(diào)用不同算子的時(shí)候不需要更新,表現(xiàn)出一定的持久性糊秆。這種變量就包括 DramCtrl 本身武福,還有 dma 用于定義傳輸任務(wù)的 descriptors,所以刨掉這部分變量占用的空間痘番,從 dram_cst_use 位置開始的 dram 才是算子調(diào)用自由使用的空間捉片。
有了數(shù)據(jù)結(jié)構(gòu)之后平痰,還需要定義一些接口函數(shù),才能滿足基本的管理需求:
void dram_init(): 在 DSP 開機(jī)后伍纫,調(diào)用第一個(gè)算子前宗雇,執(zhí)行 dram_init,初始化 DramCtrl 結(jié)構(gòu)體莹规,dram_cst_use=dram_free=dram_start+sizeof(DramCtrl)
void dram_static_alloc(): 在 dma_init 調(diào)用之后赔蒲,分配 dma 的 descriptors,dram_cst_use+=sizeof(descriptors), dram_free=dram_cst_use
void dram_free_size(): 查詢當(dāng)前還有多少空閑內(nèi)存良漱,返回的是 dram_end-dram_free
void dram_alloc(sz): 分配 tile 等變量的空間舞虱,先 check 空閑空間大小,分配成功后修改 dram_free+=sz
void dram_reset(): 在一次算子執(zhí)行結(jié)束后調(diào)用母市,重置 dram_free=dram_cst_use
pingpong dma 搬運(yùn)
dma 完成一次 tile 搬運(yùn)的延遲是相當(dāng)可觀的矾兜,如果 dma 搬運(yùn)與算子調(diào)用是串行執(zhí)行的話,性能就會(huì)嚴(yán)重受累于 dma 的搬運(yùn)患久。所以正確的做法是椅寺,借用 pingpong buffer 的概念,在計(jì)算當(dāng)前 tile 的同時(shí)墙杯,進(jìn)行下一個(gè) tile 的預(yù)取配并,這樣 dma 搬運(yùn)的時(shí)間就可以被計(jì)算時(shí)間隱藏「吒洌基于 pingpong dma 的算子執(zhí)行邏輯如下:
step 0. dram_alloc src_tile[2], dst_tile[2] and set pingpong = 0
step 1. dma pull src_tile[pingpong]
step 2. dma sync, make src_tile[pingpong] be ready on dram
// loop begin ->
loop_for (h = 0; h < image_height; h += tile_height)
loop_for (w = 0; w < image_width; w += tile_width)
step 3. prefetch, using dma pull src_tile[pingpong^1]
step 4. exec on src_tile[pingpong] to get dst_tile[pingong]
step 5. dma sync, sync for last iter dma push and this iter prefetch
step 6. dma push dst_tile[pingong]
step 7. pingpong = pingpong^1
// loop end <-
step 8. dma sync & dram_reset
分塊邏輯
現(xiàn)在溉旋,我們已經(jīng)認(rèn)識(shí)了 tile 的概念,有了簡(jiǎn)單的 dram 內(nèi)存管理嫉髓,以及 pingpong dma 搬運(yùn)和計(jì)算并行的邏輯观腊,但是還缺了一塊兒:分塊邏輯。分塊就是在 dram 容量的約束條件下算行,依據(jù) src_tile 和 dst_tile 的尺寸關(guān)系確定 tile 的尺寸梧油。其實(shí)沒(méi)有普適的分塊邏輯,很多時(shí)候都是具體問(wèn)題具體分析州邢,這里筆者根據(jù)開發(fā)經(jīng)驗(yàn)給出三種分類:
- 第一類:src_tile 和 dst_tile 尺寸一致
比如 elelwise 類和 filter 類儡陨,elemwise 類算子輸入輸出的尺寸是完全一樣的,filter 類只比 elemwise 類多了一圈 tile_edge量淌。這一類算子的 tile 尺寸很好確定:假定算子的輸入輸出 image 個(gè)數(shù)之和為 inout_cnt骗村,且 tile_width 等于 tile_height,則有
tile_w=tile_h=srqt(min_dram_sz / inout_cnt)
其中呀枢,min_dram_sz 是取兩個(gè) dram 容量的小值胚股,因?yàn)?pingpong dma 的需要,實(shí)際分配的 tile 總數(shù)是 inout_cnt * 2裙秋。
- 第二類:src_tile 和 dst_tile 的尺寸不相等琅拌,但是有明確的相對(duì)關(guān)系
比如 resize 算子缨伊,src_tile 和 dst_tile 的尺寸不再是一樣的,但是縮放比例 scale_x 和 scale_y 決定了 tile 的尺寸關(guān)系:
dst_tile_w=dst_tile_h=srqt(min_dram_sz / (1.0 + scale_x * scale_y))
src_tile_w=dst_tile_w * scale_x
src_tile_h=dst_tile_h * scale_y
- 第三類:src_tile 和 dst_tile 沒(méi)有明確的尺寸關(guān)系
比如 warp_perspective 算子进宝,因?yàn)橐粋€(gè)矩形的 dst_tile 通過(guò) warp_perspective 映射到 src_image 上刻坊,得到的是一個(gè)凸四邊形,需要框出這個(gè)凸四邊形的 bounding_box 作為 src_tile党晋。另外紧唱,很重要的一點(diǎn),相同尺寸不同坐標(biāo)位置的 dst_tile 映射得到的 src_tile 尺寸也是不一樣的隶校。為了保證 dst_image 中所有 dst_tile 映射得到的 src_tile 在 dram 中都能放得下,就需要一個(gè)搜索策略來(lái)確定 tile 的尺寸:
int guess_tile_size(min_dram_sz, frame) {
int l = 0, r = sqrt(min_dram_sz);
while (l <= r) {
int mid = (l + r) / 2;
int ret = 0;
ret = iter_warp_perspective(mid, frame);
if (ret < 0)
r = mid - 1; // ret < 0 表示當(dāng)前嘗試的 dst_tile 的尺寸會(huì)使得 src_tile 在 dram 上放不下蛹锰,所以可行域直接減半
else
l = mid + 1; // ret = 0 表示當(dāng)前嘗試的 dst_tile 的尺寸是 ok 的深胳,但是繼續(xù)嘗試更優(yōu)解
}
if (r < 0) {
LOG(ERROR, "get tile size failed %d\n");
return -1;
}
LOG(DEBUG, "get the best guess tile width %d\n", r);
return r;
}
其中,frame 里存的 dst_image 的整圖尺寸铜犬,iter_warp_perspective 里的邏輯就是遍歷 dst_image 各個(gè)坐標(biāo)位置的 dst_tile舞终,通過(guò) warp_perspective 的映射矩陣反算出 src_tile 的 bounding_box 的大小,并檢查 dram 是否放得下癣猾。如果所有位置的 check 都通過(guò)了敛劝,iter_warp_perspective 返回 0,反之返回-1纷宇。
ISA 介紹
先插播一段語(yǔ)法介紹夸盟,Cadence DSP 上的 SIMD 指令大體由四部分組成:prefix_op_size_suffix。第一部分的指令前綴都是 IVP(image vector prcessing); 第二部分就是具體運(yùn)算指令的名稱縮寫像捶,如 ADD,MUL,SEL 等上陕;第三部分是指定向量中的通道關(guān)系,比如是 64lanes * 8bit 還是 32lanes * 16it拓春,不過(guò)前者實(shí)際寫成 2NX8, 后者寫成 NX16释簿,因?yàn)樵谶@里 N 表示 32; 第四部分是一些后綴的修飾詞,比如 U 表示一元運(yùn)算的數(shù)據(jù)是無(wú)符號(hào)數(shù)硼莽,US 表示二元運(yùn)算的數(shù)據(jù)分別是無(wú)符號(hào)數(shù)和有符號(hào)數(shù)庶溶,T 表示該運(yùn)算會(huì)帶 mask, PACK 表示該運(yùn)算會(huì)對(duì)中間計(jì)算結(jié)果做位壓縮再返回較窄的數(shù)據(jù)類型,等等懂鸵。
現(xiàn)在放幾條簡(jiǎn)單的 SIMD 指令偏螺,讓大家對(duì)號(hào)入座,溫故一下:
IVP_ADDNX16: 32lanes * 16bit 有符號(hào)整數(shù)的加法運(yùn)算
IVP_MUL2NX8U: 64lanes * 8bit 無(wú)符號(hào)整數(shù)的乘法運(yùn)算
IVP_LV2NX8U_I: LV 表示 vector load, _I 后綴在這里是表示有一個(gè)立即數(shù)(immediate)的 offset矾瑰,該命令是在一個(gè) 64byte 對(duì)齊的地址(base_ptr + offset)上 load 64lanes * 8bit 的數(shù)據(jù)
考慮到介紹 ISA 是比較枯燥的砖茸,而且很多人對(duì) CPU 上的 SIMD 指令都有一些了解,所以這里只展開介紹四組較于一般的 SIMD 實(shí)現(xiàn)有一些不同點(diǎn)殴穴,同時(shí)使用頻率非常高的指令凉夯。
第一組:帶指針自動(dòng)對(duì)齊货葬、自動(dòng)偏移以及支持可變長(zhǎng)度的 VLOAD 指令
Cadence DSP 要求 VLOAD 訪問(wèn)不可以跨 bank,而 bank 的位寬是 512bit劲够,也即限制了 VLOAD 的地址必須是 64byte 對(duì)齊的震桶。如果地址滿足對(duì)齊要求,就可以使用 IVP_LVxxx 指令直接進(jìn)行訪存操作征绎,反之就需要使用 IVP_LAxxx 指令進(jìn)行指針自動(dòng)對(duì)齊的訪存操作:
void IVP_LAVNX16_XP(xb_vecNx16 v_out, valign a_load /*inout*/, const xb_vecNx16 * src_ptr /*inout*/, int bytes_cnt);
在單次或連續(xù)一組 IVP_LAVNX16_XP 調(diào)用前需要調(diào)用一次:
valign a_load = IVP_LANX16_PP(src_ptr);
其中蹲姐,a_load 存放的是起始地址為 [src_ptr & 0x40] 連續(xù) 64byte 的數(shù)據(jù),a_load 的 64bytes 和 [src_ptr & 0x40 + 64] 地址處連續(xù) load 的 64bytes 組成一個(gè) 128byte 的數(shù)組人柿,以 [src_ptr | 0x40] 為偏移量從 128bytes 的數(shù)組中截取 bytes_cnt 個(gè) bytes 輸出到 v_out柴墩。注意 bytes_cnt 的值會(huì)被截?cái)嗟?0~64 的合法范圍,也意味著這個(gè)指令可以 cover 不足 64byte 的 load 操作凫岖,也就是所謂 tail_load江咳。還有一個(gè)要說(shuō)明的特點(diǎn)是,這個(gè)指令在 load 操作完成后會(huì)更新 src_ptr 和 a_load哥放,src_ptr 的偏移量為 bytes_cnt 截?cái)嗪蟮闹导咧福琣_load 更新為 v_out 的內(nèi)容,這兩項(xiàng)更新使得該指令可以連續(xù)調(diào)用甥雕,而不用重新調(diào)用 IVP_LANX16_PP 和手動(dòng)移動(dòng)指針 src_ptr踩身。
第二組:multiply、pack
Cadence DSP 上典型的計(jì)算流是 load 數(shù)據(jù)到 vector 中社露,施加計(jì)算指令挟阻,如果得到的中間結(jié)果的數(shù)值范圍有升位的需求,就需要用位寬更大的 wide vector 來(lái)存呵哨,而后再通過(guò) PACK 類指令將 wide vector 中的數(shù)據(jù)安全地壓縮到 vector 的位寬表達(dá)范圍內(nèi):
xb_vec2Nx24 IVP_MULUSP2N8XR16(xb_vec2Nx8U b, xb_vec2Nx8U c, xb_int32 d);
上面這條命令是兩個(gè)類型為 xb_vec2Nx8U 的 vector 和兩個(gè) int16 捉對(duì)進(jìn)行向量乘法赁濒,兩個(gè)向量乘法的結(jié)果做一次向量加法,得到的輸出是類型為 xb_vec2Nx24 的 wide vector孟害。兩組乘法分別是 b 和 d 的高 16 位之間進(jìn)行拒炎,以及 c 和 d 的低 16 位之間進(jìn)行。
xb_vec2Nx8U IVP_PACKVRU2NX24(xb_vec2Nx24 b, int c);
而這條 pack 指令就是可以將類型為 xb_vec2Nx24 的 wide vector 中每一個(gè)通道 24bit 的數(shù)據(jù)右移 c 位挨务,接著飽和處理到 u8 的表達(dá)范圍击你,得到的輸出就是類型為 xb_vec2Nx8U 的 vector。
第三組:select
有時(shí)候我們的算法邏輯需要對(duì)兩個(gè) vector 進(jìn)行 interleave 或者 deinterleave谎柄,下面這個(gè)指令就可以實(shí)現(xiàn):
void IVP_DSELNX16I(xb_vecNx16 a, xb_vecNx16 b, xb_vecNx16 c, xb_vecNx16 d, immediate e);
該命令會(huì)將 d 中的 64byte 數(shù)據(jù)看成 64lanes * u8本刽,c 中的 64byte 數(shù)據(jù)看成 64lanes * u8糙捺,然后先 d 后 c拦坠,從低位到高位滥酥,將 d 和 c 中的數(shù)據(jù)拼接成一個(gè) 128lanes * u8 數(shù)組。而 e 是一個(gè)立即數(shù)劈猿,e 的每一個(gè)不同取值都對(duì)應(yīng)一個(gè)預(yù)置的 index_list拙吉,每一個(gè) index_list 都是 0~127 整數(shù)序列的一個(gè)重排列潮孽。預(yù)置的 index_list 有 8bit/16bit 兩種粒度的 interleave/deinterleave 操作,額外的筷黔,還支持各種 rotate_left/rotate_right 操作往史。
但是可能會(huì)遇到,IVP_DSEL2NX8I 中預(yù)置的若干種 index_list 均不能滿足我們的需求佛舱,那就需要下面這個(gè)命令:
void IVP_DSELNX16(xb_vecNx16 a, xb_vecNx16 b, xb_vecNx16 c, xb_vecNx16 d, xb_vec2Nx8 e);
該命令將 d 和 c 中的數(shù)據(jù)按照先 d 后 c, 從低位到高位的順序排成了一個(gè) 64lanes * 16bit 的數(shù)組椎例,而 e 中數(shù)據(jù)就是 0~63 整數(shù)序的一個(gè)自定義序列。
第四組:gather/scatter
最后一組要介紹的指令就是高效的 gather/scatter请祖。gather 是從一組不連續(xù)的 dram 地址中 load 數(shù)據(jù)存到一個(gè) vector 里面订歪,而 scatter 就是反向操作,將一個(gè) vector 里面的數(shù)據(jù) store 到離散的 dram 地址中去肆捕。想象中 gather/scatter 的指令開銷應(yīng)該非常大陌粹,但是實(shí)際應(yīng)用中發(fā)現(xiàn) gather/scatter 確實(shí)比一般的指令多花一些 cycle,但是 overhead 不明顯福压,且有些場(chǎng)景不用 gather/scatter 的話 SIMD 就玩不轉(zhuǎn)了,就只能用標(biāo)量計(jì)算了或舞。
簡(jiǎn)單解釋一下 Cadence DSP 的 gather/scatter 效率高的原因荆姆。gather/scatter 指令不同于普通指令,gather/scatter 在觸發(fā) issue 之后是由 SuperGather 硬件模塊全權(quán)接管映凳。后者會(huì)將 dram 512bit 寬的 bank 進(jìn)一步拆分成 8 個(gè) 64bit 寬的 sub-bank胆筒,并從硬件層面支持同時(shí) load 分布在不同 sub_bank 的數(shù)據(jù)(當(dāng)然這里存在更嚴(yán)重的 sub_bank_conflict 的風(fēng)險(xiǎn),后文詳細(xì)解釋)诈豌。此外仆救,gather 還被拆分成兩個(gè)子命令,gathera 和 gatherd矫渔。gathera 才是 SuperGather 實(shí)際接管的指令彤蔽,該指令負(fù)責(zé)收集離散地址上的數(shù)據(jù)到 gr 寄存器(gather register)。拆分指令的原因是 gathera 可以異步執(zhí)行庙洼,不阻塞 DSP 的處理器繼續(xù)執(zhí)行其他指令顿痪。而 gatherd 是一條執(zhí)行在 DSP 處理器上的指令,負(fù)責(zé)將 gr 寄存器里面收集完畢的數(shù)據(jù)拷貝到普通的 vector 寄存器油够,所以只有依賴 gatherd 返回值的命令才必須等待 gather 操作執(zhí)行完畢蚁袭。至于 scatter,除了 sub_bank 并發(fā) store 的功勞石咬,最關(guān)鍵的原因是 scatter 在遇到 sub_bank_conflict 的時(shí)候會(huì)做硬件層面的 buffer揩悄,等到有空閑 slot 的時(shí)候再調(diào)度 store 操作。
gather/scatter 的指令如下:
xb_gsr IVP_GATHERANX8U(const unsigned char * base_ptr, xb_vecNx16U offset_vec);
xb_vecNx16U IVP_GATHERDNX16(xb_gsr b);
void IVP_SCATTERNX16U(xb_vecNx16U out, const unsigned short * base_ptr, xb_vecNx16U offset_vec);
這里解釋一下鬼悠,IVP_GATHERANX8U 是 gather 32lanes * u8 的數(shù)據(jù)删性,然后每一個(gè)通道的 u8 數(shù)據(jù)高位補(bǔ) 0 拓展到 u16, 所以 gr 寄存器里面存的是 32lanes * u16 的數(shù)據(jù)亏娜。
性能優(yōu)化
前文介紹了一些高頻使用的 SIMD 指令,讀者可以嘗試開發(fā)自己的 DSP 算子了镇匀,但是第一版實(shí)現(xiàn)的性能可能不 ok照藻,所以本節(jié)將補(bǔ)充一些優(yōu)化算子性能的知識(shí)點(diǎn)。
理解 SWP
首先要介紹 Cadence DSP 的編譯器進(jìn)行優(yōu)化調(diào)度的核心概念--SWP(software pipeline)晰韵,不同于處理器執(zhí)行指令時(shí)進(jìn)行硬件層面的流水雪猪,SWP 是編譯器對(duì)算子 inner loop 的不同 iter 的指令進(jìn)行軟件層面的流水官觅,目的就是讓 inner loop 編譯后的 VLIW 中有效指令的密度更高序苏,最小化 nop 的比例。
為了更直觀的理解 SWP, 下面以 alphablend 為例,詳細(xì)講解一下編譯器實(shí)際調(diào)度得到的 SWP:
#define _LOCAL_DRAM0_ __attribute__((section(".dram0.data"))) // 變量是分配在 dram0 上
#define _LOCAL_DRAM1_ __attribute__((section(".dram1.data"))) // 變量是分配在 dram1 上
#define ALIGN64 __attribute__((aligned(64))) // 變量在 dram 上的起始地址是 64byte 對(duì)齊的
#define WIDTH 256
#define HEIGHT 32
#define DATA_SIZE 8192 // 256 * 32 = 8192
uint8_t _LOCAL_DRAM0_ ALIGN64 src0[DATA_SIZE];
uint8_t _LOCAL_DRAM1_ ALIGN64 src1[DATA_SIZE];
uint8_t _LOCAL_DRAM1_ ALIGN64 dst[DATA_SIZE];
void alpha_blend(uint8_t* psrc0, uint8_t* psrc1, uint8_t* pdst, int16_t alpha) {
int32_t i, j, alpha_beta;
xb_vec2Nx8U* __restrict vpsrc0 = (xb_vec2Nx8U*) psrc0;
xb_vec2Nx8U* __restrict vpsrc1 = (xb_vec2Nx8U*) psrc1;
xb_vec2Nx8U* __restrict vpdst = (xb_vec2Nx8U*) pdst;
xb_vec2Nx8U vsrc0, vsrc1, vdst;
xb_vec2Nx24 wvec0;
alpha_beta = ((0x3fff - alpha) << 16) + alpha;
// DATA_SIZE = 256 * 32
// XCHAL_IVPN_SIMD_WIDTH = 32
for (i = 0; i < DATA_SIZE / 2 / XCHAL_IVPN_SIMD_WIDTH; ++i) {
vsrc0 = *vpsrc0++; // 因?yàn)檫@里 psrc0/psrc1 的地址是 64byte 對(duì)齊的鸳玩,
// 所以匯編指令為 ivp_lv2nx8_ip vsrc0,vpsrc0,64
vsrc1 = *vpsrc1++;
wvec0 = IVP_MULUSP2N8XR16(vsrc1, vsrc0, alpha_beta);
vdst = IVP_PACKVRU2NX24(wvec0, 14);
*vpdst++ = vdst;
}
}
// call alpha_blend in main function
alpha_blend(src0, src1, dst, 8192);
上面的代碼就是用 SIMD 寫的一個(gè) alpha_blend 算子吕座,使用命令行工具拿到編譯器調(diào)度后的匯編文件:
xt-xcc -S alphablend.c -o alphablend.s -O2
截取匯編文件中的 SWP 的部分如下:
#<loop> Loop body line 139, nesting depth: 1, kernel iterations: 62
#<loop> unrolled 2 times
#<swps>
#<swps> 4 cycles per pipeline stage in steady state with unroll=2
#<swps> 3 pipeline stages
#<swps> 10 real ops (excluding nop)
#<swps>
#<swps> 4 cycles lower bound required by resources
#<swps> min 3 cycles required by recurrences
#<swps> min 4 cycles required by resources/recurrence
#<swps> min 9 cycles required for critical path
#<swps> 12 cycles non-loop schedule length
#<swps> register file usage:
#<swps> 'a' total 4 out of 16 [2-4,10]
#<swps> 'v' total 6 out of 32 [0-5]
#<swps> 'wv' total 2 out of 4 [0-1]
#<swps> 'pr' total 1 out of 16 [0]
#<swps>
#<freq> BB:72 => BB:72 probability = 0.98438
#<freq> BB:72 => BB:79 probability = 0.01562
.frequency 1.000 63.492
// steady 階段
{ # format N2
ivp_lv2nx8_ip v0,a2,128 # [0*II+0] id:45
ivp_lv2nx8_i v3,a2,64 # [0*II+0] id:45
}
{ # format N2
ivp_lv2nx8_ip v1,a3,128 # [0*II+1] id:46
ivp_lv2nx8_i v4,a3,64 # [0*II+1] id:46
}
{ # format F0
ivp_sv2nx8_ip v2,a4,128 # [2*II+2] id:47
ivp_packvru2nx24 v2,wv0,a10 # [1*II+2]
ivp_mulusp2n8xr16 wv0,v1,v0,pr0 # [0*II+2]
nop #
}
{ # format F0
ivp_sv2nx8_i v5,a4,-64 # [2*II+3] id:47
ivp_packvru2nx24 v5,wv1,a10 # [1*II+3]
ivp_mulusp2n8xr16 wv1,v4,v3,pr0 # [0*II+3]
nop #
}
從注釋部分可以看出,編譯器對(duì)循環(huán)體做了 unroll=2 的循環(huán)展開,展開后的 loop count 是 62涵但,得到了一個(gè) 4cycle 3stage 的 SWP,并且告訴你該 SWP 中發(fā)射了 10 個(gè)非 nop 的指令(可以計(jì)算一下 CPI(cycle per instruction) 為 0.4)审姓,額外的還有一些寄存器占用比例的分析數(shù)據(jù)魔吐。
代碼部分的每一個(gè)花括號(hào)就是一個(gè) VLIW画畅,注意每一個(gè) VLIW 編碼的指令數(shù)可以不一樣淫僻,這是因?yàn)?Cadence DSP 支持了十余種不同 format 的 VLIW(代碼中每個(gè)花括號(hào)上面都有一句注釋表明了 format 類型)针贬。然后每一句指令的右邊都有一句注釋,只需關(guān)注方括號(hào)的部分谆棱,加號(hào)右邊的數(shù)字是表征當(dāng)前指令在 SWP 的第幾個(gè) cycle 發(fā)射出去快压,加號(hào)左邊與 II 相乘的數(shù)字表征的是 stage。因?yàn)?alphablend 調(diào)度出來(lái)的是一個(gè) 3stage 的 SWP垃瞧,所以可以看到與 II 相乘的數(shù)字是 0,1,2蔫劣,將其分別指代成 stage 0/1/2。這里 3 stage 的意思是个从,SWP 里會(huì)出現(xiàn)一個(gè) VLIW 同時(shí)打包了三個(gè) iter(unroll 之后的三個(gè) iter)的指令拦宣,stage 0 是當(dāng)前 iter, stage 1 是上一個(gè) iter, stage 2 是上上一個(gè) iter。
類比處理器硬件的 pipeline,上面這段代碼準(zhǔn)確說(shuō)是 SWP 流水線填滿的 steady 階段鸵隧,SWP 也有流水線填充和退出的階段绸罗,分別稱為 prologue 和 epilogue。這也解釋了原始 loop 的 loop count 其實(shí)是 256 * 32 / 32 / 2 為 128豆瘫,但是 SWP unroll 之后的 loop count 不是 64珊蟀,而是 62。為了更直觀的理解 SWP外驱,下面將 prologue育灸、steady 和 epilogue 三個(gè)階段的匯編代碼粘貼到一張圖中,如下:
從圖中可以看出昵宇,所謂的 SWP 就是將一個(gè)原始 iter 下不同的指令看成不同的 stage磅崭,并應(yīng)用流水線的概念,把一個(gè)原始 iter 中有嚴(yán)格時(shí)序邏輯的多個(gè)指令的發(fā)射時(shí)機(jī)分散到 SWP 的不同 iter 中瓦哎,目的就是追求更低的 CPI砸喻。
其實(shí),我們還可以根據(jù)匯編代碼估計(jì)算子的執(zhí)行時(shí)間:steady 階段 4cycle * 62 + prologue 階段 7cycle + epilogue 階段 5cycle = 260cycle蒋譬,執(zhí)行 xt-run 測(cè)得這個(gè)循環(huán)體的耗時(shí)是 276cycle割岛,其他的 overhead 是 276-260=16cycle,所以根據(jù)匯編代碼估算的計(jì)算時(shí)間已經(jīng)很準(zhǔn)了(但是也有例外犯助,后文會(huì)提及)癣漆。
了解了 SWP 的概念,接下來(lái)我們對(duì) alphablend 的實(shí)現(xiàn)做一些修改剂买,觀察對(duì) SWP 的影響惠爽。第一個(gè)試驗(yàn)就是將修飾指針變量的__restrict 去掉,重新拿到匯編文件瞬哼,SWP 現(xiàn)在長(zhǎng)這樣:
#<loop> Loop body line 143, nesting depth: 1, iterations: 128
#<swps>
#<swps> 8 cycles per pipeline stage in steady state with unroll=1
#<swps> 1 pipeline stages
#<swps> 5 real ops (excluding nop)
#<swps>
#<swps> 2 cycles lower bound required by resources
#<swps> min 8 cycles required by recurrences
#<swps> min 8 cycles required by resources/recurrence
#<swps> min 8 cycles required for critical path
#<swps> 8 cycles non-loop schedule length
#<swps> register file usage:
#<swps> 'a' total 4 out of 16 [2-4,11]
#<swps> 'v' total 2 out of 32 [0-1]
#<swps> 'wv' total 1 out of 4 [0]
#<swps> 'pr' total 1 out of 16 [0]
#<swps>
#<freq> BB:30 => BB:30 probability = 0.99219
#<freq> BB:30 => BB:32 probability = 0.00781
.frequency 1.000 127.992
{ # format N2
ivp_lv2nx8_ip v0,a2,64 # [0*II+0] id:49
ivp_lv2nx8_ip v1,a3,64 # [0*II+0] id:50
}
{ # format N1
nop #
ivp_mulusp2n8xr16 wv0,v1,v0,pr0 # [0*II+1]
}
{ # format N2
nop #
ivp_packvru2nx24 v0,wv0,a11 # [0*II+4]
}
{ # format N1
ivp_sv2nx8_ip v0,a4,64 # [0*II+7] id:51
nop #
}
可以看到新的 SWP 沒(méi)有 unroll, 且 stage 為 1婚肆,這種情況表示編譯器沒(méi)有幫我們做 software pipeline,盡管還有這段 SWP 的注釋代碼倒槐,但是沒(méi)有做任何有意義的調(diào)度。現(xiàn)在的 CPI=8/5=1.6附井,之前的版本 CPI 是 0.4, 所以性能下降了 3 倍多讨越。當(dāng)然這里速度差別這么大,還有一個(gè)原因是 inner loop 的邏輯太簡(jiǎn)單永毅,不 unroll 的情況下編譯器實(shí)在沒(méi)有啥可調(diào)度的空間把跨,無(wú)法發(fā)揮 VLIW 的優(yōu)勢(shì)。如果 inner loop 邏輯比較復(fù)雜沼死,即使不 unroll着逐,編譯器通過(guò) VLIW 也能提高指令的并行度,與 SWP 有效調(diào)度后的性能差距就不會(huì)如此明顯。
這里解釋一下耸别,可能有讀者看到 SWP 里面只有 4 個(gè) VLIW健芭,不清楚為啥要用 8 個(gè) cycle。請(qǐng)注意看每條指令右側(cè)方括號(hào)里面的注釋秀姐,cycle 數(shù)確實(shí)是橫跨了 0~7慈迈,而中間不連續(xù)的數(shù)字都會(huì)替換成相應(yīng)個(gè)數(shù)的 bubble。為什么會(huì)產(chǎn)生 bubble? 是因?yàn)楫?dāng)前內(nèi)循環(huán)的四條 VLIW省有,調(diào)度的是同一個(gè) iter 的不同指令痒留,相鄰的 VLIW 的數(shù)據(jù)又都是寫后讀的依賴關(guān)系,所以連續(xù)發(fā)射出去之后蠢沿,會(huì)存在前一個(gè)指令的結(jié)果還沒(méi)有寫回伸头,后一個(gè)指令已經(jīng)讀取該數(shù)并來(lái)到 execute 階段了,也就是所謂的 pipeline interlock舷蟀。為了解決 interlock恤磷,就需要在前后兩條 VLIW 之間加一定數(shù)量的 bubble⊙┙模可能還會(huì)有細(xì)心的讀者糾結(jié)為啥 vload 和 vmul 之間沒(méi)有 bubble碗殷,這是因?yàn)楣P者在 Q7 上跑的代碼,而 Q7 對(duì) dram 的 vload 延遲做了特殊優(yōu)化速缨。如果在 P6 上跑這個(gè)代碼锌妻,就會(huì)看到 vload 和 vmul 之間也會(huì)有 bubble。
接著旬牲,我們針對(duì) SWP 做第二個(gè)試驗(yàn):地址非對(duì)齊訪問(wèn)仿粹。前面說(shuō)過(guò) src0/src1 都是 64byte 對(duì)齊的,所以看匯編代碼會(huì)發(fā)現(xiàn)原茅,vload 實(shí)際使用的是 ivp_lv2nx8_ip 指令吭历。但是假設(shè)現(xiàn)在無(wú)法保證 src0/src1 是 64bytes 對(duì)齊的,就需要實(shí)現(xiàn)以下更通用版本的 alphablend:
void alpha_blend(uint8_t* psrc0, uint8_t* psrc1, uint8_t* pdst, int16_t alpha) {
// 注意擂橘,直接粗暴的把 64byte 對(duì)齊的地址都加了 1晌区,構(gòu)造非對(duì)齊地址
psrc0++;
psrc1++;
pdst++;
int32_t i, j, alpha_beta;
xb_vec2Nx8U* __restrict vpsrc0 = (xb_vec2Nx8U*) psrc0;
xb_vec2Nx8U* __restrict vpsrc1 = (xb_vec2Nx8U*) psrc1;
xb_vec2Nx8U* __restrict vpdst = (xb_vec2Nx8U*) pdst;
xb_vec2Nx8U vsrc0, vsrc1, vdst;
xb_vec2Nx24 wvec0;
alpha_beta = ((0x3fff - alpha) << 16) + alpha;
// DATA_SIZE = 256 * 32
// XCHAL_IVPN_SIMD_WIDTH = 32
valign va_dst = IVP_ZALIGN();
valign a_load1 = IVP_LA2NX8U_PP(vpsrc0);
valign a_load2 = IVP_LA2NX8U_PP(vpsrc1);
for (i = 0; i < DATA_SIZE / 2 / XCHAL_IVPN_SIMD_WIDTH; ++i) {
IVP_LAV2NX8U_XP(vsrc0, a_load1, vpsrc0, DATA_SIZE - 1 - i * 64);
IVP_LAV2NX8U_XP(vsrc1, a_load2, vpsrc1, DATA_SIZE - 1 - i * 64);
wvec0 = IVP_MULUSP2N8XR16(vsrc1, vsrc0, alpha_beta);
vdst = IVP_PACKVRU2NX24(wvec0, 14);
IVP_SAV2NX8U_XP(vdst, va_dst, vpdst, DATA_SIZE - 1 - i * 64);
}
IVP_SAV2NX8UPOS_FP(va_dst, vpdst);
}
使用 xt-run 測(cè)得內(nèi)循環(huán)的耗時(shí)是 298cycle,略多于對(duì)齊地址版本的 276cycle通贞,繼續(xù)查看非對(duì)齊地址版本的 SWP(unroll 太多朗若,篇幅原因只截取 SWP 頭部的注釋):
#<loop> Loop body line 112, nesting depth: 1, kernel iterations: 15
#<loop> unrolled 8 times
#<swps>
#<swps> 16 cycles per pipeline stage in steady state with unroll=8
#<swps> 2 pipeline stages
#<swps> 48 real ops (excluding nop)
#<swps>
#<swps> 14 cycles lower bound required by resources
#<swps> min 8 cycles required by recurrences
#<swps> min 14 cycles required by resources/recurrence
#<swps> min 15 cycles required for critical path
#<swps> 23 cycles non-loop schedule length
#<swps> register file usage:
#<swps> 'a' total 12 out of 16 [2-5,8-15]
#<swps> 'v' total 4 out of 32 [0-3]
#<swps> 'u' total 3 out of 4 [0-2]
#<swps> 'wv' total 2 out of 4 [0-1]
#<swps> 'pr' total 1 out of 16 [0]
#<swps>
#<freq> BB:83 => BB:83 probability = 0.93750
#<freq> BB:83 => BB:88 probability = 0.06250
發(fā)現(xiàn)編譯器搜出來(lái)一個(gè) unroll=8,CPI=16/48=0.33(比地址對(duì)齊版本的 0.4 更低一點(diǎn))的 SWP昌罩,但是因?yàn)?unroll 太大哭懈,prologue/epilogue 的 CPI 比較大才導(dǎo)致總的 cycle 數(shù)略大于地址對(duì)齊版本。但是如果 loop_count 更大一點(diǎn)茎用,兩個(gè)版本的速度差異就更小了遣总。不知道讀者會(huì)不會(huì)失望了睬罗,這個(gè)試驗(yàn)的結(jié)果并沒(méi)有告訴我們?cè)趺醋鏊俣雀欤皇堑贸鲆粋€(gè)結(jié)論:非地址對(duì)齊的算子速度不一定比地址對(duì)齊的版本要慢旭斥,但是非地址對(duì)齊版本的算子會(huì)更通用一點(diǎn)容达。
理解 bank_conflict
回過(guò)頭來(lái)填一個(gè)坑,為什么基于匯編代碼估算 DSP 時(shí)間有時(shí)候會(huì)不準(zhǔn)琉预?其實(shí)原因前文也有提過(guò)董饰,就是 bank_conflict 和 sub_bank_confilct 搞的鬼。
先說(shuō) bank_conflict 的影響圆米,還是拿前面的 alphablend 做例子卒暂。該算子有兩個(gè)輸入 src0/src1,如果有兩條 vload 指令被調(diào)度到同一個(gè) VLIW 里面娄帖,且訪問(wèn)的兩個(gè)地址是同一個(gè) dram 上同一個(gè) bank 的不同位置也祠,就觸發(fā)了 bank_confilct,處理器必須 stall 一個(gè) cycle近速。直覺(jué)告訴我們?nèi)绻麑?src0 和 src1 放在不同的 dram 上诈嘿,應(yīng)該會(huì)降低 bank_confilct 發(fā)生的概率。
做個(gè)試驗(yàn)驗(yàn)證下削葱,把前面最初版本的 alphablend 的 src0/src1 都放到 dram0 上奖亚,測(cè)得內(nèi)循環(huán)的耗時(shí)從原先的 276cycles 變成了 280cycles。速度下降好像并不明顯析砸,查看 SWP 沒(méi)有發(fā)生任何變化昔字,后面這點(diǎn)倒是符合預(yù)期,因?yàn)榫幾g期并不會(huì)檢查每一次地址訪問(wèn)有沒(méi)有發(fā)生 bank_conflict首繁。仔細(xì)看匯編代碼可以發(fā)現(xiàn)作郭,steady 階段的代碼都是同一個(gè) dram 的連續(xù)兩個(gè) 64byte 的 vload 被放在了一個(gè) VLIW 里面,所以本次試驗(yàn)修改對(duì)其不產(chǎn)生影響弦疮。增加的 4 個(gè) cycle 其實(shí)是因?yàn)?prologue 階段有四個(gè)綁定了不同 dram 上 vload 指令的 VLIW夹攒,恰好這四個(gè) VLIW 都觸發(fā)了 bank_confilct⌒踩考慮到不同算子的調(diào)度情況是不一樣的咏尝,為了減少 bank_conflict 對(duì)性能的影響,我們還是應(yīng)該將多個(gè)輸入 tile 創(chuàng)建在不同的 dram 上啸罢。
接著编检,我們來(lái)分析一下 sub_bank_conflcit 對(duì)性能的影響。sub_bank_conflict 只會(huì)發(fā)生在 gathera 指令的執(zhí)行過(guò)程中伺糠,當(dāng) gathera 收集非連續(xù)地址上的多個(gè)數(shù)據(jù)時(shí)蒙谓,如果出現(xiàn)多個(gè)數(shù)據(jù)的地址正好在同一個(gè) dram 的同一個(gè) bank 的同一個(gè) sub_bank 中的不同地址時(shí)斥季,就會(huì)出現(xiàn)多次 sub_bank_conflict训桶,最極端的情況下收集一個(gè) vector32累驮,卻出現(xiàn)了 32 次 conflcit,gathera 收集完成需要 32 個(gè) cycle舵揭。
所以如果我們開發(fā)的算子里面用到了 gathera 指令谤专,請(qǐng)加上-mem_model 選項(xiàng)在模擬器上跑一下,執(zhí)行完會(huì)打印一些統(tǒng)計(jì)參數(shù)午绳,其中一項(xiàng)就是 gather stall 的 cycle 數(shù)置侍。如果不幸 gather stall 比較大,就需要檢查算子里面的訪存邏輯拦焚,看看是否需要調(diào)整 tile 上數(shù)據(jù)的分布情況蜡坊,比如可以在 tile 的水平方向插入若干列的無(wú)用數(shù)據(jù),降低 gathera 目標(biāo)數(shù)據(jù)在同一個(gè) sub_bank 的可能性赎败。
性能優(yōu)化小結(jié)
綜上秕衙,從算子實(shí)現(xiàn)的維度上看,Cadence DSP 算子的速度只受限于 SWP 調(diào)度出來(lái)的 CPI僵刮,以及訪存的 bank 沖突据忘。
最后,將散落在本文各個(gè)地方會(huì)影響算子性能的點(diǎn)集中到一起(還有一些可以降低 CPI 的技巧前文沒(méi)有提及)搞糕,做成 checklist 便于大家對(duì)照查看:
確認(rèn)算子頻繁訪問(wèn)的數(shù)據(jù)是不是位于 dram 上勇吊?而不是 ddr 上。
確認(rèn)是否使用了 pingpog dma窍仰,以及使用了之后是否真的隱藏了 dma 搬運(yùn)的開銷汉规?可以檢查 kernel 耗時(shí)占總耗時(shí)的比例。
確認(rèn) load/store 和 gather/scatter 訪問(wèn)的 dram 指針是否都加上了__restrict?
如果算子核心計(jì)算邏輯是兩重 for 循環(huán)辈赋,確認(rèn)是否將 tile_height 作為內(nèi)層循環(huán)鲫忍?以及是否將外層循環(huán) unroll?
如果使用了 gather 指令,請(qǐng)查看 gather stall 的 cycle 數(shù)是否很高钥屈?然后對(duì)癥下藥悟民。
確認(rèn)算子內(nèi)循環(huán)中使用的局部變量是否過(guò)多?如 filter2d 在 kernel_size 大于等于 5 的情況篷就,為了避免寄存器溢出射亏,需要將單一的內(nèi)循環(huán)拆分成多個(gè)小的獨(dú)立的內(nèi)循環(huán)。
如果是 elemwise 類操作竭业,計(jì)算邏輯比較復(fù)雜智润,但是每一個(gè)像素值最終的處理結(jié)果在一個(gè)比較有限的范圍內(nèi),比如一些色彩處理類算子未辆,輸入是 u8 或 u8 的二元組窟绷,經(jīng)過(guò)一系列處理邏輯,最后的結(jié)果還是 u8 或 u8 的二元組咐柜。這種情況下建議轉(zhuǎn)換思路看看查表操作是否 ok(因?yàn)槲覀冊(cè)?DSP 上有 SuperGather)兼蜈。
如果算子有多個(gè)輸入攘残,確認(rèn)有沒(méi)有施加降低 bank_confilct 的措施?
9为狸、如果從較早的 DSP 型號(hào)上移植算子到較新的 DSP 型號(hào)上歼郭,比如移植 P6 上的代碼到 Q7,需要注意新指令的應(yīng)用辐棒,比如 Q7 比 P6 多了 Dual-Quad 8x8 和 Quad 32x16 multiply 兩個(gè)可選的增強(qiáng)模塊病曾。
雜項(xiàng)
本節(jié)整理了一些在前文不便展開的細(xì)節(jié),但其實(shí)也很重要:
gathera 指令有很多變種漾根,有些變種 gathera 指令的 offset_vec 是 16lanes * u32, 但是必須注意的是 offset_vec 里的數(shù)據(jù)必須是 0~65535 范圍內(nèi)泰涂。否則,SuperGather 會(huì)讀取不到對(duì)應(yīng)地址的數(shù)據(jù)辐怕,并給該 lanes 直接賦 0负敏。
開發(fā)測(cè)試的過(guò)程中可能會(huì)有修改 memmap.xmm 文件的需求,比如調(diào)整椕厣撸空間的位置和大小其做,只需要編輯完 xmm 文件之后,使用 xtensa 命令行工具 xt-genldscripts赁还,在 xmm 文件所在目錄下執(zhí)行
"xt-genldscripts -b ."
命令妖泄,即可在。/ldscripts 目錄下得到新的 linker scripts 文件艘策,對(duì) xmm 的修改也即生效蹈胡。舉兩個(gè)有可能發(fā)生高頻 sub_bank_conflict 的例子:一個(gè)例子是 padding 算子在做 left_padding 和 right_padding 的時(shí)候,會(huì) gather 一個(gè) tile 的某一列連續(xù)若干個(gè)數(shù)據(jù)朋蔫,如果恰好該列所有的數(shù)據(jù)都在同一個(gè) sub_bank 就會(huì)性能非常差罚渐;還有一個(gè)例子就是 transpose,dst_tile 的一行其實(shí)是 src_tile 的一列驯妄,所以 gather 的時(shí)候同樣有可能出現(xiàn)極端的 sub_bank_conflict荷并。
解釋一下編譯器只對(duì) inner loop 應(yīng)用 SWP 調(diào)度優(yōu)化的原因,Cadence DSP 的應(yīng)用定位就是圖形處理青扔,而一般圖形處理算法的 inner loop 計(jì)算密度非常高源织,幾乎決定了整個(gè)算法的性能。
SWP 優(yōu)化調(diào)度的 inner loop 實(shí)際上是號(hào)稱 zero overhead loop 的微猖,也就是說(shuō)沒(méi)有普通 loop 檢查循環(huán)條件谈息,更新 loop iter 等工作的開銷,但是上面的 alphablend 的例子中好像 inner loop 還是有一些 overhead 的凛剥,是因?yàn)橄胍@取 zero overhead 的 inner loop侠仇,還需要兩個(gè)額外條件:inner loop 里面指令數(shù)不能太少,且 loop count 相對(duì)較大犁珠。
前文提到將不同的輸入 tile 存放在不同 dram 上來(lái)減少 bank_confilct逻炊,但是減少 bank_conflict 的終極策略是:先將不同的 tile 存放在不同的 dram 上踢代,然后代碼里使用
#pragma ymemory (tile_on_dram0)
告訴編譯器哪一個(gè) tile 是在 dram1 上的,編譯器會(huì)將該 tile 的內(nèi)存類型標(biāo)記為 ymemory嗅骄,而其他 tile 的內(nèi)存類型標(biāo)記為 xmemory,最后增加一個(gè)編譯選項(xiàng)-mcbox饼疙,告訴編譯器只有訪問(wèn)不同內(nèi)存類型的兩個(gè) vload 指令才可以打包到同一個(gè) VLIW 里去溺森。
總結(jié)
本文詳細(xì)介紹了 Cadence DSP 的架構(gòu)特點(diǎn),算子的調(diào)用流程窑眯,算子的分塊執(zhí)行邏輯屏积,以及算子的開發(fā)、調(diào)試和優(yōu)化實(shí)踐磅甩,希望可以給后面從事相關(guān)開發(fā)的同學(xué)起到一個(gè)拋磚引玉的作用炊林。
GitHub 源碼:https://github.com/MegEngine/MegEngine