算子注册机制
ncnn中算子的注册在编译期完成,主要从两个方面来学习:
- 统一抽象接口
- 算子实例创建
- 编译期实现算子注册
统一抽象接口
面向对象的三大特性:封装、继承和多态
所有算子继承自统一的Layer类,然后各自实现其功能。在使用时,将算子的对象指针转为父类的指针进行调用,这个地方使用的是多态的特性。
具体的代码示例:
基类的抽象接口:
class Layer
{
public:
// empty
Layer();
// virtual destructor
virtual ~Layer();
#if NCNN_STDIO
#if NCNN_STRING
// load layer specific parameter from plain param file
// return 0 if success
virtual int load_param(FILE* paramfp);
#endif // NCNN_STRING
// load layer specific parameter from binary param file
// return 0 if success
virtual int load_param_bin(FILE* paramfp);
// load layer specific weight data from model file
// return 0 if success
virtual int load_model(FILE* binfp);
#endif // NCNN_STDIO
// load layer specific parameter from memory
// memory pointer is 32-bit aligned
// return 0 if success
virtual int load_param(const unsigned char*& mem);
// load layer specific weight data from memory
// memory pointer is 32-bit aligned
// return 0 if success
virtual int load_model(const unsigned char*& mem);
public:
// one input and one output blob
bool one_blob_only;
// support inplace inference
bool support_inplace;
public:
// implement inference
// return 0 if success
virtual int forward(const std::vector<Mat>& bottom_blobs, std::vector<Mat>& top_blobs) const;
virtual int forward(const Mat& bottom_blob, Mat& top_blob) const;
// implement inplace inference
// return 0 if success
virtual int forward_inplace(std::vector<Mat>& bottom_top_blobs) const;
virtual int forward_inplace(Mat& bottom_top_blob) const;
public:
#if NCNN_STRING
// layer type name
std::string type;
// layer name
std::string name;
#endif // NCNN_STRING
// blob index which this layer needs as input
std::vector<int> bottoms;
// blob index which this layer produces as output
std::vector<int> tops;
};
下面是几个子类的示例:
卷积算子
class Convolution : public Layer
{
public:
Convolution();
virtual ~Convolution();
#if NCNN_STDIO
#if NCNN_STRING
virtual int load_param(FILE* paramfp);
#endif // NCNN_STRING
virtual int load_param_bin(FILE* paramfp);
virtual int load_model(FILE* binfp);
#endif // NCNN_STDIO
virtual int load_param(const unsigned char*& mem);
virtual int load_model(const unsigned char*& mem);
virtual int forward(const Mat& bottom_blobs, Mat& top_blobs) const;
public:
// param
int num_output;
int kernel_size;
int dilation;
int stride;
int pad;
int bias_term;
int weight_data_size;
// model
Mat weight_data;
Mat bias_data;
};
crop算子:
class Crop : public Layer
{
public:
Crop();
#if NCNN_STDIO
#if NCNN_STRING
virtual int load_param(FILE* paramfp);
#endif // NCNN_STRING
virtual int load_param_bin(FILE* paramfp);
#endif // NCNN_STDIO
virtual int load_param(const unsigned char*& mem);
virtual int forward(const std::vector<Mat>& bottom_blobs, std::vector<Mat>& top_blobs) const;
public:
int woffset;
int hoffset;
};
算子实例创建
算子实例的创建,在每个算子实现的cpp中,定义相应的函数,返回为基类指针,例如下面形式:
Layer* 算子名_layer_creator() {
return new 算子类;
}
为了方便,在算子基类的头文件中利用宏定义的方式,替代上述的函数:
#define DEFINE_LAYER_CREATOR(name) \
Layer* name##_layer_creator() { return new name;}
使用上述的宏定义,在每个算子中:
DEFINE_LAYER_CREATOR(Convolution);
就等价于在Convolution算子实现的cpp中定义了如下的方法:
Layer* Convolution_layer_creator() {
return new Convolution;
}
其他的算子类似上述Convolution算子。
然后根据每个算子中都存在上述的函数,那可以将每个算子的名字和对应算子中的函数的函数指针关联起来。
因此定义上述函数的函数指针类型:
typedef Layer* (*layer_creator_func)();
上面的layer_creator_func就是一个函数指针类型,可以将任意算子函数的指针赋值给这个类型定义的变量。
layer_creator_func creator;
creator = Convolution_layer_creator;
然后使用一个结构体将名字和上述的函数指针和名字关联起来
struct layer_registry_entry
{
#if NCNN_STRING
// layer type name
const char* name;
#endif // NCNN_STRING
// layer factory entry
layer_creator_func creator;
};
根据上述的结构体,就可以根据名字来创建相应的算子实例:
layer_registry_entry conv;
conv.name = "convolution";
conv.creator = Convolution_layer_creator;
Layer* layer = nullptr;
if (conv.name == "convolution") {
layer_creator_func create = conv.creator;
layer = create();
}
编译期实现算子注册
根据上面的可知在每个算子实现的cpp文件中,存在一个如下的函数:
Layer* 算子类名_layer_creator() {
return new 算子类;
}
想要在外部使用该函数,则需要使用extern的方式,来声明外部的函数:
extern Layer* 算子类名_layer_creator();
声明了外部的函数以后,根据上面定义的函数指针类型,可以使用该函数名对其进行赋值:
layer_creator_func creator = 算子类名_layer_creator;
根据上述的使用方法,可以对结构体进行赋值:
//声明外部函数
extern Layer* 算子类名_layer_creator();
layer_registry_entry conv;
conv.name = "convolution";
conv.creator = 算子类名_layer_creator;
上述代码中对结构的赋值是通过对每个成员进行单独赋值实现的。也可以通过构造的方式进行实现:
extern Layer* 算子类名_layer_creator();
layer_registry_entry conv{"convolution", 算子类名_layer_creator};
从上面可以看出,只要我们将每个算子都对上述的结构体赋值即可,单以往对结构的赋值都是在代码中实现的,也就是在运行期才会赋值成功。
而对结构体的赋值还有一种新的方式,下面进行举例说明:
struct Test {
string name;
int age;
float weight;
};
上述就是结构的具体定义,下面存在一个头文件:
test.h
{"nihao",12,12.3},
{"hello",13,23.4},
结合上面的内容就可以有以下的用法:
static const Test t[] = {
#include "test.h"
};
static const int t_count = sizeof(t)/ sizeof(Test);
通过上述的代码,可以实现通过一个头文件,对结构体的数组进行赋值,并通过sizeof得到数组的长度。
在ncnn就是通过头文件的方式来初始化结构体数组,而头文件,则是在编译期生成,不是提前添加好的。
对于头文件的自动生成,并且其中包含相应的内容,可以在在CMakeLists.txt中完成,在ncnn中
macro(ncnn_add_layer class)
if(WITH_LAYER_${name})
file(APPEND ${CMAKE_CURRENT_BINARY_DIR}/layer_declaration.h
"extern Layer* ${class}_x86_layer_creator();\n")
file(APPEND ${CMAKE_CURRENT_BINARY_DIR}/layer_registry.h
"#if NCNN_STRING\n{\"${class}\",${class}_x86_layer_creator},\n#else\n{${class}_x86_layer_creator},\n#endif\n")
endif()
endmacro()
ncnn_add_layer(Convolution)
ncnn_add_layer(Crop)
上述的cmake代码中,会生成两个头文件,layer_declaration.h
文件中添加外部函数声明的内容;layer_registry.h
文件中包含初始化结构体数组的内容。
在代码中使用的方法如下:
//引入外部函数的声明
#include "layer_declaration.h"
static const layer_registry_entry layer_registry[] =
{
#include "layer_registry.h"
};
static const int layer_registry_entry_count = sizeof(layer_registry) / sizeof(layer_registry_entry);
上述编译完成后,layer_registry
变量中存储的就是所有的算子名字和creator.
在算子基类所在的文件,可以提供生成算子函数接口。这样所有的算子注册集合就需要对外进行暴露。
在ncnn中的实现如下
Layer* create_layer(int index)
{
if (index < 0 || index >= layer_registry_entry_count)
{
fprintf(stderr, "layer index %d not exists\n", index);
return 0;
}
layer_creator_func layer_creator = layer_registry[index].creator;
if (!layer_creator)
{
fprintf(stderr, "layer index %d not enabled\n", index);
return 0;
}
return layer_creator();
}
同样可以通过算子名称进行创建,重载一下create_layer即可。