这是一篇解释的文章,主要是填补我在HLS的Conv和Pool部分时的一个坑!
首先,感谢一波ESELAB蓝同学,不断被询问下,我也理解了之前的这个点。原创好文。
1、首先,回顾下卷积中的一些概念。
在卷积的过程中,输入特征用feature_in表示,不分块的话,是一个三维的数据,即feature_in的长W、宽H和张数C。卷积核用kernel表示,不分块的话,是一个四维的数据,即kernel的长Kx、宽Ky、一个channel上的相同卷积核的数目K和不同卷积核的数目CHout。用下面的图片表示。
除了feature_in和kernel的维度定义外,还需要明白channel的概念,对于feature_in的channel数目,就是feature_in的张数,即C的大小。对于kernel的channel数目,就是不同的卷积核数目,即CHout的值。feature_in的一个channel上只有一张feature_in,但kernel的一个channel上可以有很多个kernel,以此来提取多张图的同一个特征。总体而言,多张feature_in与一个kernel进行卷积,其实就是把feature_in的多个channel输入到卷积核的一个channel中,最终运算得到一张feature map。至于多张feature_in最后为啥得到的是一张feature map,可以看下面的过程。这段有其他理解,不懂的可以看2021年05月16日的补充点,在最后面。
卷积层的基本运算流程为:C个channel的feature_in的第一个数据,与K个相同的kernel的第一个核数据,对应相乘,由于C和K是相等的,相乘后有C或K个结果,将这些结果相加,可以得到第一个数据卷积运算的第一步结果,再将Kx和Ky其他数据点,按类似的操作,对应位置乘和,根据Kx和Ky的大小,可以得到KxKy个数据。这些KxKy个数据直接相加,就是最终的feature map的第一个数据点。
2、紧接着,来看下不进行分块带来的一些问题。
根据上面提到的卷积运算,如果不进行分块,假设一次乘法或一次加法都消耗1T,那么不分块下,得到feature map的第一个数据,需要消耗的周期数为(C+1)·9+1=9C+10。
需要说明的是,要从CPU顺序运行的角度,来思考不分块下带来的问题,之后再想到分块并行下的好处,不能上来就认为块与块之前是并行计算的,否则,即使不分块也直接是消耗周期最小了,这是不合实际的。
从得到的结果,可以发现,消耗的周期数与C有关(C其实就是一个kernel的channel的相同kernel个数K,后面都用C表示),为了降低周期数,可以让C的值降下来,而C就是乘法的运算周期,可以用分块的方式,完成并行加速。
3、channel方向上的分块处理。
先说明下channel方向,这里的channel方向是针对feature_in和kernel分别而言的,如果对feature_in进行分块,feature_in就会变成四维数据,如果对kernel进行分块,kernel就会变成五维数据。虽然分别对feature_in和kernel分别而言,但分块的方式是一样的,这也很好理解,因为,需要做子块间的相乘,相乘时的尺寸要求就需要一样了。OK,说明完最基本的概念。那么对于feature_in的channel方向来分块,其实就是将feature_in的张数C进行平分,如果分成3个块,每个块的feature_in张数,就是C/3,不能整除先补0,这个之前HLS文章中有提到。另外,对于kernel的channel方向来分块,其实就是对一个kernel的channel通道上的所有kernel数据,做一个平分,来匹配feature_in的分块策略。
解释完channel方向上的问题后,看下为啥要进行分块,其实,对比之前不分块+CPU消耗周期的理解,这里很快就可以发现,如果做了分块,比如做个2分块,那么一个块的feature_in或kernel个数,就是C/2,而2个块间的乘法可以做到并行,那么最后,就只需要4.5C+10个周期了。
具体的对比可以看下面这图。在这图里,可以明显的发现相乘的周期缩短了两倍,提升了速度。总之,分块的策略是改变数据存放的方式,由不同的存放方式来完成计算的加速,后续有机会研究下NumPy那边的内存形式,这部分可能在那边也会得到巩固。
2021年的5月16日,来补充一点。
关于输入特征和卷积核的channel上的理解,很多文章和视频里提到的与本文中稍有差别。为此,我觉得使用他们的理解,反而可以更符合大众,去看懂更多的东西,因此,不得不补充下。
关于输入特征的channel,就是图中C的大小,表示feature_in的张数,这个是相同的。但对于kernel的channel,指的是一个卷积核中,使用的相同的kernel的数目,我想,这是为了跟feature_in相对应的,也是比较好理解的。但不同的卷积核,就不能用channel来表示了,为此,提出了kernel的深度,这个深度就是表示不同卷积核的数目。