介绍
MKLDNN是一个Intel开发的基于Intel芯片架构的开源库,提供用于深度学习的API.
Intel 开源软件中心
MKLDNN Github 主页
教程主页
编译安装mkldnn
|
|
make install之后:
如果第二步没有指定目录,会安装在/usr/local目录下面
Shared libraries (/usr/local/lib):
头文件:
Header files (/usr/local/include):
文档:
Documentation (/usr/local/share/doc/mkldnn):
编译链接代码:
入门文档
这个文档内容有点问题:
具体编译的命令看github的readme:https://github.com/01org/mkl-dnn
首先配置环境变量
编译完的运行:
报错找不到mkldnn的动态链接库
添加动态链接库
Simple_Net Code API 代码的解释
https://software.intel.com/en-us/articles/intel-mkl-dnn-part-2-sample-code-build-and-walkthrough
调试simple_net.cpp代码
看一个primitive的例子:
step1:
line:117
convolution_forward(conv1_prim_desc, conv1_src_memory,
conv1_weights_memory, user_bias_memory,
conv1_dst_memory)
convolution_forward就是primitive的名字
step2:
看convolution_forward的构造函数调用了mkldnn_primitive_create
step3:
mkldnn_primitive_create的构造函数:
调用了primitive_desc->create_primitive
step4:
看primitive_desc->create_primitive
这个是个虚函数,需要看具体的primitive_desc子类的实现
用gdb去看看到底调用哪个实现函数
debug
单步调试n,遇到return primitive_desc->create_primitive(primitive, inputs, outputs); s:step in这个实现
看到create_primitive函数创建memory的时候调用了
|
|
reoder这个op对应了
|
|
conv这个op
对应调用到了/home/lesliefang/mkldnn/mkl-dnn-0.17.2/src/cpu/
jit_avx512_common_convolution.hpp:46
进一步看generate方法:
jcp变量
jcp是一个jit_conv_conf_t类型的成员变量
在jit_avx512_common_conv_fwdkernel初始化成员列表里面做初始化jcp(ajcp)
ajcp是传进来的参数conf.jcp
conf是jit_avx512_common_convolution_fwd_t的成员变量pdt conf;
在jit_avx512_common_convolution_fwdt初始化成员函数列表里面构造conf(*pd)
generate函数开始从jcp里面读出输入输出变量的维度
计算会调用compute_loop函数,compute_loop根据jcp.ver类型选择调用compute_loop_vnni或者compute_loop_4fma
多线程计算的实现
需要链接openMP去实现
step1:
全局搜
应该是封装在了这个parallel函数里面
一般用openMP的线程不用tbb的线程
step2:
在src目录下面搜索parallel的函数调用
是在execute函数里面调用的,所以并行化是在stream提交了之后,调用kernel的execute的函数的时候执行的
继续以simple_net.cpp代码中的jit_avx512_common_convolution.hpp:46的jit_avx512_common_convolution_fwd_t作为例子
stream提交之后会调用jit_avx512_common_convolution_fwd_t中的execute函数,如果2D卷积的话调用execute_forward_2d函数
这个函数里面调用了parallel函数去做多线程运算
parallel函数的第二个参数就是每个线程要执行的函数,一般就是个lamba匿名函数
匿名函数里面通过jit_conv_ker_pipeline_owthr去调用kernel->jit_ker执行运算
kernel_变量在_jit_avx512_common_convolution_fwdt构造函数里面实现
kernel = new jit_avx512_common_conv_fwdkernel(conf.jcp,
*conf.attr());
jit_ker在jit_avx512_common_conv_fwd_kernel的构造函数里面通过jit_ker = (void (*)(jit_conv_call_s *))getCode();实现
MKLDNN的primitive如何选择对应的kernel
在调用privimite->create_primitivate的时候会去遍历所有的kernel,在每个kernel的函数中都有一个init_conf的函数,在遍历所有的kernel的时候,会去看这个init_conf的函数是否满足条件,满足条件就意味着调用这个kernel,否则的话就看下个kernel
以_jit_avx512_common_convolution_fwd_t为例子
是在构造primitive desc的时候就选择好了使用哪个kernel
step1:
在simple_net.cpp line:93
auto conv1_prim_desc
= convolution_forward::primitive_desc(conv1_desc, cpu_engine);
创建primitive desc
step2:
convolution_forward::primitive_desc构造函数中调用mkldnn::primitive_desc
step3:
mkldnn::primitive_desc里面调用mkldnn_primitive_desc_iterator_create_v2
step4:
mkldnn_primitive_desc_iterator_create_v2函数里面创建primitive_desc_iterator_t对象
这个函数比较难懂,记录下自己的理解
- new primitive_desc_iterator_t的时候返回一个iterator,同时lastidx遍历impllist所有op直到末尾,用于end()成员方法的调用。
impllist 通过engine_->get_implementationlist() 在构造函数的初始化列表里面生成
engine对应的cpu_engine 看get_implementation_list方法
是cpu_engine.cpp文件里面的函数,直接返回了cpu_impl_list变量
cpu_impl_list数组里面,每个元素都调用instance 宏创建了一个模板函数指针
istance宏其实是根据输入类型创建了mkldnn_primitive_desc::create这个模板函数指针(在primitive_desc.hpp文件里面)
在后面调用++it的时候,这段代码(auto s = impllistidx_;)其实就是调用了这个实例化mkldnn_primitive_desc::create这个模板函数指针
mkldnn_primitive_desc::create函数里面会去尝试创建对应的primitive description
mkldnn_primitive_desc::create方法调用失败返回unimplemented
调用成功,把创建的pd赋值给传入的参数,同时返回success
同时++it函数里面也会去判断是否success
Ps.
source insight里面搜索primitive_desc_iterator_t符号
using primitive_desc_iterator_t = mkldnn_primitive_desc_iterator;
impllist包含了所有kernel的实现
- ++(*it); primitive_desc_iterator_t里面对++运算符进行了重载,在++重载的运算符中遍历尝试根据输入的类型去创建primitive desc(auto s = impllistidx_;),创建失败就尝试下一个primitive desc直到成功
- if (*it == it->end()) 判断一下是不是创建成功退出的
- *iterator = it; 创建成功的话,直接把it赋值给iterator返回
通过以上步骤就找到了对应要调用的primitive_desc
后面在创建primitive的时候调用primitive_desc->create_primitive,就是对应pd的create_primitive函数
通过下面对PD的分析可以知道这里调用到的就是pd里面DECLARE_COMMON_PD_T里面的create_primitive方法
get_implementation_list在哪里实例化list里面的kernel
这个engine_是传进来构造的
看cpu_engine.cpp文件中get_implementation_list的实现line:331
这里的cpu_impl_list是个列表每个成员都是pd_create_f对象
#define INSTANCE(…) &primitive_desc_t::create<__va_args__::pd_t>
调用了primitive_desc_t::create<__va_args__::pd_t>创建了每个primitive desc的类型放在cpu_impl_list里面
using primitive_desc_t = mkldnn_primitive_desc;
primitive_desc_t就是mkldnn_primitive_desc
mkldnn_primitive_desc
mkldnn_primitive_desc有create方法,传进来的参数是VA_ARGS::pd_t
比如_jit_avx512_common_convolution_fwd_t就是这个op的pd_t成员
看mkldnn_primitive_desc有create方法,里面调用了_pd->init(),就是_jit_avx512_common_convolution_fwd_t的pd_t成员的init方法
MKLDNN框架介绍
Caffe调用MKLDNN
可以我的另外一篇博客参考:Caffe中的生产者模式
- caffe的layer_factory会去创建mkldnn的层
- caffe在调用layer.forward函数的时候会调用到对应mkldnn的层的forward_cpu函数
- 在这个函数中(initxxxpd)的方法会先去判断对应的pd(privimite descriptive)和privimite是否存在,如果不存在的话会去创建对应的pd
- 在initxxxpd方法中会去调用reset的方法去创建pd(privimite descriptive)和privimite
- reset方法的传入的参数就是new出来的一个新的mkldnn的primitive
进入mkldnn - 在new一个新的mkldnn的primitive的时候,可以看到create后缀或者init后缀的函数
- 在这些create函数中最后会去调用一个privimite->create_primitivate的函数,这是个虚函数
- 这个虚函数和具体的运算kernel之间如何调用实现的,暂时不是很清楚,应该有复杂的调用关系,但是核心的思想是:在调用privimite->create_primitivate的时候会去遍历所有的kernel,在每个kernel的函数中都有一个init_conf的函数,在遍历所有的kernel的时候,会去看这个init_conf的函数是否满足条件,满足条件就意味着调用这个kernel,否则的话就看下个kernel
MKLDNN with VTune
https://github.com/intel/mkl-dnn/blob/master/doc/perf_profile.md#intelr-vtunetm-profiling
kernel的名字
igemm_s8u8s32:blas
s8u8s32: s8表示输入的数据类型,u8表示权重的数据类型
术语:https://github.com/intel/mkl-dnn/tree/master/tests/benchdnn
运行benchdnn
https://github.com/intel/mkl-dnn/tree/master/tests/benchdnn
编译完mkldnn在build目录下面
环境变量
vebose
export MKLDNN_VERBOSE=1
https://intel.github.io/mkl-dnn/perf_profile.html
Dumping JIT-kernels
export MKLDNN_JIT_DUMP=1
https://intel.github.io/mkl-dnn/perf_profile.html
VERBOSE如何工作
环境变量设置了VERBOSE=1的话mkldnn_verbose()->level的值就是1
看这个文件cpu_engine_t.cpp
在stream在submit的时候,如果设置了verbose就会打印出来
p->pd()->info()就是这个primitive对应的pd的信息
p是mkldnn_primitive类型的
primitive.hpp文件定义了这个类的成员和函数
p->pd()
p->kind()
根据MKLDNN_VERBOSE输出内容查看对应调用到的primitive和kernel
设置verbose=1之后看到日志输入
关键字: jit:avx512_common
mkldnn找到这两个文件
jit_avx512_common_convolution.hpp
jit_avx512_common_convolution.cpp
hpp文件里面订了了这个primitive的类,里面pd_t的结构体就是这个promitive的desciptive
调用pd->create_primitive的时候就调用了这个pd_t结构体里面的
这个内联函数,里面创建了这个primitive的实例
看这个primitive的构造函数
关键:
这里引出了两个kernel的文件:
jit_avx512_common_conv_kernel.cpp
jit_avx512_common_conv_kernel.hpp
看jit_avx512_common_conv_fwd_kernel的构造函数
generate();函数就是去生成汇编代码
jit_ker = (void (*)(jit_conv_call_s *))getCode();函数就是把汇编代码放到jit_ker这个kernel对象的成员变量里面(这个kernel对象又是这个primitive对象的成员变量)
getcode函数也会去dump bin文件(如果设置了环境变量:export MKLDNN_JIT_DUMP=1)
stream submit之后发生了什么
stream->submit(pd)
之后调用了jit_avx512_common_convolution.hpp里面的exectue函数
exectue函数根据条件选择jit_avx512_commonconvolution.cpp里面具体的exectue的实现
具体的exectue的实现里面,调用了parallel去做并行化的计算,每个线程里面调用kernel->jit_ker(jit_ker按照上一节的解释就是对应的汇编代码)