大佬说选择移植Tengine Python API这个任务,一方面是因为他之前做过涉及Python和C++/C交互的开源项目工作,如 MXNet 中DLPack的Python API和他自己的开源项目 MobulaOP,这些工作让他踩了不少的坑;另一方面是因为他认为,了解一个框架需要先把例子跑起来, 就像学习一门新的编程语言要先跑通它的Hello World程序。通过这次任务,可以了解基于Tengine Lite的图像分类实现,对TengineLite有一个初始的、直观的感受。
以下为大佬第一人称自述\~
初探源码
这个任务的工作可以用一句话来概括: 在Tengine跑起Python例子, 再在Tengine Lite跑起同样的例子。
第一步需要找到Tengine Python API的例子。但Tengine没有的Python例子, 也没有Python API的文档。想从单元测试入手,但也没有Python API的单元测试,难怪移植Python API的任务难度比移植C++ API的任务难度高。还有什么方法可以了解Python API的用法呢? 阅读源码。Tengine的Python API放在pytengine文件夹, 里面有八个模块,分别是:base、context、device、 graph、libinfo、node、tengine和tensor,可以重点关注base, graph和tensor。
从base.py源码中可以看出Tengine使用ctypes的形式进行Python和C++/C的交互,把动态链接库libtengine.so读取后保存到变量\_LIB中,通过\_LIB.可以调用Tengine的C API. graph.py实现计算图部分的API,tensor.py实现了作为输入和输出的张量API。
编译和导入模块
找到Python API代码的位置后,编译Tengine,并尝试在Python中导入tengine模块。从libinfo.py中可以了解到,Tengine Python API会在Python API目录和环境变量LD\_LIBRARY\_PATH所指向的目录中,查找动态链接库libtengine.so.这时遇到了第一个Bug,我用的操作系统没有定义LD\_LIBRARY\_PATH这个环境变量,而API里直接用下标访问的形式取这个环境变量的值,出现了KeyError的错误。改成os.environ.get('LD\_LIBRARY\_PATH', '')即可。设置好动态链接库的路径后,可以成功导入pytengine模块了。
编写Python分类示例
Tengine提供C++/C的分类示例代码,在examples目录下,可以作为编写Python例子时的参考。其中,classification.cpp用了Tengine的C++ API,比C API多了Net类的封装,Net类封装了对于计算图Graph的操作。而classification\_old\_api.cpp用的是Tengine的C API. 由于Tengine Python API调用的是Tengine C API,因此可以拿classification\_old\_api.cpp作为参考。Tengine Python API封装得很简洁,很容易能找到每个Python函数调用的C函数。C++分类示例代码和Python API的代码互相对照,就可以写出Python分类示例的代码。写完后就可以尝试运行了。
运行Python分类示例
运行Python分类示例并不顺利,一开始就在构建计算图上出错了。定位到Python API的源码后, 发现是在以下两行出错。
# pytengine/tengine/graph.py:L24-L25
params = [ c_str(item) for item in kwarg]
self.graph = _LIB.create_graph(ctypes.c_void_p(context), c_str(model), *params)create\_graph有三个参数: 第一个参数context是模型执行的上下文,第二个参数是模型的格式,第三个参数是模型的文件名。这两行代码看起来没什么毛病,和C++例子里的调用方式是一模一样的。但出错的原因就在给变量params赋值的这一行,里面的item是一个临时变量,当这条语句结束时,item离开了作用域就被释放了。而c\_str(item)是指向原来item的位置,变成了一个野指针。为了解决这个问题,可以把该行改为params = [ c\_str(kwarg[i]) for i in range(len(kwarg)) ],此时c\_str(kwarg[i])指向的是变量kwargs中存储的值,变量kwargs在调用函数create\_graph时仍在作用域内。
另外遇到的一个复杂的问题和Tensor类有关, pytengine的Tensor类还不完善,无法取出Tensor里的数据.。对照C语言写的例子修改Tensor的buf函数,得到数据内存地址,占用的内存大小,类型,尺寸后,转换为NumPy数组。 需要注意一下Tengine Lite前端的执行步骤:
# 建立计算图并读取模型文件
tm_file graph = tg.Graph(None, 'tengine', tm_file)
# 取出输入
Tensor input_tensor = graph.getInputTensor(0, 0)
# 设置输入Tensor的尺寸
dims = [1, 3, img_h, img_w]
input_tensor.shape = dims
# 预先运行以分配资源, 必须加上
graph.preRun()
# 设置输入数据的内存地址, 这里的data是尺寸为(3, img_h, img_w)的NumPy数组. 注意: 这里不会检查shape
input_tensor.buf = data
# 以同步方式让网络进行推断(前向传播), 其中1表示使用同步的方式
graph.run(1) # 1 is blocking
# 取出输出的Tensor
output_tensor = graph.getOutputTensor(0, 0)
# 将Tensor转为NumPy数组
output = np.array(output_tensor.buf)把这些问题解决后,就能在Tengine上运行Python示例了。
需要注意的是,Tengine Lite和Tengine在做推断前,都需要调用preRun()函数对资源进行分配,这是必须要调用的。 虽然现在的Python API用起来有点复杂,但相信之后会封装得更好的。
从Tengine到Tengine Lite
在Tengine上成功运行Python示例后,移植就变得方便了。直接把pytengine文件夹下的所有代码, 以及Python分类示例复制粘贴到Tengine Lite中,将动态库名称从libtengine.so改为libtengine-lite.so, 然后运行Python分类示例。不出意料,出错了。 原因是Tengine Lite在设置输入Tensor的数据内存地址时,也会检查数据的大小,而之前的Python API的数据大小的计算是错误的。 修复Bug后, 成功在Tengine Lite上运行图像分类示例。移植完成。不得不夸一下Tengine Lite的C API兼容性做得真好!
在EAIDK-310上运行Tengine Lite的Python图像分类示例
之前参加OPEN AI LAB的活动,得到了一块EAIDK-310开发板, 刚好可以在上面进行测试。
这里使用可爱的虎猫(Tiger Cat)作为测试图片,模型采用MobileNet。 图片和模型都可以在Tengine项目的页面中找到链接 (Tengine快速上手指南)。

- 下载代码
[openailab@localhost proj]$ git clone https://github.com/OAID/Tengine
# 进入Tengine的目录
cd Tengine
[openailab@localhost Tengine]$ git branch
* tengine-lite当前Tengine的默认分支是Tengine Lite。
2. 编译Tengine Lite
mkdir build
cd build
cmake ..
make -j2注意不要把编译线程数设太大,因为在最后编译MobileNet SSD例子时消耗显存比较多。六分钟多可以编译完。
3. 配置Tengine Lite的Python API编辑/home/openailab/.bashrc,再最后一行后面加入:
export TENGINE_LITE_PATH=/home/openailab/proj/Tengine
export PYTHONPATH=$PYTHONPATH:$TENGINE_LITE_PATH/pytengine
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$TENGINE_LITE_PATH/build/src/环境变量TENGINE_LITE_PATH设置为Tengine Lite的根目录路径, 设置好后重新打开终端。 打开Python, 能成功导入tengine。
[openailab@localhost examples]$ python
Python 3.6.5 (default, Mar 29 2018, 17:45:40)
[GCC 8.0.1 20180317 (Red Hat 8.0.1-0.19)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import tengine4. 将三个文件cat.jpg\, mobilenet.tmfile\, synset\_words.txt放在examples的目录下, 文件结构如下所示:
[openailab@localhost examples]$ pwd
/home/openailab/proj/Tengine/examples
[openailab@localhost examples]$ tree
.
├── cat.jpg
├── classification.py
├── mobilenet.tmfile
├── synset_words.txt5. 运行examples文件夹下的图像分类示例classification.py
[openailab@localhost examples]$ python classification.py
n02123159 tiger cat 8.5975923538208
n02119022 red fox, Vulpes vulpes 7.954988956451416
n02119789 kit fox, Vulpes macrotis 7.867891311645508
n02113023 Pembroke, Pembroke Welsh corgi 7.427407264709473
n02123045 tabby, tabby cat 6.364651679992676由此,就能够在Tengine Lite上正确预测出虎猫啦 : )
下一步工作
对于Tengine Lite Python API,我觉得在API设计方面可以进一步改进。比如把数据预处理, 计算图构建等操作隐藏起来,比如:
image = cv2.imread('./cat.jpg')
model = tg.Model(tm_file)
pred = model(image)这样可以减少出错概率, 一些错误比如忘记对数据做预处理,使用的数据内存分布(NCHW还是NHWC)不正确,忘记调用prerun。
本次Tengine Python API移植任务,大佬做的工作是编写一个Python的图像分类示例,在Tengine上跑通代码,再将pytengine移植(复制)到Tengine Lite上, 再在Tengine Lite上跑通代码,其中还修复了pytengine中的一些bug。
更多Tengine相关内容请关注Tengine-边缘AI推理框架专栏。