CUDA开发总结笔记

点击上方“视学算法”,选择加"星标"或“置顶

重磅干货,第一时间送达a9e30f71a15f23326eeb698d0c27bb72.jpeg

作者丨周彬@知乎(已授权)

来源丨https://zhuanlan.zhihu.com/p/570795544

编辑丨极市平台

导读

 

本文不是一篇教程,而是笔者的一篇总结笔记,概括性地整理CUDA开发相关的经验和知识。

基本编程模型

最基本的概念就是显存、kernel函数、线程块、stream:

  1. 开发者可以通过CUDA Runtime API,申请、释放显存,并在内存和显存间进行数据拷贝。

  2. 开发者可以编写专用于在GPU上执行的kernel函数,在主机侧通过CUDA C扩展调用kernel函数,调用将创建数以万计的GPU线程,每个GPU线程均会完整执行一次kernel函数,kernel函数内可以对显存进行读、写等各种操作。数以万计的GPU线程之间靠只读的内置变量(线程ID等)互相区分。

  3. 一次kernel调用对应的GPU线程,需划分为一个个尺寸相同的线程块。线程块是向GPU进行调度的最小单位,GPU同时支持多个线程块的执行,达到上限后,只有旧的线程块内的线程全部执行完成后,新的线程块才会被调度入GPU。

  4. stream相当于是GPU上的任务队列。每个kernel调用或大多数CUDA API都可以指定关联到某一个stream。同一个stream的任务是严格保证顺序的,上一个命令执行完成才会执行下一个命令。不同stream的命令不保证任何执行顺序。部分优化技巧需要用到多个stream才能实现。如在执行kernel的同时进行数据拷贝,需要一个stream执行kernel,另一个stream进行数据拷贝。

基本硬件架构及其在Kernel执行中的作用

显卡由内部的主板、显存颗粒、GPU芯片等组成。GPU内部的架构如下图:

7c3dce8a07c846cdfe0962570b18d54f.jpeg

GPU架构图

可以看到GPU由许许多多的SM(Stream Multiprocesser)组成。SM内部的架构如下图:

478b6386dbc8320fd41f185c16d9d335.jpeg

SM架构图

线程块就是被调度到SM上执行的,按照线程块占用的硬件资源不同,SM可以同时执行一个或多个线程块。

SM内部的主要部件有:

  1. L1 Cache及Shared Memory。

  2. L1 Cache是高速缓存。

  3. Shared Memory是一块可以由开发者编程控制的高速缓存。开发者可以在kernel函数内通过编程手段写入或读取数据。其访问latency远远小于全局显存。

  4. SM分为4个子区域,称为SMSP。每个SMSP包括如下功能单元。

  5. Warp Scheduler+Dispatch

  6. Register File寄存器文件

  7. 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访存效率

  1. 对Global Memory的访存需要注意合并访存(coalesced )。[2]

  2. warp的访存合并后,起始地址及访存大小对齐到32字节

  3. 尽量避免跨步访存

  4. 8.0及以上的设备可以通过编程控制L2的访存策略提高L2命中率。

  • 提高Shared Memory的访存效率

  1. shared memory由32个bank组成

  2. 每个bank每时钟周期的带宽为4字节

  3. 连续的4字节单元映射到连续的bank。如0-3字节在bank0,4-7字节在bank1……字节128-131字节在bank0

  4. 若warp中不同的线程访问相同的bank,则会发生bank冲突(bank conflict),bank冲突时,warp的一条访存指令会被拆分为n条不冲突的访存请求,降低shared memory的有效带宽。所以需要尽量避免bank冲突。

  5. 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指标的包括以下因素:

  1. Thread Block 线程块的大小。

  2. 如线程块为128,不考虑其它情况下,调度8个线程块到SM即可保持满Occupancy。

  3. 如线程块为768,则若调度一个线程块到SM,Occupancy只有0.75,而调度两个线程块则不可能——已超出2080Ti 一个SM最多1024个线程的限制。

  4. 每个线程块的Shared Memory使用量

  5. 2080Ti每个线程块最多使用48Kb Shared Memory;每个SM只有64Kb Shared Memory。

  6. 若线程块尺寸为128,而线程块使用的Shared Memory有30Kb,则由于Shared Memory限制,最多只有两个线程块(256线程)被调度到SM,Occupancy只有0.25。Shared Memory使用60Kb。

  7. 若线程块尺寸为128, 每个线程块使用7Kb,则共可调度8个线程块到SM,使用Shared Memory 56Kb。

  8. 每个线程使用的Register(寄存器数量)

  9. 2080Ti每个SM有65536个32位寄存器,平均到最多1024个线程,则每个线程只能使用64个寄存器。

  10. 若某些算法比较复杂需要使用更多寄存器(如矩阵乘法中,需要加载更多数据到寄存器以提高计算访存比),如每线程需要使用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的效率

  1. Register File也存在bank冲突,但在CUDA C层面上没有直接办法进行物理寄存器控制。

  2. 可以通过SASS汇编器,人工进行指令寄存器分配,以尽量消除register bank conflict。

  3. 可以通过SASS汇编器,为寄存器访问添加reuse标记,以尽量消除register bank conflict。

使用TensorCore进一步加速矩阵运算[6]

TensorCore可以用来快速进行D=A*B+C矩阵运算,提供load_matrix_syncstore_matrix_syncmma_sync 等API。

使用CUDA生态的各库

NVIDIA已经提供了不少库,效率高性能好,合理使用可以大大提高开发效率,减少开发工作量。

  • cuBLAS[7]

  • TensorRT[8]

  • cudnn[9]

  • NVCodeC[10]

  • DeepStream[11]

  • nvJPEG[12]

  • NCCL[13]

  • CUTLASS [14]


参考

  1. ^Default Stream Implicit Synchronization https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#implicit-synchronization

  2. ^合并访存官方文档 https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html#coalesced-access-to-global-memory

  3. ^async-copy feature https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html#async-copy

  4. ^Occupancy https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html#occupancy

  5. ^CuAssembler https://github.com/cloudcores/CuAssembler

  6. ^Warp Matrix Functions https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#wmma

  7. ^cublas https://developer.nvidia.com/cublas

  8. ^tensorrt https://developer.nvidia.com/tensorrt

  9. ^cudnn https://developer.nvidia.com/cudnn

  10. ^nvidia-video-codec-sdk https://developer.nvidia.com/nvidia-video-codec-sdk

  11. ^deepstream-sdk https://developer.nvidia.com/deepstream-sdk

  12. ^nvjpeg https://developer.nvidia.com/nvjpeg

  13. ^nccl https://developer.nvidia.com/nccl

  14. ^cutlass https://github.com/NVIDIA/cutlass

c08f4fd20fe7153571ff1f433470c05c.png

outside_default.png

点个在看 paper不断!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值