為什么加班的總是我:那個天天12點到崗的程序員,又準點下班陪女友了?!
作為我司頭發儲量前三的程序員始終仗著頭發多奮斗在加班的第一線時時靈魂拷問自己年輕人,你憑什么不加班?雖然我沒有女朋友
但是,我有代碼呀
但我不明白的是,隔壁工位那個,到崗比我遲,下班比我早,天天準點兒下班接女朋友,工作還完成的不錯的樣子,當然,頭發也還不錯。除了長得比我顯老,難道他有什么制勝法寶嗎?趁著午休,以一禮拜咖啡為代價,我偷師了他的制勝法寶。
GET了秘訣,或許我也可以事業愛情雙豐收了。
直接集成NCNN的缺點
直接集成NCNN熬老少男顏哇,想當年我一邊淚流滿面地集成,一邊想用女友的SK2給自己的臉補補(不,你沒有,both SK2和女友),咋回事兒呢,為SqueezeNet接入NCNN,把相關的模型文件,NCNN的頭文件和庫,JNI調用,前處理和后處理相關業務邏輯等。把這些內容都放在SqueezeNet Sample工程里。這樣簡單直接的集成方法,問題也很明顯,和業務耦合比較多,不具有通用性,前處理后處理都和SqueezeNcnn這個Sample有關,不能很方便地提供給其他業務組件使用。深入思考一下,如果我們把AI業務,作為一個一個單獨的AI組件提供給業務的同學使用,會發生這樣的情況:
每個組件都要依賴和包含NCNN的庫,而且每個組件的開發同學,都要去熟悉NCNN的接口,寫C的調用代碼,寫JNI。所以我們很自然地會想到要提取一個NCNN的組件出來,提取以后呢長得順眼了很多,大概是這個樣子。
AOE SDK里的NCNN組件
有了AOE SDK,我也可以一頓操作猛如虎了!在AOE開源SDK里,我們提供了NCNN組件,下面我們從4個方面來講一講NCNN組件:
- NCNN組件的設計
- 對SqueezeNet Sample的改造
- 應用如何接入NCNN組件
- 對NCNN組件的一些思考
NCNN組件的設計
不懂NCNN的組件設計,即使一頓操作猛如虎,你可能最后也只有兩塊五。那它的組件是什么嘞?NCNN組件的設計理念是組件里不包含具體的業務邏輯,只包含對NCNN接口的封裝和調用。具體的業務邏輯,由業務方在外部實現。在接口定義和設計上,我們參考了TF Lite的源碼和接口設計。目前提供的對外調用接口,長這個樣子:
- // 加載模型和param
- void loadModelAndParam(...)
- // 初始化是否成功
- boolean isLoadModelSuccess()
- // 輸入rgba數據
- void inputRgba(...)
- // 進行推理
- void run(...)
- // 多輸入多輸出推理
- void runForMultipleInputsOutputs(...)
- // 得到推理結果
- Tensor getOutputTensor(...)
- // 關閉和清理內存
- void close()
而機智騷年本人,用的是這個:
- ├── AndroidManifest.xml
- ├── cpp
- │ └── ncnn
- │ ├── c_api_internal.h
- │ ├── include
- │ ├── interpreter.cpp
- │ ├── Interpreter.h
- │ ├── jni_util.cpp
- │ ├── jni_utils.h
- │ ├── nativeinterpreterwrapper_jni.cpp
- │ ├── nativeinterpreterwrapper_jni.h
- │ ├── tensor_jni.cpp
- │ └── tensor_jni.h
- ├── java
- │ └── com
- │ └── didi
- │ └── aoe
- │ └── runtime
- │ └── ncnn
- │ ├── Interpreter.java
- │ ├── NativeInterpreterWrapper.java
- │ └── Tensor.java
- └── jniLibs
- ├── arm64-v8a
- │ └── libncnn.a
- └── armeabi-v7a
- └── libncnn.a
- Interpreter,提供給外部調用,提供模型加載,推理這些方法。
- NativeInterpreterWrapper是具體的實現類,里面對native進行調用。
- Tensor,主要是一些數據和native層的交互。
AOE NCNN用的好,任務完成早,奧秘在此。
- 支持多輸入多輸出。
- 使用ByteBuffer來提升效率。
- 使用Object作為輸入和輸出(實際支持了ByteBuffer和多維數組)。
光說不練假把式,AOE NCNN的實現過程,且聽我細細道來。
★ 如何支持多輸入多輸出
為了支持多輸入和多輸出,我們在Native層創建了一個Tensor對象的列表,每個Tensor對象里保存了相關的輸入和輸出數據。Native層的Tensor對象,通過tensor_jni提供給java層調用,java層維護這個指向native層tensor的“指針”地址。這樣在有多輸入和多輸出的時候,只要拿到這個列表里的對應的Tensor,就可以就行數據的操作了。
★ ByteBuffer的使用
ByteBuffer,字節緩存區處理子節的,比傳統的數組的效率要高。
DirectByteBuffer,使用的是堆外內存,省去了數據到內核的拷貝,因此效率比用ByteBuffer要高。
當然ByteBuffer的使用方法不是我們要說的重點,我們說說使用了ByteBuffer以后,給我們帶來的好處:
1.接口里的字節操作更加便捷,例如里面的putInt,getInt,putFloat,getFloat,flip等一系列接口,可以很方便的對數據進行操作。
2.和native層做交互,使用DirectByteBuffer,提升了效率。我們可以簡單理解為java層和native層可以直接對一塊“共享”內存進行操作,減少了中間的字節的拷貝過程。
★ 如何使用Object作為輸入和輸出
目前我們只支持了ByteBuffer和MultiDimensionalArray。在實際的操作過程中,如果是ByteBuffer,我們會判斷是否是direct buffer,來進行不同的讀寫操作。如果是MultiDimensionalArray,我們會根據不同的數據類型(例如int, float等),維度等,來對數據進行讀寫操作。
★ 對SqueezeNet Sample的改造
集成AOE NCNN組件以后,讓SqueezeNet依賴NCNN Module,SqueezeNet Sample里面只包含了模型文件,前處理和后處理相關的業務邏輯,前處理和后處理可以用java,也可以用c來實現,由具體的業務實現來決定。新的代碼結構變得非常簡潔,目錄如下:
- ├── AndroidManifest.xml
- ├── assets
- │ └── squeeze
- │ ├── model.config
- │ ├── squeezenet_v1.1.bin
- │ ├── squeezenet_v1.1.id.h
- │ ├── squeezenet_v1.1.param.bin
- │ └── synset_words.txt
- └── java
- └── com
- └── didi
- └── aoe
- └── features
- │ ├── squeezenet_v1.1.id.h
- │ ├── squeezenet_v1.1.param.bin
- │ └── synset_words.txt
- └── java
- └── com
- └── didi
- └── aoe
- └── features
- └── squeeze
- └── SqueezeInterpreter.java
↑ 本Sample也適用于其他的AI業務組件對NCNN組件的調用。
(牛逼就完事兒)
★ 應用如何接入NCNN組件
對NCNN組件的接入,有兩種方式
●直接接入
●通過AOE SDK接入
▲兩種接入方式比較:
不BATTLE了,我單方面宣布,AOE SDK完勝!
★ 對NCNN組件的總結和思考
通過對NCNN組件的封裝,現在業務集成NCNN更加快捷方便了。之前我們一個新的業務集成NCNN,可能需要半天到一天的時間。使用AOE NCNN組件以后,可能只需要1-2小時的時間。當然NCNN組件目前還存在很多不完善的地方,我們對NCNN還需要去加深學習和理解。后面會通過不斷的學習,持續的對NCNN組件進行改造和優化。