姓名:閔旭曈 學(xué)號:22009200796
【轉(zhuǎn)自】https://zhuanlan.zhihu.com/p/338121531
【嵌牛導(dǎo)讀】本文主要介紹了ncnn神經(jīng)網(wǎng)絡(luò)
【嵌牛提問】ncnn怎么學(xué)
【嵌牛正文】
$0x00$. 想法來源
CNN從15年的ResNet在ImageNet比賽中大放異彩容贝,到今天各種層出不窮的網(wǎng)絡(luò)結(jié)構(gòu)被提出以解決生活中碰到的各種問題磷籍。然而沈矿,在CNN長期發(fā)展過程中兄旬,也伴隨著很多的挑戰(zhàn)伶唯,比如如何調(diào)整算法使得在特定場景或者說數(shù)據(jù)集上取得最好的精度,如何將學(xué)術(shù)界出色的算法落地到工業(yè)界,如何設(shè)計出在邊緣端或者有限硬件條件下的定制化CNN等。前兩天看到騰訊優(yōu)圖的文章:騰訊優(yōu)圖開源這三年床佳,里面提到了NCNN背后的故事,十分感動和佩服榄审,然后我也是白嫖了很多NCNN的算法實現(xiàn)以及一些調(diào)優(yōu)技巧砌们。所以為了讓很多不太了解NCNN的人能更好的理解騰訊優(yōu)圖這個"從0到1"的深度學(xué)習(xí)框架,我將結(jié)合我自己擅長的東西來介紹「我眼中的NCNN它是什么樣的」搁进?
0x01. 如何使用NCNN
這篇文章的重點不是如何跑起來NCNN的各種Demo浪感,也不是如何使用NCNN來部署自己的業(yè)務(wù)網(wǎng)絡(luò),這部分沒有什么比官方wiki介紹得更加清楚的資料了饼问。所以這部分我只是簡要匯總一些資料影兽,以及說明一些我認為非常重要的東西。
官方wiki指路:https://github.com/Tencent/ncnn/wiki
在NCNN中新建一個自定義層教程: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. 運行流程解析
要了解一個深度學(xué)習(xí)框架莱革,首先得搞清楚這個框架是如何通過讀取一張圖片然后獲得的我們想要的輸出結(jié)果峻堰,這個運行流程究竟是長什么樣的讹开?我們看一下NCNN官方wiki中提供一個示例代碼:
#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ù)的值域為[-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ò)輸入左冬,線程數(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對于我們給定的一個網(wǎng)絡(luò)(首先轉(zhuǎn)換為NCNN的param和bin文件)和輸入梅忌,首先執(zhí)行圖像預(yù)處理,這是基于「ncnn::Mat」這個數(shù)據(jù)結(jié)構(gòu)完成的除破。
其中牧氮,from_pixels_resize()這個函數(shù)的作用是生成目標尺寸大小的網(wǎng)絡(luò)輸入Mat,它的實現(xiàn)在https://github.com/Tencent/ncnn/blob/b93775a27273618501a15a235355738cda102a38/src/mat_pixel.cpp#L2543瑰枫。它的內(nèi)部實際上是「根據(jù)傳入的輸入圖像的通道數(shù)」完成resize_bilinear_c1/c2/c3/4即一通道/二通道/三通道/四通道 圖像變形算法踱葛,可以看到使用的是雙線性插值算法。這些操作的實現(xiàn)在https://github.com/Tencent/ncnn/blob/master/src/mat_pixel_resize.cpp#L27光坝。然后經(jīng)過Resize之后尸诽,需要將像素圖像轉(zhuǎn)換成ncnn::Mat。這里調(diào)用的是Mat::from_pixels()這個函數(shù)盯另,它將我們Resize操作之后獲得的像素圖像數(shù)據(jù)(即float*數(shù)據(jù))根據(jù)特定的輸入類型賦值給ncnn::Mat性含。
接下來,我們講講substract_mean_normalize()這個函數(shù)鸳惯,它實現(xiàn)了減均值和歸一化操作商蕴,它的實現(xiàn)在:https://github.com/Tencent/ncnn/blob/master/src/mat.cpp#L34。具體來說芝发,這個函數(shù)根據(jù)均值參數(shù)和歸一化參數(shù)的有無分成這幾種情況:
「有均值參數(shù)」
「創(chuàng)建 偏置層」ncnn::create_layer(ncnn::LayerType::Bias);? 載入層參數(shù) op->load_param(pd);? 3通道
「載入層權(quán)重數(shù)據(jù)」op->load_model(ncnn::ModelBinFromMatArray(weights));? -均值參數(shù)
「運行層」op->forward_inplace(*this);
「有歸一化參數(shù)」
「創(chuàng)建 尺度層」ncnn::create_layer(ncnn::LayerType::Scale);? 載入層參數(shù) op->load_param(pd);? 3通道
「載入層權(quán)重數(shù)據(jù)」op->load_model(ncnn::ModelBinFromMatArray(weights));? 尺度參數(shù)
「運行層」op->forward_inplace(*this);
「有均值和歸一化參數(shù)」
「創(chuàng)建 尺度層」ncnn::create_layer(ncnn::LayerType::Scale);? 載入層參數(shù) op->load_param(pd);? 3通道
「載入層權(quán)重數(shù)據(jù)」op->load_model(ncnn::ModelBinFromMatArray(weights));? -均值參數(shù) 和 尺度參數(shù)
「運行層」op->forward_inplace(*this);
可以看到NCNN的均值和歸一化操作绪商,是直接利用了它的Bias Layer和Scale Layer來實現(xiàn)的,也就是說NCNN中的每個層都可以單獨拿出來運行我們自己數(shù)據(jù)辅鲸,更加方便我們白嫖 格郁。
0x02.01 模型解析ncnn::Net
param 解析
完成了圖像預(yù)處理之后,新增了一個ncnn::Net瓢湃,然后調(diào)用Net::load_param來載入網(wǎng)絡(luò)參數(shù)文件*.proto理张, 這部分的實現(xiàn)在https://github.com/Tencent/ncnn/blob/master/src/net.cpp#L115。在講解這個函數(shù)在的過程之前绵患,我們先來一起分析一下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
? ...
? 層類型? ? ? ? ? ? 層名字? 輸入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卷積步長 stride();
? ? ? ? ? 4卷積填充pad_size();? ? ? 5卷積偏置有無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池化方式:最大值织狐、均值暂幼、隨機? ? 1池化核大小 kernel_size();? ? 2池化核步長 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
? 后面的第一個參數(shù)表示數(shù)組元素數(shù)量,2表示包含兩個元素
然后官方的wiki中提供了所有網(wǎng)絡(luò)層的詳細參數(shù)設(shè)置移迫,地址為:https://github.com/Tencent/ncnn/wiki/operation-param-weight-table
了解了Param的基本含義之后旺嬉,我們可以來看一下Net::load_param這個函數(shù)是在做什么了。
從函數(shù)實現(xiàn)厨埋,我們知道邪媳,首先會遍歷param文件中的所有網(wǎng)絡(luò)層,然后根據(jù)當前層的類型調(diào)用create_layer()/ net::create_custom_layer()來創(chuàng)建網(wǎng)絡(luò)層荡陷,然后讀取輸入Blobs和輸出Blobs和當前層綁定雨效,再調(diào)用paramDict::load_param(fp)解析當前層的特定參數(shù)(參數(shù)字典),按照id=參數(shù)/參數(shù)數(shù)組來解析废赞。最后徽龟,當前層調(diào)用layer->load_param(pd)載入解析得到的層特殊參數(shù)即獲得當前層特有的參數(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)// 讀取 等號前面的 key=========
? ? {
? ? ? ? bool is_array = id <= -23300;
? ? ? ? if (is_array)
? ? ? ? {
? ? ? ? ? ? id = -id - 23300;// 數(shù)組 關(guān)鍵字 -23300? 得到該參數(shù)在參數(shù)數(shù)組中的 index
? ? ? ? }
// 是以 -23300 開頭表示的數(shù)組===========
? ? ? ? if (is_array)
? ? ? ? {
? ? ? ? ? ? int len = 0;
? ? ? ? ? ? int nscan = fscanf(fp, "%d", &len);// 后面的第一個參數(shù)表示數(shù)組元素數(shù)量唉地,5表示包含兩個元素
? ? ? ? ? ? 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);// 檢查該字段是否為 浮點數(shù)的字符串
? ? ? ? ? ? ? ? if (is_float)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? float* ptr = params[id].v;
? ? ? ? ? ? ? ? ? ? nscan = sscanf(vstr, "%f", &ptr[j]);// 轉(zhuǎ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);// 獲取等號后面的 字符串
? ? ? ? ? ? if (nscan != 1)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? fprintf(stderr, "ParamDict read value fail\n");
? ? ? ? ? ? ? ? return -1;
? ? ? ? ? ? }
? ? ? ? ? ? bool is_float = vstr_is_float(vstr);// 判斷是否為浮點數(shù)
? ? ? ? ? ? if (is_float)
? ? ? ? ? ? ? ? nscan = sscanf(vstr, "%f", ¶ms[id].f); // 讀入為浮點數(shù)
? ? ? ? ? ? else
? ? ? ? ? ? ? ? nscan = sscanf(vstr, "%d", ¶ms[id].i);// 讀入為整數(shù)
? ? ? ? ? ? if (nscan != 1)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? fprintf(stderr, "ParamDict parse value fail\n");
? ? ? ? ? ? ? ? return -1;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? params[id].loaded = 1;// 設(shè)置該 參數(shù)以及載入
? ? }
? ? return 0;
}
// 讀取 二進制格式的 參數(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);// 讀入一個整數(shù)長度的 index
? ? while (id != -233)// 結(jié)尾
? ? {
? ? ? ? bool is_array = id <= -23300;
? ? ? ? if (is_array)
? ? ? ? {
? ? ? ? ? ? id = -id - 23300;// 數(shù)組關(guān)鍵字對應(yīng)的 index
? ? ? ? }
// 是數(shù)組數(shù)據(jù)=======
? ? ? ? if (is_array)
? ? ? ? {
? ? ? ? ? ? int len = 0;
? ? ? ? ? ? fread(&len, sizeof(int), 1, fp);// 數(shù)組元素數(shù)量
? ? ? ? ? ? params[id].v.create(len);
? ? ? ? ? ? float* ptr = params[id].v;
? ? ? ? ? ? fread(ptr, sizeof(float), len, fp);// 按浮點數(shù)長度*數(shù)組長度 讀取每一個數(shù)組元素====
? ? ? ? }
// 是普通數(shù)據(jù)=======
? ? ? ? else
? ? ? ? {
? ? ? ? ? ? fread(¶ms[id].f, sizeof(float), 1, fp);// 按浮點數(shù)長度讀取 該普通字段對應(yīng)的元素
? ? ? ? }
? ? ? ? params[id].loaded = 1;
? ? ? ? fread(&id, sizeof(int), 1, fp);// 讀取 下一個 index
? ? }
? ? return 0;
}
bin 解析
解析完param文件据悔,接下來需要對bin文件進行解析,這部分的實現(xiàn)在:https://github.com/Tencent/ncnn/blob/master/src/net.cpp#L672耘沼。這里執(zhí)行的主要的操作如下:
創(chuàng)建 ModelBinFromStdio 對象 提供載入?yún)?shù)的接口函數(shù)ModelBinFromStdio::load()根據(jù) 權(quán)重數(shù)據(jù)開始的一個四字節(jié)數(shù)據(jù)類型參數(shù)(float32/float16/int8等) 和 指定的參數(shù)數(shù)量 讀取數(shù)據(jù)到 Mat 并返回Mat极颓, 這個函數(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ù) 調(diào)整運行流水線 layer->create_pipeline 例如卷積層和全連接層
量化的網(wǎng)絡(luò)需要融合 Net::fuse_network()
bin文件的結(jié)構(gòu)如下:
? ? +---------+---------+---------+---------+---------+---------+
? ? | weight1 | weight2 | weight3 | weight4 | ....... | weightN |
? ? +---------+---------+---------+---------+---------+---------+
? ? ^? ? ? ? ^? ? ? ? ^? ? ? ? ^
? ? 0x0? ? ? 0x80? ? ? 0x140? ? 0x1C0
? 所有權(quán)重數(shù)據(jù)連接起來, 每個權(quán)重占 32bit讼昆。
? 權(quán)重數(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,? 如果層實現(xiàn)顯式強制存儲類型,則可以省略? ? ?
? ? ? raw data : 原始權(quán)重數(shù)據(jù)骚烧、little endian浸赫、float32數(shù)據(jù)或float16數(shù)據(jù)或量化表和索引,具體取決于存儲類型標志
? ? ? padding : 32位對齊的填充空間赃绊,如果已經(jīng)對齊既峡,則可以省略。
感覺bin解析這部分了解一下就好碧查,如果感興趣可以自己去看看源碼运敢。
0x02.03 網(wǎng)絡(luò)運行 ncnn::Extractor
至此,我們將網(wǎng)絡(luò)的結(jié)構(gòu)和權(quán)重信息都放到了ncnn::Net這個結(jié)構(gòu)中忠售,接下來我們就可以新建網(wǎng)絡(luò)提取器Extractor Net::create_extractor传惠,它給我們提供了設(shè)置網(wǎng)絡(luò)輸入(Extractor::input),獲取網(wǎng)絡(luò)輸出(Extractor::extract)稻扬,設(shè)置網(wǎng)絡(luò)運行線程參數(shù)(Extractor::set_num_threads)等接口卦方。接下來,我們只需要調(diào)用Extractor::extract運行網(wǎng)絡(luò)(net)的前向傳播函數(shù)net->forward_layer就可以獲得最后的結(jié)果了泰佳。
另外盼砍,ncnn::Extractor還可以設(shè)置一個輕模式省內(nèi)存 即set_light_mode(true)尘吗,原理是net中每個layer都會產(chǎn)生blob,除了最后的結(jié)果和多分支中間結(jié)果浇坐,大部分blob都不值得保留睬捶,開啟輕模式可以在運算后自動回收,省下內(nèi)存近刘。但需要注意的是擒贸,一旦開啟這個模式,我們就不能獲得中間層的特征值了跌宛,因為中間層的內(nèi)存在獲得最終結(jié)果之前都被回收掉了酗宋。例如:「某網(wǎng)絡(luò)結(jié)構(gòu)為 A -> B -> C积仗,在輕模式下疆拘,向ncnn索要C結(jié)果時,A結(jié)果會在運算B時自動回收寂曹,而B結(jié)果會在運算C時自動回收哎迄,最后只保留C結(jié)果,后面再需要C結(jié)果會直接獲得隆圆,滿足大多數(shù)深度網(wǎng)絡(luò)的使用方式」漱挚。
最后,我們需要明確一下渺氧,我們剛才是先創(chuàng)建了ncnn::net旨涝,然后我們調(diào)用的ncnn::Extractor作為運算實例,因此運算實例是不受net限制的侣背。換句話說白华,雖然我們只有一個net,但我們可以開多個ncnn::Extractor贩耐,這些實例都是單獨完成特定網(wǎng)絡(luò)的推理弧腥,互不影響。
這樣我們就大致了解了NCNN的運行流程了潮太,更多的細節(jié)可以關(guān)注NCNN源碼管搪。
0x03. NCNN源碼目錄分析
這一節(jié),我們來分析一下NCNN源碼目錄以便更好的理解整個工程铡买。src的目錄結(jié)構(gòu)如下:
/src 目錄:
./src/layer下是所有的layer定義代碼
./src/layer/arm是arm下的計算加速的layer
./src/layer/x86是x86下的計算加速的layer更鲁。
./src/layer/mips是mips下的計算加速的layer。
./src/layer/.h + ./src/layer/.cpp 是各種layer的基礎(chǔ)實現(xiàn)奇钞,無加速澡为。
目錄頂層下是一些基礎(chǔ)代碼,如宏定義蛇券,平臺檢測缀壤,mat數(shù)據(jù)結(jié)構(gòu)樊拓,layer定義,blob定義塘慕,net定義等筋夏。
benchmark.h + benchmark.cpp 測試各個模型的執(zhí)行速度
allocator.h + allocator.cpp 內(nèi)存池管理,內(nèi)存對齊
paramdict.h + paramdict.cpp 層參數(shù)解析 讀取二進制格式图呢、字符串格式条篷、密文格式的參數(shù)文件
opencv.h opencv.cpp? opencv 風(fēng)格的數(shù)據(jù)結(jié)構(gòu) 的 mini實現(xiàn),包含大小結(jié)構(gòu)體 Size蛤织,矩陣框結(jié)構(gòu)體 Rect_ 交集 并集運算符重載赴叹,點結(jié)構(gòu)體? ? Point_,矩陣結(jié)構(gòu)體? Mat? ? 深拷貝 淺拷貝 獲取指定矩形框中的roi 讀取圖像 寫圖像 雙線性插值算法改變大小等等
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(), 矩陣周圍填充; resize_bilinear_image(),雙線性插值等函數(shù)免猾。
net.h net.cpp? ncnn框架接口是辕,包含注冊 用戶定義的新層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ù)猎提。
源碼目錄除了這些還有很多文件获三,介于篇幅原因就不再枚舉了,感興趣的可以自行查看源碼锨苏。由于我只對x86和arm端的指令集加速熟悉一些疙教,所以這里再枚舉一下src/layers下面的NCNN支持的層的目錄:
├── absval.cpp? ? ? ? ? ? ? ? ? ? ? // 絕對值層
├── absval.h
├── argmax.cpp? ? ? ? ? ? ? ? ? ? ? // 最大值層
├── argmax.h
├── arm ============================ arm平臺下的層
│? ├── absval_arm.cpp? ? ? ? ? ? ? // 絕對值層
│? ├── 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(點乘), 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ù)庫
│? ├── 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 負指數(shù)倒數(shù)歸一化 激活層? 1/(1 + e^(-zi))
│? ├── sigmoid_arm.h
│? ├── softmax_arm.cpp? ? ? ? ? ? ? ? // softmax 指數(shù)求和歸一化 激活層? e^(zi) / sum(e^(zi))
│? └── softmax_arm.h
|
|
|================================ 普通平臺 待優(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的簡稱 f(x)=log(1 + exp(x))? 激活層
├── bnll.h
├── clip.cpp? ? ? ? ? ? ? ? ? // 截斷=====
├── 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 的檢測輸出層================================
├── detectionoutput.h
├── dropout.cpp? ? ? ? ? ? ? // 隨機失活層 在訓(xùn)練時由于舍棄了一些神經(jīng)元,因此在測試時需要在激勵的結(jié)果中乘上因子p進行縮放.
├── dropout.h
├── eltwise.cpp? ? ? ? ? ? ? // 逐元素操作, product(點乘), sum(相加減) 和 max(取大值)
├── eltwise.h
├── elu.cpp? ? ? ? ? ? ? ? ? // 指數(shù)線性單元relu激活層 Prelu : (a*x, x) ----> Erelu : (a*(e^x - 1), x)
├── elu.h
├── embed.cpp? ? ? ? ? ? ? ? // 嵌入層驯击,用在網(wǎng)絡(luò)的開始層將你的輸入轉(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? ? ? ? ? // 單樣本 標準化 規(guī)范化
├── instancenorm.h
├── interp.cpp? ? ? ? ? ? ? ? // 插值層 上下采樣等
├── interp.h
├── log.cpp? ? ? ? ? ? ? ? ? // 對數(shù)層
├── log.h
├── lrn.cpp? ? ? ? ? ? ? ? ? // Local Response Normalization烁兰,即局部響應(yīng)歸一化層
├── lrn.h? ? ? ? ? ? ? ? ? ? // 對局部神經(jīng)元的活動創(chuàng)建競爭機制,使得其中響應(yīng)比較大的值變得相對更大徊都,
|? ? ? ? ? ? ? ? ? ? ? ? ? ? // 并抑制其他反饋較小的神經(jīng)元沪斟,增強了模型的泛化能力
├── lstm.cpp? ? ? ? ? ? ? ?
├── lstm.h? ? ? ? ? ? ? ? ? ? // lstm 長短詞記憶層
├── memorydata.cpp? ? ? ? ? ? // 內(nèi)存數(shù)據(jù)層
├── memorydata.h
├── mvn.cpp
├── mvn.h
├── normalize.cpp? ? ? ? ? ? // 歸一化
├── normalize.h
├── padding.cpp? ? ? ? ? ? ? // 填充,警戒線
├── 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 獨有的層 建議框生成層 L1 loss 擬合============================
├── priorbox.h
├── proposal.cpp? ? ? ? ? ? ? // faster rcnn 獨有的層 建議框生成,將rpn網(wǎng)絡(luò)的輸出轉(zhuǎn)換成建議框========
├── proposal.h
├── quantize.cpp? ? ? ? ? ? ? // 量化層
├── quantize.h
├── reduction.cpp? ? ? ? ? ? // 將輸入的特征圖按照給定的維度進行求和或求平均
├── reduction.h
├── relu.cpp? ? ? ? ? ? ? ? ? // relu 激活層: (0,x)
├── relu.h
├── reorg.cpp? ? ? ? ? ? ? ? // yolov2 獨有的層主之, 一拆四層择吊,一個大矩陣,下采樣到四個小矩陣=================
├── reorg.h
├── reshape.cpp? ? ? ? ? ? ? // 變形層: 在不改變數(shù)據(jù)的情況下槽奕,改變輸入的維度
├── reshape.h
├── rnn.cpp? ? ? ? ? ? ? ? ? // rnn 循環(huán)神經(jīng)網(wǎng)絡(luò)
├── rnn.h
├── roipooling.cpp? ? ? ? ? ? // faster Rcnn 獨有的層几睛, ROI池化層: 輸入m*n 均勻劃分成 a*b個格子后池化,得到固定長度的特征向量 ==========
├── roipooling.h
├── scale.cpp? ? ? ? ? ? ? ? // bn 層之后的 平移縮放層
├── scale.h
├── shufflechannel.cpp? ? ? ? // ShuffleNet 獨有的層粤攒,通道打亂所森,通道混合層=================================
├── shufflechannel.h
├── sigmoid.cpp? ? ? ? ? ? ? // 負指數(shù)倒數(shù)歸一化層? 1/(1 + e^(-zi))
├── sigmoid.h
├── slice.cpp? ? ? ? ? ? ? ? // concat的反向操作, 通道分開層夯接,適用于多任務(wù)網(wǎng)絡(luò)
├── slice.h
├── softmax.cpp? ? ? ? ? ? ? // 指數(shù)求和歸一化層? e^(zi) / sum(e^(zi))
├── softmax.h
├── split.cpp? ? ? ? ? ? ? ? // 將blob復(fù)制幾份焕济,分別給不同的layer,這些上層layer共享這個blob盔几。
├── split.h
├── spp.cpp? ? ? ? ? ? ? ? ? // 空間金字塔池化層 1+4+16=21 SPP-NET 獨有===================================
├── spp.h
├── squeeze.cpp? ? ? ? ? ? ? // squeezeNet獨有層晴弃, 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的某個維度,擴大n倍问欠。比如原來是1234肝匆,擴大兩倍變成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 目標檢測輸出層=========================================
└── yolodetectionoutput.h
當然還有一些支持的層沒有列舉到度硝,具體以源碼為準肿轨。
0x04. NCNN是如何加速的?
之所以要單獨列出這部分蕊程,是因為NCNN作為一個前向推理框架椒袍,推理速度肯定是尤其重要的。所以這一節(jié)我就來科普一下NCNN為了提升網(wǎng)絡(luò)的運行速度做了哪些關(guān)鍵優(yōu)化藻茂。我們需要明確一點驹暑,當代CNN的計算量主要集中在卷積操作上,只要卷積層的速度優(yōu)化到位辨赐,那么整個網(wǎng)絡(luò)的運行速度就能獲得極大提升优俘。所以,我們這里先以卷積層為例來講講NCNN是如何優(yōu)化的掀序。
在講解之前帆焕,先貼出我前面很長一段時間學(xué)習(xí)的一些優(yōu)化策略和復(fù)現(xiàn)相關(guān)的文章鏈接,因為這些思路至少一半來自于NCNN不恭,所以先把鏈接匯總在這里叶雹,供需要的小伙伴獲取财饥。
詳解Im2Col+Pack+Sgemm策略更好的優(yōu)化卷積運算
NCNN中對卷積的加速過程(以Arm側(cè)為例)在我看來有:
無優(yōu)化
即用即取+共用行
Im2Col+GEMM
WinoGrad
SIMD
內(nèi)聯(lián)匯編
針對特定架構(gòu)如A53和A55提供更好的指令排布方式,不斷提高硬件利用率
后面又加入了Pack策略折晦,更好的改善訪存佑力,進一步提升速度。
不得不說筋遭,NCNN的底層優(yōu)化做得還是比較細致的打颤,所以大家一定要去白嫖 啊。這里列舉的是Arm的優(yōu)化策略漓滔,如果是x86或者其它平臺以實際代碼為準编饺。
下面貼一個帶注釋的ARM neon優(yōu)化絕對值層的例子作為結(jié)束吧,首先絕對值層的普通C++版本如下:
// 絕對值層特性: 單輸入响驴,單輸出透且,可直接對輸入進行修改
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;// 一個通道的元素數(shù)量
? ? #pragma omp parallel for num_threads(opt.num_threads)? // openmp 并行
? ? for (int q=0; q<channels; q++)// 每個 通道
? ? {
? ? ? ? float* ptr = bottom_top_blob.channel(q);// 當前通道數(shù)據(jù)的起始指針
? ? ? ? for (int i=0; i<size; i++)// 遍歷每個值
? ? ? ? {
? ? ? ? ? ? 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(
// 代碼列表
// : 輸出運算符列表? ? ? ? "r" 表示同用寄存器? "m" 表示內(nèi)存地址 "I" 立即數(shù)
// : 輸入運算符列表? ? ? ? "=r" 修飾符 = 表示只寫豁鲤,無修飾符表示只讀秽誊,+修飾符表示可讀可寫,&修飾符表示只作為輸出
// : 被更改資源列表
// );
// __asm__ __volatile__();
// __volatile__或volatile 是可選的琳骡,假如用了它锅论,則是向GCC 聲明不答應(yīng)對該內(nèi)聯(lián)匯編優(yōu)化,
// 否則當 使用了優(yōu)化選項(-O)進行編譯時楣号,GCC 將會根據(jù)自己的判定決定是否將這個內(nèi)聯(lián)匯編表達式中的指令優(yōu)化掉最易。
// 換行符和制表符的使用可以使得指令列表看起來變得美觀。
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;// 一個通道的元素數(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個float,剩余不夠4個的藻懒,最后面直接c語言執(zhí)行
? ? ? ? int remain = size - (nn << 2);// 4*32 =128字節(jié)對其后 剩余的 float32個數(shù), 剩余不夠4個的數(shù)量
#else
? ? ? ? int remain = size;
#endif // __ARM_NEON
/*
從內(nèi)存中載入:
v7:
? 帶了前綴v的就是v7 32bit指令的標志;
? ld1表示是順序讀取视译,還可以取ld2就是跳一個讀取嬉荆,ld3、ld4就是跳3酷含、4個位置讀取鄙早,這在RGB分解的時候賊方便;
? 后綴是f32表示單精度浮點第美,還可以是s32蝶锋、s16表示有符號的32、16位整型值什往。
? 這里Q寄存器是用q表示扳缕,q5對應(yīng)d10、d11可以分開單獨訪問(注:v8就沒這么方便了。)
? 大括號里面最多只有兩個Q寄存器躯舔。
? ? "vld1.f32? {q10}, [%3]!? ? ? ? \n"
? ? "vld1.s16 {q0, q1}, [%2]!? ? ? \n"
v8:
? ARMV8(64位cpu) NEON寄存器 用 v來表示 v1.8b v2.8h? v3.4s v4.2d
? 后綴為8b/16b/4h/8h/2s/4s/2d)
? 大括號內(nèi)最多支持4個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 處理器,是移動手機端使用的CPU
? ? ? ? if (nn > 0)
? ? ? ? {
? ? ? ? asm volatile(
? ? ? ? ? ? "0:? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \n"? // 0: 作為標志粥庄,局部標簽
? ? ? ? ? ? "prfm? ? ? pldl1keep, [%1, #128] \n"? //? 預(yù)取 128個字節(jié) 4*32 = 128
? ? ? ? ? ? "ld1? ? ? ? {v0.4s}, [%1]? ? ? ? \n"? //? 載入 ptr 指針對應(yīng)的值丧失,連續(xù)4個
? ? ? ? ? ? "fabs? ? ? v0.4s, v0.4s? ? ? ? ? \n"? //? ptr 指針對應(yīng)的值 連續(xù)4個,使用fabs函數(shù) 進行絕對值操作 4s表示浮點數(shù)
? ? ? ? ? ? "subs? ? ? %w0, %w0, #1? ? ? ? ? \n"? //? %0 引用 參數(shù) nn 操作次數(shù)每次 -1? #1表示1
? ? ? ? ? ? "st1? ? ? ? {v0.4s}, [%1], #16? ? \n"? //? %1 引用 參數(shù) ptr 指針 向前移動 4*4=16字節(jié)
? ? ? ? ? ? "bne? ? ? ? 0b? ? ? ? ? ? ? ? ? ? \n"? // 如果非0惜互,則向后跳轉(zhuǎn)到 0標志處執(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: 作為標志布讹,局部標簽
? ? ? ? ? ? "vld1.f32? {d0-d1}, [%1]? ? ? \n"? // 載入 ptr處的值? q0寄存器 = d0 = d1
? ? ? ? ? ? "vabs.f32? q0, q0? ? ? ? ? ? ? \n"? // abs 絕對值運算
? ? ? ? ? ? "subs? ? ? %0, #1? ? ? ? ? ? ? \n"? //? %0 引用 參數(shù) nn 操作次數(shù)每次 -1? #1表示1
? ? ? ? ? ? "vst1.f32? {d0-d1}, [%1]!? ? ? \n"? // %1 引用 參數(shù) ptr 指針 向前移動 4*4=16字節(jié)
? ? ? ? ? ? "bne? ? ? ? 0b? ? ? ? ? ? ? ? ? \n"? // 如果非0,則向后跳轉(zhuǎn)到 0標志處執(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個的直接c語言執(zhí)行
? ? ? ? {
? ? ? ? ? ? *ptr = *ptr > 0 ? *ptr : -*ptr;
? ? ? ? ? ? ptr++;
? ? ? ? }
? ? }
? ? return 0;
}
0x05. 結(jié)語
介紹到這里就要結(jié)束了训堆,這篇文章只是以我自己的視角看了一遍NCNN描验,如果有什么錯誤或者筆誤歡迎評論區(qū)指出。在NCNN之后各家廠商紛紛推出了自己的開源前向推理框架坑鱼,例如MNN膘流,OpenAILab的Tengine,阿里的tengine鲁沥,曠視的MegEngine呼股,華為Bolt等等,希望各個CVer都能多多支持國產(chǎn)端側(cè)推理框架画恰。
0x06. 友情鏈接
https://github.com/Tencent/ncnn
https://github.com/MegEngine/MegEngine
https://github.com/alibaba/tengine
https://github.com/OAID/Tengine