详解Paddle Lite底层在backend上的Kernel选择策略
Posted 飞桨PaddlePaddle
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详解Paddle Lite底层在backend上的Kernel选择策略相关的知识,希望对你有一定的参考价值。
/* Place specifies the execution context of a Kernel or input/output for a
* kernel. It is used to make the analysis of the MIR more clear and accurate.
*/
struct LITE_API Place {
TargetType target{TARGET(kUnk)};
PrecisionType precision{PRECISION(kUnk)};
DataLayoutType layout{DATALAYOUT(kUnk)};
int16_t device{ 0}; // device ID
Place() = default;
Place(TargetType target,
PrecisionType precision = PRECISION(kFloat),
DataLayoutType layout = DATALAYOUT(kNCHW),
int16_t device = 0)
: target(target), precision(precision), layout(layout), device(device) {}
}
conv2d, kARM, kInt8, kNCHW, def
conv2d, kARM, kFloat, kNCHW, def
conv2d, kARM, kFloat, kNHWC, def
conv2d, kOpenCL, kFloat, kNCHW, def
conv2d, kOpenCL, kFloat, kImageDefault, def
std:: vector<Place> valid_places({
Place{TARGET(kARM), PRECISION(kFloat)},
});
std:: vector<Place> valid_places({
Place{TARGET(kOpenCL), PRECISION(kFP16), DATALAYOUT(kImageDefault)},
Place{TARGET(kOpenCL), PRECISION(kFloat), DATALAYOUT(kNCHW)},
Place{TARGET(kOpenCL), PRECISION(kAny), DATALAYOUT(kImageDefault)},
Place{TARGET(kOpenCL), PRECISION(kAny), DATALAYOUT(kNCHW)},
TARGET(kARM), // enable kARM CPU Kernel when no opencl Kernel
});
3.1 全图遍历选择Kernel
// lite/core/mir/static_kernel_pick_pass.cc
// 1. 依次遍历模型graph节点
for ( auto& node : graph->mutable_nodes()) {
if (!node.IsStmt()) continue; // 跳过非计算节点
auto& instruct = node.AsStmt();
// 获取所有该节点的输入和输出的tensor精度,实现略
std:: unordered_map< std:: string, PrecisionType> in_precision_types;
std:: unordered_map< std:: string, PrecisionType> out_precision_types;
// 获取该层op的不同kernel候选实现:instruct.kernels()
// 比方该层是conv2d,那么instruct.kernels()方法,
// 就可获取到所有编译进去的conv2d的不同实现。
// 2. 依次(for)对不同Kernel实现打分(KernelGrade),
// KernelGrade用来找出该Kernel实现的最佳Place,
// 及最佳Place下的Kernel得分。
std:: vector< std::pair< float, std:: unique_ptr<KernelBase>>> scored;
for ( auto&& kernel : instruct.kernels()) {
float score = KernelGrade(instruct,
*kernel,
graph->valid_places(),
in_precision_types,
out_precision_types,
instruct.op_info()->input_names(),
instruct.op_info()->output_names());
// 3. 记录每种Kernel实现在最佳Place下的最高分值
scored.emplace_back(score, std::move(kernel));
}
// 4. 对打分结果scored排序,clear清空候选Kernel列表
// 重置候选Kernel列表为分数最高的那一个Kernel,
// 即最终选中要执行的Kernel
std::sort(scored.begin(), scored.end(), KernelScoreCmp);
instruct.kernels().clear();
instruct.kernels().emplace_back( std::move(scored.front().second));
}
3.2 KernelGrade:对Kernel的不同Place打分
// lite/core/mir/static_kernel_pick_pass.h
size_t KernelGrade(
const mir::Node::Stmt& instruct,
const KernelBase& kernel,
const vector<Place>& valid_places,
const unordered_map< std:: string, PrecisionType>& in_node_precisons,
const unordered_map< std:: string, PrecisionType>& out_node_precisons) {
float final_score_for_winner_place{ -1.};
const int kMax = numeric_limits< int>::max();
size_t place_size = valid_places.size();
for ( size_t pidx = 0; pidx < place_size; ++pidx) {
const auto& place = valid_places[pidx];
float weight = static_cast< float>(place_size - pidx) / place_size;
size_t place_score{ 0};
if (place.target == kernel.target())
place_score += kMax / KernelPickFactor::Factor::TargetFirst;
if (place.precision == kernel.precision())
place_score += kMax / KernelPickFactor::Factor::PrecisionFirst;
if (place.layout == kernel.layout())
place_score += kMax / KernelPickFactor::Factor::DataLayoutFirst;
if ((in_node_precisons == kernel_registered_in_tensor_precisions) &&
out_node_precisons == kernel_registered_out_tensor_precisions))
place_score *= 2;
if (weight * place_score > final_score_for_winner_place) {
final_score_for_winner_place = weight * place_score;
winner_place = place;
}
}
return final_score_for_winner_place;
}
// /lite/core/types.h
// 系数在实际计算中转为分母
class KernelPickFactor {
public:
using value_type = unsigned char;
enum class Factor : int {
// The following factors are sorted by priority.
TargetFirst = 1,
PrecisionFirst = 1 << 1,
DataLayoutFirst = 1 << 2,
DeviceFirst = 1 << 3,
};
-
设备target(系数为1):相比Place中的其他两个数据,设备系数排在首位,因为数据在不同设备上的传输开销极大。若模型中conv都是GPU计算,中间有些层的实现是CPU的,且无zero copy前提下,来回的数据拷贝带来的性能下降就很明显。 -
精度precision(系数为1/4):其实精度还有数据排布哪个排在第二位更好,还需实践检验,以OpenCL来说,数据排布layout为cl::image(kImageDefault)可利用L1 cache,一般性能比cl::buffer(NCHW)要好,精度FP16比FP32性能也要好不少,就从OpenCL来说可能二者打分的系数可以一样。当前Paddle Lite的实现是精度的重要性系数(比layout)更大。 -
数据排布datalayout(系数为1/8):同上。访存的优化也是必要的,CPU为了更极致的计算性能,而定义了NHWC的数据排布,也是打分的一项考量。 -
Kernel注册的输入输出的tensor精度,与该graph中当前op的输入输出精度是否匹配。全部匹配就分数翻倍。该打分会检查当前graph中的节点精度和Kernel注册时tensor的精度是否一致。其实不仅是精度,layout和target也可以做这个判断。 -
分数乘以当前place在valid_places中的排位系数。这个前面已经说过,排在越靠前的place,对应Kernel被选中的 概率就越大。
-
Paddle Lite的Kernel选择前先做graph层级op粒度的融合操作,与硬件无关; -
在之后,是与硬件信息相关的静态Kernel选择。选择基于Place{target, precision, layout}信息,从而确定要执行的Kernel,其中没有参考如卷积核的大小,输入的大小等信息。换句话说,该过程与模型输入、op具体信息无关,选择的依据粒度仍然较大; -
static_pick_kernel_pass是模型转换为Paddle Lite格式的过程中一个pass,在之后的pass里应该还有更大的操作空间。比方结合试跑,结合模型更细粒度的信息做一些更细粒度的Kernel选择和定制化修改。
静态选择和具体选择,对应的两个阶段需要打通。即StaticPickKernel过程,与具体的Kernel选择绑定,这时可以全盘考虑。这个过程也需要拿到conv的kernel size,input shape等信息。但这样虽然两个阶段的Kernel选择打通,但是二阶段的具体Kernel判断需要再写一遍,维护上有一定成本;
两阶段分开做Kernel选择,即每个阶段相对于局部的最优,从而达到相对全局的(次)最优。其实我们的目的是找一个模型在所有不同target、precision、layout的Kernel实现上排列组合这个模型下的最佳性能。但静态选择的策略,在本质上已经考虑了backend不同带来的差异。端侧对性能的极致要求,可能不同backend下的Kernel组合出的一个模型,也会带来性能不稳定,在端侧会非常不友好,而且还有拷贝带来的性能损耗。
END
以上是关于详解Paddle Lite底层在backend上的Kernel选择策略的主要内容,如果未能解决你的问题,请参考以下文章
百度推出端侧推理引擎 Paddle Lite,支持华为 NPU 在线编译
百度飞桨重磅推出端侧推理引擎Paddle Lite 支持更多硬件平台
飞桨端侧推理引擎重磅升级为Paddle Lite,更高扩展性更极致性能!
如何在Jetson nano上同时编译TensorRT与Paddle Lite框架