前言: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>> &inputs,
std::vector>> &outputs) override;
static ParseParameterAttrStatus GetInstance(const std::shared_ptr &op,
std::shared_ptr &relu_layer);
};
}
#endif //KUIPER_INFER_SOURCE_LAYER_BINOCULAR_RELU_HPP_
在GetInstance()方法中实现实例化这个类:
ParseParameterAttrStatus ReluLayer::GetInstance(
const std::shared_ptr &op,
std::shared_ptr &relu_layer) {
CHECK(op != nullptr) << "Relu operator is nullptr";
relu_layer = std::make_shared();
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 CreateRegistry;
注册creator就是向这个哈希表中插入键值对:
void LayerRegisterer::RegisterCreator(const std::string &layer_type,
const Creator &creator) {
CHECK(creator != nullptr);
CreateRegistry ®istry = 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 &op, std::shared_ptr &layer);
这是一个typedef函数指针的语法糖,表示Creator是一个函数指针,每一个Creator表示一个工厂。返回值类型是ParseParameterAttrStatus,参数是const std::shared_ptr
目前框架实现了二三十个算子,对比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>> &inputs,
std::vector>> &outputs) override;
static ParseParameterAttrStatus GetInstance(const std::shared_ptr &op,
std::shared_ptr &sigmoid_layer);
};
}
cpp文件里首先实现单例函数的GetInstance方法,这里需要复习一下智能指针的使用方法。
ParseParameterAttrStatus SigmoidLayer::GetInstance(const std::shared_ptr &op,
std::shared_ptr &sigmoid_layer) {
CHECK(op != nullptr) << "Sigmoid operator is nullptr";
sigmoid_layer = std::make_shared();
return ParseParameterAttrStatus::kParameterAttrParseSuccess;
}
kuiper的尽可能地使用了智能指针管理内存,其实主要就是shared_ptr。
在 C++ 中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针(smart pointer)。智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。
关于只能指针强烈推荐复习一下大丙的文章!
共享智能指针 | 爱编程的大丙 共享智能指针 | 爱编程的大丙
这里还是用刚才的GetInstance当做例子吧,我们本来是要把
relu_layer = std::make_shared();
写作是
ReluLayer *relu_layer = new ReluLayer();
本来是想讲完整个算子开发流程的,但是不知不觉写了6k+,还是分成几篇博客系列介绍吧~
这篇博客完全是自己的个人笔记,仅供自我参考!还是强烈建议去看up主的教程!
- 自制深度学习推理框架系列-手写第一个算子Relu - 知乎
- 自制深度学习推理框架-第六课-MaxPooling算子的实现_哔哩哔哩_bilibili
- 深度学习编器CINN(2):以reciprocal算子为例看算子开发方法_深度学习算子开发_沉迷单车的追风少年的博客-CSDN博客