tvm部署c++神經網絡前向代碼到android端

tvm部署c++神經網絡前向代碼到android端

tvm是端到端的神經網絡編譯器。簡單來說,他可以把神經網絡模型編譯成一個動態鏈接庫,並部署到各種硬件上去執行,包括移動設備。

模型

tvm可以支持編譯各種框架模型,包括tflite,onnx等,本文主要描寫從onnx到tvm部署到android的過程。
tvm支持的框架以及各種模型編譯教程網址:https://docs.tvm.ai/tutorials/index.html

生成交叉編譯工具

我實在ubantu上編譯onnx,需要下載android sdk,並利用sdk生成一個交叉編譯器。因爲我是在ubantu平臺上編譯arm平臺的鏈接庫。

cd /sdk/ndk-bundle/build/tools/
./make-standalone-toolchain.sh --platform=android-24 --use-llvm --arch=arm64 --install-dir=/opt/android-toolchain-arm64

我用的架構師arm64-v8a,所以–arch參數爲arm64。最後生成的交叉編譯器在/opt/android-toolchain-arm64目錄下。

tvm下載和編譯

1.克隆倉庫

git clone --recursive https://github.com/dmlc/tvm

2.安裝依賴

sudo apt-get update
sudo apt-get install -y python python-dev python-setuptools gcc \
     libtinfo-dev zlib1g-dev build-essential cmake

3.安裝llvm
要安裝大於4.0版本的,而ubuntu 16.04 apt官方源最新只有3.x,ubuntu 18.04則沒問題(安裝的是6.0)。如果apt官方最新的llvm版本小於4,那麼使用llvm的源:

apt install software-properties-common
apt-add-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial main"
apt-get update
apt-get install clang

這裏最後一步如果報了***文件找不到的錯誤,可以touch *** 新建一個空文件試一下,可能會有四到五文件報錯說找不到,新建一個空文件就可以。

4.移動config.cmake

cd tvm
mkdir build
cp cmake/config.cmake build

編輯build/config.cmake文件,裏面有一些功能開關,打開了set(USE_LLVM ON).

5.編譯
(1)編譯生成x86-64版本的tvm

make clean
make -4j

由於是在ubantu環境下,默認編譯x86-64版本的tvm。

(2)編譯生成arm64-v8a版本的tvm
首先導入環境變量

export AR_host="ar"
export CC_host="gcc"
export CXX_host="g++"
export LINK_host="g++"
export ARCH=arm64
export PATH=/opt/android-toolchain-arm64/bin:$PATH
export CROSS_COMPILE=aarch64-linux-android-
export CC=/opt/android-toolchain-arm64/bin/aarch64-linux-android-gcc
export CXX=/opt/android-toolchain-arm64/bin/aarch64-linux-android-g++
export LD=/opt/android-toolchain-arm64/bin/aarch64-linux-android-ld
export AR=/opt/android-toolchain-arm64/bin/aarch64-linux-android-ar
export AS=/opt/android-toolchain-arm64/bin/aarch64-linux-android-as
export RANLIB=/opt/android-toolchain-arm64/bin/aarch64-linux-android-ranlib

顯然,這裏引入的是之前的交叉編譯工具:/opt/android-toolchain-arm64
然後執行:

make clean
make -4j

問題:這裏爲什麼要編譯兩種版本的tvm?
在部署到移動端的過程中,兩個版本的tvm都要用到。在將onnx編譯成arm64版本的動態鏈接庫的時候,要用到x86-64版本的tvm,因爲我們是在x86-64的宿主機上進行的編譯;而arm64版本的tvm裏面有一些動態鏈接庫我們要放到移動端。

編譯過後會生成如下的動態鏈接庫文件,只是兩種編譯方式生成的so文件架構不同。

[  5%] Linking CXX shared library libvta.so
[ 12%] Linking CXX shared library libtvm_runtime.so
[ 86%] Linking CXX shared library libtvm.so
[ 94%] Linking CXX shared library libtvm_topi.so
[100%] Linking CXX shared library libnnvm_compiler.so

編譯onnx

編譯onnx需要在x86-64版本的tvm下進行,需要安裝onnx:pip installl onnx
設置python環境變量:

export TVM_HOME=~/tvm/
export PYTHONPATH=$TVM_HOME/python:$TVM_HOME/topi/python:$TVM_HOME/nnvm/python:${PYTHONPATH}

編譯代碼:

import onnx
import numpy as np
import tvm
import tvm.relay as relay
import os
from tvm.contrib import util, ndk, graph_runtime as runtime
from tvm.contrib.download import download_testdata

onnx_model = onnx.load('****.onnx')

x = np.ones([1,3,256,256])                             //輸入的tensor shape
arch = "arm64"
target =  "llvm -target=%s-linux-android" % arch              //編譯的目標架構
input_name = 'input'                                                       //網絡輸入節點名
shape_dict = {input_name: x.shape}
sym, params = relay.frontend.from_onnx(onnx_model, shape_dict)

with relay.build_config(opt_level=0):
    intrp = relay.build_module.create_executor('graph', sym, tvm.cpu(0), target)
dtype = 'float32'
with relay.build_config(opt_level=0):
    graph, lib, params = relay.build_module.build(sym, target, params=params)

print("Output model files")
libpath = "model.so"
lib.export_library(libpath, cc="/opt/android-toolchain-arm64/bin/aarch64-linux-android-gcc")

graph_json_path = "model.json"
with open(graph_json_path, 'w') as fo:
    fo.write(graph)

param_path = "model.params"
with open(param_path, 'wb') as fo:
    fo.write(relay.save_param_dict(params))

以上代碼生成三個文件model.so, model.json, model.params。這三個文件要放到安卓的assets目錄下。

移動端部署

其實移動端部署的C++代碼官網已經寫的非常清楚:https://docs.tvm.ai/deploy/nnvm.html

將以下C++文件命名爲libnative-lib.cpp

#include <dlpack/dlpack.h>
#include <tvm/runtime/module.h>
#include <tvm/runtime/registry.h>
#include <tvm/runtime/packed_func.h>

#include <fstream>
#include <iterator>
#include <algorithm>

int main()
{
    // tvm module for compiled functions
    tvm::runtime::Module mod_syslib = tvm::runtime::Module::LoadFromFile("deploy.so");

    // json graph
    std::ifstream json_in("deploy.json", std::ios::in);
    std::string json_data((std::istreambuf_iterator<char>(json_in)), std::istreambuf_iterator<char>());
    json_in.close();

    // parameters in binary
    std::ifstream params_in("deploy.params", std::ios::binary);
    std::string params_data((std::istreambuf_iterator<char>(params_in)), std::istreambuf_iterator<char>());
    params_in.close();

    // parameters need to be TVMByteArray type to indicate the binary data
    TVMByteArray params_arr;
    params_arr.data = params_data.c_str();
    params_arr.size = params_data.length();

    int dtype_code = kDLFloat;
    int dtype_bits = 32;
    int dtype_lanes = 1;
    int device_type = kDLCPU;
    int device_id = 0;

    // get global function module for graph runtime
    tvm::runtime::Module mod = (*tvm::runtime::Registry::Get("tvm.graph_runtime.create"))(json_data, mod_syslib, device_type, device_id);

    DLTensor* x;
    int in_ndim = 4;
    int64_t in_shape[4] = {1, 3, 256, 256};
    TVMArrayAlloc(in_shape, in_ndim, dtype_code, dtype_bits, dtype_lanes, device_type, device_id, &x);
    // load image data saved in binary
    std::ifstream data_fin("cat.bin", std::ios::binary);
    data_fin.read(static_cast<char*>(x->data), 3 * 256 * 256 * 4);

    // get the function from the module(set input data)
    tvm::runtime::PackedFunc set_input = mod.GetFunction("set_input");
    set_input("data", x);

    // get the function from the module(load patameters)
    tvm::runtime::PackedFunc load_params = mod.GetFunction("load_params");
    load_params(params_arr);

    // get the function from the module(run it)
    tvm::runtime::PackedFunc run = mod.GetFunction("run");
    run();

    DLTensor* y;
    int out_ndim = 2;
    int64_t out_shape[2] = {1, 1000};
    TVMArrayAlloc(out_shape, out_ndim, dtype_code, dtype_bits, dtype_lanes, device_type, device_id, &y);

    // get the function from the module(get output data)
    tvm::runtime::PackedFunc get_output = mod.GetFunction("get_output");
    get_output(0, y);

    // get the maximum position in output vector
    auto y_iter = static_cast<float*>(y->data);
    auto max_iter = std::max_element(y_iter, y_iter + 1000);
    auto max_index = std::distance(y_iter, max_iter);
    std::cout << "The maximum position in output vector is: " << max_index << std::endl;

    TVMArrayFree(x);
    TVMArrayFree(y);

    return 0;
}

這裏的頭文件是之前編譯tvm之後生成的,放到項目中src/main/cpp目錄下,和native-lib.cpp代碼一個目錄。
當然,這裏我項目裏的話其實是在jni層,這裏只說明tvm前向的語法,不會對jni的語法做描述,大家可以根據自己的需要將前向代碼移植到自己的jni層,然後可以在java層做調用。

注意C++端TVM的運行依於一個tvm的動態鏈接庫:arm64版本的libtvm_runtime.so,需要將該文件放到android項目的app/src/main/libs/arm64-v8a目錄下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章