在Android端實現基於OPENGL ES 的深度學習前向傳播框架
這個專案斷斷續續寫了快4個月了,最近忙起來了,可能沒什麼時間完善了,先用blog記錄一下思路,要有機會再完善了。
目前整個專案只實現了卷積,池化,全連線,concat,flat(和tensorflow的flat邏輯不一樣),softmax,卷積部分實現了常規卷積,GEMM卷積(mat4級別的),winograd卷積。整個專案沒能達到夕陽嘆大神實現版本的效率,能力有限也不知道怎麼優化了,希望能有OpenGL的大神能review下我寫的shader,是不是實現上有問題,感激不盡。
大致的benchmark,個人時間有限就只與NCNN對比了一下執行squeezenet
cpu | ncnn(4執行緒) | mine |
高通660 | 60ms | 70ms |
高通710 | - | 35ms |
高通835 | - | 40ms |
測試標準都是除去前10次後,然後前向執行100次耗時取均值。測試手機為小米note3,注意不同rom對opengl效率有影響,最好用最新的開發版或正式版。
基本實現流程:
1.輸入層:
輸入層使用RGBA16F格式的 GL_TEXTURE_2D_ARRAY 儲存,紋理深度為1,紋理寬高同輸入矩陣的寬高維度,通常輸入矩陣的channel為1或者3,所以將輸入矩陣傳入紋理時,需要對其不足的通道補零。
2.卷積層
卷積層的輸出紋理為RGBA16F的 GL_TEXTURE_2D_ARRAY,紋理深度為輸出 ceil(channel/4),紋理高寬同輸出高寬維度。
卷積層的kernel也使用紋理儲存,紋理深度同輸入紋理。最開始我是按照夕陽嘆的思路用的ssbo,但是效率很慢,原因未知。換成紋理後效率有所提升。就我測試,基本運算有一半以上的耗時花在了從紋理讀取資料上了,這部分不知道有沒有優化空間。
kernel的維度為 n x (kernel_area + 1)x d。 n為kernel的數量,kernel_area 為kernel的截面積,d為輸入紋理的深度,因為kernel的channel需要與輸入channel相同。每一個kernel均儲存為紋理上的1行,紋理每行的最後一列的第一個值用於儲存改行kernel的bias。
常規卷積的邏輯為每個計算器計算1個輸出,所以計算器座標同輸出座標。
GEMM卷積為mat4級別的,所以一個計算器會同時計算4個輸出值。
winograd卷積的kernel紋理時儲存GgGt矩陣,儲存邏輯同上。winograd實現時是將輸出轉變為nxn個2x2的輸出,所以一個計算器也會計算4個輸出值
3.池化層
池化層的輸出紋理為RGBA16F的 GL_TEXTURE_2D_ARRAY,紋理深度為輸出 ceil(channel/4),紋理高寬同輸出高寬維度。
池化層的計算器座標與輸出紋理一一對應,每個座標的計算器只進行對應座標的池化運算。
4.concat層
沒有判斷輸入channel是否為4對齊,現在就實現了2個輸入,並且輸入channel均為4對齊的情況。通用的concat有時間再補上吧。
5.全連線層
全連線為了直接接卷積或池化的輸出,輸出紋理的格式設定為 1 x 1 x d, d為全連線層的神經元數量,輸出紋理的深度就為ceil(d/4)。同樣每個神經元的kernel也用紋理的1行儲存,最後一列用於儲存bias。
6.flat層
這層主要是為了方便讀取最終結果,opengl通過framebuffer讀取資料時,只能讀取一個深度紋理上的資料。所以通過flat將紋理所有深度上的資料,移動到一個深度的紋理上。
7.softmax層
沒什麼東西了,直接實現的softmax公式。
目前在ARM Mail GPU上還有bug,cifa10的10分類模型能跑,squeezeNet就輸出0,原因還沒找到,手邊也沒有這種GPU的手機,應該也沒機會修復了。
順便記錄一下ARM opengl程式設計遇到的一點小坑:
1.arm 下 opengles 的計算器localsize 最大值為128,而高通的為1024
2.arm 下shader定義image時必須設定精度(也許也可以直接在全域性設定,我沒試),而高通不需要
layout(binding = 0, rgba16f) readonly uniform highp image2DArray input_image;
紋理精度設定為highp或lowp,效率沒變化。
3.shader的shared變數空間比高通的小(具體多少沒查到,也沒手機試),高通的手機為1000 個 float。
裡面包括一個cifar10 10分類和squeezeNet 1000分類的模型。
cifar10的權重提取自CnnDroid
squeezenet的權重提取自squeezenet