从GPU架构到CUDA

一 CPU 和 GPU 的基础结构

提到处理器结构,有2个指标是经常要考虑的:
延迟,是指从发出指令到最终返回结果中间经历的时间间隔。
吞吐量,就是单位之间内处理的指令的条数。

1.1 符号解释

DRAM:动态随机存取存储器(Dynamic Random Access Memory,DRAM)

CPU 的 L1、L2 和 L3 Cache:
在现代计算机架构中起着关键作用。它们的设计、大小、位置以及读写速度和延迟都有显著的差异。这些差异源自它们与 CPU 核心的距离、存储容量、硬件设计的复杂性和访问频率等因素。

L1 Cache:CPU 中的高速小型缓存
L1 Cache 是离 CPU 核心最近的缓存,也是访问速度最快的一层。它的设计通常非常紧凑,以确保处理器能够以极高的速度从中读取数据。
L1 Cache 通常分为两部分:指令缓存(Instruction Cache, I-Cache)和数据缓存(Data Cache, D-Cache)。
这种设计确保 CPU 核心能够快速访问指令和操作数据。
L1 Cache 的大小通常在 32KB 到 128KB 之间。
由于其靠近 CPU 核心,L1 Cache 的访问延迟非常低,通常只有几个时钟周期。这使得它在 CPU 的频繁操作中表现极为高效,尤其是在处理循环操作或重复指令时。它的读取和写入速度非常接近处理器的时钟速度,几乎不会造成瓶颈。
然而,由于大小限制,它只能存储当前处理任务中最常用的数据。
一个现实的例子可以用现代处理器的执行速度来说明。假设我们在运行一个数据处理程序,该程序需要频繁访问某些变量。L1 Cache 会保存这些高频访问的数据,因此每次访问不必耗费时间从内存中取出数据。由于 L1 Cache 的超高速度,这种访问几乎是瞬间完成的。

L2 Cache:中间层缓存,容量与速度的平衡
L2 Cache 通常比 L1 Cache 大,但速度稍慢。它的设计目标是为 L1 Cache 提供进一步的数据支持。当 L1 Cache 未命中时,L2 Cache 会被查询以寻找所需的数据。
L2 Cache 的大小通常在 256KB 到几 MB 之间,具体大小取决于处理器的架构。
L2 Cache 的访问速度虽然比 L1 Cache 慢,但比系统内存(RAM)快得多。它的延迟通常在 10 到 20 个时钟周期之间。
L2 Cache 是专门为提供更多存储容量而设计的,它在维持速度和容量之间寻找平衡。更大的容量允许更多数据驻留在 L2 Cache 中,从而减少 CPU 从系统内存读取数据的频率。
在现实中,我们可以想象这样一个场景:当 CPU 处理一个复杂的图像渲染任务时,涉及的数据量较大。此时,L1 Cache 容量不足以存储所有相关的数据,因此 L2 Cache 提供了一个次优的存储位置,允许 CPU 更快地访问这些数据,而不必回到远程的 RAM。

L3 Cache共享的、远离 CPU 核心的缓存
L3 Cache 是 CPU 内核共享的最后一层缓存。
相比 L1 和 L2 Cache,L3 Cache 更大,通常从几 MB 到几十 MB 不等。
L3 Cache 的设计主要用于减少核心之间的数据交换延迟和内存访问冲突,因此它通常被设计为多个核心共享的结构。
尽管 L3 Cache 的大小比 L1 和 L2 Cache 更大,但它的访问速度相对较慢,延迟可以达到数十到上百个时钟周期。
然而,它仍比访问主内存快得多,尤其是在多核处理器中,每个核心都可以通过 L3 Cache 更高效地共享数据。
一个典型的应用案例是现代服务器处理多个并行任务时。L3 Cache 可以缓存多个任务中的部分数据,使得 CPU 核心之间不必频繁从主内存中获取数据,从而提高整体性能。
例如,在云计算平台上,多个虚拟机或容器可能在同一 CPU 上运行,L3 Cache 的设计帮助这些并行任务高效地共享数据资源。

1.2 CPU示意图

在这里插入图片描述

从图中可以看出 CPU 的几个特点:
CPU 中包含了多级高速的缓存结构。因为我们知道处理运算的速度远高于访问存储的速度,那么奔着空间换时间的思想,设计了多级高速的缓存结构,将经常访问的内容放到低级缓存中,将不经常访问的内容放到高级缓存中,从而提升了指令访问存储的速度。
CPU 中包含了很多控制单元。具体有2种,一个是分支预测机制,另一个是流水线前传机制。
CPU 的运算单元 (Core) 强大,整型浮点型复杂运算速度快。

所以综合以上三点,CPU 在设计时的导向就是减少指令的时延,我们称之为延迟导向设计

1.3 GPU示意图

在这里插入图片描述

从图中可以看出 GPU 的几个特点 (注意紫色和黄色的区域分别是缓存单元和控制单元):
GPU 中虽有缓存结构但是数量少。因为要减少指令访问缓存的次数。
GPU 中控制单元非常简单。控制单元中也没有分支预测机制和数据转发机制。对于复杂的指令运算就会比较慢。
GPU 的运算单元 (Core) 非常多,采用长延时流水线以实现高吞吐量。每一行的运算单元的控制器只有一个,意味着每一行的运算单元使用的指令是相同的,不同的是它们的数据内容。那么这种整齐划一的运算方式使得 GPU 对于那些控制简单但运算高效的指令的效率显著增加。

所以,GPU 在设计过程中以一个原则为核心:增加简单指令的吞吐。因此,我们称 GPU 为吞吐导向设计

1.3 在什么情况下使用 CPU,什么情况下使用 GPU 呢?

CPU 在连续计算部分,延迟优先,CPU 比 GPU ,单条复杂指令延迟快10倍以上。
GPU 在并行计算部分,吞吐优先,GPU 比 CPU ,单位时间内执行指令数量10倍以上。

适合 GPU 的问题:
计算密集:数值计算的比例要远大于内存操作,因此内存访问的延时可以被计算掩盖。
数据并行:大任务可以拆解为执行相同指令的小任务,因此对复杂流程控制的需求较低。

二 CUDA 编程的重要概念

CUDA (Compute Unified Device Architecture),由英伟达公司2007年开始推出,初衷是为 GPU 增加一个易用的编程接口,让开发者无需学习复杂的着色语言或者图形处理原语。

OpenCL (Open Computing Languge) 是2008年发布的异构平台并行编程的开放标准,也是一个编程框架。OpenCL 相比 CUDA,支持的平台更多,除了 GPU 还支持 CPU、DSP、FPGA 等设备。

下面我们将以 CUDA 为例,介绍 GPU 编程的基本思想和基本操作。
首先我们将 CPU 以及系统的内存称为主机 (host),而将 GPU 及其内存称为设备 (device)。
在 GPU 设备上执行的函数通常称为核函数 (Kernel)。

一个 CUDA 程序,我们可以把它分成3个部分:

  • 从主机 (host) 端申请 device memory,把要拷贝的内容从 host memory 拷贝到申请的 device memory 里面。
  • 设备端的核函数对拷贝进来的东西进行计算,来得到和实现运算的结果,图4中的 Kernel 就是指在 GPU 上运行的函数。
  • 把结果从 device memory 拷贝到申请的 host memory 里面,并且释放设备端的显存和内存。

三 CUDA 编程中的内存模型

从硬件的角度来讲:
CUDA 内存模型的最基本的单位就是== SP (线程处理器)==。每个线程处理器 (SP) 都用自己的 registers (寄存器)local memory (局部内存)。寄存器和局部内存只能被自己访问,不同的线程处理器之间呢是彼此独立的。
由多个线程处理器 (SP) 和一块共享内存所构成的就是 SM (多核处理器) (灰色部分)。多核处理器里边的多个线程处理器是互相并行的,是不互相影响的。每个多核处理器 (SM) 内都有自己的 shared memory (共享内存),shared memory 可以被线程块内所有线程访问。
再往上,由这个 SM (多核处理器) 和一块全局内存,就构成了 GPU。一个 GPU 的所有 SM 共有一块 global memory (全局内存),不同线程块的线程都可使用。
在这里插入图片描述

从软件的角度来讲:
线程处理器 (SP) 对应线程 (thread)。
多核处理器 (SM) 对应线程块 (thread block)。
设备端 (device) 对应线程块组合体 (grid)。
在这里插入图片描述

3.1 线程块

所谓线程块内存模型是软件侧的一个最基本的执行单位,所以我们从这里开始梳理。线程块就是线程的组合体,它具有如下这些特点:
块内的线程通过共享内存、原子操作和屏障同步进行协作 (shared memory, atomic operations and barrier synchronization)
不同块中的线程不能协作。

如下图所示的线程块就是由256个线程组成的,它执行的任务就是一个最基本的向量相加的一个操作。在线程块内,这256个线程的计算是彼此互相独立的,并行的。下面的这个 [i],就是如何确定每个线程的索引 (在显存中的位置)。在计算完以后 (图中弯箭头的头部),会设置一个时钟,将这256个线程的计算结果进行同步。
在这里插入图片描述

3.2 网格 (grid)

网格 (grid),其实就是线程块的组合体
网格 (grid) 内的线程块是彼此互相独立,互不影响的。
全局内存可以由所有的线程块进行访问。
CUDA 核函数由线程网格 (数组) 执行。每个线程都有一个索引,用于计算内存地址和做出控制决策。在计算完以后 (图中所有弯箭头的头部),会设置一个时钟,将这N个线程块的计算结果进行同步。

在这里插入图片描述

3.3 线程块 id & 线程 id:定位独立线程的门牌号

核函数需要确定每个线程在显存中的位置,我们之前提到 CUDA 的核函数是要在设备端来进行计算和处理的,在执行核函数时需要访问到每个线程的 registers (寄存器) 和 local memory (局部内存)。在这个过程中需要确定每一个线程在显存上的位置。
所以我们需要像下图那样使用线程块的 index 和线程的 index 来确定线程在显存上的位置。
在这里插入图片描述

使用线程块的 index 和线程的 index 来确定线程在显存上的位置
如图所示,图中的线程块索引是2维的,每个网格都由2×2个线程块组成;线程索引是3维的,每个线程块都由2×4×2个线程组成,所以代码应该是:
在这里插入图片描述
M=N=2,P,Q,S=2,4,2。
每个线程x的那一维应该是线程块的索引×线程块的x维度大小+线程的索引。(设备端线程x的那一维的索引)。
每个线程y的那一维应该是线程块的索引×线程块的y维度大小+线程的索引。(设备端线程y的那一维的索引)。

3.4 线程束 (warp)

前面我们提到,如图所示的每一行由1个控制单元加上若干计算单元所组成,这些所有的计算单元执行的控制指令是一个。这其实就是个非常典型的 “单指令多数据流机制”。
在这里插入图片描述

单指令多数据流机制是说:执行的指令是一条,只不过不同的计算单元使用的数据是不一样的。而上面这一行,我们就称之为一个线程束 (warp)。

所以,SM 采用的 SIMT (Single-Instruction, Multiple-Thread,单指令多线程) 架构,warp (线程束) 是最基本的执行单元。
一个 warp 包含32个并行 thread,这些 thread 以不同数据资源执行相同的指令。
一个 warp 只包含一条指令,所以:warp 本质上是线程在 GPU 上运行的最小单元。
由于warp的大小为32,所以block所含的thread的大小一般要设置为32的倍数。

当一个 kernel 被执行时,grid 中的线程块被分配到 SM (多核处理器) 上,
一个线程块的 thread 只能在一个SM 上调度,SM 一般可以调度多个线程块,大量的 thread 可能被分到不同的 SM 上。
每个 thread 拥有它自己的程序计数器和状态寄存器,并且用该线程自己的数据执行指令,这就是所谓的 Single Instruction Multiple Thread (SIMT),
在这里插入图片描述

四 CUDA 编程中的内存模型

参考:CUDA 编程(一):CUDA C 编程及 GPU 基本知识

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值