ncnn源码阅读(五)----算子注册机制

算子注册机制

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即可。

猜你喜欢

转载自blog.csdn.net/qq_25105061/article/details/132048138