基本编程模型
最基本的概念就是显存、kernel函数、线程块、stream:
- 开发者可以通过CUDA Runtime API,申请、释放显存,并在内存和显存间进行数据拷贝。
- 开发者可以编写专用于在GPU上执行的kernel函数,在主机侧通过CUDA C扩展调用kernel函数,调用将创建数以万计的GPU线程,每个GPU线程均会完整执行一次kernel函数,kernel函数内可以对显存进行读、写等各种操作。数以万计的GPU线程之间靠只读的内置变量(线程ID等)互相区分。
- 一次kernel调用对应的GPU线程,需划分为一个个尺寸相同的线程块。线程块是向GPU进行调度的最小单位,GPU同时支持多个线程块的执行,达到上限后,只有旧的线程块内的线程全部执行完成后,新的线程块才会被调度入GPU。
- stream相当于是GPU上的任务队列。每个kernel调用或大多数CUDA API都可以指定关联到某一个stream。同一个stream的任务是严格保证顺序的,上一个命令执行完成才会执行下一个命令。不同stream的命令不保证任何执行顺序。部分优化技巧需要用到多个stream才能实现。如在执行kernel的同时进行数据拷贝,需要一个stream执行kernel,另一个stream进行数据拷贝。
基本硬件架构及其在Kernel执行中的作用
显卡由内部的主板、显存颗粒、GPU芯片等组成。GPU内部的架构如下图:
GPU架构图
可以看到GPU由许许多多的SM(Stream Multiprocesser)组成。SM内部的架构如下图:
SM架构图
线程块就是被调度到SM上执行的,按照线程块占用的硬件资源不同,SM可以同时执行一个或多个线程块。SM内部的主要部件有:
- L1 Cache及Shared Memory。
- L1 Cache是高速缓存。
- Shared Memory是一块可以由开发者编程控制的高速缓存。开发者可以在kernel函数内通过编程手段写入或读取数据。其访问latency远远小于全局显存。
- SM分为4个子区域,称为SMSP。每个SMSP包括如下功能单元。
- Warp Scheduler+Dispatch
- Register File寄存器文件
- 16个INT32、16个FP32、2个Tensor Core及多个LD/ST等单元
线程块在SM内部执行时,又进一步细分为线程束(Warp)。截止目前所有的NVIDIA GPU中,线程束均由32个线程组成。线程束(Warp)的执行采用SIMT(单指令多数据)模型。SMSP就是真正负责线程束执行的硬件单元。按照NVIDIA的文档,2080 Ti每个SM上最多同时存在1024个线程,即32个warp。这些Warp平均分配到4个SMSP上,每个SMSP负责8个Warp的执行。与CPU进行线程切换时需要将当前线程寄存器内容储存到内存,再从内存中读取出目标线程的寄存器内容不同。SMSP在不同的Warp间切换执行的代价是非常低的,因为SMSP有足够的寄存器分给每一个正在执行的线程独占使用。如负责8个Warp合计256线程执行的SMSP,有16384个32位通用寄存器,平均每个线程在生命周期内独占至少64个寄存器。Warp有以下状态:
- Active:被调度到某个SMSP的warp。
- Eligible:就绪执行下一条指令的warp(即warp未stalled)。
- ...
在每个时钟周期,SMSP内的Warp Scheduler+Dispatch从其负责的8个warp中选中一个Eligible Warp,发射(issue)该warp的一条指令。当没有任何一个warp处于Eligible状态时,该周期被跳过。当某warp的一条指令被发射后,SMSP的对应INT32、FP32或其它单元就会进行对应的计算。当前实例架构中有16个FP32单元,所以一个warp的浮点运算ALU需要两个周期才能算完。故每个2个周期发射一条浮点指令即可使FP32单元保持满载。空余的那一个周期可以穿插发射一些访存、INT32等其它指令。SM的版本是由Compute Capacity表示的。绝大多数情况下,同一个系列的显卡有相同的Compute Capacity,代表其SM的架构是一样的,主要区别在SM数量、显存大小、工作主频等其他方面。
优化技巧
使用异步API
使用异步API如cudaMemcpyAsync可让GPU操作与CPU操作并行,CPU忙完后调用cudaStreamSynchronize,cudaEventWait等操作等待GPU任务完成。
优化内存与显存传输效率
- 使用Pinned(page-locked) Memory提高传输速度
- 通过在不同的Stream里同时分别执行kernel调用及数据传输,使数据传输与运算并行。(注意default stream的坑[1])
- 尽量将小的数据在GPU端合成大块数据后传输
- 有些情况下,即使数据不太适合使用kernel处理,但如果为了较低的算法latency,也可权衡传输代价后使用kernel处理数据
- 注意PCI-e插口的通道个数
优化Kernel访存效率
- 提高Global Memory访存效率
- 对Global Memory的访存需要注意合并访存(coalesced )。[2]
- warp的访存合并后,起始地址及访存大小对齐到32字节
- 尽量避免跨步访存
- 0及以上的设备可以通过编程控制L2的访存策略提高L2命中率。
- 提高Shared Memory的访存效率
- shared memory由32个bank组成
- 每个bank每时钟周期的带宽为4字节
- 连续的4字节单元映射到连续的bank。如0-3字节在bank0,4-7字节在bank1……字节128-131字节在bank0
- 若warp中不同的线程访问相同的bank,则会发生bank冲突(bank conflict),bank冲突时,warp的一条访存指令会被拆分为n条不冲突的访存请求,降低shared memory的有效带宽。所以需要尽量避免bank冲突。
- CUDA 11.0以上可以使用_async-copy_ feature[3]
优化线程级并行
在SMSP工作时,某些warp会由于访存依赖、寄存器依赖等原因stall。此时warp scheduler可以选中另一个eligible warp,执行其指令,以隐藏前一个warp的stall,使SMSP中的各个硬件资源尽量保持忙碌。但假如SMSP中所有的warp都不在eligible状态,则硬件只能空转等待某个warp从stall中恢复(如从global中请求的数据终于回来了)。Occupancy[4]指标用来衡量SM当前activate warp数量与理论上最多支持的activate warp数量的比值。Occupancy数量越高,代表SMSP负责的activate warp越多,当某个warp stall时,有更多的备选warp,有更大的概率可以找到一个eligible warp。极端情况Occupancy为1/8时,SM仅4个warp,每个SMSP 1个warp,当该warp stall时,smsp没有其它warp可以选择,硬件必然空转等待。影响Occupancy指标的包括以下因素:
- Thread Block 线程块的大小。
- 如线程块为128,不考虑其它情况下,调度8个线程块到SM即可保持满Occupancy。
- 如线程块为768,则若调度一个线程块到SM,Occupancy只有0.75,而调度两个线程块则不可能——已超出2080Ti 一个SM最多1024个线程的限制。
- 每个线程块的Shared Memory使用量
- 2080Ti每个线程块最多使用48Kb Shared Memory;每个SM只有64Kb Shared Memory。
- 若线程块尺寸为128,而线程块使用的Shared Memory有30Kb,则由于Shared Memory限制,最多只有两个线程块(256线程)被调度到SM,Occupancy只有0.25。Shared Memory使用60Kb。
- 若线程块尺寸为128, 每个线程块使用7Kb,则共可调度8个线程块到SM,使用Shared Memory 56Kb。
- 每个线程使用的Register(寄存器数量)
- 2080Ti每个SM有65536个32位寄存器,平均到最多1024个线程,则每个线程只能使用64个寄存器。
- 若某些算法比较复杂需要使用更多寄存器(如矩阵乘法中,需要加载更多数据到寄存器以提高计算访存比),如每线程需要使用128个寄存器,此时由于寄存器限制,SM上最多可以有512线程,此时Occupancy最多为0.5.
高的Occupancy不一定代表较高的性能,如某些算法确实需要每线程128寄存器时,保持0.5的Occupancy反而是最优选择。但过低的Occupancy会对性能带来较大的负面影响。
指令级优化
- 提高计算访存比
GPU执行计算时,需要LDS、LDG等指令先将数据读入寄存器,再进行计算,最后通过STS、STG等指令将数据保存下来。以矩阵乘法为例,先进行矩阵分块,最终拆解为每个线程计算MxK,KxN的两个小矩阵的乘法:若两小矩阵为M=2,N=2,K=1,即2x1;1x2,最后得到2x2的矩阵作为结果。则读入4个float需4条指令,计算指令也是4条,计算访存比4/4=1;若两小矩阵为M=8,N=8,K=1,即8x1;1x8,最后得到8x8的矩阵作为结果。则读入16个float,需读取指令16条,计算指令8x8=64条,计算访存比64/16=4;若使用向量读(float4)每条指令读入4个float,则读取指令仅4条,计算访存比64/4=16提高计算访存比,可以让GPU的更多时钟周期用于进行计算,相对的进行数据IO占用的时钟周期更少。
- 提高指令级并行
指令级并行基本原理:
- 现代不论是CPU还是GPU,指令的执行都是通过流水线进行的,流水线分为多个stage,即一条指令执行完成需要每个stage的工作都执行完成。而一个时钟周期并不是完成一条指令执行的所有时间,而是每一个stage完成当前工作的时间。流水线可以同时执行多条指令的不同阶段。
- 当后续指令的执行需要依赖前面指令的结果写回寄存器,我们说出现了寄存器依赖。此时后续指令需要等待第前面指令结果写回寄存器才能执行,若后续指令执行时前面指令结果尚未写回寄存器,流水线会失速(stall),此时warp scheduler开始切换到其它eligible warp,若无eligible warp,则SMSP将会空转。
- 若后续指令不依赖前面指令的结果,则即使前面指令未执行完毕,后续指令也可以开始执行。特别的,即使前序指令是一条耗时几百周期的LDG(全局内存读取)指令或耗时几十周期的LDS(共享内存读取)指令,只要后续一系列指令不依赖读取回来的数据,后续一系列指令可以正常执行而不必等待该LDG/LDS指令执写回寄存器。
通过以下方式,可以提高指令级并行,在线程级并行达不到较好效果的情况下,进一步提高程序性能:
- 数据预取(Prefetch):数据1已读取到寄存器,使用该数据1计算前,先将后续数据2的读取指令发射,再执行一系列数据1的处理指令;这样数据1的处理和数据2的读取在流水线上同时执行着。当数据1处理完成,需要处理数据2时,可以确保数据2已经存在于寄存器中,此时类似的将数据3的读取和数据2的处理同步执行起来。
- 指令重排:在存在寄存器依赖的指令间插入足够的其它指令,使得后续指令执行时,前面计算指令的结果已写回到寄存器。从CUDA C层面有意识地提供一些语句间的并行性,nvcc编译器可以一定程度上自动进行指令重排。若对nvcc重排结果不满意需要自己重排时,官方尚未开放SASS汇编器,目前只存在一些第三方SASS汇编器工具[5]。
- 提高Register的效率
- Register File也存在bank冲突,但在CUDA C层面上没有直接办法进行物理寄存器控制。
- 可以通过SASS汇编器,人工进行指令寄存器分配,以尽量消除register bank conflict。
- 可以通过SASS汇编器,为寄存器访问添加reuse标记,以尽量消除register bank conflict。
使用TensorCore进一步加速矩阵运算[6]
TensorCore可以用来快速进行D=A*B+C矩阵运算,提供load_matrix_sync, store_matrix_sync, mma_sync 等API。
使用CUDA生态的各库
NVIDIA已经提供了不少库,效率高性能好,合理使用可以大大提高开发效率,减少开发工作量。
- cuBLAS[7]
- TensorRT[8]
- cudnn[9]
- NVCodeC[10]
- DeepStream[11]
- nvJPEG[12]
- NCCL[13]
- CUTLASS [14]
参考
- ^Default Stream Implicit Synchronization https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#implicit-synchronization
- ^合并访存官方文档 https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html#coalesced-access-to-global-memory
- ^async-copy feature https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html#async-copy
- ^Occupancy https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html#occupancy
- ^CuAssembler https://github.com/cloudcores/CuAssembler
- ^Warp Matrix Functions https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#wmma
- ^cublas https://developer.nvidia.com/cublas
- ^tensorrt https://developer.nvidia.com/tensorrt
- ^cudnn https://developer.nvidia.com/cudnn
- ^nvidia-video-codec-sdk https://developer.nvidia.com/nvidia-video-codec-sdk
- ^deepstream-sdk https://developer.nvidia.com/deepstream-sdk
- ^nvjpeg https://developer.nvidia.com/nvjpeg
- ^nccl https://developer.nvidia.com/nccl
- ^cutlass https://github.com/NVIDIA/cutlass
内容中包含的图片若涉及版权问题,请及时与我们联系删除
评论
沙发等你来抢