【GPU】GPU架构入门指南 | GPU内部工作原理 | 执行过程 | 如何运行kernel | GPU与CPU有什么不同 | CUDA | Warp
Summary
TLDR本期视频深入探讨了GPU的工作原理及其与CPU的区别,强调了GPU在大规模并行计算中的优势。通过比较CPU和GPU的设计目标和架构,解释了GPU如何实现高吞吐量。详细介绍了GPU的内存层次结构和执行kernel的过程,包括线程网格的配置和动态资源分区。最后,强调了编写高效GPU kernel的重要性和挑战,鼓励程序员优化代码以提高GPU资源的利用率。
Takeaways
- 🔍 CPU和GPU的主要区别在于设计目标,CPU专注于顺序执行,而GPU专为大规模并行处理设计。
- 🚀 GPU在深度学习等领域的广泛应用推动了其重要性的提升,特别是在执行大量线性代数和数值计算任务时。
- 📈 与CPU相比,GPU在数值计算性能上有显著优势,如NVIDIA的Ampere A100在32位精度下的吞吐量远超Intel的24核处理器。
- 🌟 GPU的核心组件包括流式多处理器(SM)、核心、线程、片上内存和基于硬件的线程调度器。
- 🔧 GPU内存层次结构包括寄存器、常量缓存、共享内存、L1和L2缓存,以及片外全局内存,每一层都有特定用途。
- 🛠️ CUDA是NVIDIA提供的编程接口,用于编写在GPU上运行的程序,其中的核心概念是kernel。
- 🔗 在GPU上执行kernel需要数据从主机内存复制到全局内存,以及合理配置线程网格以实现并行计算。
- 🔄 GPU通过warp执行模型实现单指令多线程(SIMT),并引入独立线程调度以提高资源利用率。
- ⚙️ 动态资源分区和占用率是衡量GPU资源利用效率的关键指标,合理的资源分配对于保持高占用率和降低时延至关重要。
- 📊 编写高效的GPU kernel需要仔细优化代码,平衡资源使用和性能,以达到最佳执行效率。
Q & A
CPU和GPU的主要设计目标有何不同?
-CPU的设计初衷是执行顺序指令,而GPU则专门为大规模并行和高吞吐量而设计。
为什么GPU在深度学习中变得重要?
-GPU在深度学习中变得重要是因为它们能够以极高的速度执行大量线性代数和数值计算,这正是深度学习所需要的。
GPU的架构是如何实现高吞吐量的?
-GPU通过使用大量流式多处理器(SM)、核心或线程、以及多层不同类型的内存来实现高吞吐量。
在执行数值计算时,CPU和GPU的性能如何比较?
-CPU在执行单个数值计算时比GPU更快,但在需要进行大量数值计算时,GPU由于其强大的并行处理能力,将比CPU更快地完成任务。
什么是CUDA?
-CUDA是NVIDIA提供的编程接口,用来编写运行在其GPU上的程序。
GPU中的kernel是如何定义和执行的?
-在CUDA中,kernel是一个类似于C/C++函数的形式,用来表达在GPU上运行的计算。它在并行中操作向量形式的数字,这些数字作为函数参数提供给它。
线程块和线程在GPU中的作用是什么?
-线程块和线程是GPU执行kernel的基本单位。线程块由一个或多个线程组成,它们共同执行一个kernel函数,并且可以共享数据和同步操作。
warp是什么,它在GPU执行中扮演什么角色?
-warp是GPU中一组同时执行相同指令的线程。这种执行模型称为单指令多线程(SIMT),它使得GPU能够有效地利用所有可用计算资源,提高吞吐量。
动态资源分区在GPU中如何实现?
-动态资源分区是根据线程的需求和GPU的限制,在线程之间动态划分执行资源,如寄存器、共享内存等。这种方式比固定分区更能有效地利用GPU的计算资源。
如何衡量GPU资源的利用率?
-通常使用一个称为'占用率'的指标来衡量GPU资源的利用率,它表示分配给SM的warp数量与SM所能支持的最大warp数量之间的比值。
为什么GPU的占用率可能无法达到100%?
-由于GPU的执行资源有限,如寄存器、共享内存等,以及各种约束条件,实际应用中可能无法始终达到100%的占用率。
Outlines
🖥️ CPU与GPU的比较
本段落主要介绍了CPU和GPU的基本区别和设计目标。CPU是为了执行顺序指令而设计,虽然现代CPU支持并行处理,但其主要关注点在于提高顺序执行的性能。而GPU则专注于大规模并行处理和高吞吐量,适用于需要大量线性代数和数值计算的应用场景。通过比较CPU和GPU在执行不同规模计算任务时的性能,突出了GPU在处理大规模数据时的优势。
🔍 GPU架构深入解析
这一部分详细探讨了GPU的内部架构,包括流式多处理器(SM)、核心、线程、片上内存和控制单元等组成部分。通过介绍NVIDIA H100 GPU的具体配置,说明了GPU如何通过其架构实现高吞吐量。同时,还讨论了GPU内存层次结构的不同层级,包括寄存器、常量缓存、共享内存、L1和L2缓存以及全局内存,以及它们在GPU计算中的作用。
💡 GPU Kernel执行原理
本段落深入解释了GPU是如何执行Kernel的。首先介绍了CUDA编程接口和Kernel的概念,然后详细描述了在GPU上执行Kernel所需的步骤,包括数据的准备、线程网格的配置、线程块和线程的分配以及warp的执行模型。此外,还提到了Volta及以后GPU引入的独立线程调度机制,以及warp执行中的资源利用和线程切换效率。
🔧 动态资源分区与优化
最后一部分讨论了GPU资源的动态分区和固定分区,以及如何通过优化代码来提高GPU的占用率和计算吞吐量。解释了由于资源限制导致的占用率不足问题,并强调了合理分配寄存器和共享内存等资源的重要性。通过举例说明了如何通过调整线程块大小和资源需求来优化GPU Kernel的性能。
Mindmap
Keywords
💡CPU
💡GPU
💡并行计算
💡指令时延
💡流式多处理器
💡寄存器
💡共享内存
💡缓存
💡kernel
💡warp
💡占用率
Highlights
CPU和GPU的设计目标不同,CPU专注于顺序执行,而GPU专为大规模并行和高吞吐量设计。
GPU在深度学习等领域的广泛应用使其变得极为重要,每位软件工程师都有必要了解其基本工作原理。
CPU通过指令流水线、乱序执行、预测执行和多级缓存等功能减少指令执行的时延。
GPU通过其大规模并行能力在执行大量计算时比CPU更快,尤其在深度学习中的表现。
GPU的架构由流式多处理器(SM)组成,每个SM包含多个核心和线程,以及片上内存。
GPU内存层次结构包括寄存器、常量缓存、共享内存、L1缓存、L2缓存和全局内存。
执行GPU上的kernel需要理解CUDA编程接口,以及如何配置线程网格和线程块。
GPU执行kernel时,数据必须先从主机内存复制到GPU的全局内存。
GPU中的warp是一组同时执行相同指令的线程,这种模型也称为单指令多线程(SIMT)。
Volta及以后的GPU引入了独立线程调度,允许线程之间完全并发,不受warp的限制。
GPU通过动态资源分区来优化资源利用,根据需求和限制在线程间动态划分执行资源。
占用率是衡量GPU资源利用率的指标,表示分配给SM的warp数量与SM所能支持的最大warp数量之间的比值。
合理优化代码对保持高占用率和降低时延至关重要,以实现GPU的最佳性能。
GPU的片外全局内存通常是高带宽内存(HBM)或动态随机存取内存(DRAM),与SM相距较远,时延较高。
每个SM上都有一小块可配置的共享内存,用于线程块内线程共享数据,提高性能。
GPU上执行kernel时,线程进一步划分为大小为32的组(warp),在不同数据部分执行相同指令。
程序员需要仔细优化代码,确保执行过程中达到最高的SM占用率,以充分利用GPU的计算资源。
Transcripts
大家好,这里是最佳拍档,我是大飞
大多数工程师对CPU和顺序编程都十分熟悉
这是因为自从他们开始编写基于CPU的代码以来
就与这方面密切接触
然而
对于GPU的内部工作原理及其独特的地方
他们则了解的相对较少
过去十年
由于GPU在深度学习中得到广泛应用而变得极为重要
因此
每位软件工程师都有必要了解其基本工作原理
今天我们就来介绍一些这方面的背景知识
首先,我们需要来比较一下CPU和GPU
这能帮助我们更好地了解GPU的发展状况
CPU和GPU的主要区别在于它们的设计目标
CPU的设计初衷是执行顺序指令,当然
得益于超线程技术和多核处理器
CPU现在也可以并行执行任务
但是长期以来
大量工作都是在致力于提高顺序执行的性能
为了提高顺序执行性能
CPU设计中引入了许多的功能
重点在于减少指令执行的时延
让CPU能够尽可能快地执行一系列指令
这些功能包括指令流水线、乱序执行、预测执行和多级缓存等等
而GPU则专门为大规模并行和高吞吐量而设计
但是这种设计导致了中到高程度的指令时延
这个设计方向
主要是受到GPU在视频游戏、图形处理、数值计算
以及现如今的深度学习中的广泛应用所影响
所有这些应用都需要以极高的速度执行大量线性代数和数值计算
因此人们倾注了大量精力来提升这些设备的吞吐量
我们来思考一个具体的例子
由于指令时延较低
CPU在执行两个数字相加的操作时比GPU更快
在按顺序执行多个这样的计算时
CPU能够比GPU更快地完成
然而
当需要进行数百万甚至数十亿次这样的计算时
由于GPU具有强大的大规模并行能力
它将比CPU更快地完成这些计算任务
我们可以通过具体数据来进行说明
硬件在数值计算方面的性能
是以每秒浮点运算次数FLOPS来衡量的
NVIDIA的Ampere A100在32位精度下的吞吐量为19.5 TFLOPS
相比之下,2021年
Intel的24核处理器在32位精度下的吞吐量仅为0.66 TFLOPS
同时,随着时间推移
GPU与CPU在吞吐量性能上的差距正在逐年扩大
我们可以通过一个视频来直观的看一下二者的差异
如果我们对比一下CPU和GPU的架构
可以发现
CPU在芯片领域中主要用于降低指令时延的功能
例如大型缓存、较少的算术逻辑单元ALU和更多的控制单元
与此相比
GPU则利用大量的ALU来最大化计算能力和吞吐量
只使用极小的芯片面积用于缓存和控制单元
这些元件主要用于减少CPU的时延
或许你会好奇
GPU如何能够容忍高时延并同时提供高性能呢?
这就要靠GPU所拥有大量线程和强大的计算能力了
即使单个指令具有高延迟
GPU也会有效地调度线程运行
以便它们在任意时间点都能利用计算能力
例如
当某些线程正在等待指令结果的时候
GPU将切换到运行其他非等待线程
这可确保 GPU 上的计算单元在所有时间点
都以其最大的容量运行
从而提供高吞吐量
稍后当我们讨论如何在 GPU 上运行kernel的时候
我们将对这一点有更清晰的了解
我们已经了解到GPU有利于实现高吞吐量
但是它们是通过怎样的架构来实现这一目标的呢
接下来,我们就来了解一下GPU的架构
GPU由一系列流式多处理器
英文名叫streaming multiprocessors
简称SM组成
其中每个SM又由多个流式处理器、核心或者线程组成
例如,NVIDIA H100 GPU具有132个SM
每个SM拥有64个核心
总计核心高达8448个
每个SM都拥有一定数量的片上内存
on-chip memory
通常称为共享内存或者临时存储器
这些共享内存被所有的核心所共享
同样
SM上的控制单元资源也被所有的核心所共享
此外
每个SM都配备了基于硬件的线程调度器
用来执行线程
除此之外
每个SM还配备了几个功能单元或其他加速计算单元
例如张量核心(tensor core)或者光线追踪单元(ray tracing unit)
用来满足GPU所处理的工作负载的特定计算需求
接下来
让我们继续深入剖析GPU的内存
并了解其中的细节
GPU具有多层不同类型的内存
每一层都有它的特定用途
如图所示是GPU中一个SM的内存层次结构
其中包括几个部分
首先是寄存器
GPU中的每个SM都拥有大量寄存器
例如,NVIDIA的A100和H100模型中
每个SM拥有65536个寄存器
这些寄存器在核心之间共享
并且根据线程需求动态分配
在执行过程中
每个线程都被分配了私有寄存器
其他线程无法读取或写入这些寄存器
接下来是芯片上的常量缓存
这些缓存用来缓存SM上执行的代码中使用的常量数据
为了利用这些缓存
程序员需要在代码中明确将对象声明为常量
以便GPU可以将它们缓存并保存在常量缓存中
每个SM还拥有一块共享内存或者临时内存
它是一种小型、快速且低时延的片上可编程SRAM内存
供运行在SM上的线程块共享使用
共享内存的设计思路是
如果多个线程需要处理相同的数据
只需要其中一个线程从全局内存中加载
而其他线程将共享这一数据
合理使用共享内存可以减少从全局内存加载重复数据的操作
并且提高内核执行性能
共享内存还可以用作线程块内的线程之间的同步机制
除此之外,每个SM还拥有一个L1缓存
它可以缓存从L2缓存中频繁访问的数据
所有SM都共享一个L2缓存
它用于缓存全局内存中被频繁访问的数据
从而可以降低时延
需要注意的是
L1和L2缓存对于SM来说是公开的
SM并不知道它是从L1还是L2中获取数据
SM从全局内存中获取数据
这类似于CPU中L1/L2/L3缓存的工作方式
GPU还拥有一个片外全局内存
它是一种容量大且带宽高的动态随机存取存储器(DRAM)
例如
NVIDIA H100拥有80 GB的高带宽内存(HBM)
带宽达每秒3000 GB
由于与SM相距较远
全局内存的时延相当高
然而
芯片上还有几个额外的存储层以及大量的计算单元
有助于掩饰这种时延
现在我们已经了解GPU硬件的关键组成部分
接下来我们深入一步
了解执行代码时这些组件是如何发挥作用的
要理解GPU是如何执行kernel的
我们首先需要了解什么是kernel及其配置
首先我们要说一下CUDA
CUDA是NVIDIA提供的编程接口
用来编写运行在其GPU上的程序
在CUDA中
你会以类似于C/C++函数的形式
来表达想要在GPU上运行的计算
这个函数被称为kernel
kernel在并行中操作向量形式的数字
这些数字以函数参数的形式提供给它
举一个简单的例子
比如一个执行向量加法的kernel
它会接受两个向量作为输入
逐个元素相加
并将结果写入第三个向量
要在GPU上执行kernel
我们需要启用多个线程
这些线程总体上被称为一个网格(grid)
但是网格还具有更多的结构
一个网格由一个或多个线程块
有时我们也简称为块组成
而每个线程块又由一个或多个线程组成
线程块和线程的数量
取决于数据的大小和我们所需的并行度
例如,在向量相加的示例中
如果我们要对256维的向量进行相加运算
那么可以配置一个包含256个线程的单个线程块
这样每个线程就可以处理向量的一个元素
如果数据更大
GPU上也许没有足够的线程可用
这时我们可能需要每个线程能够处理多个数据点
编写一个kernel需要两步
第一步是运行在CPU上的主机代码
这部分代码用来加载数据
为GPU分配内存
并使用配置的线程网格启动kernel
第二步是编写在GPU上执行的设备代码
比如这个CUDA kernel的主机代码
就是用来将两个向量相加
而这个设备代码
它定义了实际的kernel函数
由于我们这期视频的重点不是讲解如何使用CUDA
所以我们跳过这段代码
来看看在GPU上执行kernel的具体步骤
首先,在调度执行kernel之前
必须将其所需的全部数据
从主机(即CPU)内存复制到GPU的全局内存(即设备内存)
不过在最新的GPU硬件中
我们还可以使用统一虚拟内存
直接从主机内存中读取数据
接下来
当GPU的内存中拥有全部所需的数据后
它会将线程块分配给SM
同一个块内的所有线程将同时由同一个SM进行处理
为此,GPU必须在开始执行线程之前
在SM上为这些线程预留资源
在实际操作中
可以将多个线程块分配给同一个SM以实现并行执行
由于SM的数量有限
而大型kernel可能包含大量线程块
因此并非所有线程块都可以立即分配执行
GPU会维护一个待分配和执行的线程块列表
当有任何一个线程块执行完成时
GPU会从该列表中选择一个线程块执行
众所周知
一个块(block)中的所有线程都会被分配到同一个SM上
但是在此之后
线程还会进一步划分为大小为32的组
称为warp
并一起分配到一个称为处理块(processing block)的核心集合上
进行执行
SM通过获取并向所有线程发出相同的指令
来同时执行warp中的所有线程
然后这些线程将在数据的不同部分
同时执行该指令
在向量相加的示例中
一个warp中的所有线程可能都在执行相加指令
但是它们会在向量的不同索引上进行操作
由于多个线程同时执行相同的指令
这种warp的执行模型也称为单指令多线程
SIMT
这类似于CPU中的单指令多数据
SIMD指令
Volta及其之后的新一代GPU引入了一种替代指令调度的机制
称为独立线程调度(Independent Thread Scheduling)
它允许线程之间完全并发
不受warp的限制
独立线程调度可以更好地利用执行资源
也可以作为线程之间的同步机制
这方面的更多内容大家可以参考一下CUDA编程指南
关于warp的运行原理
有一些值得讨论的有趣之处
即使SM内的所有处理块(核心组)都在处理warp
但是在任何给定时刻
只有其中少数块正在积极执行指令
因为SM中可用的执行单元数量是有限的
有些指令的执行时间较长
这会导致warp需要等待指令结果
在这种情况下
SM会将处于等待状态的warp休眠
并执行另一个不需要等待任何结果的warp
这使得GPU能够最大限度地利用所有可用计算资源
并提高吞吐量
由于每个warp中的每个线程都有自己的一组寄存器
因此SM从执行一个warp切换到另一个warp时
没有额外的计算开销
与CPU上进程之间的上下文切换方式(context-switching)不同
如果一个进程需要等待一个长时间运行的操作
CPU在此期间
会在该核心上调度执行另一个进程
然而
在CPU中进行上下文切换的代价昂贵
这是因为CPU需要将寄存器状态保存到主内存中
并恢复另一个进程的状态
最后
当kernel的所有线程都执行完毕后
最后一步就是将结果复制回主机内存
好了
到这里我们已经介绍了有关典型kernel执行的全部内容
但是还有一点值得讨论
那就是动态资源分区
我们通常会通过一个称为“occupancy(占用率)”的指标
来衡量GPU资源的利用率
它表示分配给SM的warp数量
与SM所能支持的最大warp数量之间的比值
为了实现最大吞吐量
我们希望拥有100%的占用率
然而,在实践中
由于各种约束条件,这并不容易实现
为什么我们无法始终达到100%的占用率呢?
SM拥有一组固定的执行资源
包括寄存器、共享内存、线程块槽和线程槽
这些资源根据需求和GPU的限制
在线程之间进行动态划分
例如,在NVIDIA H100上
每个SM可以处理32个线程块、64个warp
即2048个线程
每个线程块拥有1024个线程
如果我们启动一个包含1024个线程的网格
GPU将把2048个可用线程槽划分为2个线程块
那么动态分区和固定分区相比
动态分区能够更为有效地利用GPU的计算资源
固定分区为每个线程块分配了固定数量的执行资源
这种方式并不总是最有效的
在某些情况下
固定分区可能会导致线程被分配多于其实际需求的资源
造成资源浪费和吞吐量降低
我们可以通过一个例子来说明
资源分配对SM占用率的影响
假设我们使用32个线程的线程块
并且需要总共2048个线程
那么我们将需要64个这样的线程块
然而每个SM一次只能处理32个线程块
因此呢
即使一个SM可以运行2,048个线程
但是他一次也只能同时运行1,024个线程
占用率仅仅为50%
同样每个SM具有65536个寄存器
要同时执行2,048个线程
每个线程最多有32个寄存器
因为65536除以2,048等于32
如果一个kernel需要每个线程有64个寄存器
那么每个SM只能运行1,024个线程
占用率同样为50%
如果占用率不足
可能就无法提供足够的时延容忍度
或者所需要的计算吞吐量
也就难以达到硬件的最佳性能
高效创建GPU的kernel是一项复杂的任务
我们必须合理的分配资源
在保持高占用率的同时尽量的降低时延
比如说拥有大量寄存器可以加快代码的运行速度
但是可能会降低占用率
因此呢谨慎的优化代码至关重要
以上呢就是对GPU架构和计算的一些入门介绍
我们再对要点进行一下总结方便大家回顾
首先呢GPU是由多个SM组成的
每个SM又包含多个处理芯片
GPU上存在着一个片外的全局内存
通常是高带宽内存HBM
或者是动态随机存取内存DRAM
它与芯片上的SM相距较远
因此时延较高
GPU中有两个级别的缓存
分别是片外L2缓存和片上L1缓存
L1和L2缓存的工作方式类似于CPU中的L1、L2缓存
每个SM上都有一小块可以配置的共享内存
这块共享内存在处理核心之间共享
通常情况下
线程块内的线程会将一段数据加载到共享内存中
并在需要的时候重复使用
而不是每次再从全局内存中加载
每个SM都有大量的寄存器
寄存器会根据线程需求进行划分
NVIDIA H100每个SM有65,536个寄存器
在GPU上执行kernel的时候
我们需要启动一个线程网格
网格是由一个或者多个线程块组成的
而每个线程块由一个或者多个线程组成
根据资源的可用性
GPU会分配一个或者多个线程块在SM上执行
同一个线程块中的所有线程
都会被分配到同一个SM上执行
这样做的目的呢是为了充分利用数据的局部性
并实现线程之间的同步
被分配给SM的线程进一步的分为大小为32的组
称为warp
一个warp内的所有线程同时执行相同的指令
但是在数据的不同部分上执行
GPU会根据每个线程的需求和SM的限制
在线程之间进行动态的资源划分
程序员们需要仔细的优化代码
来确保在执行过程中达到最高的SM占用率
好了以上就是本期的全部内容
感谢大家的观看
我们下期再见
Browse More Related Video
How to write more flexible game code
Simplifying Key Order Blocks in Forex Trading
Intro to Algorithms: Crash Course Computer Science #13
Psychology of Computing: Crash Course Computer Science #38
The Central Processing Unit (CPU): Crash Course Computer Science #7
Networking for GenAI Training and Inference Clusters | Jongsoo Park & Petr Lapukhov
5.0 / 5 (0 votes)