天天看點

不加班的秘訣:如何通過AOE快速內建NCNN?

直接內建NCNN的缺點

直接內建NCNN相對麻煩,為SqueezeNet接入NCNN,把相關的模型檔案,NCNN的頭檔案和庫,JNI調用,前處理和後處理相關業務邏輯等。把這些内容都放在SqueezeNet Sample工程裡。這樣簡單直接的內建方法,問題也很明顯,和業務耦合比較多,不具有通用性,前處理後處理都和SqueezeNcnn這個Sample有關,不能很友善地提供給其他業務元件使用。深入思考一下,如果我們把AI業務,作為一個一個單獨的AI元件提供給業務的同學使用,會發生這樣的情況:

不加班的秘訣:如何通過AOE快速內建NCNN?

每個元件都要依賴和包含NCNN的庫,而且每個元件的開發同學,都要去熟悉NCNN的接口,寫C的調用代碼,寫JNI。是以我們很自然地會想到要提取一個NCNN的元件出來,提取以後如圖所示:

不加班的秘訣:如何通過AOE快速內建NCNN?

AOE SDK裡的NCNN元件

有了AOE SDK,就可以使操作更加簡便。在AOE開源SDK裡,我們提供了NCNN元件,下面我們從4個方面來講一講NCNN元件:

1.NCNN元件的設計

2.對SqueezeNet Sample的改造

3.應用如何接入NCNN元件

4.對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用的好,任務完成早,奧秘在此。

1.支援多輸入多輸出。

2.使用ByteBuffer來提升效率。

3.使用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
                    └── squeeze
                        └── SqueezeInterpreter.java
           

↑ Sample也适用于其他的AI業務元件對NCNN元件的調用。

應用如何接入NCNN元件

對NCNN元件的接入,有兩種方式

●直接接入

不加班的秘訣:如何通過AOE快速內建NCNN?

●通過AOE SDK接入

不加班的秘訣:如何通過AOE快速內建NCNN?

▲兩種接入方式比較:

不加班的秘訣:如何通過AOE快速內建NCNN?

不BATTLE了,我單方面宣布,AOE SDK完勝!

對NCNN元件的總結和思考

通過對NCNN元件的封裝,現在業務內建NCNN更加快捷友善了。之前我們一個新的業務內建NCNN,可能需要半天到一天的時間。使用AOE NCNN元件以後,可能隻需要1-2小時的時間。當然NCNN元件目前還存在很多不完善的地方,我們對NCNN還需要去加深學習和了解。後面會通過不斷的學習,持續的對NCNN元件進行改造和優化。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - A o E - - - - - - - - - - - - - - - - - - - - - - - - - - - -