docs/develop_guides/architecture-intro.md
这篇文档会从开发者角度详细介绍开发 Paddle Lite 需要的相关信息。
近年来,各种深度学习预估硬件层出不穷,从手机 APP 到车载设备,再到音箱,均需要部署深度学习预测,且有如下共性需求:
Paddle Lite 的架构方面便是定向参考如上需求设计实现的,具体地
Paddle Lite 的架构尝试从强类型推导的角度建模支持多硬件,多种计算模式(不同量化精度、不同的 Data Layout等)的混合计算,从而实现宏观上的各异硬件和计算模式的混合。
框架部分已经经过 FPGA、GPU、NPU 等异构硬件的打磨,各项能力也在完善中。
OpLite 是 Paddle Lite 中的 Operator,用户扩展单个硬件时,最多的就是扩展 Op 和 Kernel。 重要方法如下:
class OpLite : public Registry {
public:
// Check the shape.
virtual bool CheckShape() const { return true; }
// Inference the outputs' shape.
virtual bool InferShape() const { return true; }
virtual bool InferShape();
// Infer the outputs's data type during opt period
virtual bool InferType() {return false};
// Run this operator.
virtual bool Run();
// Indicate whether the Op runs only once or not
virtual bool run_once() const { return false; }
// Attach it with the runtime environment.
virtual bool AttachImpl(const cpp::OpDesc &opdesc, lite::Scope *scope) = 0;
};
其中,分析期执行
InferTypeAttachImpl执行期执行
CheckShapeInferShape扩展须知:
CheckShape 只在第一个 Batch 执行,所以耗时不敏感
InferShape 需要在每个 Batch 执行,应该严格耗时
可以通过添加 Member Variable 的方式,对其中一部分信息增加 Cache,比如
class XXOp : public OpLite {
void InferShape() {
int batch_size = param().input.shape[0];
if (!shape_cache_.empty()) {
shape_cache_[0] = batch_size;
param().output->Resize(shape_cache_);
}
}
private:
shape_t shape_cache_;
}
OpParam 用于存储执行期 Kernel 需要的各项参数。 所有字段可以直接存储(比如指针或者 int),以避免执行中获取参数的延迟。
因为没有需求,OpParam 暂时没有设置基类。
实际例子:
// For Softmax op
struct SoftmaxParam {
lite::Tensor* x{};
lite::Tensor* output{};
int axis{-1};
};
OpLite 的 AttachImpl 方法就用于构建 OpParam ,复制传递给 Kernel 用于执行。
OpParam 是执行期的重要模块,需要严格保证性能,相应的扩展要求:
template <TargetType Target,
PrecisionType Precision,
DataLayoutType DataLayout = DataLayoutType::kNCHW>
class KernelLite : public KernelBase {
public:
// Run the kernel.
virtual void Run() { CHECK(false) << "Not Implemented"; }
TargetType target() const override { return Target; }
PrecisionType precision() const override { return Precision; }
DataLayoutType layout() const override { return DataLayout; }
Place place() const override { return Place{Target, Precision, DataLayout}; }
std::string name() const override;
};
Kernel 是执行期的重要概念,因此设计地非常简单高效。
其中,执行期的 Run 是其唯一重要的接口,其中包含具体的计算逻辑。
模板中的参数主要用于方便多硬件编译,以及自解释:
这部分信息用于帮助挑选 Kernel,具体的值并不严格。
Kernel 的注册需要用到 TypeSystem,不光对 Kernel 本身的特性进行描述,对其输入和输出均进行详尽的定义。
例如 FullyConnected 的注册
REGISTER_LITE_KERNEL(
fc, kARM, kFloat, kNCHW, paddle::lite::kernels::arm::FcCompute, def)
.BindInput("Input", {LiteType::GetTensorTy(TARGET(kARM), PRECISION(kFloat), LAYOUT(kNCHW))})
.BindInput("Bias", {LiteType::GetTensorTy(TARGET(kARM))})
.BindInput("W", {LiteType::GetTensorTy(TARGET(kARM))})
.BindOutput("Out", {LiteType::GetTensorTy(TARGET(kARM))})
.Finalize();
Kernel 自身定义是 kARM 的,也就是 ARM 上的 Kernel,主要的计算精度是 kFloat,主要的 Data Layout 是 kNCHW。
接着会对其所有的输入和输出做详细定义,比如看 Input 输入的定义是 LiteType::GetTensorTy(TARGET(kARM), PRECISION(kFloat), LAYOUT(kNCHW)),也就是声明其 Target 是 kARM, Precision 是 kFloat,Data Layout 是 kNCHW。
这里的设计思想是类似 C++ 中的函数重载,同一个 Kernel(的名字),在重载了其输入输出的类型之后可以是不同的 Kernel。
float 和 int 的输入,但其不算量化 Kernel,那应该设置为 Precision=float,代表常规的计算精度中使用MIR 类似于 LLVM 里的 IR,只是加上了硬件和执行期的信息参与分析优化。
Pass 是 MIR 中的模块化策略,其输入和输出都是 SSA Graph.
框架会自动基于模型的 Program 构建 SSA Graph,之后按 Optimizer 中定义的 Pass 的顺序调用一系列 Pass。
MIR 中的 PatternMacher 实现了简单有效的基于图的模板识别的算法,相关的 Op Fusion 的图操作可以基于此实现。
实际的例子可以参考 fc_fuse_pass.h。
TypeSystem 是 Paddle Lite 中构建复杂计算图的基础模块,核心思想是协助 SSA Graph 构建一个状态机,表示其中不同的状态。
这里的 Type 主要包含下面四组信息,更多的信息可以按需扩展:
状态机的表示:
Tensor0(kARM, kFloat, kNCHW) --pass--> Tensor1(kOpenCL, kFloat, kNCHW)
MIR 会识别出,Tensor0 和 Tensor1 的硬件位置不同,因此触发相应的 Pass 插入对应的 Cast Op 来进行 Type Cast,比如
Tensor0(kARM, kFloat, kNCHW) --pass-> IoCopyOp(kARM, kOpenCL) --pass-> Tensor1(kOpenCL, kFloat, kNCHW)
KernelContext 是硬件支持的核心封装,主要用于为 Kernel 提供执行期的硬件上下文。
KernelContext 的设计类似于 OpParam,两者均没有基类;对于 KernelContext,其假定是,不同的硬件间的接口和逻辑可能完全不同,比如 kARM 和 kCUDA,因此不设定基类,也不需要提供统一的接口来封装不同硬件行为。
不同硬件的 KernelContext 直接与该硬件对应的 Kernel 对接。
KernelContext 的行为可以被 MIR 在分析期确定和调度。
注意事项:
主要是扩充 Op 和 Kernel 的工作,如果需要 Fuse,则参考 MIR 章节,增加相应的 Fuse Pass 便可,具体地,可以参考
需要额外扩充如下模块,让框架能够支撑硬件执行: