CUDA编程:矩阵乘运算从CPU到GPU


CUDA编程:矩阵乘运算从CPU到GPU

仅用于站内搜索,没有排版格式,具体信息请跳转上方微信公众号内链接

点击上方“小白学视觉”,选择加”星标”或“置顶”
重磅干货,第一时间送达
来自|知乎作者丨kaiyuan
链接丨https ://zhuanlan.zhihu.com/p/573271688
编辑丨极市平台
本文主要介绍用CUDA实现矩阵乘法运算(C=AxB)的几个基本方法,帮助大家理解矩阵在GPU上面的运算与CPU上的有何异同,通过实践上手CUDA的优化计算,相比基础方法,能提速10倍以上。
本文内容涉及到CUDA矩阵1D运算、2D运算、共享内存、CUBLAS的使用。
文中的全部code:
https ://github.com/CalvinXKY/BasicCUDA/tree/master/matrix_multiply
V100上的测试对比:
矩阵C=AxB的数学运算,是线性代数里面最基本的内容,计算的基本公式如下:
矩阵中每个元素为的第1行与的列进行元素对应相乘再求和。
若:A宽wA高:hA;B宽wB高:hB;C宽wC高:hC有:
通过计算机运算我们能够很容易的得到运算部分的代码,如下:
进一步,我们还需要了解矩阵的一维数据运算方式。矩阵的数据在内存中存储的格式是线性格式(行优先/列优先),如下所示,展示的是一种行优先的存储方式。可以通过索引计算来定位矩阵中的某个元素,比如第i行第j列的元素,在线性内存中的位置:i*w+j。w为矩阵的宽度。
运算的CPU实现代码如下所示:
上述代码采用三重循环实现了全部运算。最内层是计算每个Cij元素运算,再用两个for遍历获得了整个C矩阵的结果。显然,如果用单线程的CPU运算,该过程的计算时间是
其中hA、wA是矩阵A的高和宽,wB是矩阵B的宽度,deltaT表示每次运算消耗的时间。
由于过程只有一个CPU线程在串行计算,所以矩阵越大耗时越久。为了优化这个过程,我们采用GPU来计算,GPU有大量的线程,通过增加更多的线程来并行计算,降低运算时间。理论上当我们用N个线程来运算时,整个运算时间为:
多线程编发计算道理很简单,让多个线程分担一个线程的工作量。在NVIDIA的GPU中使用多线程不像CPU中并行一样直接,如C++添加“#pragmaompparallel“。GPU中运算涉及数据的转移(CPUGPU)、GPU工作流的创建等内容,但最核心的点是线程thread的运算过程。基本上,我们只需要明确两个问题:

CUDA代码里面的Thread是如何调用的?
如何让不同的Thread与需要计算的数据匹配?
CUDA对thread的调用其实由编译器完成的。用户在编写代码时主要关注如何定义GPU能运行的函数,其次是如何调用这个函数。定义GPU线程(Thread)可运行函数,实际上就是在函数前面加上一个’_global_‘的前缀:
函数的执行需要用一个特殊的语法”>>”在主机host上面执行上述函数,尖括号里面实际上是定义执行这个函数用多少线程threads
这里需要知道如果调用上述函数,那么每个Thread都会去执行functionExample这个函数。
Thread有多少?
thread总数量=grids的数量一个grid里面block数量一个block里面threads的数量。
CUDA里面用Grid和Block作为线程组织的组织单位,一个Grid可包含了N个Block,一个Block包含N个thread。
在C++代码中(主机运行代码中)调用CUDA的多线程函数,一般过程是标记函数、设置线程数、执行函数。这里放一个CUDAGUIDE里面的样例代码:
既然有这么多的Thread去计算相同块的数据,会不会算重复或者漏算?现在是已知条件是:
一批GPU的Threads
一批待运算数据
我们需要做的是让数据与Thread对应起来。这里就涉及到了thread的编号。
thread的一维索引的计算相对简单,一般:
计算示例如下,展示了获取第6个block里面的第5个thread的索引计算:
若对thread进行二维编号,那么每个thread的编号(索引)计算就需要多一个维度编号。在前面MatAdd示例中展示的就是二维的thread索引计算。
这样获得了这个thread的索引Idx,函数里面需要用户自行去确定索引与数据的对应关系。即,用户要根据Idx,自己分配thread与计算数据映射关系。
根据矩阵运算CPU的代码,我们得到GPU运算的代码如下所示(详细源代码参看:MatMulKernel1D):
https ://github.com/CalvinXKY/BasicCUDA/blob/master/matrix_multiply/matMul1DKernel.cu
相比CPU的code,主要的不同点:
for循环由三层变为了一层(不算while循环);
增加了一个thread的索引计算(thID);
每个thread完成1个(或多个)C矩阵中元素的计算;
while循环是为了在总threads数量不等于C矩阵元素总数量时,防止”数据计算不足”或者”访问越界”;
上述过程中我们已经实现了CUDA对矩阵的计算,为了进一步优化运算。需要使用一些加速手段,这里最常用的方式是使用共享内存。共享内存是一种片上内存,它的访问速度与L1相同。共享内存特点可参看GPU显存理解(https ://zhuanlan.zhihu.com/p/462191421)。关键特点:
一个Block内的thread都能访问;
c++中通过__shared__关键字定义;
对于一些访问频率高的数据,可以从全局内存转移到共享内存中,这样能够提升运算速度。在矩阵乘法中(C=AxB),要获得C矩阵的某一行(比如i行)数据,A矩阵中的i行数据需要与B矩阵的所有列数据都相乘一次。一般而言,数据都是在运算中从全局内存加载到寄存器中,那么A矩阵的i行数据在本次运算中需要加载B的列次(假设B有K列)。如果有共享内存,我们只需要将该数据从全局内存加载一次到共享内存,然后再反复使用。数据传输方式由:
(Globalmemory->L2->L1->register)Kfactor1
变为:
Globalmemory->sharedmemory+(sharedmemory->register)Kfactor2
下图展示K=3的例子:
所以每次运算,我们将A矩阵的i行放入到共享内存中,保证第i行数据不会反复从Global中加载,从而提升运算速度。函数代码片段如下:
源码:MatMulKernel1DWithShMem
https ://github.com/CalvinXKY/BasicCUDA/blob/master/matrix_multiply/matMul1DKernel.cu
需要注意的是,共享内存的大小是有限的,不同GPU的共享内存大小不一;其次,我们需要对共享内存里的值进行初始化,并且初始化后需要让block中的线程同步。关键内容如下:
采用了共享内存后,通过实测会发现,矩阵运算的时间不增反降。其实原因很简单,因为共享内存使用的成本高于其节约的时间。这样我们需要进一步优化,比如采用2Dblock并配合共享内存。
2Dblock相比1Dblock,最大的差异是thread的编号idx由1维度变为了2维。在矩阵的乘法中,我们可以将矩阵拆成子矩阵,让每个block对应计算一个子矩阵。如下图所示,我们计算C=AxB,如果只获得C中某个子矩阵Cs(假设Cs的大小为MM),只需要抽取A的M行数据,以及B的M列数据,进行运算。
Cs矩阵的具体运算可拆解为:Cs=As0xBs0+As1xBs2+…+AsmxBsm.如下图所示,我们用宽度为M的方块去分割数据。这样每个小矩阵的大小都是M
M。那么,为什么要进行分割运算,直接运算不是很简洁?实际上就是为了使用共享内存,减少数据的加载次数。上面运算中,例如As0xBs0运算由于As0与Bs0矩阵可以足够小,都能加载到共享内存中,每个数据可减少M-1次全局内存读写。
一般而言MM设置的大小与CUDA中2DBlock的大小一致,这样能够简化运算:
优化的代码关键如下:
源码:MatMulKernel2DBlockMultiplesSize
https ://github.com/CalvinXKY/BasicCUDA/blob/master/matrix_multiply/matMul1DKernel.cu
在上述2D运算中,我们忽略一个问题,就是运算矩阵的长宽有可能不能够被Block整除,如下所示:
示例1:矩阵宽度经过M整除后,最后一个行块的宽度小于M;
示例2:矩阵的高度经过M整除后,最后一个列块的高度小于M;
这样我们需要增加一些循环+条件判断来处理最后一个行块/最后一个列块的运算问题。
源码:MatMulKernel2DBlockMultiplesSize
https ://github.com/CalvinXKY/BasicCUDA/blob/master/matrix_multiply/matMul1DKernel.cu
常用的矩阵运算,在CUDA的库CUBLAS中有现成的API函数。一般而言,它的运算方法比普通的优化运算要快,比如本例中的矩阵乘,可以调用cublasSgemm来运算。cublasSgemm调用非常方便。如下形式:
源码:matMulCublasKernel
https ://github.com/CalvinXKY/BasicCUDA/blob/master/matrix_multiply/matMulCublasKernel.cu
但是不要过分迷信CUBLAS,毕竟它是个通用库,考虑的是通用性。对于一些特殊场景手写kernel有可能超过CUBLAS的运算。
代码位置:
https ://github.com/CalvinXKY/BasicCUDA/tree/master/matrix_multiply
默认编译:
指定SM编译:比如A100机器,指定SMS=80
运行直接执行matMul,例如A(312,1000)
B(1000,11),指定“MatMul_2D_KERNEL_ANY_SIZE”函数运行:
algo是指定某个方法运算,如果不指定,即运行所有方法。可以用help查看:
运行效果(TestonA100):
参考:
https ://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html
下载1:OpenCV-Contrib扩展模块中文版教程
在「小白学视觉」公众号后台回复:扩展模块中文教程,即可下载全网第一份OpenCV扩展模块教程中文版,涵盖扩展模块安装、SFM算法、立体视觉、目标跟踪、生物视觉、超分辨率处理等二十多章内容。
下载2:Python视觉实战项目52讲
在「小白学视觉」公众号后台回复:Python视觉实战项目,即可下载包括图像分割、口罩检测、车道线检测、车辆计数、添加眼线、车牌识别、字符识别、情绪检测、文本内容提取、面部识别等31个视觉实战项目,助力快速学校计算机视觉。
下载3:前沿论文1000篇
在「小白学视觉」公众号后台回复:1000paper,即可下载最新的前沿顶会顶刊1000篇论文,涵盖:医学图像处理、目标检测、语义分割、扩散模型、大模型、自动驾驶、具身智能、超分辨率、图像去噪等多个领域。
交流群
欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器、自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN、算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三+上海交大+视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~


文章作者: ZejunCao
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ZejunCao !
  目录