目录
1、CUDA程序Thread的基本结构
3、单指令多线程架构(SIMT, Single Instruction Multi Thread)
4、硬件多线程(Hardward Multithreading)
5、GPU的显存结构(Memory Hierarchy)
本文主要对GPU的硬件,以及根据硬件定量对参数进行设置,按照先了解硬件在进行参数设置的顺序分别进行描述。
在逻辑上,threads分为如下三个层次:
而在硬件上,threads仅有两个层次:
2、流处理器(SM)
GPU由多个multithreaded Streaming Multiprocessos(SM)构成。
以Tesla K40c显卡为例(Compute Capability 3.5)为例,其有15个SM。每个SM包括192个CUDA cores。
当要执行一个kernel grid时,该grid中的blocks会被分配给可用的SM。
如果一个block执行完毕,那么新的block会被放到空出来的SM上执行。
SM的设计目的时并发执行几百个线程,因此使用了SIMT, Single Instruction Multi Thread 的架构。
在SM内部有两种级别的并行:
在SM内部,threads以warp为单位被创建/管理/调度和执行,每个warp包括32个threads。当将一个或多个blocks分配给SM时,它会首先将其分成多个warp。(每个warp所包含的thread都是按threadID有序递增的),然后使用warp scheduler来调度执行每个warp。一个warp内的32个threads在同一时间执行同一条指令,所以当32个thread的执行路径完全一致时效率最高。如果有data-dependent的分支,那么warp会分别执行每一个分支路径,不在当前分支的threads会被停用。
每个warp的执行上下文execution context(program counters, registers, etc.)在整个生命周期里都是被保存在片上存储(on-chip memory)上的,但是片上存储大小是比较小的。片上存储可以类似于CPU/单片机的内部寄存器之类的硬件,因为片上内存有限,所以现在计算机中都是包含有片外存储的。因此从一个execution context切换到另一个execution context是无消耗的,在每个instruction issue time里,一个warp scheduler都会选择一个warp,该warp中的threads需要做好准备执行下个instruction,然后给向这个warp里的threads发出指令。
具体而言,每个Multiprocessor都有
这两个条件就决定了一个SM上能同时【并发的】存在多少个warps和blocks。(同时也有最大值限制)。如果一个block需要的registers/shared mems都无法满足,那么kernel就会失败。更细节一些,即在每个instruction issue time,一个warp scheduler都会选择一个准备好的warp发出指令。等待warp准备好的这段时间(number of clock cycles)就是【latency】。要达到完全的利用率,就需要所有的warp scheduler在latency这段时间的每个clock cycles都可以发出指令给其他warp,即掩盖掉latency。因此,一个SM内越多的warp通常就会带来越高的利用率,性能越高。
在了解GPU的内存层次之前,我们先了解下如下术语:
GPU的显存层次如下:
全局显存(Global Memory)
Global Memory就是编写CUDA程序时最常使用的显存,常用的cudaMemcpy函数就是通常从CPU拷贝到全局显存的函数。Global Mem能被所有thread访问,其在GPU的位置和Cache如下:
设备显存(device memory)
device memory并非位于SM内部,而是由所有SM共享,因此访问速度较慢,需要Cache缓存加速。除此之外,device memory必须通过32/64/128-byte的【memory transaction】访问,并且要求这些memory transaction是aligned to their size。
举例而言,即读取32-byte的memory transaction时,地址必须是32的倍数;读取64-byte的mem transaction时,地址必须是64的倍数
当一个warp执行指令(load/store)来访问Global mem时,它会根据【每个thread访问的word的大小】和【每个thread访问的地址关系】来把该访存指令聚合成一个或多个memory transaction。举例而言,如果每个thread访问4byte的word,则一个warp(32个thread)就需要访问32*4=128byte的内存。
针对于每个thread所读取的word,若word size是1/2/4/8/16 byte,且是Naturally Aligned,则会被编译成一个memory instruction。(后续同一个warp的memory instruction会进行聚合)如果不满足size和alignment的条件,那么当前thread的该次mem access就会被编译为多个mem instruction,因此变慢。
L1/L2 Cache
Global Memory的读取会被缓存到L2(有时也会缓存到const cache),通过可配置选项可以选择是否缓存到L1。
(因此,仅缓存在L2对分散的内存读取有好处,可以减少over-fetch)
即,L1的Cache Line Size = 128 byte,L2的Cache Line Size = 32 byte。所以当L1/L2共存时,取最大的Cache Line Size。
L2 Cache有如下特点:
(可以通过device property中的l2CacheSize
来查看其大小)
局部显存(Local memory)
每个thread都拥有自己私有的local memory,负责存储一些局部变量(automatic variable)。
对于局部变量而说,一些小型的局部变量会被放到register里,当register不够用时,则会被放到Local Mem中。
Local Mem的位置和Cache如下:
由于local mem也是放在device memory上,所以其和global mem很像。即access latency和bandwidth都和global mem一样低,一些内存对齐的约束也得满足。
有一点local mem独有的优化是:如果warp内的threads同时访问相同的local mem里的relative address(e.g. same index in an array variable, same member in a structure variable),memory access are fully coalesced。
共享显存(Shared Memory)
shared memory位于thread block这一层,即每个block共享一块shared mem,这块shared mem对该block内的所有threads可见,且当该block执行结束时,其所占用的shared mem也会被释放。
shared mem的位置和cache如下:
shared mem本身位于芯片上,所以读取速度很快,可以作为software-managed cache来加速的执行。L1/L2 cache上存储什么数据无法由程序来直接控制,但我们可以控制shared mem上存储什么数据。
在硬件上,shared memory 被分成32(对应warp中的thread个数)个相等大小的【bank 内存块】。
这32个内存块们可以同时被访问:
常显存(Constant Memory)
Constant Memory,顾名思义是用来存储只读数据的内存。此处的【只读】针对的是device code的只读,我们可以通过Host向Constant Memory写入数据(通过cudaMemcpyToSymbol()
的接口),然后在device code中读取。常见的Constant Memory大小为64KB,其位置和Cache如下:
寄存器(Register)
Register位于SM上,每个SM都有固定数目的一组threads。每个thread使用的register越少,就有越多的block/threads可以并发的位于同一SM上,进而提高性能。每个thread使用的register的数目由编译器【启发式】的决定。但我们也可以通过Launch Bounds提供一些信息协助编译器更好的决定。
参考文献:
CUDA2.2-原理之存储器访问 - 仙守 - 博客园
CUDA程序调优指南(一):GPU硬件 - 知乎