最近因为项目需要,对intel openVINO的源码进行了解,以便为后面移植开发做准备。
OpenVINO的源码在opencv的github主页上可以找到,最新的opencv 4.1.2已经全新支持了OpenVINO,意味着一个新的平台即将展开,并且在嵌入式领域,边缘计算等场景,movidius大有赶超英伟达的趋势,因特尔在AI上的发力今后几年不可小看,很有可能在今后几年内占有一席之地。
废话不多说,OpenVINO的github源码地址为:https://github.com/opencv/dldt
由于openvino是18年刚刚出来,目前针对openvino源码分析还没有官方渠道,因特尔开发openvino工具包本意是将各种硬件平台进行封装操作,达到用户拿来就可以直接用,一般不需要注意里面细节,因为后面项目需要做移植,所以需要对openvino源码进行一定了解。
core是interference Engine的管理核心,一般使用openvino之前都需要先创建一个core,负责其中的设备管理,网络下载等等初始化造成,是整个interference Engine的核心。
core类头文件位于\inference-engine\include\ie_core.hpp
cpp文件位于:inference-engine\src\inference_engine\ie_core.cpp
core构造函数是整个openvino的程序的入口,主要功能是负责openvino中所支持的硬件设备类型注册(主要包括CPU,GPU, Modividus,VPU等),源码如下:
主要分三步:
OpenVINO源码中大量使用了将具体实施的类和接口分类的模式,core类只是定义了一个接口类,类core类的具体实施的类是在Impl类中,首先保存其Imp类的地址到_impl变量中,方便以后调用。这样设计的好处就是能够将接口与实现分类,减少模块之间的耦合,Imple类的实现还是在inference-engine\src\inference_engine\ie_core.cpp文件中。
涉及到一个OpenVINO的编码规范,在类中的变量命名规则为在变量名前面要加"_",这一点和caffe变量命名规则不一样,请注意。
Core类的构造函数可以带一个xmlConfigFile配置文件参数,该配置文件就是openvino支持的设备类型配置xml文件,如果该参数没有配置(大部分情况下都是不配置的,使用默认配置),使用默认配置文件plugins.xml,该配置文件一般都是在安装的openvino的lib路径下,是和libinference_engine.so等lib放在同一个路径下面,如果不放在同一路径下面将会出错
下面是官方安装之后的plugins.xml文件配置,和安装时选择的支持设备类型有关:
name为支持的设备类型名称,有GPU,CPU,MYRIAD等,location代表的时支持该设备所对应的lib,
各个设备所对应的lib可以查看《学习OpenVINO笔记之Inference Engine》
代码中是怎样找到这些路径的?接下来需要查看getIELibraryPath()函数实现
linux操作系统下 调用一个系统函数dladdr(), 用来获取动态库中getIELibraryPath函数的相关信息,其信息结构为Dl_info,其成员结构为:
struct {
const char *dli_fname;
void *dli_fbase;
const char *dli_sname;
void *dli_saddr;
size_t dli_size; /* ELF only */
int dli_bind; /* ELF only */
int dli_type;
};
其中dli_fname为该函数符号位于的lib名称(包含其路径信息),而 getIELibraryPath函数是位于libinference_engine.so中,获取的dli_fname为libinference_engine.so名称及路径,这样通过getPathName解析出路径名(libGNAPlugin.so这些lib都是以及plugins.xml等文件和libinference_engine.so放在一个路径),这样就可以获取到plugins.xml文件路径名,这是一个实现的小技巧
最后将获取到的路径名和plugins.xml拼接成配置文件名,进行下步处理。
接下来就是调用RegisterPlugins()函数读取xml中的信息,将其注册进去,
可以看到最后是调用的Imp类RegisterPluginsInRegistry接口
RegisterPluginsInRegistry接口代码分步骤来看:
OpenVINO解析xml文件使用的是开源第三方软件pugixml,调用load_file()加载并解析xml配置文件到xmlDoc,并检查其返回值res.status,是否成功,如果失败则直接扔出异常打断言。
读取成功之后,就要获取xml配置文件:
解析xml里面的节点,上述代码不在一一解释,每个设备类型挂载一个
上述xml文件格式,官方有明确解释:https://docs.openvinotoolkit.org/latest/classInferenceEngine_1_1Core.html
name为所支持的设备类型,location为所对应的设备类型的lib. Properties为所对应的该设备类型的一些属性,Externsion为该设备类型的外挂第三方的一些库。
Core的构造函数比较简单,本质上就是将plugin.xml文件的所支持的设备给注册上去,但是真是的环境中是否有这个设备目前并没有涉及到。
SetConfig()函数为设置设备的某些属性值,该API第一个参数为设置的属性及值,其类型为一个map, std::map
首先解析其入参deviceName_,是否带有device id,如有带id其格式为:
<.>
接下来调用Impl类的SetConfigForPlugins()函数
将设置的属性保存到所对应的pluginRegistry和plugins中,其中pluginRegistry为所支持的所有设备类型,而plugins是真正运行所创建的plugins。
支持config配置项可以从ie_plugin_config.h头文件可以看到,官方链接:https://docs.openvinotoolkit.org/latest/ie__plugin__config_8hpp.html
loadNetwork是整个OpenVINO的实施的关键,负责将IR下载到movidius中,整个loadNetwork的流程调用比较复杂,里面涉及了很多C++中的编程技巧。首先来看下入口源码:
入口函数比较简单,就是先对特殊的设备类型进行检查,如何deviceName是“HETERO:"则将冒号去掉,解析出deviceName中的device name和dedevice id,再次看到OpenVINO的另外一个编程规范,局部变量后面要加上"_".
最后会调用Imple类的GetCPPPluginByName,获取到相应的设备类型创建的Plugin,最后调用LoadNetwork下载网络,再次看到比较重要的一个概念Plugin,OpenVINO为每个设备类型创建相对应的Plugin,将设备抽象为具体的Plugin进行处理,这是OpenVINO的一个管理核心
下一步比较关键是LoadNetwork中实施比较关键的一步,调用过程比较繁琐:
将上述过程安装六个步骤分析
再次看到了plugins这个东东,主要是记录以及创建的plugin.如果已经创建则不再创建。
加载相对应的设备类型的动态lib,并获取lib中的CreatePluginEngine函数接口地址,这么复杂的操作,被完全藏在了SOPointer的构造函数中,InferenceEnginePluginPtr定义如下:
最后是调用SOPointer的构造函数:
该函数仅有短短几句话,但是包含的信息比较多。
首先调用创建一个Loader类:
Loader类在此主要指的是SharedObjectLoader类,该类主要是加载一个共享库
猜的没错,调用dlopen加载打开一个共享库,并返回其句柄指针。
将共享库的句柄指针保存在_so_loader中。
接着查找该共享库的CreatePluginEngine函数地址:
_pointedObj(details::shared_from_irelease(
SymbolLoader(_so_loader).template instantiateSymbol(SOCreatorTrait::name)
SOCreatorTrait
为 CreatePluginEngine函数名, instantiateSymbol()函数定义如下:
最后调用bind_function()函数:
获取到CreatePluginEngine在动态库的地址。
第二步骤的主要作用就是从对应的设备so中找到CreatePluginEngine()函数地址,由此可以看到每个设备对应的so都有一个CreatePluginEngine函数,该函数是其对应的主要入口,具体每个设备对应的相应的处理函数后面再详细描述。
根据获取到的CreatePluginEngine()函数地址,运行该函数生成一个相对应InferencePlugin, Plugin是整个OpenVINO的管理核心。
获取到相对应InferencePlugin中的主要API, IInferencePluginAPI该API是一个通用的API,是实际设备具体的执行
IInferencePluginAPI类接口
API名称 | 作用 |
virtual void SetName(const std::string & pluginName) | 设置Plugin name |
virtual std::string GetName() | 获取Plugin name |
virtual void SetCore(ICore *core) | 设置Plugin中的Core |
virtual const ICore& GetCore() | 获取Plugin中的Core |
virtual Parameter GetConfig(const std::string& name, const std::map |
获取Plugin中的配置 |
virtual Parameter GetMetric(const std::string& name, const std::map |
获取Plugin中的Metric |
该类中的函数都是纯虚函数,具体的实现都是在各自的so中实现。
根据获取到的API,设置相对应的配置,如Core、Extension之类的,并生成一个新的InferencePlugin类
将该plugin对应的InferencePlugin类保存在plugins中。
最后将获取到InferencePlugin中调用LoadNetwork加载网络。
遗留两个问题需要待下一步进行剥洋葱
1:Myriad的CreatePluginEngine()实现
2:LoadNetwork()具体实现