> 文章列表 > KuiperInfer深度学习推理框架-源码阅读和二次开发(1):算子开发流程之算子注册

KuiperInfer深度学习推理框架-源码阅读和二次开发(1):算子开发流程之算子注册

KuiperInfer深度学习推理框架-源码阅读和二次开发(1):算子开发流程之算子注册

前言:KuiperInfer是一个从零实现一个高性能的深度学习推理库,中文教程已经非常完善了。本系列博客主要是自己学习的一点笔记和二次开发的教程,欢迎更多的AI推理爱好者一起来玩。这篇写一下算子开发流程,重点是算子注册机制和背后的知识点,并和其他的深度学习框架(如AI编译器CINN、paddle推理inference等)对比,总结其中的异同点。

目录

算子注册

设计模式

算子注册表

算子开发模板

后记

参考


算子注册

设计模式

kuiper中的算子注册实现主要是由工厂模式和单例模式实现的,当中的细节讲解请看博客:自制深度学习推理框架-第五课-起飞!框架中的算子注册机制 - 知乎

工厂模型的简介:https://www.cnblogs.com/horacle/p/15494358.html

工厂模式概念:用一个简单的类来创建实例的过程便称为工厂,用工厂方式代替外部new操作的一种设计模式称为工厂模式。这是一种创建型模式,它提供了一个创建对象的最佳方式。在工厂模式中,我们创建对象时不会对上层暴露创建逻辑,而是通过使用一个共同结构来指向新创建的对象。

工厂模式分类:简单工厂模式、工厂方法模式、抽象工厂模式。

意图:定义一个常见对象的接口,让子类自己决定实例化哪个工厂类,工厂模式使其创建过程延迟到子类进行。

问题解决:主要解决接口选择问题。

解决方法:让其子类实现工厂接口,返回的是一个抽象的产品。

使用前提:1、编码时不能预见需要创建哪种类的实例(不同的条件下发创建不同实例);2、系统不应依赖于产品类实例如何被创建、组合和表达的细节。

关键代码:子类执行创建过程。

优点:1、使代码结构清晰,有效地封装变化,提高拓展性;2、屏蔽产品的具体实现,调用者只关心产品的接口;3、降低耦合度

单例模式的细节可看:【C++】C++ 单例模式总结(5种单例实现方法)_单例模式c++实现_unonoi的博客-CSDN博客

单例模式是指在整个系统生命周期内,保证一个类只能产生一个实例,确保该类的唯一性,并提供一个访问它的全局访问点。

一般的单例模式结构如下:

class Singleton {
public:static Singleton* GetInstance(); //供用户获取单例的全局访问点
protected:Singleton();  //方便继承,同时保证类的用户无法直接构造该类的实例Singleton(const Singleton&);
private://class members
};

保证唯一的全局访问点的能力是通过全局静态变量实现的,全局静态变量能够实现对象的全局访问,但这不能防止你实例化多个类实例。为了实现上述要求,我们需要加强类的设计,让类自身保证其实例仅有一个。也就是说,“这个类可以保证没有其它实例可以被创建,并且它可以提供一个访问该实例的方法。"

例如所有的op都是单例模式实现的,因为要确保这个op的唯一性,以relu算子为例:

class ReluLayer : public Layer {public:ReluLayer() : Layer("Relu") {}InferStatus Forward(const std::vector<std::shared_ptr<Tensor<float>>> &inputs,std::vector<std::shared_ptr<Tensor<float>>> &outputs) override;static ParseParameterAttrStatus GetInstance(const std::shared_ptr<RuntimeOperator> &op,std::shared_ptr<Layer> &relu_layer);
};
}
#endif //KUIPER_INFER_SOURCE_LAYER_BINOCULAR_RELU_HPP_

在GetInstance()方法中实现实例化这个类:

ParseParameterAttrStatus ReluLayer::GetInstance(const std::shared_ptr<RuntimeOperator> &op,std::shared_ptr<Layer> &relu_layer) {CHECK(op != nullptr) << "Relu operator is nullptr";relu_layer = std::make_shared<ReluLayer>();return ParseParameterAttrStatus::kParameterAttrParseSuccess;
}

然后需要注意到的是这里的注册表也是要用单例模式实现的,一切的一切实现关键都是static,因为static存放在静态区,这是一个全局的,可以复习一下C++的内存结构:

堆区:是由程序员手动申请(new)与释放(delete)的内存区域。从低地址向高地址申请;内存空间大、存储地址不连续,一般是链式的;速度较慢。
栈区:由编译器自动分配和释放,主要存储 函数的参数值、函数内部的变量的值、函数调用的空间。从高地址向低地址申请;容量有限;速度较快;存储地址连续,会溢出。
代码区:又叫文本段(.text),存放着程序的机器代码,可执行指令就是存储在这里的,这里的代码是只读的。
全局区(静态区):全局变量和静态变量是存储在这里的。初始化的全局变量和静态变量在一块区域(.data),未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(.bbs)。系统结束后由系统释放。
常量区:常量字符串放在这里,程序结束后,由系统进行释放。

所以要保证注册表唯一性,这里也得使用单例模式,在创建CreateRegistry的时候用static:

LayerRegisterer::CreateRegistry &LayerRegisterer::Registry() {static CreateRegistry *kRegistry = new CreateRegistry();CHECK(kRegistry != nullptr) << "Global layer register init failed!";return *kRegistry;
}

算子注册表

核心是操作和维护注册表,这是一组键值对,key是对应的OpType,用来查找对应的value,value是用于创建该层的对应方法(Creator)。

typedef std::map<OpType, Creator> CreateRegistry;

注册creator就是向这个哈希表中插入键值对:

void LayerRegisterer::RegisterCreator(const std::string &layer_type,const Creator &creator) {CHECK(creator != nullptr);CreateRegistry &registry = Registry();CHECK_EQ(registry.count(layer_type), 0)<< "Layer type: " << layer_type << " has already registered!";registry.insert({layer_type, creator});
}LayerRegisterer::CreateRegistry &LayerRegisterer::Registry() {static CreateRegistry *kRegistry = new CreateRegistry();CHECK(kRegistry != nullptr) << "Global layer register init failed!";return *kRegistry;
}

需要注意一下Creator的定义:

typedef ParseParameterAttrStatus (*Creator)(const std::shared_ptr<RuntimeOperator> &op, std::shared_ptr<Layer> &layer);

这是一个typedef函数指针的语法糖,表示Creator是一个函数指针,每一个Creator表示一个工厂。返回值类型是ParseParameterAttrStatus,参数是const std::shared_ptr<RuntimeOperator> &op, std::shared_ptr<Layer> &layer。关于这个语法的详细讲解请看:typedef 函数指针的用法_typedef函数指针_Rebirth_2017的博客-CSDN博客

算子开发模板

目前框架实现了二三十个算子,对比CINN实现80多个算子,总共需要实现的算子有一百多个。这些算子开发的方法都很有套路,这里简单的介绍一下:

hpp文件里需要声明一个Forward和一个用于实现单例模式的GetInstance,然后构造函数需要explicit修饰,表示不能发生相应的隐式类型转换,只能以显式的方式进行类型转换。

#ifndef KUIPER_INFER_SOURCE_LAYER_SIGMOID_HPP_
#define KUIPER_INFER_SOURCE_LAYER_SIGMOID_HPP_
#include "layer/abstract/layer.hpp"
namespace kuiper_infer {
class SigmoidLayer : public Layer {public:explicit SigmoidLayer(): Layer("Sigmoid"){}InferStatus Forward(const std::vector<std::shared_ptr<Tensor<float>>> &inputs,std::vector<std::shared_ptr<Tensor<float>>> &outputs) override;static ParseParameterAttrStatus GetInstance(const std::shared_ptr<RuntimeOperator> &op,std::shared_ptr<Layer> &sigmoid_layer);
};
}

cpp文件里首先实现单例函数的GetInstance方法,这里需要复习一下智能指针的使用方法。

ParseParameterAttrStatus SigmoidLayer::GetInstance(const std::shared_ptr<RuntimeOperator> &op,std::shared_ptr<Layer> &sigmoid_layer) {CHECK(op != nullptr) << "Sigmoid operator is nullptr";sigmoid_layer = std::make_shared<SigmoidLayer>();return ParseParameterAttrStatus::kParameterAttrParseSuccess;
}

kuiper的尽可能地使用了智能指针管理内存,其实主要就是shared_ptr。

在 C++ 中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针(smart pointer)。智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。

关于只能指针强烈推荐复习一下大丙的文章!

共享智能指针 | 爱编程的大丙 共享智能指针 | 爱编程的大丙

这里还是用刚才的GetInstance当做例子吧,我们本来是要把

relu_layer = std::make_shared<ReluLayer>();

写作是

ReluLayer *relu_layer = new ReluLayer();

后记

本来是想讲完整个算子开发流程的,但是不知不觉写了6k+,还是分成几篇博客系列介绍吧~

这篇博客完全是自己的个人笔记,仅供自我参考!还是强烈建议去看up主的教程!

参考

  • 自制深度学习推理框架系列-手写第一个算子Relu - 知乎
  • 自制深度学习推理框架-第六课-MaxPooling算子的实现_哔哩哔哩_bilibili
  • 深度学习编器CINN(2):以reciprocal算子为例看算子开发方法_深度学习算子开发_沉迷单车的追风少年的博客-CSDN博客