ncnn学习
对ncnn学习的一些汇总。
Mat内存分布
Mat是ncnn所有的数据对象集合,因此我们必须对其有所了解,下面这个函数就是其中一个构造函数(这里需要吐槽一下,ncnn的设计就是单幅图像输入,所以mat最大只支持三维),其中需要注意到的就是内存分配时需要对齐:
void Mat::create(int _w, int _h, int _c, size_t _elemsize, int _elempack, Allocator* _allocator) |
块对齐 (block aligin)
这是nihui对于mat内存排布的说明。
mat shape w=3 h=2 c=4 |
by channel alignSize
ncnn通常把以单个通道的图像(h*w)进行读取,然后进行一些卷积操作,所以要by
channel的对数据进行对齐,考虑到对于不同的元素有不同的elemsize
,同时在分配时还需要对内存块大小进行对齐,为了快速的对hw的内存进行读写,他这里默认分配16bit的倍数。
// element size in bytes |
比如我们申请float矩阵w=3,h=9,c=4
,那么每一个channel
的内存块本来应该是3*9=27 byte = 27*elemsize = 108 bit
,然而108
不是16
的倍数,所以用alignSize
计算到最小16的倍数为112,然后再除elemsize得到cstep为28。
cstep = alignSize((size_t)w * h * elemsize, 16) / elemsize;
whole alignSize
然后根据上述思路,整体的大小是以4倍大小分配的。 size_t totalsize = alignSize(total() * elemsize, 4);
内存申请 (Malloc)
同时申请到的内存位置也需要对齐在内存上,便于我们的cpu整块读取,下面是整体的函数,我这里执行的是posix_memalign
,不过还是有必要讲讲具体思路。
|
fastMalloc
malloc
假设我是内存对齐MALLOC_ALIGN
是32位。 unsigned char* udata = (unsigned char*)malloc(size + sizeof(void*) + MALLOC_ALIGN);
sizeof(void*)
是为了用来保存原始malloc出来的数据空间,然后加上MALLOC_ALIGN
的大小,因为我们要进行多大的对齐,我们需要偏移的大小总是在0~MALLOC_ALIGN
中,所以加上MALLOC_ALIGN
即可。
如果我的size
是112
,那么实际申请的大小是112+8+32=152
,假设我这里申请到的内存为0x555555b1cb30
。
align ptr
接下来我们要对刚刚申请的udata
进行一系列操作,首要的事情就是要把起步的内存块进行一个内存对齐,我们要对一个指针操作,那么需要先转成指针的指针,并且需要考虑到对内存进行偏移之后,我们需要保存原来的block
的起始地址用于释放内存,否则就会出现问题,ncnn的思路就是原来申请的内存块的头部存放着原始地址,后面一块数据才是实际使用的。
所以我们会跳过一个小的内存开始对齐,其中对齐的方法也是和找到MALLOC_ALIGN
:
unsigned char **adata = alignPtr((unsigned char **)udata + 1, MALLOC_ALIGN);
然后我们返回对齐的指针adata,然后把adata前面一个地址空间保存raw的地址,最终的内存分布如下图所示:
align with MALLOC_ALIGN |
NCNN x86 cpu加速
想要使用ncnn的一些加速方法,需要从内存管理就开始适配,比如我想给定输入大小进行malloc,ncnn底层就会自动帮我padding到4的倍数,这就需要十分注意。所以这里显示的h,w是正确的,但是cstep并不是6。
TEST(cpp_lang, ncnn_mat_create_shape)
{
nncase::runtime_shape_t in_shape { 2, 3, 4 };
Mat m((int)in_shape[0], (int)in_shape[1], (int)in_shape[2]);
cout << m.cstep << endl; // 8
cout << m.w << endl; // 2
cout << m.h << endl; // 3
cout << in_shape[0] * in_shape[1] << endl; // 6
}
elempack
首先获得输入的elemsize
和elempack
,然后默认输出的out_elempack=1
int w = bottom_blob.w;
int h = bottom_blob.h;
int channels = bottom_blob.c;
size_t elemsize = bottom_blob.elemsize;
int elempack = bottom_blob.elempack;
const int kernel_extent_w = dilation_w * (kernel_w - 1) + 1;
const int kernel_extent_h = dilation_h * (kernel_h - 1) + 1;
Mat bottom_blob_bordered;
make_padding(bottom_blob, bottom_blob_bordered, opt);
if (bottom_blob_bordered.empty())
return -100;
w = bottom_blob_bordered.w;
h = bottom_blob_bordered.h;
int outw = (w - kernel_extent_w) / stride_w + 1;
int outh = (h - kernel_extent_h) / stride_h + 1;
int out_elempack = 1;
根据平台特性设定out_elempack
:
|
计算输出elemsize
,假设原始输入c是3,他的elemsize是4,输入数据是3*4=12,
out channel是64,假设elemsize是4,输出数据是64*4=256 ,
现在输出要8个一组,所以256/8=32
所以输出的elemsize是32。最后输出top blob
申请的内存就是outw,outh,channel=8 (64/8),elemsize=32 (4*8)
所以ncnn这都是channel通道上的packing,所以对于channel数大的情况下,卷积速度就快。
size_t out_elemsize = elemsize / elempack * out_elempack; |
卷积执行操作
x86的cpu优化主要还是看packing的,所以他的卷积函数选择都是看输入packing大小和输出packing大小,主要我看了一下就这一些选项,其中的卷积函数都是类似的。
if (elempack == 8 && out_elempack == 8)
if (elempack == 1 && out_elempack == 8)
if (elempack == 4 && out_elempack == 8)
if (elempack == 8 && out_elempack == 1)
if (elempack == 8 && out_elempack == 4)
同时这里卷积的输出后,他可能是packing的,所以ncnn还提供了convert_packing
函数对pack的mat进行转换,不过他只支持output与pack的值为倍数关系时才能成功转换,这里进行转换之后原来的内存块padding的位置应该是被填充了,然后尾部会多余一些值。
h*w |