ncnn神經(jīng)網(wǎng)絡(luò)

姓名:閔旭曈 學(xué)號(hào):22009200796

【轉(zhuǎn)自】https://zhuanlan.zhihu.com/p/338121531

【嵌牛導(dǎo)讀】本文主要介紹了ncnn神經(jīng)網(wǎng)絡(luò)

【嵌牛提問(wèn)】ncnn怎么學(xué)

【嵌牛正文】

$0x00$. 想法來(lái)源

CNN從15年的ResNet在ImageNet比賽中大放異彩,到今天各種層出不窮的網(wǎng)絡(luò)結(jié)構(gòu)被提出以解決生活中碰到的各種問(wèn)題。然而,在CNN長(zhǎng)期發(fā)展過(guò)程中,也伴隨著很多的挑戰(zhàn),比如如何調(diào)整算法使得在特定場(chǎng)景或者說(shuō)數(shù)據(jù)集上取得最好的精度,如何將學(xué)術(shù)界出色的算法落地到工業(yè)界,如何設(shè)計(jì)出在邊緣端或者有限硬件條件下的定制化CNN等。前兩天看到騰訊優(yōu)圖的文章:騰訊優(yōu)圖開(kāi)源這三年,里面提到了NCNN背后的故事,十分感動(dòng)和佩服,然后我也是白嫖了很多NCNN的算法實(shí)現(xiàn)以及一些調(diào)優(yōu)技巧。所以為了讓很多不太了解NCNN的人能更好的理解騰訊優(yōu)圖這個(gè)"從0到1"的深度學(xué)習(xí)框架,我將結(jié)合我自己擅長(zhǎng)的東西來(lái)介紹「我眼中的NCNN它是什么樣的」?

0x01. 如何使用NCNN

這篇文章的重點(diǎn)不是如何跑起來(lái)NCNN的各種Demo,也不是如何使用NCNN來(lái)部署自己的業(yè)務(wù)網(wǎng)絡(luò),這部分沒(méi)有什么比官方wiki介紹得更加清楚的資料了。所以這部分我只是簡(jiǎn)要匯總一些資料,以及說(shuō)明一些我認(rèn)為非常重要的東西。

官方wiki指路:https://github.com/Tencent/ncnn/wiki

在NCNN中新建一個(gè)自定義層教程:https://github.com/Ewenwan/MVision/blob/master/CNN/HighPerformanceComputing/example/ncnn_%E6%96%B0%E5%BB%BA%E5%B1%82.md

NCNN下載編譯以及使用:https://github.com/Ewenwan/MVision/blob/master/CNN/HighPerformanceComputing/example/readme.md

0x02. 運(yùn)行流程解析

要了解一個(gè)深度學(xué)習(xí)框架,首先得搞清楚這個(gè)框架是如何通過(guò)讀取一張圖片然后獲得的我們想要的輸出結(jié)果,這個(gè)運(yùn)行流程究竟是長(zhǎng)什么樣的?我們看一下NCNN官方wiki中提供一個(gè)示例代碼:

#include <opencv2/core/core.hpp>

#include <opencv2/highgui/highgui.hpp>

#include "net.h"

int main()

{

// opencv讀取輸入圖片

? ? cv::Mat img = cv::imread("image.ppm", CV_LOAD_IMAGE_GRAYSCALE);

? ? int w = img.cols;

? ? int h = img.rows;

? ? // 減均值以及縮放操作,最后輸入數(shù)據(jù)的值域?yàn)閇-1,1]

? ? ncnn::Mat in = ncnn::Mat::from_pixels_resize(img.data, ncnn::Mat::PIXEL_GRAY, w, h, 60, 60);

? ? float mean[1] = { 128.f };

? ? float norm[1] = { 1/128.f };

? ? in.substract_mean_normalize(mean, norm);

// 構(gòu)建NCNN的net,并加載轉(zhuǎn)換好的模型

? ? ncnn::Net net;

? ? net.load_param("model.param");

? ? net.load_model("model.bin");

// 創(chuàng)建網(wǎng)絡(luò)提取器,設(shè)置網(wǎng)絡(luò)輸入,線(xiàn)程數(shù),light模式等等

? ? ncnn::Extractor ex = net.create_extractor();

? ? ex.set_light_mode(true);

? ? ex.set_num_threads(4);

? ? ex.input("data", in);

// 調(diào)用extract接口,完成網(wǎng)絡(luò)推理,獲得輸出結(jié)果

? ? ncnn::Mat feat;

? ? ex.extract("output", feat);

? ? return 0;

0x02.00 圖像預(yù)處理ncnn::Mat

可以看到NCNN對(duì)于我們給定的一個(gè)網(wǎng)絡(luò)(首先轉(zhuǎn)換為NCNN的param和bin文件)和輸入,首先執(zhí)行圖像預(yù)處理,這是基于「ncnn::Mat」這個(gè)數(shù)據(jù)結(jié)構(gòu)完成的。

其中,from_pixels_resize()這個(gè)函數(shù)的作用是生成目標(biāo)尺寸大小的網(wǎng)絡(luò)輸入Mat,它的實(shí)現(xiàn)在https://github.com/Tencent/ncnn/blob/b93775a27273618501a15a235355738cda102a38/src/mat_pixel.cpp#L2543。它的內(nèi)部實(shí)際上是「根據(jù)傳入的輸入圖像的通道數(shù)」完成resize_bilinear_c1/c2/c3/4即一通道/二通道/三通道/四通道 圖像變形算法,可以看到使用的是雙線(xiàn)性插值算法。這些操作的實(shí)現(xiàn)在https://github.com/Tencent/ncnn/blob/master/src/mat_pixel_resize.cpp#L27。然后經(jīng)過(guò)Resize之后,需要將像素圖像轉(zhuǎn)換成ncnn::Mat。這里調(diào)用的是Mat::from_pixels()這個(gè)函數(shù),它將我們Resize操作之后獲得的像素圖像數(shù)據(jù)(即float*數(shù)據(jù))根據(jù)特定的輸入類(lèi)型賦值給ncnn::Mat。

接下來(lái),我們講講substract_mean_normalize()這個(gè)函數(shù),它實(shí)現(xiàn)了減均值和歸一化操作,它的實(shí)現(xiàn)在:https://github.com/Tencent/ncnn/blob/master/src/mat.cpp#L34。具體來(lái)說(shuō),這個(gè)函數(shù)根據(jù)均值參數(shù)和歸一化參數(shù)的有無(wú)分成這幾種情況:

「有均值參數(shù)」

「創(chuàng)建 偏置層」ncnn::create_layer(ncnn::LayerType::Bias);? 載入層參數(shù) op->load_param(pd);? 3通道

「載入層權(quán)重?cái)?shù)據(jù)」op->load_model(ncnn::ModelBinFromMatArray(weights));? -均值參數(shù)

「運(yùn)行層」op->forward_inplace(*this);

「有歸一化參數(shù)」

「創(chuàng)建 尺度層」ncnn::create_layer(ncnn::LayerType::Scale);? 載入層參數(shù) op->load_param(pd);? 3通道

「載入層權(quán)重?cái)?shù)據(jù)」op->load_model(ncnn::ModelBinFromMatArray(weights));? 尺度參數(shù)

「運(yùn)行層」op->forward_inplace(*this);

「有均值和歸一化參數(shù)」

「創(chuàng)建 尺度層」ncnn::create_layer(ncnn::LayerType::Scale);? 載入層參數(shù) op->load_param(pd);? 3通道

「載入層權(quán)重?cái)?shù)據(jù)」op->load_model(ncnn::ModelBinFromMatArray(weights));? -均值參數(shù) 和 尺度參數(shù)

「運(yùn)行層」op->forward_inplace(*this);

可以看到NCNN的均值和歸一化操作,是直接利用了它的Bias Layer和Scale Layer來(lái)實(shí)現(xiàn)的,也就是說(shuō)NCNN中的每個(gè)層都可以單獨(dú)拿出來(lái)運(yùn)行我們自己數(shù)據(jù),更加方便我們白嫖 。

0x02.01 模型解析ncnn::Net

param 解析

完成了圖像預(yù)處理之后,新增了一個(gè)ncnn::Net,然后調(diào)用Net::load_param來(lái)載入網(wǎng)絡(luò)參數(shù)文件*.proto, 這部分的實(shí)現(xiàn)在https://github.com/Tencent/ncnn/blob/master/src/net.cpp#L115。在講解這個(gè)函數(shù)在的過(guò)程之前,我們先來(lái)一起分析一下NCNN的param文件,舉例如下:

? 7767517? # 文件頭 魔數(shù)

? 75 83? ? # 層數(shù)量? 輸入輸出blob數(shù)量

? ? ? ? ? ? # 下面有75行

? Input? ? ? ? ? ? data? ? ? ? ? ? 0 1 data 0=227 1=227 2=3

? Convolution? ? ? conv1? ? ? ? ? ? 1 1 data conv1 0=64 1=3 2=1 3=2 4=0 5=1 6=1728

? ReLU? ? ? ? ? ? relu_conv1? ? ? 1 1 conv1 conv1_relu_conv1 0=0.000000

? Pooling? ? ? ? ? pool1? ? ? ? ? ? 1 1 conv1_relu_conv1 pool1 0=0 1=3 2=2 3=0 4=0

? Convolution? ? ? fire2/squeeze1x1 1 1 pool1 fire2/squeeze1x1 0=16 1=1 2=1 3=1 4=0 5=1 6=1024

? ...

? 層類(lèi)型? ? ? ? ? ? 層名字? 輸入blob數(shù)量 輸出blob數(shù)量? 輸入blob名字 輸出blob名字? 參數(shù)字典


? 參數(shù)字典,每一層的意義不一樣:

? 數(shù)據(jù)輸入層 Input? ? ? ? ? ? data? ? ? ? ? ? 0 1 data 0=227 1=227 2=3? 圖像寬度×圖像高度×通道數(shù)量

? 卷積層? ? Convolution? ...? 0=64? ? 1=3? ? ? 2=1? ? 3=2? ? 4=0? ? 5=1? ? 6=1728? ? ? ? ?

? ? ? ? ? 0輸出通道數(shù) num_output() ; 1卷積核尺寸 kernel_size();? 2空洞卷積參數(shù) dilation(); 3卷積步長(zhǎng) stride();

? ? ? ? ? 4卷積填充pad_size();? ? ? 5卷積偏置有無(wú)bias_term();? 6卷積核參數(shù)數(shù)量 weight_blob.data_size();

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? C_OUT * C_in * W_h * W_w = 64*3*3*3 = 1728

? 池化層? ? Pooling? ? ? 0=0? ? ? 1=3? ? ? 2=2? ? ? ? 3=0? ? ? 4=0

? ? ? ? ? ? ? ? ? ? ? 0池化方式:最大值、均值、隨機(jī)? ? 1池化核大小 kernel_size();? ? 2池化核步長(zhǎng) stride();

? ? ? ? ? ? ? ? ? ? ? 3池化核填充 pad();? 4是否為全局池化 global_pooling();

? 激活層? ? ReLU? ? ? 0=0.000000? ? 下限閾值 negative_slope();

? ? ? ? ? ReLU6? ? ? 0=0.000000? ? 1=6.000000 上下限


? 綜合示例:

? 0=1 1=2.5 -23303=2,2.0,3.0


? 數(shù)組關(guān)鍵字 : -23300

? -(-23303) - 23300 = 3 表示該參數(shù)在參數(shù)數(shù)組中的index

? 后面的第一個(gè)參數(shù)表示數(shù)組元素?cái)?shù)量,2表示包含兩個(gè)元素

然后官方的wiki中提供了所有網(wǎng)絡(luò)層的詳細(xì)參數(shù)設(shè)置,地址為:https://github.com/Tencent/ncnn/wiki/operation-param-weight-table

了解了Param的基本含義之后,我們可以來(lái)看一下Net::load_param這個(gè)函數(shù)是在做什么了。

從函數(shù)實(shí)現(xiàn),我們知道,首先會(huì)遍歷param文件中的所有網(wǎng)絡(luò)層,然后根據(jù)當(dāng)前層的類(lèi)型調(diào)用create_layer()/ net::create_custom_layer()來(lái)創(chuàng)建網(wǎng)絡(luò)層,然后讀取輸入Blobs和輸出Blobs和當(dāng)前層綁定,再調(diào)用paramDict::load_param(fp)解析當(dāng)前層的特定參數(shù)(參數(shù)字典),按照id=參數(shù)/參數(shù)數(shù)組來(lái)解析。最后,當(dāng)前層調(diào)用layer->load_param(pd)載入解析得到的層特殊參數(shù)即獲得當(dāng)前層特有的參數(shù)。

核心代碼解析如下:

// 參數(shù)讀取 程序

// 讀取字符串格式的 參數(shù)文件

int ParamDict::load_param(FILE* fp)

{

? ? clear();

//? ? 0=100 1=1.250000 -23303=5,0.1,0.2,0.4,0.8,1.0

? ? // parse each key=value pair

? ? int id = 0;

? ? while (fscanf(fp, "%d=", &id) == 1)// 讀取 等號(hào)前面的 key=========

? ? {

? ? ? ? bool is_array = id <= -23300;

? ? ? ? if (is_array)

? ? ? ? {

? ? ? ? ? ? id = -id - 23300;// 數(shù)組 關(guān)鍵字 -23300? 得到該參數(shù)在參數(shù)數(shù)組中的 index

? ? ? ? }


// 是以 -23300 開(kāi)頭表示的數(shù)組===========

? ? ? ? if (is_array)

? ? ? ? {

? ? ? ? ? ? int len = 0;

? ? ? ? ? ? int nscan = fscanf(fp, "%d", &len);// 后面的第一個(gè)參數(shù)表示數(shù)組元素?cái)?shù)量,5表示包含兩個(gè)元素

? ? ? ? ? ? if (nscan != 1)

? ? ? ? ? ? {

? ? ? ? ? ? ? ? fprintf(stderr, "ParamDict read array length fail\n");

? ? ? ? ? ? ? ? return -1;

? ? ? ? ? ? }

? ? ? ? ? ? params[id].v.create(len);

? ? ? ? ? ? for (int j = 0; j < len; j++)

? ? ? ? ? ? {

? ? ? ? ? ? ? ? char vstr[16];

? ? ? ? ? ? ? ? nscan = fscanf(fp, ",%15[^,\n ]", vstr);//按格式解析字符串============

? ? ? ? ? ? ? ? if (nscan != 1)

? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? fprintf(stderr, "ParamDict read array element fail\n");

? ? ? ? ? ? ? ? ? ? return -1;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? bool is_float = vstr_is_float(vstr);// 檢查該字段是否為 浮點(diǎn)數(shù)的字符串

? ? ? ? ? ? ? ? if (is_float)

? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? float* ptr = params[id].v;

? ? ? ? ? ? ? ? ? ? nscan = sscanf(vstr, "%f", &ptr[j]);// 轉(zhuǎn)換成浮點(diǎn)數(shù)后存入?yún)?shù)字典中

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? else

? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? int* ptr = params[id].v;

? ? ? ? ? ? ? ? ? ? nscan = sscanf(vstr, "%d", &ptr[j]);// 轉(zhuǎn)換成 整數(shù)后 存入字典中

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? if (nscan != 1)

? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? fprintf(stderr, "ParamDict parse array element fail\n");

? ? ? ? ? ? ? ? ? ? return -1;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

// 普通關(guān)鍵字=========================

? ? ? ? else

? ? ? ? {

? ? ? ? ? ? char vstr[16];

? ? ? ? ? ? int nscan = fscanf(fp, "%15s", vstr);// 獲取等號(hào)后面的 字符串

? ? ? ? ? ? if (nscan != 1)

? ? ? ? ? ? {

? ? ? ? ? ? ? ? fprintf(stderr, "ParamDict read value fail\n");

? ? ? ? ? ? ? ? return -1;

? ? ? ? ? ? }

? ? ? ? ? ? bool is_float = vstr_is_float(vstr);// 判斷是否為浮點(diǎn)數(shù)

? ? ? ? ? ? if (is_float)

? ? ? ? ? ? ? ? nscan = sscanf(vstr, "%f", &params[id].f); // 讀入為浮點(diǎn)數(shù)

? ? ? ? ? ? else

? ? ? ? ? ? ? ? nscan = sscanf(vstr, "%d", &params[id].i);// 讀入為整數(shù)

? ? ? ? ? ? if (nscan != 1)

? ? ? ? ? ? {

? ? ? ? ? ? ? ? fprintf(stderr, "ParamDict parse value fail\n");

? ? ? ? ? ? ? ? return -1;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? params[id].loaded = 1;// 設(shè)置該 參數(shù)以及載入

? ? }

? ? return 0;

}

// 讀取 二進(jìn)制格式的 參數(shù)文件===================

int ParamDict::load_param_bin(FILE* fp)

{

? ? clear();

//? ? binary 0

//? ? binary 100

//? ? binary 1

//? ? binary 1.250000

//? ? binary 3 | array_bit

//? ? binary 5

//? ? binary 0.1

//? ? binary 0.2

//? ? binary 0.4

//? ? binary 0.8

//? ? binary 1.0

//? ? binary -233(EOP)

? ? int id = 0;

? ? fread(&id, sizeof(int), 1, fp);// 讀入一個(gè)整數(shù)長(zhǎng)度的 index

? ? while (id != -233)// 結(jié)尾

? ? {

? ? ? ? bool is_array = id <= -23300;

? ? ? ? if (is_array)

? ? ? ? {

? ? ? ? ? ? id = -id - 23300;// 數(shù)組關(guān)鍵字對(duì)應(yīng)的 index

? ? ? ? }

// 是數(shù)組數(shù)據(jù)=======

? ? ? ? if (is_array)

? ? ? ? {

? ? ? ? ? ? int len = 0;

? ? ? ? ? ? fread(&len, sizeof(int), 1, fp);// 數(shù)組元素?cái)?shù)量

? ? ? ? ? ? params[id].v.create(len);

? ? ? ? ? ? float* ptr = params[id].v;

? ? ? ? ? ? fread(ptr, sizeof(float), len, fp);// 按浮點(diǎn)數(shù)長(zhǎng)度*數(shù)組長(zhǎng)度 讀取每一個(gè)數(shù)組元素====

? ? ? ? }

// 是普通數(shù)據(jù)=======

? ? ? ? else

? ? ? ? {

? ? ? ? ? ? fread(&params[id].f, sizeof(float), 1, fp);// 按浮點(diǎn)數(shù)長(zhǎng)度讀取 該普通字段對(duì)應(yīng)的元素

? ? ? ? }

? ? ? ? params[id].loaded = 1;

? ? ? ? fread(&id, sizeof(int), 1, fp);// 讀取 下一個(gè) index

? ? }

? ? return 0;

}

bin 解析

解析完param文件,接下來(lái)需要對(duì)bin文件進(jìn)行解析,這部分的實(shí)現(xiàn)在:https://github.com/Tencent/ncnn/blob/master/src/net.cpp#L672。這里執(zhí)行的主要的操作如下:

創(chuàng)建 ModelBinFromStdio 對(duì)象 提供載入?yún)?shù)的接口函數(shù)ModelBinFromStdio::load()根據(jù) 權(quán)重?cái)?shù)據(jù)開(kāi)始的一個(gè)四字節(jié)數(shù)據(jù)類(lèi)型參數(shù)(float32/float16/int8等) 和 指定的參數(shù)數(shù)量 讀取數(shù)據(jù)到 Mat 并返回Mat, 這個(gè)函數(shù)的實(shí)現(xiàn)在https://github.com/Tencent/ncnn/blob/master/src/modelbin.cpp#L50。

根據(jù)load_param 獲取到的網(wǎng)絡(luò)層信息 遍歷每一層 載入每一層的模型數(shù)據(jù) layer->load_model() 每一層特有函數(shù)。

部分層需要 根據(jù)層實(shí)際參數(shù) 調(diào)整運(yùn)行流水線(xiàn) layer->create_pipeline 例如卷積層和全連接層

量化的網(wǎng)絡(luò)需要融合 Net::fuse_network()

bin文件的結(jié)構(gòu)如下:

? ? +---------+---------+---------+---------+---------+---------+

? ? | weight1 | weight2 | weight3 | weight4 | ....... | weightN |

? ? +---------+---------+---------+---------+---------+---------+

? ? ^? ? ? ? ^? ? ? ? ^? ? ? ? ^

? ? 0x0? ? ? 0x80? ? ? 0x140? ? 0x1C0

? 所有權(quán)重?cái)?shù)據(jù)連接起來(lái), 每個(gè)權(quán)重占 32bit。

? 權(quán)重?cái)?shù)據(jù) weight buffer

? [flag] (optional 可選)

? [raw data]

? [padding] (optional 可選)

? ? ? flag : unsigned int, little-endian, indicating the weight storage type,

? ? ? ? ? ? 0? ? ? ? ? => float32,

? ? ? ? ? ? 0x01306B47 => float16,

? ? ? ? ? ? 其它非0 => int8,? 如果層實(shí)現(xiàn)顯式強(qiáng)制存儲(chǔ)類(lèi)型,則可以省略? ? ?

? ? ? raw data : 原始權(quán)重?cái)?shù)據(jù)、little endian、float32數(shù)據(jù)或float16數(shù)據(jù)或量化表和索引,具體取決于存儲(chǔ)類(lèi)型標(biāo)志

? ? ? padding : 32位對(duì)齊的填充空間,如果已經(jīng)對(duì)齊,則可以省略。

感覺(jué)bin解析這部分了解一下就好,如果感興趣可以自己去看看源碼。

0x02.03 網(wǎng)絡(luò)運(yùn)行 ncnn::Extractor

至此,我們將網(wǎng)絡(luò)的結(jié)構(gòu)和權(quán)重信息都放到了ncnn::Net這個(gè)結(jié)構(gòu)中,接下來(lái)我們就可以新建網(wǎng)絡(luò)提取器Extractor Net::create_extractor,它給我們提供了設(shè)置網(wǎng)絡(luò)輸入(Extractor::input),獲取網(wǎng)絡(luò)輸出(Extractor::extract),設(shè)置網(wǎng)絡(luò)運(yùn)行線(xiàn)程參數(shù)(Extractor::set_num_threads)等接口。接下來(lái),我們只需要調(diào)用Extractor::extract運(yùn)行網(wǎng)絡(luò)(net)的前向傳播函數(shù)net->forward_layer就可以獲得最后的結(jié)果了。

另外,ncnn::Extractor還可以設(shè)置一個(gè)輕模式省內(nèi)存 即set_light_mode(true),原理是net中每個(gè)layer都會(huì)產(chǎn)生blob,除了最后的結(jié)果和多分支中間結(jié)果,大部分blob都不值得保留,開(kāi)啟輕模式可以在運(yùn)算后自動(dòng)回收,省下內(nèi)存。但需要注意的是,一旦開(kāi)啟這個(gè)模式,我們就不能獲得中間層的特征值了,因?yàn)橹虚g層的內(nèi)存在獲得最終結(jié)果之前都被回收掉了。例如:「某網(wǎng)絡(luò)結(jié)構(gòu)為 A -> B -> C,在輕模式下,向ncnn索要C結(jié)果時(shí),A結(jié)果會(huì)在運(yùn)算B時(shí)自動(dòng)回收,而B(niǎo)結(jié)果會(huì)在運(yùn)算C時(shí)自動(dòng)回收,最后只保留C結(jié)果,后面再需要C結(jié)果會(huì)直接獲得,滿(mǎn)足大多數(shù)深度網(wǎng)絡(luò)的使用方式」

最后,我們需要明確一下,我們剛才是先創(chuàng)建了ncnn::net,然后我們調(diào)用的ncnn::Extractor作為運(yùn)算實(shí)例,因此運(yùn)算實(shí)例是不受net限制的。換句話(huà)說(shuō),雖然我們只有一個(gè)net,但我們可以開(kāi)多個(gè)ncnn::Extractor,這些實(shí)例都是單獨(dú)完成特定網(wǎng)絡(luò)的推理,互不影響。

這樣我們就大致了解了NCNN的運(yùn)行流程了,更多的細(xì)節(jié)可以關(guān)注NCNN源碼。

0x03. NCNN源碼目錄分析

這一節(jié),我們來(lái)分析一下NCNN源碼目錄以便更好的理解整個(gè)工程。src的目錄結(jié)構(gòu)如下:

/src 目錄:

./src/layer下是所有的layer定義代碼

./src/layer/arm是arm下的計(jì)算加速的layer

./src/layer/x86是x86下的計(jì)算加速的layer。

./src/layer/mips是mips下的計(jì)算加速的layer。

./src/layer/.h + ./src/layer/.cpp 是各種layer的基礎(chǔ)實(shí)現(xiàn),無(wú)加速。

目錄頂層下是一些基礎(chǔ)代碼,如宏定義,平臺(tái)檢測(cè),mat數(shù)據(jù)結(jié)構(gòu),layer定義,blob定義,net定義等。

http://platform.h.in平臺(tái)檢測(cè)

benchmark.h + benchmark.cpp 測(cè)試各個(gè)模型的執(zhí)行速度

allocator.h + allocator.cpp 內(nèi)存池管理,內(nèi)存對(duì)齊

paramdict.h + paramdict.cpp 層參數(shù)解析 讀取二進(jìn)制格式、字符串格式、密文格式的參數(shù)文件

opencv.h opencv.cpp? opencv 風(fēng)格的數(shù)據(jù)結(jié)構(gòu) 的 mini實(shí)現(xiàn),包含大小結(jié)構(gòu)體 Size,矩陣框結(jié)構(gòu)體 Rect_ 交集 并集運(yùn)算符重載,點(diǎn)結(jié)構(gòu)體? ? Point_,矩陣結(jié)構(gòu)體? Mat? ? 深拷貝 淺拷貝 獲取指定矩形框中的roi 讀取圖像 寫(xiě)圖像 雙線(xiàn)性插值算法改變大小等等

mat.h mat.cpp? 三維矩陣數(shù)據(jù)結(jié)構(gòu), 在層間傳播的就是Mat數(shù)據(jù),Blob數(shù)據(jù)是工具人,另外包含 substract_mean_normalize(),去均值并歸一化;half2float(),float16 的 data 轉(zhuǎn)換成 float32 的 data;? copy_make_border(), 矩陣周?chē)畛? resize_bilinear_image(),雙線(xiàn)性插值等函數(shù)。

net.h net.cpp? ncnn框架接口,包含注冊(cè) 用戶(hù)定義的新層Net::register_custom_layer(); 網(wǎng)絡(luò)載入 模型參數(shù)? Net::load_param(); 載入? ? 模型權(quán)重? Net::load_model(); 網(wǎng)絡(luò)blob 輸入 Net::input();? 網(wǎng)絡(luò)前向傳播Net::forward_layer();被Extractor::extract() 執(zhí)行;創(chuàng)建網(wǎng)絡(luò)模型提取器? Net::create_extractor(); 模型提取器提取某一層輸出Extractor::extract()等函數(shù)。


源碼目錄除了這些還有很多文件,介于篇幅原因就不再枚舉了,感興趣的可以自行查看源碼。由于我只對(duì)x86和arm端的指令集加速熟悉一些,所以這里再枚舉一下src/layers下面的NCNN支持的層的目錄:

├── absval.cpp? ? ? ? ? ? ? ? ? ? ? // 絕對(duì)值層

├── absval.h

├── argmax.cpp? ? ? ? ? ? ? ? ? ? ? // 最大值層

├── argmax.h

├── arm ============================ arm平臺(tái)下的層

│? ├── absval_arm.cpp? ? ? ? ? ? ? // 絕對(duì)值層

│? ├── absval_arm.h

│? ├── batchnorm_arm.cpp? ? ? ? ? ? // 批歸一化 去均值除方差

│? ├── batchnorm_arm.h

│? ├── bias_arm.cpp? ? ? ? ? ? ? ? // 偏置

│? ├── bias_arm.h

│? ├── convolution_1x1.h? ? ? ? ? ? // 1*1 float32 卷積

│? ├── convolution_1x1_int8.h? ? ? // 1*1 int8? ? 卷積

│? ├── convolution_2x2.h? ? ? ? ? ? // 2*2 float32 卷積

│? ├── convolution_3x3.h? ? ? ? ? ? // 3*3 float32 卷積

│? ├── convolution_3x3_int8.h? ? ? // 3*3 int8? ? 卷積

│? ├── convolution_4x4.h? ? ? ? ? ? // 4*4 float32 卷積

│? ├── convolution_5x5.h? ? ? ? ? ? // 5*5 float32 卷積

│? ├── convolution_7x7.h? ? ? ? ? ? // 7*7 float32 卷積

│? ├── convolution_arm.cpp? ? ? ? ? // 卷積層

│? ├── convolution_arm.h

│? ├── convolutiondepthwise_3x3.h? ? ? // 3*3 逐通道 float32 卷積

│? ├── convolutiondepthwise_3x3_int8.h // 3*3 逐通道 int8? ? 卷積

│? ├── convolutiondepthwise_arm.cpp? ? // 逐通道卷積

│? ├── convolutiondepthwise_arm.h

│? ├── deconvolution_3x3.h? ? ? ? ? ? // 3*3 反卷積

│? ├── deconvolution_4x4.h? ? ? ? ? ? // 4*4 反卷積

│? ├── deconvolution_arm.cpp? ? ? ? ? // 反卷積

│? ├── deconvolution_arm.h

│? ├── deconvolutiondepthwise_arm.cpp? // 反逐通道卷積

│? ├── deconvolutiondepthwise_arm.h

│? ├── dequantize_arm.cpp? ? ? ? ? ? ? // 反量化

│? ├── dequantize_arm.h

│? ├── eltwise_arm.cpp? ? ? ? ? ? ? ? // 逐元素操作,product(點(diǎn)乘), sum(相加減) 和 max(取大值)

│? ├── eltwise_arm.h

│? ├── innerproduct_arm.cpp? ? ? ? ? ? // 即 fully_connected (fc)layer, 全連接層

│? ├── innerproduct_arm.h

│? ├── lrn_arm.cpp? ? ? ? ? ? ? ? ? ? // Local Response Normalization,即局部響應(yīng)歸一化層

│? ├── lrn_arm.h

│? ├── neon_mathfun.h? ? ? ? ? ? ? ? ? // neon 數(shù)學(xué)函數(shù)庫(kù)

│? ├── pooling_2x2.h? ? ? ? ? ? ? ? ? // 2*2 池化層

│? ├── pooling_3x3.h? ? ? ? ? ? ? ? ? // 3*3 池化層

│? ├── pooling_arm.cpp? ? ? ? ? ? ? ? // 池化層

│? ├── pooling_arm.h

│? ├── prelu_arm.cpp? ? ? ? ? ? ? ? ? // (a*x,x) 前置relu激活層

│? ├── prelu_arm.h

│? ├── quantize_arm.cpp? ? ? ? ? ? ? ? // 量化層

│? ├── quantize_arm.h

│? ├── relu_arm.cpp? ? ? ? ? ? ? ? ? ? // relu 層 (0,x)

│? ├── relu_arm.h

│? ├── scale_arm.cpp? ? ? ? ? ? ? ? ? // BN層后的 平移和縮放層 scale

│? ├── scale_arm.h

│? ├── sigmoid_arm.cpp? ? ? ? ? ? ? ? // sigmod 負(fù)指數(shù)倒數(shù)歸一化 激活層? 1/(1 + e^(-zi))

│? ├── sigmoid_arm.h

│? ├── softmax_arm.cpp? ? ? ? ? ? ? ? // softmax 指數(shù)求和歸一化 激活層? e^(zi) / sum(e^(zi))

│? └── softmax_arm.h

|

|

|================================ 普通平臺(tái) 待優(yōu)化=============

├── batchnorm.cpp? ? ? ? ? ? // 批歸一化 去均值除方差

├── batchnorm.h

├── bias.cpp? ? ? ? ? ? ? ? ? // 偏置

├── bias.h

├── binaryop.cpp? ? ? ? ? ? ? // 二元操作: add,sub, div, mul,mod等

├── binaryop.h

├── bnll.cpp? ? ? ? ? ? ? ? ? // binomial normal log likelihood的簡(jiǎn)稱(chēng) f(x)=log(1 + exp(x))? 激活層

├── bnll.h

├── clip.cpp? ? ? ? ? ? ? ? ? // 截?cái)?====

├── clip.h

├── concat.cpp? ? ? ? ? ? ? ? // 通道疊加

├── concat.h

├── convolution.cpp? ? ? ? ? // 普通卷積層

├── convolutiondepthwise.cpp? // 逐通道卷積

├── convolutiondepthwise.h

├── convolution.h

├── crop.cpp? ? ? ? ? ? ? ? ? // 剪裁層

├── crop.h

├── deconvolution.cpp? ? ? ? // 反卷積

├── deconvolutiondepthwise.cpp// 反逐通道卷積

├── deconvolutiondepthwise.h

├── deconvolution.h

├── dequantize.cpp? ? ? ? ? ? // 反量化

├── dequantize.h

├── detectionoutput.cpp? ? ? // ssd 的檢測(cè)輸出層================================

├── detectionoutput.h

├── dropout.cpp? ? ? ? ? ? ? // 隨機(jī)失活層 在訓(xùn)練時(shí)由于舍棄了一些神經(jīng)元,因此在測(cè)試時(shí)需要在激勵(lì)的結(jié)果中乘上因子p進(jìn)行縮放.

├── dropout.h

├── eltwise.cpp? ? ? ? ? ? ? // 逐元素操作, product(點(diǎn)乘), sum(相加減) 和 max(取大值)

├── eltwise.h

├── elu.cpp? ? ? ? ? ? ? ? ? // 指數(shù)線(xiàn)性單元relu激活層 Prelu : (a*x, x) ----> Erelu : (a*(e^x - 1), x)

├── elu.h

├── embed.cpp? ? ? ? ? ? ? ? // 嵌入層,用在網(wǎng)絡(luò)的開(kāi)始層將你的輸入轉(zhuǎn)換成向量

├── embed.h

├── expanddims.cpp? ? ? ? ? ? // 增加維度

├── expanddims.h

├── exp.cpp? ? ? ? ? ? ? ? ? // 指數(shù)映射

├── exp.h

├── flatten.cpp? ? ? ? ? ? ? // 攤平層

├── flatten.h

├── innerproduct.cpp? ? ? ? ? // 全連接層

├── innerproduct.h

├── input.cpp? ? ? ? ? ? ? ? // 數(shù)據(jù)輸入層

├── input.h

├── instancenorm.cpp? ? ? ? ? // 單樣本 標(biāo)準(zhǔn)化 規(guī)范化

├── instancenorm.h

├── interp.cpp? ? ? ? ? ? ? ? // 插值層 上下采樣等

├── interp.h

├── log.cpp? ? ? ? ? ? ? ? ? // 對(duì)數(shù)層

├── log.h

├── lrn.cpp? ? ? ? ? ? ? ? ? // Local Response Normalization,即局部響應(yīng)歸一化層

├── lrn.h? ? ? ? ? ? ? ? ? ? // 對(duì)局部神經(jīng)元的活動(dòng)創(chuàng)建競(jìng)爭(zhēng)機(jī)制,使得其中響應(yīng)比較大的值變得相對(duì)更大,

|? ? ? ? ? ? ? ? ? ? ? ? ? ? // 并抑制其他反饋較小的神經(jīng)元,增強(qiáng)了模型的泛化能力

├── lstm.cpp? ? ? ? ? ? ? ?

├── lstm.h? ? ? ? ? ? ? ? ? ? // lstm 長(zhǎng)短詞記憶層

├── memorydata.cpp? ? ? ? ? ? // 內(nèi)存數(shù)據(jù)層

├── memorydata.h

├── mvn.cpp

├── mvn.h

├── normalize.cpp? ? ? ? ? ? // 歸一化

├── normalize.h

├── padding.cpp? ? ? ? ? ? ? // 填充,警戒線(xiàn)

├── padding.h

├── permute.cpp? ? ? ? ? ? ? //? ssd 特有層 交換通道順序 [bantch_num, channels, h, w] ---> [bantch_num, h, w, channels]]=========

├── permute.h

├── pooling.cpp? ? ? ? ? ? ? // 池化層

├── pooling.h

├── power.cpp? ? ? ? ? ? ? ? // 平移縮放乘方 : (shift + scale * x) ^ power

├── power.h

├── prelu.cpp? ? ? ? ? ? ? ? // Prelu? (a*x,x)

├── prelu.h

├── priorbox.cpp? ? ? ? ? ? ? // ssd 獨(dú)有的層 建議框生成層 L1 loss 擬合============================

├── priorbox.h

├── proposal.cpp? ? ? ? ? ? ? // faster rcnn 獨(dú)有的層 建議框生成,將rpn網(wǎng)絡(luò)的輸出轉(zhuǎn)換成建議框========

├── proposal.h

├── quantize.cpp? ? ? ? ? ? ? // 量化層

├── quantize.h

├── reduction.cpp? ? ? ? ? ? // 將輸入的特征圖按照給定的維度進(jìn)行求和或求平均

├── reduction.h

├── relu.cpp? ? ? ? ? ? ? ? ? // relu 激活層: (0,x)

├── relu.h

├── reorg.cpp? ? ? ? ? ? ? ? // yolov2 獨(dú)有的層, 一拆四層,一個(gè)大矩陣,下采樣到四個(gè)小矩陣=================

├── reorg.h

├── reshape.cpp? ? ? ? ? ? ? // 變形層: 在不改變數(shù)據(jù)的情況下,改變輸入的維度

├── reshape.h

├── rnn.cpp? ? ? ? ? ? ? ? ? // rnn 循環(huán)神經(jīng)網(wǎng)絡(luò)

├── rnn.h

├── roipooling.cpp? ? ? ? ? ? // faster Rcnn 獨(dú)有的層, ROI池化層: 輸入m*n 均勻劃分成 a*b個(gè)格子后池化,得到固定長(zhǎng)度的特征向量 ==========

├── roipooling.h

├── scale.cpp? ? ? ? ? ? ? ? // bn 層之后的 平移縮放層

├── scale.h

├── shufflechannel.cpp? ? ? ? // ShuffleNet 獨(dú)有的層,通道打亂,通道混合層=================================

├── shufflechannel.h

├── sigmoid.cpp? ? ? ? ? ? ? // 負(fù)指數(shù)倒數(shù)歸一化層? 1/(1 + e^(-zi))

├── sigmoid.h

├── slice.cpp? ? ? ? ? ? ? ? // concat的反向操作, 通道分開(kāi)層,適用于多任務(wù)網(wǎng)絡(luò)

├── slice.h

├── softmax.cpp? ? ? ? ? ? ? // 指數(shù)求和歸一化層? e^(zi) / sum(e^(zi))

├── softmax.h

├── split.cpp? ? ? ? ? ? ? ? // 將blob復(fù)制幾份,分別給不同的layer,這些上層layer共享這個(gè)blob。

├── split.h

├── spp.cpp? ? ? ? ? ? ? ? ? // 空間金字塔池化層 1+4+16=21 SPP-NET 獨(dú)有===================================

├── spp.h

├── squeeze.cpp? ? ? ? ? ? ? // squeezeNet獨(dú)有層, Fire Module, 一層conv層變成兩層:squeeze層+expand層, 1*1卷積---> 1*1 + 3*3=======

├── squeeze.h

├── tanh.cpp? ? ? ? ? ? ? ? ? // 雙曲正切激活函數(shù)? (e^(zi) - e^(-zi)) / (e^(zi) + e^(-zi))

├── tanh.h

├── threshold.cpp? ? ? ? ? ? // 閾值函數(shù)層

├── threshold.h

├── tile.cpp? ? ? ? ? ? ? ? ? // 將blob的某個(gè)維度,擴(kuò)大n倍。比如原來(lái)是1234,擴(kuò)大兩倍變成11223344。

├── tile.h

├── unaryop.cpp? ? ? ? ? ? ? // 一元操作: abs, sqrt, exp, sin, cos,conj(共軛)等

├── unaryop.h

|

|==============================x86下特殊的優(yōu)化層=====

├── x86

│? ├── avx_mathfun.h? ? ? ? ? ? ? ? ? ? // x86 數(shù)學(xué)函數(shù)

│? ├── convolution_1x1.h? ? ? ? ? ? ? ? // 1*1 float32 卷積

│? ├── convolution_1x1_int8.h? ? ? ? ? // 1×1 int8 卷積

│? ├── convolution_3x3.h? ? ? ? ? ? ? ? // 3*3 float32 卷積

│? ├── convolution_3x3_int8.h? ? ? ? ? // 3×3 int8 卷積

│? ├── convolution_5x5.h? ? ? ? ? ? ? ? // 5*5 float32 卷積

│? ├── convolutiondepthwise_3x3.h? ? ? // 3*3 float32 逐通道卷積

│? ├── convolutiondepthwise_3x3_int8.h? // 3*3 int8 逐通道卷積

│? ├── convolutiondepthwise_x86.cpp? ? //? 逐通道卷積

│? ├── convolutiondepthwise_x86.h

│? ├── convolution_x86.cpp? ? ? ? ? ? ? //? 卷積

│? ├── convolution_x86.h

│? └── sse_mathfun.h? ? ? ? ? ? ? ? ? ? // sse優(yōu)化 數(shù)學(xué)函數(shù)

├── yolodetectionoutput.cpp? ? ? ? ? ? ? // yolo-v2 目標(biāo)檢測(cè)輸出層=========================================

└── yolodetectionoutput.h

當(dāng)然還有一些支持的層沒(méi)有列舉到,具體以源碼為準(zhǔn)。

0x04. NCNN是如何加速的?

之所以要單獨(dú)列出這部分,是因?yàn)镹CNN作為一個(gè)前向推理框架,推理速度肯定是尤其重要的。所以這一節(jié)我就來(lái)科普一下NCNN為了提升網(wǎng)絡(luò)的運(yùn)行速度做了哪些關(guān)鍵優(yōu)化。我們需要明確一點(diǎn),當(dāng)代CNN的計(jì)算量主要集中在卷積操作上,只要卷積層的速度優(yōu)化到位,那么整個(gè)網(wǎng)絡(luò)的運(yùn)行速度就能獲得極大提升。所以,我們這里先以卷積層為例來(lái)講講NCNN是如何優(yōu)化的。

在講解之前,先貼出我前面很長(zhǎng)一段時(shí)間學(xué)習(xí)的一些優(yōu)化策略和復(fù)現(xiàn)相關(guān)的文章鏈接,因?yàn)檫@些思路至少一半來(lái)自于NCNN,所以先把鏈接匯總在這里,供需要的小伙伴獲取。

一份樸實(shí)無(wú)華的移動(dòng)端盒子濾波算法優(yōu)化筆記

基于NCNN的3x3可分離卷積再思考盒子濾波

詳解Im2Col+Pack+Sgemm策略更好的優(yōu)化卷積運(yùn)算

詳解卷積中的Winograd加速算法

道阻且長(zhǎng)_再探矩陣乘法優(yōu)化

NCNN中對(duì)卷積的加速過(guò)程(以Arm側(cè)為例)在我看來(lái)有:

無(wú)優(yōu)化

即用即取+共用行

Im2Col+GEMM

WinoGrad

SIMD

內(nèi)聯(lián)匯編

針對(duì)特定架構(gòu)如A53和A55提供更好的指令排布方式,不斷提高硬件利用率

后面又加入了Pack策略,更好的改善訪(fǎng)存,進(jìn)一步提升速度。

不得不說(shuō),NCNN的底層優(yōu)化做得還是比較細(xì)致的,所以大家一定要去白嫖 啊。這里列舉的是Arm的優(yōu)化策略,如果是x86或者其它平臺(tái)以實(shí)際代碼為準(zhǔn)。

下面貼一個(gè)帶注釋的ARM neon優(yōu)化絕對(duì)值層的例子作為結(jié)束吧,首先絕對(duì)值層的普通C++版本如下:

// 絕對(duì)值層特性: 單輸入,單輸出,可直接對(duì)輸入進(jìn)行修改

int AbsVal::forward_inplace(Mat& bottom_top_blob, const Option& opt) const

{

? ? int w = bottom_top_blob.w;? // 矩陣寬度

? ? int h = bottom_top_blob.h;? ? // 矩陣高度

? ? int channels = bottom_top_blob.c;// 通道數(shù)

? ? int size = w * h;// 一個(gè)通道的元素?cái)?shù)量

? ? #pragma omp parallel for num_threads(opt.num_threads)? // openmp 并行

? ? for (int q=0; q<channels; q++)// 每個(gè) 通道

? ? {

? ? ? ? float* ptr = bottom_top_blob.channel(q);// 當(dāng)前通道數(shù)據(jù)的起始指針

? ? ? ? for (int i=0; i<size; i++)// 遍歷每個(gè)值

? ? ? ? {

? ? ? ? ? ? if (ptr[i] < 0)

? ? ? ? ? ? ? ? ptr[i] = -ptr[i];// 小于零取相反數(shù),大于零保持原樣

? ? ? ? ? ? // ptr[i] = ptr[i] > 0 ? ptr[i] : -ptr[i];

? ? ? ? }

? ? }

? ? return 0;

}

ARM neon優(yōu)化版本如下:

//? arm 內(nèi)聯(lián)匯編

// asm(

// 代碼列表

// : 輸出運(yùn)算符列表? ? ? ? "r" 表示同用寄存器? "m" 表示內(nèi)存地址 "I" 立即數(shù)

// : 輸入運(yùn)算符列表? ? ? ? "=r" 修飾符 = 表示只寫(xiě),無(wú)修飾符表示只讀,+修飾符表示可讀可寫(xiě),&修飾符表示只作為輸出

// : 被更改資源列表

// );

// __asm__ __volatile__();

// __volatile__或volatile 是可選的,假如用了它,則是向GCC 聲明不答應(yīng)對(duì)該內(nèi)聯(lián)匯編優(yōu)化,

// 否則當(dāng) 使用了優(yōu)化選項(xiàng)(-O)進(jìn)行編譯時(shí),GCC 將會(huì)根據(jù)自己的判定決定是否將這個(gè)內(nèi)聯(lián)匯編表達(dá)式中的指令優(yōu)化掉。

// 換行符和制表符的使用可以使得指令列表看起來(lái)變得美觀(guān)。

int AbsVal_arm::forward_inplace(Mat& bottom_top_blob, const Option& opt) const

{

? ? int w = bottom_top_blob.w;? // 矩陣寬度

? ? int h = bottom_top_blob.h;? ? // 矩陣高度

? ? int channels = bottom_top_blob.c;// 通道數(shù)

? ? int size = w * h;// 一個(gè)通道的元素?cái)?shù)量

? ? #pragma omp parallel for num_threads(opt.num_threads)

? ? for (int q=0; q<channels; q++)

? ? {

? ? ? ? float* ptr = bottom_top_blob.channel(q);

#if __ARM_NEON

? ? ? ? int nn = size >> 2; // 128位的寄存器,一次可以操作 4個(gè)float,剩余不夠4個(gè)的,最后面直接c語(yǔ)言執(zhí)行

? ? ? ? int remain = size - (nn << 2);// 4*32 =128字節(jié)對(duì)其后 剩余的 float32個(gè)數(shù), 剩余不夠4個(gè)的數(shù)量

#else

? ? ? ? int remain = size;

#endif // __ARM_NEON

/*

從內(nèi)存中載入:

v7:

? 帶了前綴v的就是v7 32bit指令的標(biāo)志;

? ld1表示是順序讀取,還可以取ld2就是跳一個(gè)讀取,ld3、ld4就是跳3、4個(gè)位置讀取,這在RGB分解的時(shí)候賊方便;

? 后綴是f32表示單精度浮點(diǎn),還可以是s32、s16表示有符號(hào)的32、16位整型值。

? 這里Q寄存器是用q表示,q5對(duì)應(yīng)d10、d11可以分開(kāi)單獨(dú)訪(fǎng)問(wèn)(注:v8就沒(méi)這么方便了。)

? 大括號(hào)里面最多只有兩個(gè)Q寄存器。

? ? "vld1.f32? {q10}, [%3]!? ? ? ? \n"

? ? "vld1.s16 {q0, q1}, [%2]!? ? ? \n"

v8:

? ARMV8(64位cpu) NEON寄存器 用 v來(lái)表示 v1.8b v2.8h? v3.4s v4.2d

? 后綴為8b/16b/4h/8h/2s/4s/2d)

? 大括號(hào)內(nèi)最多支持4個(gè)V寄存器;

? "ld1? ? {v0.4s, v1.4s, v2.4s, v3.4s}, [%2], #64 \n"? // 4s表示float32

? "ld1? ? {v0.8h, v1.8h}, [%2], #32? ? \n"

? "ld1? ? {v0.4h, v1.4h}, [%2], #32? ? \n"? ? ? ? ? ? // 4h 表示int16

*/

#if __ARM_NEON

#if __aarch64__

// ARMv8-A 是首款64 位架構(gòu)的ARM 處理器,是移動(dòng)手機(jī)端使用的CPU

? ? ? ? if (nn > 0)

? ? ? ? {

? ? ? ? asm volatile(

? ? ? ? ? ? "0:? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \n"? // 0: 作為標(biāo)志,局部標(biāo)簽

? ? ? ? ? ? "prfm? ? ? pldl1keep, [%1, #128] \n"? //? 預(yù)取 128個(gè)字節(jié) 4*32 = 128

? ? ? ? ? ? "ld1? ? ? ? {v0.4s}, [%1]? ? ? ? \n"? //? 載入 ptr 指針對(duì)應(yīng)的值,連續(xù)4個(gè)

? ? ? ? ? ? "fabs? ? ? v0.4s, v0.4s? ? ? ? ? \n"? //? ptr 指針對(duì)應(yīng)的值 連續(xù)4個(gè),使用fabs函數(shù) 進(jìn)行絕對(duì)值操作 4s表示浮點(diǎn)數(shù)

? ? ? ? ? ? "subs? ? ? %w0, %w0, #1? ? ? ? ? \n"? //? %0 引用 參數(shù) nn 操作次數(shù)每次 -1? #1表示1

? ? ? ? ? ? "st1? ? ? ? {v0.4s}, [%1], #16? ? \n"? //? %1 引用 參數(shù) ptr 指針 向前移動(dòng) 4*4=16字節(jié)

? ? ? ? ? ? "bne? ? ? ? 0b? ? ? ? ? ? ? ? ? ? \n"? // 如果非0,則向后跳轉(zhuǎn)到 0標(biāo)志處執(zhí)行

? ? ? ? ? ? : "=r"(nn),? ? // %0 操作次數(shù)

? ? ? ? ? ? ? "=r"(ptr)? ? // %1

? ? ? ? ? ? : "0"(nn),? ? ? // %0 引用 參數(shù) nn

? ? ? ? ? ? ? "1"(ptr)? ? ? // %1 引用 參數(shù) ptr

? ? ? ? ? ? : "cc", "memory", "v0" /* 可能變化的部分 memory內(nèi)存可能變化*/

? ? ? ? );

? ? ? ? }

#else

// 32位 架構(gòu)處理器=========

? ? ? ? if (nn > 0)

? ? ? ? {

? ? ? ? asm volatile(

? ? ? ? ? ? "0:? ? ? ? ? ? ? ? ? ? ? ? ? ? \n"? // 0: 作為標(biāo)志,局部標(biāo)簽

? ? ? ? ? ? "vld1.f32? {d0-d1}, [%1]? ? ? \n"? // 載入 ptr處的值? q0寄存器 = d0 = d1

? ? ? ? ? ? "vabs.f32? q0, q0? ? ? ? ? ? ? \n"? // abs 絕對(duì)值運(yùn)算

? ? ? ? ? ? "subs? ? ? %0, #1? ? ? ? ? ? ? \n"? //? %0 引用 參數(shù) nn 操作次數(shù)每次 -1? #1表示1

? ? ? ? ? ? "vst1.f32? {d0-d1}, [%1]!? ? ? \n"? // %1 引用 參數(shù) ptr 指針 向前移動(dòng) 4*4=16字節(jié)

? ? ? ? ? ? "bne? ? ? ? 0b? ? ? ? ? ? ? ? ? \n"? // 如果非0,則向后跳轉(zhuǎn)到 0標(biāo)志處執(zhí)行

? ? ? ? ? ? : "=r"(nn),? ? // %0

? ? ? ? ? ? ? "=r"(ptr)? ? // %1

? ? ? ? ? ? : "0"(nn),

? ? ? ? ? ? ? "1"(ptr)

? ? ? ? ? ? : "cc", "memory", "q0"? ? ? ? ? ? ? ? /* 可能變化的部分 memory內(nèi)存可能變化*/

? ? ? ? );

? ? ? ? }

#endif // __aarch64__

#endif // __ARM_NEON

? ? ? ? for (; remain>0; remain--) // 剩余不夠4個(gè)的直接c語(yǔ)言執(zhí)行

? ? ? ? {

? ? ? ? ? ? *ptr = *ptr > 0 ? *ptr : -*ptr;

? ? ? ? ? ? ptr++;

? ? ? ? }

? ? }

? ? return 0;

}

0x05. 結(jié)語(yǔ)

介紹到這里就要結(jié)束了,這篇文章只是以我自己的視角看了一遍NCNN,如果有什么錯(cuò)誤或者筆誤歡迎評(píng)論區(qū)指出。在NCNN之后各家廠(chǎng)商紛紛推出了自己的開(kāi)源前向推理框架,例如MNN,OpenAILab的Tengine,阿里的tengine,曠視的MegEngine,華為Bolt等等,希望各個(gè)CVer都能多多支持國(guó)產(chǎn)端側(cè)推理框架。

0x06. 友情鏈接

https://github.com/Tencent/ncnn

https://github.com/MegEngine/MegEngine

https://github.com/alibaba/tengine

https://github.com/OAID/Tengine

https://github.com/alibaba/MNN

https://github.com/Ewenwan/MVision

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容