第三章 CUDA编程接口
CUDA C++ 为熟悉 C++ 编程语言的用户提供了一种可以轻松编写设备执行程序的简单途径。
它由 C++ 语言的最小扩展集和运行时库组成。
编程模型通过引入了核心语言扩展,使得程序员可将内核函数定义为 C++ 函数,并在每次函数调用时通过一些新语法来指定网格和块的维度。所有扩展的完整描述可以在 C++ 语言扩展中找到。包含这些扩展名的任何源文件都必须使用 nvcc
进行编译,如 使用NVCC编译 中所述。
CUDA Runtime 通过引入运行时,提供了一些可在主机上执行的 C 和 C++ 函 数,这些函数用于设备内存的分配和释放、主机内存和设备内存之间的数据传输、具有多个设备的系统管理等。运行时的完整描述可以在 CUDA 参考手册中找到。
运行时构建在更低级别的 C API(即 CUDA 驱动程序 API)之上,同时应用程序也可以访问 CUDA 驱动程序 API。驱动程序 API 通过暴露诸如 CUDA 上下文(类似于设备的"主机"进程)和 CUDA 模块(类似于设备的动态加载库)等较低级别的概念 (Concept) 来提供额外级别的控制。因为不需要这种额外级别的控制,大多数应用程序不使用驱动程序 API,并且在使用 CUDA 运行时的过程中,上下文和模块管理也会隐式进行,这也可以使得应用程序代码更 简洁。由于运行时可与驱动程序 API 相互操作,因此大多数需要驱动程序 API 功能的应用程序可以默认使用运行时 API,同时仅在需要时使用驱动程序 API。 Driver API 中介绍了驱动 API 并在参考手册中进行了全面描述。
3.1 利用 NVCC 编译
内核可以使用被称为 PTX
的 CUDA 指令集架构来编写,PTX
参考手册中对此进行了描述。 但是通常使用高级编程语言(如 C++)更有效。在这两种情况下,内核都必须通过 nvcc
编译成二进制代码才能在设备上执行。
nvcc
是一种编译器驱动程序,可简化 C++
或 PTX
代码的编译流程:它提供了简单且熟悉的命令行选项,并通过调用不同编译阶段的工具集来执行代码编译。 本节概述了 nvcc
工作流程和命令选项。 完整的描述可以在 nvcc
用户手册中找到。
3.1.1 编译流程
3.1.1.1 离线编译
使用 nvcc 编译的源文件可以包含主机代码(即在host
上执行的代码)和设备代码(即在device
上执行的代码)。 nvcc 的基本工 作流程包括将设备代码与主机代码分离,然后:
- 将设备代码编译成汇编形式(
PTX
代码)或二进制形式(cubin
对象) - 通过 CUDA 运行时中的函数调用来替换主机代码中的
<<<...>>>
语法,对主机代码进行修改(更具体的描述可以参照执行配置),来从PTX
代码或cubin
对象中加载和启动每个编译好的内核。
修改后的主机代码要么作为 C++ 代码输出,然后使用另一个工具编译,要么作为目标代码直接输出——通过让 nvcc 在最后编译阶段调用主机编译器对代码进行编译。
然后应用程序可以:
- 链接已编译的主机代码(这是最常见的情况),
- 或者忽略修改后的主机代码(如果有),使用 CUDA 驱动程序 API(请参阅驱动程序 API)来加载和执行
PTX
代码或cubin
对象。
3.1.1.2 即时编译
应用程序在运行时加载的任何 PTX
代码都由设备驱动程序进一步编译为二进制代码。这称为即时编译(just-in-time compilation
)。即时编译增加了应用程序加载时间,但它使得应用程序可以从每个新的设备驱动程序内置的新编译器中获得性能改进。同时它也是使得应用程序能够在那些编译时不存在的设备中运行的唯一方式,如应用程序兼容性中所述。
当设备驱动程序为某些应用程序即时编译一些 PTX
代码时,驱动程序会自动缓存生成的二进制代码副本,避免应用程序在后续函数调用中重复的编译。缓存(称为计算缓存)在设备驱动程序升级时自动失效,因此应用程序可以从新的设备驱动程序的内置即时编译器中获得改进收益。
环境变量可用于控制即时编译,如 CUDA 环境变量中所述
作为 nvcc
编译 CUDA C++ 设备代码的替代方法,NVRTC
可在运行时将 CUDA C++ 设备代码编译为 PTX
。 NVRTC
是 CUDA C++ 的运行时编译库;更多信息可以在 NVRTC
用户指南中找到。
3.1.2 Binary 兼容性
二进制代码是特定于体系结构的。 使用指定目标体系结构的编译器选项 -code
生成 cubin
对象:例如,使用 -code=sm_35
编译会为计算能力为 3.5 的设备生成二进制代码。 从一个次要修订版到下一个修订版都保证了二进制兼容性,但不能保证从一个次要修订版到前一个修订版或跨主要修订版。 换句话说,为计算能力为 X.y 生成的 cubin
对象只能在计算能力为X.z , z≥y 的设备上执行。
注意:仅桌面型产品 支持二进制兼容性。 Tegra 型产品不支持它。 此外,不支持桌面和 Tegra 之间的二进制兼容性。
3.1.3 PTX 兼容性
某些 PTX 指令仅在具有较高计算能力的设备上受支持。 例如,Warp Shuffle Functions 仅在计算能力 3.0 及以上的设备上支持。 -arch 编译器选项指定了将 C++ 编译为 PTX 代码时假定的计算能力。 因此,例如,包含 warp shuffle
的代码必须使用 -arch=compute_30(或更高版本)进行编译。
为某些特定计算能力生成的 PTX 代码始终可以编译为比当前设备计算能力更高或相等的设备的二进制代码。 请注意,从早期 PTX 版本编译的二进制文件可能无法使用某些硬件功能。 例如,将在计算能力 6.0 (Pascal) 环境中生成的 PTX 代码编译为计算能力 7.0 (Volta) 的二进制目标设备将不会使用 Tensor Core 指令,因为这些指令在 Pascal 上不可用。 因此,最终二进制文件的性能可能会比使用最新版本的 PTX 生成的二进制文件更差。
3.1.4 应用程序兼容性
要在具有特定计算能力的设备上执行代码,应用程序必须加载与此计算能力兼容的二进制或 PTX 代码,如二进制兼容性和 PTX 兼容性中所述。 特别是,为了能够在具有更高计算能力的未来架构上执行代码(尚无法生成二进制代码),应用程序必须加载 PTX 代码,并在这些设备中即时编译(参见即时编译)。
哪些 PTX
和二进制代码会嵌入到 CUDA C++ 应用程序中由 -arch
和 -code
编译器选项或 -gencode
编译器选项控制,详见 nvcc 用户手册。 例如:
nvcc x.cu
-gencode arch=compute_50,code=sm_50
-gencode arch=compute_60,code=sm_60
-gencode arch=compute_70,code=\"compute_70,sm_70\"
嵌入与计算能力 5.0 和 6.0(第一和第二-gencode
选项)兼容的二进制代码以及与计算能力 7.0(第三-gencode
选项)兼容的 PTX 和二进制代码。
生成主机代码会在运行时自动选择最合适的代码来加载和执行,在上面的示例中,这些代码将是:
- 面向计算能力 5.0 和 5.2 的设备生成的 5.0 二进制代码,
- 面向计算能力 6.0 和 6.1 的设备生成的 6.0 二进制代码,
- 面向计算能力 7.0 和 7.5 的设 备生成的 7.0 二进制代码,
- PTX 代码在运行时编译为具有计算能力 8.0 和 8.6 的设备的二进制代码。
例如,x.cu
可以有一个优化代码的方法,使用 warp shuffle 操作,这些操作仅在计算能力 3.0 及更高版本的设备中受支持。 __CUDA_ARCH__
宏可根据计算能力区分各种代码方案。 它仅为设备代码定义。 例如,当使用 -arch=compute_35
编译时,__CUDA_ARCH__
等于 350。
使用驱动 API 的应用程序必须编译代码以分离文件并在运行时显式加载和执行最合适的文件。
Volta 架构引入了独立线程调度(Independent Thread Scheduling
),它改变了在 GPU 上调度线程的方式。 对于依赖于以前架构中 SIMT 调度的特定行为的代码,独立线程调度可能会改变参与线程的集合,从而导致不正确的结果。 为了实现在独立线程调度中详述的纠正措施并同时帮助迁移,Volta 开发人员可以使用编译器选项组合 -arch=compute_60 -code=sm_70
加入 Pascal 的线程调度。
nvcc 用户手册列出了 -arch、-code
和 -gencode
编译器选项的各种简写。 例如,-arch=sm_70
是 -arch=compute_70 -code=compute_70,sm_70
的简写(与 -gencode arch=compute_70,code=\"compute_70,sm_70\"
相同)。
3.1.5 C++兼容性
编译器前端根据 C++ 语法规则处理 CUDA 源文件。 主机代码支持完整的 C++。 但是,设备代码仅完整支持 C++ 的一个子集,如 C++ 语言支持中所述。
3.1.6 64位支持
64 位版本的 nvcc
以 64 位模式编译设备代码(即指针是 64 位的)。 以 64 位模式编译的设备代码仅支持以 64 位模式编译的主机代码。
同样,32 位版本的 nvcc
以 32 位模式编译设备代码,而以 32 位模式编译的设备代码仅支持以 32 位模式编译的主机代码。
32 位版本的 nvcc
也可以使用 -m64
编译器选项以 64 位模式编译设备代码。
64 位版本的 nvcc
也可以使用 -m32
编译器选项以 32 位模式编译设备代码。
3.2 CUDA Runtime
运行时在 cudart
库中实现,该库链接到应用程序,可以通过 cudart.lib
或 libcudart.a
静态链接,也可以通过 cudart.dll
或 libcudart.so
动态链接。 通常动态链接 cudart.dll
或 cudart.so
的应用程序会将运行时库作为应用程序安装包的一部分。只有在两个组件链接到同一个 CUDA 运行时实例时,在它们之间进行 CUDA 运行时符号的地址传递才是安全的。
它的所有入口都以 cuda
为前缀。
如异构编程中所述,CUDA 编程模型假设系统由主机和设备组成,每个设备都有自己独立的内存。 设备内存概述了用于管理设备内存的运行时函数。
共享内存 说明了使用线程层次结构中引入的共享内存来最大化性能。
Page-Locked Host Memory 引入了 page-locked 主机内存,它需要将内核执行与主机和设备内存之间的数据传输重叠。
异步并发执行 描述了用于在系统的各个级别启用异步并发执行的概念和 API。
多设备系统 展示了编程模型如何扩展到具有多个设备连接到同一主机的系统中。
错误检查 描述了如何正确检查运行时生成的错误。
调用堆栈 提到了那些用于管理 CUDA C++ 调用堆栈的运行时函数。
Texture and Surface Memory 描述了纹理和表面内存空间,它们提供了另一种访问设备内存的方式;它们还公开了 GPU 纹理硬件的一个子集。
图形互操作性 介绍了运行时提供的各种功能,用于与两个主要图形 API(OpenGL 和 Direct3D)进行互操作。
3.2.1 初始化
运行时没有显式的初始化函数;它在第一次调用运行时函数(更具体地说,这包括参考手册中除了错误处理和版本管理部分的函数之外的任何函数)时进行初始化。因此,在对运行时的函数进行计时,以及将错误代码从第一次函数调用解释到运行时的时候,需要牢记这一点。
运行时为系统中的每个设备创建一个 CUDA 上下文(有关 CUDA 上下文的更多详细信息,请参阅上下文)。此context
是设备的主要(primary
)上下文,并且在设备需要活动上下文时,通过第一个运行时函数进行初始化。它在应用程序的所有主机线程之间共享。作为创建上下文的一部分,设备代码在必要时会进行即时编译(请参阅即时编译)并加载到设备内存中。这一切都是透明地发生的。如果需要,例如根据驱动程序 API 和 CUDA 运行时的互操作性,可以从驱动程序 API 访问设备的主要上下文,如运行时和驱动程序 API 之间的互操作性中所述。
当主机线程调用 cudaDeviceReset()
时,主机线程会破坏当前设备的主要上下文(即设备选择中定义的当前设备)。当一个新的主机线程将此设备设置为当前设备时,它可以通过调用下一个运行时函数为该设备创建一个新的主要上下文。
注意:CUDA接口使用全局状态,在主机程序初始化时初始化,在主机程序终止时销毁。 CUDA 运行时和驱动程序无法检测此状态是否有效,因此在程序启动或 main 后终止期间使用其中任意接口(隐式或显式)将导致未定义的行为。
3.2.2 设备存储
如异构编程中所述,CUDA 编程模型假设系统由主机和设备组成,每个设备都有自己独立的内存。 内核在设备内存之外运行,因此运行时提供了分配、释放和复制设备内存以及在主机内存和设备内存之间传输数据的功能。
设备内存可以分配为线性内存
或 CUDA 数组
。
CUDA 数组是针对纹理获取进行优化了的不透明内存布局。 它们在纹理和表面内存中有所描述。
线性内存分配在一个统一的地址空间中,这意味着单独分配的内存实体可以通过指针相互 引用,例如在二叉树或链表中。 地址空间的大小取决于主机系统 (CPU) 和所用 GPU 的计算能力:
Table 1. Linear Memory Address Space
x86_64 (AMD64) | POWER (ppc64le) | ARM64 | |
---|---|---|---|
up to compute capability 5.3 (Maxwell) | 40bit | 40bit | 40bit |
compute capability 6.0 (Pascal) or newer | up to 47bit | up to 49bit | up to 48bit |
注意:在计算能力为 5.3 (Maxwell) 及更早版本的设备上,CUDA 驱动程序会创建一个未提交的 40 位虚拟地址预留,以确保内存分配(指针)在支持的范围内。 此预留显示为预留虚拟内存,但在程序实际分配内存之前不会占用任何物理内存。
线性内存通常使用 cudaMalloc()
分配并使用 cudaFree()
释放,主机内存和设备内存之间的数据传输通常使用 cudaMemcpy()
完成。 在Kernels的向量加法代码示例中,需要将向量从主机内存复制到设备内存:
// Device code
__global__ void VecAdd(float* A, float* B, float* C, int N)
{
int i = blockDim.x * blockIdx.x + threadIdx.x;
if (i < N)
C[i] = A[i] + B[i];
}
// Host code
int main()
{
int N = ...;
size_t size = N * sizeof(float);
// Allocate input vectors h_A and h_B in host memory
float* h_A = (float*)malloc(size);
float* h_B = (float*)malloc(size);
float* h_C = (float*)malloc(size);
// Initialize input vectors
...
// Allocate vectors in device memory
float* d_A;
cudaMalloc(&d_A, size);
float* d_B;
cudaMalloc(&d_B, size);
float* d_C;
cudaMalloc(&d_C, size);
// Copy vectors from host memory to device memory
cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice);
// Invoke kernel
int threadsPerBlock = 256;
int blocksPerGrid =
(N + threadsPerBlock - 1) / threadsPerBlock;
VecAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);
// Copy result from device memory to host memory
// h_C contains the result in host memory
cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost);
// Free device memory
cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);
// Free host memory
...
}
线性内存也可以通过 cudaMallocPitch()
和 cudaMalloc3D()
分配。 建议将这些函数用于 2D 或 3D 数组的分配,因为它确保分配的内存会被适当地填充以满足设备内存访问中描述的对齐要求,从而确保在访问行地址或在 2D 数组和其他区域设备内存之间执行复制时获得最佳性能(使用 cudaMemcpy2D() 和 cudaMemcpy3D() 函数)。 返回的间距(或步幅)必须用于访问数组元素。 以下代码示例分配一个width x height
的 2D 浮点数组,并展示了如何在设备代码中循环遍历数组元素:
// Host code
int width = 64, height = 64;
float* devPtr;
size_t pitch;
cudaMallocPitch(&devPtr, &pitch,
width * sizeof(float), height);
MyKernel<<<100, 512>>>(devPtr, pitch, width, height);
// Device code
__global__ void MyKernel(float* devPtr,
size_t pitch, int width, int height)
{
for (int r = 0; r < height; ++r) {
float* row = (float*)((char*)devPtr + r * pitch);
for (int c = 0; c < width; ++c) {
float element = row[c];
}
}
}
以下代码示例分配了一个width x height x depth
的 3D 浮点数组,并展示了如何在设备代码中循环遍历数组元素:
// Host code
int width = 64, height = 64, depth = 64;
cudaExtent extent = make_cudaExtent(width * sizeof(float),
height, depth);
cudaPitchedPtr devPitchedPtr;
cudaMalloc3D(&devPitchedPtr, extent);
MyKernel<<<100, 512>>>(devPitchedPtr, width, height, depth);
// Device code
__global__ void MyKernel(cudaPitchedPtr devPitchedPtr,
int width, int height, int depth)
{
char* devPtr = devPitchedPtr.ptr;
size_t pitch = devPitchedPtr.pitch;
size_t slicePitch = pitch * height;
for (int z = 0; z < depth; ++z) {
char* slice = devPtr + z * slicePitch;
for (int y = 0; y < height; ++y) {
float* row = (float*)(slice + y * pitch);
for (int x = 0; x < width; ++x) {
float element = row[x];
}
}
}
}
注意:为避免分配过多内存从而影响系统层面的性能,请根据问题大小从用户请求中获得分配参数。 如果分配失败,您可以回退到其他较慢的内存类型(cudaMallocHost()
、cudaHostRegister()
等),或者返回一个错误,告诉用户需要多少内存被拒绝。 如果您的应用程序由于某种原因无法请求内存分配参数,我们建议对其使用 cudaMallocManaged()
。
参考手册列出了用于在使用 cudaMalloc()
分配的线性内存、使用 cudaMallocPitch()
或 cudaMalloc3D()
分配的线性内存、CUDA 数组以及为在全局或常量内存空间中声明的变量分配的内存之间复制内存的所有各种函数。
以下代码示例说明了通过运行时 API 访问全局变量的各种方法:
__constant__ float constData[256];
float data[256];
cudaMemcpyToSymbol(constData, data, sizeof(data));
cudaMemcpyFromSymbol(data, constData, sizeof(data));
__device__ float devData;
float value = 3.14f;
cudaMemcpyToSymbol(devData, &value, sizeof(float));
__device__ float* devPointer;
float* ptr;
cudaMalloc(&ptr, 256 * sizeof(float));
cudaMemcpyToSymbol(devPointer, &ptr, sizeof(ptr));
cudaGetSymbolAddress()
用于检索那些声明在全局内存空间中的变量内存地址。 分配内存的大小是通过 cudaGetSymbolSize()
获得的。
3.2.3 L2 级设备内存管理
当一个 CUDA 内核重复访问全局内存中的一个数据区域时,这种数据访问可以被认为是持久化的 (persisting
)。 另一方面,如果数据只被访问一次,那么这种数据访问可以被认为是流式的 (streaming
)。
从 CUDA 11.0 开始,计算能力 8.0 及以上的设备能够影响 L2 缓存中数据的持久性,进而可能对全局内存的访问提供更高带宽和更低延迟。
3.2.3.1 为持久访问预留L2缓存
可以留出一部分 L2 缓存用于对全局内存的持久化数据访问。 持久访问优先使用 L2 缓存的这个预留部分,而对全局内存的访问来说,无论正常访问或流式访问,它都只能使用那些未被持久访问使用的 L2 缓存。
在一定范围内,用于持久访问的 L2 缓存预留大小可以修改:
cudaGetDeviceProperties(&prop, device_id);
size_t size = min(int(prop.l2CacheSize * 0.75), prop.persistingL2CacheMaxSize);
cudaDeviceSetLimit(cudaLimitPersistingL2CacheSize, size); /* set-aside 3/4 of L2 cache for persisting accesses or the max allowed*/
在多实例 GPU (MIG) 模式下配置 GPU 时,L2 缓存预留功能被禁用。
使用多进程服务 (MPS) 时,cudaDeviceSetLimit
无法更改 L2 缓存预留大小。 而作为替代方法,只能在 MPS 服务器启动时通过环境变量 CUDA_DEVICE_DEFAULT_PERSISTING_L2_CACHE_PERCENTAGE_LIMIT
指定预留大小。
3.2.3.2 L2持久化访问策略
访问策略窗口指定了全局内存的连续区域,以及用于访问该区域的 L2 缓存中的持久性属性。
下面的代码示例展示了如何使用 CUDA stream 设置 L2 持久访问窗口。
cudaStreamAttrValue stream_attribute; // Stream level attributes data structure
stream_attribute.accessPolicyWindow.base_ptr = reinterpret_cast<void*>(ptr); // Global Memory data pointer
stream_attribute.accessPolicyWindow.num_bytes = num_bytes; // Number of bytes for persistence access.
// (Must be less than cudaDeviceProp::accessPolicyMaxWindowSize)
stream_attribute.accessPolicyWindow.hitRatio = 0.6; // Hint for cache hit ratio
stream_attribute.accessPolicyWindow.hitProp = cudaAccessPropertyPersisting; // Type of access property on cache hit
stream_attribute.accessPolicyWindow.missProp = cudaAccessPropertyStreaming; // Type of access property on cache miss.
//Set the attributes to a CUDA stream of type cudaStream_t
cudaStreamSetAttribute(stream, cudaStreamAttributeAccessPolicyWindow, &stream_attribute);
当内核随后在 CUDA stream 中执行时,在全局内存范围 [ptr..ptr+num_bytes)
内的内存访问比对其他全局内存位置的访问更有可能保留在 L2 缓存中。
也可以为 CUDA Graph Kernel Node 节点设置 L2 持久性,如下例所示:
cudaKernelNodeAttrValue node_attribute; // Kernel level attributes data structure
node_attribute.accessPolicyWindow.base_ptr = reinterpret_cast<void*>(ptr); // Global Memory data pointer
node_attribute.accessPolicyWindow.num_bytes = num_bytes; // Number of bytes for persistence access.
// (Must be less than cudaDeviceProp::accessPolicyMaxWindowSize)
node_attribute.accessPolicyWindow.hitRatio = 0.6; // Hint for cache hit ratio
node_attribute.accessPolicyWindow.hitProp = cudaAccessPropertyPersisting; // Type of access property on cache hit
node_attribute.accessPolicyWindow.missProp = cudaAccessPropertyStreaming; // Type of access property on cache miss.
//Set the attributes to a CUDA Graph Kernel node of type cudaGraphNode_t
cudaGraphKernelNodeSetAttribute(node, cudaKernelNodeAttributeAccessPolicyWindow, &node_attribute);
hitRatio
参数可用于指定接收 hitProp
属性的访问比 例。 在上面的两个示例中,全局内存区域 [ptr..ptr+num_bytes)
中 60% 的内存访问具有持久属性,40% 的内存访问具有流属性。 哪些特定的内存访问被归类为持久化访问(hitProp
)是随机的,概率大约为 hitRatio
; 概率分布取决于硬件架构和内存范围。
例如,如果 L2 预留缓存大小为 16KB,而 accessPolicyWindow
中的 num_bytes
为 32KB:
hitRatio
为 0.5 时,硬件将随机选择 32KB 窗口中的 16KB 指定为持久化并缓存在预留的 L2 缓存区域中。hitRatio
为 1.0 时,硬件将尝试在预留的 L2 缓存区域中缓存一整个 32KB 窗口。 由于预留区域小于窗口,缓存行将被换出,以保持将 32KB 数据中最近使用的 16KB 保留在 L2 缓存的预留部分。
因此,hitRatio
可用于避免缓存的抖动,并从整体上减少移入和移出 L2 高速缓存的数据量。
低于 1.0 的 hitRatio
值可用于手动控制并发的 CUDA 流中不同 accessPolicyWindows
可以缓存在 L2 中的数据量。 例如,让 L2 预留缓存大小为 16KB; 两个不同 CUDA 流中的并发内核,每个内核都有一个 16KB 的 accessPolicyWindow
,并且两者的 hitRatio
值都为 1.0:由于它们需要竞争共享的 L2 资源,因此可能会清除彼此的缓存。 但是,如果两个 accessPolicyWindows
的 hitRatio
值都为 0.5时,就不太可能会清除自己或彼此的持久缓存。
3.2.3.3 L2 访问属性
为不同的全局内存数据访问定义了三种类型的访问属性:
cudaAccessPropertyStreaming
:使用流属性产生的内存访问不太可能在 L2 缓存中持久存在,因为这些访问会优先被清除。cudaAccessPropertyPersisting
:使用持久属性产生的内存访问更有可能持久存在于 L2 缓存中,因为这些访问会优先保留在 L2 缓存的预留部分中。cudaAccessPropertyNormal
:此访问属性会强制将先前应用的持久访问属性重置为正常状态。从以前的 CUDA 内核中获得的带有持久化属性的内存访问可能会在使用后很长时间内都保留在L2缓存中。这种使用后的持久性减少了 L2 缓存量,这些 L2 缓存本来可被后续不使用持久化属性的内核所使用。使用cudaAccessPropertyNormal
属性重置访问属性窗口会移除先前访问所产生的持久化(优先保留)状态,使得先前访问就像没有访问属性一样。
3.2.3.4 L2 持久性示例
以下示例展示了如何为持久化访问预留 L2 缓存,通过 CUDA Stream 在 CUDA 内核中使用预留的 L2 缓存,然后重置 L2 缓存。
cudaStream_t stream;
cudaStreamCreate(&stream); // Create CUDA stream
cudaDeviceProp prop; // CUDA device properties variable
cudaGetDeviceProperties( &prop, device_id); // Query GPU properties
size_t size = min( int(prop.l2CacheSize * 0.75) , prop.persistingL2CacheMaxSize );
cudaDeviceSetLimit( cudaLimitPersistingL2CacheSize, size); // set-aside 3/4 of L2 cache for persisting accesses or the max allowed
size_t window_size = min(prop.accessPolicyMaxWindowSize, num_bytes); // Select minimum of user defined num_bytes and max window size.
cudaStreamAttrValue stream_attribute; // Stream level attributes data structure
stream_attribute.accessPolicyWindow.base_ptr = reinterpret_cast<void*>(data1); // Global Memory data pointer
stream_attribute.accessPolicyWindow.num_bytes = window_size; // Number of bytes for persistence access
stream_attribute.accessPolicyWindow.hitRatio = 0.6; // Hint for cache hit ratio
stream_attribute.accessPolicyWindow.hitProp = cudaAccessPropertyPersisting; // Persistence Property
stream_attribute.accessPolicyWindow.missProp = cudaAccessPropertyStreaming; // Type of access property on cache miss
cudaStreamSetAttribute(stream, cudaStreamAttributeAccessPolicyWindow, &stream_attribute); // Set the attributes to a CUDA Stream
for(int i = 0; i < 10; i++) {
cuda_kernelA<<<grid_size,block_size,0,stream>>>(data1); // This data1 is used by a kernel multiple times
} // [data1 + num_bytes) benefits from L2 persistence
cuda_kernelB<<<grid_size,block_size,0,stream>>>(data1); // A different kernel in the same stream can also benefit
// from the persistence of data1
stream_attribute.accessPolicyWindow.num_bytes = 0; // Setting the window size to 0 disable it
cudaStreamSetAttribute(stream, cudaStreamAttributeAccessPolicyWindow, &stream_attribute); // Overwrite the access policy attribute to a CUDA Stream
cudaCtxResetPersistingL2Cache(); // Remove any persistent lines in L2
cuda_kernelC<<<grid_size,block_size,0,stream>>>(data2); // data2 can now benefit from full L2 in normal mode
3.2.3.5 将 L2 Access 重置为 Normal
以前 CUDA 内核中的 L2 缓存在被使用后可能会长期保存在 L2 中。因此,L2 缓存重设为正常状态对于 Stream 或 Normal 内存访问很重要,以便它们可以用正常的优先级使用 L2 缓存。有三种方法可以将持久化访问重置为正常状态。
- 使用访问属性
cudaAccessPropertyNormal
重置之前的持久化内存区域。 - 通过调用
cudaCtxResetPersistingL2Cache()
将所有持久化 L2 缓存行重置为正常。 - 最终,未触及的缓存行会自动重置为正常。对自动复位的依赖性很强。