CUDA基础知识(GPU工作)
CUDA基础知识(GPU工作)
一、GPU和CPU对比
图1 GPU和CPU的区别
CPU指令时延较低,计算速度快,计算任务较少时,CPU有优势;GPU时延长,但是并行能力强,所以能做大规模的计算任务。
CPU算数逻辑单元(ALU)较少,控制单元较多,存储空间(Cache)较多。GPU利用大量ALU最大化计算能力和吞吐量,极小的芯片面积用于缓存和控制单元,如图2。
图2 CPU/GPU结构模拟图
GPU高时延、高性能的原因:拥有大量线程,以及强大的计算能力,如图2。
二、GPU实现高吞吐量的架构基础
(1) 流式多处理器
首先介绍“流式多处理器”(简称“SM”),每个SM同时由多个流式处理器、核心或者线程组成。每个SM都配备基于硬件的线程调度器,用来执行线程。每个SM配备几个功能单元或其他加速计算单元,如张量核心或光线追踪单元。
每个SM都拥有一定数量的“片上内存”,通常称为“共享内存”或“临时存储器”,之所以说是共享的,意思是被所有的核心共享。同样,SM上的控制单元资源也被所有的核心所共享。
图3 GPU架构
(2) 内存
SM的内存层次结构:
①寄存器:每个SM中都存在大量寄存器,核心之间共享,根据线程需求动态分配。执行过程中,每个线程被分配私有寄存器,其他线程无法读取或写入这些寄存器。
②常量缓存:用来缓存SM上执行的代码中的常量数据,GPU会把明确设置为常量的对象缓存并保存在常量缓存中。
③共享内存:小型、快速、低时延的片上可编程SRAM内存,供运行在SM上的线程块共享使用。设计思路是:如果多个线程需要处理相同的数据,只需要其中一个线程从全局内存中加载,其他线程将共享这一数据。合理使用共享内存可以减少从全局内存加载重复数据的操作,提高内核执行性能。
④L1缓存:缓存从L2缓存中频繁访问的数据,所有SM共享一个L2缓存,L2缓存用来缓存全局内存中,被频繁访问的数据,从而降低时延。(注:SM并不知道它从L1和L2哪个里面获取数据。)
⑤片外全局内存:容量大、带宽高的DRAM。与SM物理距离较远,所以时延较高。
图4 SM的内存层次结构
三、组件如何发挥作用(GPU如何执行kernel)
(1) 介绍CUDA
CUDA是NVIDIA提供的编程接口,用来编写运行在GPU上的程序,在CUDA中会以类似C/C++函数的形式,来表达想在GPU上运行的计算(这就是kernel)。
(2) 什么是kernel
kernel在并行中操作向量形式的数字,这些数字以函数参数的形式提供给kernel。
举个栗子🌰:一个执行向量加法的kernel,会接收两个向量作为输入,逐个元素相加,并将结果写入第三个向量。
图5 网格
(3) kernel的配置
在GPU上执行kernel,需要启用多个线程,这些线程总体上被称为一个网格(grid),一个网格有更多的结构,一个网格由一个或多个线程块(简称为:块)组成。线程块和线程的数量,取决于数据的大小和我们所需的并行度。
举个栗子🌰:在上面向量相加的栗子🌰中,如果要用256维的向量进行相加运算,可以配置包含256个线程的单个线程块,这样每个线程处理向量的一个元素。如果数据太大,GPU上没有足够的线程可用,需要每个线程能够处理多个数据点。
(4) 编写kernel的步骤
① 运行在CPU上的主机代码:这部分代码用来加载数据,为GPU分配内存。并使用配置的线程网格(Grid)启动kernel。图中主机代码用来将两个向量相加,如图6。
图6 CUDA/kernel主机代码
② 编写在GPU上执行的设备代码:举个栗子🌰,设备代码定义了实际的kernal函数,如图7。
图7 设备代码
当GPU内存中拥有全部的所需数据后,会将线程块分配给SM,同一个块内的所有线程将同时由同一个SM处理(因此,GPU需要在开始执行线程之前,在SM上为线程预留资源,实际操作中可以将多个线程块分配给同一个SM实现并行执行。)
由于SM数量有限,大型kernel可能包含大量线程块**(SM数量<线程块数量),所以,并非所有线程块都可以立即分配执行,GPU将维护一个等待分配和执行的线程块列表(待命列表),当有任何线程块执行完成时,GPU会从这个列表**中选择一个线程块执行。
填充🍖:上面讲到:“一个块(block)中的所有线程都会被分配到同一个SM上”。在此之后,线程会进一步划分为大小为32的组,称为warp。,并一起分配到一个称为“处理块”的核心集合上,进行执行。SM通过获取并向所有线程发出相同的指令,来同时执行warp中的所有线程,之后相关线程会在数据的不同部分同时执行SM发出的指令。如图8。
举个梨子🍐:在向量相加的栗子🌰中,一个warp中的所有线程可能都在执行相加的指令,虽然是同一个指令,但是这些线程会在向量的不同索引上进行操作。我们可以把线程比作上面的油画喷头,每个线程都是一个喷头。向量的不同索引就是油画的每个像素点。
图8 英伟达介绍GPU的名场面
由于多个线程同时执行相同的指令,这种warp的执行模型被称为“单指令多线程SIMT”,类似于CPU中的单指令多数据SIMD指令,如图9。
图9 老一代GPU工作模型
Volta及其之后的新一代GPU引入了一种替代指令调度的机制——“独立线程调度”。允许线程之间完全并发,不受warp限制。独立线程调度可以更好地利用执行资源,也可以作为线程之间的同步机制,如图10。
图10 新一代GPU机制
③ warp运行原理的探讨(kernel执行的最后一步):
即使SM内的所有处理块(核心组),都在处理warp,但是在任何给定时刻,只有其中的少数块在积极执行指令,因为SM中可用的执行单元数量是有限的。有些指令的执行时间较长,这导致warp需要等待指令结果,这种情况下,SM会将处于等待状态的warp休眠,去执行另一个不需要等待任何结果的warp,这使得GPU能够最大限度地利用所有可用计算资源,并提高吞吐量。
由于每个warp中的每个线程都有自己的一组寄存器,因此SM从执行一个warp切换到另一个warp时,没有额外的计算开销。这与CPU上进程之间的上下文切换方式不同。如果一个进程需要等待长时间运行的操作,CPU在此期间会在核心上调度执行另一个进程,然后在CPU中进行上下文切换的代价昂贵,这是因为CPU需要将寄存器状态保存到主内存中,并恢复另一个进程的状态。
当kernel所有线程都执行完毕后,最后一步是将结果复制回主机内存。
(5) 动态资源分区
我们通常会通过一个称为“占用率Occupancy”的指标,来衡量GPU资源的利用率,表示分配给SM的warp数量/SM能够支持的最大warp数量之间的比值,如图11。
图11
为了实现最大吞吐量,我们希望拥有100%的占用率,但实践中这并不容易实现,这是为什么呢?为什么🧠:**
SM拥有一组固定的执行资源,包括寄存器、共享内存、线程块和线程槽,这些资源根据需求和GPU的限制,在线程之间进行动态划分。在NVIDIA H100上,每个SM可以处理32个线程块、64个warp,即2048个线程,每个线程块拥有1024个线程,如果我们启动一个包含1024个线程的网格(grid),GPU将把2048个可用线程槽划分为2个线程块。
动态分区和固定分区相比,动态分区能够更为有效地利用GPU的计算资源,固定分区为每个线程块分配了固定数量的执行资源,这种当时并不总是有效的,某些情况下,固定分区可能会导致线程被分配多于其实际需求的资源,造成资源的浪费和吞吐量的降低。
举个李子🍑:假设使用32个线程的线程块,并且需要总共2048个线程,我们将需要64个同样的线程块。然而,每个SM一次只能处理32个线程块,因此,即使一个SM可以运行2048个线程,但这个SM一次只能运行1024个线程,占用率仅为50%