原創教程,轉載請聯繫作者並註明出處:github.com/WalkerLau

源碼地址:github.com/WalkerLau/Ac

最近發現很多小夥伴都想用FPGA加速卷積神經網路

運算,而恰好我剛做完的本科畢設就是這個題目,所以就有了寫這個教程的想法,希望能給還沒開始的小夥伴一點思路與幫助,更希望大神們給出一些進一步優化的建議。

最終加速性能

話不多說,先看最終的加速效果。本加速系統僅加速卷積層的運算,下圖展示了僅採用CPU採用CPU+FPGA加速系統來處理VIPLFaceNet人臉識別演算法時,計算7個卷積層所耗費的時鐘數的對比。由圖可見,相比於4核ARM A53處理器,本加速系統最終可以對VIPLFaceNet的大部分卷積層實現45~75倍的運算加速。

項目描述及特點

本加速系統採用中科院計算所的SeetaFace人臉識別項目進行加速功能的驗證,所用的卷積神經網路模型是VIPLFaceNet。本項目的設計工具是Xilinx SDSOC,個人認為這是個人或小團隊進行FPGA嵌入式開發最高效的工具,因此不會涉及HDL的編寫。本加速系統具有以下特點:

  • 容易移植:本項目採用Xilinx SDSOC進行設計,可以直接把C/C++代碼綜合成FPGA電路,只需修改FPGA加速模塊的代碼中卷積層結構相關的參數就可以移植到別的卷積神經網路演算法中。
  • 高性能,採用了如下幾種加速策略,具體原理見最後一節:
  • 獨創的輸入體復用架構
  • 數據的低精度轉換
  • 16通道並行計算單元及加法樹結構
  • 流水線策略

  • 片上存儲BRAM的partition及卷積層間共享
  • 多層卷積的加速實施策略

你需要準備什麼?

硬體:

  • Xilinx Ultrascale+ MPSOC ZCU102 (也可以用ZCU104或其他合適的Xilinx嵌入式開發板)

軟體:

  • Ubuntu 16.04 操作系統(用於安裝和運行SDSOC,由於後面需要編譯板載Linux系統,必須使用Linux主機不能用Windows,以下所有開發都在Linux環境中進行)
  • Xilinx SDSOC 2018.2 開發套件(戳這裡的SDSOC安裝及配置教程,請務必跟教程走)
  • Xilinx reVISION platform(由於SeetaFace的小部分代碼用到了OpenCV,這裡安裝reVISION是為了使用裡面的xfopencv庫,上一項的SDSOC安裝教程詳細講了reVISION的安裝與使用方法)
  • [可選,建議安裝] CodeBlocks(用於對演算法程序進行離板調試)

  • [可選,建議安裝] 安裝OpenCV 2.4.13.6(SeetaFace的小部分代碼用到了OpenCV,用於對演算法程序進行離板調試)

一些基本知識:

  • 雖然在上面的SDSOC安裝及配置教程的最後一節中已經提到過SDSOC的官方使用教程和相應的手冊,還是要強調一下這個教程的重要性,它寫的很好可以快速帶你入門SDSOC,請跟著教程了解SDSOC後再閱讀以下內容。
  • 基本的 C/C++ 知識。

項目安裝流程

  1. 下載本項目文件夾。
  2. 創建SDSOC項目並配置編譯環境,具體步驟請戳SDSOC安裝及配置教程,查看其中第二節的「工程配置」及「設置編譯選項」兩個小節。
  3. src 文件夾中的所有代碼文件添加到SDSOC項目。這裡想說一下的是,src 中的代碼文件是SeetaFace的源碼的FPGA加速版,我把SeetaFace的所有源碼都集中到這一個文件夾里了。項目的頂層main函數在 test_face_recognizer.cpp 文件中,若你想直接查看底層經過FPGA加速優化的卷積層運算代碼,可以直接跳到 conv_net.cpp 文件。
  4. 然後在SDSOC的project explorer窗口找到 convolute1.cpp 並展開,如圖在帶綠點的convolute1上右鍵,選擇「Toggle HW/SW」;同樣地,再找到 math_functions.cpp 並展開,在帶綠點的matrix_procuct上右鍵,選擇「Toggle HW/SW」。Toggle HW/SW 是把函數標記為硬體函數,硬體函數將被放進FPGA進行處理。Toggle完了以後可以在項目設置窗口看到有兩個函數被標記為了硬體函數。

5. 在項目設置窗口(Application Project Setting)勾選Generate SD card image後,就可以準備編譯了。在SDSOC左下角的assistant窗口,選中之前根據教程配置好的編譯環境(圖中顯示的是Seeta,但它和教程中修改的release配置是一樣的),點擊上方的鎚子圖片進行編譯。本項目的編譯一般需要1~3個小時。

6. 編譯結束後,在SDSOC項目文件目錄中導航到與編譯環境同名的文件夾(我這裡是Seeta,若你跟教程走的話就是release),在裡面找到sd_card文件夾,把裡面的所有文件複製到SD卡根目錄,準備下板。

7. 打開本項目的 model 文件夾,解壓裡面的兩個壓縮包,得到一個大約110MB大小的參數文件 seeta_fr_v1.0.bin,然後把這個參數文件移動到 model 目錄下。

8. 把本項目文件中 modeldata 兩個文件夾也拷貝到SD卡的根目錄。

9. 根據SDSOC安裝及配置教程第二節中「配置 uart」小節的指導,完成下板操作並查看加速效果。

離板調試

我們在上面介紹了本項目的安裝流程,其中所有的代碼都將在FPGA開發板上運行。但假如你要修改本項目的代碼或把本項目的FPGA加速方法移植到其他演算法中,你就需要對代碼進行離板調試。離板調試仍然需要在Linux環境中進行。

  1. 下載OpenCV 2.4.13.6,並安裝(OpenCV安裝教程)。
  2. 安裝codeblocks,創建工程項目,然後配置OpenCV環境(codeblocks中OpenCV的配置教程,可直接跳到該教程的第六步。注意,這個教程不是我寫的,它調用的是適配於Visaul Studio的lib,而我們要用的是之前自己安裝的lib,添加路徑和lib文件的時候要注意這點)。
  3. SeetaFace源碼用到了C++11標準,所以還要在codeblocks的build option中勾選對c++11的支持。
  4. 複製 off-board debug 文件夾中的兩個 .cpp 文件,粘貼到 src 文件夾並替換掉其中的兩個同名 .cpp 文件,然後把 src 文件夾的所有代碼文件導入codeblocks的工程中。

  5. 編譯工程並運行。

致謝

衷心感謝西安電子科技大學王樹龍老師和高全學老師對本項目的指導與支持。

一段很長的廢話

FPGA加速計算的代碼主要集中在 conv_net.cppconvolute1.cppmath_functions.cpp 三個文件中,你會發現其中的代碼風格可能顯得有些奇怪,比如為什麼不採用函數或模板來代替重複的代碼?為什麼計算部分的代碼如此冗長?為什麼不用動態內存分配而採用固定大小的數組?為什麼......?

事實上,所有代碼都是經過精心調校與測試的(當然不排除本人水平不足導致有的問題處理得並不完美,希望大神們指出)。上面奇怪的代碼風格只是為了讓編譯器能綜合出可運行的且性能更佳的FPGA電路,至於本系統所設計的一系列加速策略的具體優化原理,請看下面非常長的講解。

1. 低精度處理

在數據傳輸進FPGA之前,我對所有參與卷積層的數據都進行了低精度處理,即把所有數據從32位單精度浮點數強制轉換為16位半精度浮點數。測試表明,與單精度浮點數相比,採用半精度浮點數處理完VIPLFaceNet的7個卷積層和2個全連接層後,所得的特徵值與原來仍保持高達99.9999%的相似度。然而,採用半精度浮點數可以節省許多硬體資源,尤其是片上存儲BRAM資源減少了近一半,計算速度也提升了30%左右。

2. 並行計算單元

並行計算能力是FPGA的顯著優勢,也是本加速系統非常關鍵的加速策略之一。本系統的底層計算是並行的:以VIPLFaceNet的第四層卷積為例,其過濾器有尺寸為 3x3x128,本系統採用了一個16通道並行的底層計算單元,即可以同時完成 3x3x16 一共144個數據的乘加運算,極大提升了計算速度。當然,你也可以選擇8通道並行或32通道並行,這會影響硬體資源使用量和加速性能,採用32通道並行有時會引起時序問題。

該底層並行計算單元的加法部分由加法樹結構組成,負責把144個乘積儘可能快地加起來得到一個部分和。測試表明引入加法樹結構後可以將總加速性能提高19%左右。

3. 流水線

流水線(Pipeline)算是一種比較常用的提高處理效率的方法了,具體的原理如圖(圖是從Xilinx文檔扒下來的)。Xilinx SDSOC 可以很好地支持流水線的綜合,具體可以查看文檔UG1235第四章流水線相關的內容。本系統在底層並行計算單元中應用了流水線。

4. 輸入體復用架構

輸入體復用架構是一種針對卷積層的優化運算策略。高能預警!!! Σ(っ °Д °;)っ 後面的內容可能略微抽象,那就....多看幾次就好了hhhh。

輸入體復用架構的完整計算策略如圖所示(圖中展示第四層卷積的情況)。 首先,計算單元將分別從輸入體(input volume)和卷積核的第一個過濾器中讀取前 16 個通道的 144 個元素並進行卷積運算,得到輸出體(output volume)中第一張特徵圖的第一個元素的部分和(注意是部分和而不是完整的輸出體元素值)。隨後,保留計算單元中來自輸入體的 144 個 操作數不變,把來自卷積核的 144 個操作數更新為卷積核中第二個過濾器的前 16 個通道的 144 個參數,並與輸入體操作數進行卷積運算,得到輸出體中第二張特徵圖的第一個元素的部分和。如此循環,在得到了輸出體第 256 張特徵圖的第一個元素的部分和後,我們將進行首次移窗,同時首次更新計算單元中的 144 個來自輸入體的操作數。隨後,如前面所述那樣計算出輸出體的新的 256 個 元素的部分和。在移窗結束後,我們便得到輸出體所有元素的「第一輪部分和」。 接下來,我們回到原點,讓計算單元分別從輸入體和卷積核中讀取第 17 到第 32 個 通道的 144 個元素,並計算新的一輪部分和。對第四層而言,我們總共需要計算 8 (128 ÷ 16)輪部分和,把輸出體元素每一輪的部分和進行累加,就可以算出最終的輸出體。輸出體復用架構可以提升性能10倍左右。

5. 數據傳輸與片上存儲

在後期的測試發現 PS 與 PL 間的數據傳輸時間佔了 FPGA 總處理時間的很大一部分,所以要盡量減少數據的片外傳輸。藉助 FPGA 的片上存儲資源 BRAM(block RAM)來緩存數據,可以讓計算所需的所有數據僅經過一次片外傳輸。

然而,每個 BRAM 單元僅有2個數據訪問埠,即同時只能從一個 BRAM 單元中讀寫2個數據。這裡需要提醒的是,我們之前提到的16通道並行的底層計算單元需要同時計算144個數據,所以假如把所有數據都放到同一個 BRAM 單元中就會導致數據的訪問瓶頸,阻礙加速性能的發揮。

如何解決這個瓶頸問題?Xilinx 為開發者提供了一個解決方案,可以對存放數據的數組進行partition,讓它們分別存放到不同的 BRAM 單元中,從而增加數據的讀寫埠。關於Array Partition的更多詳情可查看文檔UG1235。

6. 拓展到多個卷積層

在把這些加速策略應用到不同卷積層時需要注意幾個問題。第一個問題是為了減少片上BRAM資源的使用,本系統採取了層間共享BRAM的策略。針對VIPLFaceNet的結構特點,本系統為各層輸入體分配了3個獨立共享空間,分別用來存第一層、第二到第三層、第四到第七層卷積的輸入體數據;為各層卷積核分配了2個共享空間,分別存第一層和其餘六層卷積的卷積核數據。

第二個是傳輸界面阻塞的問題。在數據的片外傳輸上,本系統採用的accelerator interface是streaming interface,該interface要求數據的實際傳輸量必須等於預期傳輸量。由於每層卷積的結構不同,所以每層實際的數據傳輸量也不一樣,但我們所有層都共用一個accelerator interface,這似乎無法滿足streaming interface的要求,造成傳輸界面阻塞,最終下板的時候會使程序卡住不能結束該層的計算。為了解決這個問題,要引入防阻塞機制。首先,把傳輸界面的預期數據傳輸量設置為各個層中的最大值;然後,加入防阻塞機制(代碼中已備註為actor),在FPGA結束了對實際傳輸數據的讀寫之後,繼續從DDR中讀取一些無用數據,直到讀夠預期數據傳輸量為止。顯然,防阻塞機制會使系統性能略微下降(畢竟讀取無用數據會帶來額外的處理時間),但由於畢設後期時間緊迫,暫時沒想到更好的解決方法,如果你有更好的方法記得請@我一下哦~

可以參考的官方手冊

文檔UG1235:介紹了常見的SOSOC優化策略,講的比較概括,可以提供大體的優化思路。

文檔UG902:把C/C++編譯成FPGA IP核的工作是SDSOC通過調用HLS完成的,UG902是HLS的詳細介紹,包括如何編寫高效的C/C++代碼以及哪些代碼不能被綜合成硬體電路等。

文檔UG1253:用C/C++高級綜合轉換到硬體電路常常需要添加一些編譯指令(pragma)來告訴編譯器一些額外的操作,UG1253詳細介紹了這些編譯指令的使用方法。

最後再回到SDSOC這個工具的使用上,可以參考文檔UG1027和文檔UG1146,文檔UG1282介紹了SDSOC的Debug方法。

推薦閱讀:

相关文章