AOE工程實踐-NCNN組件

  • 2019 年 10 月 3 日
  • 筆記

作者:楊科

NCNN是騰訊開源的一個為手機端極致優化的高性能神經網絡前向計算框架。在AOE開源工程里,我們提供了NCNN組件,下面我們以SqueezeNet物體識別這個Sample為例,來講一講NCNN組件的設計和用法。

直接集成NCNN缺點

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

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

AOE SDK里的NCNN組件

在AOE開源SDK里,我們提供了NCNN組件,下面我們從4個方面來講一講NCNN組件:

  • NCNN組件的設計
  • 對SqueezeNet Sample的改造
  • 應用如何接入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和多維數組)。

下面我們來說說具體是如何做的。

如何支持多輸入多輸出
為了支持多輸入和多輸出,我們在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

其他的AI業務組件對NCNN組件的調用,都可以參考SqueezeNet這個Sample。

應用如何接入NCNN組件

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

  • 直接接入

  • 通過AOE SDK接入

兩種接入方式比較:

功能特性 直接接入 通過AOE SDK接入
易用性 容易 容易
穩定性 不能確定,依賴實現方的實現 高,安卓有獨立進程機制,更加穩定,推理過程不影響主進程
模型配置
模型下載和動態升級
模型配置
模型準確率,性能等數據分析
提供圖像處理工具包 AOE SDK提供了Vision組件
支持模型加密 要看具體的模型和框架 AOE SDK提供了模型加密解密組件

通過比較,我們更建議是通過AOE SDK來對我們的NCNN組件進行接入。

對NCNN組件的總結和思考

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

歡迎大家來使用和提建議

AoE (AI on Edge,終端智能,邊緣計算) 是一個終端側AI集成運行時環境 (IRE),幫助開發者提升效率。 https://github.com/didi/aoe

Github地址:

歡迎star~