caffe注册机制浅析——宏与类的使用

简介: 一、楔子15年那会儿,我刚入坑深度学习。那时候几大主流框架caffe,theano,和torch7(不是pytorch)分别代表了C++、python、Lua几大语言。其中尤以caffe最为风靡也最受欢迎。因为我本科是电子工程,不是计算机科班出身,在甫一阅读caffe的代码,尤其是看到众多我不熟悉的概念,如常引用,模板类,protobuf,glog,gtest等等时,自然是一脸懵逼,直接劝退。当然

一、楔子

15年那会儿,我刚入坑深度学习。那时候几大主流框架caffe,theano,和torch7(不是pytorch)分别代表了C++、python、Lua几大语言。其中尤以caffe最为风靡也最受欢迎。因为我本科是电子工程,不是计算机科班出身,在甫一阅读caffe的代码,尤其是看到众多我不熟悉的概念,如常引用,模板类,protobuf,glog,gtest等等时,自然是一脸懵逼,直接劝退。当然后面的tensorflow,pytorch等框架横空出世,编程便不再成为科研问题,这是后话。

工作后,由于接手的代码全为C++,加之我个人是个很执着的人,我竟又想到了当年的“白月光”,不得不说,没把caffe玩明白一直是我心里一大遗憾。所以也想借着休息时间,再乘时光机回到6年前,读一读当时没有读懂的代码,也了了一桩心愿罢。

二、简介

1. 源码位置

本文主要关注的是caffe一个小的方面,即注册机制(Registration)。在caffe中,涉及到注册的部分主要有:

  • /include/caffe/solver_factory.hpp
  • /include/caffe/layer_factory.hpp
  • /tools/caffe.cpp

2. 何为注册

简单谈谈注册。其实很简单,以python语言为例,我的理解就是有一个全局的字典dict,每写一个新类,就把(类的名字,类)给添加到dict里面去。这样后面给出一个字符串string,我就能得到字符串对应的类实例,如:

REGISTERER = dict()

# Define
class A(object):
    def run(self):
        print(f"Run class: {self.__class__.__name__}")
# Register
REGISTERER[A.__name__] = A


# Define
class B(object):
    def run(self):
        print(f"Run class: {self.__class__.__name__}")
# Register
REGISTERER[B.__name__] = B


if __name__ == "__main__":
    instance = REGISTERER["B"]()
    instance.run()

从上面这个例子可以看出,如果我把和字典相关的几行撤掉,换做 if - elif - else,可能也没啥问题。是的,注册就是完成这么一回事情。当然在C++中代码有不同的写法,下面我们来看看C++是如何实现的。

三、源码分析

主要分析layer_factory.hpp中的写法,会顺便带一下solver_factory.hpp和caffe.cpp中的东西,都是一样的原理。有条件的同学打开代码地址,边看源码边看分析。

首先,layer_factory.hpp是一个头文件,所有的声明都包含在caffe这个namespace下。

其次,头文件中包含了两个模板类LayerRegistry和LayerRegisterer,以及两个宏REGISTER_LAYER_CREATOR和REGISTER_LAYER_CLASS。这是代码的基本结构。下面分别介绍这4个东东。

1. LayerRegistry

先说第一个模板类LayerRegistry,这个类是主要干活的类。

template <typename Dtype>
class LayerRegistry {
 public:
  typedef shared_ptr<Layer<Dtype> > (*Creator)(const LayerParameter&);
  typedef std::map<string, Creator> CreatorRegistry;

  static CreatorRegistry& Registry() {
    static CreatorRegistry* g_registry_ = new CreatorRegistry();
    return *g_registry_;
  }

  // Adds a creator.
  static void AddCreator(const string& type, Creator creator) {
    CreatorRegistry& registry = Registry();
    CHECK_EQ(registry.count(type), 0)
        << "Layer type " << type << " already registered.";
    registry[type] = creator;
  }

  // Get a layer using a LayerParameter.
  static shared_ptr<Layer<Dtype> > CreateLayer(const LayerParameter& param) {
    if (Caffe::root_solver()) {
      LOG(INFO) << "Creating layer " << param.name();
    }
    const string& type = param.type();
    CreatorRegistry& registry = Registry();
    CHECK_EQ(registry.count(type), 1) << "Unknown layer type: " << type
        << " (known types: " << LayerTypeListString() << ")";
    return registry[type](param);
  }

  static vector<string> LayerTypeList() {
    CreatorRegistry& registry = Registry();
    vector<string> layer_types;
    for (typename CreatorRegistry::iterator iter = registry.begin();
         iter != registry.end(); ++iter) {
      layer_types.push_back(iter->first);
    }
    return layer_types;
  }

 private:
  // Layer registry should never be instantiated - everything is done with its
  // static variables.
  LayerRegistry() {}

  static string LayerTypeListString() {
    vector<string> layer_types = LayerTypeList();
    string layer_types_str;
    for (vector<string>::iterator iter = layer_types.begin();
         iter != layer_types.end(); ++iter) {
      if (iter != layer_types.begin()) {
        layer_types_str += ", ";
      }
      layer_types_str += *iter;
    }
    return layer_types_str;
  }
};

看着有点长,我们一点点地来。

typedef shared_ptr<Layer<Dtype> > (*Creator)(const LayerParameter&);
typedef std::map<string, Creator> CreatorRegistry;
  • 首先使用typedef定义了两个类型。Creator是一个函数指针,它代表一类函数,接收LayerParameter类对象,返回一个Layer<Dtype>的smart pointer;其次,定义了一种字典类型CreatorRegistry,它的key是字符串,value是一个函数指针。怎么样?是不是和上面python代码里面的字典对上号了?

static CreatorRegistry& Registry() {
    static CreatorRegistry* g_registry_ = new CreatorRegistry();
    return *g_registry_;
}
  • LayerRegistry中的所有成员方法均为静态方法,也就是说这些函数并不与类实例对象所绑定,而是直接绑在了类上,此其一。注意到Registry函数内部是定义了一个静态的CreatorRegistry*的变量。静态变量有个特点,即它的生存期是持续到程序结束的,这是因为它并不存在与堆区或者栈区,在可重定向文件,也就是.o文件中,会单独为静态变量以及全局变量建立符号表。

      

    回过来,这个函数的意思是,如果第一次执行那么new一个对象,而如果是第二次即以后执行,那么就使用之前new出来的对象,很明显,这就是一种典型的单例模型的写法。还要竹弈一个细节,这个函数的返回值是一个引用,也就是我们就是想拿到函数内作用域的那个对象,而非进行拷贝,这点也很关键。

// Adds a creator.
  static void AddCreator(const string& type, Creator creator) {
    CreatorRegistry& registry = Registry();
    CHECK_EQ(registry.count(type), 0)
        << "Layer type " << type << " already registered.";
    registry[type] = creator;
  }
  • AddCreator仍然是一个静态的成员方法,首先调用上面的Registry函数,拿到字典,然后就是把字符串type和函数指针creator保存到字典里面。

// Get a layer using a LayerParameter.
  static shared_ptr<Layer<Dtype> > CreateLayer(const LayerParameter& param) {
    if (Caffe::root_solver()) {
      LOG(INFO) << "Creating layer " << param.name();
    }
    const string& type = param.type();
    CreatorRegistry& registry = Registry();
    CHECK_EQ(registry.count(type), 1) << "Unknown layer type: " << type
        << " (known types: " << LayerTypeListString() << ")";
    return registry[type](param);
  }
  • CreateLayer的函数实现的目的是,给定LayerParameter类对象,拿到这个parameter里面的type,去之前的字典里面查询有无,如果有,那么就把函数指针registry[type]给取出来,再把param传给这个函数,返回最终的Layer对象的smart pointer。

[小结]

基本上就3个东西,新建字典,插入字典,查询字典。

2. LayerRegisterer

template <typename Dtype>
class LayerRegisterer {
 public:
  LayerRegisterer(const string& type,
                  shared_ptr<Layer<Dtype> > (*creator)(const LayerParameter&)) {
    // LOG(INFO) << "Registering layer type: " << type;
    LayerRegistry<Dtype>::AddCreator(type, creator);
  }
};

非常小的一个类,只有一个构造函数,意味着只在实例化时才起作用。其实实际上也完全可以不用类的方式,而是简单地写个函数达到同样效果。

可以看到它真正的作用是调用了AddCreator,也就是上面所说的把字符串和函数指针给加入到字典里面去。如果是第一次执行,那么LayerRegistry会自动new一个字典出来,后面再执行,都是在那个老字典里面add了。

3. 两个宏

#define REGISTER_LAYER_CREATOR(type, creator)                                  \
  static LayerRegisterer<float> g_creator_f_##type(#type, creator<float>);     \
  static LayerRegisterer<double> g_creator_d_##type(#type, creator<double>)    \

#define REGISTER_LAYER_CLASS(type)                                             \
  template <typename Dtype>                                                    \
  shared_ptr<Layer<Dtype> > Creator_##type##Layer(const LayerParameter& param) \
  {                                                                            \
    return shared_ptr<Layer<Dtype> >(new type##Layer<Dtype>(param));           \
  }                                                                            \
  REGISTER_LAYER_CREATOR(type, Creator_##type##Layer)

两个宏的关系,下面的REGISTER_LAYER_CLASS调用了上面的REGISTER_LAYER_CREATOR,并且是对外使用的宏。宏的两个基本只是是单警号#变成字符串,双井号##黏贴左右。

那么从下面开始看起,首先声明并了一个函数,举个例子,此处的type为Concat,那么函数名字是Creator_ConcatLayer。这个函数就是把param传到了类ConcatLayer里面new出一个实例,然后返回对应的smart pointer。

然后进入到上面的宏。声明了两个静态全局变量,这两个静态全局变量都是LayerRegisterer类型的,前面已经说过,这个类型除了实例化的时候起点作用,其他时候不齐作用,所以变量本身不重要,重要的是把字符串“Concat”(也就是#type)和模板实例化后的函数指针creator<float>作为类参传给LayerRegisterer,也就能传给LayerRegistry里面的字典拉,也就能注册啦。

嚯,好家伙!绕了这么一大圈,终于给盘明白了。其实它的逻辑和我写的那个简陋的python代码并无二致,就是没字典建字典,当你新写了一个类,你把这个类名字和类本身,当成一条记录插入到字典里面去,后面别人就好查询了呗~

至于solver_factory完全是一模一样的东西。而caffe.cpp也就是定义了一大堆函数,定义完每个函数后,将函数名和函数指针给存到字典里面去,需要时再查询,再用。

四、使用流程

最后再简单地聊一聊这个注册机制的使用,仍以ConcatLayer为例(src/caffe/layers/concat_layer.cpp),定义完Layer类的各个方法以后,会执行下面2行代码:

INSTANTIATE_CLASS(ConcatLayer);
REGISTER_LAYER_CLASS(Concat);

其中,宏INSTANTIATE_CLASS定义在:/include/caffe/common.hpp,

// Instantiate a class with float and double specifications.
#define INSTANTIATE_CLASS(classname) \
  char gInstantiationGuard##classname; \
  template class classname<float>; \
  template class classname<double>

定义了一个字符(没明白这一块),然后用float和double实例化了两个模板类,在这个例子里面,我们把ConcatLayer<Dtype>给实例化成ConcatLayer<float>和ConcatLayer<double>。

那么第2行的REGISTER_LAYER_CLASS在前文已经分析的比较清楚了,我们把“Concat”和对应的返回ConcatLayer类实例的函数,添加到字典中。

最后,在Net的初始化里面(src/caffe/net.cpp),

template <typename Dtype>
void Net<Dtype>::Init(const NetParameter& in_param) {
    // ... ...
    for (int layer_id = 0; layer_id < param.layer_size(); ++layer_id) {
        // ... ...
        // Setup layer.
        const LayerParameter& layer_param = param.layer(layer_id);
        layers_.push_back(LayerRegistry<Dtype>::CreateLayer(layer_param));  // vector<shared_ptr<Layer<Dtype> > > layers_;
        layers_[layer_id]->SetUp(bottom_vecs_[layer_id], top_vecs_[layer_id]);

有选择地截取了net.cpp里面的一些东西,可以看到,在Init方法里面,通过对layer_id的for循环,我们拿到对应的layer_param,然后传到了CreateLayer里面去创建Layer类实例的smart pointer,一并存储到layer_这个vector里面,后面就拿layers_这个数据结构做setup,forward,backward等等一系列事啦。

五、总结

简单地看了一下caffe中的注册机制,其实很多时候读不懂代码,一方面固然是基础薄弱,但更重要的还是不知道他想去干什么。如果能把顶层的逻辑捋清,相信一切都能够迎刃而解~~

目录
相关文章
|
8月前
|
存储 缓存 Java
【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践
【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践
|
缓存 NoSQL Java
分布式系列教程(01) -Ehcache缓存架构
分布式系列教程(01) -Ehcache缓存架构
329 0
|
8月前
|
缓存 NoSQL Java
SpringCache通用缓存学习
SpringCache通用缓存学习
|
安全 Java 数据库
深入Spring Security魔幻山谷-获取认证机制核心原理讲解(新版)
在神秘的Web系统世界里,有一座名为Spring Security的山谷,它高耸入云,蔓延千里,鸟飞不过,兽攀不了。这座山谷只有一条逼仄的道路可通。然而,若要通过这条道路前往另一头的世界,就必须先拿到一块名为token的令牌,只有这样,道路上戍守关口的士兵才会放行。
55 0
|
存储 缓存 监控
【深入浅出Spring原理及实战】「缓存Cache开发系列」带你深入分析Spring所提供的缓存Cache抽象详解的核心原理探索
缓存的工作机制是先从缓存中读取数据,如果没有再从慢速设备上读取实际数据,并将数据存入缓存中。通常情况下,我们会将那些经常读取且不经常修改的数据或昂贵(CPU/IO)的且对于相同请求有相同计算结果的数据存储到缓存中。
202 1
|
设计模式 存储 开发框架
C++ 插件机制的实现原理、过程、及使用
C++ 插件机制的实现原理、过程、及使用
|
缓存 NoSQL Java
SpringCache通用缓存学习(二)
SpringCache通用缓存学习
191 2
SpringCache通用缓存学习(二)
|
SQL 缓存 NoSQL
SpringCache通用缓存学习(一)
SpringCache通用缓存学习
214 2
SpringCache通用缓存学习(一)
|
负载均衡 Dubbo Java
分布式注册的中心的实现原理|学习笔记
快速学习分布式注册的中心的实现原理
229 0
分布式注册的中心的实现原理|学习笔记
|
缓存 关系型数据库 MySQL
图文详述Eureka的缓存机制/三级缓存
图文详述Eureka的缓存机制/三级缓存
736 0
图文详述Eureka的缓存机制/三级缓存