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:

# 在 ncnn 项目目录下
mkdir build && cd build
cmake ..
make -j 

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

set -e
set -x
if [ "${1}" = "x86" ]; thenTOOLCHAIN_FILE=./toolchains/host.gcc.toolchain.cmake
elif [ "${1}" = "arm" ]; thenTOOLCHAIN_FILE=./toolchains/aarch64-linux-gnu.toolchain.cmake
elif [ "${1}" = "cross" ]; thenTOOLCHAIN_FILE=./toolchains/aarch64-linux-gnu.toolchain.cmake
elif [ "${1}" = "pi" ]; thenTOOLCHAIN_FILE=./toolchains/arm-linux-gnueabihf.toolchain.cmake
elseecho "Usage: ./compile.sh <x86|arm|cross|pi>"exit 1
fiOS_TYPE=`uname -s`
if [ "${OS_TYPE}" == "Linux" ]; thenCPU_NUM=`cat /proc/cpuinfo | grep "processor" | wc -l`
elif [ "${OS_TYPE}" = "Darwin" ]; thenCPU_NUM=`sysctl -n machdep.cpu.core_count`
elseecho "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、指定安装目录等

上面的编译脚本使用:

  • 给脚本添加执行权限
chmod +x ./build_custom.sh
  • 根据不同平台执行编译
# x86
./build_custom.sh x86# arm
./build_custom.sh arm
  • 安装,编译完成后,在编译目录执行 make install 就可以完成到指定目录的安装
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:

#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,这两个数据结构如下:

// 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 nameconst char* name;
#endif // NCNN_STRING// layer factory entrylayer_creator_func creator;
};struct custom_layer_registry_entry
{
#if NCNN_STRING// layer type nameconst char* name;
#endif // NCNN_STRING// layer factory entrylayer_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 宏完成的:

macro(ncnn_add_layer class)string(TOLOWER ${class} name)# WITH_LAYER_xxx optionif(${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 x86set(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:

...
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,它的内容如下:

@layer_declaration@

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

#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这个文件,它的内容如下:

static const layer_registry_entry layer_registry[] = {
@layer_registry@
};
...

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

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:

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的赋值也是一样的:

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 的创建就很清晰了:

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_AVX512if (ncnn::cpu_support_x86_avx512()){layer_creator = layer_registry_avx512[index].creator;}else
#endif// NCNN_RUNTIME_CPU && NCNN_AVX512
#if NCNN_RUNTIME_CPU && NCNN_FMAif (ncnn::cpu_support_x86_fma()){layer_creator = layer_registry_fma[index].creator;}else
#endif// NCNN_RUNTIME_CPU && NCNN_FMA
#if NCNN_RUNTIME_CPU && NCNN_AVXif (ncnn::cpu_support_x86_avx()){layer_creator = layer_registry_avx[index].creator;}else
#endif // NCNN_RUNTIME_CPU && NCNN_AVX
#if NCNN_RUNTIME_CPU && NCNN_MSAif (ncnn::cpu_support_mips_msa()){layer_creator = layer_registry_msa[index].creator;}else
#endif // NCNN_RUNTIME_CPU && NCNN_MSA
#if NCNN_RUNTIME_CPU && NCNN_RVVif (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 onif (!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 指令的加速实现等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1540481.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

有关JS下隐藏的敏感信息

免责声明&#xff1a;本文仅做分享&#xff01; 目录 JavaScript 介绍 核心组成 工具 FindSomething ** 浏览器检查 ** LinkFinder URLfinder ** SuperSearchPlus ** ffuf ParasCollector waymore Packer Fuzzer JS逆向 应用&#xff1a; 小结&#xff1a; Ja…

简明linux系统编程--互斥锁--TCP--UDP初识

目录 1.互斥锁 2.信号 2.1介绍 2.2信号的内核机制 3.linux网络编程概述 3.1一览七层协议 3.2一览数据传输过程 3.3四层网络模型 3.4服务端和客户端的数据交互 4.TCP服务端编程 5.TCP客户端编程 6.UDP服务端编程 7.UDP客户端编程 1.互斥锁 互斥锁也是和信号量一样&a…

【C++】——优先级队列和容器适配器

文章目录 优先级队列容器适配器 优先级队列 优先级队列是一种特殊的队列&#xff0c;他的元素出队列顺序并不按照先进先出原则&#xff0c;而是根据元素的优先级来。优先级高的先出&#xff0c;优先级低的后出。(类似于堆) 优先级队列常用成员函数&#xff1a; empty()&#x…

6.C++程序中的基本数据类型

数据类型是指在C中用于声明不同类型变量或函数的一个系统或抽象或者是一个分类&#xff0c;它决定了变量存储占用的内存空间以及解析存储的位模式。其实数据类型可以理解为固定内存大小的别名&#xff0c;是创建变量的模具&#xff0c;具体使用哪种模具&#xff08;包括自定义&…

ai写作软件排行榜前十名,5个软件帮助你快速使用ai写作

ai写作软件排行榜前十名&#xff0c;5个软件帮助你快速使用ai写作 AI写作软件已经成为许多人工作和创作中的重要工具&#xff0c;尤其是在快速生成内容、提高写作效率以及优化文本方面。以下是五款优秀的AI写作软件&#xff0c;它们能够帮助你轻松完成各种写作任务&#xff0c…

芯片级配件产品研发的小众企业生存之路

在半导体行业中&#xff0c;芯片级配件产品的研发一直是一个充满挑战的领域&#xff0c;尤其是对于小众企业而言&#xff0c;如何在技术壁垒高、资金需求大的市场中生存并发展&#xff0c;成为了业界普遍关注的问题。芯片级配件产品涉及到晶圆制造、封装、测试等多个复杂工艺环…

计算机人工智能前沿进展-大语言模型方向-2024-09-20

计算机人工智能前沿进展-大语言模型方向-2024-09-20 1. Multimodal Fusion with LLMs for Engagement Prediction in Natural Conversation Authors: Cheng Charles Ma, Kevin Hyekang Joo, Alexandria K. Vail, Sunreeta Bhattacharya, Alvaro Fern’andez Garc’ia, Kailan…

码头童话,“丈量”行业数智化转型

作者 | 曾响铃 文 | 响铃说 一箱车厘子从地球正对的另一边远渡重洋来到中国&#xff0c;而一旦到达&#xff0c;5个小时内它就能变成北京、天津、河北、河南等区域老百姓果盘里的美味。 这一幕&#xff0c;来自央视联合华为制作发布的《新智中国说-谈智一会间》第一期“码头…

win10下使用docker、k8s部署java应用

在上一篇文章 Windows10上Docker和Kubernetes的安装 中&#xff0c;已经介绍了在 Windows10上安装Docker和Kubernetes &#xff0c;有了这个环境基础之后&#xff0c;就可以用来部署服务了 在项目目录下新建Dockfile文件&#xff0c;内容如下&#xff08;请根据实际情况调整&am…

鸿蒙开发之ArkUI 界面篇 十五 交叉轴对其方式

鸿蒙界面有两个容器一个是Colum、一个是Row&#xff0c;Colum主轴是垂直方向&#xff0c;交叉轴是水平方向&#xff0c;Row的主轴是水平方向&#xff0c;交叉轴是垂直方向&#xff0c;对应方向调整子控件的话&#xff0c;justifyContent调整的是主轴方向的子控件距离&#xff0…

Java发送Outlook邮件:从设置到发送攻略!

Java发送Outlook邮件详细步骤&#xff01;如何使用Java发邮件&#xff1f; Java作为一种广泛使用的编程语言&#xff0c;提供了强大的功能来实现自动化邮件发送。AokSend将详细介绍如何使用Java发送Outlook邮件&#xff0c;从基本的设置到最终的发送过程。 Java发送Outlook邮…

美元降息,对普通人有哪些影响?

美元降息&#xff0c;对普通人有哪些影响&#xff1f; 美元降息了。很多朋友都说我又不炒股&#xff0c;我手里又没有美金&#xff0c;美元跟我有啥关系啊&#xff1f;那我们就来聊聊美元降息&#xff0c;对我们国内经济到底有哪些影响&#xff1f;你再来看看跟你有没有关系&a…

短视频矩阵系统开发|技术源代码部署

产品功能亮点&#xff1a; 1. 支持多账号多平台一键 授权管理 2.支持矩阵视频批量剪辑&#xff0c;批量发布 3. 多平台关键词布局&#xff0c;提升企业及产品曝光 4. 评论区关键词自动回复&#xff0c;意向线索智能挖掘 5. 多账号投放数据统计&#xff0c;省时省力 6. 留资…

Jmeter 线程组解析

1.seUp线程组 一种特殊的 threadGroup &#xff0c;可用于执行预测试操作&#xff1b;它的行为完全像一个正常的线程组元件&#xff0c;不同的是执行顺序。 它会在普通线程组执行之前被触发。 应用场景&#xff1a; 测试数据库操作功能时&#xff0c;用于执行打开数据库连接的…

jetcache-阿里多级缓存框架神器一定要掌握

文章目录 1. 简介2. springboot集成jetcache2.1 引入依赖2.2 配置文件2.3 高级API模式&#xff1a;通过CacheManager使用缓存&#xff0c;2.7 版本才可使用2.4 &#xff08;推荐&#xff09;AOP模式&#xff1a;通过Cached,CacheUpdate,CacheInvalidate注解 1. 简介 JetCache是…

Redis基本命令详解

1. 基本命令 命令不区分大小写&#xff0c;而key是区分大小写的 # select 数据库间的切换 数据库共计16个 127.0.0.1:6379> select 1# dbsize 返回当前数据库的 key 的数量 127.0.0.1:6379[1]> dbsize# keys * 查看数据库所有的key 127.0.0.1:6379[1]> keys *# fl…

SpringBoot+Vue+MySQL驾校预约管理系统

目录 前言 功能设计 系统实现 获取源码 博主主页&#xff1a;百成Java 往期系列&#xff1a;Spring Boot、SSM、JavaWeb、python、小程序 前言 随着社会的进步&#xff0c;各行各业都在充分利用信息化时代的优势。由于计算机技术的广泛应用和普及&#xff0c;各种信息系统…

极越联手百度这你受得了吗!SU7还能稳坐“7字辈”头把交椅?

文/王俣祺 导语&#xff1a;自从今年上半年小米SU7标榜为“年轻人的第一台纯电轿车”&#xff0c;各家车企全都坐不住了。尤其是与小米“颇有渊源”的吉利&#xff0c;从极氪再到领克&#xff0c;目标已经可以说是路人皆知了。现在极越07也来了&#xff0c;可以看出吉利也是下了…

在Windows环境下设置SSH克隆GitHub仓库

在Windows环境下设置SSH克隆GitHub仓库的步骤如下&#xff1a; 1. 生成SSH密钥 打开 Git Bash&#xff08;如果你已经安装了Git&#xff09;。输入以下命令生成SSH密钥&#xff1a;ssh-keygen -t rsa -b 4096 -C "your_emailexample.com" 按 Enter 键接受默认文件名…

OpenHarmony(鸿蒙南向开发)——小型系统内核(LiteOS-A)【内核通信机制】下

往期知识点记录&#xff1a; 鸿蒙&#xff08;HarmonyOS&#xff09;应用层开发&#xff08;北向&#xff09;知识点汇总 鸿蒙&#xff08;OpenHarmony&#xff09;南向开发保姆级知识点汇总~ 子系统开发内核 轻量系统内核&#xff08;LiteOS-M&#xff09; 轻量系统内核&#…