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

作者:洪超 | 曠視科技 MegEngine 架構(gòu)師

前言

Cadence 的 Vision P6/Q6/Q7 系列 DSP 在很多的 ISP (“Image Signal Processor”) 芯片中都有部署,可以在圖像處理場景補(bǔ)充甚至碾壓 CPU 算力。而且 Cadence 官方提供了一個(gè)比較全的基礎(chǔ)算子庫 libxi,很多標(biāo)準(zhǔn)算子在 libxi 中都有特定參數(shù)組合下的參考實(shí)現(xiàn)。但是鑒于 Cadence DSP 開發(fā)群體比較小,網(wǎng)絡(luò)上能找到的中文資源幾乎沒有,從零進(jìn)入開發(fā)狀態(tài)的門檻還是不低的。本文梳理了一些 Cadence DSP 算子開發(fā)中的重點(diǎn),希望可以給對 Cadence DSP 開發(fā)有興趣的同學(xué)帶來幫助。

DSP 架構(gòu)特點(diǎn)

首先,以 Cadence 的 Q7 為例,介紹一下 DSP 架構(gòu)上的特性。下圖是 Q7 硬件架構(gòu)的簡化。

image

從圖中可以直觀的得到 DSP 處理器的算力、寄存器等信息,注意 DSP 上有兩塊 data ram(簡稱 dram),每一塊 dram 又分為兩個(gè)寬為 512bit 的 bank。同時(shí),DSP 上有兩個(gè) Load/Store 單元,Load/Store 模塊訪問 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ì)算,后者是一種謀求指令級并行 (ILP, instruction level parallelism) 的技術(shù)。VLIW 可以將多個(gè)指令打包后在一起同時(shí)發(fā)射,從而獲取指令級的并行度。與超標(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) 決定,而用戶可以通過名為 memmap.xmm 的內(nèi)存配置文件來定義和修改 LSP。截取了一段 xmm 文件的內(nèi)容,簡單注釋如下:

// 存指令的地址段
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 的??臻g,創(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é)的背景知識,我們來感性地了解下一個(gè) DSP 算子是如何被調(diào)起來的。

我們從 CPU 側(cè)發(fā)起調(diào)用,通過 rpc 協(xié)議調(diào)起 DSP 側(cè)提供的服務(wù),將 CPU 側(cè)程序稱為 rpc_host,而 DSP 側(cè)程序稱為 rpc_dsp。rpc_dsp 負(fù)責(zé)起一個(gè)線程監(jiān)聽來自 rpc_host 的 message,并從 message 解析出需要進(jìn)行的動作,并在執(zhí)行完該動作后回復(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 的部分有說明應(yīng)該如何進(jìn)行內(nèi)存映射),同時(shí)把 rpc_dsp 側(cè)的監(jiān)聽線程 run 起來,最后 rpc_host 發(fā)起 rpc 調(diào)用并等待 rpc 返回。

需要說明一點(diǎn),CPU 和 DSP 之間一般會使用 IPCM(核間通信模塊)實(shí)現(xiàn)對一段 ddr 地址空間的共享。但是 DSP 直接訪問這段 ddr 的延遲是遠(yuǎn)大于訪問 dram 的延遲,所以對于算子執(zhí)行過程中需要頻繁訪問的 ddr 數(shù)據(jù),一般是先使用 dma 將其搬運(yùn)到 dram 上,算子執(zhí)行結(jié)束后,計(jì)算的輸出再通過 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ù)要求編譯器做更多的事情,來盡可能獲得一個(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í)行,可以對 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ù)級別的 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)用場景是圖像處理,現(xiàn)實(shí)的業(yè)務(wù)中圖片尺寸經(jīng)常都是 1080P 甚至 4K 的分辨率,而 DSP 的 dram 容量雖然可配置,但是通常都是 200KB 左右的級別(壕配十幾兆 dram 的是例外),根本放不下一張大圖,這就是導(dǎo)致了我們的算子必須分塊計(jì)算。通過將大圖分成一個(gè)個(gè)小塊(tile), 每次通過 dma 從 ddr 搬運(yùn)一個(gè) src_tile 到 dram 上,執(zhí)行算子得到一個(gè) dst_tile, 再通過 dma 把 dst_tile 搬到 ddr 上。

認(rèn)識 tile

拿一張圖說明一下 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í)行的話,性能就會嚴(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)識了 tile 的概念,有了簡單的 dram 內(nèi)存管理,以及 pingpong dma 搬運(yùn)和計(jì)算并行的邏輯,但是還缺了一塊兒:分塊邏輯。分塊就是在 dram 容量的約束條件下,依據(jù) src_tile 和 dst_tile 的尺寸關(guān)系確定 tile 的尺寸。其實(shí)沒有普適的分塊邏輯,很多時(shí)候都是具體問題具體分析,這里筆者根據(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 的尺寸不相等,但是有明確的相對關(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 沒有明確的尺寸關(guān)系

比如 warp_perspective 算子,因?yàn)橐粋€(gè)矩形的 dst_tile 通過 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è)搜索策略來確定 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 的尺寸會使得 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,通過 warp_perspective 的映射矩陣反算出 src_tile 的 bounding_box 的大小,并檢查 dram 是否放得下。如果所有位置的 check 都通過了,iter_warp_perspective 返回 0,反之返回-1。

ISA 介紹

先插播一段語法介紹,Cadence DSP 上的 SIMD 指令大體由四部分組成:prefix_op_size_suffix。第一部分的指令前綴都是 IVP(image vector prcessing); 第二部分就是具體運(yùn)算指令的名稱縮寫,如 ADD,MUL,SEL 等;第三部分是指定向量中的通道關(guān)系,比如是 64lanes * 8bit 還是 32lanes * 16it,不過前者實(shí)際寫成 2NX8, 后者寫成 NX16,因?yàn)樵谶@里 N 表示 32; 第四部分是一些后綴的修飾詞,比如 U 表示一元運(yùn)算的數(shù)據(jù)是無符號數(shù),US 表示二元運(yùn)算的數(shù)據(jù)分別是無符號數(shù)和有符號數(shù),T 表示該運(yùn)算會帶 mask, PACK 表示該運(yùn)算會對中間計(jì)算結(jié)果做位壓縮再返回較窄的數(shù)據(jù)類型,等等。

現(xiàn)在放幾條簡單的 SIMD 指令,讓大家對號入座,溫故一下:

IVP_ADDNX16: 32lanes * 16bit 有符號整數(shù)的加法運(yùn)算
IVP_MUL2NX8U: 64lanes * 8bit 無符號整數(shù)的乘法運(yùn)算
IVP_LV2NX8U_I: LV 表示 vector load, _I 后綴在這里是表示有一個(gè)立即數(shù)(immediate)的 offset,該命令是在一個(gè) 64byte 對齊的地址(base_ptr + offset)上 load 64lanes * 8bit 的數(shù)據(jù)

考慮到介紹 ISA 是比較枯燥的,而且很多人對 CPU 上的 SIMD 指令都有一些了解,所以這里只展開介紹四組較于一般的 SIMD 實(shí)現(xiàn)有一些不同點(diǎn),同時(shí)使用頻率非常高的指令。

第一組:帶指針自動對齊、自動偏移以及支持可變長度的 VLOAD 指令

Cadence DSP 要求 VLOAD 訪問不可以跨 bank,而 bank 的位寬是 512bit,也即限制了 VLOAD 的地址必須是 64byte 對齊的。如果地址滿足對齊要求,就可以使用 IVP_LVxxx 指令直接進(jìn)行訪存操作,反之就需要使用 IVP_LAxxx 指令進(jìn)行指針自動對齊的訪存操作:

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 的值會被截?cái)嗟?0~64 的合法范圍,也意味著這個(gè)指令可以 cover 不足 64byte 的 load 操作,也就是所謂 tail_load。還有一個(gè)要說明的特點(diǎn)是,這個(gè)指令在 load 操作完成后會更新 src_ptr 和 a_load,src_ptr 的偏移量為 bytes_cnt 截?cái)嗪蟮闹?,a_load 更新為 v_out 的內(nèi)容,這兩項(xiàng)更新使得該指令可以連續(xù)調(diào)用,而不用重新調(diào)用 IVP_LANX16_PP 和手動移動指針 src_ptr。

第二組:multiply、pack

Cadence DSP 上典型的計(jì)算流是 load 數(shù)據(jù)到 vector 中,施加計(jì)算指令,如果得到的中間結(jié)果的數(shù)值范圍有升位的需求,就需要用位寬更大的 wide vector 來存,而后再通過 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 捉對進(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í)候我們的算法邏輯需要對兩個(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);

該命令會將 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è)不同取值都對應(yīng)一個(gè)預(yù)置的 index_list,每一個(gè) index_list 都是 0~127 整數(shù)序列的一個(gè)重排列。預(yù)置的 index_list 有 8bit/16bit 兩種粒度的 interleave/deinterleave 操作,額外的,還支持各種 rotate_left/rotate_right 操作。

但是可能會遇到,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 不明顯,且有些場景不用 gather/scatter 的話 SIMD 就玩不轉(zhuǎn)了,就只能用標(biāo)量計(jì)算了。

簡單解釋一下 Cadence DSP 的 gather/scatter 效率高的原因。gather/scatter 指令不同于普通指令,gather/scatter 在觸發(fā) issue 之后是由 SuperGather 硬件模塊全權(quán)接管。后者會將 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í)候會做硬件層面的 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)化算子性能的知識點(diǎn)。

理解 SWP

首先要介紹 Cadence DSP 的編譯器進(jìn)行優(yōu)化調(diào)度的核心概念--SWP(software pipeline),不同于處理器執(zhí)行指令時(shí)進(jìn)行硬件層面的流水,SWP 是編譯器對算子 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 對齊的

#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 對齊的,
                       // 所以匯編指令為 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                                 #  
 }

從注釋部分可以看出,編譯器對循環(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è)花括號就是一個(gè) VLIW,注意每一個(gè) VLIW 編碼的指令數(shù)可以不一樣,這是因?yàn)?Cadence DSP 支持了十余種不同 format 的 VLIW(代碼中每個(gè)花括號上面都有一句注釋表明了 format 類型)。然后每一句指令的右邊都有一句注釋,只需關(guān)注方括號的部分,加號右邊的數(shù)字是表征當(dāng)前指令在 SWP 的第幾個(gè) cycle 發(fā)射出去,加號左邊與 II 相乘的數(shù)字表征的是 stage。因?yàn)?alphablend 調(diào)度出來的是一個(gè) 3stage 的 SWP,所以可以看到與 II 相乘的數(shù)字是 0,1,2,將其分別指代成 stage 0/1/2。這里 3 stage 的意思是,SWP 里會出現(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)確說是 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 測得這個(gè)循環(huán)體的耗時(shí)是 276cycle,其他的 overhead 是 276-260=16cycle,所以根據(jù)匯編代碼估算的計(jì)算時(shí)間已經(jīng)很準(zhǔn)了(但是也有例外,后文會提及)。

了解了 SWP 的概念,接下來我們對 alphablend 的實(shí)現(xiàn)做一些修改,觀察對 SWP 的影響。第一個(gè)試驗(yàn)就是將修飾指針變量的__restrict 去掉,重新拿到匯編文件,SWP 現(xiàn)在長這樣:

#<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 沒有 unroll, 且 stage 為 1,這種情況表示編譯器沒有幫我們做 software pipeline,盡管還有這段 SWP 的注釋代碼,但是沒有做任何有意義的調(diào)度?,F(xiàn)在的 CPI=8/5=1.6,之前的版本 CPI 是 0.4, 所以性能下降了 3 倍多。當(dāng)然這里速度差別這么大,還有一個(gè)原因是 inner loop 的邏輯太簡單,不 unroll 的情況下編譯器實(shí)在沒有啥可調(diào)度的空間,無法發(fā)揮 VLIW 的優(yōu)勢。如果 inner loop 邏輯比較復(fù)雜,即使不 unroll,編譯器通過 VLIW 也能提高指令的并行度,與 SWP 有效調(diào)度后的性能差距就不會如此明顯。

這里解釋一下,可能有讀者看到 SWP 里面只有 4 個(gè) VLIW,不清楚為啥要用 8 個(gè) cycle。請注意看每條指令右側(cè)方括號里面的注釋,cycle 數(shù)確實(shí)是橫跨了 0~7,而中間不連續(xù)的數(shù)字都會替換成相應(yīng)個(gè)數(shù)的 bubble。為什么會產(chǎn)生 bubble? 是因?yàn)楫?dāng)前內(nèi)循環(huán)的四條 VLIW,調(diào)度的是同一個(gè) iter 的不同指令,相鄰的 VLIW 的數(shù)據(jù)又都是寫后讀的依賴關(guān)系,所以連續(xù)發(fā)射出去之后,會存在前一個(gè)指令的結(jié)果還沒有寫回,后一個(gè)指令已經(jīng)讀取該數(shù)并來到 execute 階段了,也就是所謂的 pipeline interlock。為了解決 interlock,就需要在前后兩條 VLIW 之間加一定數(shù)量的 bubble??赡苓€會有細(xì)心的讀者糾結(jié)為啥 vload 和 vmul 之間沒有 bubble,這是因?yàn)楣P者在 Q7 上跑的代碼,而 Q7 對 dram 的 vload 延遲做了特殊優(yōu)化。如果在 P6 上跑這個(gè)代碼,就會看到 vload 和 vmul 之間也會有 bubble。

接著,我們針對 SWP 做第二個(gè)試驗(yàn):地址非對齊訪問。前面說過 src0/src1 都是 64byte 對齊的,所以看匯編代碼會發(fā)現(xiàn),vload 實(shí)際使用的是 ivp_lv2nx8_ip 指令。但是假設(shè)現(xiàn)在無法保證 src0/src1 是 64bytes 對齊的,就需要實(shí)現(xiàn)以下更通用版本的 alphablend:

void alpha_blend(uint8_t* psrc0, uint8_t* psrc1, uint8_t* pdst, int16_t alpha) {
  // 注意,直接粗暴的把 64byte 對齊的地址都加了 1,構(gòu)造非對齊地址
  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 測得內(nèi)循環(huán)的耗時(shí)是 298cycle,略多于對齊地址版本的 276cycle,繼續(xù)查看非對齊地址版本的 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)編譯器搜出來一個(gè) unroll=8,CPI=16/48=0.33(比地址對齊版本的 0.4 更低一點(diǎn))的 SWP,但是因?yàn)?unroll 太大,prologue/epilogue 的 CPI 比較大才導(dǎo)致總的 cycle 數(shù)略大于地址對齊版本。但是如果 loop_count 更大一點(diǎn),兩個(gè)版本的速度差異就更小了。不知道讀者會不會失望了,這個(gè)試驗(yàn)的結(jié)果并沒有告訴我們怎么做速度更快,只是得出一個(gè)結(jié)論:非地址對齊的算子速度不一定比地址對齊的版本要慢,但是非地址對齊版本的算子會更通用一點(diǎn)。

理解 bank_conflict

回過頭來填一個(gè)坑,為什么基于匯編代碼估算 DSP 時(shí)間有時(shí)候會不準(zhǔn)?其實(shí)原因前文也有提過,就是 bank_conflict 和 sub_bank_confilct 搞的鬼。

先說 bank_conflict 的影響,還是拿前面的 alphablend 做例子。該算子有兩個(gè)輸入 src0/src1,如果有兩條 vload 指令被調(diào)度到同一個(gè) VLIW 里面,且訪問的兩個(gè)地址是同一個(gè) dram 上同一個(gè) bank 的不同位置,就觸發(fā)了 bank_confilct,處理器必須 stall 一個(gè) cycle。直覺告訴我們?nèi)绻麑?src0 和 src1 放在不同的 dram 上,應(yīng)該會降低 bank_confilct 發(fā)生的概率。

做個(gè)試驗(yàn)驗(yàn)證下,把前面最初版本的 alphablend 的 src0/src1 都放到 dram0 上,測得內(nèi)循環(huán)的耗時(shí)從原先的 276cycles 變成了 280cycles。速度下降好像并不明顯,查看 SWP 沒有發(fā)生任何變化,后面這點(diǎn)倒是符合預(yù)期,因?yàn)榫幾g期并不會檢查每一次地址訪問有沒有發(fā)生 bank_conflict。仔細(xì)看匯編代碼可以發(fā)現(xiàn),steady 階段的代碼都是同一個(gè) dram 的連續(xù)兩個(gè) 64byte 的 vload 被放在了一個(gè) VLIW 里面,所以本次試驗(yàn)修改對其不產(chǎn)生影響。增加的 4 個(gè) cycle 其實(shí)是因?yàn)?prologue 階段有四個(gè)綁定了不同 dram 上 vload 指令的 VLIW,恰好這四個(gè) VLIW 都觸發(fā)了 bank_confilct??紤]到不同算子的調(diào)度情況是不一樣的,為了減少 bank_conflict 對性能的影響,我們還是應(yīng)該將多個(gè)輸入 tile 創(chuàng)建在不同的 dram 上。

接著,我們來分析一下 sub_bank_conflcit 對性能的影響。sub_bank_conflict 只會發(fā)生在 gathera 指令的執(zhí)行過程中,當(dāng) gathera 收集非連續(xù)地址上的多個(gè)數(shù)據(jù)時(shí),如果出現(xiàn)多個(gè)數(shù)據(jù)的地址正好在同一個(gè) dram 的同一個(gè) bank 的同一個(gè) sub_bank 中的不同地址時(shí),就會出現(xiàn)多次 sub_bank_conflict,最極端的情況下收集一個(gè) vector32,卻出現(xiàn)了 32 次 conflcit,gathera 收集完成需要 32 個(gè) cycle。

所以如果我們開發(fā)的算子里面用到了 gathera 指令,請加上-mem_model 選項(xiàng)在模擬器上跑一下,執(zhí)行完會打印一些統(tǒng)計(jì)參數(shù),其中一項(xiàng)就是 gather stall 的 cycle 數(shù)。如果不幸 gather stall 比較大,就需要檢查算子里面的訪存邏輯,看看是否需要調(diào)整 tile 上數(shù)據(jù)的分布情況,比如可以在 tile 的水平方向插入若干列的無用數(shù)據(jù),降低 gathera 目標(biāo)數(shù)據(jù)在同一個(gè) sub_bank 的可能性。

性能優(yōu)化小結(jié)

綜上,從算子實(shí)現(xiàn)的維度上看,Cadence DSP 算子的速度只受限于 SWP 調(diào)度出來的 CPI,以及訪存的 bank 沖突。

最后,將散落在本文各個(gè)地方會影響算子性能的點(diǎn)集中到一起(還有一些可以降低 CPI 的技巧前文沒有提及),做成 checklist 便于大家對照查看:

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

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

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

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

  5. 如果使用了 gather 指令,請查看 gather stall 的 cycle 數(shù)是否很高?然后對癥下藥。

  6. 確認(rèn)算子內(nèi)循環(huán)中使用的局部變量是否過多?如 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)過一系列處理邏輯,最后的結(jié)果還是 u8 或 u8 的二元組。這種情況下建議轉(zhuǎn)換思路看看查表操作是否 ok(因?yàn)槲覀冊?DSP 上有 SuperGather)。

  8. 如果算子有多個(gè)輸入,確認(rèn)有沒有施加降低 bank_confilct 的措施?

9、如果從較早的 DSP 型號上移植算子到較新的 DSP 型號上,比如移植 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 會讀取不到對應(yīng)地址的數(shù)據(jù),并給該 lanes 直接賦 0。

  • 開發(fā)測試的過程中可能會有修改 memmap.xmm 文件的需求,比如調(diào)整棧空間的位置和大小,只需要編輯完 xmm 文件之后,使用 xtensa 命令行工具 xt-genldscripts,在 xmm 文件所在目錄下執(zhí)行"xt-genldscripts -b ."命令,即可在。/ldscripts 目錄下得到新的 linker scripts 文件,對 xmm 的修改也即生效。

  • 舉兩個(gè)有可能發(fā)生高頻 sub_bank_conflict 的例子:一個(gè)例子是 padding 算子在做 left_padding 和 right_padding 的時(shí)候,會 gather 一個(gè) tile 的某一列連續(xù)若干個(gè)數(shù)據(jù),如果恰好該列所有的數(shù)據(jù)都在同一個(gè) sub_bank 就會性能非常差;還有一個(gè)例子就是 transpose,dst_tile 的一行其實(shí)是 src_tile 的一列,所以 gather 的時(shí)候同樣有可能出現(xiàn)極端的 sub_bank_conflict。

  • 解釋一下編譯器只對 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í)際上是號稱 zero overhead loop 的,也就是說沒有普通 loop 檢查循環(huán)條件,更新 loop iter 等工作的開銷,但是上面的 alphablend 的例子中好像 inner loop 還是有一些 overhead 的,是因?yàn)橄胍@取 zero overhead 的 inner loop,還需要兩個(gè)額外條件:inner loop 里面指令數(shù)不能太少,且 loop count 相對較大。

  • 前文提到將不同的輸入 tile 存放在不同 dram 上來減少 bank_confilct,但是減少 bank_conflict 的終極策略是:先將不同的 tile 存放在不同的 dram 上,然后代碼里使用#pragma ymemory (tile_on_dram0)告訴編譯器哪一個(gè) tile 是在 dram1 上的,編譯器會將該 tile 的內(nèi)存類型標(biāo)記為 ymemory,而其他 tile 的內(nèi)存類型標(biāo)記為 xmemory,最后增加一個(gè)編譯選項(xiàng)-mcbox,告訴編譯器只有訪問不同內(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)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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