cuda_learn_record

刷了一遍谭升的cuda教程,这里记录一些自己的心得体会和关键点。

理解cuda编程的核心概念

学过计算机系统组成都知道,软件的效率,和你对硬件的了解,针对硬件的优化息息相关,比如cpu的分支预测、缓存命中,内存、硬盘的读写效率,如何去挖掘和适应硬件,决定了你程序性能。所以针对某个硬件的编程,自然,从硬件本身的结构、特点入手,了解清楚了在去想很多问题就很简单了,软件层面很多的写法、妥协,都是因为硬件工程师是这么设计硬件的……

cpu和cuda的协作,cuda的硬件结构

cpu_cuda
为什么需要cuda进行并行计算就不解释了,这张图能看出什么?

数据传输

host也就是cpu主机端,和cuda也就是device端,有数据传输的过程,这里一般通过pcie总线进行传输,既然是数据传输,那么就会有数据传输速度的上限,这里通常用带宽来表示,一般是xxGB/s。

线程的组织结构

一个核函数只能有一个grid,一个grid可以有很多个块,每个块可以有很多的线程
grid和block的维度最大是三维。注意线程块、网格都是逻辑上的产物,实际上硬件SM的排列大概率和内存差不多。都是一维的。[详见]:(https://face2ai.com/CUDA-F-2-0-CUDA%E7%BC%96%E7%A8%8B%E6%A8%A1%E5%9E%8B%E6%A6%82%E8%BF%B01/)

wraps

线程束

线程块是个逻辑产物,因为在计算机里,内存总是一维线性存在的,所以执行起来也是一维的访问线程块中的线程,但是我们在写程序的时候却可以以二维三维的方式进行,原因是方便我们写程序,比如处理图像或者三维的数据,三维块就会变得很直接,很方便。
在块中,每个线程有唯一的编号(可能是个三维的编号),threadIdx。
网格中,每个线程块也有唯一的编号(可能是个三维的编号),blockIdx
那么每个线程就有在网格中的唯一编号。
当一个线程块中有128个线程的时候,其分配到SM上执行时,会分成4个块:

1
2
3
4
warp0: thread  0,........thread31
warp1: thread 32,........thread63
warp2: thread 64,........thread95
warp3: thread 96,........thread127

详见

线程束分化

线程束被执行的时候会被分配给相同的指令,处理各自私有的数据,也就是说一个线程束当时执行的指令都是一样的,处理的数据可以不一样。但这种执行指令的方式,遇到有分支的情况,比如当前给线程束的指令是执行if下边的语句,这个线程束内还存在需要执行else部分的,那么else这些线程就只能阻塞,直到分配了执行else的指令。条件分支越多,并行性削弱越严重,这就是线程束执行指令特点导致的。
thread_wrap

SM执行特点

每个SM上有多个block,一个block有多个线程(可以是几百个,但不会超过某个最大值),但是从机器的角度,在某时刻T,SM上只执行一个线程束,也就是32个线程在同时同步执行。
因为SM有限,虽然我们的编程模型层面看所有线程都是并行执行的,但是在微观上看,所有线程块也是分批次的在物理层面的机器上执行,线程块里不同的线程可能进度都不一样,但是同一个线程束内的线程拥有相同的进度。
并行就会引起竞争,多线程以未定义的顺序访问同一个数据,就导致了不可预测的行为,CUDA只提供了一种块内同步的方式,块之间没办法同步!

cuda_memory
上边的图是

cuda的内存组织方式

CUDA中每个线程都有自己的私有的本地内存;线程块有自己的共享内存,对线程块内所有线程可见;所有线程都能访问读取常量内存和纹理内存,但是不能写,因为他们是只读的;全局内存,常量内存和纹理内存空间有不同的用途。对于一个应用来说,全局内存,常量内存和纹理内存有相同的生命周期 详见

memory_access

线程访问内存的方式

核函数运行时需要从全局内存(DRAM)中读取数据不是想取多少是多少,而是一次性读特定粒度大小到缓存,然后在从缓存加载特定的数据。这块缓存机制和cpu的差不多。解释下“粒度”,可以理解为最小单位,也就是核函数运行时每次读内存,哪怕是读一个字节的变量,也要读128字节,或者32字节,而具体是到底是32还是128还是要看访问方式:对于CPU来说,一级缓存或者二级缓存是不能被编程的,但是CUDA是支持通过编译指令停用一级缓存的。如果启用一级缓存,那么每次从DRAM上加载数据的粒度是128字节,如果不适用一级缓存,只是用二级缓存,那么粒度是32字节。

共享内存

共享内存(shared memory,SMEM)是GPU的一个关键部分,物理层面,每个SM都有一个小的内存池,这个线程池被次SM上执行的线程块中的所有线程所共享。共享内存使同一个线程块中可以相互协同,便于片上的内存可以被最大化的利用,降低回到全局内存读取的延迟。SM上有共享内存,L1一级缓存,ReadOnly 只读缓存,Constant常量缓存。所有从Dram全局内存中过来的数据都要经过二级缓存,相比之下,更接近SM计算核心的SMEM,L1,ReadOnly,Constant拥有更快的读取速度,SMEM和L1相比于L2延迟低大概20~30倍,带宽大约是10倍。每个SM上有若干KB的片上内存,共享内存和L1共享这若干KB

内存存储体

共享内存是一个一维的地址空间,注意这句话的意思是,共享内存的地址是一维的,也就是和所有我们前面提到过的内存一样,都是线性的,二维三维更多维的地址都要转换成一维的来对应物理上的内存地址。
共享内存有个特殊的形式是,分为32个同样大小的内存模型,称为存储体,可以同时访问。32个存储体的目的是对应一个线程束中有32个线程,这些线程在访问共享内存的时候,如果都访问不同存储体(无冲突),那么一个事务就能够完成,否则(有冲突)需要多个内存事务了,这样带宽利用率降低。 一个存储体大小特定,访问存储体时一次访问多大看计算能力。
具体存储体冲突详见

总结

通过以上的这些概念,很容易总结出cuda的性能容易在哪儿出问题,如何优化。

  • 优化block和grid的形状,使得线程利用硬件资源最大
  • 由于线程束的执行特点,如何避免分支分化
  • 不同线程束、不同block、甚至grid直接如何同步?数据传输如何同步?
  • 可否使得单个线程尽可能完成更多的任务,减少资源占用,如循环展开
  • 如何利用全局内存加快数据访问
  • 如何保存内存高效率连续读写,而不是交叉访问,甚至于访问冲突?
  • 优化cpu和gpu之间的数据传输,使其充分利用带宽
正在加载今日诗词....