一步一步解讀神經(jīng)網(wǎng)絡(luò)編譯器TVM(二)——利用TVM完成C++端的部署
從 https://www.bookstack.cn/read/HyperDL-Tutorial/5.deploy-README.md
的對(duì)比來看已球,ncnn是最輕量的一個(gè)框架恤筛,不過功能相對(duì)單一计技,速度不是最快但也不錯(cuò)壮莹。
從https://tvm.ai/2018/10/03/auto-opt-all.html
對(duì)比來看TVM要比ncnn快一點(diǎn)
目前還不能指望通過更換框架得到太多的速度提升饿这。
注意:以下翻譯總結(jié)可能沒有原文詳盡坯屿,更逗內(nèi)容請(qǐng)查看官方文檔
TVM簡(jiǎn)介
TVM是一個(gè)開源深度學(xué)習(xí)編譯棧淹真,支持CPU抓韩、GPU和特定的加速器纠永。它旨在縮小以生產(chǎn)為導(dǎo)向的深度學(xué)習(xí)框架和以性能為導(dǎo)向的硬件后端。TVM有兩個(gè)主要特性:
1谒拴、支持將Keras尝江、MxNet、PyTorch彪薛、Tensorflow茂装、CoreML、DarkNet框架的深度學(xué)習(xí)模型編譯為多種硬件后端的最小可部署模型善延。
2少态、能夠2自動(dòng)生成和優(yōu)化多個(gè)后端的張量操作并達(dá)到更好的性能。
TVM堆棧始于華盛頓大學(xué)Paul G. Allen計(jì)算機(jī)科學(xué)與工程學(xué)院SAMPL小組的研究項(xiàng)目易遣。該項(xiàng)目現(xiàn)在由一個(gè)涉及多個(gè)行業(yè)和學(xué)術(shù)機(jī)構(gòu)的開源社區(qū)推動(dòng)彼妻,遵循Apache協(xié)議。
TVM提供如下圖所示的兩個(gè)等級(jí)的優(yōu)化侨歉。計(jì)算圖優(yōu)化包括高級(jí)操作融合、層轉(zhuǎn)換和內(nèi)存管理炮温。張量操作優(yōu)化和代碼生成層優(yōu)化張量操做畸颅。
部署和集成
TVM包含兩個(gè)主要部分:
1假颇、TVM編譯器骨稿,用來做編譯和優(yōu)化。
2姜钳、TVM運(yùn)行環(huán)境坦冠,用來在目標(biāo)設(shè)備上運(yùn)行。
還可以使用RPC進(jìn)行遠(yuǎn)程測(cè)試和優(yōu)化哥桥。
安裝
Install from Source
1辙浑、首先用C++代碼編譯動(dòng)態(tài)庫(libtvm.so for linux, libtvm.dylib for macOS and libtvm.dll for windows)
2、設(shè)置語言包
clone代碼 git clone --recursive https://github.com/dmlc/tvm
編譯動(dòng)態(tài)庫
- 在Linux上是libtvm.so, libtbm_topi.so
- macOS上是libtvm.dylib, libtvm_topi.dylib
- Windows上是libtvm.dll, libtvm_topi.dll
最小編譯依賴:
- 支持C++ 11的c++編譯器(g++4.8或更高版本)
- CMake 3.5 或更高版本
- 強(qiáng)烈建議使用LLVM編譯以打開所有特性
- 如果只用CUDA/OpenCL拟糕,可以不依賴LLVM
- 如果使用NNVM編譯器判呕,需要LLVM
使用cmake編譯,配置可以在config.cmake中修改
- 首先送滞,檢查系統(tǒng)的cmake侠草。如果沒有可以從official website下載最新版。
- 先創(chuàng)建一個(gè)編譯目錄犁嗅,復(fù)制 cmake/config.cmake到該目錄边涕。
mkdir build
cp cmake/config.cmake build
- 編輯build/config.cmake定制編譯選項(xiàng)
- 在macOS,對(duì)于一些版本的Xcode,需要在LDFLAGS中添加
-lc++abi
,否則會(huì)有鏈接錯(cuò)誤功蜓。 - 修改
set(USE_CUDA OFF)
為set(USE_CUDA ON)
打開CUDA后端园爷。其他后端和庫(OpenCL、RCOM式撼、METAL童社、VULKAN等)也是如此。
- 根據(jù)某些選項(xiàng)TVM會(huì)依賴LLVM著隆。有些CPU平臺(tái)的編譯會(huì)需要LLVM扰楼。
- 如果依賴LLVM,需要 4.0 或者更高版本旅东。記住默認(rèn)的LLVM版本可能低于4.0灭抑。
- 因?yàn)樵创a編譯LLVM會(huì)花費(fèi)很多時(shí)間,可以從LLVM Download Page
下載預(yù)編譯版本抵代。
(1)解壓到指定目錄腾节,修改build/config.cmake
去添加set(USE_LLVM /path/to/your/llvm/bin/llvm-config)
(2)也可以直接設(shè)置set(USE_LLVM ON)
讓cmake搜索可用的LLVM。 - 也可以使用LLVM Nightly Ubuntu Build
注意apt包要在llvm-config
后面跟上版本號(hào)荤牍。比如案腺,如果已經(jīng)經(jīng)安裝了set(LLVM_CONFIG llvm-config-4.0)
接下來執(zhí)行
cd build
cmake ..
make all
安裝Python包
python包位于 tvm/python .有兩種方式安裝這些包:
方式1
這種方式適用于可能修改代碼的開發(fā)者。
在.bashrc中設(shè)置環(huán)境變量PYTHONPATH告訴python在哪里找這個(gè)庫康吵。
export TVM_HOME=/path/to/tvm
export PYTHONPATH=$TVM_HOME/python:$TVM_HOME/topi/python:$TVM_HOME/nnvm/python:${PYTHONPATH}
方式2
使用setup.py安裝
# install tvm package for the current user
# NOTE: if you installed python via homebrew, --user is not needed during installaiton
# it will be automatically installed to your user directory.
# providing --user flag may trigger error during installation in such case.
export MACOSX_DEPLOYMENT_TARGET=10.9 # This is required for mac to avoid symbol conflicts with libstdc++
cd python; python setup.py install --user; cd ..
cd topi/python; python setup.py install --user; cd ../..
cd nnvm/python; python setup.py install --user; cd ../..
Python依賴
- 必需的依賴:
pip install --user numpy decorator attrs
- 如果使用RPC
pip install --user tornado
- 如果使用自動(dòng)調(diào)優(yōu)模塊
pip install --user tornado psutil xgboost
安裝NNPACK
NNPACK是一個(gè)神經(jīng)網(wǎng)絡(luò)加速包劈榨,可以運(yùn)行在x86-64,ARMv7,或者ARM64架構(gòu)CPU上晦嵌。使用NNPACK同辣,高級(jí)的庫像MXNet可以在多核CPU上面加速運(yùn)行,包括筆記本電腦和移動(dòng)設(shè)備惭载。
Note:因?yàn)門VM已經(jīng)內(nèi)置了調(diào)優(yōu)旱函,NNPACK在這里主要用來參考和對(duì)比。正常使用優(yōu)先選TVM內(nèi)置優(yōu)化描滔。
TVM支持NNPACK做卷積棒妨、最大池化和全連接層的前向計(jì)算。
使用
如何利用 TVM 優(yōu)化深度學(xué)習(xí)GPU op含长?教你用幾十行Python代碼實(shí)現(xiàn)2-3倍提升
編譯和部署
部署時(shí)需要交叉編譯運(yùn)行時(shí)環(huán)境券腔,然后部署到目標(biāo)設(shè)備上。
如果運(yùn)行環(huán)境為L(zhǎng)inux系統(tǒng):
git clone --recursive https://github.com/dmlc/tvm
cd tvm
mkdir build
cp cmake/config.cmake build
cd build
cmake ..
make runtime
如果是編譯tvm編譯器拘泞,就把make runtime
改為make
即可纷纫。
調(diào)優(yōu)
推薦使用RPC API在嵌入式目標(biāo)設(shè)備上進(jìn)行測(cè)試、調(diào)優(yōu)田弥,下面是相關(guān)鏈接:
- Cross Compilation and RPC
- tutorial-deploy-model-on-mali-gpu
- Deploy the Pretrained Model on Raspberry Pi
1涛酗、 交叉編譯和RPC
通過交叉編譯和RPC,可以在本地機(jī)器編譯程序然后再遠(yuǎn)測(cè)設(shè)備運(yùn)行。這在遠(yuǎn)程設(shè)備資源受限時(shí)是有用的商叹,比如樹莓派和手機(jī)燕刻。In this tutorial, we will take Raspberry Pi for CPU example and Firefly-RK3399 for opencl example.
(1)在設(shè)備上編譯運(yùn)行時(shí)環(huán)境
第一步是在遠(yuǎn)程設(shè)備編譯TVM運(yùn)行環(huán)境。
注意:
這部分和下部分的所有指令都應(yīng)該運(yùn)行在目標(biāo)設(shè)備上剖笙,我們目標(biāo)設(shè)備假設(shè)是Linux系統(tǒng)卵洗。
(在目標(biāo)設(shè)備上)編譯目標(biāo)設(shè)備運(yùn)行環(huán)境:
git clone --recursive https://github.com/dmlc/tvm
cd tvm
make runtime -j2
運(yùn)行環(huán)境編譯完成后,需要在(目標(biāo)設(shè)備) /.bashrc中設(shè)置環(huán)境變量弥咪,添加(假定TVM目錄在/tvm)
export PYTHONPATH=$PYTHONPATH:~/tvm/python
運(yùn)行 source ~/.bashrc激活環(huán)境變量过蹂。
(2)在目標(biāo)設(shè)備設(shè)置RPC服務(wù)
在遠(yuǎn)程設(shè)備執(zhí)行以下命令啟動(dòng)RPC服務(wù)
python -m tvm.exec.rpc_server --host 0.0.0.0 --port=9090
如果看到下面這行,說明RPC服務(wù)已經(jīng)成功啟動(dòng)
INFO:root:RPCServer: bind to 0.0.0.0:9090
(3)在本地機(jī)器聲明和交叉編譯內(nèi)核
注意:
現(xiàn)在切換到有完整TVM環(huán)境的本地機(jī)器(PC)聚至。
這里我們?cè)诒镜貦C(jī)器聲明一個(gè)簡(jiǎn)單的kernel:
import numpy as np
import tvm
from tvm import rpc
from tvm.contrib import util
n = tvm.convert(1024)
A = tvm.placeholder((n,),name='A')
B = tvm.compute((n,), lambda i: A[i]+1.0, name='B')
s = tvm.create_schedule(B.op)
然后我們交叉編譯這個(gè)kernel. 對(duì)于Raspberry Pi 3B, target應(yīng)該是 llvm -target=armv7l-linux-gnueabihf
,但是這里我們使用llvm
使得能夠運(yùn)行在我們得網(wǎng)頁server上酷勺。
local_demo = True
if local_demo:
target = 'llvm'
else:
target = 'llvm -target=armv7l-linux-gnueabihf'
func = tvm.build(s, [A, B], target=target, name='add_one')
# save the lib at a local temp folder
temp = util.tempdir()
path = temp.relpath('lib.tar')
func.export_library(path)
注意:
如果要在真實(shí)的遠(yuǎn)程設(shè)備運(yùn)行,修改local_demo
為False并且將build
函數(shù)中的target
修改為設(shè)備真正的架構(gòu)扳躬。不同設(shè)備可能會(huì)不同脆诉。比如, Raspberry Pi 3B 是llvm -target=armv7l-linux-gnueabihf
贷币, RK3399是llvm -target=aarch64-linux-gnu
(4)通過RPC遠(yuǎn)程運(yùn)行CPU kernel
我們展示在遠(yuǎn)程設(shè)備運(yùn)行生成的cpu kernel击胜。首先,從遠(yuǎn)程設(shè)備獲取RPC會(huì)話役纹。
if local_demo:
remote = rpc.LocalSession()
else:
# The following is my environment, change this to the IP address of your target device
host = '10.77.1.162'
port = 9090
remote = rpc.connect(host, port)
上傳 lib到遠(yuǎn)程設(shè)備偶摔,然后調(diào)用設(shè)備編譯器重新鏈接它。現(xiàn)在func是一個(gè)遠(yuǎn)程module對(duì)象促脉。
remote.upload(path)
func = remote.load_module('lib.tar')
# create arrays on the remote device
ctx = remote.cpu()
a = tvm.nd.array(np.random.uniform(size=1024).astype(A.dtype), ctx)
b = tvm.nd.array(np.zeros(1024, dtype=A.dtype), ctx)
# the function will run on the remote device
func(a, b)
np.testing.assert_equal(b.asnumpy(), a.asnumpy() + 1)
使用time_evaluator
多次運(yùn)行辰斋,評(píng)估每次運(yùn)行的時(shí)間
time_f = func.time_evaluator(func.entry_name, ctx, number=10)
cost = time_f(a, b).mean
print('%g secs/op' % cost)
部署
調(diào)優(yōu)完成后,需要把模型不依賴RPC部署到目標(biāo)設(shè)備瘸味。以下是參考鏈接:
- Deploy TVM Module using C++ API
- Deploy to Android
- Deploy NNVM Modules
-
Integrate TVM into Your Project
- DLPack Support
- Integrate User Defined C++ Array
-
Integrate User Defined Python Array
1亡呵、使用C++ API部署TVM模型
部署示例:apps/howto_deploy
執(zhí)行以下命令運(yùn)行示例:
cd apps/howto_deploy
./run_example.sh
(1)獲取TVM運(yùn)行庫
現(xiàn)在需要做的是在目標(biāo)平臺(tái)鏈接運(yùn)行庫。TVM提供了一個(gè)最小運(yùn)行庫硫戈,大約300k到600k,取決于模型大小下硕。大多數(shù)情況下丁逝,可以使用 編譯時(shí)產(chǎn)生的 libtvm_runtime.so
。
如果libtvm_runtime
難以編譯梭姓,checkout tvm_runtime_pack.cc霜幼。它是一個(gè)能夠獲取TVM運(yùn)行環(huán)境的 all in one file 例子。你可以把它編譯并且包含到你的工程中誉尖。
你也可以checkout apps
示例應(yīng)用罪既,包含iOS,Android和其他平臺(tái)。
(2)動(dòng)態(tài)庫 vs 系統(tǒng)模塊
TVM提供兩種編譯庫的方式。你可以checkout prepare_test_libs.py
了解如何生成庫琢感,checkout cpp_deploy.cc了解如何使用丢间。
a.保存為動(dòng)態(tài)庫并且動(dòng)態(tài)加載到工程中。
b.Bundle the compiled library into your project in system module mode.
動(dòng)態(tài)加載更加靈活并且可以在運(yùn)行時(shí)加載新模塊驹针。系統(tǒng)模塊是一個(gè)更加static的方法烘挫。我們可以在動(dòng)態(tài)加載被禁用的地方使用系統(tǒng)模塊。
2柬甥、部署到Android
(1)為Android編譯模型
Android平臺(tái)模型的NNVM編譯可以采用一些方法比如android_rpc.
示例chainer-nnvm-example
上面的例子會(huì)以RPC的方式直接在目標(biāo)設(shè)備上運(yùn)行編譯好的模型饮六。下面對(duì)于rum_mobile.py修改android平臺(tái)需要的編譯輸出
lib.export_library("deploy_lib.so", ndk.create_shared)
with open("deploy_graph.json", "w") as fo:
fo.write(graph.json())
with open("deploy_param.params", "wb") as fo:
fo.write(nnvm.compiler.save_param_dict(params))
把生成的deploy_lib.so, deploy_graph.json, deploy_param.params放到Android設(shè)備上。
(2)Android TVM運(yùn)行環(huán)境
參考here
去編譯CPU/OpenCL版本的TVM運(yùn)行環(huán)境到Android設(shè)備苛蒲。從android java TVM AP加載運(yùn)行模型參考這個(gè)java代碼卤橄。
實(shí)踐:用TVM部署Tensorflow模型到Android
首先根據(jù)上面的內(nèi)容搭建好編譯和運(yùn)行環(huán)境,下面開始部署模型臂外。
Android的部署可以使用TVM4J窟扑,詳見 here
這里我們不用Java,仍然使用C++寄月。
一辜膝、環(huán)境配置
環(huán)境依賴有l(wèi)lvm和tvm runtime。
1漾肮、對(duì)于llvm 我們從LLVM Download Page下載armv7a Linux 架構(gòu)的預(yù)編譯庫厂抖。可能需要根據(jù)Android系統(tǒng)的環(huán)境選取其他版本克懊。
2忱辅、對(duì)于tvm runtime, 我們把tvm的源碼放入Android Studio谭溉,然后將 tvm_runtime_pack.cc加入到CMakeLists.txt中進(jìn)行編譯墙懂。
二、模型部署
1扮念、Tensorflow轉(zhuǎn)onnx
https://github.com/onnx/tutorials/blob/master/tutorials/OnnxTensorflowExport.ipynb
示例代碼:https://github.com/onnx/tutorials/blob/master/tutorials/assets/tf-train-mnist.py
(1)編譯時(shí)保存模型圖
在模型訓(xùn)練代碼中加上以下代碼
with open("graph.proto", "wb") as file:
graph = tf.get_default_graph().as_graph_def(add_shapes=True)
file.write(graph.SerializeToString())
(2)Graph Freezing损搬,將模型圖和參數(shù)合并到一個(gè)文件
Secondly, we freeze the graph. Here, we include quotes from Tensorflow documentation about what graph freezing is:
One confusing part about this is that the weights usually aren't stored inside the file format during training. Instead, they're held in separate checkpoint files, and there are Variable ops in the graph that load the latest values when they're initialized. It's often not very convenient to have separate files when you're deploying to production, so there's the freeze_graph.py script that takes a graph definition and a set of checkpoints and freezes them together into a single file.
Thus here we build the freeze_graph tool in the Tensorflow source folder and execute it with the information about where the GraphProto is, where the checkpoint file is and where to put the frozen graph. One caveat is that you need to supply the name of the output node to this utility. If you are having trouble finding the name of the output node, please refer to this article for help.
bazel build tensorflow/python/tools:freeze_graph
bazel-bin/tensorflow/python/tools/freeze_graph \
--input_graph=/home/mnist-tf/graph.proto \
--input_checkpoint=/home/mnist-tf/ckpt/model.ckpt \
--output_graph=/tmp/frozen_graph.pb \
--output_node_names=fc2/add \
--input_binary=True
Note that now we have obtained the frozen_graph.pb
with graph definition as well as weight information in one file.
(3)模型轉(zhuǎn)換
Thirdly, we convert the model to ONNX format using onnx-tensorflow. Using tensorflow_graph_to_onnx_model
from onnx-tensorflow API (documentation available at https://github.com/onnx/onnx-tensorflow/blob/master/doc/API.md).
import tensorflow as tf
from onnx_tf.frontend import tensorflow_graph_to_onnx_model
with tf.gfile.GFile("frozen_graph.pb", "rb") as f:
graph_def = tf.GraphDef()
graph_def.ParseFromString(f.read())
onnx_model = tensorflow_graph_to_onnx_model(graph_def,
"fc2/add",
opset=6)
file = open("mnist.onnx", "wb")
file.write(onnx_model.SerializeToString())
file.close()
此步驟完成后得到一個(gè)onnx模型文件。
2柜与、導(dǎo)出onnx模型編譯為動(dòng)態(tài)庫
這里的onnx模型也就是前面所說的kernel巧勤。導(dǎo)出代碼可以參考https://oldpan.me/archives/the-first-step-towards-tvm-2
注意修改target。
這里會(huì)導(dǎo)出三個(gè)文件 xxx.so, xxx.json, xxx.params弄匕。
3颅悉、將第2步導(dǎo)出的三個(gè)文件放到Android設(shè)備中,加載到TVM推理代碼中運(yùn)行迁匠。
關(guān)于tvm_runtime的線程安全:https://discuss.tvm.ai/t/is-tvmruntime-thread-safe/84