上节课我们学习了如何进行模型的训练, 这节课以及下一节课, 我们将会指导大家将训练出来的模型放在我们的ai开发板上面运行, 并亲眼看到实际运行的效果. 这里就需要使用我们为大家提供的嵌入式代码. 因此这一节课, 我将会带领大家熟悉一下我们的嵌入式代码发布包, 希望对大家了解和使用地平线的bpu进行智能应用的开发有所帮助.
1. 环境构建
1.1 基础说明
我们提供了3个发布包,用以帮助用户理解如何使用以及验证我们的api,这3个发布包分别为:
embedded_release_sdk的目录结构,和本文档结构对应,这里简单介绍一下各大章节的内容:
2_sdk_program_guide: 主要为api使用教学示例,从最简单的用api运行一个模型推理(2.2),到api的使用场景介绍(2.3),再到将api组成一个简单的应用,用于验证和展示模型的使用效果(2.4)
3_app_zoo: 展示如何基于地平线的应用程序框架,组织一个实际的应用程序 (3.0)
4_tools: 提供了已经编译好的应用程序,以及各种测试脚本,用来测试多种模型在地平线bpu上运行的功能,性能,精度等 (4.0)
1.2 源码编译
1.2.1 真机编译
要编译可在地平线芯片上运行的程序,需要预先安装对应的交叉编译工具链:gcc-linaro-6.5.0-2018.12-x86_64_aarch64-linux-gnu
在embedded_release_sdk目录下,按照如下操作,进行编译
1.2.2 模拟器编译
我们同样提供了模拟器,用于给手上没有实际硬件的用户,在模拟器上通过软件模拟硬件的方式,来适应和学习地平线芯片的特性以及API的使用。
模拟器编译,需要的开发环境为centos, 编译器版本为 gcc 4.8.5和g++ 4.8.5
编译方式如下:
1.3 运行测试
在前面的编译过程中,我们已经通过make install, 将编译出来的可执行程序,以及依赖库,都安装到4_tools目录下面。
用户可以通过将整个4_tools,embedded_data_zoo,embedded_model_zoo3个包都上传到运行环境下。
将embedded_data_zoo通过软链接的方式,命名为model_zoo放在4_tools目录下,
同时在各个脚本执行目录,将embedded_data_zoo通过软件连接的方式,放在脚本执行目录下。
修改脚本目录下的base_config中的平台,然后通过各个start脚本,即可运行各个模型的例子
修改如下内容:

2. SDK 使用指南
2.1 整体说明
bpu_predict封装了关于bpu操作的底层接口,以简单易用的接口形式供用户调度。并实现了高并行度的调度策略,可以帮助用户快速集成基于bpu的模型集成,并达到较好的性效果。

2.2 基本流程
如2.2_run_mobileNet_224_224_from_nv12中的代码所示,一个典型的sdk使用流程,包含将模型加载到bpu,准备输入数据,调用接口运行模型进行推理,获取推理结果进行解析。其代码流程大致如下:
接下来的章节,将会详细介绍流程中各个步骤的接口的含义,以及如何使用。
2.3 接口说明
2.3.1 IO接口
首先要对bpu-predict库的io部分进行介绍,这些io接口支持将camera输入或者本地图像导入bpu 内存中,以便后续模型在bpu上的前向推理,接口可分为三类,pyramid,fakeimage以及feedback,其中pyramid是基于camera输入的场景,而fakeimage和feedback则是本地图片输入的场景。
2.3.1.1 pyramid
在使用pyramid相关接口之前,需要先创建对应的handle。
接着获取金字塔的结果,如果还没有图像进来,接口就会阻塞,并等待金字塔处理完成。
获取当前图像帧的frame id。
获取当前图像帧的时间戳。
除此之外,还可以直接获取金字塔每一层处理结果的图像指针,图像为nv12类型。
pyramid结果用完之后要记得释放,否则会导致内存泄漏问题。
程序最后也要释放pyramd handle占用的资源。
2.3.1.2 fakeimage
首先要创建一个BPUFakeImageHandle的对象,这里输入参数宽和高对应着图像的尺寸,也就是说一个handle只能对应一种图像尺寸。
接下来需要输入图像数据,并获得对应的BPUFakeImage类型的对象,fakeimage仅支持nv12类型的图像输入。
在模型跑完之后,需要将获取的fakeimage释放,由于释放fakeimage时会间接释放内部申请的bpu内存,因此如果不及时释放,会导致内存泄漏问题。
结束时也要记得释放BPUFakeImageHandle。
2.3.1.3 feedback
2.3.1.2 的fakeimage类型虽然支持了任意尺寸的图像,但是相较于2.3.1.1节的pyramid类型又缺少了对输入图像的金字塔处理,多尺度图像输入在特殊场合中会有重要的作用,因此为了弥补这一缺点,我们提供了另一种feedback类型作为图像输入。Feedback相关接口通过软件调用底层硬件实现了对图像的金字塔处理,并且相较于基于camera输入的pyramid类型的尺寸单一,Feedback类型也可支持任意尺寸,仅有的约束是高是4的倍数,宽是16的倍数。
首先创建BPUFeedbackHandle的对象。
将nv12类型的输入图像回灌金字塔硬件中,并获取金字塔处理结果。
这里输入必须是nv12类型的图像,并且会对数据的大小和handle对应的尺寸做检查,同时由于生成的金字塔处理结果也是BPUPyramidBuffer类型,因此基于BPUPyramidBuffer类型获取信息的相关接口,这里也都能适用,具体参考2.3.1.1小节,这里不再赘述。
在模型跑完之后需要将回灌结果释放掉,否则会造成内存泄露的问题。
程序最后也要释放BPUFeedbackHandle
2.3.2 基础接口
首先是模型文件加载,运行模型之前必须先将模型文件加载到bpu内存中,通过调用BPU_loadModel接口加载并创建一个BPUHandle类型的句柄,这个句柄用于后续获取各种模型信息。
代码中通过函数LoadBpuModel封装了加载的过程,可以看到调用后将接口BPU_loadModel的返回值与BPU_OK(BPU_OK属于错误码的枚举类型)进行比较,当返回值不等于BPU_OK时表示模型加载失败,失败后可通过调用BPU_getErrorName来获取返回的错误码对应的信息来协助代码的debug。
模型加载成功之后,可以获取bpu-predict库的版本信息,接口输入的参数是加载接口创建好的handle句柄,返回的指针指向了携带版本信息的字符串,当调用失败后会返回空指针。
由于一个模型文件中可能包含一个或多个模型,可以通过获取模型文件中的模型名称列表及相应的模型个数来确定需要的模型是否存在,并且后续很多接口也需要指定模型名字来对相应的模型进行操作。
为了能够执行模型并解析输出结果,我们还需要掌握模型输入输出的有关信息,以便能做好相应的准备,BPU_getModelInputInfo和GetModelOutputInfo可分别用于获取模型的输入输出信息,这两个接口参数和调用方法类似,其中输入参数model_name指定模型文件中的模型,而输出参数BPUModelInfo结构体则存储着模型输入输出的节点信息。
对于BPU_getModelInputInfo返回的输入节点的结构体中,num表示输入节点的个数,valid_shape_array表示若干个输入节点的有效shape拼接成的一维数组,dtype_array表示输入节点的数据类型(单字节还是四字节),通过这些信息我们就可以正确的准备模型的输入了。
对于BPU_getModelOutputInfo返回的输出节点的结构体中, num表示输出节点的个数,size_array表示输出节点的对齐内存大小,这两个字段可以计算出模型输出需要占据的总内存,当后续执行模型需要为输出申请内存时会用到;而dtype_array可以确定输出的数据类型是int8还是int32,is_big_endian可以判断大小端读取int32类型数据的方式,shift_value是定点结果的移位值,aligned_shape_array和valid_shape_array分别是输出结果的对齐尺寸和有效尺寸,这些字段可以帮助用户解析模型的输出结果,完成模型的后处理部分。
当然在程序的最后,我们也需要把模型文件卸载,可以调用BPU_release来释放模型文件占用的资源。
2.3.3 内存管理
首先基于申请的内存地址来创建buffer。
创建输入输出的buffer数量可以从2.3.1节的BPU_getModelInputInfo和BPU_getModelOutputInfo返回的BPUModelInfo结构体中得到。
当然也可以访问buffer中的数据,例如在解析模型结果时,需要从BPU_Buffer_Handle中返回模型输出的首地址以及内存大小。
最后记得要释放申请的buffer。
empty buffer是专门为了模型输出准备的,在我们的芯片中bpu和cpu的内存是独立的,但是当前的bernoulli和bernoulli2架构中,bpu的内存可以直接被cpu访问,因此我们不需要在外部为输出申请内存,然后将bpu的处理结果导入,而是直接将bpu内存的地址返回,这样会大大减少消耗的时间。
可以看到创建emptybuffer的接口没有任何输入参数,更加的方便,并且输出也是BPU_Buffer_Handle类型,因此获取结果的指针和大小的方法与之前一致,因此当准备输出的buffer时更推荐emptybuffer这一种方式。
2.3.4 模型执行
bpu-predict库中提供了六种模型执行的接口,这些接口都是异步调用方式,为了方便介绍可按照模型输入来源划分成三类,这里的模型输入来源不是指软件的变量类型,而是模型的编译选项,这些模型执行接口是与编译选项绑定的。不管是tf模型还是mxnet模型都需要通过编译指令转换为hbm模型文件,才可以在bpu中执行,而编译指令中有一个-i的编译选项,可提供三种模型输入参数,分别是pyramid,ddr以及reiszer。
2.3.4.1编译选项–i pyramid
选项-i pyramid输入是专门支持nv12类型的图像输入,当模型执行时,硬件会先将nv12图像转换为yuv444图像,然后接着执行前向推理。基于这类编译选项,可调用的软件执行接口有BPU_runModelFromPyramid,BPU_runModelFromImage以及BPU_runModelCropPyramid。
a) BPU_ runModelFromPyramid
首先介绍接口BPU_runModelFromPyramid,这个接口使用金字塔结果BPUPyramidBuffer类型作为输入(BPUPyramidBuffer可参见2.3.1.1小节),使用BPU_Buffer_Handle存储模型输出地址(可参见2.3.3小节)。
这是个异步执行接口,会将执行任务添加至任务队列。该任务将由后台 engine 执行。所以当该接口返回,并不意味着模型执行已经完成。当返回时,会设置 BPUModelHandle变量,这个变量用于调用 BPU_getModelOutput接口,等待模型执行完成,获取执行结果。
当解析完结果后,也需要释放当前的BPUModelHandle变量所占用的资源,如果申请的output buffer是empty buffer类型,那么释放后buffer指向的地址不能再被访问。
后续的执行接口也都是异步调用,流程与上述一致。
b) BPU_runModelCropPyramid
BPU_runModelCropPyramid接口与BPU_runModelFromPyramid类似,也是接收金字塔处理结果作为输入,但不同的是BPU_runModelFromPyramid接口是选取某一层图像作为输入,而BPU_runModelCropPyramid是在某一层图像的基础上裁剪出一个roi作为输入。
注意:调用这个接口时,还需要一个模型编译选项-pyramid-stride的帮助,用于图像宽度的步进。通常模型的输入尺寸需要和图像尺寸一致,而编译选项-pyramid-stride会默认为模型宽度,但是在这里模型的输入尺寸不能大于图像,这样才能裁剪出一个roi,因此编译hbm文件时要指定-pyramid-stride与所选的金字塔图像层的宽度一致,而不是默认为模型宽度。
c) BPU_runModelFromImage
BPU_runModelFromImage接口除了使用BPUFakeImage类型作为输入,其余用法与上述一致,BPUFakeImage类型可参见2.3.1.2节。
2.3.4.2编译选项–i ddr
a) BPU_runModelFromDDR
需要BPU_Buffer_Handle类型作为输入输出,注意的是由于底层硬件限制,输入数据必须是对齐的,而bpu不会对ddr类型的输入做任何预处理,因此必须软件自己做这部分工作,即对nhwc的每一维度都加padding进行对齐。
对齐的shape大小可以通过接口BPU_getModelInputInfo获得
输入与输出的BPU_Buffer_Handle类型的创建可参考2.3.2节,然后就可以调用执行接口了。
b) BPU_runModelFromDDRWithConvertLayout
相较于BPU_runModelFromDDR需要用户自己对输入数据加padding对齐,接口BPU_runModelFromDDRWithConvertLayout则将这部分操作集成到了接口内部,方便的用户的使用,用户只需要将输入数据直接送入bpu进行运算即可。
2.3.4.3 编译选项-i resizer
BPU_runModelFromResizer封装了resize和模型执行的操作,更方便于用户的使用。
注意:由于BPU_runModelFromResizer接口可能会做多次模型运算,因此需要申请足够多的output buffer,数量应该是模型输出的节点个数与roi数量的乘积,并且部分的roi可能会失败,没有输出,那么成功跑模型的roi的结果会顺序存储在buffer中,剩余的buffer里没有结果,例如模型输出节点为n,输入的roi数量为a,能被resize的roi数量为b,则申请output buffer的数量应该为(n*a),执行结果会存在前(n*b)个buffer中,后(n*(a-b))个buffer没有实际结果。
2.3.5 多模型运行的资源分配控制
1) 在bpu_config配置中将"engine_type"字段改为"group"类型, 例如:
2) 创建并将相应的模型添加到对应的组内, 并给该组分配对应的资源占用百分比
设置返回值为0则表示设置成功, 在一定时间内, 该组模型在BPU上运行的时间片占比不会超过相应的比例数值.
2.4 典型示例
本章节,将前面介绍的bpu_predict的接口串联起来,形成一些完整的应用示例。通过这些应用示例,我们可以学习了解如果用我们的api,来实现一个完整的应用,同时可以通过这些示例,来验证地平线芯片在各种公开测试集上,公版模型的性能。
2.4.1 代码结构
从如上结构可以看出,我们将整个工程分为了input, output 以及postprocess3部分,sample中我们提供了3个典型的示例,用来展示如何在不同场景下,进行流程搭建。
2.4.1.1 Input
input实现了bpu_io相关的数据输入功能,从外部(camera, network,静态图)读取图片,并放入到bpu的内存中(FakeImage, Pyramid),其基本结构如下:

我们通过input_type,来决定使用哪种数据输入,同时每种输入的配置,在init接口中进行解析。每次调用NextFrameData, 则可以获取一帧图像。isFinish用来判断,当前数据源图像读取是否已经结束。
2.4.1.2 PostProcess
postprocess封装了模型执行后的结果的解析,不同结构的模型,有不同的解析方式。大部分模型都是按照公版模型通用的解析方式进行处理,部分模型地平线芯片做有特殊优化处理,具体在2.4.2章节介绍

我们可以通过GetIns接口,通过模型名称,来获取到对应的模型处理实例,不同模型解析,需要不同的配置文件,通过Init函数,将配置文件传入实例中进行初始化,最后就可以通过Periodic接口,来进行各个模型推理后数据的解析
2.4.1.3 output
output封装了模型推理结果以及原始视频数据的处理,支持通过网络传输到client端进行展示,在本地渲染录制成视频或者jpg图片,也支持将模型推理结果按照指定结构记录为精度记录文件,已进行精度分析(4.2章节)

用户可以通过GetIns,来获取output的实体,而在Init的配置文件中,可以配置打开哪种能力,用户可以打开1种或者N种output。目前支持network发送至client端,写精度日志,写video,以及写jpeg文件四种output能力
2.4.1.4 sample
sample中我们实现了3个典型的示例,在main函数代码中,包含了完整调用bpu_predict进行数据准备,模型推理,结果解析的完整流程。
2.4.2 特殊后处理
在2.4.1.2中,我们描述了我们后处理代码的组织方式,其中大部分后处理,都是通用的公版模型的后处理解析方式,这里特别说明其中几个特殊的后处理
2.4.2.1 fasterrcnn & maskrcnn
在fasterrcnn和maskrcnn中,我们经过内部优化,可以直接在BPU中进行检测框的解析,直接调用接口,即可解析出具体的检测框,而不需要在后处理中,再做复杂的计算:
2.4.2.2 yolo & ssd & s3fd
在这些后处理中,我们使用exptable来替代exp指数计算,用查表的方式代替计算过程,以提高计算性能。具体exptable的值,可以在src/utils/exptable.cc中看到
2.4.2.3 parsing
parsing模型的输出已经做了像素类别间argmax的处理,所以可以直接读取图像分割的结果。
3. 应用开发
这部分还在开发当中, 我们将会在不久之后对该部分内容进行更多的补充
3.1 框架说明
简单介绍应用框架,具体链接框架介绍页面
3.2 工程迁入框架
将2.4 典型示例进行封装,告诉用户如何从裸工程,迁移到应用框架
3.3 典型demo
基于场景的典型demo,如识别机,车辆视频结构化代码结构说明
4. 模型支持
如我们在第1章节所写,我们将整个example源码,编译打包后,存放入了4_tools目录,并在改目录预置了大量脚本,用以帮助用户快速验证我们芯片和模型的能力
4.1 功能测试
在demo目录中,我们提供了部分公版模型的在各种典型输入下的执行脚本,支持列表如下:

在启动其中某一个示例时,可以通过配置client的IP地址,实时查看该部分示例实时展示效果
4.2 精度测试
在accuracy目录中,我们提供了2个子目录。
shell目录为各个模型在精度测试模式下的启动脚本
python目录包含程序完成后,进行精度计算的脚本,以及发送图片数据,到精度测试进行的send_tools
由于精度测试时,数据集较大,一般的测试步骤为:
4.3 性能测试
在performance目录中,我们提供了大量脚本,来进行各个模型的性能测试。由于目前示例中,都是以单帧同步的方式在执行测试,所以当前性能数据,是根据单帧执行时间,计算整体性能
性能测试的一般步骤为:
我们在了解了代码结构之后, 有助于我们根据自身需求, 进行相应的个性化改动及验证, 在对代码整体结构有了大致了解后, 我们下面将会教大家如何将之前我们得到的模型文件上板运行, 敬请期待.
