解读CUDA C Programming Guide (2/5)

本系列为《解读CUDA C Programming Guide》.

本书旨在介绍进行CUDA并行优化的C编程指导。共5章,内容分别是:

  1. Introduction
  2. Programming Model
  3. Programming Interface
  4. Hardware Implementation
  5. Performance Guidelines

 

本文简单解读第二章:Programming Model.

本章主要内容包括:

  1. Kernels
  2. Thread Hierarchy
  3.  Memory Hierarchy
  4. Heterogeneous Programming
  5. Compute Capacity

 

本章通过概述CUDA编程模型在C中的公开方式,介绍了CUDA编程模型背后的主要概念。Programming Interface 中对CUDA C进行了广泛的描述。

本章和下一章中使用的向量加法示例的完整代码可以在 vectorAdd CUDA 示例中找到。

Kernels

CUDA C 通过允许程序员定义称为内核的C函数来扩展C,这些函数在被调用时由N个不同的CUDA线程并行执行N次,而不是像常规C函数那样仅执行一次。使用__global__声明说明符定义内核,并使用新的<<< ... >>> 执行配置语法指定为给定内核调用执行该内核的CUDA线程数。每个执行内核的线程都有一个唯一的线程ID,可通过内置的threadIdx变量在内核中访问该ID。作为说明,以下示例代码将两个大小为N的向量A和B相加,并将结果存储到向量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);
    ...
}

在这里,执行VecAdd()的N个线程中的每一个都执行二元的加法。

Thread Hierarchy

为了方便起见,threadIdx是3分量向量,因此可以使用一维,二维或三维线程索引来标识线程,

形成一维,二维或三维线程块,称为线程块。这提供了一种自然的方法来调用跨域中的元素(例如向量,矩阵或体积)的计算。

线程的索引及其线程ID以直接的方式相互关联:

  • 对于一维块,它们是相同的;
  • 对于大小为(Dx,Dy)的二维块,线程的线程ID 索引(x,y)是(x + y Dx);
  • 对于三维尺寸块(Dx,Dy,Dz),索引为(x,y,z)的线程,其线程ID为(x + y Dx + z Dx Dy)。

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

__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()
{
	...
	int numblocks = 1;
	dim3 threadsPerBlock(N, N);
	matAdd<<<numblocks, threadsPerBlock>>>(A, B, C)
	...
}

每个块的线程数(threadsPerBlock)是有限制的,因为块的所有线程都应驻留在同一处理器内核上,并且必须共享该内核的有限内存资源

在当前的GPU上,一个线程块最多可以包含1024个线程。

但是,内核可以由多个形状相同的线程块执行,因此线程总数等于每个块的线程数乘以块数(numBlocks)

块被组织成一维,二维或三维线程块的网格,如图6所示。网格中的线程块数通常由正在处理的数据大小或系统中的处理器数量决定,而数据量可能会大大超过该数量。

在<<< ... >>>语法中指定的每个块的线程数和每个网格的块数可以是intdim3类型。可以像上面的示例中那样指定二维块或网格。可以通过内置的blockIdx变量在内核中访问的一维,二维或三维索引来标识网格内的每个块。可通过内置的blockDim变量在内核中访问线程块的维.

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

__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 = blockIdy.y * blockDim.y + threadIdy.y;
	if(i < N && j < N)
	{
		C[i][j] = A[i][j] + B[i][j];
	}
}

int main()
{
	...
	dim3 threadsPerBlock(16,16);
	dim3 numBlocks(N / threadsPerBlock.x, N / threadsPerBlock.y);
	MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C);
	...
}

通常选择16x16(256个线程)的线程块大小,尽管在这种情况下是任意的。与以前一样,使用足够的块创建网格以使每个矩阵元素具有一个线程。为简单起见,此示例假定每个维度中每个网格的线程数可以被该维度中每个块的线程数平均整除,尽管并非必须如此。

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

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

为了进行有效的协作,共享内存应该是每个处理器核心附近的低延迟内存(非常类似于L1缓存),而__syncthreads()则应该是轻量级的。

CUDA线程在执行过程中可能会从多个内存空间访问数据,如图7所示。每个线程都有专用的本地内存。每个线程块具有对该块的所有线程可见的共享内存,并且具有与该块相同的生存期。所有线程都可以访问相同的全局内存

所有线程还可以访问两个附加的只读存储空间:常量存储空间纹理存储空间。全局,常量和纹理内存空间针对不同的内存使用进行了优化(请参阅设备内存访问)。纹理编程模型存储器还为某些特定的数据格式提供了不同的寻址模式以及数据过滤(请参见纹理和表面存储器)。

全局,常量和纹理存储空间在同一应用程序的内核启动之间是持久的.

Heterogeneous Programming

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

CUDA编程模型还假定主机和设备都在DRAM中维护自己的独立存储空间,分别称为主机存储器设备存储器。因此,程序通过调用CUDA运行时来管理内核可见的全局,常量和纹理存储空间(在编程接口中进行了介绍)。这包括设备内存的分配和释放以及主机与设备内存之间的数据传输

统一内存提供托管内存,以​​桥接主机和设备内存空间。系统中的所有CPU和GPU都可以将托管内存作为具有公共地址空间的单个一致内存映像进行访问。此功能可消除设备内存的超额订购,并且无需在主机和设备上显式镜像数据,从而可以大大简化移植应用程序的任务。有关统一内存的介绍,请参见统一内存编程。

Compute Capability

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

计算能力包括主要修订号X次要修订号Y,并由X.Y表示。

具有相同主要版本号的设备具有相同的核心体系结构。对于基于Volta架构的设备,主要修订版本号为7;对于基于Pascal架构的设备,主要版本号为6;对于基于Maxwell架构的设备,其主要版本号为3:针对开普勒架构的设备的版本号为3;针对基于Fermi架构的设备的版本号为2; 1为基于Tesla架构的设备。

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

Turing是计算能力为7.5的设备的体系结构,并且是基于Volta体系结构的增量更新。

启用CUDA的GPU列出了所有启用CUDA的设备及其计算能力。计算能力给出了每种计算能力的技术规格。

 

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

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

发布了239 篇原创文章 · 获赞 41 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/NXHYD/article/details/104819295