Cadence DSP 算子開發(fā)上手指南

作者:洪超 | 曠視科技 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)化耸采。

image

從圖中可以直觀的得到 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)系的若干步驟耳峦,如下所示:

image

工具鏈介紹

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ù):

image

可以看到 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è)階段的匯編代碼粘貼到一張圖中,如下:

image

從圖中可以看出昵宇,所謂的 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ì)照查看:

  1. 確認(rèn)算子頻繁訪問(wèn)的數(shù)據(jù)是不是位于 dram 上勇吊?而不是 ddr 上。

  2. 確認(rèn)是否使用了 pingpog dma窍仰,以及使用了之后是否真的隱藏了 dma 搬運(yùn)的開銷汉规?可以檢查 kernel 耗時(shí)占總耗時(shí)的比例。

  3. 確認(rèn) load/store 和 gather/scatter 訪問(wèn)的 dram 指針是否都加上了__restrict?

  4. 如果算子核心計(jì)算邏輯是兩重 for 循環(huán)辈赋,確認(rèn)是否將 tile_height 作為內(nèi)層循環(huán)鲫忍?以及是否將外層循環(huán) unroll?

  5. 如果使用了 gather 指令,請(qǐng)查看 gather stall 的 cycle 數(shù)是否很高钥屈?然后對(duì)癥下藥悟民。

  6. 確認(rèn)算子內(nèi)循環(huán)中使用的局部變量是否過(guò)多?如 filter2d 在 kernel_size 大于等于 5 的情況篷就,為了避免寄存器溢出射亏,需要將單一的內(nèi)循環(huán)拆分成多個(gè)小的獨(dú)立的內(nèi)循環(huán)。

  7. 如果是 elemwise 類操作竭业,計(jì)算邏輯比較復(fù)雜智润,但是每一個(gè)像素值最終的處理結(jié)果在一個(gè)比較有限的范圍內(nèi),比如一些色彩處理類算子未辆,輸入是 u8 或 u8 的二元組窟绷,經(jīng)過(guò)一系列處理邏輯,最后的結(jié)果還是 u8 或 u8 的二元組咐柜。這種情況下建議轉(zhuǎn)換思路看看查表操作是否 ok(因?yàn)槲覀冊(cè)?DSP 上有 SuperGather)兼蜈。

  8. 如果算子有多個(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市卷要,隨后出現(xiàn)的幾起案子渣聚,更是在濱河造成了極大的恐慌,老刑警劉巖僧叉,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奕枝,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡瓶堕,警方通過(guò)查閱死者的電腦和手機(jī)隘道,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)郎笆,“玉大人谭梗,你說(shuō)我怎么就攤上這事⊥痱荆” “怎么了激捏?”我有些...
    開封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)凄吏。 經(jīng)常有香客問(wèn)我缩幸,道長(zhǎng),這世上最難降的妖魔是什么竞思? 我笑而不...
    開封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任表谊,我火速辦了婚禮,結(jié)果婚禮上盖喷,老公的妹妹穿的比我還像新娘爆办。我一直安慰自己,他們只是感情好课梳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開白布距辆。 她就那樣靜靜地躺著余佃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪跨算。 梳的紋絲不亂的頭發(fā)上爆土,一...
    開封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音诸蚕,去河邊找鬼步势。 笑死,一個(gè)胖子當(dāng)著我的面吹牛背犯,可吹牛的內(nèi)容都是我干的坏瘩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼漠魏,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼倔矾!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起柱锹,我...
    開封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤哪自,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后禁熏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體提陶,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年匹层,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了隙笆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡升筏,死狀恐怖撑柔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情您访,我是刑警寧澤铅忿,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站灵汪,受9級(jí)特大地震影響檀训,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜享言,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一峻凫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧览露,春花似錦荧琼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)堰乔。三九已至,卻和暖如春脐恩,著一層夾襖步出監(jiān)牢的瞬間镐侯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工驶冒, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留苟翻,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓只怎,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親怜俐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子身堡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容