Skip to content

Instantly share code, notes, and snippets.

@kketernality
Last active November 22, 2020 02:58
Show Gist options
  • Save kketernality/75d6e37018bc61871b5bfd643df5278c to your computer and use it in GitHub Desktop.
Save kketernality/75d6e37018bc61871b5bfd643df5278c to your computer and use it in GitHub Desktop.

Detail behind NCNN's factory pattern

NCNN adopts the factory pattern to create the layers of a nueral network. It's also the way the well-known library Caffe takes. It differs from Caffe in the implementation of the registry table. On one hand, the Caffe registry is populated in runtime as the side effect of initializion of global variable (which is a popular way for library initialization). On the other hand, the NCNN registry is determined in compile time. The registry is generated in a brilliant way using CMake instead of a hand-crafted table. NCNN's approach provides several benefits compared to Caffe's approach.

First, it's suitable for building a static library. When building a static library, the linker will strip any unused global variable to minimize the size of the library. This makes sense but it also strips the global variable which need to be inintialized to insert te layer creator into the registry. Tricky linker flags and related instrutions are required to resolve this issue. By creating the registry in compile time, it ensures the completeness of the registry when building static library.

Second, the size of the library can be minimzed. In Caffe's approach, the registry is registered during the initialization of a global variable defined in the layer implementation file. As a result all of the layer implementation file is compiled into the library. In constrast, NCNN selects the implementation files using a macro to compile into the library because the registry is known in compile time.

Registry and Layer Creator

The infrastructure of factory pattern is defined in the file layer.h and layer.cpp. The layer creator function layer_creator_func and the create method Layer* create_layer() are declared as the following. Note that there is NCNN_STRING flag to enable or disable the usage of string. This flag alters the structure of the registry and the creation method. It is used to further minimize the size of the library. Normally, it is simply defined.

// layer factory function
typedef Layer* (*layer_creator_func)();

#if NCNN_STRING
// get layer type from type name
int layer_to_index(const char* type);
// create layer from type name
Layer* create_layer(const char* type);
#endif // NCNN_STRING
// create layer from layer type
Layer* create_layer(int index);

In NCNN, the registry layer_registry consists of a list of entries instead of a map in Caffe. It includes the layer_registry.h which is populated during CMake configuration which will be mentioned in the next section. It contains the information of layer name and its creator.

struct layer_registry_entry
{
#if NCNN_STRING
    // layer type name
    const char* name;
#endif // NCNN_STRING
    // layer factory entry
    layer_creator_func creator;
};

static const layer_registry_entry layer_registry[] =
{
#include "layer_registry.h"
};

In file layer.cpp, the implementation of create_layer() method simply traverses the registry to match the layer name and return the assocaited creator.

#if NCNN_STRING
int layer_to_index(const char* type)
{
    for (int i=0; i<layer_registry_entry_count; i++)
    {
        if (strcmp(type, layer_registry[i].name) == 0)
            return i;
    }

    return -1;
}

Layer* create_layer(const char* type)
{
    int index = layer_to_index(type);
    if (index == -1)
        return 0;

    return create_layer(index);
}
#endif // NCNN_STRING

Layer* create_layer(int index)
{
    if (index < 0 || index >= layer_registry_entry_count)
        return 0;

    layer_creator_func layer_creator = layer_registry[index].creator;
    if (!layer_creator)
        return 0;

    Layer* layer = layer_creator();
    layer->typeindex = index;
    return layer;
}

Generate the registry of layer factory

File src/CMakeLists.txt

The CMake file gathers the source files and builds the ncnn static library. During configuration, it generates the layer_declaration.h, layer_registry.h and layer_type_enum.h which contain the essence part of factory pattern according to user-provided options to turn on or off the compilation of specific layers.

NCNN uses a fancy way to find whether there exists a architecture specific implemention for the layer. For exmaple, given layer ${class} and architecture ${arch}. First, convert ${class} to all lowercase ${name}. Second, it checks if there exists a file in the path ./layer/${arch}/${name}_${arch}.cpp which is the where the architecure specific file locates. On the other hand, the architecure independent implementation file locates at ./layer/${name}.cpp.

macro(ncnn_add_layer class)
  string(TOLOWER ${class} name}
  ......
  else()
    option(WITH_LAYER_${name} “...” ON)
  endif()
  ......
  
  if(WITH_LAYER_${name})
    # Append arch-independent impl file to build
    list(APPEND ncnn_SRCS .../layer/${name}.cpp)
    
    # Determine current arch
    ......
    else()
      set(arch x86)
    endif()

    # Look for the optimized impl file for this arch
    # The file path should follow the rule
    set(LAYER_ARCH_SRC .../layer/${arch}/${name}_${arch}.cpp)
    if(EXISTS ${LAYER_ARCH_SRC})
      set(WITH_LAYER_${name}_${arch} 1)
      list(APPEND ncnn_SRCS ${LAYER_ARCH_SRC})
    endif()
    .......
  endif()
    
  if(WITH_LAYER_${name})
    # Append arch-independent to `layer_declaration`
    set(layer_declaration “${layer_declaration} ...”)
  endif()
    
  if(WITH_LAYER_${name}_${arch})
    # Append arch-specific to `layer_declaration`
    set(layer_declaration “${layer_declaration} ...”)
  endif
  ......  
 
  if(WITH_LAYER_${name})
    # Append the creator to `layer_registry`
    set(layer_registry “${layer_registry} {\”${class}\”, ${class}_final_layer_creator}\n”)
  endif()
  # Type enum file
  set(layer_type_enum “${layer_type_enum}${class} = ${idx}”)
  math(EXPR idx “${idx}+1”)
endmacro()

......
ncnn_add_layer(Convolution)
......
configure_file(layer_declaration.h.in .../layer_declaration.h)
configure_file(layer_registry.h.in .../layer_registry.h)
configure_file(layer_type_enum.h.in .../layer_type_enum.h)
......

add_library(ncnn STATIC ${ncnn_SRCS})

Pre-substitution Files

There are three template files (more if count gpu support using Vulkan). All of them only contains a placeholder for subsitution with Cmake variables to fullfill the NCNN's factory pattern.

File layer_declaration.h.in

@layer_declaration@

File layer_registry.h.in

@layer_registry@

File layer_type_enum.h.in

@layer_type_enum@

File layer_declaration.h

This is generated from layer_declaration.h.in by subsituting @layer_declaration@ with Cmake variable layer_declaration during Cmake configuration. This file creates a new class ${name}_final which inherits from arch-independent and arch-sepcific implementation classes. This new class is then wrapped as the final creator using C macro named ##name_layer_creator().

.........
#include “layer/convolution.h”
#include “layer/x86/convolution_x86.h”
namespace ncnn {
class Convolution_final : 
  virtual public Convolution, 
  virtual public Convolution_x86
{
public:
  virtual int create_pipeline(const Option& opt) 
  { ...; }
  virtual int destroy_pipeline(const Option& opt)
  { ...; }
};
DEFINE_LAYER_CREATOR(Convolution_final)
} // namespace ncnn
.........

This file is included in the head of layer.cpp to provide the declaration of final class of layer and its associated creator for every layer used in the NCNN library.

.........
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Woverloaded-virtual"
#endif
#include "layer_declaration.h"
#ifdef __clang__
#pragma clang diagnostic pop
#endif
.........

File layer_registry.h

This file is generated using the similar way like the layer_declaration.h using Cmake configuration. It contains the body part of the registry table. If the NCNN_STRING is enabled, each entry pair contains the final class of the layer ##name_final and its associated creater ##name_final_layer_creater. Otherwise, only the associated creator is included.

......
#if NCNN_STRING
{"Convolution",Convolution_final_layer_creator},
#else
{Convolution_final_layer_creator},
#endif
......

The file is included in the layer.cpp to complete the registry table of factory.

static const layer_registry_entry layer_registry[] =
{
#include "layer_registry.h"
};

File layer_type_enum.h

This file is generated using the similar way like the layer_declaration.h using Cmake configuration. It contains the class name and its enum value.

......
Convolution = 6,
......

The file is included in the layer_type.h to complete the enum in LayerType. It is not related to the factory pattern directory (need to dig more detail).

namespace LayerType {
enum
{
#include "layer_type_enum.h"
    CustomBit = (1<<8),
};
} // namespace LayerType

} // namespace ncnn
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment