内存组织介绍
现代计算机中的内存往往存在一种组织结构(hierarchy)。在这种结构中,含有多种类 型的内存,每种内存分别具有不同的容量和延迟(latency,可以理解为处理器等待内存数据的时间)。一般来说,延迟低(速度高)的内存容量小,延迟高(速度低)的内存容量大。
CUDA 不同类型的内存
全局内存
- 其含义是核函数中的所有线程都能够访问其中的数 据,和C++中的“全局变量”不是一回事。我们已经用过这种内存,在数组相加的例子中, 指针 d_x、d_y 和 d_z 都是指向全局内存的。全局内存由于没有存放在GPU的芯片上,因此具有较高的延迟和较低的访问速度。
- 全局内存的主要作用是为核函数提供数据,并在主机与设备及设备与设备之间传递数 据。首先,我们用cudaMalloc 函数为全局内存变量分配设备内存。然后,可以直接在核函数中访问分配的内存,改变其中的数据值。
- 全局内存对整个网格的所有线程可见。也就是说,一个网格的所有线程都可以访问(读 或写)传入核函数的设备指针所指向的全局内存中的全部数据。
- 全局内存的生命周期(lifetime)不是由核函数决定的,而是由主机端决定的。在数组 相加的例子中,由指针 d_x、d_y 和 d_z 所指向的全局内存缓冲区的生命周期就是从主机端用 cudaMalloc 对它们分配内存开始,到主机端用 cudaFree 释放它们的内存结束。
静态全局内存
静态全局内存变量由以下方式在任何函数外部定义
__device__ T x; // 单个变量
__device__ T y[N]; // 固定长度的数组
其中,修饰符 device 说明该变量是设备中的变量,而不是主机中的变量;T 是变量的 类型;N是一个整型常数。
在核函数中,可直接对静态全局内存变量进行访问,并不需要将它们以参数的形式传给核 函数。不可在主机函数中直接访问静态全局内存变量,但可以用 cudaMemcpyToSymbol 函数和 cudaMemcpyFromSymbol 函数在静态全局内存与主机内存之间传输数据。
常量内存
常量内存(constant memory)是有常量缓存的全局内存,数量有限,一共仅有 64 KB。它的可见范围和生命周期与全局内存一样。不同的是,常量内存仅可读、不可写。由于有 缓存,常量内存的访问速度比全局内存高,但得到高访问速度的前提是一个线程束中的线程(一个线程块中相邻的 32 个线程)要读取相同的常量内存数据。
一个使用常量内存的方法是在核函数外面用 constant 定义变量,并用前面介绍 的CUDA运行时API函数cudaMemcpyToSymbol将数据从主机端复制到设备的常量内存后供核函数使用。
在数组相加的例子中,核函数的参数 const int N 就是在主机端定义的变量,并通过传值的方式传送给核函数中的线程 使用。在核函数中的代码段 if (n < N) 中,这个参数 N 就被每一个线程使用了。所以, 核函数中的每一个线程都知道该变量的值,而且对它的访问比对全局内存的访问要快。除给核函数传递单个的变量外,还可以传递结构体,同样也是使用常量内存。
纹理内存和表面内存
纹理内存(texture memory)和表面内存(surface memory)类似于常量内存,也是一 种具有缓存的全局内存,有相同的可见范围和生命周期,而且一般仅可读(表面内存也可写)。不同的是,纹理内存和表面内存容量更大,而且使用方式和常量内存也不一样。
寄存器
在核函数中定义的不加任何限定符的变量一般来说就存放于寄存器(register)中。核函数中定义的不加任何限定符的数组有可能存放于寄存器中,但也有可能存放于 局部内存中。另外,以前提到过的各种内建变量,如 gridDim、blockDim、blockIdx、 threadIdx 及 warpSize 都保存在特殊的寄存器中。在核函数中访问这些内建变量是很高效的。
constint n = blockDim.x * blockIdx.x + threadIdx.x;
这里的 n 就是一个寄存器变量。寄存器可读可写。上述语句的作用就是定义一个寄存器变 量 n 并将赋值号右边计算出来的值赋给它(写入)。在稍后的语句
z[n] = x[n] + y[n];
中,寄存器变量 n 的值被使用(读出)。寄存器变量仅仅被一个线程可见。也就是说,每一个线程都有一个变量 n 的副本。虽 然在核函数的代码中用了这同一个变量名,但是不同的线程中该寄存器变量的值是可以不 同的。
局部内存
局部内存和寄存器几乎一 样。核函数中定义的不加任何限定符的变量有可能在寄存器中,也有可能在局部内存中。寄存器中放不下的变量,以及索引值不能在编译时就确定的数组,都有可能放在局部内存中。
这种判断是由编译器自动做的。对于数组相加例子中的变量 n 来说,作者可以肯定它在寄 存器中,而不是局部内存中,因为核函数所用寄存器数量还远远没有达到上限。
共享内存
共享内存和寄存器类似,存在于芯片 上,具有仅次于寄存器的读写速度,数量也有限。表 6.2 列出了与几个计算能力对应的共享 内存数量指标。
不同于寄存器的是,共享内存对整个线程块可见,其生命周期也与整个线程块一致。也就是说,每个线程块拥有一个共享内存变量的副本。共享内存变量的值在不同的线程块中 可以不同。一个线程块中的所有线程都可以访问该线程块的共享内存变量副本,但是不能访问其他线程块的共享内存变量副本。
L1 、L2缓存
从费米架构开始,有了SM层次的L1缓存(一级缓存)和设备(一个设备有多个SM) 层次的 L2 缓存(二级缓存)。它们主要用来缓存全局内存和局部内存的访问,减少延迟。
流多处理器SM
一个 GPU 是由多个SM构成的。一个SM包含如下资源:
- 一定数量的寄存器。
- 一定数量的共享内存。
- 常量内存的缓存。
- 纹理和表面内存的缓存。
- L1 缓存。
- 两个(计算能力 6.0)或 4 个(其他计算能力)线程束调度器(warp scheduler),用于 在不同线程的上下文之间迅速地切换,以及为准备就绪的线程束发出执行指令。
执行核心,包括:
- 若干整型数运算的核心(INT32)。
- 若干单精度浮点数运算的核心(FP32)。
- 若干双精度浮点数运算的核心(FP64)。
- 若干单精度浮点数超越函数(transcendental functions)的特殊函数单元(Special Function Units,SFUs)。
- 若干混合精度的张量核心(tensor cores,由伏特架构引入,适用于机器学习中的低精度矩阵计算,本书不讨论)。
SM占有率
因为一个 SM 中的各种计算资源是有限的,那么有些情况下一个 SM 中驻留的线程数 目就有可能达不到理想的最大值。此时,我们说该SM的占有率小于 100%。获得 100%的 占有率并不是获得高性能的必要或充分条件,但一般来说,要尽量让 SM 的占有率不小于某个值,比如 25%,才有可能获得较高的性能。
用CUDA运行时 API 函数查询设备
#include "stdio.h"
#define CHECK(call) \
do \
{ \
const cudaError_t error_code = call; \
if (error_code != cudaSuccess) \
{ \
printf("CUDA Error:\n"); \
printf(" File: %s\n", __FILE__); \
printf(" Line: %d\n", __LINE__); \
printf(" Error code: %d\n", error_code); \
printf(" Error text: %s\n", \
cudaGetErrorString(error_code)); \
exit(1); \
} \
} while (0)
int main(int argc, char* argv[])
{
int device_id = 0;
if (argc > 1) device_id = atoi(argv[1]);
CHECK(cudaSetDevice(device_id));
cudaDeviceProp prop;
CHECK(cudaGetDeviceProperties(&prop, device_id));
printf("Device id: %d\n",
device_id);
printf("Device name: %s\n",
prop.name);
printf("Compute capability: %d.%d\n",
prop.major, prop.minor);
printf("Amount of global memory: %g GB\n",
prop.totalGlobalMem / (1024.0 * 1024 * 1024));
printf("Amount of constant memory: %g KB\n",
prop.totalConstMem / 1024.0);
printf("Maximum grid size: %d %d %d\n",
prop.maxGridSize[0],
prop.maxGridSize[1], prop.maxGridSize[2]);
printf("Maximum block size: %d %d %d\n",
prop.maxThreadsDim[0], prop.maxThreadsDim[1],
prop.maxThreadsDim[2]);
printf("Number of SMs: %d\n",
prop.multiProcessorCount);
printf("Maximum amount of shared memory per block: %g KB\n",
prop.sharedMemPerBlock / 1024.0);
printf("Maximum amount of shared memory per SM: %g KB\n",
prop.sharedMemPerMultiprocessor / 1024.0);
printf("Maximum number of registers per block: %d K\n",
prop.regsPerBlock / 1024);
printf("Maximum number of registers per SM: %d K\n",
prop.regsPerMultiprocessor / 1024);
printf("Maximum number of threads per block: %d\n",
prop.maxThreadsPerBlock);
printf("Maximum number of threads per SM: %d\n",
prop.maxThreadsPerMultiProcessor);
return0;
}