Open ysh329 opened 4 years ago
答:plite在模型转换过程(非运行期,离线)会做op大粒度基于用户偏好Place(设备,精度,数据排布)的lite kernel选择。模型和设备绑定。这也是模型转换工具opt所做的事情。
但细粒度如conv3x3s2p1具体要执行的kernel,会在运行期lite kernel第一次执行的时候基于op具体信息做选择。此外,如果是动态shape,即当前层op这次输入与下次不一样,也会做这个具体kernel的重新选择。
以opencl为例,选择具体要执行的cl kernel的时候,也是lite kernel里做,像是lws等与硬件相关的信息(和性能直接相关)也会在这个地方确定。要是想做针对opencl这一个backend的模型自动化调优需要在lite kernel这个粒度来做。而且也仅限当前kernel这个Place,前面我们说过place包含三个信息target/precision/layout,对于opencl有两种layout,kNCHW和kImageDefault,对应cl::Buffer和cl::Image2D。但前面说了lite kernel的layout已经在静态选kernel时确定了,一次只能调一种。
答:先说一下目前plite还不支持基于试跑的最佳kernel搜索。拿opencl来说,因为模型优化过程(opt)的static_pick_kernel,与运行时具体cl kernel,有这两个不同粒度的kernel选择阶段。
虽然目前plite不支持kernel的搜索,不过可以想一下。如果要针对backend来做最佳性能的kernel搜索的话,就要做试跑,可能有2种方式:
两个阶段打通,在opt阶段的本文所说的static_pick_kernel的过程中,与具体的kernel选择绑定,即在这个过程也能拿到conv的kernel size,input shape等信息。但这样虽然两个阶段的kernel选择打通,但是二阶段的具体kernel判断需要再写一遍,维护上有一定成本;
两阶段分开做kernel选择,即每个阶段相对于局部的最优,从而达到相对全局的(次)最优。Ps:本来我这里写了一堆,但是想了想意义不大。其实我们的目的是找一个模型在所有不同target/precision/layout的kernel实现上排列组合这个模型下的最佳性能。但其实本质上static_pick_kernel已经考虑了backend不同带来的差异,端侧对性能的极致要求,可能不同backend下的kernel组合出的一个模型,也会带来性能不稳定,在端侧会非常不友好。而且还有拷贝带来的性能损耗。
for (auto& node : graph->mutable_nodes()) {
if (!node.IsStmt()) continue;
auto& instruct = node.AsStmt();
std::unordered_map<std::string, PrecisionType> in_types;
std::unordered_map<std::string, PrecisionType> out_types;
for (std::list<Node*>::iterator i = node.inlinks.begin();
i != node.inlinks.end();
++i) {
if ((*i)->arg()->type)
in_types[(*i)->arg()->name] = (*i)->arg()->type->precision();
}
for (std::list<Node*>::iterator i = node.outlinks.begin();
i != node.outlinks.end();
++i) {
if ((*i)->arg()->type)
out_types[(*i)->arg()->name] = (*i)->arg()->type->precision();
}
// Get candidate kernels
std::vector<std::pair<float, std::unique_ptr<KernelBase>>> scored;
CHECK(!instruct.kernels().empty()) << "No kernels found for "
<< instruct.op_type();
VLOG(4) << "instruct.kernels().size():" << instruct.kernels().size();
for (auto&& kernel : instruct.kernels()) {
float score = KernelGrade(instruct,
*kernel,
graph->valid_places(),
in_types,
out_types,
instruct.op_info()->input_names(),
instruct.op_info()->output_names());
VLOG(4) << "kernel->summary():" << kernel->summary()
<< " score:" << score;
scored.emplace_back(score, std::move(kernel));
}
std::sort(scored.begin(), scored.end(), KernelScoreCmp);
instruct.kernels().clear();
if (!instruct.op_info()->HasAttr("enable_int8")) {
// Move kernel back
// Just keep a single best kernel.
// TODO(Superjomn) reconsider this.
instruct.kernels().emplace_back(std::move(scored.front().second));
VLOG(2) << "pick " << instruct.kernels().front()->name() << "\n\n";
}
valid_places
;paddlelite(以下简称plite)在底层kernel选择上会考虑候选Place,Place由设备Target,精度Precision,数据排布(DataLayout)构成(还有一个常被忽略的
device_id
,用来区分多gpu的id。不过在选择策略中没出现可忽略)。1. 用于kernel注册的Place:同一个kernel根据Place的不同可以有多种实现(注册)
同一个kernel如conv2d,可能会有不同设备的实现如armcpu/opencl/x86/cuda等。在kernel注册时,需要指定kernel的Place信息。
kernel的Place用于kernel的注册,以区分唯一性。比方实现了一个arm cpu以NCHW数据排布并以fp32计算的conv2d kernel,那么其注册时候就会以conv2d,kARM,kFloat,kNCHW,def,用来区分这个kernel的唯一性。下面是conv2d的多种不冲突的kernel注册形式:
PS:你问我def是干啥的,好像依稀记得def默认大概也是用来区分kernel注册时唯一性起名的一部分,作为补充。
2. 用于模型执行时kernel选择的候选
valid_places
那模型执行的时候,遇到conv2d是选择opencl还是arm(cpu)来执行呢?就比方上面5个conv2d,模型执行时候选哪个?
这个涉及同一个op算子或称为layer层,在对应不同kernel注册的Place(上面5个conv2d kernel),和候选的执行
valid_places
的比较打分排序。用户需要(注:现在用户已经不需要了,有预设)给出候选的模型执行
valid_places
。例如下面是arm cpu跑Float模型的预设valid_places
:再例如,下面是opencl跑模型时的预设
valid_places
:用于模型执行候选kernel的
valid_places
,其中的顺序也很重要,越靠前,同一op的候选kernel的选择,就越倾向valid_places顺序靠前的Place,即权重系数越大(见后文KernelGrade方法中的weight
计算)。后面会再说到。3. 静态kernel选择(static kernel pick):候选kernel们的place与用户valid_places的笛卡尔积
注:这里,代码即就走到与硬件直接相关的
static_kernel_pick_pass
,前面已经完成了conv-bn/conv-relu/elem-act等graph级别的与硬件设备无关的大粒度的op融合策略。和动态地对同一个op/layer,选择多种kernel实现试跑的方法不同。plite对同一个op/layer的不同kernel实现(绑定不同的Place),与模型选择的
valid_places
,静态地选择要执行的kernel。那选择策略又是怎样的呢?plite有一个基于二者Place做打分策略的方法,实现的代码对应下面两个文件:
./lite/core/mir/static_kernel_pick_pass.cc
:全图遍历选择kernel。遍历模型中的计算节点(graph概念),并针对同一位置节点的不同候选kernel的place,与用户传入的valid_places
的候选place,两两打分(笛卡尔积),选择分数最高的kernel;./lite/core/mir/static_kernel_pick_pass.h
:KernelGrade对kernel的不同Place打分。针对确定的某个kernel,与用户传入的valid_places
候选依次打分。打分策略,主要基于Place信息中包含的设备(Target),精度(Precision),数据排布(DataLayout)等信息。下面描述一下这两个过程:
3.1 全图遍历选择kernel
上面第一个过程代码化简如下,主要流程见下面注释:
3.2 KernelGrade:对kernel的不同Place打分
可以看到在全图遍历选择kernel的过程中,KernelGrade起了至关重要的作用:该方法找出当前kernel下的最佳Place(方法内会*对用户传入的
valid_places
遍历计算打分:`final_score = score weight`**),及最佳Place下的该kernel得分。公式中
weight
就是valid_places
中的次序,越靠前的Place,weight
越大。比方我们希望模型以CPU的lNCHW的layout来跑,其中的valid_places
第一个必须是Place{kARM, kFloat, kNCHW},假设第二个是Place{kARM, kFloat, kNHWC}
,除了layout其他都和第一个Place一样,那么,在两个Place都有对应kernel注册且实现过的前提下(候选kernel里二者都有),因NCHW是第一位,则NCHW对应的Place的weight就更大,包含NCHW的Place最终被选中为winner_place概率会大,包含NCHW的Place的kernel被选中的概率也会更大。kernel对place打分的过程,有5个阶段,我将代码简化如下:
这5个阶段对应place的设备/place精度/place数据排布/输入输出精度检查/place排位系数,前3个在计算时有对应系数,来看看代码中的设定以及思考:
以上,便是kernel静态选择的整个过程。
4. 思考
其实可以看到:
static_pick_kernel_pass
是模型转换为plite格式的过程中一个pass,在之后的pass里应该还有更大的操作空间。比方结合试跑,结合模型更细粒度的信息做一些更细粒度的kernel选择,或者加载很多硬件试跑后的性能数据等。