以前的神經網絡幾乎都是部署在雲端(服務器上),設備端采集到數據通過網絡發送給服務器做inference(推理),結果再通過網絡返回給設備端。如今越來越多的神經網絡部署在嵌入式設備端上,即inference在設備端上做。嵌入式設備的特點是算力不強、memory小。可以通過對神經網絡做量化來降load和省memory,但有時可能memory還吃緊,就需要對神經網絡在memory使用上做進一步優化。本文就以一維卷積神經網絡為例談談怎么來進一步優化卷積神經網絡使用的memory。
文章(卷積神經網絡中一維卷積的計算過程 )講了卷積神經網絡一維卷積的處理過程,可以看出卷積層的輸入是一個大矩陣,輸出也是一個大矩陣,保存這些矩陣是挺耗memory的。其實做卷積計算時,每次都是從輸入中取出kernel size大小的數據與kernel做卷積運算,得到的是一個值,保存在卷積層的輸出buffer里,輸出也是下一層的輸入。如果輸入是卷積層kernel的大小(即是能做運算的最小size),輸出是下一層的能做運算的最小size,相對於整個輸入和輸出的size而言,能節省不少memory。下面就講講怎么用這種思路來做CNN的memory優化。
假設當前層和下一層均為卷積層,stride為1,padding模式為same。當前層的輸入是一個MxN的矩陣,kernel size是P,kernel count是Q,所以kernel是一個MxP的矩陣,當前層的輸出是一個QxN矩陣。當前卷積層的輸出就是下一卷積層的輸入。下一卷積層的kernel size是J,kernel count是K,所以下一卷積層的kernel是一個QxJ的矩陣,輸出是一個KxN的矩陣。示意如下圖:

首先從輸入矩陣中取出0~(P-1)列(共P列)放進輸入buffer中,正好放滿。與第一個kernel做卷積運算就得到一個值放進輸出buffer中(0,0)位置。同樣與第二個kernel做卷積得到的值放在輸出buffer中(1,0)位置,與所有kernel做完卷積后得到是一個Q行1列的矩陣,放在輸出buffer第一列上,如下圖:

再將輸入矩陣的第1~P列取出放進輸入buffer中(即把輸入buffer中每列向前移一格,0列移出buffer,第P列放在最后1列),同上面一樣計算,得到的依舊是1列數值放進輸出buffer的第2列中。如下圖:

依次這么做下去,當輸出buffer里的最后一列被填滿時,就要觸發下一卷積層做卷積運算,與K個kernel卷積運算后得到的是一個K行1列的值放進下一卷積層輸出buffer的第一列中。如下圖:

由於下一卷積層的輸出buffer未滿,不能觸發后面的運算。又回到從輸入矩陣中取數據放進當前卷積層的輸入buffer中,再進行當前卷積層和下一卷積層的運算,結果放在各自的輸出buffer里(如輸出buffer滿了就要把每列左移一格,0列移出buffer,新生成的1列數據放在最后1列)。類似的處理,直到把輸入矩陣中取到最后一列,再把各個層的輸入buffer全處理完,最后得到結果。
從上面的思路看出,輸入和輸出buffer從大矩陣變成kernel大小的小矩陣可以省不少memory。具體軟件實現時,有輸入層,中間各層(包括卷積層等),每一層為一個整體,要用event機制去觸發下一個要處理的層。當一層處理完后要判斷下一步是哪一層做處理(有可能是下一層,也有可能是當前層或上一層,還有可能是輸入層等),就給那一層發event,那一層收到event后就會繼續處理。示意如下圖:

軟件實現時還有很多細節要處理,尤其是當輸入層數據取完后后面各層的處理,這里就不一一細述了。軟件調試時先用不省memory的原始code生成每層的輸出,保存在各自的文件里,用於做比特校驗。然后對省memory的code進行調試,先從第一層開始,一層一層的調試。優化后的代碼每層的輸出跟優化前的輸出是完全一樣的才算調試完成。
