在本文中,我們將循環(huán)并行化應用于先前任務并行化的推理內核,并平衡層與層之間的執(zhí)行時間。
此外,當前內核的外部內存訪問效率低下,因此內存訪問也是瓶頸。在這種狀態(tài)下,即使進行循環(huán)并行化,內存訪問最終也會成為瓶頸。
當前內核瓶頸
下面轉載上一篇文章中附上的內核執(zhí)行時間報告。
運行時報告:
實際時間:
這里,每幅圖中的①、②、③分別對應第一個卷積層(conv1)、第二個卷積層(conv2)和第一個全連接層(fc1)。執(zhí)行時間報告顯示conv2、conv1、fc1的執(zhí)行時間比例為51。另一方面,在真機實踐上,conv2:conv1的比例約為5:3,但fc1層的執(zhí)行時間與它們相比非常短。整個推理過程的執(zhí)行時間也是 12.65 ms/image,明顯長于報告的吞吐量(504098 cycles / 300MHz = 1.68 ms/image)。
HLS 報告 <-> 實際機器的性能存在這種差異的原因是,HLS 報告是在假設可以在請求外部存儲器的時間立即提供數據的情況下創(chuàng)建的。由于在真機上訪問外部內存不是那么快,所以在真機上性能明顯更差。
內存訪問優(yōu)化
我們發(fā)現內存訪問效率低下,將對其進行優(yōu)化。當前內核為卷積層中的每個乘法累加運算從外部 DRAM 獲取系數數據。使用此配置,對 DRAM 的訪問以非常細的粒度進行操作,因此 DRAM 上的負載變得非常高。
Xilinx的FPGA內部的內存層次結構如下圖所示,FPGA中存在分布式RAM(Distributed RAM)、BRAM(Block RAM)、URAM(Ultra RAM)三種。
這些 FPGA 內部的存儲器可以比 DRAM 運行得更快,并且每個周期都可以穩(wěn)定地讀寫數據。因此,這次我們將圖像、權值大小等數據提前全部復制到FPGA中,并進行修改,讓每一層都從FPGA的內存中讀取數據。在這種情況下,圖像和權重大小讀取時間足夠小,因此我將堅持使用 HLS 默認值(BRAM 或分布式 RAM)。
要創(chuàng)建的電路框圖如下所示。
添加一個新電路x,x_local臨時存儲來自本地內存的 DRAM 輸入的數據。至于loadweight輸出,它也是將層的數據緩沖到本地內存中,并從端口biasfc2y_localy輸出結果。
在此圖中,為簡單起見,假定本地緩沖區(qū)是單個緩沖區(qū)。對于上次解釋的任務并行化,這個緩沖區(qū)應該是乒乓緩沖區(qū)。
要在 HLS 中實現此電路,代碼中定義load一個store函數和一個本地storememcpy緩沖區(qū)。其實我們不需要自己定義這個函數,如果我們使用C標準庫,load它會自動生成一個高效的電路,所以我們就用它。
代碼如下所示:
111voidinference_with_local_buffer(constfloatx[kMaxSize], 112constfloatweight0[kMaxSize],constfloatbias0[kMaxSize], 113constfloatweight1[kMaxSize],constfloatbias1[kMaxSize], 114constfloatweight2[kMaxSize],constfloatbias2[kMaxSize], 115constfloatweight3[kMaxSize],constfloatbias3[kMaxSize], 116floaty[kMaxSize]){ 117#pragmaHLSdataflow 118#pragmaHLSinterfacem_axiport=xoffset=slavebundle=gmem0 ... 151 152conststd::size_tx_size=1*28*28; 153conststd::size_tw0_size=4*1*3*3,b0_size=4; ... 157conststd::size_ty_size=10; 158 159floatx_local[x_size]; 160floatw0_local[w0_size],b0_local[b0_size]; ... 164floaty_local[y_size]; 165 166//fetchtolocalbuffer 167std::memcpy(x_local,x,x_size*sizeof(float)); 168std::memcpy(w0_local,weight0,w0_size*sizeof(float)); ... 176 177//runinferencewithlocalbuffer 178dnnk::inference(x_local, 179w0_local,b0_local, 180w1_local,b1_local, 181w2_local,b2_local, 182w3_local,b3_local, 183y_local); 184 185//storetoglobalbuffer 186std::memcpy(y,y_local,y_size*sizeof(float)); 187}
第167行,將DRAM上的內存復制到xFPGAx_local內部的內存中。之后,我們用來運行x_local推理dnnk::inferencey_localmemcpy函數,最終輸出到DRAM。
以下是綜合此電路并在真機上執(zhí)行的日志。可以看出,原本需要 12.65 [ms/image] 的執(zhí)行時間已經減少到 1.61 [ms/image]。
$./host/run_inference./host/inference_with_local_buffer_hw.xclbininference_with_local_buffer1 Elapsedtime:1.61029[ms/image] accuracy:0.973
卷積層在這個內核中的表現也稍好一些,因為原始內核在卷積層內部沒有 DRAM 訪問,將處理周期總數減少到 504898 -> 481378 個周期。481378個周期在300MHz轉換時為1.604 ms,與上述真機執(zhí)行時間(1.61 ms)相差無幾。因此,從inference_with_local_buffer可以看出,對于使用本地緩沖區(qū)進行緩存的函數,內存訪問時間不會對整體性能產生不利影響。
通過循環(huán)并行化加速卷積層
到此為止,HLS報告的執(zhí)行時間和真機差不多,所以本文的主題循環(huán)并行化將從以下開始。
在卷積函數的最內層循環(huán)中,大致進行了以下三個過程。
像素,負載權重
像素,權重的乘積
將乘法結果添加到求和寄存器
這三個過程都是在下面的卷積函數的第31行完成的。
17for(int32_tich=0;ich
粗略地說,上述內核的處理流程如下圖所示。
這里,假設處理load需要1個周期,fmul處理需要3個周期,fadd處理需要4個周期。第一行是迭代(循環(huán)的迭代次數)i,下一行是下一次迭代i+1,最后i+2是處理后的波形。在不提取循環(huán)并行度的情況下,每次迭代的處理完全不重疊,每次迭代需要8個周期的處理時間。
比如load2-9、10-17等周期,電路都沒有運行,一直運行可以進一步提高性能。load電路一直運行時的波形如下圖所示。
到目前為止,下一次迭代每 8 個周期開始一次,但在本例中,下一次迭代每 1 個周期開始一次。以這種方式提取不同迭代之間的并行性稱為循環(huán)并行化。可以進行一次迭代的時間間隔稱為II(Iteration Interval),本例中寫為II=1。
在循環(huán)并行中,并行的抽象方式與之前的任務并行幾乎相同。然而,任務并行性提取幀之間的并行性,而循環(huán)并行性提取每一層內處理迭代之間的并行性。此外,為了提取任務并行性,需要同時處理多個幀,因此存在需要將多個幀的輸入數據預先擴展到FPGA上的DRAM等限制。另一方面,由于循環(huán)并行僅在幀內完成,因此可以沒有特別限制地提取并行。
循環(huán)并行化的方法很簡單,#pragma HLS pipeline II=1只需要在循環(huán)中添加符號,如下所示:這樣做kw可以優(yōu)化變量的循環(huán),以便它們可以一次處理一個循環(huán)。
17for(int32_tich=0;ich
僅通過添加#pragma HLS pipeline II=1就可以實現II=1,但即使對上述修改后的內核進行綜合,也會輸出以下記錄:目標(Target)為II=1,但實際電路(Final)為II=4。
INFO:[v++204-61]Pipeliningloop'Loop1.1'. INFO:[v++204-61]Pipeliningresult:TargetII=1,FinalII=4,Depth=12.
這是因為在第31行的處理中,下一次迭代的計算依賴于上一次迭代sum += ...的相加結果。另一方面,x[pix_idx]的加載處理和x[pix_idx]*weight[weight_idx]的乘法處理不依賴于前迭代的結果,因此可以先處理。
#pragma HLS pipeline應用后的波形大致如下。
load, fmul可以先運行,fadd但要等到上一次迭代完成后才能運行,所以整體faddII受到 4 個周期延遲的速率限制。
通過復制和寄存器提高性能
前面說了這個卷積不可能每個循環(huán)都做,因為i+1迭代的結果取決于迭代次數。i在這里,sum通過將 sum 寄存器復制成四個,我們改變了依賴關系,使得i迭代依賴于迭代的結果i-4。由于用文字難以理解,目標波形如下所示。
fadd(橙色、藍色、水色和綠色)的顏色fadd代表輸出目標寄存器,輸出目標寄存器在每個循環(huán)中切換。這樣,從第5周期開始到第8周期結束的一次迭代i的計算結果將被fadd第9周期的迭代首次使用。
要創(chuàng)建的電路應該是上面描述的那種,但是從 Vivado HLS/Vitis 創(chuàng)建它需要稍微特殊的編寫方式。以下描述基于名為 shift_register_c 的 SDAccel 教程的內容。
下面是使用移位寄存器的卷積函數的代碼。
82staticvoidconv2d_pipelined_v2(constfloat*x,constfloat*weight,constfloat*bias,int32_twidth,int32_theight, 83int32_tin_channels,int32_tout_channels,int32_tksize,float*y){ 84staticconstintkShiftRegLength=4; 85 86for(int32_toch=0;och
主要有以下三個區(qū)別:
1、移位寄存器定義(L89-L90)
2、本地求和:重復求和寄存器(L111-L122)的求和
3、全局求和:重復求和寄存器(L130-L134)之間的求和處理
1的移位寄存器定義將4+1求和寄存器定義為FPGA上的寄存器。+1只是一個臨時寄存器,按照C語言語法只用來臨時存放加法的結果,在高級綜合時刪除。第90 行添加了一個新的 pragma(#pragma HLS array_partition)()以將移位寄存器定義為寄存器(完整),默認情況下將其推斷為 BRAM。pragma 本身可以做很多其他事情,但我將在下一個數據并行化中觸及細節(jié)。
2 的本地求和在四個求和寄存器上累加乘法結果 (mul)。這里,glob_idx是ich、kh、kw 3個循環(huán)的索引。通常情況下,shift_reg[glob_idx % 4] += mul可以復制我們這次正在做的輸出寄存器,但是這樣,高級綜合結果II=4就不會改變。因此,這里使用官方示例中也使用的移位處理(shift_reg[i] = shift_reg[i + 1])II=1來實現這一點。每次對這兩個寄存器進行shift_reg[0]加法mul運算shift_reg[0]時,它所包含的求和寄存器的數字(0 到 3)每個周期都會發(fā)生變化。
3 的全局求和對四個求和寄存器執(zhí)行求和運算。#pragma HLS pipeline我們也在這里指定,但fadd由于延遲,這里我們沒有 II=1。
此修改允許kw循環(huán)的 II 為 1,從而實現最有效的循環(huán)并行化。另一方面,此修復程序并沒有提供 4 倍的加速,因為它添加了另一個全局求和循環(huán)。
評估
檢查綜合結果
比較以下三種配置的性能。
內存訪問優(yōu)化后(無循環(huán)并行)
#pragma HLS pipeline II=1
使用移位寄存器加速后
結果總結在下表中。
方法 | 卷積層 II | 第二層卷積迭代(二) | 整個推理過程的迭代區(qū)間(二) |
---|---|---|---|
無循環(huán)并行 | 8 | 481377 | 481378 |
pipeline | 4 | 257153 | 257154 |
移位寄存器后 | 1 | 127009 | 172482 |
著眼于第2個卷積層,通過pipeline改變loop parallelism -> onlypipeline獲得了約1.87倍的性能提升,通過應用shift register -> 獲得了約2.02倍的性能提升。這里,本來是II = 4 -> II = 1,所以我們希望性能提升4倍左右,但實際上,上面描述的全局求和過程占用了很多時間,所以速度未獲得提升。
下面是應用移位寄存器后配置的 HLS 報告。
在前面的推理過程中,瓶頸是第二個卷積層(conv2d_pipelined_v2),但這里的瓶頸是第一個卷積層(conv2d_pipelined_v2_1)。這是因為第一個卷積層是 1ch,3×3 卷積,所以一開始每個像素只執(zhí)行九次操作。在這種情況下,由于能夠執(zhí)行 II=1 的局部求和而帶來的性能增益被添加全局求和循環(huán)所帶來的性能損失所抵消,反之性能下降。另一方面,隨著輸入通道數和內核大小的增加,局部求和處理的性能提升變得更加明顯,因此這種優(yōu)化在大規(guī)模網絡中變得更加有效。
總結
到目前為止通過調整的加速率如下。
方法 | 執(zhí)行時間(毫秒/圖像) | 比以前的實施提速 | 相對于基線的改進百分比 |
---|---|---|---|
基線 | 20.81 | 1.00 | 1.00 |
任務并行化 | 12.65 | 1.65 | 1.65 |
通過本地緩沖區(qū)減少外部存儲器訪問 | 1.61 | 7.86 | 12.93 |
循環(huán)并行化(僅限卷積層) | 0.61 | 2.64 | 34.11 |
盡管最初的實現根本不關心速度,但一些編譯指示添加和代碼修復產生了比基線快 34 倍的速度。
我在本文開頭所做的內存訪問調優(yōu)目前特別有效。FPGA 的優(yōu)勢之一是其豐富的內部 RAM 帶寬,因此隱藏對外部存儲器的訪問通常會產生顯著的性能提升,如本例所示。
審核編輯:劉清
-
FPGA
+關注
關注
1629文章
21748瀏覽量
603954 -
DRAM
+關注
關注
40文章
2316瀏覽量
183579 -
存儲器
+關注
關注
38文章
7496瀏覽量
163932 -
HLS
+關注
關注
1文章
129瀏覽量
24136
原文標題:從FPGA說起的深度學習(七)-循環(huán)并行化
文章出處:【微信號:Open_FPGA,微信公眾號:OpenFPGA】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論