CUDA C++ Programming Guide学习记录——Chapter 5

Chapter 5. Programming Model

本章通过概述 CUDA 编程模型背后的主要概念如何在 C++ 中公开来介绍它们。
Programming Interface中给出了 CUDA C++ 的详细描述。
本章和下一章中使用的向量加法示例的完整代码可以在 vectorAdd CUDA sample 中找到。

5.1. Kernels

CUDA C++ 通过允许程序员定义称为 kernel 的 C++ 函数来扩展 C++,这些函数在调用时由 N 个不同的 CUDA 线程并行执行 N 次,而不是像常规 C++ 函数那样只执行一次。

∕∕ Kernel definition
__global__ void VecAdd(float* A, float* B, float* C) 
{
	int i = threadIdx.x; 
	C[i] = A[i] + B[i]; 
}

int main() 
{ 
	...
	∕∕ Kernel invocation with N threads
	VecAdd<<<1, N>>>(A, B, C); 
	... 
}

5.2. Thread Hierarchy

为方便起见,threadIdx 是一个 3 分量向量,因此可以使用一维、二维或三维 thread index 来识别线程,形成一维、二维或三维线程块,称为 thread block。这提供了一种自然的方式来调用域中元素的计算,例如vector, matrix, 或 volume。

线程的索引及其 thread ID 以直接的方式相互关联:对于一维块,它们是相同的;对于大小为 (Dx, Dy) 的二维块,索引线程 (x, y) 的线程 ID 是 (x + y Dx);对于大小为 (Dx, Dy, Dz) 的 3 维块,索引线程 (x, y, z) 的线程 ID 是 (x + y Dx + z Dx Dy)。

例如,以下代码添加了两个大小为 NxN 的矩阵 A 和 B,并将结果存储到矩阵 C 中:

∕∕ Kernel definition
__global__ void MatAdd(float A[N][N], float B[N][N],float C[N][N]) 
{
	int i = threadIdx.x;
	int j = threadIdx.y; 
	C[i][j] = A[i][j] + B[i][j]; 
}
int main() 
{
	...
	∕∕ Kernel invocation with one block of N * N * 1 threads
	int numBlocks = 1; 
	dim3 threadsPerBlock(N, N); 
	MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C); 
	... 
}

每个块的线程数有一个限制,因为块的所有线程都期望驻留在同一个流多处理器核心上,并且必须共享该核心的有限内存资源。在当前的 GPU 上,线程块可能包含多达 1024 个线程。

但是,内核可以通过多个等形状的线程块执行,以便线程的总数等于 threadsPerBlock 乘以 numBlocks。

块被组织成一个一维、二维或三维线程块网格(grid),如图4所示。网格中线程块的数量通常由正在处理的数据的大小决定,这通常超过系统中的处理器数量。

在这里插入图片描述

<<<…>>> 语法中指定的每个块的线程数和每个网格的块数可以是 int 或 dim3 类型。

网格内的每个块都可以通过一维、二维或三维唯一索引来标识,该索引可通过内置的 blockIdx 变量在内核中访问。线程块的尺寸可以通过内置的 blockDim 变量在内核中访问。

扩展前面的 MatAdd() 示例以处理多个块,代码如下。

∕∕ Kernel definition
__global__ void MatAdd(float A[N][N], float B[N][N],float C[N][N])
{
	int i = blockIdx.x * blockDim.x + threadIdx.x;
	int j = blockIdx.y * blockDim.y + threadIdx.y;
	if (i < N && j < N) 
		C[i][j] = A[i][j] + B[i][j]; 
}
int main() 
{ 
	...
	∕∕ Kernel invocation
	dim3 threadsPerBlock(16, 16); 
	dim3 numBlocks(N ∕ threadsPerBlock.x, N ∕ threadsPerBlock.y); 
	MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C); 
	... 
}

线程块大小为16x16(256个线程),虽然在这种情况下是任意的,但这是一种常见的选择。网格是用足够的块创建的,每个矩阵元素像以前一样有一个线程。为简单起见,本例假设每个维度中每个网格的线程数可以被该维度中每个块的线程数整除,尽管事实并非如此。

线程块需要独立执行:必须能够以任何顺序(并行或串行)执行它们。这种独立性要求允许在任意数量的内核上以任意顺序调度线程块,从而使程序员能够编写随内核数量扩展的代码。

块内的线程可以通过某些 shared memory 共享数据并同步其执行来协调内存访问来进行协作。更准确地说,可以通过调用 __syncthreads() 内部函数来指定内核中的同步点; __syncthreads() 充当屏障,块中的所有线程都必须等待,然后才允许任何线程继续进行。Shared Memory给出了使用共享内存的示例。除了 __syncthreads() 之外,Cooperative Groups API 还提供了一组丰富的线程同步原语。

为了高效合作,共享内存应该是每个处理器核心附近的低延迟内存(很像 L1 缓存),并且 __syncthreads() 应该是轻量级的。

5.2.1. Thread Block Clusters

随着 NVIDIA 计算能力 9.0 的推出,CUDA 编程模型引入了一个可选的层次结构级别,称为由线程块组成的线程块集群。与如何保证线程块中的线程在流式多处理器(SM)上共同调度类似,集群中的线程块也保证在 GPU 中的 GPU Processing Cluster (GPC) 上共同调度。

与线程块类似,簇也被组织成一维、二维或三维,如图5所示。簇中线程块的数量可以由用户定义,一个簇中最多8个线程块。 CUDA 中支持可移植的簇大小。请注意,在 GPU 硬件或 MIG 配置上太小而无法支持 8 个多处理器时,最大集群大小将相应减小。这些较小配置以及支持超过 8 的线程块簇大小的较大配置的标识是特定于体系结构的,并且可以使用 cudaOccupancyMaxPotentialClusterSize API 进行查询。

在这里插入图片描述
可以使用编译器时内核属性使用 _cluster_dims_(X,Y,Z) 或使用 CUDA 内核启动 API cudaLaunchKernelEx 在内核中启用线程块集群。下面的示例展示了如何使用编译器时内核属性启动集群。使用内核属性的簇大小在编译时固定,然后可以使用经典的 <<< , >>> 启动内核。如果内核使用编译时簇大小,则在启动内核时无法修改簇大小。

∕∕ Kernel definition 
∕∕ Compile time cluster size 2 in X-dimension and 1 in Y and Z dimension
__global__ void __cluster_dims__(2, 1, 1) cluster_kernel(float *input, float* output) 
{ 

}

int main() 
{
	float *input, *output;
	∕∕ Kernel invocation with compile time cluster size
	dim3 threadsPerBlock(16, 16); 
	dim3 numBlocks(N ∕ threadsPerBlock.x, N ∕ threadsPerBlock.y);
	
	∕∕ The grid dimension is not affected by cluster launch, and is still enumerated 
	∕∕ using number of blocks. 
	∕∕ The grid dimension must be a multiple of cluster size.
	cluster_kernel<<<numBlocks, threadsPerBlock>>>(input, output); 
}

线程块集群大小也可以在运行时设置,内核可以使用CUDA内核启动API cudaLaunchKernelEx启动。下面的代码示例展示了如何使用可扩展的 API 发起集群内核。

∕∕ Kernel definition 
∕∕ No compile time attribute attached to the kernel
__global__ void cluster_kernel(float *input, float* output) 
{ 

}
int main() 
{
	float *input, *output; 
	dim3 threadsPerBlock(16, 16); 
	dim3 numBlocks(N ∕ threadsPerBlock.x, N ∕ threadsPerBlock.y);
	∕∕ Kernel invocation with runtime cluster size
	{ 
		cudaLaunchConfig_t config = {0};
		∕∕ The grid dimension is not affected by cluster launch, and is still enumerated 
		∕∕ using number of blocks. 
		∕∕ The grid dimension should be a multiple of cluster size.
		config.gridDim = numBlocks; 
		config.blockDim = threadsPerBlock; 
		
		cudaLaunchAttribute attribute[1]; 
		attribute[0].id = cudaLaunchAttributeClusterDimension; 
		attribute[0].val.clusterDim.x = 2; ∕∕ Cluster size in X-dimension
		attribute[0].val.clusterDim.y = 1; 
		attribute[0].val.clusterDim.z = 1; 
		config.attrs = attribute; 
		config.numAttrs = 1; 
		
		cudaLaunchKernelEx(&config, cluster_kernel, input, output); 
	} 
}

在计算能力为 9.0 的 GPU 中,集群中的所有线程块都保证在单个 GPU 处理集群 (GPC) 上共同调度,并允许集群中的线程块使用 Cluster Group API 集群执行硬件支持的同步.sync()。集群组还提供了成员函数,分别使用 num_threads() 和 num_blocks() API 在线程数或块数方面查询集群组大小。集群组中的线程或块的排名可以分别使用 dim_threads() 和 dim_blocks() API 查询。

属于集群的线程块可以访问分布式共享内存(Distributed Shared Memory)。集群中的线程块有能力读取、写入和执行原子到分布式共享内存中的任何地址。

5.3. Memory Hierarchy

CUDA线程在执行过程中可以访问来自多个内存空间的数据,如图6所示。每个线程都有私有的本地内存。每个线程块对块的所有线程都可见的共享内存,并且与块具有相同的生命周期。线程块集群中的线程块可以对彼此的共享内存执行读取、写入和原子操作。所有线程都可以访问相同的全局内存。

所有线程也可以访问两个额外的只读内存空间:常量和纹理内存空间。针对不同的内存使用(参见设备内存访问)优化了全局、常量和纹理内存空间。对于某些特定的数据格式(参见纹理和表面记忆),纹理记忆还提供了不同的寻址模式以及数据过滤。

全局、常量和纹理内存空间在同一应用程序启动的内核之间持续存在。
在这里插入图片描述

5.4. Heterogeneous Programming

如图 7 所示,CUDA 编程模型假设 CUDA 线程在物理独立的设备上执行,该设备上充当运行 C++ 程序的主机的协处理器。例如,这种情况是,当内核在 GPU 上执行并且 C++ 程序的其余部分在 CPU 上执行时。

CUDA编程模型还假设主机和设备在DRAM中维护自己的单独内存空间,分别称为 host memory 和 device memory 。因此,程序通过调用CUDA运行时(在编程接口中描述)来管理内核可见的全局、常量和纹理内存空间。这包括设备内存分配和托管以及主机和设备内存之间的数据传输。

统一内存提供托管内存来连接主机和设备内存空间。系统中的所有cpu和gpu都可以访问托管内存,作为具有公共地址空间的单个连贯内存映像。此功能支持设备内存的超额订阅,并且可以通过消除在主机和设备上显式镜像数据的需要,大大简化移植应用程序的任务。有关统一内存的介绍,请参阅统一内存编程。
在这里插入图片描述

5.5. Asynchronous SIMT Programming Model

在CUDA编程模型中,线程是执行计算或内存操作的最低抽象级别。从基于NVIDIA Ampere GPU架构的设备开始,CUDA编程模型通过异步编程模型为内存操作提供加速。异步编程模型定义了与CUDA线程相关的异步操作的行为。

异步编程模型定义了用于 CUDA 线程之间同步的 Asynchronous Barrier 的行为。该模型还解释并定义了如何使用 cuda::memcpy_async 在 GPU 中计算时从全局内存异步移动数据。

5.5.1. Asynchronous Operations

异步操作被定义为由CUDA线程启动并像由另一个线程异步执行的操作。在格式良好的程序中,一个或多个CUDA线程与异步操作同步。启动异步操作的CUDA线程不需要在同步线程中。

这样的异步线程始终与启动异步操作的 CUDA 线程相关联。异步操作使用同步对象来同步操作的完成。这样的同步对象可以由用户显式管理(例如,cuda::memcpy_async)或在库内隐​​式管理(例如,cooperative_groups::memcpy_async)。

同步对象可以是 cuda::barrier 或 cuda::pipeline。这些对象在使用 cuda::pipeline 的异步屏障和异步数据副本中详细解释。这些同步对象可以在不同的线程范围内使用。范围定义了可以使用同步对象来与异步操作同步的线程集。下表定义了 CUDA C++ 中可用的线程范围以及可以与每个线程同步的线程。

在这里插入图片描述
这些线程作用域是在CUDA标准c++库中作为标准c++的扩展实现的。

5.6. Compute Capability

设备的 compute capability 由版本号表示,有时也称为“SM版本”。此版本号标识GPU硬件支持的功能,并由应用程序在运行时使用,以确定当前GPU上可用的硬件功能和/或指令。

计算能力由主修订号X和次修订号Y组成,用X.Y表示。

具有相同主修订号的设备具有相同的核心架构。基于NVIDIA Hopper GPU架构的设备的主要修订号为9,基于theNVIDIA Ampere GPU架构的设备的主要修订号为8,基于Volta架构的设备的主要修订号为7,基于Pascal架构的设备的主要修订号为6,基于Maxwell架构的主要修订号为5,基于Kepler架构的主要修订号为3。

次要修订号对应于对核心体系结构的增量改进,可能包括新特性。

Turing是计算能力7.5设备的架构,是基于Volta架构的增量更新。

注意:特定 GPU 的计算能力版本不应与 CUDA 版本(例如 CUDA 7.5、CUDA 8、CUDA 9)混淆,后者是 CUDA 软件平台的版本。应用程序开发人员使用 CUDA 平台来创建在多代 GPU 架构上运行的应用程序,包括尚未发明的未来 GPU 架构。虽然 CUDA 平台的新版本通常通过支持新 GPU 架构的计算能力版本来添加对该架构的本机支持,但 CUDA 平台的新版本通常还包括独立于硬件生成的软件功能。

从 CUDA 7.0 和 CUDA 9.0 开始分别不再支持 Tesla 和 Fermi 架构。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值