這是新的系列教程,在本教程中,我們將介紹使用 FPGA 實(shí)現(xiàn)深度學(xué)習(xí)的技術(shù),深度學(xué)習(xí)是近年來(lái)人工智能領(lǐng)域的熱門(mén)話題。
在本教程中,旨在加深對(duì)深度學(xué)習(xí)和 FPGA 的理解。
用 C/C++ 編寫(xiě)深度學(xué)習(xí)推理代碼
高級(jí)綜合 (HLS) 將 C/C++ 代碼轉(zhuǎn)換為硬件描述語(yǔ)言
FPGA 運(yùn)行驗(yàn)證
在上一篇文章中,我們?cè)?MNIST 數(shù)據(jù)集上創(chuàng)建并訓(xùn)練了一個(gè)網(wǎng)絡(luò)模型。從本文開(kāi)始,為了在 FPGA 上運(yùn)行推理處理,我們將首先用 C++ 編寫(xiě)推理處理代碼。
在這篇 C++ 實(shí)現(xiàn)的第一篇文章中,我們開(kāi)始針對(duì)卷積層的 C++ 實(shí)現(xiàn)。具體內(nèi)容是(1)卷積層的實(shí)現(xiàn),(2)運(yùn)算校驗(yàn)(C驗(yàn)證,C/RTL協(xié)同驗(yàn)證)(就是HLS的流程)。
卷積層實(shí)現(xiàn)
在上一篇文章中,我解釋了卷積層是對(duì)圖像的過(guò)濾過(guò)程,但是并沒(méi)有解釋輸入輸出通道如何處理,過(guò)濾時(shí)圖像的邊緣處理等。由于本文旨在實(shí)現(xiàn)層面的理解,因此我將詳細(xì)介紹這些要點(diǎn)。
處理 I/O 通道
在圖像處理中,對(duì)RGB輸入圖像進(jìn)行噪聲去除等濾波處理,并頻繁地進(jìn)行RGB圖像的處理。在這種情況下,卷積過(guò)程往往是針對(duì)每個(gè)通道(R/G/B)獨(dú)立完成的,輸入的G/B通道值不影響輸出的R通道結(jié)果。
每通道獨(dú)立卷積
另一方面,在卷積層中執(zhí)行的卷積過(guò)程中,所有輸入通道的值影響每個(gè)輸出通道。因此,對(duì)于輸出圖像的每個(gè)像素(輸出通道,Y坐標(biāo),X坐標(biāo)),所有輸入通道和周?chē)南袼貐^(qū)域都會(huì)參與計(jì)算,導(dǎo)致計(jì)算量非常大。
使用所有通道的卷積
另外,如上所述每個(gè)通道獨(dú)立卷積的卷積層稱為Depthwise Convolution。
這通常用于減少計(jì)算量的網(wǎng)絡(luò)模型,例如MobileNet(https://arxiv.org/abs/1704.04861)。
圖像邊緣處理
在對(duì)圖像進(jìn)行卷積處理時(shí),圖像邊緣的處理往往是一個(gè)問(wèn)題。
由于卷積過(guò)程在計(jì)算某個(gè)像素時(shí)使用了周?chē)袼兀虼藢?duì)于沒(méi)有周?chē)袼氐南袼兀鐖D像邊緣的像素,就無(wú)法獲取周?chē)袼亍?/p>
卷積神經(jīng)網(wǎng)絡(luò)主要通過(guò)以下兩種方式處理邊緣像素。
無(wú)填充:輸出圖像減少了輸入圖像的卷積區(qū)域。
補(bǔ)零:將輸入圖像預(yù)先用卷積區(qū)域擴(kuò)展,用零填充該區(qū)域,對(duì)原始輸入圖像進(jìn)行卷積處理。
沒(méi)有填充的卷積的圖形表示如下所示:在這種情況下,輸出圖像將是比輸入圖像小一個(gè)濾波器尺寸的區(qū)域(橙色部分)。如果內(nèi)核大小為 3(中心像素 +/-1),則輸出圖像大小在寬度和高度上都將為 -2,因?yàn)閳D像之外的 1 個(gè)像素是無(wú)法進(jìn)行卷積的區(qū)域。
無(wú)填充卷積:輸出圖像縮小
接下來(lái),零填充的圖形表示如下所示。在這個(gè)例子中,預(yù)先在輸入圖像的外部添加了一個(gè)值為0的區(qū)域(灰色區(qū)域),進(jìn)行卷積,這樣就不會(huì)出現(xiàn)圖像縮小現(xiàn)象。如果內(nèi)核大小為 3,則帶填充的輸入圖像大小在寬度和高度上均為 +2,因?yàn)?+1 像素將添加到屏幕外部且值為零。
零填充卷積:輸出圖像大小保持不變
在我們的模型中,我們?cè)谒芯矸e層中使用零填充。
C代碼
如果根據(jù)目前為止的解釋用 C 語(yǔ)言實(shí)現(xiàn)卷積過(guò)程,它將類(lèi)似于下面的代碼。
?void?conv2d(const?float*?x,?const?float*?weight,?const?float*?bias,?int32_t?width,?int32_t?height, ?????????????int32_t?in_channels,?int32_t?out_channels,?int32_t?ksize,?float*?y)?{ ???for?(int32_t?och?=?0;?och?=?height?||?pw?0?||?pw?>=?width)?{ ?????????????????continue; ???????????????} ???????????????int64_t?pix_idx?=?(ich?*?height?+?ph)?*?width?+?pw; ???????????????int64_t?weight_idx?=?((och?*?in_channels?+?ich)?*?ksize?+?kh)?*?ksize?+?kw; ???????????????sum?+=?x[pix_idx]?*?weight[weight_idx]; ?????????????} ???????????} ?????????} ?????????//?add?bias ?????????sum?+=?bias[och]; ?????????y[(och?*?height?+?h)?*?width?+?w]?=?sum; ???????} ?????} ???} ?}
此函數(shù)的解釋如下所示:
輸入
-- x: 輸入圖像。shape=(in_channels, height, width)
-- weight: 權(quán)重因子。shape=(out_channels, in_channels, ksize, ksize)
-- bias: 偏置值。shape=(out_channels)
輸出
-- y: 輸出圖像。shape=(out_channels, height, width)
參數(shù):-- width: 輸入/輸出圖像的寬度
-- height: 輸入/輸出圖像高度
-- in_channels:輸入圖像的通道數(shù)
-- out_channels:輸出圖像的通道數(shù)
-- ksize: 內(nèi)核大小
每個(gè)輸入/輸出的內(nèi)存布局shape=(...)如表格所示,但float x[in_channels][height][width];將其視為定義為三維數(shù)組。
卷積層的處理是一個(gè)6級(jí)循環(huán)。第一個(gè)三級(jí)循環(huán)確定輸出圖像上的位置,隨后的三級(jí)循環(huán)對(duì)該位置執(zhí)行卷積操作。
零填充在第 24-26 行完成。由于實(shí)際創(chuàng)建零填充輸入圖像是低效的,所以零填充是通過(guò)在訪問(wèn)圖像外部時(shí)不參與乘積之和來(lái)實(shí)現(xiàn)的。
第31行是卷積過(guò)程中的積和運(yùn)算部分,這個(gè)積和運(yùn)算out_channels * height * width * in_channels * ksize * ksize進(jìn)行了兩次。這個(gè)卷積過(guò)程的操作數(shù)量非常大,在很多情況下,卷積層支配著卷積神經(jīng)網(wǎng)絡(luò)的執(zhí)行時(shí)間。這就是為什么計(jì)算單元比 CPU 多的 GPU 和 FPGA 更適合處理神經(jīng)網(wǎng)絡(luò)。
第37行是偏差處理部分。到目前為止,我還沒(méi)有觸及什么是偏差處理,但正如我在這里所寫(xiě)的那樣,它是一個(gè)簡(jiǎn)單地對(duì)輸出值進(jìn)行偏移的過(guò)程。這種偏差處理在輸入通道/內(nèi)核大小 (Y,X) 循環(huán)之外,因此處理步驟的數(shù)量非常微不足道。
運(yùn)算檢查
作為對(duì)上一節(jié)創(chuàng)建的函數(shù)運(yùn)行的確認(rèn),conv2d我們將比較結(jié)果是否足夠接近在 PyTorch 的 C++ API (libtorch) 上執(zhí)行的卷積計(jì)算。
每個(gè)測(cè)試包括以下兩個(gè)步驟。
C. 驗(yàn)證
C/RTL 協(xié)同驗(yàn)證
1、C 驗(yàn)證類(lèi)似于正常的軟件開(kāi)發(fā),gcc只是用通用的編譯器編譯源代碼并檢查結(jié)果。
2、C/RTL協(xié)同驗(yàn)證是使用AMD-Xilinx提供的高階綜合工具Vitis HLS進(jìn)行驗(yàn)證。對(duì)于此驗(yàn)證,HLS 首先將 C 源代碼轉(zhuǎn)換為 Verilog HDL 等 RTL。然后在 Vivado 中對(duì)生成的 RTL 執(zhí)行功能仿真。
在這個(gè)邏輯仿真中,將一個(gè)類(lèi)似于C驗(yàn)證的數(shù)據(jù)序列輸入到創(chuàng)建的電路中,確認(rèn)輸出結(jié)果是否正確。
本節(jié)以后的內(nèi)容將以運(yùn)行創(chuàng)建的源代碼的形式進(jìn)行說(shuō)明。
源代碼將在后面發(fā)布。
運(yùn)行環(huán)境
運(yùn)行環(huán)境面向 Linux 機(jī)器。不支持 Windows/Mac 操作系統(tǒng)。此外,由于預(yù)裝gcc版本,該發(fā)行版針對(duì) Ubuntu 18.04。難以自行準(zhǔn)備運(yùn)行環(huán)境的朋友,看看就行。
需要以下工具。
Vivado ?2020.2(推薦 2019.2)
cmake >= 3.11
cmake比較麻煩,因?yàn)樗枰陌姹颈萢pt等包管理器可以安裝的版本高,但是可以下載預(yù)構(gòu)建的二進(jìn)制文件(cmake--Linux-x86_64.tar.gz)。
C. 驗(yàn)證
測(cè)試代碼/tests/ref/conv2d.cc的使用,我不會(huì)在本文中詳細(xì)介紹,但測(cè)試將是一個(gè)正常的隨機(jī)測(cè)試。
可以按照以下步驟構(gòu)建代碼。請(qǐng)將 -DVIVADO_HLS_ROOT 的值相應(yīng)地替換為安裝的 Vivado 的路徑。
$?mkdir?/build $?cd? /build $?cmake?..?-DVIVADO_HLS_ROOT=/tools/Xilinx/Vivado/2022.2 $?cmake?--build?.
使用以下命令進(jìn)行測(cè)試:如果沒(méi)有任何錯(cuò)誤,那它就是成功的。
$?ctest?-V?-R?"conv2d_ref"
C/RTL 協(xié)同驗(yàn)證
運(yùn)行以下命令以使用 HLS 啟動(dòng) C/RTL 協(xié)同驗(yàn)證。大約需要 5 分鐘。
$?ctest?-V?-R?"conv2d_hls_cosim"
當(dāng)執(zhí)行 C/RTL 協(xié)同驗(yàn)證時(shí),會(huì)自動(dòng)創(chuàng)建一個(gè) HLS 項(xiàng)目文件,因此可以使用它來(lái)檢查高級(jí)綜合和 RTL 仿真波形的結(jié)果。
要檢查這一點(diǎn),請(qǐng)使用以下命令啟動(dòng) HLS:
$?vitis_hls?&
HLS 打開(kāi)后,單擊“打開(kāi)項(xiàng)目”,如下所示,導(dǎo)航到/build/tests/hls/conv2d/conv2d_hls_cosim目錄并單擊“確定”。
然后,HLS 綜合報(bào)告將顯示如下屏幕所示。
從此報(bào)告中,可以看到從 Performance Estimates 列創(chuàng)建的電路的估計(jì)性能,以及從 Utilization Estimates 看到在目標(biāo)設(shè)備上實(shí)施時(shí)的估計(jì)資源使用情況。
點(diǎn)擊頂部紅框包圍的區(qū)域,可以看到仿真的波形。
波形如下所示,可以看出可以通過(guò)某種方式讀取到該值,大概2000.00ns就能輸出y的第一個(gè)值。
通過(guò)這種方式,我們能夠創(chuàng)建一個(gè)邏輯電路,該邏輯電路使用 ?HLS 執(zhí)行卷積層計(jì)算,而無(wú)需特別注意 HW。
然而,由于這個(gè)電路根本沒(méi)有調(diào)整,它的設(shè)計(jì)只是實(shí)現(xiàn)功能,在后續(xù)會(huì)對(duì)此進(jìn)行優(yōu)化。
總結(jié)
在這篇文章中,用 C++ 實(shí)現(xiàn)了一個(gè)卷積層并確認(rèn)了它的運(yùn)行。我們還在這個(gè) C++ 實(shí)現(xiàn)上使用 HLS 進(jìn)行了高級(jí)綜合,并確認(rèn)它在 C/RTL 協(xié)同驗(yàn)證中沒(méi)有任何問(wèn)題。
原編輯::黃飛
評(píng)論