zhangjun / zhangjun.github.io

https://zhangjun.github.io
2 stars 0 forks source link

Paddle Lite 代码阅读 #1

Open zhangjun opened 2 years ago

zhangjun commented 2 years ago

optimizer

lite::Optimizer optimize a program. It utilize the mir passes to analysis the program and export an optimized program.

std::unique_ptr<RuntimeProgram> RunDefaultOptimizer(
    Program&& program,
    const std::vector<Place>& valid_places,
    core::KernelPickFactor kernel_pick_factor,
    const std::vector<std::string>& passes) {
  Optimizer optim(valid_places, kernel_pick_factor);
  // ...
  for (auto& pass_name : passes_local) {
    optim.AddPass(pass_name);
  }

  return optim.Run(std::move(program));
}
class Optimizer {
 public:
  Optimizer(const std::vector<Place>& valid_places,
            core::KernelPickFactor kernel_pick_factor)
      : valid_places_(valid_places), kernel_pick_factor_(kernel_pick_factor) {
    CHECK(!valid_places.empty()) << "At least one valid_place should be set";
  }

  // Append a pass to the optimizer.
  void AddPass(const std::string& pass_name);
  // Optimize a program to generate a runtime program.
  std::unique_ptr<RuntimeProgram> Run(Program&& program);

 protected:
  // Run all the added passes.
  void ApplyPasses(std::vector<std::unique_ptr<mir::SSAGraph>>* graphes);

  // Generate the optimized runtime program.
  std::unique_ptr<RuntimeProgram> GenRuntimeProgram(
      std::vector<std::unique_ptr<mir::SSAGraph>>* graphs);

  void InitTargetTypeTransformPass();
  void InitControlFlowOpUnusedInputsAndOutputsEliminatePass();
  void InitControlFlowOpSharedInputsAndOutputsPlaceSyncPass();
  void SpecifyKernelPickTactic(core::KernelPickFactor factor);
  Scope* exec_scope() { return exec_scope_; }

 private:
  std::vector<Place> valid_places_;
  Scope* exec_scope_{};
  std::vector<mir::Pass*> passes_;
  std::vector<std::unique_ptr<mir::SSAGraph>> graphs_;
  core::KernelPickFactor kernel_pick_factor_;
};
zhangjun commented 2 years ago

Paddle Lite 介绍

设计及思考 近年来,各种深度学习预估硬件称出不穷,从手机APP到车载设备,再到音箱,均需要部署深度学习预测,且有如下共性需求:

  1. 高性能
  2. 硬件支持和扩展容易
  3. 轻量级部署

Paddle-Lite 的架构方面便是定向参考如上需求设计实现的,具体地

框架部分已经经过 FPGA,GPU,NPU 等异构硬件的打磨,各项能力也在完善中。

重要模块介绍

OpLite

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; }
 // Link the external execution environ to internal context.
 bool AttachImpl(const cpp::OpDesc &opdesc, lite::Scope *scope);
};

其中,分析期执行

执行期执行

扩展须知:

  1. CheckShape 只在第一个 batch 执行,所以耗时不敏感
  2. 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

OpParam 用于存储执行期 Kernel 需要的各项参数。 所有字段可以直接存储(比如指针或者 int),以避免执行中获取参数的延迟。

因为没有需求,OpParam 暂时没有设置基类。

实际例子:

// For Softmax op
struct SoftmaxParam {
 lite::Tensor* x{};
 lite::Tensor* output{};
 int axis{-1};
};

OpLite 的 AttachImpl 方法就用于构建 OpParam ,复制传递给 Kernel 用于执行。

OpParam 是执行期的重要模块,需要严格保证性能,相应的扩展要求:

字段的获取必须是低延迟的,可以直接用指针,或者直接复制值 避免执行无关信息混入,包括 debug 信息 命名需要与 Paddle OpDesc 中的信息严格一致,以降低功能对齐和理解的难度

Kernel

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 的注册需要用到 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。

扩展须知

  1. 模板参数选用计算中主要的来表示 比如,scale kernel,同时能接受 float 和 int 的输入,但其不算量化 kernel,那应该设置为 Precision=float,代表常规的计算精度中使用
  2. Kernel 输入输出的定义需要足够精确,是什么类型就是什么类型;框架会根据其输入输出的定义来动态构建状态机,否则会出现分析期和执行期的状态机不一致,造成未定义行为

MIR

MIR 类似于 LLVM 里的 IR,只是加上了硬件和执行期的信息参与分析优化。

Pass 是MIR中的模块化策略,其输入和输出都是 SSA Graph.

框架会自动基于模型的Program 构建 SSA Graph,之后按 Optimizer 中定义的pass的顺序调用一系列 Pass。

Op Fusion

MIR 中的 PatternMacher 实现了简单有效的基于图的模板识别的算法,相关的 op fusion 的图操作可以基于此实现。

实际的例子可以参考 fc_fuse_pass.h

TypeSystem

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

KernelContext 是硬件支持的核心封装,主要用于为 Kernel 提供执行期的硬件上下文。

KernelContext 的设计类似于 OpParam,两者均没有基类;对于 KernelContext,其假定是,不同的硬件间的接口和逻辑可能完全不同,比如 kARM 和 kCUDA,因此不设定基类,也不需要提供统一的接口来封装不同硬件行为。

不同硬件的 KernelContext 直接与该硬件对应的 Kernel 对接。

KernelContext 的行为可以被 MIR 在分析期确定和调度。

注意事项:

  1. 由于是执行期概念,KernelContext 也需要注意性能和轻量化
  2. 移动端部署时只会部署执行期,因此 MIR 和 KernelContext 会拆开,因此 KernelContext 相应的设置需要能够序列化到 ProgramDesc 中,以便执行期载入和执行

    扩展硬件后端

    扩展现有的硬件后端

    主要是扩充 Op 和 Kernel 的工作,如果需要 fuse,则参考 MIR 章节,增加相应的fuse pass便可,具体地,可以参考

zhangjun commented 1 year ago

通用优化

内存复用

typedef struct {
  std::string name;                     // var name
  int cluster;
  std::pair<int, int> lifetime;      // begin id, end id
  std::set<std::string> adj;
} MemNode;

using lifecycle_t = std::pair<int, int>;
using lifecycle_map_t = std::map<std::string, lifecycle_t>;
std::map<std::string, lifecycle_map_t>* lifecycles;