NCNN 学习(1)-编译与算子注册

0 NCNN 介绍

ncnn(Nebula Convolutional Neural Network)是一个高效、轻量级的深度学习框架,支持各种神经网络模型,如pytorch、tensorflow、onnx等,以及多种硬件后端,如x86、arm、riscv、mips、vulkan等。由 Tencent 开发,具有以下特性:

  • 支持卷积神经网络,支持多输入和多分支结构,可计算部分分支

    ncnn 支持卷积神经网络结构,以及多分支多输入的复杂网络结构,如主流的 VGG、GoogLeNet、ResNet、SqueezeNet 等。

  • 无任何第三方库依赖,不依赖 BLAS/NNPACK 等计算框架

    ncnn 不依赖任何第三方库,完全独立实现所有计算过程

  • 纯 C++ 实现,跨平台,支持 Android、 iOS 等

    ncnn 代码全部使用 C/C++ 实现,跨平台的 cmake 编译系统,可在已知的绝大多数平台编译运行,如 Linux、Windows、Mac OS、Android、iOS 等。采用 C++ 03 标准实现,只用到了 std::vector 和 std::string 两个 STL 模板

  • ARM NEON Instrinsic 优化,计算速度快

    ncnn 为手机端 CPU 运行做了深度细致的优化,使用 ARM NEON 指令集实现卷积层、全连接层、池化层等大部分 CNN 关键层。对于寄存器压力较大的 armv7 架构,手工编写 neon 汇编,内存预对齐,cache 预缓存,排列流水线,充分利用一切硬件资源,防止编译器意外负优化。

    这个就很奈斯了,大有可用,本来之前还想着自己实现的,找到了这个就可以直接用了。

  • 精细的内存管理和数据结构设计,内存占用低

    ,在卷积层、全连接层等计算量较大的层实现中,没有采用通常框架中的 im2col + 矩阵乘法,因为这种方式会构造出非常大的矩阵,消耗大量内存。因此,ncnn 采用原始的滑动窗口卷积实现,并在此基础上进行优化,大幅节省了内存。在前向网络计算过程中,ncnn 可自动释放中间结果所占用的内存,进一步减少内存占用。

  • 支持多核并行计算加速,ARM big.LITTLE CPU 调度优化

    ncnn 提供了基于 OpenMP 的多核心并行计算加速,在多核心 CPU 上启用后能够获得很高的加速收益。ncnn 提供线程数控制接口,可以针对每个运行实例分别调控,满足不同场景的需求。

  • 可扩展的模型设计,支持 8bit 量化和半精度浮点存储

  • 支持直接内存零拷贝引用加载网络模型

  • 可注册自定义层实现并扩展

    ncnn 提供了注册自定义层实现的扩展方式,可以将自己实现的特殊层内嵌到 ncnn 的前向计算过程中

1 NCNN 编译与安装

最直接的编译方式就是使用 cmake:

bash 复制代码
# 在 ncnn 项目目录下
mkdir build && cd build
cmake ..
make -j 

如果在编译的时候需要设置一些 cmake 变量等操作来控制编译过程,可以使用 toolchains 目录下面的 cmake 脚本,自己编写编译脚本,来对多种不同的处理器和编译器提供支持:
build_custom.sh

bash 复制代码
set -e
set -x
if [ "${1}" = "x86" ]; then
  TOOLCHAIN_FILE=./toolchains/host.gcc.toolchain.cmake
elif [ "${1}" = "arm" ]; then
  TOOLCHAIN_FILE=./toolchains/aarch64-linux-gnu.toolchain.cmake
elif [ "${1}" = "cross" ]; then
  TOOLCHAIN_FILE=./toolchains/aarch64-linux-gnu.toolchain.cmake
elif [ "${1}" = "pi" ]; then
  TOOLCHAIN_FILE=./toolchains/arm-linux-gnueabihf.toolchain.cmake
else
  echo "Usage: ./compile.sh <x86|arm|cross|pi>"
  exit 1
fi

OS_TYPE=`uname -s`
if [ "${OS_TYPE}" == "Linux" ]; then
  CPU_NUM=`cat /proc/cpuinfo | grep "processor" | wc -l`
elif [ "${OS_TYPE}" = "Darwin" ]; then
  CPU_NUM=`sysctl -n machdep.cpu.core_count`
else
  echo "Unknown OS type: ${OS_TYPE}, use cpu number 4 in default"
  CPU_NUM=2
fi
SOURCE_DIR=./
BUILD_DIR=${SOURCE_DIR}/build/
INSTALL_DIR=${BUILD_DIR}/install/

cmake \
  -S ${SOURCE_DIR} \
  -B ${BUILD_DIR} \
  -G "Unix Makefiles" \
  -DNCNN_SHARED_LIB=ON \
  -DENABLE_RTTI=ON \
  -DNCNN_DISABLE_RTTI=OFF \
  -DNCNN_BUILD_BENCHMARK=OFF \
  -DNCNN_BUILD_EXAMPLES=OFF \
  -DNCNN_BUILD_TOOLS=ON \
  -DNCNN_BUILD_TESTS=OFF \
  -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_INSTALL_PREFIX=${INSTALL_DIR} \
  -DCMAKE_EXPORT_COMPILE_COMMANDS=1 \
  -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_FILE}

cmake --build ${BUILD_DIR} -j${CPU_NUM}

上面的编译脚本提供了这样的功能:

  • 通过脚本参数的不同,而选择使用 toolchains 中提供的不同的编译脚本、
  • 通过获取 cpu number,作为最后编译时候使用的最大 job 数,来尽可能的优化编译时间
  • 通过使用 cmake 的 -S-B-G选项,来支持指定代码目录、编译目录以及指定生成器
  • 通过使用-D来选择编译过程中打开、关闭哪些开关,ncnn 支持的开关位于项目根目录的CMakeLists.txt中,例如可以控制是否生成动态库、是否编译 tests、是否编译 tools、指定安装目录等

上面的编译脚本使用:

  • 给脚本添加执行权限
bash 复制代码
chmod +x ./build_custom.sh
  • 根据不同平台执行编译
bash 复制代码
# x86
./build_custom.sh x86

# arm
./build_custom.sh arm
  • 安装,编译完成后,在编译目录执行 make install 就可以完成到指定目录的安装
bash 复制代码
cd build
make install

2 算子注册

2.1 DEFINE_LAYER_CREATOR

这个宏创建了一个 layer 的对象,与之对应的还有一个DEFINE_LAYER_DESTROYER宏。这个的实现位于 https://github.com/Tencent/ncnn/blob/20220701/src/layer.h#L201:

cpp 复制代码
#define DEFINE_LAYER_CREATOR(name)                          \
    ::ncnn::Layer* name##_layer_creator(void* /*userdata*/) \
    {                                                       \
        return new name;                                    \
    }

#define DEFINE_LAYER_DESTROYER(name)                                      \
    void name##_layer_destroyer(::ncnn::Layer* layer, void* /*userdata*/) \
    {                                                                     \
        delete layer;                                                     \
    }   
2.2 layer_registry_entry

算子的注册过程中,注册的是用于创建指定 layer 的函数对象,需要一个统一的数据结构用来存储这个函数对象,这就要用到layer_registry_entry,定义于 https://github.com/Tencent/ncnn/blob/20220701/src/layer.h#L170,除了这个数据结构,ncnn 还支持用户自定义算子注册和释放,需要用到custom_layer_registry_entry,这两个数据结构如下:

cpp 复制代码
// layer factory function
typedef Layer* (*layer_creator_func)(void*);
typedef void (*layer_destroyer_func)(Layer*, void*);

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

struct custom_layer_registry_entry
{
#if NCNN_STRING
    // layer type name
    const char* name;
#endif // NCNN_STRING
    // layer factory entry
    layer_creator_func creator;
    layer_destroyer_func destroyer;
    void* userdata;
};
2.3 ncnn_add_layer

这是一个 cmake 的宏,定义于 https://github.com/Tencent/ncnn/blob/20220701/cmake/ncnn_add_layer.cmake#L79,总体来讲,所有 layer 的注册过程就是通过这个 cmake 宏完成的:

cpp 复制代码
macro(ncnn_add_layer class)
    string(TOLOWER ${class} name)

    # WITH_LAYER_xxx option
    if(${ARGC} EQUAL 2)
        option(WITH_LAYER_${name} "build with layer ${name}" ${ARGV1})
    else()
        option(WITH_LAYER_${name} "build with layer ${name}" ON)
    endif()

    if(NCNN_CMAKE_VERBOSE)
        message(STATUS "WITH_LAYER_${name} = ${WITH_LAYER_${name}}")
    endif()

    if(WITH_LAYER_${name})
        list(APPEND ncnn_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/layer/${name}.cpp)

        # look for arch specific implementation and append source
        # optimized implementation for armv7, aarch64 or x86
        set(LAYER_ARCH_SRC ${CMAKE_CURRENT_SOURCE_DIR}/layer/${NCNN_TARGET_ARCH}/${name}_${NCNN_TARGET_ARCH}.cpp)
        if(EXISTS ${LAYER_ARCH_SRC})
            set(WITH_LAYER_${name}_${NCNN_TARGET_ARCH} 1)
            list(APPEND ncnn_SRCS ${LAYER_ARCH_SRC})
        endif()

        set(LAYER_VULKAN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/layer/vulkan/${name}_vulkan.cpp)
        if(NCNN_VULKAN AND EXISTS ${LAYER_VULKAN_SRC})
            set(WITH_LAYER_${name}_vulkan 1)
            list(APPEND ncnn_SRCS ${LAYER_VULKAN_SRC})
        endif()
    endif()

下面是几个调用这个宏来注册 layer 的代码片段,位于 src/CMakeLists.txt:

cpp 复制代码
...
ncnn_add_layer(Convolution1D)
ncnn_add_layer(Pooling1D)
ncnn_add_layer(ConvolutionDepthWise1D)
ncnn_add_layer(Pooling3D)
ncnn_add_layer(MatMul)
ncnn_add_layer(Deconvolution1D)
ncnn_add_layer(DeconvolutionDepthWise1D)
ncnn_add_layer(Einsum)

...

configure_file(layer_declaration.h.in ${CMAKE_CURRENT_BINARY_DIR}/layer_declaration.h)
configure_file(layer_registry.h.in ${CMAKE_CURRENT_BINARY_DIR}/layer_registry.h)
configure_file(layer_type_enum.h.in ${CMAKE_CURRENT_BINARY_DIR}/layer_type_enum.h)

这里的 configure_file 很重要,它把 cmake 代码和 c++ 代码连接起来,它可以把 cmake 中定义的变量的内容,替换到指定的目标文件中,比如上面的layer_declaration.h.in,它的内容如下:

cpp 复制代码
@layer_declaration@

这个文件只有一行内容,layer_declaration是 cmake 中的变量,编译过后,就会生成build/src//layer_declaration.h这个文件,这个文件的内容就是layer_declaration变量的值,在 ncnn 中,它长下面这个样子:

cpp 复制代码
#include "layer/absval.h"
namespace ncnn {
class AbsVal_final : virtual public AbsVal
{
public:
    virtual int create_pipeline(const Option& opt) {
        { int ret = AbsVal::create_pipeline(opt); if (ret) return ret; }
        return 0;
    }
    virtual int destroy_pipeline(const Option& opt) {
        { int ret = AbsVal::destroy_pipeline(opt); if (ret) return ret; }
        return 0;
    }
};
DEFINE_LAYER_CREATOR(AbsVal_final)
} // namespace ncnn
...

这里调用了前面提到的DEFINE_LAYER_CREATOR这个宏,也就是定义了AbsVal这个 layer 的创建函数,这里只列了AbsVal这一个 layer,实际上这个自动生成的文件内容包含后面有所有的 layer 的创建函数。这个 layer 的创建函数的注册涉及到layer_registry.h.in这个文件,它的内容如下:

cpp 复制代码
static const layer_registry_entry layer_registry[] = {
@layer_registry@
};
...

这里面的layer_registry,也是一个 cmake 变量,在编译之后这个文件会被自动生成·build/src/layer_registry.h·这个文件,内容如下:

cpp 复制代码
static const layer_registry_entry layer_registry[] = {
#if NCNN_STRING
{"AbsVal", AbsVal_final_layer_creator},
#else
{AbsVal_final_layer_creator},
#endif
...
...
};

这就是 layer 注册的过程,layer 创建函数被维护到layer_registry这个数组中,数组的元素类型就是前面提到的layer_registry_entry

前面提到 cmake 会给上面的这些变量赋值,具体细节就在前面给出来的ncnn_add_layer这个宏里面,实际上就是写入hard code:

cpp 复制代码
if(WITH_LAYER_${name})
        set(layer_declaration "${layer_declaration}namespace ncnn {\n${layer_declaration_class}\n{\n")
        set(layer_declaration "${layer_declaration}public:\n")
        set(layer_declaration "${layer_declaration}    virtual int create_pipeline(const Option& opt) {\n${create_pipeline_content}        return 0;\n    }\n")
        set(layer_declaration "${layer_declaration}    virtual int destroy_pipeline(const Option& opt) {\n${destroy_pipeline_content}        return 0;\n    }\n")
        set(layer_declaration "${layer_declaration}};\n")
        set(layer_declaration "${layer_declaration}DEFINE_LAYER_CREATOR(${class}_final)\n} // namespace ncnn\n\n")
    endif()

layer_registry的赋值也是一样的:

cpp 复制代码
if(WITH_LAYER_${name})
        set(layer_registry "${layer_registry}#if NCNN_STRING\n{\"${class}\", ${class}_final_layer_creator},\n#else\n{${class}_final_layer_creator},\n#endif\n")
    else()
        set(layer_registry "${layer_registry}#if NCNN_STRING\n{\"${class}\", 0},\n#else\n{0},\n#endif\n")
    endif()
2.4 create_layer

layer 的注册过程明了之后,layer 的创建就很清晰了:

cpp 复制代码
Layer* create_layer(int index)
{
    if (index < 0 || index >= layer_registry_entry_count)
        return 0;

    // clang-format off
    // *INDENT-OFF*
    layer_creator_func layer_creator = 0;
#if NCNN_RUNTIME_CPU && NCNN_AVX512
    if (ncnn::cpu_support_x86_avx512())
    {
        layer_creator = layer_registry_avx512[index].creator;
    }
    else
#endif// NCNN_RUNTIME_CPU && NCNN_AVX512
#if NCNN_RUNTIME_CPU && NCNN_FMA
    if (ncnn::cpu_support_x86_fma())
    {
        layer_creator = layer_registry_fma[index].creator;
    }
    else
#endif// NCNN_RUNTIME_CPU && NCNN_FMA
#if NCNN_RUNTIME_CPU && NCNN_AVX
    if (ncnn::cpu_support_x86_avx())
    {
        layer_creator = layer_registry_avx[index].creator;
    }
    else
#endif // NCNN_RUNTIME_CPU && NCNN_AVX
#if NCNN_RUNTIME_CPU && NCNN_MSA
    if (ncnn::cpu_support_mips_msa())
    {
        layer_creator = layer_registry_msa[index].creator;
    }
    else
#endif // NCNN_RUNTIME_CPU && NCNN_MSA
#if NCNN_RUNTIME_CPU && NCNN_RVV
    if (ncnn::cpu_support_riscv_v())
    {
        layer_creator = layer_registry_rvv[index].creator;
    }
    else
#endif // NCNN_RUNTIME_CPU && NCNN_RVV
    {
        layer_creator = layer_registry[index].creator;
    }
    // *INDENT-ON*
    // clang-format on
    if (!layer_creator)
        return 0;

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

这里的 layer 创建是有优先级的,特定后端的加速实现如果存在,就创建特定实现的 layer,如果没有特定实现,就创建最 naive 的 layer 实现。这里特定后端的加速实现指的是例如在 x86 机器上基于 avx512/avx2/avx/fma 等矢量加速指令的实现,在 arm 机器上基于 ARM Neon 指令的加速实现等。

相关推荐
Colddd_d几秒前
动手学深度学习(五)循环神经网络RNN
人工智能·rnn·深度学习
fydw_71519 分钟前
PyTorch 池化层详解
人工智能·深度学习
EterNity_TiMe_38 分钟前
【Linux基础IO】深入Linux文件描述符与重定向:解锁高效IO操作的秘密
linux·运维·服务器·学习·性能优化·学习方法
奥利给少年43 分钟前
深度学习——管理模型的参数
人工智能·深度学习
好家伙VCC1 小时前
STM32与51单片机的区别:是否应该直接学习STM32?
stm32·学习·51单片机
秋秋秋叶1 小时前
Python学习——【3.1】函数
python·学习
攸攸太上3 小时前
Docker学习
java·网络·学习·docker·容器
天蓝蓝235283 小时前
PyTorch开源的深度学习框架
pytorch·深度学习·开源
Ylucius3 小时前
JavaScript 与 Java 的继承有何区别?-----原型继承,单继承有何联系?
java·开发语言·前端·javascript·后端·学习