CUDA C++ 编程指南
CUDA 模型和接口的编程指南。
版本 12.8 中的更改
添加了 TMA Swizzle 部分
1. 介绍
1.1. 使用 GPU 的好处
图形处理单元 (GPU)1 在相似的价格和功耗范围内,比 CPU 提供更高的指令吞吐量和内存带宽。 许多应用程序利用这些更高的性能,在 GPU 上的运行速度比在 CPU 上更快(请参阅 GPU 应用程序)。 其他计算设备(如 FPGA)的能效也很高,但与 GPU 相比,编程灵活性要差得多。
GPU 和 CPU 之间功能上的这种差异是因为它们的设计目标不同。 CPU 的设计目标是擅长尽可能快地执行一系列操作(称为线程),并且可以并行执行几十个这样的线程,而 GPU 的设计目标是擅长并行执行数千个线程(通过降低单线程性能来获得更高的吞吐量)。
GPU 专用于高度并行的计算,因此在设计时,更多晶体管用于数据处理,而不是数据缓存和流控制。图 1 的示意图显示了 CPU 与 GPU 的芯片资源分配示例。

图 1 GPU 将更多晶体管用于数据处理
例如,对于浮点计算等高度并行计算,将更多晶体管用于数据处理是有益的; GPU 可以通过计算来隐藏内存访问延迟,而不是依赖大型数据缓存和复杂的流控制来避免长时间的内存访问延迟,这两者在晶体管方面都很昂贵。
通常,应用程序混合了并行部分和顺序部分,因此系统设计为混合使用 GPU 和 CPU,以最大限度地提高整体性能。 具有高度并行性的应用程序可以利用 GPU 的这种大规模并行特性来实现比 CPU 更高的性能。
1.2. CUDA®:通用并行计算平台和编程模型
2006 年 11 月,NVIDIA® 推出了 CUDA®,这是一个通用并行计算平台和编程模型,它利用 NVIDIA GPU 中的并行计算引擎,以比 CPU 更有效的方式解决许多复杂的计算问题。
CUDA 附带一个软件环境,允许开发人员使用 C++ 作为高级编程语言。 正如 图 2 所示,还支持其他语言、应用程序编程接口或基于指令的方法,例如 FORTRAN、DirectCompute、OpenACC。

图 2 GPU 计算应用程序。 CUDA 旨在支持各种语言和应用程序编程接口。
1.3. 可扩展的编程模型
多核 CPU 和众核 GPU 的出现意味着主流处理器芯片现在是并行系统。 挑战在于开发应用程序软件,使其并行性透明地扩展,以利用越来越多的处理器内核,就像 3D 图形应用程序透明地将其并行性扩展到具有数量差异很大的内核的众核 GPU 一样。
CUDA 并行编程模型旨在克服这一挑战,同时为熟悉 C 等标准编程语言的程序员保持较低的学习曲线。
它的核心是三个关键抽象概念 - 线程组的层次结构、共享内存和屏障同步 - 它们只是作为一组最少的语言扩展暴露给程序员。
这些抽象概念提供了细粒度的数据并行性和线程并行性,嵌套在粗粒度的数据并行性和任务并行性中。 它们引导程序员将问题划分为粗略的子问题,这些子问题可以由线程块并行独立地解决,并将每个子问题划分为更精细的部分,这些部分可以由块内所有线程并行协作地解决。
这种分解通过允许线程在解决每个子问题时进行协作来保留语言表达能力,同时实现自动可扩展性。 实际上,每个线程块都可以调度到 GPU 内的任何可用多处理器上,以任何顺序、并发或顺序方式调度,因此编译后的 CUDA 程序可以在任意数量的多处理器上执行,如 图 3 所示,并且只有运行时系统需要知道物理多处理器的数量。
这种可扩展的编程模型允许 GPU 架构通过简单地扩展多处理器和内存分区的数量来跨越广泛的市场范围:从高性能发烧友 GeForce GPU 和专业 Quadro 和 Tesla 计算产品到各种廉价的主流 GeForce GPU(有关所有支持 CUDA 的 GPU 的列表,请参阅 支持 CUDA 的 GPU)。

图 3 自动可扩展性
注意
GPU 是围绕流式多处理器 (SM) 阵列构建的(有关更多详细信息,请参阅 硬件实现)。 多线程程序被划分为彼此独立执行的线程块,因此具有更多多处理器的 GPU 将比具有较少多处理器的 GPU 花费更少的时间来自动执行程序。
1.4. 文档结构
本文档分为以下部分
介绍 是 CUDA 的一般介绍。
编程模型 概述了 CUDA 编程模型。
编程接口 描述了编程接口。
硬件实现 描述了硬件实现。
性能指南 提供了一些关于如何实现最佳性能的指导。
支持 CUDA 的 GPU 列出了所有支持 CUDA 的设备。
C++ 语言扩展 详细描述了 C++ 语言的所有扩展。
协作组 描述了用于各种 CUDA 线程组的同步原语。
CUDA 动态并行性 描述了如何从一个内核启动和同步另一个内核。
虚拟内存管理 描述了如何管理统一虚拟地址空间。
流有序内存分配器 描述了应用程序如何对内存分配和释放进行排序。
图内存节点 描述了图如何创建和拥有内存分配。
数学函数 列出了 CUDA 中支持的数学函数。
C++ 语言支持 列出了设备代码中支持的 C++ 功能。
纹理获取 提供了关于纹理获取的更多详细信息。
计算能力 提供了各种设备的技术规格以及更多架构细节。
驱动程序 API 介绍了低级驱动程序 API。
CUDA 环境变量 列出了所有 CUDA 环境变量。
统一内存编程 介绍了统一内存编程模型。
- 1
图形限定词来自于这样一个事实,即当 GPU 最初在 20 年前创建时,它被设计为加速图形渲染的专用处理器。 在对实时、高清、3D 图形的永不满足的市场需求的驱动下,它已发展成为一种通用处理器,用于比图形渲染更多的其他工作负载。
2. 编程模型
本章通过概述 CUDA 编程模型背后的主要概念如何在 C++ 中公开来介绍这些概念。
编程接口 中给出了 CUDA C++ 的详细描述。
本章和下一章中使用的向量加法示例的完整代码可以在 vectorAdd CUDA 示例中找到。
2.1. 内核
CUDA C++ 通过允许程序员定义 C++ 函数(称为内核)来扩展 C++,这些函数在被调用时,由 N 个不同的 CUDA 线程并行执行 N 次,而不是像常规 C++ 函数那样只执行一次。
内核是使用 __global__
声明说明符定义的,并且使用新的 <<<...>>>
执行配置语法(请参阅 执行配置)指定用于给定内核调用的执行该内核的 CUDA 线程数。 执行内核的每个线程都获得唯一的线程 ID,该 ID 可以通过内置变量在内核中访问。
作为示例,以下示例代码使用内置变量 threadIdx
将大小为 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 个线程中的每个线程执行一次成对加法。
2.2. 线程层次结构
为方便起见,threadIdx
是一个 3 分量向量,因此可以使用一维、二维或三维线程索引来标识线程,从而形成一维、二维或三维线程块,称为线程块。 这提供了一种在向量、矩阵或卷等域中的元素上调用计算的自然方法。
线程的索引及其线程 ID 以直接的方式相互关联:对于一维块,它们是相同的; 对于大小为 (Dx, Dy) 的二维块,索引为 (x, y) 的线程的线程 ID 为 (x + y Dx); 对于大小为 (Dx, Dy, Dz) 的三维块,索引为 (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 个线程。
但是,一个内核可以由多个形状相同的线程块执行,因此线程总数等于每个块的线程数乘以块数。
块被组织成一维、二维或三维网格的线程块,如 图 4 所示。 网格中线程块的数量通常由正在处理的数据的大小决定,数据大小通常超过系统中的处理器数量。

图 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 个线程)的线程块大小虽然在这种情况下是任意的,但却是常见的选择。 创建的网格具有足够的块,以便每个矩阵元素都有一个线程,如前所述。 为简单起见,此示例假设每个维度中每个网格的线程数可以被该维度中每个块的线程数均匀整除,尽管情况不必如此。
线程块需要独立执行。 必须可以按任何顺序、并行或串行执行块。 这种独立性要求允许线程块以任何顺序和跨任意数量的内核进行调度,如 图 3 所示,使程序员能够编写可随内核数量扩展的代码。
块内的线程可以通过共享内存共享数据,并通过同步其执行来协调内存访问来协作。 更准确地说,可以通过调用 __syncthreads()
内在函数在内核中指定同步点; __syncthreads()
充当屏障,块中的所有线程都必须在该屏障处等待,然后才能允许任何线程继续执行。 共享内存 给出了使用共享内存的示例。 除了 __syncthreads()
之外,协作组 API 还提供了一组丰富的线程同步原语。
为了实现高效协作,共享内存应是靠近每个处理器内核的低延迟内存(很像 L1 缓存),并且 __syncthreads()
应是轻量级的。
2.2.1. 线程块集群
随着 NVIDIA 计算能力 9.0 的推出,CUDA 编程模型引入了一个可选的层次结构级别,称为线程块集群,它由线程块组成。 与线程块中的线程保证在流式多处理器上共同调度类似,集群中的线程块也保证在 GPU 中的 GPU 处理集群 (GPC) 上共同调度。
与线程块类似,集群也组织成一维、二维或三维网格的线程块集群,如 图 5 所示。 集群中线程块的数量可以是用户定义的,并且在 CUDA 中,集群中最多支持 8 个线程块作为可移植集群大小。 请注意,在 GPU 硬件或 MIG 配置上,如果太小而无法支持 8 个多处理器,则最大集群大小将相应减小。 这些较小配置以及支持超出 8 个线程块集群大小的较大配置的识别是特定于架构的,可以使用 cudaOccupancyMaxPotentialClusterSize
API 进行查询。

图 5 线程块集群网格
注意
在使用集群支持启动的内核中,出于兼容性目的,gridDim 变量仍然表示线程块数量的大小。 集群中块的排名可以使用 集群组 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) 上共同调度,并允许集群中的线程块使用 集群组 API cluster.sync()
执行硬件支持的同步。 集群组还提供成员函数,分别使用 num_threads()
和 num_blocks()
API 查询集群组大小(以线程数或块数表示)。 可以分别使用 dim_threads()
和 dim_blocks()
API 查询集群组中线程或块的排名。
属于集群的线程块可以访问分布式共享内存。 集群中的线程块能够读取、写入和执行对分布式共享内存中任何地址的原子操作。 分布式共享内存 给出了在分布式共享内存中执行直方图的示例。
2.3. 内存层次结构
CUDA 线程在执行期间可能会从多个内存空间访问数据,如 图 6 所示。 每个线程都有私有本地内存。 每个线程块都有共享内存,该内存对块的所有线程可见,并且与该块具有相同的生命周期。 线程块集群中的线程块可以对彼此的共享内存执行读取、写入和原子操作。 所有线程都可以访问相同的全局内存。
还有两个额外的只读内存空间可供所有线程访问:常量和纹理内存空间。 全局、常量和纹理内存空间针对不同的内存使用情况进行了优化(请参阅 设备内存访问)。 纹理内存还为某些特定数据格式提供了不同的寻址模式以及数据过滤(请参阅 纹理和表面内存)。
全局、常量和纹理内存空间在同一应用程序的内核启动之间是持久存在的。

图 6 内存层次结构
2.4. 异构编程
如 图 7 所示,CUDA 编程模型假设 CUDA 线程在物理上独立的设备上执行,该设备作为运行 C++ 程序的主机的协处理器运行。 例如,当内核在 GPU 上执行,而 C++ 程序的其余部分在 CPU 上执行时,就是这种情况。
CUDA 编程模型还假设主机和设备都在 DRAM 中维护它们各自独立的内存空间,分别称为主机内存和设备内存。 因此,程序通过调用 CUDA 运行时(在 编程接口 中描述)来管理内核可见的全局、常量和纹理内存空间。 这包括设备内存分配和释放,以及主机和设备内存之间的数据传输。
统一内存提供托管内存来桥接主机和设备内存空间。 托管内存可以从系统中所有 CPU 和 GPU 以单个、一致的内存映像访问,并具有公共地址空间。 此功能支持设备内存的超额订阅,并且可以通过消除显式镜像主机和设备上的数据的需求来大大简化应用程序移植任务。 有关统一内存的介绍,请参阅 统一内存编程。

图 7 异构编程
注意
串行代码在主机上执行,而并行代码在设备上执行。
2.5. 异步 SIMT 编程模型
在 CUDA 编程模型中,线程是执行计算或内存操作的最低级别抽象。从基于 NVIDIA Ampere GPU 架构的设备开始,CUDA 编程模型通过异步编程模型为内存操作提供加速。异步编程模型定义了异步操作相对于 CUDA 线程的行为。
异步编程模型定义了 异步屏障 的行为,用于 CUDA 线程之间的同步。该模型还解释并定义了如何使用 cuda::memcpy_async 从全局内存异步移动数据,同时在 GPU 中进行计算。
2.5.1. 异步操作
异步操作被定义为由 CUDA 线程发起并异步执行的操作,就像由另一个线程执行一样。在一个良好形式的程序中,一个或多个 CUDA 线程与异步操作同步。发起异步操作的 CUDA 线程不一定需要在同步线程之中。
这种异步线程(as-if 线程)始终与发起异步操作的 CUDA 线程相关联。异步操作使用同步对象来同步操作的完成。这种同步对象可以由用户显式管理(例如,cuda::memcpy_async
),也可以在库中隐式管理(例如,cooperative_groups::memcpy_async
)。
同步对象可以是 cuda::barrier
或 cuda::pipeline
。这些对象在 异步屏障 和 使用 cuda::pipeline 的异步数据复制 中详细解释。这些同步对象可以在不同的线程作用域中使用。作用域定义了可以使用同步对象与异步操作同步的线程集合。下表定义了 CUDA C++ 中可用的线程作用域以及可以与每个作用域同步的线程。
线程作用域 |
描述 |
---|---|
|
只有发起异步操作的 CUDA 线程进行同步。 |
|
与发起线程在同一线程块内的所有或任何 CUDA 线程进行同步。 |
|
与发起线程在同一 GPU 设备中的所有或任何 CUDA 线程进行同步。 |
|
与发起线程在同一系统中的所有或任何 CUDA 或 CPU 线程进行同步。 |
这些线程作用域作为标准 C++ 的扩展在 CUDA 标准 C++ 库中实现。
2.6. 计算能力
设备的计算能力由一个版本号表示,有时也称为其“SM 版本”。此版本号标识 GPU 硬件支持的功能,并由应用程序在运行时使用,以确定当前 GPU 上可用的硬件功能和/或指令。
计算能力包括一个主修订号 X 和一个次修订号 Y,并表示为 X.Y。
具有相同主修订号的设备属于相同的核心架构。主修订号对于基于 NVIDIA Hopper GPU 架构的设备为 9,对于基于 NVIDIA Ampere GPU 架构的设备为 8,对于基于 Volta 架构的设备为 7,对于基于 Pascal 架构的设备为 6,对于基于 Maxwell 架构的设备为 5,以及对于基于 Kepler 架构的设备为 3。
次修订号对应于核心架构的增量改进,可能包括新功能。
Turing 是计算能力为 7.5 的设备的架构,是基于 Volta 架构的增量更新。
支持 CUDA 的 GPU 列出了所有支持 CUDA 的设备及其计算能力。计算能力 给出了每种计算能力的技术规格。
注意
特定 GPU 的计算能力版本不应与 CUDA 版本(例如,CUDA 7.5、CUDA 8、CUDA 9)混淆,CUDA 版本是 CUDA 软件平台的版本。CUDA 平台供应用程序开发人员创建可在多代 GPU 架构(包括未来尚未发明的 GPU 架构)上运行的应用程序。虽然新版本的 CUDA 平台通常通过支持新 GPU 架构的计算能力版本来添加对新 GPU 架构的原生支持,但新版本的 CUDA 平台通常也包括独立于硬件代的软件功能。
从 CUDA 7.0 和 CUDA 9.0 开始,Tesla 和 Fermi 架构分别不再受支持。
3. 编程接口
CUDA C++ 为熟悉 C++ 编程语言的用户提供了一条简单的途径,可以轻松地编写程序以供设备执行。
它由 C++ 语言的一组最小扩展和一个运行时库组成。
核心语言扩展已在 编程模型 中介绍。它们允许程序员将内核定义为 C++ 函数,并使用一些新语法来指定每次调用该函数时的网格和块维度。所有扩展的完整描述可以在 C++ 语言扩展 中找到。任何包含这些扩展的源文件都必须使用 nvcc
进行编译,如 使用 NVCC 编译 中所述。
运行时在 CUDA 运行时 中介绍。它提供了在主机上执行的 C 和 C++ 函数,用于分配和释放设备内存、在主机内存和设备内存之间传输数据、管理具有多个设备的系统等。运行时的完整描述可以在 CUDA 参考手册中找到。
运行时构建在较低级别的 C API(CUDA 驱动程序 API)之上,应用程序也可以访问该 API。驱动程序 API 通过公开较低级别的概念(如 CUDA 上下文 - 设备的主机进程的类似物 - 和 CUDA 模块 - 设备的动态加载库的类似物)来提供额外的控制级别。大多数应用程序不使用驱动程序 API,因为它们不需要这种额外的控制级别,并且在使用运行时时,上下文和模块管理是隐式的,从而产生更简洁的代码。由于运行时与驱动程序 API 可互操作,因此大多数需要某些驱动程序 API 功能的应用程序可以默认使用运行时 API,并且仅在需要时使用驱动程序 API。驱动程序 API 在 驱动程序 API 中介绍,并在参考手册中完整描述。
3.1. 使用 NVCC 编译
可以使用 CUDA 指令集架构(称为 PTX)编写内核,该架构在 PTX 参考手册中描述。然而,通常更有效的方法是使用高级编程语言,如 C++。在这两种情况下,内核都必须由 nvcc
编译为二进制代码才能在设备上执行。
nvcc
是一个编译器驱动程序,它简化了编译 C++ 或 PTX 代码的过程:它提供简单且熟悉的命令行选项,并通过调用实现不同编译阶段的工具集合来执行它们。本节概述了 nvcc
工作流程和命令行选项。完整的描述可以在 nvcc
用户手册中找到。
3.1.1. 编译工作流程
3.1.1.1. 离线编译
使用 nvcc
编译的源文件可以包含主机代码(即,在主机上执行的代码)和设备代码(即,在设备上执行的代码)的混合。nvcc
的基本工作流程包括将设备代码与主机代码分离,然后
将设备代码编译为汇编形式(PTX 代码)和/或二进制形式(cubin 对象),
并通过将 内核 中引入(并在 执行配置 中更详细地描述)的
<<<...>>>
语法替换为必要的 CUDA 运行时函数调用,以从 PTX 代码和/或 cubin 对象加载和启动每个已编译的内核,来修改主机代码。
修改后的主机代码可以输出为 C++ 代码,以便使用另一个工具进行编译,或者直接让 nvcc
在最后一个编译阶段调用主机编译器,从而输出为目标代码。
然后,应用程序可以
链接到已编译的主机代码(这是最常见的情况),
或者忽略修改后的主机代码(如果有),并使用 CUDA 驱动程序 API(参见 驱动程序 API)来加载和执行 PTX 代码或 cubin 对象。
3.1.1.2. 即时编译
应用程序在运行时加载的任何 PTX 代码都将由设备驱动程序进一步编译为二进制代码。这称为即时编译。即时编译会增加应用程序加载时间,但允许应用程序从每个新设备驱动程序带来的任何新的编译器改进中受益。这也是应用程序在应用程序编译时不存在的设备上运行的唯一方法,如 应用程序兼容性 中详述。
当设备驱动程序为某些应用程序即时编译某些 PTX 代码时,它会自动缓存生成的二进制代码的副本,以避免在后续应用程序调用中重复编译。缓存(称为计算缓存)在设备驱动程序升级时会自动失效,以便应用程序可以从设备驱动程序内置的新即时编译器中的改进中受益。
环境变量可用于控制即时编译,如 CUDA 环境变量 中所述。
作为使用 nvcc
编译 CUDA C++ 设备代码的替代方案,可以使用 NVRTC 在运行时将 CUDA C++ 设备代码编译为 PTX。NVRTC 是 CUDA C++ 的运行时编译库;更多信息可以在 NVRTC 用户指南中找到。
3.1.2. 二进制兼容性
二进制代码是特定于架构的。cubin 对象是使用编译器选项 -code
生成的,该选项指定目标架构:例如,使用 -code=sm_80
编译会生成用于 计算能力 8.0 设备的二进制代码。二进制兼容性保证从一个次要版本到下一个次要版本,但不保证从一个次要版本到上一个次要版本或跨主要版本。换句话说,为计算能力 X.y 生成的 cubin 对象将仅在计算能力为 X.z 的设备上执行,其中 z≥y。
注意
二进制兼容性仅在台式机上受支持。Tegra 不支持它。此外,台式机和 Tegra 之间的二进制兼容性也不受支持。
3.1.3. PTX 兼容性
某些 PTX 指令仅在具有更高计算能力的设备上受支持。例如,Warp Shuffle 函数 仅在计算能力为 5.0 及以上的设备上受支持。-arch
编译器选项指定将 C++ 编译为 PTX 代码时假定的计算能力。因此,例如,包含 warp shuffle 的代码必须使用 -arch=compute_50
(或更高版本)编译。
为某些特定计算能力生成的 PTX 代码始终可以编译为具有更大或相等计算能力的二进制代码。请注意,从较早的 PTX 版本编译的二进制文件可能无法利用某些硬件功能。例如,从为计算能力 6.0 (Pascal) 生成的 PTX 编译的针对计算能力 7.0 (Volta) 设备的二进制文件将不会使用 Tensor Core 指令,因为这些指令在 Pascal 上不可用。因此,最终二进制文件的性能可能比使用最新版本的 PTX 生成二进制文件时可能获得的性能更差。
编译为针对 架构条件功能 的 PTX 代码仅在完全相同的物理架构上运行,而不能在其他任何地方运行。架构条件 PTX 代码不向前和向后兼容。例如,使用 sm_90a
或 compute_90a
编译的代码仅在计算能力为 9.0 的设备上运行,并且不向后或向前兼容。
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 兼容的 PTX 和二进制代码(第三个 -gencode
选项)。
生成主机代码以在运行时自动选择最合适的代码进行加载和执行,在上面的示例中,这将是
用于计算能力 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 reduction 操作,这些操作仅在计算能力为 8.0 及以上的设备中受支持。__CUDA_ARCH__
宏可用于根据计算能力区分各种代码路径。它仅为设备代码定义。例如,当使用 -arch=compute_80
进行编译时,__CUDA_ARCH__
等于 800
。
如果为 架构条件功能 编译 x.cu
,例如使用 sm_90a
或 compute_90a
,则该代码只能在计算能力为 9.0 的设备上运行。
使用驱动程序 API 的应用程序必须将代码编译为单独的文件,并在运行时显式加载和执行最合适的文件。
Volta 架构引入了独立线程调度,这改变了线程在 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 位兼容性
nvcc
的 64 位版本以 64 位模式编译设备代码(即,指针为 64 位)。以 64 位模式编译的设备代码仅受以 64 位模式编译的主机代码支持。
3.2. CUDA 运行时
运行时在 cudart
库中实现,该库静态地通过 cudart.lib
或 libcudart.a
或动态地通过 cudart.dll
或 libcudart.so
链接到应用程序。需要 cudart.dll
和/或 libcudart.so
进行动态链接的应用程序通常将它们包含在应用程序安装包中。仅在链接到同一 CUDA 运行实例的组件之间传递 CUDA 运行时符号的地址才是安全的。
其所有入口点都以 cuda
为前缀。
如 异构编程 中所述,CUDA 编程模型假定系统由主机和设备组成,每个主机和设备都有自己独立的内存。设备内存 概述了用于管理设备内存的运行时函数。
共享内存 说明了共享内存的用法,共享内存在 线程层次结构 中介绍,用于最大化性能。
页锁定主机内存 介绍了页锁定主机内存,这是覆盖内核执行与主机和设备内存之间的数据传输所必需的。
异步并发执行 描述了用于在系统中各个级别启用异步并发执行的概念和 API。
多设备系统 展示了编程模型如何扩展到具有连接到同一主机的多个设备的系统。
错误检查 描述了如何正确检查运行时生成的错误。
调用堆栈 提到了用于管理 CUDA C++ 调用堆栈的运行时函数。
纹理和表面内存 介绍了纹理和表面内存空间,它们提供了另一种访问设备内存的方式;它们还公开了 GPU 纹理硬件的子集。
图形互操作性 介绍了运行时提供的各种函数,用于与两个主要的图形 API(OpenGL 和 Direct3D)进行互操作。
3.2.1. 初始化
从 CUDA 12.0 开始,cudaInitDevice()
和 cudaSetDevice()
调用初始化运行时和与指定设备关联的主上下文。如果没有这些调用,运行时将隐式使用设备 0 并根据需要进行自初始化以处理其他运行时 API 请求。在计时运行时函数调用以及解释运行时第一次调用的错误代码时,需要记住这一点。在 12.0 之前,cudaSetDevice()
不会初始化运行时,应用程序通常会使用空操作运行时调用 cudaFree(0)
来将运行时初始化与其他 API 活动隔离开来(为了计时和错误处理)。
运行时为系统中的每个设备创建一个 CUDA 上下文(有关 CUDA 上下文的更多详细信息,请参见 上下文)。此上下文是此设备的主上下文,并在第一个需要此设备上的活动上下文的运行时函数处初始化。它在应用程序的所有主机线程之间共享。作为此上下文创建的一部分,设备代码在必要时进行即时编译(参见 即时编译)并加载到设备内存中。这一切都是透明地发生的。如果需要(例如,为了驱动程序 API 互操作性),可以从驱动程序 API 访问设备的主上下文,如 运行时 API 和驱动程序 API 之间的互操作性 中所述。
当主机线程调用 cudaDeviceReset()
时,这将销毁主机线程当前操作的设备(即,设备选择 中定义的当前设备)的主上下文。任何将此设备作为当前设备的主机线程进行的下一个运行时函数调用都将为此设备创建一个新的主上下文。
注意
CUDA 接口使用在主机程序启动期间初始化并在主机程序终止期间销毁的全局状态。CUDA 运行时和驱动程序无法检测此状态是否无效,因此在程序启动或 main 之后的终止期间(隐式或显式)使用任何这些接口都将导致未定义的行为。
从 CUDA 12.0 开始,cudaSetDevice()
现在将在更改主机线程的当前设备后显式初始化运行时。以前版本的 CUDA 会延迟新设备上的运行时初始化,直到在 cudaSetDevice()
之后进行第一次运行时调用。此更改意味着现在检查 cudaSetDevice()
的返回值以查找初始化错误非常重要。
参考手册的错误处理和版本管理部分中的运行时函数不会初始化运行时。
3.2.2. 设备内存
如 异构编程 中所述,CUDA 编程模型假定系统由主机和设备组成,每个主机和设备都有自己独立的内存。内核在设备内存中运行,因此运行时提供函数来分配、释放和复制设备内存,以及在主机内存和设备内存之间传输数据。
设备内存可以分配为线性内存或 CUDA 数组。
CUDA 数组是为纹理获取优化的不透明内存布局。它们在 纹理和表面内存 中描述。
线性内存在单个统一地址空间中分配,这意味着单独分配的实体可以通过指针相互引用,例如,在二叉树或链表中。地址空间的大小取决于主机系统 (CPU) 和所用 GPU 的计算能力
x86_64 (AMD64) |
POWER (ppc64le) |
ARM64 |
|
---|---|---|---|
最高计算能力 5.3 (Maxwell) |
40 位 |
40 位 |
40 位 |
计算能力 6.0 (Pascal) 或更高版本 |
最高 47 位 |
最高 49 位 |
最高 48 位 |
注意
在计算能力为 5.3 (Maxwell) 及更早版本的设备上,CUDA 驱动程序创建一个未提交的 40 位虚拟地址保留,以确保内存分配(指针)落在支持的范围内。此保留显示为保留的虚拟内存,但在程序实际分配内存之前不占用任何物理内存。
线性内存通常使用 cudaMalloc()
分配,并使用 cudaFree()
释放,并且主机内存和设备内存之间的数据传输通常使用 cudaMemcpy()
完成。在 内核 的向量加法代码示例中,需要将向量从主机内存复制到设备内存
// 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()
函数)时,确保最佳性能。返回的 pitch(或步幅)必须用于访问数组元素。以下代码示例分配了一个 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 内核重复访问全局内存中的数据区域时,此类数据访问可以被认为是持久的。另一方面,如果数据仅被访问一次,则此类数据访问可以被认为是流式的。
从 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 配置为多实例 GPU (MIG) 模式时,L2 缓存预留功能将被禁用。
当使用多进程服务 (MPS) 时,L2 缓存预留大小无法通过 cudaDeviceSetLimit
更改。相反,预留大小只能在 MPS 服务器启动时通过环境变量 CUDA_DEVICE_DEFAULT_PERSISTING_L2_CACHE_PERCENTAGE_LIMIT
指定。
3.2.3.2. 持久访问的 L2 策略
访问策略窗口指定全局内存的连续区域以及该区域内访问的 L2 缓存中的持久性属性。
以下代码示例展示了如何使用 CUDA 流设置 L2 持久访问窗口。
CUDA 流示例
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 缓存中持久存在。
L2 持久性也可以为 CUDA Graph Kernel Node 设置,如下例所示
CUDA GraphKernelNode 示例
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 时,硬件将尝试将整个 32KB 窗口缓存在预留 L2 缓存区域中。由于预留区域小于窗口,缓存行将被逐出,以将 32KB 数据中最近使用的 16KB 保留在 L2 缓存的预留部分中。
因此,hitRatio
可用于避免缓存行抖动,并总体上减少移入和移出 L2 缓存的数据量。
hitRatio
值低于 1.0 可用于手动控制来自并发 CUDA 流的不同 accessPolicyWindow
可以在 L2 中缓存的数据量。例如,假设 L2 预留缓存大小为 16KB;两个不同 CUDA 流中的两个并发内核,每个内核都有 16KB 的 accessPolicyWindow
,并且 hitRatio
值都为 1.0,则在争用共享 L2 资源时,可能会相互逐出缓存行。但是,如果两个 accessPolicyWindow
的 hitRatio 值都为 0.5,则它们不太可能逐出自身或其他持久性缓存行。
3.2.3.3. L2 访问属性
为不同的全局内存数据访问定义了三种类型的访问属性
cudaAccessPropertyStreaming
:具有流式属性的内存访问不太可能在 L2 缓存中持久存在,因为这些访问会被优先逐出。cudaAccessPropertyPersisting
:具有持久性属性的内存访问更可能在 L2 缓存中持久存在,因为这些访问会被优先保留在 L2 缓存的预留部分中。cudaAccessPropertyNormal
:此访问属性强制将先前应用的持久性访问属性重置为正常状态。来自先前 CUDA 内核的具有持久性属性的内存访问可能会在其预期用途之后很长时间仍保留在 L2 缓存中。这种使用后持久性会减少后续不使用持久性属性的内核可用的 L2 缓存量。使用cudaAccessPropertyNormal
属性重置访问属性窗口会移除先前访问的持久性(优先保留)状态,就好像先前的访问没有访问属性一样。
3.2.3.4. L2 持久性示例
以下示例展示了如何为持久访问预留 L2 缓存,通过 CUDA 流在 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 访问重置为正常
来自先前 CUDA 内核的持久性 L2 缓存行可能会在使用后很长时间仍保留在 L2 中。因此,对于流式或正常内存访问,将 L2 缓存重置为正常对于以正常优先级利用 L2 缓存非常重要。有三种方法可以将持久性访问重置为正常状态。
使用访问属性
cudaAccessPropertyNormal
重置先前的持久性内存区域。通过调用
cudaCtxResetPersistingL2Cache()
将所有持久性 L2 缓存行重置为正常状态。最终,未触及的行将自动重置为正常状态。强烈建议不要依赖自动重置,因为自动重置发生所需的时间长度不确定。
3.2.3.6. 管理 L2 预留缓存的利用率
在不同 CUDA 流中并发执行的多个 CUDA 内核可能为其流分配了不同的访问策略窗口。但是,L2 预留缓存部分在所有这些并发 CUDA 内核之间共享。因此,此预留缓存部分的净利用率是所有并发内核的各个用途的总和。当持久访问量超过预留 L2 缓存容量时,将内存访问指定为持久性的好处会减少。
为了管理预留 L2 缓存部分的利用率,应用程序必须考虑以下几点
L2 预留缓存的大小。
可能并发执行的 CUDA 内核。
可能并发执行的所有 CUDA 内核的访问策略窗口。
何时以及如何需要 L2 重置,以允许正常或流式访问以相同的优先级利用先前预留的 L2 缓存。
3.2.3.7. 查询 L2 缓存属性
与 L2 缓存相关的属性是 cudaDeviceProp
结构的一部分,可以使用 CUDA 运行时 API cudaGetDeviceProperties
查询
CUDA 设备属性包括
l2CacheSize
:GPU 上可用的 L2 缓存量。persistingL2CacheMaxSize
:可以为持久内存访问预留的最大 L2 缓存量。accessPolicyMaxWindowSize
:访问策略窗口的最大大小。
3.2.3.8. 控制持久内存访问的 L2 缓存预留大小
持久内存访问的 L2 预留缓存大小使用 CUDA 运行时 API cudaDeviceGetLimit
查询,并使用 CUDA 运行时 API cudaDeviceSetLimit
设置为 cudaLimit
。设置此限制的最大值为 cudaDeviceProp::persistingL2CacheMaxSize
。
enum cudaLimit {
/* other fields not shown */
cudaLimitPersistingL2CacheSize
};
3.2.6. 页锁定主机内存
运行时提供了允许使用页锁定(也称为pinned)主机内存(与 malloc()
分配的常规可分页主机内存相对)的函数
cudaHostAlloc()
和cudaFreeHost()
分配和释放页锁定主机内存;cudaHostRegister()
页锁定由malloc()
分配的内存范围(有关限制,请参阅参考手册)。
使用页锁定主机内存有几个好处
对于某些设备,页锁定主机内存和设备内存之间的复制可以与内核执行并发执行,如 异步并发执行 中所述。
在某些设备上,页锁定主机内存可以映射到设备的地址空间中,从而无需将其复制到设备内存或从设备内存复制,如 映射内存 中详述。
在具有前端总线的系统上,如果主机内存分配为页锁定,并且如果此外还分配为写组合,则主机内存和设备内存之间的带宽更高,如 写组合内存 中所述。
注意
页锁定主机内存不会在非 I/O 一致性 Tegra 设备上缓存。此外,非 I/O 一致性 Tegra 设备不支持 cudaHostRegister()
。
简单的零拷贝 CUDA 示例附带了关于页锁定内存 API 的详细文档。
3.2.6.1. 可移植内存
页锁定内存块可以与系统中的任何设备结合使用(有关多设备系统的更多详细信息,请参阅 多设备系统),但默认情况下,上述使用页锁定内存的好处仅在与分配块时当前的设备(以及与所有共享相同统一地址空间的设备,如果有的话,如 统一虚拟地址空间 中所述)结合使用时可用。为了使所有设备都可获得这些优势,需要通过将标志 cudaHostAllocPortable
传递给 cudaHostAlloc()
或通过将标志 cudaHostRegisterPortable
传递给 cudaHostRegister()
来页锁定块。
3.2.6.2. 写组合内存
默认情况下,页锁定主机内存分配为可缓存的。可以选择将其分配为写组合,方法是将标志 cudaHostAllocWriteCombined
传递给 cudaHostAlloc()
。写组合内存释放了主机的 L1 和 L2 缓存资源,使更多缓存可用于应用程序的其余部分。此外,在跨 PCI Express 总线传输期间,不会侦听写组合内存,这可以将传输性能提高高达 40%。
从主机读取写组合内存的速度非常慢,因此写组合内存通常应仅用于主机仅写入的内存。
应避免在 WC 内存上使用 CPU 原子指令,因为并非所有 CPU 实现都保证该功能。
3.2.6.3. 映射内存
页锁定主机内存块也可以通过将标志 cudaHostAllocMapped
传递给 cudaHostAlloc()
或通过将标志 cudaHostRegisterMapped
传递给 cudaHostRegister()
来映射到设备的地址空间中。因此,这样的块通常有两个地址:一个在主机内存中,由 cudaHostAlloc()
或 malloc()
返回,另一个在设备内存中,可以使用 cudaHostGetDevicePointer()
检索,然后用于从内核内部访问该块。唯一的例外是使用 cudaHostAlloc()
分配的指针,以及当主机和设备使用统一地址空间时,如 统一虚拟地址空间 中所述。
从内核内部直接访问主机内存不会提供与设备内存相同的带宽,但确实有一些优点
无需在设备内存中分配块并在该块和主机内存中的块之间复制数据;数据传输由内核根据需要隐式执行;
无需使用流(请参阅 并发数据传输)将数据传输与内核执行重叠;内核发起的数据传输自动与内核执行重叠。
由于映射的页锁定内存在主机和设备之间共享,因此应用程序必须使用流或事件(请参阅 异步并发执行)同步内存访问,以避免任何潜在的写后读、读后写或写后写危险。
为了能够检索任何映射的页锁定内存的设备指针,必须在执行任何其他 CUDA 调用之前,通过使用 cudaDeviceMapHost
标志调用 cudaSetDeviceFlags()
来启用页锁定内存映射。否则,cudaHostGetDevicePointer()
将返回错误。
如果设备不支持映射的页锁定主机内存,cudaHostGetDevicePointer()
也会返回错误。应用程序可以通过检查 canMapHostMemory
设备属性(请参阅 设备枚举)来查询此功能,对于支持映射的页锁定主机内存的设备,该属性等于 1。
请注意,从主机或其他设备角度来看,对映射的页锁定内存进行操作的原子函数(请参阅 原子函数)不是原子的。
另请注意,CUDA 运行时要求从设备发起的对主机内存的 1 字节、2 字节、4 字节、8 字节和 16 字节自然对齐的加载和存储操作,从主机和其他设备角度来看,应作为单次访问保留。在某些平台上,对内存的原子操作可能会被硬件分解为单独的加载和存储操作。这些组件加载和存储操作对自然对齐访问的保留具有相同的要求。CUDA 运行时不支持 PCI Express 总线拓扑,其中 PCI Express 桥接器拆分 8 字节自然对齐的操作,并且 NVIDIA 不知道任何拆分 16 字节自然对齐操作的拓扑。
3.2.7. 内存同步域
3.2.7.1. 内存栅栏干扰
由于内存栅栏/刷新操作等待的事务比 CUDA 内存一致性模型所需的事务更多,因此某些 CUDA 应用程序可能会看到性能下降。
__managed__ int x = 0;
__device__ cuda::atomic<int, cuda::thread_scope_device> a(0);
__managed__ cuda::atomic<int, cuda::thread_scope_system> b(0);
|
||
线程 1 (SM) x = 1;
a = 1;
|
线程 2 (SM) while (a != 1) ;
assert(x == 1);
b = 1;
|
线程 3 (CPU) while (b != 1) ;
assert(x == 1);
|
考虑上面的例子。CUDA 内存一致性模型保证断言条件为真,因此来自线程 1 的对 x
的写入必须在来自线程 2 的对 b
的写入之前对线程 3 可见。
由 a
的释放和获取提供的内存排序仅足以使 x
对线程 2 可见,而不是对线程 3 可见,因为它是一个设备范围的操作。因此,由 b
的释放和获取提供的系统范围排序,不仅需要确保从线程 2 本身发出的写入对线程 3 可见,还需要确保对线程 2 可见的来自其他线程的写入。这被称为累积性。由于 GPU 在执行时无法知道哪些写入已在源级别保证可见,哪些仅因偶然时序而可见,因此它必须为正在进行的内存操作撒下一张保守的广阔的网络。
这有时会导致干扰:因为 GPU 正在等待源级别不需要的内存操作,所以栅栏/刷新可能比必要的时间更长。
请注意,栅栏可能会以代码中的内部函数或原子操作的形式显式出现(如示例中所示),或者隐式地实现任务边界处的同步于关系。
一个常见的例子是,当内核在本地 GPU 内存中执行计算时,而并行内核(例如来自 NCCL)正在与对等方执行通信。完成时,本地内核将隐式刷新其写入,以满足任何与下游工作的同步于关系。这可能会不必要地完全或部分等待来自通信内核的较慢的 nvlink 或 PCIe 写入。
3.2.7.2. 使用域隔离流量
从 Hopper 架构 GPU 和 CUDA 12.0 开始,内存同步域功能提供了一种减轻此类干扰的方法。为了换取代码的显式协助,GPU 可以减少栅栏操作撒下的网络。每个内核启动都给定一个域 ID。写入和栅栏都标有 ID,并且栅栏将仅对与栅栏域匹配的写入进行排序。在并发计算与通信的示例中,通信内核可以放置在不同的域中。
当使用域时,代码必须遵守以下规则:同一 GPU 上不同域之间的排序或同步需要系统范围的栅栏。在一个域内,设备范围的栅栏仍然足够。这对于累积性是必要的,因为一个内核的写入将不会被来自另一个域的内核发出的栅栏所包含。本质上,累积性通过确保跨域流量提前刷新到系统范围来满足。
请注意,这修改了 thread_scope_device
的定义。但是,由于内核将默认设置为域 0(如下所述),因此保持了向后兼容性。
3.2.7.3. 在 CUDA 中使用域
域可以通过新的启动属性 cudaLaunchAttributeMemSyncDomain
和 cudaLaunchAttributeMemSyncDomainMap
访问。前者在逻辑域 cudaLaunchMemSyncDomainDefault
和 cudaLaunchMemSyncDomainRemote
之间进行选择,后者提供从逻辑域到物理域的映射。远程域旨在用于执行远程内存访问的内核,以便将其内存流量与本地内核隔离。但是请注意,选择特定域不会影响内核可能合法执行的内存访问。
域计数可以通过设备属性 cudaDevAttrMemSyncDomainCount
查询。Hopper 有 4 个域。为了方便可移植代码,域功能可以在所有设备上使用,并且在 Hopper 之前 CUDA 将报告计数为 1。
拥有逻辑域简化了应用程序组合。堆栈中较低级别的单个内核启动(例如来自 NCCL)可以选择语义逻辑域,而无需担心周围的应用程序架构。更高级别可以使用映射来引导逻辑域。如果未设置逻辑域,则逻辑域的默认值为默认域,默认映射是将默认域映射到 0,并将远程域映射到 1(在具有 1 个以上域的 GPU 上)。特定库可能会在 CUDA 12.0 及更高版本中标记带有远程域的启动;例如,NCCL 2.16 将这样做。总之,这为常见的开箱即用应用程序提供了有益的使用模式,而无需在其他组件、框架或应用程序级别进行代码更改。另一种使用模式,例如在使用 nvshmem 或内核类型没有明确分离的应用程序中,可能是对并行流进行分区。流 A 可以将两个逻辑域都映射到物理域 0,流 B 映射到 1,依此类推。
// Example of launching a kernel with the remote logical domain
cudaLaunchAttribute domainAttr;
domainAttr.id = cudaLaunchAttrMemSyncDomain;
domainAttr.val = cudaLaunchMemSyncDomainRemote;
cudaLaunchConfig_t config;
// Fill out other config fields
config.attrs = &domainAttr;
config.numAttrs = 1;
cudaLaunchKernelEx(&config, myKernel, kernelArg1, kernelArg2...);
// Example of setting a mapping for a stream
// (This mapping is the default for streams starting on Hopper if not
// explicitly set, and provided for illustration)
cudaLaunchAttributeValue mapAttr;
mapAttr.memSyncDomainMap.default_ = 0;
mapAttr.memSyncDomainMap.remote = 1;
cudaStreamSetAttribute(stream, cudaLaunchAttributeMemSyncDomainMap, &mapAttr);
// Example of mapping different streams to different physical domains, ignoring
// logical domain settings
cudaLaunchAttributeValue mapAttr;
mapAttr.memSyncDomainMap.default_ = 0;
mapAttr.memSyncDomainMap.remote = 0;
cudaStreamSetAttribute(streamA, cudaLaunchAttributeMemSyncDomainMap, &mapAttr);
mapAttr.memSyncDomainMap.default_ = 1;
mapAttr.memSyncDomainMap.remote = 1;
cudaStreamSetAttribute(streamB, cudaLaunchAttributeMemSyncDomainMap, &mapAttr);
与其他启动属性一样,这些属性在 CUDA 流、使用 cudaLaunchKernelEx
的单个启动以及 CUDA 图中的内核节点上统一公开。典型的用法是在流级别设置映射,并在启动级别(或括起流使用部分)设置逻辑域,如上所述。
Both attributes are copied to graph nodes during stream capture. Graphs take both attributes from the node itself, essentially an indirect way of specifying a physical domain. Domain-related attributes set on the stream a graph is launched into are not used in execution of the graph.
CUDA 将以下操作作为独立任务公开,这些任务可以彼此并发运行
主机上的计算;
设备上的计算;
从主机到设备的内存传输;
从设备到主机的内存传输;
给定设备的内存内的内存传输;
设备之间的内存传输。
这些操作之间实现的并发级别将取决于设备的特性集和计算能力,如下所述。
并发主机执行通过异步库函数实现,这些函数在设备完成请求的任务之前将控制权返回给主机线程。 使用异步调用,可以将许多设备操作一起排队,以便在适当的设备资源可用时由 CUDA 驱动程序执行。 这减轻了主机线程管理设备的大部分责任,使其可以自由地执行其他任务。 以下设备操作相对于主机是异步的:
内核启动;
单个设备内存内的内存复制;
从主机到设备的小于等于 64 KB 的内存块的内存复制;
由带有
Async
后缀的函数执行的内存复制;内存设置函数调用。
程序员可以通过将 CUDA_LAUNCH_BLOCKING
环境变量设置为 1,全局禁用系统上运行的所有 CUDA 应用程序的内核启动的异步性。 此功能仅用于调试目的,不应将其用作使生产软件可靠运行的方法。
如果通过分析器(Nsight、Visual Profiler)收集硬件计数器,则内核启动是同步的,除非启用了并发内核分析。 如果 Async
内存复制涉及未页锁定的主机内存,则它们也可能是同步的。
一些计算能力为 2.x 及更高的设备可以并发执行多个内核。 应用程序可以通过检查 concurrentKernels
设备属性(请参阅 设备枚举)来查询此功能,对于支持它的设备,该属性等于 1。
设备可以并发执行的最大内核启动次数取决于其计算能力,并在 表 21 中列出。
来自一个 CUDA 上下文的内核不能与来自另一个 CUDA 上下文的内核并发执行。 GPU 可能会进行时间分片,以便为每个上下文提供向前进展。 如果用户想要在 SM 上同时运行来自多个进程的内核,则必须启用 MPS。
使用大量纹理或大量本地内存的内核不太可能与其他内核并发执行。
一些设备可以在内核执行的同时执行与 GPU 的异步内存复制。 应用程序可以通过检查 asyncEngineCount
设备属性(请参阅 设备枚举)来查询此功能,对于支持它的设备,该属性大于零。 如果主机内存参与复制,则它必须是页锁定的。
在支持 concurrentKernels
设备属性的设备上,也可以在内核执行的同时执行设备内复制,和/或在支持 asyncEngineCount
属性的设备上,也可以在内核执行的同时执行与设备的复制。 设备内复制使用标准内存复制函数启动,目标地址和源地址都位于同一设备上。
一些计算能力为 2.x 及更高的设备可以重叠与设备的复制。 应用程序可以通过检查 asyncEngineCount
设备属性(请参阅 设备枚举)来查询此功能,对于支持它的设备,该属性等于 2。 为了重叠,任何参与传输的主机内存都必须是页锁定的。
应用程序通过流管理上述并发操作。 流是按顺序执行的命令序列(可能由不同的主机线程发出)。 另一方面,不同的流可以相对于彼此无序或并发地执行其命令; 此行为不保证,因此不应依赖于正确性(例如,内核间通信是未定义的)。 当命令的所有依赖项都满足时,可以执行在流上发出的命令。 依赖项可能是同一流上先前启动的命令,也可能是来自其他流的依赖项。 同步调用的成功完成保证了所有启动的命令都已完成。
流通过创建流对象并将其指定为内核启动序列和主机 <->
设备内存复制的流参数来定义。 以下代码示例创建两个流,并在页锁定内存中分配一个 float
数组 hostPtr
。
cudaStream_t stream[2];
for (int i = 0; i < 2; ++i)
cudaStreamCreate(&stream[i]);
float* hostPtr;
cudaMallocHost(&hostPtr, 2 * size);
这些流中的每一个都由以下代码示例定义为从主机到设备的一个内存复制、一个内核启动和一个从设备到主机的内存复制的序列
for (int i = 0; i < 2; ++i) {
cudaMemcpyAsync(inputDevPtr + i * size, hostPtr + i * size,
size, cudaMemcpyHostToDevice, stream[i]);
MyKernel <<<100, 512, 0, stream[i]>>>
(outputDevPtr + i * size, inputDevPtr + i * size, size);
cudaMemcpyAsync(hostPtr + i * size, outputDevPtr + i * size,
size, cudaMemcpyDeviceToHost, stream[i]);
}
每个流将其输入数组 hostPtr
的一部分复制到设备内存中的数组 inputDevPtr
,通过调用 MyKernel()
在设备上处理 inputDevPtr
,并将结果 outputDevPtr
复制回 hostPtr
的同一部分。 重叠行为 描述了在此示例中流如何重叠,具体取决于设备的功能。 请注意,hostPtr
必须指向页锁定的主机内存才能发生任何重叠。
流通过调用 cudaStreamDestroy()
释放。
for (int i = 0; i < 2; ++i)
cudaStreamDestroy(stream[i]);
如果在调用 cudaStreamDestroy()
时设备仍在流中执行工作,则该函数将立即返回,并且一旦设备完成流中的所有工作,与该流关联的资源将自动释放。
未指定任何流参数或等效地将流参数设置为零的内核启动和主机 <->
设备内存复制将发布到默认流。 因此,它们按顺序执行。
对于使用 --default-stream per-thread
编译标志编译的代码(或在包含 CUDA 头文件(cuda.h
和 cuda_runtime.h
))之前定义 CUDA_API_PER_THREAD_DEFAULT_STREAM
宏的代码),默认流是常规流,并且每个主机线程都有其自己的默认流。
注意
#define CUDA_API_PER_THREAD_DEFAULT_STREAM 1
不能用于在代码由 nvcc
编译时启用此行为,因为 nvcc
在翻译单元的顶部隐式包含 cuda_runtime.h
。 在这种情况下,需要使用 --default-stream per-thread
编译标志,或者需要使用 -DCUDA_API_PER_THREAD_DEFAULT_STREAM=1
编译器标志定义 CUDA_API_PER_THREAD_DEFAULT_STREAM
宏。
对于使用 --default-stream legacy
编译标志编译的代码,默认流是一个特殊的流,称为 NULL 流,并且每个设备都有一个用于所有主机线程的单个 NULL 流。 NULL 流是特殊的,因为它会导致 隐式同步 中描述的隐式同步。
对于在编译时未指定 --default-stream
编译标志的代码,默认情况下假定为 --default-stream legacy
。
有多种方法可以显式同步流。
cudaDeviceSynchronize()
等待直到所有主机线程的所有流中的所有先前命令都已完成。
cudaStreamSynchronize()
接受一个流作为参数,并等待直到给定流中的所有先前命令都已完成。 它可用于将主机与特定流同步,从而允许其他流继续在设备上执行。
cudaStreamWaitEvent()
接受一个流和一个事件作为参数(有关事件的描述,请参阅 事件),并使在调用 cudaStreamWaitEvent()
之后添加到给定流的所有命令延迟执行,直到给定事件完成。
cudaStreamQuery()
为应用程序提供了一种方法来了解流中的所有先前命令是否已完成。
如果在这两个操作之间提交了 NULL 流上的任何 CUDA 操作,则来自不同流的两个操作不能并发运行,除非这些流是非阻塞流(使用 cudaStreamNonBlocking
标志创建)。
应用程序应遵循以下准则来提高其并发内核执行的潜力:
所有独立操作都应在依赖操作之前发出,
任何类型的同步都应尽可能延迟。
两个流之间的执行重叠量取决于命令发布到每个流的顺序,以及设备是否支持数据传输和内核执行的重叠(请参阅 数据传输和内核执行的重叠)、并发内核执行(请参阅 并发内核执行)和/或并发数据传输(请参阅 并发数据传输)。
例如,在不支持并发数据传输的设备上,流的创建和销毁 的代码示例中的两个流根本不重叠,因为从主机到设备的内存复制是在从设备到主机的内存复制发布到 stream[0] 之后才发布到 stream[1] 的,因此它只能在发布到 stream[0] 的从设备到主机的内存复制完成后才能开始。 如果代码以以下方式重写(并假设设备支持数据传输和内核执行的重叠)
for (int i = 0; i < 2; ++i)
cudaMemcpyAsync(inputDevPtr + i * size, hostPtr + i * size,
size, cudaMemcpyHostToDevice, stream[i]);
for (int i = 0; i < 2; ++i)
MyKernel<<<100, 512, 0, stream[i]>>>
(outputDevPtr + i * size, inputDevPtr + i * size, size);
for (int i = 0; i < 2; ++i)
cudaMemcpyAsync(hostPtr + i * size, outputDevPtr + i * size,
size, cudaMemcpyDeviceToHost, stream[i]);
然后,发布到 stream[1] 的从主机到设备的内存复制与发布到 stream[0] 的内核启动重叠。
在支持并发数据传输的设备上,流的创建和销毁 的代码示例中的两个流确实重叠:发布到 stream[1] 的从主机到设备的内存复制与发布到 stream[0] 的从设备到主机的内存复制甚至与发布到 stream[0] 的内核启动重叠(假设设备支持数据传输和内核执行的重叠)。
运行时提供了一种通过 cudaLaunchHostFunc()
在流中的任何点插入 CPU 函数调用的方法。 一旦在回调之前发布到流的所有命令都已完成,提供的函数将在主机上执行。
以下代码示例在将主机到设备内存复制、内核启动和设备到主机内存复制发布到每个流后,将主机函数 MyCallback
添加到两个流中的每一个。 该函数将在每个设备到主机内存复制完成后开始在主机上执行。
void CUDART_CB MyCallback(void *data){
printf("Inside callback %d\n", (size_t)data);
}
...
for (size_t i = 0; i < 2; ++i) {
cudaMemcpyAsync(devPtrIn[i], hostPtr[i], size, cudaMemcpyHostToDevice, stream[i]);
MyKernel<<<100, 512, 0, stream[i]>>>(devPtrOut[i], devPtrIn[i], size);
cudaMemcpyAsync(hostPtr[i], devPtrOut[i], size, cudaMemcpyDeviceToHost, stream[i]);
cudaLaunchHostFunc(stream[i], MyCallback, (void*)i);
}
在主机函数之后在流中发出的命令在函数完成之前不会开始执行。
排队到流中的主机函数不得进行 CUDA API 调用(直接或间接),因为它可能会最终等待自身(如果它进行这样的调用),从而导致死锁。
可以使用 cudaStreamCreateWithPriority()
在创建时指定流的相对优先级。 允许的优先级范围(按 [ 最高优先级,最低优先级 ] 排序)可以使用 cudaDeviceGetStreamPriorityRange()
函数获得。 在运行时,GPU 调度程序利用流优先级来确定任务执行顺序,但这些优先级充当提示而不是保证。 在选择要启动的工作时,较高优先级流中的待处理任务优先于较低优先级流中的任务。 较高优先级的任务不会抢占已经在运行的较低优先级的任务。 GPU 在任务执行期间不会重新评估工作队列,并且提高流的优先级不会中断正在进行的工作。 流优先级影响任务执行,而无需强制执行严格的排序,因此用户可以利用流优先级来影响任务执行,而无需依赖严格的排序保证。
以下代码示例获取当前设备允许的优先级范围,并创建具有最高和最低可用优先级的流。
// get the range of stream priorities for this device
int leastPriority, greatestPriority;
cudaDeviceGetStreamPriorityRange(&leastPriority, &greatestPriority);
// create streams with highest and lowest available priorities
cudaStream_t st_high, st_low;
cudaStreamCreateWithPriority(&st_high, cudaStreamNonBlocking, greatestPriority));
cudaStreamCreateWithPriority(&st_low, cudaStreamNonBlocking, leastPriority);
程序化依赖启动 机制允许依赖的辅助 内核在同一 CUDA 流中依赖它的主 内核完成执行之前启动。 从计算能力为 9.0 的设备开始提供,当辅助 内核可以完成不依赖于主 内核结果的重大工作时,此技术可以提供性能优势。
CUDA 应用程序通过在其上启动和执行多个内核来利用 GPU。 典型的 GPU 活动时间线如图 图 10 所示。

在此,secondary_kernel
在 primary_kernel
完成执行后启动。 串行执行通常是必要的,因为 secondary_kernel
依赖于 primary_kernel
生成的结果数据。 如果 secondary_kernel
不依赖于 primary_kernel
,则可以使用 流 并发启动它们。 即使 secondary_kernel
依赖于 primary_kernel
,也存在一些并发执行的潜力。 例如,几乎所有内核都有某种序言 部分,在此期间执行诸如清零缓冲区或加载常量值之类的任务。

图 11 展示了 secondary_kernel
的一部分,该部分可以并发执行而不会影响应用程序。 请注意,并发启动还允许我们将 secondary_kernel
的启动延迟隐藏在 primary_kernel
的执行背后。

使用程序化依赖启动 可以实现 图 12 中所示的 secondary_kernel
的并发启动和执行。
程序化依赖启动 对 CUDA 内核启动 API 进行了更改,如下节所述。 这些 API 至少需要计算能力 9.0 才能提供重叠执行。
在程序化依赖启动中,主内核和辅助内核在同一 CUDA 流中启动。 当主内核准备好启动辅助内核时,主内核应使用所有线程块执行 cudaTriggerProgrammaticLaunchCompletion
。 辅助内核必须使用可扩展启动 API 启动,如下所示。
__global__ void primary_kernel() {
// Initial work that should finish before starting secondary kernel
// Trigger the secondary kernel
cudaTriggerProgrammaticLaunchCompletion();
// Work that can coincide with the secondary kernel
}
__global__ void secondary_kernel()
{
// Independent work
// Will block until all primary kernels the secondary kernel is dependent on have completed and flushed results to global memory
cudaGridDependencySynchronize();
// Dependent work
}
cudaLaunchAttribute attribute[1];
attribute[0].id = cudaLaunchAttributeProgrammaticStreamSerialization;
attribute[0].val.programmaticStreamSerializationAllowed = 1;
configSecondary.attrs = attribute;
configSecondary.numAttrs = 1;
primary_kernel<<<grid_dim, block_dim, 0, stream>>>();
cudaLaunchKernelEx(&configSecondary, secondary_kernel);
当使用 cudaLaunchAttributeProgrammaticStreamSerialization
属性启动辅助内核时,CUDA 驱动程序可以安全地提前启动辅助内核,而不必等待主内核完成和内存刷新后再启动辅助内核。
当所有主线程块都已启动并执行 cudaTriggerProgrammaticLaunchCompletion
时,CUDA 驱动程序可以启动辅助内核。 如果主内核未执行触发器,则它会在主内核中的所有线程块退出后隐式发生。
在任何一种情况下,辅助线程块都可能在主内核写入的数据可见之前启动。 因此,当辅助内核配置为程序化依赖启动 时,它必须始终使用 cudaGridDependencySynchronize
或其他方法来验证来自主内核的结果数据是否可用。
请注意,这些方法提供了主内核和辅助内核并发执行的机会,但是此行为是机会性的,并且不保证导致并发内核执行。 以这种方式依赖并发执行是不安全的,并且可能导致死锁。
程序化依赖启动可以在 CUDA 图 中通过 流捕获 或直接通过 边数据 使用。 要在具有边数据的 CUDA 图中对该功能进行编程,请在连接两个内核节点的边上使用 cudaGraphDependencyType
值为 cudaGraphDependencyTypeProgrammatic
。 此边类型使上游内核对下游内核中的 cudaGridDependencySynchronize()
可见。 此类型必须与 cudaGraphKernelNodePortLaunchCompletion
或 cudaGraphKernelNodePortProgrammatic
的传出端口之一一起使用。
流捕获的最终图等效项如下所示:
流代码(缩写) |
生成的图边 |
---|---|
cudaLaunchAttribute attribute;
attribute.id = cudaLaunchAttributeProgrammaticStreamSerialization;
attribute.val.programmaticStreamSerializationAllowed = 1;
|
cudaGraphEdgeData edgeData;
edgeData.type = cudaGraphDependencyTypeProgrammatic;
edgeData.from_port = cudaGraphKernelNodePortProgrammatic;
|
cudaLaunchAttribute attribute;
attribute.id = cudaLaunchAttributeProgrammaticEvent;
attribute.val.programmaticEvent.triggerAtBlockStart = 0;
|
cudaGraphEdgeData edgeData;
edgeData.type = cudaGraphDependencyTypeProgrammatic;
edgeData.from_port = cudaGraphKernelNodePortProgrammatic;
|
cudaLaunchAttribute attribute;
attribute.id = cudaLaunchAttributeProgrammaticEvent;
attribute.val.programmaticEvent.triggerAtBlockStart = 1;
|
cudaGraphEdgeData edgeData;
edgeData.type = cudaGraphDependencyTypeProgrammatic;
edgeData.from_port = cudaGraphKernelNodePortLaunchCompletion;
|
CUDA 图提出了一种新的 CUDA 工作提交模型。 图是一系列操作(例如内核启动),这些操作通过依赖项连接,并且与其执行分开定义。 这允许图定义一次,然后重复启动。 将图的定义与其执行分离可以实现许多优化:首先,与流相比,CPU 启动成本降低了,因为大部分设置都是预先完成的; 其次,向 CUDA 呈现整个工作流程可以实现优化,这在使用流的逐段工作提交机制时可能无法实现。
要了解图可能实现的优化,请考虑流中发生的情况:当您将内核放入流中时,主机驱动程序会执行一系列操作,为在 GPU 上执行内核做准备。 这些操作对于设置和启动内核是必要的,是一种开销成本,对于发出的每个内核都必须支付。 对于执行时间短的 GPU 内核,此开销成本可能占总端到端执行时间的很大一部分。
使用图的工作提交分为三个不同的阶段:定义、实例化和执行。
在定义阶段,程序创建图中操作的描述以及它们之间的依赖关系。
实例化获取图模板的快照,验证它,并执行大部分工作设置和初始化,目的是最大限度地减少启动时需要完成的工作。 生成的实例称为可执行图。
可执行图可以启动到流中,类似于任何其他 CUDA 工作。 它可以启动任意次数,而无需重复实例化。
操作构成图中的节点。 操作之间的依赖关系是边。 这些依赖关系约束了操作的执行顺序。
一旦它依赖的节点完成,操作可以在任何时间调度。 调度留给 CUDA 系统。
图节点可以是以下之一:

CUDA 12.3 在 CUDA 图上引入了边数据。 边数据修改由边指定的依赖关系,由三个部分组成:传出端口、传入端口和类型。 传出端口指定何时触发关联的边。 传入端口指定节点的哪个部分依赖于关联的边。 类型修改端点之间的关系。
端口值特定于节点类型和方向,并且边类型可能限制为特定节点类型。 在所有情况下,零初始化的边数据表示默认行为。 传出端口 0 等待整个任务,传入端口 0 阻止整个任务,边类型 0 与具有内存同步行为的完全依赖关系相关联。
边数据在各种图形 API 中可选地指定,作为关联节点的并行数组。 如果将其作为输入参数省略,则使用零初始化的数据。 如果将其作为输出(查询)参数省略,则如果被忽略的边数据全部为零初始化,则 API 接受此操作,并且如果调用将丢弃信息,则返回 cudaErrorLossyQuery
。
边数据在某些流捕获 API 中也可用:cudaStreamBeginCaptureToGraph()
、cudaStreamGetCaptureInfo()
和 cudaStreamUpdateCaptureDependencies()
。 在这些情况下,尚没有下游节点。 数据与悬空边(半边)关联,该悬空边将连接到未来的捕获节点或在流捕获终止时丢弃。 请注意,某些边类型不等待上游节点的完全完成。 在考虑流捕获是否已完全重新加入到原始流时,将忽略这些边,并且不能在捕获结束时丢弃这些边。 请参阅 使用流捕获创建图。
目前,没有节点类型定义其他传入端口,并且只有内核节点定义其他传出端口。 有一个非默认依赖类型 cudaGraphDependencyTypeProgrammatic
,它在两个内核节点之间启用 程序化依赖启动。
可以通过两种机制创建图:显式 API 和流捕获。 以下是创建和执行下图的示例。

// Create the graph - it starts out empty
cudaGraphCreate(&graph, 0);
// For the purpose of this example, we'll create
// the nodes separately from the dependencies to
// demonstrate that it can be done in two stages.
// Note that dependencies can also be specified
// at node creation.
cudaGraphAddKernelNode(&a, graph, NULL, 0, &nodeParams);
cudaGraphAddKernelNode(&b, graph, NULL, 0, &nodeParams);
cudaGraphAddKernelNode(&c, graph, NULL, 0, &nodeParams);
cudaGraphAddKernelNode(&d, graph, NULL, 0, &nodeParams);
// Now set up dependencies on each node
cudaGraphAddDependencies(graph, &a, &b, 1); // A->B
cudaGraphAddDependencies(graph, &a, &c, 1); // A->C
cudaGraphAddDependencies(graph, &b, &d, 1); // B->D
cudaGraphAddDependencies(graph, &c, &d, 1); // C->D
流捕获提供了一种从现有基于流的 API 创建图的机制。 可以使用对 cudaStreamBeginCapture()
和 cudaStreamEndCapture()
的调用将启动工作到流中的代码段(包括现有代码)括起来。 见下文。
cudaGraph_t graph;
cudaStreamBeginCapture(stream);
kernel_A<<< ..., stream >>>(...);
kernel_B<<< ..., stream >>>(...);
libraryCall(stream);
kernel_C<<< ..., stream >>>(...);
cudaStreamEndCapture(stream, &graph);
调用 cudaStreamBeginCapture()
会将流置于捕获模式。当流处于捕获状态时,启动到流中的工作不会排队等待执行。相反,它会被附加到一个内部图中,该图会逐步构建。然后,通过调用 cudaStreamEndCapture()
返回此图,这也结束了流的捕获模式。通过流捕获主动构建的图称为捕获图。
流捕获可以用于任何 CUDA 流,除了 cudaStreamLegacy
(“NULL 流”)。请注意,它可以用于 cudaStreamPerThread
。如果程序正在使用传统流,则有可能将流 0 重新定义为每线程流,而不会产生功能上的变化。请参阅 默认流。
可以使用 cudaStreamIsCapturing()
查询流是否正在被捕获。
可以使用 cudaStreamBeginCaptureToGraph()
将工作捕获到现有图中。工作不是捕获到内部图,而是捕获到用户提供的图中。
3.2.8.7.3.1. 跨流依赖和事件
流捕获可以处理使用 cudaEventRecord()
和 cudaStreamWaitEvent()
表示的跨流依赖关系,前提是等待的事件记录在同一个捕获图中。
当事件记录在处于捕获模式的流中时,它会产生一个捕获事件。捕获事件表示捕获图中的一组节点。
当流等待捕获事件时,如果该流尚未处于捕获模式,则会将其置于捕获模式,并且流中的下一个项目将对捕获事件中的节点具有额外的依赖关系。然后,这两个流将被捕获到同一个捕获图中。
当流捕获中存在跨流依赖关系时,cudaStreamEndCapture()
仍然必须在调用 cudaStreamBeginCapture()
的同一流中调用;这是原始流。由于基于事件的依赖关系,任何其他被捕获到同一捕获图的流也必须重新加入到原始流。如下所示。所有被捕获到同一捕获图的流都会在 cudaStreamEndCapture()
时退出捕获模式。未能重新加入到原始流将导致整个捕获操作失败。
// stream1 is the origin stream
cudaStreamBeginCapture(stream1);
kernel_A<<< ..., stream1 >>>(...);
// Fork into stream2
cudaEventRecord(event1, stream1);
cudaStreamWaitEvent(stream2, event1);
kernel_B<<< ..., stream1 >>>(...);
kernel_C<<< ..., stream2 >>>(...);
// Join stream2 back to origin stream (stream1)
cudaEventRecord(event2, stream2);
cudaStreamWaitEvent(stream1, event2);
kernel_D<<< ..., stream1 >>>(...);
// End capture in the origin stream
cudaStreamEndCapture(stream1, &graph);
// stream1 and stream2 no longer in capture mode
上面代码返回的图显示在 图 14 中。
注意
当流退出捕获模式时,流中的下一个非捕获项目(如果有)仍然会对最近的先前非捕获项目具有依赖关系,尽管中间的项目已被删除。
3.2.8.7.3.2. 禁止和未处理的操作
同步或查询正在被捕获的流或捕获事件的执行状态是无效的,因为它们不代表计划执行的项目。查询或同步包含活动流捕获的更广泛的句柄(例如,当任何关联的流处于捕获模式时,设备或上下文句柄)的执行状态也是无效的。
当同一上下文中的任何流正在被捕获,并且它不是使用 cudaStreamNonBlocking
创建的时,任何尝试使用传统流都是无效的。这是因为传统流句柄始终包含这些其他流;排队到传统流将创建对正在捕获的流的依赖关系,并且查询或同步它将查询或同步正在捕获的流。
因此,在这种情况下调用同步 API 也是无效的。同步 API(例如 cudaMemcpy()
)将工作排队到传统流并在返回之前同步它。
注意
作为一般规则,当依赖关系会将捕获的内容与未捕获而是排队等待执行的内容连接起来时,CUDA 倾向于返回错误,而不是忽略依赖关系。将流置于捕获模式或退出捕获模式是一个例外;这会断开模式转换之前和之后立即添加到流中的项目之间的依赖关系。
通过等待来自正在被捕获的流的捕获事件来合并两个单独的捕获图是无效的,该流与事件的不同捕获图相关联。从正在被捕获的流等待非捕获事件而不指定 cudaEventWaitExternal 标志是无效的。
少量将异步操作排队到流中的 API 当前在图中不受支持,如果使用正在被捕获的流调用,将返回错误,例如 cudaStreamAttachMemAsync()
。
3.2.8.7.3.3. 失效
当在流捕获期间尝试无效操作时,任何关联的捕获图都会失效。当捕获图失效时,进一步使用任何正在被捕获的流或与该图关联的捕获事件都是无效的,并将返回错误,直到流捕获以 cudaStreamEndCapture()
结束。此调用将使关联的流退出捕获模式,但也会返回错误值和 NULL 图。
3.2.8.7.4. CUDA 用户对象
CUDA 用户对象可以用于帮助管理 CUDA 中异步工作使用的资源的生命周期。特别是,此功能对于 CUDA 图形 和 流捕获 非常有用。
各种资源管理方案与 CUDA 图形不兼容。例如,考虑基于事件的池或同步创建、异步销毁方案。
// Library API with pool allocation
void libraryWork(cudaStream_t stream) {
auto &resource = pool.claimTemporaryResource();
resource.waitOnReadyEventInStream(stream);
launchWork(stream, resource);
resource.recordReadyEvent(stream);
}
// Library API with asynchronous resource deletion
void libraryWork(cudaStream_t stream) {
Resource *resource = new Resource(...);
launchWork(stream, resource);
cudaStreamAddCallback(
stream,
[](cudaStream_t, cudaError_t, void *resource) {
delete static_cast<Resource *>(resource);
},
resource,
0);
// Error handling considerations not shown
}
这些方案对于 CUDA 图形来说很困难,因为资源的指针或句柄不固定,需要间接寻址或图形更新,并且每次提交工作时都需要同步 CPU 代码。如果这些考虑因素对库的调用者隐藏,并且由于在捕获期间使用了不允许的 API,它们也无法与流捕获一起工作。存在各种解决方案,例如向调用者公开资源。CUDA 用户对象提出了另一种方法。
CUDA 用户对象将用户指定析构函数回调与内部引用计数相关联,类似于 C++ shared_ptr
。引用可以由 CPU 上的用户代码和 CUDA 图形拥有。请注意,对于用户拥有的引用,与 C++ 智能指针不同,没有表示引用的对象;用户必须手动跟踪用户拥有的引用。典型的用例是在创建用户对象后立即将唯一的用户拥有的引用移动到 CUDA 图形。
当引用与 CUDA 图形关联时,CUDA 将自动管理图形操作。克隆的 cudaGraph_t
保留源 cudaGraph_t
拥有的每个引用的副本,具有相同的多重性。实例化的 cudaGraphExec_t
保留源 cudaGraph_t
中的每个引用的副本。当 cudaGraphExec_t
在未同步的情况下被销毁时,引用将被保留,直到执行完成。
这是一个使用示例。
cudaGraph_t graph; // Preexisting graph
Object *object = new Object; // C++ object with possibly nontrivial destructor
cudaUserObject_t cuObject;
cudaUserObjectCreate(
&cuObject,
object, // Here we use a CUDA-provided template wrapper for this API,
// which supplies a callback to delete the C++ object pointer
1, // Initial refcount
cudaUserObjectNoDestructorSync // Acknowledge that the callback cannot be
// waited on via CUDA
);
cudaGraphRetainUserObject(
graph,
cuObject,
1, // Number of references
cudaGraphUserObjectMove // Transfer a reference owned by the caller (do
// not modify the total reference count)
);
// No more references owned by this thread; no need to call release API
cudaGraphExec_t graphExec;
cudaGraphInstantiate(&graphExec, graph, nullptr, nullptr, 0); // Will retain a
// new reference
cudaGraphDestroy(graph); // graphExec still owns a reference
cudaGraphLaunch(graphExec, 0); // Async launch has access to the user objects
cudaGraphExecDestroy(graphExec); // Launch is not synchronized; the release
// will be deferred if needed
cudaStreamSynchronize(0); // After the launch is synchronized, the remaining
// reference is released and the destructor will
// execute. Note this happens asynchronously.
// If the destructor callback had signaled a synchronization object, it would
// be safe to wait on it at this point.
子图形节点中图形拥有的引用与子图形关联,而不是与父图形关联。如果子图形被更新或删除,则引用会相应地更改。如果可执行图形或子图形使用 cudaGraphExecUpdate
或 cudaGraphExecChildGraphNodeSetParams
更新,则新源图形中的引用将被克隆并替换目标图形中的引用。在任何一种情况下,如果先前的启动未同步,则任何将被释放的引用都将保留到启动完成执行为止。
目前没有通过 CUDA API 等待用户对象析构函数的机制。用户可以从析构函数代码手动发出同步对象的信号。此外,从析构函数调用 CUDA API 是不合法的,类似于对 cudaLaunchHostFunc
的限制。这是为了避免阻塞 CUDA 内部共享线程并阻止向前进行。如果依赖关系是单向的,并且执行调用的线程不能阻止 CUDA 工作的向前进行,则可以向另一个线程发出信号以执行 API 调用。
用户对象使用 cudaUserObjectCreate
创建,这是一个浏览相关 API 的良好起点。
3.2.8.7.5. 更新实例化的图形
使用图形提交工作分为三个不同的阶段:定义、实例化和执行。在工作流程没有变化的情况下,定义和实例化的开销可以在多次执行中摊销,并且图形比流具有明显的优势。
图形是工作流程的快照,包括内核、参数和依赖关系,以便尽可能快速有效地重放它。在工作流程发生变化的情况下,图形会过时,必须进行修改。图形结构的重大更改(例如拓扑或节点类型)将需要重新实例化源图形,因为必须重新应用各种与拓扑相关的优化技术。
重复实例化的成本可能会降低图形执行的总体性能优势,但通常只有节点参数(例如内核参数和 cudaMemcpy
地址)会发生变化,而图形拓扑保持不变。对于这种情况,CUDA 提供了一种称为“图形更新”的轻量级机制,该机制允许就地修改某些节点参数,而无需重建整个图形。这比重新实例化效率高得多。
更新将在下次启动图形时生效,因此它们不会影响之前的图形启动,即使它们在更新时正在运行。图形可以被更新和重新启动多次,因此可以在流上排队多个更新/启动。
CUDA 提供了两种更新实例化图形参数的机制,即整个图形更新和单个节点更新。整个图形更新允许用户提供拓扑结构相同的 cudaGraph_t
对象,其节点包含更新的参数。单个节点更新允许用户显式更新单个节点的参数。当大量节点正在更新时,或者当调用者不知道图形拓扑时(即,图形是由库调用的流捕获产生的),使用更新的 cudaGraph_t
更方便。当更改数量较少且用户拥有需要更新的节点的句柄时,首选使用单个节点更新。单个节点更新跳过拓扑检查和未更改节点的比较,因此在许多情况下可能更有效。
CUDA 还提供了一种机制,用于启用和禁用单个节点,而不会影响其当前参数。
以下部分更详细地解释了每种方法。
3.2.8.7.5.1. 图形更新限制
内核节点
函数的拥有上下文不能更改。
最初不使用 CUDA 动态并行性的节点不能更新为使用 CUDA 动态并行性的函数。
cudaMemset
和 cudaMemcpy
节点
操作数分配/映射到的 CUDA 设备不能更改。
源/目标内存必须从与原始源/目标内存相同的上下文中分配。
只能更改 1D
cudaMemset
/cudaMemcpy
节点。
其他内存复制节点限制
更改源或目标内存类型(即,
cudaPitchedPtr
、cudaArray_t
等)或传输类型(即,cudaMemcpyKind
)不受支持。
外部信号量等待节点和记录节点
不支持更改信号量的数量。
条件节点
图形之间的句柄创建和赋值顺序必须匹配。
不支持更改节点参数(即,条件中的图形数量、节点上下文等)。
条件主体图形中节点的参数更改受上述规则的约束。
对主机节点、事件记录节点或事件等待节点的更新没有限制。
3.2.8.7.5.2. 整个图形更新
cudaGraphExecUpdate()
允许使用来自拓扑结构相同的图形(“更新”图形)的参数来更新实例化的图形(“原始图形”)。更新图形的拓扑结构必须与用于实例化 cudaGraphExec_t
的原始图形相同。此外,指定依赖关系的顺序必须匹配。最后,CUDA 需要一致地排序 sink 节点(没有依赖关系的节点)。CUDA 依赖于特定 API 调用的顺序来实现一致的 sink 节点排序。
更明确地说,遵循以下规则将使 cudaGraphExecUpdate()
确定性地配对原始图形和更新图形中的节点
对于任何捕获流,对该流进行操作的 API 调用必须以相同的顺序进行,包括事件等待和其他不直接对应于节点创建的 API 调用。
直接操作给定图形节点的传入边(包括捕获流 API、节点添加 API 和边添加/删除 API)的 API 调用必须以相同的顺序进行。此外,当依赖关系在数组中指定给这些 API 时,在这些数组内部指定依赖关系的顺序必须匹配。
Sink 节点必须一致排序。Sink 节点是在调用
cudaGraphExecUpdate()
时最终图形中没有依赖节点/传出边的节点。以下操作会影响 sink 节点排序(如果存在),并且必须(作为一个组合集)以相同的顺序进行导致 sink 节点的节点添加 API。
导致节点成为 sink 节点的边删除。
cudaStreamUpdateCaptureDependencies()
,如果它从捕获流的依赖关系集中删除 sink 节点。cudaStreamEndCapture()
.
以下示例显示了如何使用 API 来更新实例化的图形
cudaGraphExec_t graphExec = NULL;
for (int i = 0; i < 10; i++) {
cudaGraph_t graph;
cudaGraphExecUpdateResult updateResult;
cudaGraphNode_t errorNode;
// In this example we use stream capture to create the graph.
// You can also use the Graph API to produce a graph.
cudaStreamBeginCapture(stream, cudaStreamCaptureModeGlobal);
// Call a user-defined, stream based workload, for example
do_cuda_work(stream);
cudaStreamEndCapture(stream, &graph);
// If we've already instantiated the graph, try to update it directly
// and avoid the instantiation overhead
if (graphExec != NULL) {
// If the graph fails to update, errorNode will be set to the
// node causing the failure and updateResult will be set to a
// reason code.
cudaGraphExecUpdate(graphExec, graph, &errorNode, &updateResult);
}
// Instantiate during the first iteration or whenever the update
// fails for any reason
if (graphExec == NULL || updateResult != cudaGraphExecUpdateSuccess) {
// If a previous update failed, destroy the cudaGraphExec_t
// before re-instantiating it
if (graphExec != NULL) {
cudaGraphExecDestroy(graphExec);
}
// Instantiate graphExec from graph. The error node and
// error message parameters are unused here.
cudaGraphInstantiate(&graphExec, graph, NULL, NULL, 0);
}
cudaGraphDestroy(graph);
cudaGraphLaunch(graphExec, stream);
cudaStreamSynchronize(stream);
}
典型的工作流程是使用流捕获或图形 API 创建初始 cudaGraph_t
。cudaGraph_t
然后被实例化并正常启动。在初始启动之后,使用与初始图形相同的方法创建一个新的 cudaGraph_t
,并调用 cudaGraphExecUpdate()
。如果图形更新成功,如上面示例中的 updateResult
参数所示,则启动更新后的 cudaGraphExec_t
。如果更新由于任何原因失败,则调用 cudaGraphExecDestroy()
和 cudaGraphInstantiate()
来销毁原始 cudaGraphExec_t
并实例化一个新的。
也可以直接更新 cudaGraph_t
节点(即,使用 cudaGraphKernelNodeSetParams()
),然后更新 cudaGraphExec_t
,但是,使用下一节介绍的显式节点更新 API 更有效。
条件句柄标志和默认值作为图形更新的一部分进行更新。
有关用法和当前限制的更多信息,请参阅 图形 API。
3.2.8.7.5.3. 单个节点更新
可以直接更新实例化图形节点参数。这消除了实例化的开销以及创建新的 cudaGraph_t
的开销。如果需要更新的节点数量相对于图形中的节点总数较小,则最好单独更新节点。以下方法可用于更新 cudaGraphExec_t
节点
cudaGraphExecKernelNodeSetParams()
cudaGraphExecMemcpyNodeSetParams()
cudaGraphExecMemsetNodeSetParams()
cudaGraphExecHostNodeSetParams()
cudaGraphExecChildGraphNodeSetParams()
cudaGraphExecEventRecordNodeSetEvent()
cudaGraphExecEventWaitNodeSetEvent()
cudaGraphExecExternalSemaphoresSignalNodeSetParams()
cudaGraphExecExternalSemaphoresWaitNodeSetParams()
有关用法和当前限制的更多信息,请参阅 图形 API。
3.2.8.7.5.4. 单个节点启用
可以使用 cudaGraphNodeSetEnabled()
API 启用或禁用实例化图形中的内核、memset 和 memcpy 节点。这允许创建包含所需功能超集的图形,该图形可以针对每次启动进行自定义。可以使用 cudaGraphNodeGetEnabled()
API 查询节点的启用状态。
禁用的节点在功能上等同于空节点,直到重新启用为止。节点参数不受启用/禁用节点的影响。启用状态不受单个节点更新或使用 cudaGraphExecUpdate()
进行的整个图形更新的影响。当节点重新启用时,禁用状态下的参数更新将生效。
以下方法可用于启用/禁用 cudaGraphExec_t
节点,以及查询其状态
cudaGraphNodeSetEnabled()
cudaGraphNodeGetEnabled()
有关用法和当前限制的更多信息,请参阅 图形 API。
3.2.8.7.6. 使用图形 API
cudaGraph_t
对象不是线程安全的。用户有责任确保多个线程不会同时访问同一个 cudaGraph_t
。
cudaGraphExec_t
不能与其自身并发运行。cudaGraphExec_t
的启动将在同一可执行图形的先前启动之后排序。
图形执行在流中完成,以便与其他异步工作排序。但是,该流仅用于排序;它不限制图形的内部并行性,也不影响图形节点在哪里执行。
请参阅 图形 API。
3.2.8.7.7. 设备图形启动
许多工作流程需要在运行时做出数据相关的决策,并根据这些决策执行不同的操作。用户可能希望在设备上执行此决策过程,而不是将此决策过程卸载到主机,这可能需要从设备进行往返。为此,CUDA 提供了一种从设备启动图形的机制。
设备图形启动提供了一种从设备执行动态控制流的便捷方法,无论是像循环一样简单,还是像设备端工作调度器一样复杂。此功能仅在支持 统一虚拟地址空间 的系统上可用。
可以从设备启动的图形在下文中将称为设备图形,而不能从设备启动的图形将称为主机图形。
设备图形可以从主机和设备启动,而主机图形只能从主机启动。与主机启动不同,当图形的先前启动正在运行时,从设备启动设备图形将导致错误,返回 cudaErrorInvalidValue
;因此,设备图形不能同时从设备启动两次。同时从主机和设备启动设备图形将导致未定义的行为。
3.2.8.7.7.1. 设备图形创建
为了使图形可以从设备启动,必须显式实例化以进行设备启动。这是通过将 cudaGraphInstantiateFlagDeviceLaunch
标志传递给 cudaGraphInstantiate()
调用来实现的。与主机图形的情况一样,设备图形结构在实例化时是固定的,并且在不重新实例化的情况下无法更新,并且实例化只能在主机上执行。为了使图形能够被实例化以进行设备启动,它必须遵守各种要求。
一般要求
图形的节点必须全部驻留在单个设备上。
图形只能包含内核节点、内存复制节点、memset 节点和子图形节点。
内核节点
不允许图形中的内核使用 CUDA 动态并行性。
只要未使用 MPS,就允许协同启动。
内存复制节点
仅允许涉及设备内存和/或pinned设备映射的主机内存的复制。
不允许涉及 CUDA 数组的复制。
在实例化时,两个操作数都必须可以从当前设备访问。请注意,即使复制操作的目标是另一个设备上的内存,它也将从图形驻留的设备上执行。
为了在设备上启动图形,必须首先将其上传到设备以填充必要的设备资源。这可以通过两种方式实现。
首先,可以显式上传图形,可以通过 cudaGraphUpload()
或通过请求作为实例化的一部分上传,通过 cudaGraphInstantiateWithParams()
。
或者,可以首先从主机启动图形,这将作为启动的一部分隐式执行此上传步骤。
以下可以看到所有三种方法的示例
// Explicit upload after instantiation
cudaGraphInstantiate(&deviceGraphExec1, deviceGraph1, cudaGraphInstantiateFlagDeviceLaunch);
cudaGraphUpload(deviceGraphExec1, stream);
// Explicit upload as part of instantiation
cudaGraphInstantiateParams instantiateParams = {0};
instantiateParams.flags = cudaGraphInstantiateFlagDeviceLaunch | cudaGraphInstantiateFlagUpload;
instantiateParams.uploadStream = stream;
cudaGraphInstantiateWithParams(&deviceGraphExec2, deviceGraph2, &instantiateParams);
// Implicit upload via host launch
cudaGraphInstantiate(&deviceGraphExec3, deviceGraph3, cudaGraphInstantiateFlagDeviceLaunch);
cudaGraphLaunch(deviceGraphExec3, stream);
设备图形只能从主机更新,并且必须在可执行图形更新后重新上传到设备,以便更改生效。可以使用上一节中概述的相同方法来实现此目的。与主机图形不同,当设备图形正在应用更新时从设备启动设备图形将导致未定义的行为。
3.2.8.7.7.2. 设备启动
设备图形可以通过 cudaGraphLaunch()
从主机和设备启动,它在设备上的签名与在主机上的签名相同。设备图形通过主机和设备上的相同句柄启动。从设备启动时,设备图形必须从另一个图形启动。
设备端图形启动是每线程的,并且可能同时从不同线程发生多次启动,因此用户将需要选择单个线程来从中启动给定的图形。
与主机启动不同,设备图形不能启动到常规 CUDA 流中,只能启动到不同的命名流中,每个命名流都表示特定的启动模式
流 |
启动模式 |
---|---|
|
即发即弃启动 |
|
尾部启动 |
|
同级启动 |
顾名思义,即发即弃启动会立即提交到 GPU,并且它独立于启动图形运行。在即发即弃场景中,启动图形是父图形,而启动的图形是子图形。

图 15 即发即弃启动
上面的图表可以通过下面的示例代码生成
__global__ void launchFireAndForgetGraph(cudaGraphExec_t graph) {
cudaGraphLaunch(graph, cudaStreamGraphFireAndForget);
}
void graphSetup() {
cudaGraphExec_t gExec1, gExec2;
cudaGraph_t g1, g2;
// Create, instantiate, and upload the device graph.
create_graph(&g2);
cudaGraphInstantiate(&gExec2, g2, cudaGraphInstantiateFlagDeviceLaunch);
cudaGraphUpload(gExec2, stream);
// Create and instantiate the launching graph.
cudaStreamBeginCapture(stream, cudaStreamCaptureModeGlobal);
launchFireAndForgetGraph<<<1, 1, 0, stream>>>(gExec2);
cudaStreamEndCapture(stream, &g1);
cudaGraphInstantiate(&gExec1, g1);
// Launch the host graph, which will in turn launch the device graph.
cudaGraphLaunch(gExec1, stream);
}
在一个图形的执行过程中,最多可以有 120 个即发即弃图形。此总数在同一父图形的启动之间重置。
为了充分理解设备端同步模型,首先需要理解执行环境的概念。
当图形从设备启动时,它会在其自身的执行环境中启动。给定图形的执行环境封装了图形中的所有工作以及所有生成的即发即弃工作。当图形完成执行并且所有生成的子工作都完成时,可以认为图形已完成。
下图显示了上一节中即发即弃示例代码生成的环境封装。

图 16 即发即弃启动,带有执行环境
这些环境也是分层的,因此图形环境可以包括来自即发即弃启动的多个级别的子环境。

图 17 嵌套的即发即弃环境
当图形从主机启动时,存在一个流环境,该环境是启动图形的执行环境的父环境。流环境封装了作为整体启动一部分生成的所有工作。当整体流环境标记为完成时,流启动完成(即,下游依赖工作现在可以运行)。

图 18 可视化的流环境
与主机上不同,无法通过传统方法(例如 cudaDeviceSynchronize()
或 cudaStreamSynchronize()
)从 GPU 与设备图形同步。相反,为了实现串行工作依赖关系,提供了一种不同的启动模式 - 尾部启动 - 以提供类似的功能。
当图形的环境被认为完成时(即,当图形及其所有子图形都完成时),尾部启动会执行。当图形完成时,尾部启动列表中的下一个图形的环境将替换已完成的环境,作为父环境的子环境。与即发即弃启动一样,一个图形可以有多个图形排队等待尾部启动。

图 19 简单的尾部启动
上面的执行流程可以通过以下代码生成
__global__ void launchTailGraph(cudaGraphExec_t graph) {
cudaGraphLaunch(graph, cudaStreamGraphTailLaunch);
}
void graphSetup() {
cudaGraphExec_t gExec1, gExec2;
cudaGraph_t g1, g2;
// Create, instantiate, and upload the device graph.
create_graph(&g2);
cudaGraphInstantiate(&gExec2, g2, cudaGraphInstantiateFlagDeviceLaunch);
cudaGraphUpload(gExec2, stream);
// Create and instantiate the launching graph.
cudaStreamBeginCapture(stream, cudaStreamCaptureModeGlobal);
launchTailGraph<<<1, 1, 0, stream>>>(gExec2);
cudaStreamEndCapture(stream, &g1);
cudaGraphInstantiate(&gExec1, g1);
// Launch the host graph, which will in turn launch the device graph.
cudaGraphLaunch(gExec1, stream);
}
给定图形排队的尾部启动将一次执行一个,按照它们排队的顺序。因此,第一个排队的图形将首先运行,然后是第二个,依此类推。

图 20 尾部启动排序
由尾部图形排队的尾部启动将在由尾部启动列表中先前图形排队的尾部启动之前执行。这些新的尾部启动将按照它们排队的顺序执行。

图 21 从多个图形排队时的尾部启动排序
一个图形最多可以有 255 个待处理的尾部启动。
设备图有可能将自身加入尾部启动队列,但给定的图一次只能有一个自启动加入队列。为了查询当前正在运行的设备图以便可以重新启动它,添加了一个新的设备端函数。
cudaGraphExec_t cudaGetCurrentGraphExec();
如果当前运行的图是设备图,则此函数返回当前运行图的句柄。如果当前执行的内核不是设备图中的节点,则此函数将返回 NULL。
下面是示例代码,展示了此函数在重新启动循环中的用法
__device__ int relaunchCount = 0;
__global__ void relaunchSelf() {
int relaunchMax = 100;
if (threadIdx.x == 0) {
if (relaunchCount < relaunchMax) {
cudaGraphLaunch(cudaGetCurrentGraphExec(), cudaStreamGraphTailLaunch);
}
relaunchCount++;
}
}
同级启动是即发即弃启动的一种变体,其中图的启动不是作为启动图执行环境的子级,而是作为启动图父级环境的子级。同级启动等效于从启动图的父级环境进行的即发即弃启动。

图 22 简单的同级启动
上面的图表可以通过下面的示例代码生成
__global__ void launchSiblingGraph(cudaGraphExec_t graph) {
cudaGraphLaunch(graph, cudaStreamGraphFireAndForgetAsSibling);
}
void graphSetup() {
cudaGraphExec_t gExec1, gExec2;
cudaGraph_t g1, g2;
// Create, instantiate, and upload the device graph.
create_graph(&g2);
cudaGraphInstantiate(&gExec2, g2, cudaGraphInstantiateFlagDeviceLaunch);
cudaGraphUpload(gExec2, stream);
// Create and instantiate the launching graph.
cudaStreamBeginCapture(stream, cudaStreamCaptureModeGlobal);
launchSiblingGraph<<<1, 1, 0, stream>>>(gExec2);
cudaStreamEndCapture(stream, &g1);
cudaGraphInstantiate(&gExec1, g1);
// Launch the host graph, which will in turn launch the device graph.
cudaGraphLaunch(gExec1, stream);
}
由于同级启动不是在启动图的执行环境中启动的,因此它们不会限制由启动图加入队列的尾部启动。
3.2.8.7.8. 条件图节点
条件节点允许对包含在条件节点内的图进行条件执行和循环。这允许动态和迭代的工作流程完全在图中表示,并释放主机 CPU 以并行执行其他工作。
当满足条件节点的依赖项时,条件值的评估在设备上执行。条件节点可以是以下类型之一
条件值通过 条件句柄 访问,该句柄必须在节点之前创建。条件值可以使用设备代码 cudaGraphSetConditional()
设置。在创建句柄时,也可以指定在每次图启动时应用的默认值。
创建条件节点时,将创建一个空图,并将句柄返回给用户,以便可以填充该图。可以使用 graph APIs 或 cudaStreamBeginCaptureToGraph() 填充此条件主体图。
条件节点可以嵌套。
3.2.8.7.8.1. 条件句柄
条件值由 cudaGraphConditionalHandle
表示,并通过 cudaGraphConditionalHandleCreate()
创建。
句柄必须与单个条件节点关联。句柄无法销毁。
如果在创建句柄时指定了 cudaGraphCondAssignDefault
,则条件值将在每次图执行开始时初始化为指定的默认值。如果未提供此标志,则条件值在每次图执行开始时都是未定义的,并且代码不应假设条件值在执行之间持续存在。
与句柄关联的默认值和标志将在 整个图更新 期间更新。
3.2.8.7.8.2. 条件节点主体图要求
一般要求
图形的节点必须全部驻留在单个设备上。
该图只能包含内核节点、空节点、memcpy 节点、memset 节点、子图节点和条件节点。
内核节点
不允许图形中的内核使用 CUDA 动态并行性。
只要未使用 MPS,就允许协同启动。
Memcpy/Memset 节点
仅允许涉及设备内存和/或pinned设备映射主机内存的复制/memset。
不允许涉及 CUDA 数组的复制/memset。
在实例化时,两个操作数都必须可以从当前设备访问。请注意,即使复制操作的目标是另一个设备上的内存,它也将从图形驻留的设备上执行。
3.2.8.7.8.3. 条件 IF 节点
如果在节点执行时条件为非零,则 IF 节点的主体图将执行一次。下图描述了一个 3 节点图,其中中间节点 B 是一个条件节点

图 23 条件 IF 节点
以下代码说明了如何创建一个包含 IF 条件节点的图。条件的默认值是使用上游内核设置的。条件的主体是使用 graph API 填充的。
__global__ void setHandle(cudaGraphConditionalHandle handle)
{
...
cudaGraphSetConditional(handle, value);
...
}
void graphSetup() {
cudaGraph_t graph;
cudaGraphExec_t graphExec;
cudaGraphNode_t node;
void *kernelArgs[1];
int value = 1;
cudaGraphCreate(&graph, 0);
cudaGraphConditionalHandle handle;
cudaGraphConditionalHandleCreate(&handle, graph);
// Use a kernel upstream of the conditional to set the handle value
cudaGraphNodeParams params = { cudaGraphNodeTypeKernel };
params.kernel.func = (void *)setHandle;
params.kernel.gridDim.x = params.kernel.gridDim.y = params.kernel.gridDim.z = 1;
params.kernel.blockDim.x = params.kernel.blockDim.y = params.kernel.blockDim.z = 1;
params.kernel.kernelParams = kernelArgs;
kernelArgs[0] = &handle;
cudaGraphAddNode(&node, graph, NULL, 0, ¶ms);
cudaGraphNodeParams cParams = { cudaGraphNodeTypeConditional };
cParams.conditional.handle = handle;
cParams.conditional.type = cudaGraphCondTypeIf;
cParams.conditional.size = 1;
cudaGraphAddNode(&node, graph, &node, 1, &cParams);
cudaGraph_t bodyGraph = cParams.conditional.phGraph_out[0];
// Populate the body of the conditional node
...
cudaGraphAddNode(&node, bodyGraph, NULL, 0, ¶ms);
cudaGraphInstantiate(&graphExec, graph, NULL, NULL, 0);
cudaGraphLaunch(graphExec, 0);
cudaDeviceSynchronize();
cudaGraphExecDestroy(graphExec);
cudaGraphDestroy(graph);
}
3.2.8.7.8.4. 条件 WHILE 节点
WHILE 节点的主体图将一直执行,直到条件为非零。条件将在节点执行时以及主体图完成后进行评估。下图描述了一个 3 节点图,其中中间节点 B 是一个条件节点

图 24 条件 WHILE 节点
以下代码说明了如何创建一个包含 WHILE 条件节点的图。句柄是使用 *cudaGraphCondAssignDefault* 创建的,以避免需要上游内核。条件的主体是使用 graph API 填充的。
__global__ void loopKernel(cudaGraphConditionalHandle handle)
{
static int count = 10;
cudaGraphSetConditional(handle, --count ? 1 : 0);
}
void graphSetup() {
cudaGraph_t graph;
cudaGraphExec_t graphExec;
cudaGraphNode_t node;
void *kernelArgs[1];
cuGraphCreate(&graph, 0);
cudaGraphConditionalHandle handle;
cudaGraphConditionalHandleCreate(&handle, graph, 1, cudaGraphCondAssignDefault);
cudaGraphNodeParams cParams = { cudaGraphNodeTypeConditional };
cParams.conditional.handle = handle;
cParams.conditional.type = cudaGraphCondTypeWhile;
cParams.conditional.size = 1;
cudaGraphAddNode(&node, graph, NULL, 0, &cParams);
cudaGraph_t bodyGraph = cParams.conditional.phGraph_out[0];
cudaGraphNodeParams params = { cudaGraphNodeTypeKernel };
params.kernel.func = (void *)loopKernel;
params.kernel.gridDim.x = params.kernel.gridDim.y = params.kernel.gridDim.z = 1;
params.kernel.blockDim.x = params.kernel.blockDim.y = params.kernel.blockDim.z = 1;
params.kernel.kernelParams = kernelArgs;
kernelArgs[0] = &handle;
cudaGraphAddNode(&node, bodyGraph, NULL, 0, ¶ms);
cudaGraphInstantiate(&graphExec, graph, NULL, NULL, 0);
cudaGraphLaunch(graphExec, 0);
cudaDeviceSynchronize();
cudaGraphExecDestroy(graphExec);
cudaGraphDestroy(graph);
}
3.2.8.8. 事件
运行时还提供了一种密切监视设备进度以及执行精确计时的方法,它允许应用程序在程序中的任何点异步记录*事件*,并查询这些事件何时完成。当事件之前的所有任务(或者,可选地,给定流中的所有命令)都已完成时,事件即完成。流零中的事件在所有流中所有先前的任务和命令都完成后完成。
3.2.8.8.1. 事件的创建和销毁
以下代码示例创建了两个事件
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
它们以这种方式销毁
cudaEventDestroy(start);
cudaEventDestroy(stop);
3.2.8.8.2. 经过时间
在 事件的创建和销毁 中创建的事件可以用于以以下方式计时 流的创建和销毁 的代码示例
cudaEventRecord(start, 0);
for (int i = 0; i < 2; ++i) {
cudaMemcpyAsync(inputDev + i * size, inputHost + i * size,
size, cudaMemcpyHostToDevice, stream[i]);
MyKernel<<<100, 512, 0, stream[i]>>>
(outputDev + i * size, inputDev + i * size, size);
cudaMemcpyAsync(outputHost + i * size, outputDev + i * size,
size, cudaMemcpyDeviceToHost, stream[i]);
}
cudaEventRecord(stop, 0);
cudaEventSynchronize(stop);
float elapsedTime;
cudaEventElapsedTime(&elapsedTime, start, stop);
3.2.8.9. 同步调用
当调用同步函数时,在设备完成请求的任务之前,控制不会返回到主机线程。主机线程随后是让步、阻塞还是自旋,可以通过在主机线程执行任何其他 CUDA 调用之前,使用一些特定标志调用 cudaSetDeviceFlags()
来指定(有关详细信息,请参阅参考手册)。
3.2.9. 多设备系统
3.2.9.1. 设备枚举
一个主机系统可以有多个设备。以下代码示例展示了如何枚举这些设备、查询其属性并确定启用 CUDA 的设备的数量。
int deviceCount;
cudaGetDeviceCount(&deviceCount);
int device;
for (device = 0; device < deviceCount; ++device) {
cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, device);
printf("Device %d has compute capability %d.%d.\n",
device, deviceProp.major, deviceProp.minor);
}
3.2.9.2. 设备选择
主机线程可以随时通过调用 cudaSetDevice()
来设置其操作的设备。设备内存分配和内核启动在当前设置的设备上进行;流和事件与当前设置的设备关联创建。如果没有调用 cudaSetDevice()
,则当前设备为设备 0。
以下代码示例说明了设置当前设备如何影响内存分配和内核执行。
size_t size = 1024 * sizeof(float);
cudaSetDevice(0); // Set device 0 as current
float* p0;
cudaMalloc(&p0, size); // Allocate memory on device 0
MyKernel<<<1000, 128>>>(p0); // Launch kernel on device 0
cudaSetDevice(1); // Set device 1 as current
float* p1;
cudaMalloc(&p1, size); // Allocate memory on device 1
MyKernel<<<1000, 128>>>(p1); // Launch kernel on device 1
3.2.9.3. 流和事件行为
如果内核启动发布到未与当前设备关联的流,则内核启动将失败,如下面的代码示例所示。
cudaSetDevice(0); // Set device 0 as current
cudaStream_t s0;
cudaStreamCreate(&s0); // Create stream s0 on device 0
MyKernel<<<100, 64, 0, s0>>>(); // Launch kernel on device 0 in s0
cudaSetDevice(1); // Set device 1 as current
cudaStream_t s1;
cudaStreamCreate(&s1); // Create stream s1 on device 1
MyKernel<<<100, 64, 0, s1>>>(); // Launch kernel on device 1 in s1
// This kernel launch will fail:
MyKernel<<<100, 64, 0, s0>>>(); // Launch kernel on device 1 in s0
即使内存复制发布到未与当前设备关联的流,它也会成功。
如果输入事件和输入流与不同的设备关联,则 cudaEventRecord()
将失败。
如果两个输入事件与不同的设备关联,则 cudaEventElapsedTime()
将失败。
即使输入事件与不同于当前设备的设备关联,cudaEventSynchronize()
和 cudaEventQuery()
也会成功。
即使输入流和输入事件与不同的设备关联,cudaStreamWaitEvent()
也会成功。cudaStreamWaitEvent()
因此可以用于在多个设备之间进行同步。
每个设备都有自己的默认流(参见 默认流),因此发布到设备默认流的命令可能会相对于发布到任何其他设备默认流的命令乱序或并发执行。
3.2.9.4. 点对点内存访问
根据系统属性,特别是 PCIe 和/或 NVLINK 拓扑,设备能够寻址彼此的内存(即,在一个设备上执行的内核可以取消引用指向另一个设备内存的指针)。如果 cudaDeviceCanAccessPeer()
对这两个设备返回 true,则这两个设备之间支持此点对点内存访问功能。
点对点内存访问仅在 64 位应用程序中受支持,并且必须通过调用 cudaDeviceEnablePeerAccess()
在两个设备之间启用,如下面的代码示例所示。在未启用 NVSwitch 的系统上,每个设备最多可以支持系统范围内的八个对等连接。
统一地址空间用于这两个设备(参见 统一虚拟地址空间),因此可以使用相同的指针来寻址来自这两个设备的内存,如下面的代码示例所示。
cudaSetDevice(0); // Set device 0 as current
float* p0;
size_t size = 1024 * sizeof(float);
cudaMalloc(&p0, size); // Allocate memory on device 0
MyKernel<<<1000, 128>>>(p0); // Launch kernel on device 0
cudaSetDevice(1); // Set device 1 as current
cudaDeviceEnablePeerAccess(0, 0); // Enable peer-to-peer access
// with device 0
// Launch kernel on device 1
// This kernel launch can access memory on device 0 at address p0
MyKernel<<<1000, 128>>>(p0);
3.2.9.4.1. Linux 上的 IOMMU
仅在 Linux 上,CUDA 和显示驱动程序不支持启用 IOMMU 的裸机 PCIe 点对点内存复制。但是,CUDA 和显示驱动程序通过 VM 直通支持 IOMMU。因此,在 Linux 上,当在原生裸机系统上运行时,用户应禁用 IOMMU。应启用 IOMMU,并将 VFIO 驱动程序用作虚拟机的 PCIe 直通。
在 Windows 上,上述限制不存在。
另请参阅 在 64 位平台上分配 DMA 缓冲区。
3.2.9.5. 点对点内存复制
内存复制可以在两个不同设备的内存之间执行。
当统一地址空间用于这两个设备时(参见 统一虚拟地址空间),这是使用 设备内存 中提到的常规内存复制函数完成的。
否则,这是使用 cudaMemcpyPeer()
、cudaMemcpyPeerAsync()
、cudaMemcpy3DPeer()
或 cudaMemcpy3DPeerAsync()
完成的,如下面的代码示例所示。
cudaSetDevice(0); // Set device 0 as current
float* p0;
size_t size = 1024 * sizeof(float);
cudaMalloc(&p0, size); // Allocate memory on device 0
cudaSetDevice(1); // Set device 1 as current
float* p1;
cudaMalloc(&p1, size); // Allocate memory on device 1
cudaSetDevice(0); // Set device 0 as current
MyKernel<<<1000, 128>>>(p0); // Launch kernel on device 0
cudaSetDevice(1); // Set device 1 as current
cudaMemcpyPeer(p1, 1, p0, 0, size); // Copy p0 to p1
MyKernel<<<1000, 128>>>(p1); // Launch kernel on device 1
两个不同设备内存之间的复制(在隐式 *NULL* 流中)
在先前发布到任一设备的所有命令完成之前不会开始,并且
在复制后发布到任一设备的任何命令(参见 异步并发执行)可以开始之前运行完成。
与流的正常行为一致,两个设备内存之间的异步复制可能与另一个流中的复制或内核重叠。
请注意,如果通过 cudaDeviceEnablePeerAccess()
在两个设备之间启用了点对点访问,如 点对点内存访问 中所述,则这两个设备之间的点对点内存复制不再需要通过主机进行暂存,因此速度更快。
3.2.10. 统一虚拟地址空间
当应用程序作为 64 位进程运行时,单个地址空间用于主机和所有计算能力为 2.0 及更高版本的设备。通过 CUDA API 调用进行的所有主机内存分配以及受支持设备上的所有设备内存分配都在此虚拟地址范围内。因此
可以使用
cudaPointerGetAttributes()
从指针的值确定通过 CUDA 分配的主机上的任何内存或使用统一地址空间的任何设备上的内存的位置。当复制到或从使用统一地址空间的任何设备的内存复制时,可以将
cudaMemcpy*()
的cudaMemcpyKind
参数设置为cudaMemcpyDefault
,以从指针确定位置。只要当前设备使用统一寻址,这也适用于非通过 CUDA 分配的主机指针。通过
cudaHostAlloc()
进行的分配在所有使用统一地址空间的设备之间自动是可移植的(参见 可移植内存),并且cudaHostAlloc()
返回的指针可以直接从在这些设备上运行的内核中使用(即,无需通过cudaHostGetDevicePointer()
获取设备指针,如 映射内存 中所述)。
应用程序可以通过检查 unifiedAddressing
设备属性(参见 设备枚举)是否等于 1,来查询统一地址空间是否用于特定设备。
3.2.11. 进程间通信
由主机线程创建的任何设备内存指针或事件句柄都可以由同一进程内的任何其他线程直接引用。但是,它在此进程之外无效,因此不能由属于不同进程的线程直接引用。
要跨进程共享设备内存指针和事件,应用程序必须使用进程间通信 API,该 API 在参考手册中详细描述。IPC API 仅在 Linux 上 для 64 位进程以及计算能力为 2.0 及更高版本的设备上受支持。请注意,IPC API 不支持 cudaMallocManaged
分配。
使用此 API,应用程序可以使用 cudaIpcGetMemHandle()
获取给定设备内存指针的 IPC 句柄,使用标准 IPC 机制(例如,进程间共享内存或文件)将其传递到另一个进程,并使用 cudaIpcOpenMemHandle()
从 IPC 句柄中检索设备指针,该句柄是此其他进程中的有效指针。事件句柄可以使用类似的入口点共享。
请注意,出于性能原因,通过 cudaMalloc()
进行的分配可能从更大的内存块中进行子分配。在这种情况下,CUDA IPC API 将共享整个底层内存块,这可能会导致其他子分配被共享,这可能会导致进程之间的信息泄露。为了防止这种行为,建议仅共享大小与 2MiB 对齐的分配。
使用 IPC API 的一个示例是,单个主进程生成一批输入数据,使数据可供多个辅助进程使用,而无需重新生成或复制。
使用 CUDA IPC 相互通信的应用程序应使用相同的 CUDA 驱动程序和运行时进行编译、链接和运行。
注意
自 CUDA 11.5 起,仅在 L4T 和计算能力为 7.x 及更高版本的嵌入式 Linux Tegra 设备上支持事件共享 IPC API。内存共享 IPC API 仍然在 Tegra 平台上不受支持。
3.2.12. 错误检查
所有运行时函数都返回一个错误代码,但对于异步函数(参见 异步并发执行),此错误代码不可能报告设备上可能发生的任何异步错误,因为该函数在设备完成任务之前返回;错误代码仅报告在执行任务之前主机上发生的错误,通常与参数验证有关;如果发生异步错误,它将由一些后续不相关的运行时函数调用报告。
因此,在某些异步函数调用之后立即检查异步错误的唯一方法是在调用之后立即通过调用 cudaDeviceSynchronize()
(或通过使用 异步并发执行 中描述的任何其他同步机制)进行同步,并检查 cudaDeviceSynchronize()
返回的错误代码。
运行时为每个主机线程维护一个错误变量,该变量初始化为 cudaSuccess
,并且每次发生错误时(无论是参数验证错误还是异步错误)都会被错误代码覆盖。cudaPeekAtLastError()
返回此变量。cudaGetLastError()
返回此变量并将其重置为 cudaSuccess
。
内核启动不返回任何错误代码,因此必须在内核启动后立即调用 cudaPeekAtLastError()
或 cudaGetLastError()
以检索任何预启动错误。为了确保 cudaPeekAtLastError()
或 cudaGetLastError()
返回的任何错误不是源于内核启动之前的调用,必须确保在内核启动之前将运行时错误变量设置为 cudaSuccess
,例如,通过在内核启动之前调用 cudaGetLastError()
。内核启动是异步的,因此要检查异步错误,应用程序必须在内核启动和调用 cudaPeekAtLastError()
或 cudaGetLastError()
之间进行同步。
请注意,cudaErrorNotReady
可能由 cudaStreamQuery()
和 cudaEventQuery()
返回,但不被视为错误,因此不会由 cudaPeekAtLastError()
或 cudaGetLastError()
报告。
3.2.13. 调用堆栈
在计算能力为 2.x 及更高版本的设备上,可以使用 cudaDeviceGetLimit()
查询调用堆栈的大小,并使用 cudaDeviceSetLimit()
设置调用堆栈的大小。
当调用堆栈溢出时,如果应用程序通过 CUDA 调试器(CUDA-GDB、Nsight)运行,则内核调用将失败并出现堆栈溢出错误,否则将出现未指定的启动错误。当编译器无法确定堆栈大小时,它会发出警告,提示“堆栈大小无法静态确定”。这通常发生在递归函数的情况下。一旦发出此警告,如果默认堆栈大小不足,用户将需要手动设置堆栈大小。
3.2.14. 纹理和表面内存
CUDA 支持 GPU 用于图形的纹理硬件的子集,以访问纹理和表面内存。从纹理或表面内存而不是全局内存读取数据可以带来多种性能优势,如 设备内存访问 中所述。
3.2.14.1. 纹理内存
纹理内存使用 纹理函数 中描述的设备函数从内核读取。调用这些函数之一读取纹理的过程称为*纹理提取*。每个纹理提取都为纹理对象 API 指定一个名为*纹理对象*的参数。
纹理对象指定
*纹理*,它是要提取的纹理内存片段。纹理对象在运行时创建,纹理在创建纹理对象时指定,如 纹理对象 API 中所述。
它的*维度*,它指定纹理是以一维数组(使用一个纹理坐标)、二维数组(使用两个纹理坐标)还是三维数组(使用三个纹理坐标)寻址。数组的元素称为 *texels*,是 *texture elements* 的缩写。*纹理宽度*、*高度*和*深度*是指数组在每个维度上的大小。表 21 <features-and-technical-specifications-technical-specifications-per-compute-capability> 列出了取决于设备计算能力的最大纹理宽度、高度和深度。
纹素的类型,它被限制为基本整数和单精度浮点类型,以及 内置向量类型 中定义的任何 1、2 和 4 分量向量类型,这些类型是从基本整数和单精度浮点类型派生的。
*读取模式*,它等于
cudaReadModeNormalizedFloat
或cudaReadModeElementType
。如果它是cudaReadModeNormalizedFloat
且纹素的类型是 16 位或 8 位整数类型,则纹理提取返回的值实际上作为浮点类型返回,并且整数类型的完整范围映射到无符号整数类型的 [0.0, 1.0] 和有符号整数类型的 [-1.0, 1.0];例如,值为 0xff 的无符号 8 位纹理元素读取为 1。如果它是cudaReadModeElementType
,则不执行转换。纹理坐标是否已归一化。默认情况下,纹理使用浮点坐标在范围 [0, N-1] 内引用(通过 纹理函数 的函数),其中 N 是纹理在与坐标对应的维度中的大小。例如,大小为 64x32 的纹理将分别使用范围 [0, 63] 和 [0, 31] 中的坐标引用 x 维度和 y 维度。归一化纹理坐标导致坐标在范围 [0.0, 1.0-1/N] 而不是 [0, N-1] 中指定,因此相同的 64x32 纹理将通过范围 [0, 1-1/N] 中 x 维度和 y 维度中的归一化坐标寻址。如果希望纹理坐标独立于纹理大小,则归一化纹理坐标自然适合某些应用程序的要求。
寻址模式。可以调用 B.8 节中描述的设备函数,并使用超出范围的坐标。寻址模式定义了在这种情况下会发生什么。默认的寻址模式是将坐标钳制到有效范围内:对于非归一化坐标为 [0, N),对于归一化坐标为 [0.0, 1.0)。如果指定了边框模式,则使用超出范围的纹理坐标进行的纹理获取将返回零。对于归一化坐标,还提供环绕模式和镜像模式。当使用环绕模式时,每个坐标 x 都被转换为 frac(x)=x - floor(x),其中 floor(x) 是不大于 x 的最大整数。当使用镜像模式时,如果 floor(x) 为偶数,则每个坐标 x 都被转换为 frac(x),如果 floor(x) 为奇数,则转换为 1-frac(x)。寻址模式被指定为一个大小为三的数组,其第一个、第二个和第三个元素分别指定第一个、第二个和第三个纹理坐标的寻址模式;寻址模式为
cudaAddressModeBorder
、cudaAddressModeClamp
、cudaAddressModeWrap
和cudaAddressModeMirror
;cudaAddressModeWrap
和cudaAddressModeMirror
仅支持归一化纹理坐标。滤波模式,它指定在获取纹理时,如何根据输入的纹理坐标计算返回的值。线性纹理滤波只能用于配置为返回浮点数据的纹理。它在相邻纹素之间执行低精度插值。启用后,将读取纹理获取位置周围的纹素,并根据纹理坐标落在纹素之间的位置对纹理获取的返回值进行插值。对于一维纹理执行简单的线性插值,对于二维纹理执行双线性插值,对于三维纹理执行三线性插值。纹理获取 提供了关于纹理获取的更多细节。滤波模式等于
cudaFilterModePoint
或cudaFilterModeLinear
。如果它是cudaFilterModePoint
,则返回的值是其纹理坐标最接近输入纹理坐标的纹素。如果它是cudaFilterModeLinear
,则返回的值是两个(对于一维纹理)、四个(对于二维纹理)或八个(对于三维纹理)纹素的线性插值,这些纹素的纹理坐标最接近输入纹理坐标。cudaFilterModeLinear
仅对浮点类型的返回值有效。
纹理对象 API 介绍了纹理对象 API。
16 位浮点纹理 解释了如何处理 16 位浮点纹理。
纹理也可以分层,如 分层纹理 中所述。
立方体贴图纹理 和 立方体贴图分层纹理 描述了一种特殊的纹理类型,即立方体贴图纹理。
纹理Gather 描述了一种特殊的纹理获取,即纹理 gather。
3.2.14.1.1. 纹理对象 API
纹理对象是使用 cudaCreateTextureObject()
从类型为 struct cudaResourceDesc
的资源描述创建的,该资源描述指定了纹理,并从如下定义的纹理描述创建:
struct cudaTextureDesc
{
enum cudaTextureAddressMode addressMode[3];
enum cudaTextureFilterMode filterMode;
enum cudaTextureReadMode readMode;
int sRGB;
int normalizedCoords;
unsigned int maxAnisotropy;
enum cudaTextureFilterMode mipmapFilterMode;
float mipmapLevelBias;
float minMipmapLevelClamp;
float maxMipmapLevelClamp;
};
addressMode
指定寻址模式;filterMode
指定滤波模式;readMode
指定读取模式;normalizedCoords
指定纹理坐标是否归一化;有关
sRGB
、maxAnisotropy
、mipmapFilterMode
、mipmapLevelBias
、minMipmapLevelClamp
和maxMipmapLevelClamp
,请参阅参考手册。
以下代码示例将一些简单的转换内核应用于纹理。
// Simple transformation kernel
__global__ void transformKernel(float* output,
cudaTextureObject_t texObj,
int width, int height,
float theta)
{
// Calculate normalized texture coordinates
unsigned int x = blockIdx.x * blockDim.x + threadIdx.x;
unsigned int y = blockIdx.y * blockDim.y + threadIdx.y;
float u = x / (float)width;
float v = y / (float)height;
// Transform coordinates
u -= 0.5f;
v -= 0.5f;
float tu = u * cosf(theta) - v * sinf(theta) + 0.5f;
float tv = v * cosf(theta) + u * sinf(theta) + 0.5f;
// Read from texture and write to global memory
output[y * width + x] = tex2D<float>(texObj, tu, tv);
}
// Host code
int main()
{
const int height = 1024;
const int width = 1024;
float angle = 0.5;
// Allocate and set some host data
float *h_data = (float *)std::malloc(sizeof(float) * width * height);
for (int i = 0; i < height * width; ++i)
h_data[i] = i;
// Allocate CUDA array in device memory
cudaChannelFormatDesc channelDesc =
cudaCreateChannelDesc(32, 0, 0, 0, cudaChannelFormatKindFloat);
cudaArray_t cuArray;
cudaMallocArray(&cuArray, &channelDesc, width, height);
// Set pitch of the source (the width in memory in bytes of the 2D array pointed
// to by src, including padding), we dont have any padding
const size_t spitch = width * sizeof(float);
// Copy data located at address h_data in host memory to device memory
cudaMemcpy2DToArray(cuArray, 0, 0, h_data, spitch, width * sizeof(float),
height, cudaMemcpyHostToDevice);
// Specify texture
struct cudaResourceDesc resDesc;
memset(&resDesc, 0, sizeof(resDesc));
resDesc.resType = cudaResourceTypeArray;
resDesc.res.array.array = cuArray;
// Specify texture object parameters
struct cudaTextureDesc texDesc;
memset(&texDesc, 0, sizeof(texDesc));
texDesc.addressMode[0] = cudaAddressModeWrap;
texDesc.addressMode[1] = cudaAddressModeWrap;
texDesc.filterMode = cudaFilterModeLinear;
texDesc.readMode = cudaReadModeElementType;
texDesc.normalizedCoords = 1;
// Create texture object
cudaTextureObject_t texObj = 0;
cudaCreateTextureObject(&texObj, &resDesc, &texDesc, NULL);
// Allocate result of transformation in device memory
float *output;
cudaMalloc(&output, width * height * sizeof(float));
// Invoke kernel
dim3 threadsperBlock(16, 16);
dim3 numBlocks((width + threadsperBlock.x - 1) / threadsperBlock.x,
(height + threadsperBlock.y - 1) / threadsperBlock.y);
transformKernel<<<numBlocks, threadsperBlock>>>(output, texObj, width, height,
angle);
// Copy data from device back to host
cudaMemcpy(h_data, output, width * height * sizeof(float),
cudaMemcpyDeviceToHost);
// Destroy texture object
cudaDestroyTextureObject(texObj);
// Free device memory
cudaFreeArray(cuArray);
cudaFree(output);
// Free host memory
free(h_data);
return 0;
}
3.2.14.1.2. 16 位浮点纹理
CUDA 数组支持的 16 位浮点或 半精度 格式与 IEEE 754-2008 binary16 格式相同。
CUDA C++ 不支持匹配的数据类型,但提供了通过 unsigned short
类型转换为和从 32 位浮点格式转换的内在函数:__float2half_rn(float)
和 __half2float(unsigned short)
。这些函数仅在设备代码中受支持。主机代码的等效函数可以在 OpenEXR 库中找到,例如。
16 位浮点分量在纹理获取期间提升为 32 位浮点数,然后在执行任何滤波之前。
可以通过调用 cudaCreateChannelDescHalf*()
函数之一来创建 16 位浮点格式的通道描述。
3.2.14.1.3. 分层纹理
一维或二维分层纹理(在 Direct3D 中也称为纹理数组,在 OpenGL 中也称为数组纹理)是由一系列层组成的纹理,所有层都是具有相同维度、大小和数据类型的规则纹理。
一维分层纹理使用整数索引和浮点纹理坐标寻址;索引表示序列中的层,坐标寻址该层中的纹素。二维分层纹理使用整数索引和两个浮点纹理坐标寻址;索引表示序列中的层,坐标寻址该层中的纹素。
分层纹理只能通过调用带有 cudaArrayLayered
标志的 cudaMalloc3DArray()
(对于一维分层纹理,高度为零)成为 CUDA 数组。
分层纹理使用 tex1DLayered() 和 tex2DLayered() 中描述的设备函数获取。纹理滤波(参见 纹理获取)仅在层内完成,而不是跨层完成。
分层纹理仅在计算能力为 2.0 及更高版本的设备上受支持。
3.2.14.1.4. 立方体贴图纹理
立方体贴图纹理是一种特殊的二维分层纹理,它有六层,表示立方体的六个面
层的宽度等于其高度。
立方体贴图使用三个纹理坐标 x、y 和 z 寻址,这些坐标被解释为从立方体中心发出的方向向量,指向立方体的一个面以及与该面对应的层中的纹素。更具体地说,通过具有最大幅值 m 的坐标选择面,并且使用坐标 (s/m+1)/2 和 (t/m+1)/2 寻址相应的层,其中 s 和 t 在 表 3 中定义。
面 |
m |
s |
t |
||
---|---|---|---|---|---|
|
x ≥ 0 |
0 |
x |
-z |
-y |
x < 0 |
1 |
-x |
z |
-y |
|
|
y ≥ 0 |
2 |
y |
x |
z |
y < 0 |
3 |
-y |
x |
-z |
|
|
z ≥ 0 |
4 |
z |
x |
-y |
z < 0 |
5 |
-z |
-x |
-y |
立方体贴图纹理只能通过调用带有 cudaArrayCubemap
标志的 cudaMalloc3DArray()
成为 CUDA 数组。
立方体贴图纹理使用 texCubemap() 中描述的设备函数获取。
立方体贴图纹理仅在计算能力为 2.0 及更高版本的设备上受支持。
3.2.14.1.5. 立方体贴图分层纹理
立方体贴图分层纹理是一种分层纹理,其层是具有相同维度的立方体贴图。
立方体贴图分层纹理使用整数索引和三个浮点纹理坐标寻址;索引表示序列中的立方体贴图,坐标寻址该立方体贴图中的纹素。
立方体贴图分层纹理只能通过调用带有 cudaArrayLayered
和 cudaArrayCubemap
标志的 cudaMalloc3DArray()
成为 CUDA 数组。
立方体贴图分层纹理使用 texCubemapLayered() 中描述的设备函数获取。纹理滤波(参见 纹理获取)仅在层内完成,而不是跨层完成。
立方体贴图分层纹理仅在计算能力为 2.0 及更高版本的设备上受支持。
3.2.14.1.6. 纹理 Gather
纹理 gather 是一种特殊的纹理获取,仅适用于二维纹理。它由 tex2Dgather()
函数执行,该函数具有与 tex2D()
相同的参数,外加一个额外的 comp
参数,该参数等于 0、1、2 或 3(参见 tex2Dgather())。它返回四个 32 位数字,这些数字对应于在常规纹理获取期间用于双线性滤波的四个纹素中每个纹素的 comp
分量的值。例如,如果这些纹素的值为 (253, 20, 31, 255)、(250, 25, 29, 254)、(249, 16, 37, 253)、(251, 22, 30, 250),并且 comp
为 2,则 tex2Dgather()
返回 (31, 29, 37, 30)。
请注意,纹理坐标的计算精度仅为 8 位小数。tex2Dgather()
对于 tex2D()
将权重之一(α 或 β,参见 线性滤波)设置为 1.0 的情况,可能会返回意外的结果。例如,对于 x 纹理坐标 2.49805:xB=x-0.5=1.99805,但是 xB 的小数部分以 8 位定点格式存储。由于 0.99805 比 255.f/256.f 更接近 256.f/256.f,因此 xB 的值为 2。在这种情况下,tex2Dgather()
将返回 x 中的索引 2 和 3,而不是索引 1 和 2。
纹理 gather 仅支持使用 cudaArrayTextureGather
标志创建且宽度和高度小于 表 21 中为纹理 gather 指定的最大值的 CUDA 数组,该最大值小于常规纹理获取的最大值。
纹理 gather 仅在计算能力为 2.0 及更高版本的设备上受支持。
3.2.14.2. 表面内存
对于计算能力为 2.0 及更高版本的设备,使用 cudaArraySurfaceLoadStore
标志创建的 CUDA 数组(在 立方体贴图表面 中描述)可以通过表面对象使用 表面函数 中描述的函数进行读取和写入。
表 21 列出了最大表面宽度、高度和深度,具体取决于设备的计算能力。
3.2.14.2.1. 表面对象 API
表面对象是使用 cudaCreateSurfaceObject()
从类型为 struct cudaResourceDesc
的资源描述创建的。与纹理内存不同,表面内存使用字节寻址。这意味着通过纹理函数访问纹理元素的 x 坐标需要乘以元素的大小(以字节为单位),才能通过表面函数访问相同的元素。例如,绑定到纹理对象 texObj
和表面对象 surfObj
的一维浮点 CUDA 数组的纹理坐标 x 处的元素,通过 texObj
使用 tex1d(texObj, x)
读取,但通过 surfObj
使用 surf1Dread(surfObj, 4*x)
读取。类似地,绑定到纹理对象 texObj
和表面对象 surfObj
的二维浮点 CUDA 数组的纹理坐标 x 和 y 处的元素,通过 texObj
使用 tex2d(texObj, x, y)
访问,但通过 surObj
使用 surf2Dread(surfObj, 4*x, y)
访问(y 坐标的字节偏移量在内部从 CUDA 数组的底层行距计算得出)。
以下代码示例将一些简单的转换内核应用于表面。
// Simple copy kernel
__global__ void copyKernel(cudaSurfaceObject_t inputSurfObj,
cudaSurfaceObject_t outputSurfObj,
int width, int height)
{
// Calculate surface coordinates
unsigned int x = blockIdx.x * blockDim.x + threadIdx.x;
unsigned int y = blockIdx.y * blockDim.y + threadIdx.y;
if (x < width && y < height) {
uchar4 data;
// Read from input surface
surf2Dread(&data, inputSurfObj, x * 4, y);
// Write to output surface
surf2Dwrite(data, outputSurfObj, x * 4, y);
}
}
// Host code
int main()
{
const int height = 1024;
const int width = 1024;
// Allocate and set some host data
unsigned char *h_data =
(unsigned char *)std::malloc(sizeof(unsigned char) * width * height * 4);
for (int i = 0; i < height * width * 4; ++i)
h_data[i] = i;
// Allocate CUDA arrays in device memory
cudaChannelFormatDesc channelDesc =
cudaCreateChannelDesc(8, 8, 8, 8, cudaChannelFormatKindUnsigned);
cudaArray_t cuInputArray;
cudaMallocArray(&cuInputArray, &channelDesc, width, height,
cudaArraySurfaceLoadStore);
cudaArray_t cuOutputArray;
cudaMallocArray(&cuOutputArray, &channelDesc, width, height,
cudaArraySurfaceLoadStore);
// Set pitch of the source (the width in memory in bytes of the 2D array
// pointed to by src, including padding), we dont have any padding
const size_t spitch = 4 * width * sizeof(unsigned char);
// Copy data located at address h_data in host memory to device memory
cudaMemcpy2DToArray(cuInputArray, 0, 0, h_data, spitch,
4 * width * sizeof(unsigned char), height,
cudaMemcpyHostToDevice);
// Specify surface
struct cudaResourceDesc resDesc;
memset(&resDesc, 0, sizeof(resDesc));
resDesc.resType = cudaResourceTypeArray;
// Create the surface objects
resDesc.res.array.array = cuInputArray;
cudaSurfaceObject_t inputSurfObj = 0;
cudaCreateSurfaceObject(&inputSurfObj, &resDesc);
resDesc.res.array.array = cuOutputArray;
cudaSurfaceObject_t outputSurfObj = 0;
cudaCreateSurfaceObject(&outputSurfObj, &resDesc);
// Invoke kernel
dim3 threadsperBlock(16, 16);
dim3 numBlocks((width + threadsperBlock.x - 1) / threadsperBlock.x,
(height + threadsperBlock.y - 1) / threadsperBlock.y);
copyKernel<<<numBlocks, threadsperBlock>>>(inputSurfObj, outputSurfObj, width,
height);
// Copy data from device back to host
cudaMemcpy2DFromArray(h_data, spitch, cuOutputArray, 0, 0,
4 * width * sizeof(unsigned char), height,
cudaMemcpyDeviceToHost);
// Destroy surface objects
cudaDestroySurfaceObject(inputSurfObj);
cudaDestroySurfaceObject(outputSurfObj);
// Free device memory
cudaFreeArray(cuInputArray);
cudaFreeArray(cuOutputArray);
// Free host memory
free(h_data);
return 0;
}
3.2.14.2.2. 立方体贴图表面
立方体贴图表面使用 surfCubemapread()
和 surfCubemapwrite()
(surfCubemapread() 和 surfCubemapwrite())作为二维分层表面访问,即使用整数索引表示面,并使用两个浮点纹理坐标寻址与该面对应的层中的纹素。面的顺序如 表 3 所示。
3.2.14.2.3. 立方体贴图分层表面
立方体贴图分层表面使用 surfCubemapLayeredread()
和 surfCubemapLayeredwrite()
(surfCubemapLayeredread() 和 surfCubemapLayeredwrite())作为二维分层表面访问,即使用整数索引表示立方体贴图的面之一,并使用两个浮点纹理坐标寻址与该面对应的层中的纹素。面的顺序如 表 3 所示,例如,索引 ((2 * 6) + 3) 访问第三个立方体贴图的第四个面。
3.2.14.3. CUDA 数组
CUDA 数组是为纹理获取优化的不透明内存布局。它们是一维、二维或三维的,由元素组成,每个元素具有 1、2 或 4 个分量,这些分量可以是带符号或无符号的 8 位、16 位或 32 位整数、16 位浮点数或 32 位浮点数。CUDA 数组只能通过内核通过 纹理内存 中描述的纹理获取或 表面内存 中描述的表面读取和写入来访问。
3.2.14.4. 读/写一致性
纹理和表面内存被缓存(参见 设备内存访问),并且在同一个内核调用中,缓存不会与全局内存写入和表面内存写入保持一致,因此,在同一个内核调用中,任何纹理获取或表面读取到已通过全局写入或表面写入写入的地址都将返回未定义的数据。换句话说,线程只有在该内存位置已由先前的内核调用或内存复制更新时,才能安全地读取某些纹理或表面内存位置,但如果该内存位置先前已由同一线程或来自同一内核调用的另一个线程更新,则不能安全读取。
3.2.15. 图形互操作性
来自 OpenGL 和 Direct3D 的某些资源可以映射到 CUDA 的地址空间中,以使 CUDA 能够读取由 OpenGL 或 Direct3D 写入的数据,或者使 CUDA 能够写入供 OpenGL 或 Direct3D 使用的数据。
资源必须先注册到 CUDA,然后才能使用 OpenGL 互操作性 和 Direct3D 互操作性 中提到的函数进行映射。这些函数返回指向类型为 struct cudaGraphicsResource
的 CUDA 图形资源的指针。注册资源可能开销很高,因此通常每个资源只调用一次。CUDA 图形资源使用 cudaGraphicsUnregisterResource()
注销。每个打算使用资源的 CUDA 上下文都需要单独注册它。
一旦资源注册到 CUDA,就可以根据需要多次使用 cudaGraphicsMapResources()
和 cudaGraphicsUnmapResources()
进行映射和取消映射。cudaGraphicsResourceSetMapFlags()
可以被调用以指定 CUDA 驱动程序可以用来优化资源管理的使用提示(只写、只读)。
映射的资源可以由内核读取或写入,使用 cudaGraphicsResourceGetMappedPointer()
为缓冲区返回的设备内存地址和 cudaGraphicsSubResourceGetMappedArray()
为 CUDA 数组返回的设备内存地址。
当资源被映射时,通过 OpenGL、Direct3D 或另一个 CUDA 上下文访问该资源会产生未定义的结果。OpenGL 互操作性 和 Direct3D 互操作性 给出了每个图形 API 的具体细节和一些代码示例。SLI 互操作性 给出了系统处于 SLI 模式时的具体细节。
3.2.15.1. OpenGL 互操作性
可以映射到 CUDA 地址空间中的 OpenGL 资源是 OpenGL 缓冲区、纹理和渲染缓冲区对象。
缓冲区对象使用 cudaGraphicsGLRegisterBuffer()
注册。在 CUDA 中,它显示为设备指针,因此可以由内核读取和写入,或者通过 cudaMemcpy()
调用读取和写入。
纹理或渲染缓冲区对象使用 cudaGraphicsGLRegisterImage()
注册。在 CUDA 中,它显示为 CUDA 数组。内核可以通过将其绑定到纹理或表面引用来从数组中读取数据。如果资源已使用 cudaGraphicsRegisterFlagsSurfaceLoadStore
标志注册,它们也可以通过表面写入函数写入数组。数组也可以通过 cudaMemcpy2D()
调用读取和写入。cudaGraphicsGLRegisterImage()
支持所有具有 1、2 或 4 个分量以及浮点内部类型(例如,GL_RGBA_FLOAT32
)、归一化整数(例如,GL_RGBA8, GL_INTENSITY16
)和非归一化整数(例如,GL_RGBA8UI
)的纹理格式(请注意,由于非归一化整数格式需要 OpenGL 3.0,因此它们只能由着色器写入,而不能由固定功能管线写入)。
共享其资源的 OpenGL 上下文必须是调用任何 OpenGL 互操作性 API 的主机线程的当前上下文。
请注意:当 OpenGL 纹理变为无绑定(例如,通过使用 glGetTextureHandle
*/glGetImageHandle
* API 请求图像或纹理句柄时),它不能在 CUDA 中注册。应用程序需要在请求图像或纹理句柄之前注册纹理以进行互操作。
以下代码示例使用内核动态修改存储在顶点缓冲区对象中的 width
x height
顶点网格
GLuint positionsVBO;
struct cudaGraphicsResource* positionsVBO_CUDA;
int main()
{
// Initialize OpenGL and GLUT for device 0
// and make the OpenGL context current
...
glutDisplayFunc(display);
// Explicitly set device 0
cudaSetDevice(0);
// Create buffer object and register it with CUDA
glGenBuffers(1, &positionsVBO);
glBindBuffer(GL_ARRAY_BUFFER, positionsVBO);
unsigned int size = width * height * 4 * sizeof(float);
glBufferData(GL_ARRAY_BUFFER, size, 0, GL_DYNAMIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
cudaGraphicsGLRegisterBuffer(&positionsVBO_CUDA,
positionsVBO,
cudaGraphicsMapFlagsWriteDiscard);
// Launch rendering loop
glutMainLoop();
...
}
void display()
{
// Map buffer object for writing from CUDA
float4* positions;
cudaGraphicsMapResources(1, &positionsVBO_CUDA, 0);
size_t num_bytes;
cudaGraphicsResourceGetMappedPointer((void**)&positions,
&num_bytes,
positionsVBO_CUDA));
// Execute kernel
dim3 dimBlock(16, 16, 1);
dim3 dimGrid(width / dimBlock.x, height / dimBlock.y, 1);
createVertices<<<dimGrid, dimBlock>>>(positions, time,
width, height);
// Unmap buffer object
cudaGraphicsUnmapResources(1, &positionsVBO_CUDA, 0);
// Render from buffer object
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glBindBuffer(GL_ARRAY_BUFFER, positionsVBO);
glVertexPointer(4, GL_FLOAT, 0, 0);
glEnableClientState(GL_VERTEX_ARRAY);
glDrawArrays(GL_POINTS, 0, width * height);
glDisableClientState(GL_VERTEX_ARRAY);
// Swap buffers
glutSwapBuffers();
glutPostRedisplay();
}
void deleteVBO()
{
cudaGraphicsUnregisterResource(positionsVBO_CUDA);
glDeleteBuffers(1, &positionsVBO);
}
__global__ void createVertices(float4* positions, float time,
unsigned int width, unsigned int height)
{
unsigned int x = blockIdx.x * blockDim.x + threadIdx.x;
unsigned int y = blockIdx.y * blockDim.y + threadIdx.y;
// Calculate uv coordinates
float u = x / (float)width;
float v = y / (float)height;
u = u * 2.0f - 1.0f;
v = v * 2.0f - 1.0f;
// calculate simple sine wave pattern
float freq = 4.0f;
float w = sinf(u * freq + time)
* cosf(v * freq + time) * 0.5f;
// Write positions
positions[y * width + x] = make_float4(u, w, v, 1.0f);
}
在 Windows 和 Quadro GPU 上,可以使用 cudaWGLGetDevice()
来检索与 wglEnumGpusNV()
返回的句柄关联的 CUDA 设备。在多 GPU 配置中,Quadro GPU 提供比 GeForce 和 Tesla GPU 更高性能的 OpenGL 互操作性,其中 OpenGL 渲染在 Quadro GPU 上执行,CUDA 计算在系统中的其他 GPU 上执行。
3.2.15.2. Direct3D 互操作性
Direct3D 互操作性支持 Direct3D 9Ex、Direct3D 10 和 Direct3D 11。
CUDA 上下文只能与满足以下条件的 Direct3D 设备互操作:Direct3D 9Ex 设备必须使用设置为 D3DDEVTYPE_HAL
的 DeviceType
和带有 D3DCREATE_HARDWARE_VERTEXPROCESSING
标志的 BehaviorFlags
创建;Direct3D 10 和 Direct3D 11 设备必须使用设置为 D3D_DRIVER_TYPE_HARDWARE
的 DriverType
创建。
可以映射到 CUDA 地址空间中的 Direct3D 资源是 Direct3D 缓冲区、纹理和表面。这些资源使用 cudaGraphicsD3D9RegisterResource()
、cudaGraphicsD3D10RegisterResource()
和 cudaGraphicsD3D11RegisterResource()
注册。
以下代码示例使用内核动态修改存储在顶点缓冲区对象中的 width
x height
顶点网格。
3.2.15.2.1. Direct3D 9 版本
IDirect3D9* D3D;
IDirect3DDevice9* device;
struct CUSTOMVERTEX {
FLOAT x, y, z;
DWORD color;
};
IDirect3DVertexBuffer9* positionsVB;
struct cudaGraphicsResource* positionsVB_CUDA;
int main()
{
int dev;
// Initialize Direct3D
D3D = Direct3DCreate9Ex(D3D_SDK_VERSION);
// Get a CUDA-enabled adapter
unsigned int adapter = 0;
for (; adapter < g_pD3D->GetAdapterCount(); adapter++) {
D3DADAPTER_IDENTIFIER9 adapterId;
g_pD3D->GetAdapterIdentifier(adapter, 0, &adapterId);
if (cudaD3D9GetDevice(&dev, adapterId.DeviceName)
== cudaSuccess)
break;
}
// Create device
...
D3D->CreateDeviceEx(adapter, D3DDEVTYPE_HAL, hWnd,
D3DCREATE_HARDWARE_VERTEXPROCESSING,
¶ms, NULL, &device);
// Use the same device
cudaSetDevice(dev);
// Create vertex buffer and register it with CUDA
unsigned int size = width * height * sizeof(CUSTOMVERTEX);
device->CreateVertexBuffer(size, 0, D3DFVF_CUSTOMVERTEX,
D3DPOOL_DEFAULT, &positionsVB, 0);
cudaGraphicsD3D9RegisterResource(&positionsVB_CUDA,
positionsVB,
cudaGraphicsRegisterFlagsNone);
cudaGraphicsResourceSetMapFlags(positionsVB_CUDA,
cudaGraphicsMapFlagsWriteDiscard);
// Launch rendering loop
while (...) {
...
Render();
...
}
...
}
void Render()
{
// Map vertex buffer for writing from CUDA
float4* positions;
cudaGraphicsMapResources(1, &positionsVB_CUDA, 0);
size_t num_bytes;
cudaGraphicsResourceGetMappedPointer((void**)&positions,
&num_bytes,
positionsVB_CUDA));
// Execute kernel
dim3 dimBlock(16, 16, 1);
dim3 dimGrid(width / dimBlock.x, height / dimBlock.y, 1);
createVertices<<<dimGrid, dimBlock>>>(positions, time,
width, height);
// Unmap vertex buffer
cudaGraphicsUnmapResources(1, &positionsVB_CUDA, 0);
// Draw and present
...
}
void releaseVB()
{
cudaGraphicsUnregisterResource(positionsVB_CUDA);
positionsVB->Release();
}
__global__ void createVertices(float4* positions, float time,
unsigned int width, unsigned int height)
{
unsigned int x = blockIdx.x * blockDim.x + threadIdx.x;
unsigned int y = blockIdx.y * blockDim.y + threadIdx.y;
// Calculate uv coordinates
float u = x / (float)width;
float v = y / (float)height;
u = u * 2.0f - 1.0f;
v = v * 2.0f - 1.0f;
// Calculate simple sine wave pattern
float freq = 4.0f;
float w = sinf(u * freq + time)
* cosf(v * freq + time) * 0.5f;
// Write positions
positions[y * width + x] =
make_float4(u, w, v, __int_as_float(0xff00ff00));
}
3.2.15.2.2. Direct3D 10 版本
ID3D10Device* device;
struct CUSTOMVERTEX {
FLOAT x, y, z;
DWORD color;
};
ID3D10Buffer* positionsVB;
struct cudaGraphicsResource* positionsVB_CUDA;
int main()
{
int dev;
// Get a CUDA-enabled adapter
IDXGIFactory* factory;
CreateDXGIFactory(__uuidof(IDXGIFactory), (void**)&factory);
IDXGIAdapter* adapter = 0;
for (unsigned int i = 0; !adapter; ++i) {
if (FAILED(factory->EnumAdapters(i, &adapter))
break;
if (cudaD3D10GetDevice(&dev, adapter) == cudaSuccess)
break;
adapter->Release();
}
factory->Release();
// Create swap chain and device
...
D3D10CreateDeviceAndSwapChain(adapter,
D3D10_DRIVER_TYPE_HARDWARE, 0,
D3D10_CREATE_DEVICE_DEBUG,
D3D10_SDK_VERSION,
&swapChainDesc, &swapChain,
&device);
adapter->Release();
// Use the same device
cudaSetDevice(dev);
// Create vertex buffer and register it with CUDA
unsigned int size = width * height * sizeof(CUSTOMVERTEX);
D3D10_BUFFER_DESC bufferDesc;
bufferDesc.Usage = D3D10_USAGE_DEFAULT;
bufferDesc.ByteWidth = size;
bufferDesc.BindFlags = D3D10_BIND_VERTEX_BUFFER;
bufferDesc.CPUAccessFlags = 0;
bufferDesc.MiscFlags = 0;
device->CreateBuffer(&bufferDesc, 0, &positionsVB);
cudaGraphicsD3D10RegisterResource(&positionsVB_CUDA,
positionsVB,
cudaGraphicsRegisterFlagsNone);
cudaGraphicsResourceSetMapFlags(positionsVB_CUDA,
cudaGraphicsMapFlagsWriteDiscard);
// Launch rendering loop
while (...) {
...
Render();
...
}
...
}
void Render()
{
// Map vertex buffer for writing from CUDA
float4* positions;
cudaGraphicsMapResources(1, &positionsVB_CUDA, 0);
size_t num_bytes;
cudaGraphicsResourceGetMappedPointer((void**)&positions,
&num_bytes,
positionsVB_CUDA));
// Execute kernel
dim3 dimBlock(16, 16, 1);
dim3 dimGrid(width / dimBlock.x, height / dimBlock.y, 1);
createVertices<<<dimGrid, dimBlock>>>(positions, time,
width, height);
// Unmap vertex buffer
cudaGraphicsUnmapResources(1, &positionsVB_CUDA, 0);
// Draw and present
...
}
void releaseVB()
{
cudaGraphicsUnregisterResource(positionsVB_CUDA);
positionsVB->Release();
}
__global__ void createVertices(float4* positions, float time,
unsigned int width, unsigned int height)
{
unsigned int x = blockIdx.x * blockDim.x + threadIdx.x;
unsigned int y = blockIdx.y * blockDim.y + threadIdx.y;
// Calculate uv coordinates
float u = x / (float)width;
float v = y / (float)height;
u = u * 2.0f - 1.0f;
v = v * 2.0f - 1.0f;
// Calculate simple sine wave pattern
float freq = 4.0f;
float w = sinf(u * freq + time)
* cosf(v * freq + time) * 0.5f;
// Write positions
positions[y * width + x] =
make_float4(u, w, v, __int_as_float(0xff00ff00));
}
3.2.15.2.3. Direct3D 11 版本
ID3D11Device* device;
struct CUSTOMVERTEX {
FLOAT x, y, z;
DWORD color;
};
ID3D11Buffer* positionsVB;
struct cudaGraphicsResource* positionsVB_CUDA;
int main()
{
int dev;
// Get a CUDA-enabled adapter
IDXGIFactory* factory;
CreateDXGIFactory(__uuidof(IDXGIFactory), (void**)&factory);
IDXGIAdapter* adapter = 0;
for (unsigned int i = 0; !adapter; ++i) {
if (FAILED(factory->EnumAdapters(i, &adapter))
break;
if (cudaD3D11GetDevice(&dev, adapter) == cudaSuccess)
break;
adapter->Release();
}
factory->Release();
// Create swap chain and device
...
sFnPtr_D3D11CreateDeviceAndSwapChain(adapter,
D3D11_DRIVER_TYPE_HARDWARE,
0,
D3D11_CREATE_DEVICE_DEBUG,
featureLevels, 3,
D3D11_SDK_VERSION,
&swapChainDesc, &swapChain,
&device,
&featureLevel,
&deviceContext);
adapter->Release();
// Use the same device
cudaSetDevice(dev);
// Create vertex buffer and register it with CUDA
unsigned int size = width * height * sizeof(CUSTOMVERTEX);
D3D11_BUFFER_DESC bufferDesc;
bufferDesc.Usage = D3D11_USAGE_DEFAULT;
bufferDesc.ByteWidth = size;
bufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
bufferDesc.CPUAccessFlags = 0;
bufferDesc.MiscFlags = 0;
device->CreateBuffer(&bufferDesc, 0, &positionsVB);
cudaGraphicsD3D11RegisterResource(&positionsVB_CUDA,
positionsVB,
cudaGraphicsRegisterFlagsNone);
cudaGraphicsResourceSetMapFlags(positionsVB_CUDA,
cudaGraphicsMapFlagsWriteDiscard);
// Launch rendering loop
while (...) {
...
Render();
...
}
...
}
void Render()
{
// Map vertex buffer for writing from CUDA
float4* positions;
cudaGraphicsMapResources(1, &positionsVB_CUDA, 0);
size_t num_bytes;
cudaGraphicsResourceGetMappedPointer((void**)&positions,
&num_bytes,
positionsVB_CUDA));
// Execute kernel
dim3 dimBlock(16, 16, 1);
dim3 dimGrid(width / dimBlock.x, height / dimBlock.y, 1);
createVertices<<<dimGrid, dimBlock>>>(positions, time,
width, height);
// Unmap vertex buffer
cudaGraphicsUnmapResources(1, &positionsVB_CUDA, 0);
// Draw and present
...
}
void releaseVB()
{
cudaGraphicsUnregisterResource(positionsVB_CUDA);
positionsVB->Release();
}
__global__ void createVertices(float4* positions, float time,
unsigned int width, unsigned int height)
{
unsigned int x = blockIdx.x * blockDim.x + threadIdx.x;
unsigned int y = blockIdx.y * blockDim.y + threadIdx.y;
// Calculate uv coordinates
float u = x / (float)width;
float v = y / (float)height;
u = u * 2.0f - 1.0f;
v = v * 2.0f - 1.0f;
// Calculate simple sine wave pattern
float freq = 4.0f;
float w = sinf(u * freq + time)
* cosf(v * freq + time) * 0.5f;
// Write positions
positions[y * width + x] =
make_float4(u, w, v, __int_as_float(0xff00ff00));
}
3.2.15.3. SLI 互操作性
在具有多个 GPU 的系统中,所有支持 CUDA 的 GPU 都可以通过 CUDA 驱动程序和运行时作为单独的设备访问。但是,当系统处于 SLI 模式时,需要考虑以下特殊事项。
首先,在一个 GPU 上的一个 CUDA 设备中分配内存将消耗属于 Direct3D 或 OpenGL 设备的 SLI 配置的其他 GPU 上的内存。因此,分配可能会比预期更早失败。
其次,应用程序应创建多个 CUDA 上下文,每个 GPU 在 SLI 配置中创建一个上下文。虽然这不是严格的要求,但它可以避免设备之间不必要的数据传输。应用程序可以使用 cudaD3D[9|10|11]GetDevices()
用于 Direct3D 和 cudaGLGetDevices()
用于 OpenGL 调用集,以识别在当前帧和下一帧中执行渲染的设备对应的 CUDA 设备句柄。有了这些信息,当 deviceList
参数设置为 cudaD3D[9|10|11]DeviceListCurrentFrame
或 cudaGLDeviceListCurrentFrame
时,应用程序通常会选择适当的设备并将 Direct3D 或 OpenGL 资源映射到 cudaD3D[9|10|11]GetDevices()
或 cudaGLGetDevices()
返回的 CUDA 设备。
请注意,从 cudaGraphicsD9D[9|10|11]RegisterResource
和 cudaGraphicsGLRegister[Buffer|Image]
返回的资源必须仅在发生注册的设备上使用。因此,在 SLI 配置中,当不同帧的数据在不同的 CUDA 设备上计算时,有必要为每个设备单独注册资源。
有关 CUDA 运行时如何分别与 Direct3D 和 OpenGL 互操作的详细信息,请参阅 Direct3D 互操作性 和 OpenGL 互操作性。
3.2.16. 外部资源互操作性
外部资源互操作性允许 CUDA 导入其他 API 显式导出的某些资源。这些对象通常由其他 API 使用操作系统本地的句柄导出,例如 Linux 上的文件描述符或 Windows 上的 NT 句柄。它们也可以使用其他统一接口导出,例如 NVIDIA 软件通信接口。可以导入两种类型的资源:内存对象和同步对象。
内存对象可以使用 cudaImportExternalMemory()
导入到 CUDA 中。导入的内存对象可以从内核内部访问,方法是使用通过 cudaExternalMemoryGetMappedBuffer()
映射到内存对象的设备指针或通过 cudaExternalMemoryGetMappedMipmappedArray()
映射的 CUDA mipmapped 数组。根据内存对象的类型,可能可以在单个内存对象上设置多个映射。映射必须与导出 API 中设置的映射匹配。任何不匹配的映射都会导致未定义的行为。导入的内存对象必须使用 cudaDestroyExternalMemory()
释放。释放内存对象不会释放任何到该对象的映射。因此,映射到该对象的任何设备指针都必须使用 cudaFree()
显式释放,并且映射到该对象的任何 CUDA mipmapped 数组都必须使用 cudaFreeMipmappedArray()
显式释放。在对象被销毁后访问对象的映射是非法的。
可以使用 cudaImportExternalSemaphore()
将同步对象导入到 CUDA 中。导入的同步对象随后可以使用 cudaSignalExternalSemaphoresAsync()
发出信号,并使用 cudaWaitExternalSemaphoresAsync()
等待信号。在相应的信号发出之前发出等待是非法的。此外,根据导入的同步对象的类型,对于如何发出信号和等待信号可能存在额外的约束,这将在后续章节中描述。导入的信号量对象必须使用 cudaDestroyExternalSemaphore()
释放。所有未完成的信号和等待都必须在信号量对象销毁之前完成。
3.2.16.1. Vulkan 互操作性
3.2.16.1.1. 匹配设备 UUID
当导入 Vulkan 导出的内存和同步对象时,它们必须在创建它们的同一设备上导入和映射。与创建对象的 Vulkan 物理设备对应的 CUDA 设备可以通过比较 CUDA 设备的 UUID 和 Vulkan 物理设备的 UUID 来确定,如下面的代码示例所示。请注意,Vulkan 物理设备不应是包含多个 Vulkan 物理设备的设备组的一部分。由 vkEnumeratePhysicalDeviceGroups
返回的包含给定 Vulkan 物理设备的设备组必须具有物理设备计数为 1。
int getCudaDeviceForVulkanPhysicalDevice(VkPhysicalDevice vkPhysicalDevice) {
VkPhysicalDeviceIDProperties vkPhysicalDeviceIDProperties = {};
vkPhysicalDeviceIDProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES;
vkPhysicalDeviceIDProperties.pNext = NULL;
VkPhysicalDeviceProperties2 vkPhysicalDeviceProperties2 = {};
vkPhysicalDeviceProperties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
vkPhysicalDeviceProperties2.pNext = &vkPhysicalDeviceIDProperties;
vkGetPhysicalDeviceProperties2(vkPhysicalDevice, &vkPhysicalDeviceProperties2);
int cudaDeviceCount;
cudaGetDeviceCount(&cudaDeviceCount);
for (int cudaDevice = 0; cudaDevice < cudaDeviceCount; cudaDevice++) {
cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, cudaDevice);
if (!memcmp(&deviceProp.uuid, vkPhysicalDeviceIDProperties.deviceUUID, VK_UUID_SIZE)) {
return cudaDevice;
}
}
return cudaInvalidDeviceId;
}
3.2.16.1.2. 导入内存对象
在 Linux 和 Windows 10 上,Vulkan 导出的专用和非专用内存对象都可以导入到 CUDA 中。在 Windows 7 上,只能导入专用内存对象。当导入 Vulkan 专用内存对象时,必须设置标志 cudaExternalMemoryDedicated
。
使用 VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT
导出的 Vulkan 内存对象可以使用与该对象关联的文件描述符导入到 CUDA 中,如下所示。请注意,CUDA 在导入后会获得文件描述符的所有权。成功导入后使用文件描述符会导致未定义的行为。
cudaExternalMemory_t importVulkanMemoryObjectFromFileDescriptor(int fd, unsigned long long size, bool isDedicated) {
cudaExternalMemory_t extMem = NULL;
cudaExternalMemoryHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalMemoryHandleTypeOpaqueFd;
desc.handle.fd = fd;
desc.size = size;
if (isDedicated) {
desc.flags |= cudaExternalMemoryDedicated;
}
cudaImportExternalMemory(&extMem, &desc);
// Input parameter 'fd' should not be used beyond this point as CUDA has assumed ownership of it
return extMem;
}
使用 VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT
导出的 Vulkan 内存对象可以使用与该对象关联的 NT 句柄导入到 CUDA 中,如下所示。请注意,CUDA 不会获得 NT 句柄的所有权,应用程序有责任在不再需要 NT 句柄时关闭它。NT 句柄持有对资源的引用,因此在可以释放底层内存之前,必须显式释放它。
cudaExternalMemory_t importVulkanMemoryObjectFromNTHandle(HANDLE handle, unsigned long long size, bool isDedicated) {
cudaExternalMemory_t extMem = NULL;
cudaExternalMemoryHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalMemoryHandleTypeOpaqueWin32;
desc.handle.win32.handle = handle;
desc.size = size;
if (isDedicated) {
desc.flags |= cudaExternalMemoryDedicated;
}
cudaImportExternalMemory(&extMem, &desc);
// Input parameter 'handle' should be closed if it's not needed anymore
CloseHandle(handle);
return extMem;
}
使用 VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT
导出的 Vulkan 内存对象也可以使用命名句柄导入(如果存在),如下所示。
cudaExternalMemory_t importVulkanMemoryObjectFromNamedNTHandle(LPCWSTR name, unsigned long long size, bool isDedicated) {
cudaExternalMemory_t extMem = NULL;
cudaExternalMemoryHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalMemoryHandleTypeOpaqueWin32;
desc.handle.win32.name = (void *)name;
desc.size = size;
if (isDedicated) {
desc.flags |= cudaExternalMemoryDedicated;
}
cudaImportExternalMemory(&extMem, &desc);
return extMem;
}
使用 VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_KMT_BIT 导出的 Vulkan 内存对象可以使用与该对象关联的全局共享 D3DKMT 句柄导入到 CUDA 中,如下所示。由于全局共享 D3DKMT 句柄不持有对底层内存的引用,因此当对资源的所有其他引用都被销毁时,它会自动销毁。
cudaExternalMemory_t importVulkanMemoryObjectFromKMTHandle(HANDLE handle, unsigned long long size, bool isDedicated) {
cudaExternalMemory_t extMem = NULL;
cudaExternalMemoryHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalMemoryHandleTypeOpaqueWin32Kmt;
desc.handle.win32.handle = (void *)handle;
desc.size = size;
if (isDedicated) {
desc.flags |= cudaExternalMemoryDedicated;
}
cudaImportExternalMemory(&extMem, &desc);
return extMem;
}
3.2.16.1.3. 将缓冲区映射到导入的内存对象
设备指针可以映射到导入的内存对象上,如下所示。映射的偏移量和大小必须与使用相应的 Vulkan API 创建映射时指定的值相匹配。所有映射的设备指针都必须使用 cudaFree()
释放。
void * mapBufferOntoExternalMemory(cudaExternalMemory_t extMem, unsigned long long offset, unsigned long long size) {
void *ptr = NULL;
cudaExternalMemoryBufferDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.offset = offset;
desc.size = size;
cudaExternalMemoryGetMappedBuffer(&ptr, extMem, &desc);
// Note: ‘ptr’ must eventually be freed using cudaFree()
return ptr;
}
3.2.16.1.4. 将 Mipmap 数组映射到导入的内存对象
CUDA Mipmap 数组可以映射到导入的内存对象上,如下所示。偏移量、尺寸、格式和 Mipmap 级别数必须与使用相应的 Vulkan API 创建映射时指定的值相匹配。此外,如果 Mipmap 数组在 Vulkan 中绑定为颜色目标,则必须设置标志 cudaArrayColorAttachment
。所有映射的 Mipmap 数组都必须使用 cudaFreeMipmappedArray()
释放。以下代码示例展示了当将 Mipmap 数组映射到导入的内存对象上时,如何将 Vulkan 参数转换为相应的 CUDA 参数。
cudaMipmappedArray_t mapMipmappedArrayOntoExternalMemory(cudaExternalMemory_t extMem, unsigned long long offset, cudaChannelFormatDesc *formatDesc, cudaExtent *extent, unsigned int flags, unsigned int numLevels) {
cudaMipmappedArray_t mipmap = NULL;
cudaExternalMemoryMipmappedArrayDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.offset = offset;
desc.formatDesc = *formatDesc;
desc.extent = *extent;
desc.flags = flags;
desc.numLevels = numLevels;
// Note: 'mipmap' must eventually be freed using cudaFreeMipmappedArray()
cudaExternalMemoryGetMappedMipmappedArray(&mipmap, extMem, &desc);
return mipmap;
}
cudaChannelFormatDesc getCudaChannelFormatDescForVulkanFormat(VkFormat format)
{
cudaChannelFormatDesc d;
memset(&d, 0, sizeof(d));
switch (format) {
case VK_FORMAT_R8_UINT: d.x = 8; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case VK_FORMAT_R8_SINT: d.x = 8; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case VK_FORMAT_R8G8_UINT: d.x = 8; d.y = 8; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case VK_FORMAT_R8G8_SINT: d.x = 8; d.y = 8; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case VK_FORMAT_R8G8B8A8_UINT: d.x = 8; d.y = 8; d.z = 8; d.w = 8; d.f = cudaChannelFormatKindUnsigned; break;
case VK_FORMAT_R8G8B8A8_SINT: d.x = 8; d.y = 8; d.z = 8; d.w = 8; d.f = cudaChannelFormatKindSigned; break;
case VK_FORMAT_R16_UINT: d.x = 16; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case VK_FORMAT_R16_SINT: d.x = 16; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case VK_FORMAT_R16G16_UINT: d.x = 16; d.y = 16; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case VK_FORMAT_R16G16_SINT: d.x = 16; d.y = 16; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case VK_FORMAT_R16G16B16A16_UINT: d.x = 16; d.y = 16; d.z = 16; d.w = 16; d.f = cudaChannelFormatKindUnsigned; break;
case VK_FORMAT_R16G16B16A16_SINT: d.x = 16; d.y = 16; d.z = 16; d.w = 16; d.f = cudaChannelFormatKindSigned; break;
case VK_FORMAT_R32_UINT: d.x = 32; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case VK_FORMAT_R32_SINT: d.x = 32; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case VK_FORMAT_R32_SFLOAT: d.x = 32; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindFloat; break;
case VK_FORMAT_R32G32_UINT: d.x = 32; d.y = 32; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case VK_FORMAT_R32G32_SINT: d.x = 32; d.y = 32; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case VK_FORMAT_R32G32_SFLOAT: d.x = 32; d.y = 32; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindFloat; break;
case VK_FORMAT_R32G32B32A32_UINT: d.x = 32; d.y = 32; d.z = 32; d.w = 32; d.f = cudaChannelFormatKindUnsigned; break;
case VK_FORMAT_R32G32B32A32_SINT: d.x = 32; d.y = 32; d.z = 32; d.w = 32; d.f = cudaChannelFormatKindSigned; break;
case VK_FORMAT_R32G32B32A32_SFLOAT: d.x = 32; d.y = 32; d.z = 32; d.w = 32; d.f = cudaChannelFormatKindFloat; break;
default: assert(0);
}
return d;
}
cudaExtent getCudaExtentForVulkanExtent(VkExtent3D vkExt, uint32_t arrayLayers, VkImageViewType vkImageViewType) {
cudaExtent e = { 0, 0, 0 };
switch (vkImageViewType) {
case VK_IMAGE_VIEW_TYPE_1D: e.width = vkExt.width; e.height = 0; e.depth = 0; break;
case VK_IMAGE_VIEW_TYPE_2D: e.width = vkExt.width; e.height = vkExt.height; e.depth = 0; break;
case VK_IMAGE_VIEW_TYPE_3D: e.width = vkExt.width; e.height = vkExt.height; e.depth = vkExt.depth; break;
case VK_IMAGE_VIEW_TYPE_CUBE: e.width = vkExt.width; e.height = vkExt.height; e.depth = arrayLayers; break;
case VK_IMAGE_VIEW_TYPE_1D_ARRAY: e.width = vkExt.width; e.height = 0; e.depth = arrayLayers; break;
case VK_IMAGE_VIEW_TYPE_2D_ARRAY: e.width = vkExt.width; e.height = vkExt.height; e.depth = arrayLayers; break;
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY: e.width = vkExt.width; e.height = vkExt.height; e.depth = arrayLayers; break;
default: assert(0);
}
return e;
}
unsigned int getCudaMipmappedArrayFlagsForVulkanImage(VkImageViewType vkImageViewType, VkImageUsageFlags vkImageUsageFlags, bool allowSurfaceLoadStore) {
unsigned int flags = 0;
switch (vkImageViewType) {
case VK_IMAGE_VIEW_TYPE_CUBE: flags |= cudaArrayCubemap; break;
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY: flags |= cudaArrayCubemap | cudaArrayLayered; break;
case VK_IMAGE_VIEW_TYPE_1D_ARRAY: flags |= cudaArrayLayered; break;
case VK_IMAGE_VIEW_TYPE_2D_ARRAY: flags |= cudaArrayLayered; break;
default: break;
}
if (vkImageUsageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) {
flags |= cudaArrayColorAttachment;
}
if (allowSurfaceLoadStore) {
flags |= cudaArraySurfaceLoadStore;
}
return flags;
}
3.2.16.1.5. 导入同步对象
使用 VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT
导出的 Vulkan 信号量对象可以使用与该对象关联的文件描述符导入到 CUDA 中,如下所示。请注意,CUDA 在导入后会获得文件描述符的所有权。成功导入后使用文件描述符会导致未定义的行为。
cudaExternalSemaphore_t importVulkanSemaphoreObjectFromFileDescriptor(int fd) {
cudaExternalSemaphore_t extSem = NULL;
cudaExternalSemaphoreHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalSemaphoreHandleTypeOpaqueFd;
desc.handle.fd = fd;
cudaImportExternalSemaphore(&extSem, &desc);
// Input parameter 'fd' should not be used beyond this point as CUDA has assumed ownership of it
return extSem;
}
使用 VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT
导出的 Vulkan 信号量对象可以使用与该对象关联的 NT 句柄导入到 CUDA 中,如下所示。请注意,CUDA 不会获得 NT 句柄的所有权,应用程序有责任在不再需要 NT 句柄时关闭它。NT 句柄持有对资源的引用,因此在可以释放底层信号量之前,必须显式释放它。
cudaExternalSemaphore_t importVulkanSemaphoreObjectFromNTHandle(HANDLE handle) {
cudaExternalSemaphore_t extSem = NULL;
cudaExternalSemaphoreHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalSemaphoreHandleTypeOpaqueWin32;
desc.handle.win32.handle = handle;
cudaImportExternalSemaphore(&extSem, &desc);
// Input parameter 'handle' should be closed if it's not needed anymore
CloseHandle(handle);
return extSem;
}
使用 VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT
导出的 Vulkan 信号量对象也可以使用命名句柄导入(如果存在),如下所示。
cudaExternalSemaphore_t importVulkanSemaphoreObjectFromNamedNTHandle(LPCWSTR name) {
cudaExternalSemaphore_t extSem = NULL;
cudaExternalSemaphoreHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalSemaphoreHandleTypeOpaqueWin32;
desc.handle.win32.name = (void *)name;
cudaImportExternalSemaphore(&extSem, &desc);
return extSem;
}
使用 VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_KMT_BIT
导出的 Vulkan 信号量对象可以使用与该对象关联的全局共享 D3DKMT 句柄导入到 CUDA 中,如下所示。由于全局共享 D3DKMT 句柄不持有对底层信号量的引用,因此当对资源的所有其他引用都被销毁时,它会自动销毁。
cudaExternalSemaphore_t importVulkanSemaphoreObjectFromKMTHandle(HANDLE handle) {
cudaExternalSemaphore_t extSem = NULL;
cudaExternalSemaphoreHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalSemaphoreHandleTypeOpaqueWin32Kmt;
desc.handle.win32.handle = (void *)handle;
cudaImportExternalSemaphore(&extSem, &desc);
return extSem;
}
3.2.16.1.6. 对导入的同步对象发出信号/等待
可以对导入的 Vulkan 信号量对象发出信号,如下所示。对这样的信号量对象发出信号会将其设置为已发出信号状态。等待此信号的相应等待操作必须在 Vulkan 中发出。此外,等待此信号的等待操作必须在此信号发出之后发出。
void signalExternalSemaphore(cudaExternalSemaphore_t extSem, cudaStream_t stream) {
cudaExternalSemaphoreSignalParams params = {};
memset(¶ms, 0, sizeof(params));
cudaSignalExternalSemaphoresAsync(&extSem, ¶ms, 1, stream);
}
可以等待导入的 Vulkan 信号量对象,如下所示。等待这样的信号量对象会等待直到它达到已发出信号状态,然后将其重置回未发出信号状态。此等待操作正在等待的相应信号必须在 Vulkan 中发出。此外,信号必须在此等待操作可以发出之前发出。
void waitExternalSemaphore(cudaExternalSemaphore_t extSem, cudaStream_t stream) {
cudaExternalSemaphoreWaitParams params = {};
memset(¶ms, 0, sizeof(params));
cudaWaitExternalSemaphoresAsync(&extSem, ¶ms, 1, stream);
}
3.2.16.2. OpenGL 互操作性
如 OpenGL 互操作性 中概述的传统 OpenGL-CUDA 互操作通过 CUDA 直接使用在 OpenGL 中创建的句柄来实现。然而,由于 OpenGL 也可以使用在 Vulkan 中创建的内存和同步对象,因此存在另一种实现 OpenGL-CUDA 互操作的方法。本质上,Vulkan 导出的内存和同步对象可以导入到 OpenGL 和 CUDA 中,然后用于协调 OpenGL 和 CUDA 之间的内存访问。有关如何导入 Vulkan 导出的内存和同步对象的更多详细信息,请参阅以下 OpenGL 扩展
GL_EXT_memory_object
GL_EXT_memory_object_fd
GL_EXT_memory_object_win32
GL_EXT_semaphore
GL_EXT_semaphore_fd
GL_EXT_semaphore_win32
3.2.16.3. Direct3D 12 互操作性
3.2.16.3.1. 匹配设备 LUID
当导入 Direct3D 12 导出的内存和同步对象时,它们必须在创建它们的同一设备上导入和映射。与创建对象的 Direct3D 12 设备对应的 CUDA 设备可以通过比较 CUDA 设备的 LUID 和 Direct3D 12 设备的 LUID 来确定,如下面的代码示例所示。请注意,Direct3D 12 设备不得在链接节点适配器上创建。即,ID3D12Device::GetNodeCount
返回的节点计数必须为 1。
int getCudaDeviceForD3D12Device(ID3D12Device *d3d12Device) {
LUID d3d12Luid = d3d12Device->GetAdapterLuid();
int cudaDeviceCount;
cudaGetDeviceCount(&cudaDeviceCount);
for (int cudaDevice = 0; cudaDevice < cudaDeviceCount; cudaDevice++) {
cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, cudaDevice);
char *cudaLuid = deviceProp.luid;
if (!memcmp(&d3d12Luid.LowPart, cudaLuid, sizeof(d3d12Luid.LowPart)) &&
!memcmp(&d3d12Luid.HighPart, cudaLuid + sizeof(d3d12Luid.LowPart), sizeof(d3d12Luid.HighPart))) {
return cudaDevice;
}
}
return cudaInvalidDeviceId;
}
3.2.16.3.2. 导入内存对象
通过在调用 ID3D12Device::CreateHeap
时设置标志 D3D12_HEAP_FLAG_SHARED
创建的可共享 Direct3D 12 堆内存对象可以使用与该对象关联的 NT 句柄导入到 CUDA 中,如下所示。请注意,应用程序有责任在不再需要 NT 句柄时关闭它。NT 句柄持有对资源的引用,因此在可以释放底层内存之前,必须显式释放它。
cudaExternalMemory_t importD3D12HeapFromNTHandle(HANDLE handle, unsigned long long size) {
cudaExternalMemory_t extMem = NULL;
cudaExternalMemoryHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalMemoryHandleTypeD3D12Heap;
desc.handle.win32.handle = (void *)handle;
desc.size = size;
cudaImportExternalMemory(&extMem, &desc);
// Input parameter 'handle' should be closed if it's not needed anymore
CloseHandle(handle);
return extMem;
}
可共享的 Direct3D 12 堆内存对象也可以使用命名句柄导入(如果存在),如下所示。
cudaExternalMemory_t importD3D12HeapFromNamedNTHandle(LPCWSTR name, unsigned long long size) {
cudaExternalMemory_t extMem = NULL;
cudaExternalMemoryHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalMemoryHandleTypeD3D12Heap;
desc.handle.win32.name = (void *)name;
desc.size = size;
cudaImportExternalMemory(&extMem, &desc);
return extMem;
}
通过在调用 D3D12Device::CreateCommittedResource
时设置标志 D3D12_HEAP_FLAG_SHARED
创建的可共享 Direct3D 12 提交资源可以使用与该对象关联的 NT 句柄导入到 CUDA 中,如下所示。当导入 Direct3D 12 提交资源时,必须设置标志 cudaExternalMemoryDedicated
。请注意,应用程序有责任在不再需要 NT 句柄时关闭它。NT 句柄持有对资源的引用,因此在可以释放底层内存之前,必须显式释放它。
cudaExternalMemory_t importD3D12CommittedResourceFromNTHandle(HANDLE handle, unsigned long long size) {
cudaExternalMemory_t extMem = NULL;
cudaExternalMemoryHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalMemoryHandleTypeD3D12Resource;
desc.handle.win32.handle = (void *)handle;
desc.size = size;
desc.flags |= cudaExternalMemoryDedicated;
cudaImportExternalMemory(&extMem, &desc);
// Input parameter 'handle' should be closed if it's not needed anymore
CloseHandle(handle);
return extMem;
}
可共享的 Direct3D 12 提交资源也可以使用命名句柄导入(如果存在),如下所示。
cudaExternalMemory_t importD3D12CommittedResourceFromNamedNTHandle(LPCWSTR name, unsigned long long size) {
cudaExternalMemory_t extMem = NULL;
cudaExternalMemoryHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalMemoryHandleTypeD3D12Resource;
desc.handle.win32.name = (void *)name;
desc.size = size;
desc.flags |= cudaExternalMemoryDedicated;
cudaImportExternalMemory(&extMem, &desc);
return extMem;
}
3.2.16.3.3. 将缓冲区映射到导入的内存对象
设备指针可以映射到导入的内存对象上,如下所示。映射的偏移量和大小必须与使用相应的 Direct3D 12 API 创建映射时指定的值相匹配。所有映射的设备指针都必须使用 cudaFree()
释放。
void * mapBufferOntoExternalMemory(cudaExternalMemory_t extMem, unsigned long long offset, unsigned long long size) {
void *ptr = NULL;
cudaExternalMemoryBufferDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.offset = offset;
desc.size = size;
cudaExternalMemoryGetMappedBuffer(&ptr, extMem, &desc);
// Note: 'ptr' must eventually be freed using cudaFree()
return ptr;
}
3.2.16.3.4. 将 Mipmap 数组映射到导入的内存对象
CUDA Mipmap 数组可以映射到导入的内存对象上,如下所示。偏移量、尺寸、格式和 Mipmap 级别数必须与使用相应的 Direct3D 12 API 创建映射时指定的值相匹配。此外,如果 Mipmap 数组可以在 Direct3D 12 中绑定为渲染目标,则必须设置标志 cudaArrayColorAttachment
。所有映射的 Mipmap 数组都必须使用 cudaFreeMipmappedArray()
释放。以下代码示例展示了当将 Mipmap 数组映射到导入的内存对象上时,如何将 Vulkan 参数转换为相应的 CUDA 参数。
cudaMipmappedArray_t mapMipmappedArrayOntoExternalMemory(cudaExternalMemory_t extMem, unsigned long long offset, cudaChannelFormatDesc *formatDesc, cudaExtent *extent, unsigned int flags, unsigned int numLevels) {
cudaMipmappedArray_t mipmap = NULL;
cudaExternalMemoryMipmappedArrayDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.offset = offset;
desc.formatDesc = *formatDesc;
desc.extent = *extent;
desc.flags = flags;
desc.numLevels = numLevels;
// Note: 'mipmap' must eventually be freed using cudaFreeMipmappedArray()
cudaExternalMemoryGetMappedMipmappedArray(&mipmap, extMem, &desc);
return mipmap;
}
cudaChannelFormatDesc getCudaChannelFormatDescForDxgiFormat(DXGI_FORMAT dxgiFormat)
{
cudaChannelFormatDesc d;
memset(&d, 0, sizeof(d));
switch (dxgiFormat) {
case DXGI_FORMAT_R8_UINT: d.x = 8; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R8_SINT: d.x = 8; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R8G8_UINT: d.x = 8; d.y = 8; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R8G8_SINT: d.x = 8; d.y = 8; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R8G8B8A8_UINT: d.x = 8; d.y = 8; d.z = 8; d.w = 8; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R8G8B8A8_SINT: d.x = 8; d.y = 8; d.z = 8; d.w = 8; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R16_UINT: d.x = 16; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R16_SINT: d.x = 16; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R16G16_UINT: d.x = 16; d.y = 16; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R16G16_SINT: d.x = 16; d.y = 16; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R16G16B16A16_UINT: d.x = 16; d.y = 16; d.z = 16; d.w = 16; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R16G16B16A16_SINT: d.x = 16; d.y = 16; d.z = 16; d.w = 16; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R32_UINT: d.x = 32; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R32_SINT: d.x = 32; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R32_FLOAT: d.x = 32; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindFloat; break;
case DXGI_FORMAT_R32G32_UINT: d.x = 32; d.y = 32; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R32G32_SINT: d.x = 32; d.y = 32; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R32G32_FLOAT: d.x = 32; d.y = 32; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindFloat; break;
case DXGI_FORMAT_R32G32B32A32_UINT: d.x = 32; d.y = 32; d.z = 32; d.w = 32; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R32G32B32A32_SINT: d.x = 32; d.y = 32; d.z = 32; d.w = 32; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R32G32B32A32_FLOAT: d.x = 32; d.y = 32; d.z = 32; d.w = 32; d.f = cudaChannelFormatKindFloat; break;
default: assert(0);
}
return d;
}
cudaExtent getCudaExtentForD3D12Extent(UINT64 width, UINT height, UINT16 depthOrArraySize, D3D12_SRV_DIMENSION d3d12SRVDimension) {
cudaExtent e = { 0, 0, 0 };
switch (d3d12SRVDimension) {
case D3D12_SRV_DIMENSION_TEXTURE1D: e.width = width; e.height = 0; e.depth = 0; break;
case D3D12_SRV_DIMENSION_TEXTURE2D: e.width = width; e.height = height; e.depth = 0; break;
case D3D12_SRV_DIMENSION_TEXTURE3D: e.width = width; e.height = height; e.depth = depthOrArraySize; break;
case D3D12_SRV_DIMENSION_TEXTURECUBE: e.width = width; e.height = height; e.depth = depthOrArraySize; break;
case D3D12_SRV_DIMENSION_TEXTURE1DARRAY: e.width = width; e.height = 0; e.depth = depthOrArraySize; break;
case D3D12_SRV_DIMENSION_TEXTURE2DARRAY: e.width = width; e.height = height; e.depth = depthOrArraySize; break;
case D3D12_SRV_DIMENSION_TEXTURECUBEARRAY: e.width = width; e.height = height; e.depth = depthOrArraySize; break;
default: assert(0);
}
return e;
}
unsigned int getCudaMipmappedArrayFlagsForD3D12Resource(D3D12_SRV_DIMENSION d3d12SRVDimension, D3D12_RESOURCE_FLAGS d3d12ResourceFlags, bool allowSurfaceLoadStore) {
unsigned int flags = 0;
switch (d3d12SRVDimension) {
case D3D12_SRV_DIMENSION_TEXTURECUBE: flags |= cudaArrayCubemap; break;
case D3D12_SRV_DIMENSION_TEXTURECUBEARRAY: flags |= cudaArrayCubemap | cudaArrayLayered; break;
case D3D12_SRV_DIMENSION_TEXTURE1DARRAY: flags |= cudaArrayLayered; break;
case D3D12_SRV_DIMENSION_TEXTURE2DARRAY: flags |= cudaArrayLayered; break;
default: break;
}
if (d3d12ResourceFlags & D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET) {
flags |= cudaArrayColorAttachment;
}
if (allowSurfaceLoadStore) {
flags |= cudaArraySurfaceLoadStore;
}
return flags;
}
3.2.16.3.5. 导入同步对象
通过在调用 ID3D12Device::CreateFence
时设置标志 D3D12_FENCE_FLAG_SHARED
创建的可共享 Direct3D 12 栅栏对象可以使用与该对象关联的 NT 句柄导入到 CUDA 中,如下所示。请注意,应用程序有责任在不再需要 NT 句柄时关闭它。NT 句柄持有对资源的引用,因此在可以释放底层信号量之前,必须显式释放它。
cudaExternalSemaphore_t importD3D12FenceFromNTHandle(HANDLE handle) {
cudaExternalSemaphore_t extSem = NULL;
cudaExternalSemaphoreHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalSemaphoreHandleTypeD3D12Fence;
desc.handle.win32.handle = handle;
cudaImportExternalSemaphore(&extSem, &desc);
// Input parameter 'handle' should be closed if it's not needed anymore
CloseHandle(handle);
return extSem;
}
可共享的 Direct3D 12 栅栏对象也可以使用命名句柄导入(如果存在),如下所示。
cudaExternalSemaphore_t importD3D12FenceFromNamedNTHandle(LPCWSTR name) {
cudaExternalSemaphore_t extSem = NULL;
cudaExternalSemaphoreHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalSemaphoreHandleTypeD3D12Fence;
desc.handle.win32.name = (void *)name;
cudaImportExternalSemaphore(&extSem, &desc);
return extSem;
}
3.2.16.3.6. 对导入的同步对象发出信号/等待
可以对导入的 Direct3D 12 栅栏对象发出信号,如下所示。对这样的栅栏对象发出信号会将其值设置为指定的值。等待此信号的相应等待操作必须在 Direct3D 12 中发出。此外,等待此信号的等待操作必须在此信号发出之后发出。
void signalExternalSemaphore(cudaExternalSemaphore_t extSem, unsigned long long value, cudaStream_t stream) {
cudaExternalSemaphoreSignalParams params = {};
memset(¶ms, 0, sizeof(params));
params.params.fence.value = value;
cudaSignalExternalSemaphoresAsync(&extSem, ¶ms, 1, stream);
}
可以等待导入的 Direct3D 12 栅栏对象,如下所示。等待这样的栅栏对象会等待直到其值变得大于或等于指定值。此等待操作正在等待的相应信号必须在 Direct3D 12 中发出。此外,信号必须在此等待操作可以发出之前发出。
void waitExternalSemaphore(cudaExternalSemaphore_t extSem, unsigned long long value, cudaStream_t stream) {
cudaExternalSemaphoreWaitParams params = {};
memset(¶ms, 0, sizeof(params));
params.params.fence.value = value;
cudaWaitExternalSemaphoresAsync(&extSem, ¶ms, 1, stream);
}
3.2.16.4. Direct3D 11 互操作性
3.2.16.4.1. 匹配设备 LUID
当导入 Direct3D 11 导出的内存和同步对象时,它们必须在创建它们的同一设备上导入和映射。与创建对象的 Direct3D 11 设备对应的 CUDA 设备可以通过比较 CUDA 设备的 LUID 和 Direct3D 11 设备的 LUID 来确定,如下面的代码示例所示。
int getCudaDeviceForD3D11Device(ID3D11Device *d3d11Device) {
IDXGIDevice *dxgiDevice;
d3d11Device->QueryInterface(__uuidof(IDXGIDevice), (void **)&dxgiDevice);
IDXGIAdapter *dxgiAdapter;
dxgiDevice->GetAdapter(&dxgiAdapter);
DXGI_ADAPTER_DESC dxgiAdapterDesc;
dxgiAdapter->GetDesc(&dxgiAdapterDesc);
LUID d3d11Luid = dxgiAdapterDesc.AdapterLuid;
int cudaDeviceCount;
cudaGetDeviceCount(&cudaDeviceCount);
for (int cudaDevice = 0; cudaDevice < cudaDeviceCount; cudaDevice++) {
cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, cudaDevice);
char *cudaLuid = deviceProp.luid;
if (!memcmp(&d3d11Luid.LowPart, cudaLuid, sizeof(d3d11Luid.LowPart)) &&
!memcmp(&d3d11Luid.HighPart, cudaLuid + sizeof(d3d11Luid.LowPart), sizeof(d3d11Luid.HighPart))) {
return cudaDevice;
}
}
return cudaInvalidDeviceId;
}
3.2.16.4.2. 导入内存对象
可共享的 Direct3D 11 纹理资源,即 ID3D11Texture1D
、ID3D11Texture2D
或 ID3D11Texture3D
,可以通过在调用 ID3D11Device:CreateTexture1D
、ID3D11Device:CreateTexture2D
或 ID3D11Device:CreateTexture3D
时设置 D3D11_RESOURCE_MISC_SHARED
或 D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX
(在 Windows 7 上)或 D3D11_RESOURCE_MISC_SHARED_NTHANDLE
(在 Windows 10 上)之一来创建。可共享的 Direct3D 11 缓冲区资源 ID3D11Buffer
可以通过在调用 ID3D11Device::CreateBuffer
时指定上述标志之一来创建。通过指定 D3D11_RESOURCE_MISC_SHARED_NTHANDLE
创建的可共享资源可以使用与该对象关联的 NT 句柄导入到 CUDA 中,如下所示。请注意,应用程序有责任在不再需要 NT 句柄时关闭它。NT 句柄持有对资源的引用,因此在可以释放底层内存之前,必须显式释放它。当导入 Direct3D 11 资源时,必须设置标志 cudaExternalMemoryDedicated
。
cudaExternalMemory_t importD3D11ResourceFromNTHandle(HANDLE handle, unsigned long long size) {
cudaExternalMemory_t extMem = NULL;
cudaExternalMemoryHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalMemoryHandleTypeD3D11Resource;
desc.handle.win32.handle = (void *)handle;
desc.size = size;
desc.flags |= cudaExternalMemoryDedicated;
cudaImportExternalMemory(&extMem, &desc);
// Input parameter 'handle' should be closed if it's not needed anymore
CloseHandle(handle);
return extMem;
}
可共享的 Direct3D 11 资源也可以使用命名句柄导入(如果存在),如下所示。
cudaExternalMemory_t importD3D11ResourceFromNamedNTHandle(LPCWSTR name, unsigned long long size) {
cudaExternalMemory_t extMem = NULL;
cudaExternalMemoryHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalMemoryHandleTypeD3D11Resource;
desc.handle.win32.name = (void *)name;
desc.size = size;
desc.flags |= cudaExternalMemoryDedicated;
cudaImportExternalMemory(&extMem, &desc);
return extMem;
}
通过指定 D3D11_RESOURCE_MISC_SHARED
或 D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX
创建的可共享 Direct3D 11 资源可以使用与该对象关联的全局共享 D3DKMT
句柄导入到 CUDA 中,如下所示。由于全局共享 D3DKMT
句柄不持有对底层内存的引用,因此当对资源的所有其他引用都被销毁时,它会自动销毁。
cudaExternalMemory_t importD3D11ResourceFromKMTHandle(HANDLE handle, unsigned long long size) {
cudaExternalMemory_t extMem = NULL;
cudaExternalMemoryHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalMemoryHandleTypeD3D11ResourceKmt;
desc.handle.win32.handle = (void *)handle;
desc.size = size;
desc.flags |= cudaExternalMemoryDedicated;
cudaImportExternalMemory(&extMem, &desc);
return extMem;
}
3.2.16.4.3. 将缓冲区映射到导入的内存对象
设备指针可以映射到导入的内存对象上,如下所示。映射的偏移量和大小必须与使用相应的 Direct3D 11 API 创建映射时指定的值相匹配。所有映射的设备指针都必须使用 cudaFree()
释放。
void * mapBufferOntoExternalMemory(cudaExternalMemory_t extMem, unsigned long long offset, unsigned long long size) {
void *ptr = NULL;
cudaExternalMemoryBufferDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.offset = offset;
desc.size = size;
cudaExternalMemoryGetMappedBuffer(&ptr, extMem, &desc);
// Note: ‘ptr’ must eventually be freed using cudaFree()
return ptr;
}
3.2.16.4.4. 将 Mipmap 数组映射到导入的内存对象
CUDA Mipmap 数组可以映射到导入的内存对象上,如下所示。偏移量、尺寸、格式和 Mipmap 级别数必须与使用相应的 Direct3D 11 API 创建映射时指定的值相匹配。此外,如果 Mipmap 数组可以在 Direct3D 12 中绑定为渲染目标,则必须设置标志 cudaArrayColorAttachment
。所有映射的 Mipmap 数组都必须使用 cudaFreeMipmappedArray()
释放。以下代码示例展示了当将 Mipmap 数组映射到导入的内存对象上时,如何将 Direct3D 11 参数转换为相应的 CUDA 参数。
cudaMipmappedArray_t mapMipmappedArrayOntoExternalMemory(cudaExternalMemory_t extMem, unsigned long long offset, cudaChannelFormatDesc *formatDesc, cudaExtent *extent, unsigned int flags, unsigned int numLevels) {
cudaMipmappedArray_t mipmap = NULL;
cudaExternalMemoryMipmappedArrayDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.offset = offset;
desc.formatDesc = *formatDesc;
desc.extent = *extent;
desc.flags = flags;
desc.numLevels = numLevels;
// Note: 'mipmap' must eventually be freed using cudaFreeMipmappedArray()
cudaExternalMemoryGetMappedMipmappedArray(&mipmap, extMem, &desc);
return mipmap;
}
cudaChannelFormatDesc getCudaChannelFormatDescForDxgiFormat(DXGI_FORMAT dxgiFormat)
{
cudaChannelFormatDesc d;
memset(&d, 0, sizeof(d));
switch (dxgiFormat) {
case DXGI_FORMAT_R8_UINT: d.x = 8; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R8_SINT: d.x = 8; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R8G8_UINT: d.x = 8; d.y = 8; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R8G8_SINT: d.x = 8; d.y = 8; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R8G8B8A8_UINT: d.x = 8; d.y = 8; d.z = 8; d.w = 8; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R8G8B8A8_SINT: d.x = 8; d.y = 8; d.z = 8; d.w = 8; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R16_UINT: d.x = 16; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R16_SINT: d.x = 16; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R16G16_UINT: d.x = 16; d.y = 16; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R16G16_SINT: d.x = 16; d.y = 16; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R16G16B16A16_UINT: d.x = 16; d.y = 16; d.z = 16; d.w = 16; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R16G16B16A16_SINT: d.x = 16; d.y = 16; d.z = 16; d.w = 16; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R32_UINT: d.x = 32; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R32_SINT: d.x = 32; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R32_FLOAT: d.x = 32; d.y = 0; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindFloat; break;
case DXGI_FORMAT_R32G32_UINT: d.x = 32; d.y = 32; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R32G32_SINT: d.x = 32; d.y = 32; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R32G32_FLOAT: d.x = 32; d.y = 32; d.z = 0; d.w = 0; d.f = cudaChannelFormatKindFloat; break;
case DXGI_FORMAT_R32G32B32A32_UINT: d.x = 32; d.y = 32; d.z = 32; d.w = 32; d.f = cudaChannelFormatKindUnsigned; break;
case DXGI_FORMAT_R32G32B32A32_SINT: d.x = 32; d.y = 32; d.z = 32; d.w = 32; d.f = cudaChannelFormatKindSigned; break;
case DXGI_FORMAT_R32G32B32A32_FLOAT: d.x = 32; d.y = 32; d.z = 32; d.w = 32; d.f = cudaChannelFormatKindFloat; break;
default: assert(0);
}
return d;
}
cudaExtent getCudaExtentForD3D11Extent(UINT64 width, UINT height, UINT16 depthOrArraySize, D3D12_SRV_DIMENSION d3d11SRVDimension) {
cudaExtent e = { 0, 0, 0 };
switch (d3d11SRVDimension) {
case D3D11_SRV_DIMENSION_TEXTURE1D: e.width = width; e.height = 0; e.depth = 0; break;
case D3D11_SRV_DIMENSION_TEXTURE2D: e.width = width; e.height = height; e.depth = 0; break;
case D3D11_SRV_DIMENSION_TEXTURE3D: e.width = width; e.height = height; e.depth = depthOrArraySize; break;
case D3D11_SRV_DIMENSION_TEXTURECUBE: e.width = width; e.height = height; e.depth = depthOrArraySize; break;
case D3D11_SRV_DIMENSION_TEXTURE1DARRAY: e.width = width; e.height = 0; e.depth = depthOrArraySize; break;
case D3D11_SRV_DIMENSION_TEXTURE2DARRAY: e.width = width; e.height = height; e.depth = depthOrArraySize; break;
case D3D11_SRV_DIMENSION_TEXTURECUBEARRAY: e.width = width; e.height = height; e.depth = depthOrArraySize; break;
default: assert(0);
}
return e;
}
unsigned int getCudaMipmappedArrayFlagsForD3D12Resource(D3D11_SRV_DIMENSION d3d11SRVDimension, D3D11_BIND_FLAG d3d11BindFlags, bool allowSurfaceLoadStore) {
unsigned int flags = 0;
switch (d3d11SRVDimension) {
case D3D11_SRV_DIMENSION_TEXTURECUBE: flags |= cudaArrayCubemap; break;
case D3D11_SRV_DIMENSION_TEXTURECUBEARRAY: flags |= cudaArrayCubemap | cudaArrayLayered; break;
case D3D11_SRV_DIMENSION_TEXTURE1DARRAY: flags |= cudaArrayLayered; break;
case D3D11_SRV_DIMENSION_TEXTURE2DARRAY: flags |= cudaArrayLayered; break;
default: break;
}
if (d3d11BindFlags & D3D11_BIND_RENDER_TARGET) {
flags |= cudaArrayColorAttachment;
}
if (allowSurfaceLoadStore) {
flags |= cudaArraySurfaceLoadStore;
}
return flags;
}
3.2.16.4.5. 导入同步对象
通过在调用 ID3D11Device5::CreateFence
时设置标志 D3D11_FENCE_FLAG_SHARED
创建的可共享 Direct3D 11 栅栏对象可以使用与该对象关联的 NT 句柄导入到 CUDA 中,如下所示。请注意,应用程序有责任在不再需要 NT 句柄时关闭它。NT 句柄持有对资源的引用,因此在可以释放底层信号量之前,必须显式释放它。
cudaExternalSemaphore_t importD3D11FenceFromNTHandle(HANDLE handle) {
cudaExternalSemaphore_t extSem = NULL;
cudaExternalSemaphoreHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalSemaphoreHandleTypeD3D11Fence;
desc.handle.win32.handle = handle;
cudaImportExternalSemaphore(&extSem, &desc);
// Input parameter 'handle' should be closed if it's not needed anymore
CloseHandle(handle);
return extSem;
}
可共享的 Direct3D 11 栅栏对象也可以使用命名句柄导入(如果存在),如下所示。
cudaExternalSemaphore_t importD3D11FenceFromNamedNTHandle(LPCWSTR name) {
cudaExternalSemaphore_t extSem = NULL;
cudaExternalSemaphoreHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalSemaphoreHandleTypeD3D11Fence;
desc.handle.win32.name = (void *)name;
cudaImportExternalSemaphore(&extSem, &desc);
return extSem;
}
与可共享的 Direct3D 11 资源关联的可共享 Direct3D 11 键控互斥对象,即 IDXGIKeyedMutex
,通过设置标志 D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX
创建,可以使用与该对象关联的 NT 句柄导入到 CUDA 中,如下所示。请注意,应用程序有责任在不再需要 NT 句柄时关闭它。NT 句柄持有对资源的引用,因此在可以释放底层信号量之前,必须显式释放它。
cudaExternalSemaphore_t importD3D11KeyedMutexFromNTHandle(HANDLE handle) {
cudaExternalSemaphore_t extSem = NULL;
cudaExternalSemaphoreHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalSemaphoreHandleTypeKeyedMutex;
desc.handle.win32.handle = handle;
cudaImportExternalSemaphore(&extSem, &desc);
// Input parameter 'handle' should be closed if it's not needed anymore
CloseHandle(handle);
return extSem;
}
可共享的 Direct3D 11 键控互斥对象也可以使用命名句柄导入(如果存在),如下所示。
cudaExternalSemaphore_t importD3D11KeyedMutexFromNamedNTHandle(LPCWSTR name) {
cudaExternalSemaphore_t extSem = NULL;
cudaExternalSemaphoreHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalSemaphoreHandleTypeKeyedMutex;
desc.handle.win32.name = (void *)name;
cudaImportExternalSemaphore(&extSem, &desc);
return extSem;
}
可共享的 Direct3D 11 键控互斥对象可以使用与该对象关联的全局共享 D3DKMT 句柄导入到 CUDA 中,如下所示。由于全局共享 D3DKMT 句柄不持有对底层内存的引用,因此当对资源的所有其他引用都被销毁时,它会自动销毁。
cudaExternalSemaphore_t importD3D11FenceFromKMTHandle(HANDLE handle) {
cudaExternalSemaphore_t extSem = NULL;
cudaExternalSemaphoreHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalSemaphoreHandleTypeKeyedMutexKmt;
desc.handle.win32.handle = handle;
cudaImportExternalSemaphore(&extSem, &desc);
// Input parameter 'handle' should be closed if it's not needed anymore
CloseHandle(handle);
return extSem;
}
3.2.16.4.6. 对导入的同步对象发出信号/等待
可以对导入的 Direct3D 11 栅栏对象发出信号,如下所示。对这样的栅栏对象发出信号会将其值设置为指定的值。等待此信号的相应等待操作必须在 Direct3D 11 中发出。此外,等待此信号的等待操作必须在此信号发出之后发出。
void signalExternalSemaphore(cudaExternalSemaphore_t extSem, unsigned long long value, cudaStream_t stream) {
cudaExternalSemaphoreSignalParams params = {};
memset(¶ms, 0, sizeof(params));
params.params.fence.value = value;
cudaSignalExternalSemaphoresAsync(&extSem, ¶ms, 1, stream);
}
可以等待导入的 Direct3D 11 栅栏对象,如下所示。等待这样的栅栏对象会等待直到其值变得大于或等于指定值。此等待操作正在等待的相应信号必须在 Direct3D 11 中发出。此外,信号必须在此等待操作可以发出之前发出。
void waitExternalSemaphore(cudaExternalSemaphore_t extSem, unsigned long long value, cudaStream_t stream) {
cudaExternalSemaphoreWaitParams params = {};
memset(¶ms, 0, sizeof(params));
params.params.fence.value = value;
cudaWaitExternalSemaphoresAsync(&extSem, ¶ms, 1, stream);
}
可以对导入的 Direct3D 11 键控互斥对象发出信号,如下所示。通过指定键值对这样的键控互斥对象发出信号,会释放该值的键控互斥。等待此信号的相应等待操作必须在 Direct3D 11 中使用相同的键值发出。此外,Direct3D 11 等待操作必须在此信号发出之后发出。
void signalExternalSemaphore(cudaExternalSemaphore_t extSem, unsigned long long key, cudaStream_t stream) {
cudaExternalSemaphoreSignalParams params = {};
memset(¶ms, 0, sizeof(params));
params.params.keyedmutex.key = key;
cudaSignalExternalSemaphoresAsync(&extSem, ¶ms, 1, stream);
}
可以等待导入的 Direct3D 11 键控互斥对象,如下所示。在等待这样的键控互斥对象时,需要一个以毫秒为单位的超时值。等待操作会等待直到键控互斥值等于指定的键值或直到超时时间已过。超时时间间隔也可以是无限值。如果指定了无限值,则超时永远不会经过。必须使用 Windows INFINITE 宏来指定无限超时。此等待操作正在等待的相应信号必须在 Direct3D 11 中发出。此外,Direct3D 11 信号必须在此等待操作可以发出之前发出。
void waitExternalSemaphore(cudaExternalSemaphore_t extSem, unsigned long long key, unsigned int timeoutMs, cudaStream_t stream) {
cudaExternalSemaphoreWaitParams params = {};
memset(¶ms, 0, sizeof(params));
params.params.keyedmutex.key = key;
params.params.keyedmutex.timeoutMs = timeoutMs;
cudaWaitExternalSemaphoresAsync(&extSem, ¶ms, 1, stream);
}
3.2.16.5. NVIDIA 软件通信接口互操作性 (NVSCI)
NvSciBuf 和 NvSciSync 是为以下目的开发的接口
NvSciBuf:允许应用程序在内存中分配和交换缓冲区
NvSciSync:允许应用程序在操作边界管理同步对象
有关这些接口的更多详细信息,请访问:https://docs.nvda.net.cn/drive。
3.2.16.5.1. 导入内存对象
为了分配与给定 CUDA 设备兼容的 NvSciBuf 对象,必须使用 NvSciBuf 属性列表中的 NvSciBufGeneralAttrKey_GpuId
设置相应的 GPU ID,如下所示。可选地,应用程序可以指定以下属性 -
NvSciBufGeneralAttrKey_NeedCpuAccess
:指定缓冲区是否需要 CPU 访问NvSciBufRawBufferAttrKey_Align
:指定NvSciBufType_RawBuffer
的对齐要求NvSciBufGeneralAttrKey_RequiredPerm
:可以为每个 NvSciBuf 内存对象实例的不同 UMD 配置不同的访问权限。例如,为了为 GPU 提供对缓冲区的只读访问权限,使用NvSciBufObjDupWithReducePerm()
和NvSciBufAccessPerm_Readonly
作为输入参数创建一个重复的 NvSciBuf 对象。然后将这个新创建的具有降低权限的重复对象导入到 CUDA 中,如下所示NvSciBufGeneralAttrKey_EnableGpuCache
:控制 GPU L2 缓存性NvSciBufGeneralAttrKey_EnableGpuCompression
:指定 GPU 压缩
注意
有关这些属性及其有效输入选项的更多详细信息,请参阅 NvSciBuf 文档。
以下代码片段说明了它们的示例用法。
NvSciBufObj createNvSciBufObject() {
// Raw Buffer Attributes for CUDA
NvSciBufType bufType = NvSciBufType_RawBuffer;
uint64_t rawsize = SIZE;
uint64_t align = 0;
bool cpuaccess_flag = true;
NvSciBufAttrValAccessPerm perm = NvSciBufAccessPerm_ReadWrite;
NvSciRmGpuId gpuid[] ={};
CUuuid uuid;
cuDeviceGetUuid(&uuid, dev));
memcpy(&gpuid[0].bytes, &uuid.bytes, sizeof(uuid.bytes));
// Disable cache on dev
NvSciBufAttrValGpuCache gpuCache[] = {{gpuid[0], false}};
NvSciBufAttrValGpuCompression gpuCompression[] = {{gpuid[0], NvSciBufCompressionType_GenericCompressible}};
// Fill in values
NvSciBufAttrKeyValuePair rawbuffattrs[] = {
{ NvSciBufGeneralAttrKey_Types, &bufType, sizeof(bufType) },
{ NvSciBufRawBufferAttrKey_Size, &rawsize, sizeof(rawsize) },
{ NvSciBufRawBufferAttrKey_Align, &align, sizeof(align) },
{ NvSciBufGeneralAttrKey_NeedCpuAccess, &cpuaccess_flag, sizeof(cpuaccess_flag) },
{ NvSciBufGeneralAttrKey_RequiredPerm, &perm, sizeof(perm) },
{ NvSciBufGeneralAttrKey_GpuId, &gpuid, sizeof(gpuid) },
{ NvSciBufGeneralAttrKey_EnableGpuCache &gpuCache, sizeof(gpuCache) },
{ NvSciBufGeneralAttrKey_EnableGpuCompression &gpuCompression, sizeof(gpuCompression) }
};
// Create list by setting attributes
err = NvSciBufAttrListSetAttrs(attrListBuffer, rawbuffattrs,
sizeof(rawbuffattrs)/sizeof(NvSciBufAttrKeyValuePair));
NvSciBufAttrListCreate(NvSciBufModule, &attrListBuffer);
// Reconcile And Allocate
NvSciBufAttrListReconcile(&attrListBuffer, 1, &attrListReconciledBuffer,
&attrListConflictBuffer)
NvSciBufObjAlloc(attrListReconciledBuffer, &bufferObjRaw);
return bufferObjRaw;
}
NvSciBufObj bufferObjRo; // Readonly NvSciBuf memory obj
// Create a duplicate handle to the same memory buffer with reduced permissions
NvSciBufObjDupWithReducePerm(bufferObjRaw, NvSciBufAccessPerm_Readonly, &bufferObjRo);
return bufferObjRo;
分配的 NvSciBuf 内存对象可以使用 NvSciBufObj 句柄导入到 CUDA 中,如下所示。应用程序应查询分配的 NvSciBufObj 以获取填充 CUDA 外部内存描述符所需的属性。请注意,属性列表和 NvSciBuf 对象应由应用程序维护。如果导入到 CUDA 中的 NvSciBuf 对象也由其他驱动程序映射,那么根据 NvSciBufGeneralAttrKey_GpuSwNeedCacheCoherency
输出属性值,应用程序必须使用 NvSciSync 对象(请参阅 导入同步对象)作为适当的屏障,以保持 CUDA 和其他驱动程序之间的一致性。
注意
有关如何分配和维护 NvSciBuf 对象的更多详细信息,请参阅 NvSciBuf API 文档。
cudaExternalMemory_t importNvSciBufObject (NvSciBufObj bufferObjRaw) {
/*************** Query NvSciBuf Object **************/
NvSciBufAttrKeyValuePair bufattrs[] = {
{ NvSciBufRawBufferAttrKey_Size, NULL, 0 },
{ NvSciBufGeneralAttrKey_GpuSwNeedCacheCoherency, NULL, 0 },
{ NvSciBufGeneralAttrKey_EnableGpuCompression, NULL, 0 }
};
NvSciBufAttrListGetAttrs(retList, bufattrs,
sizeof(bufattrs)/sizeof(NvSciBufAttrKeyValuePair)));
ret_size = *(static_cast<const uint64_t*>(bufattrs[0].value));
// Note cache and compression are per GPU attributes, so read values for specific gpu by comparing UUID
// Read cacheability granted by NvSciBuf
int numGpus = bufattrs[1].len / sizeof(NvSciBufAttrValGpuCache);
NvSciBufAttrValGpuCache[] cacheVal = (NvSciBufAttrValGpuCache *)bufattrs[1].value;
bool ret_cacheVal;
for (int i = 0; i < numGpus; i++) {
if (memcmp(gpuid[0].bytes, cacheVal[i].gpuId.bytes, sizeof(CUuuid)) == 0) {
ret_cacheVal = cacheVal[i].cacheability);
}
}
// Read compression granted by NvSciBuf
numGpus = bufattrs[2].len / sizeof(NvSciBufAttrValGpuCompression);
NvSciBufAttrValGpuCompression[] compVal = (NvSciBufAttrValGpuCompression *)bufattrs[2].value;
NvSciBufCompressionType ret_compVal;
for (int i = 0; i < numGpus; i++) {
if (memcmp(gpuid[0].bytes, compVal[i].gpuId.bytes, sizeof(CUuuid)) == 0) {
ret_compVal = compVal[i].compressionType);
}
}
/*************** NvSciBuf Registration With CUDA **************/
// Fill up CUDA_EXTERNAL_MEMORY_HANDLE_DESC
cudaExternalMemoryHandleDesc memHandleDesc;
memset(&memHandleDesc, 0, sizeof(memHandleDesc));
memHandleDesc.type = cudaExternalMemoryHandleTypeNvSciBuf;
memHandleDesc.handle.nvSciBufObject = bufferObjRaw;
// Set the NvSciBuf object with required access permissions in this step
memHandleDesc.handle.nvSciBufObject = bufferObjRo;
memHandleDesc.size = ret_size;
cudaImportExternalMemory(&extMemBuffer, &memHandleDesc);
return extMemBuffer;
}
3.2.16.5.2. 将缓冲区映射到导入的内存对象
设备指针可以映射到导入的内存对象上,如下所示。偏移量和大小可以根据分配的 NvSciBufObj
的属性进行填充。所有映射的设备指针都必须使用 cudaFree()
释放。
void * mapBufferOntoExternalMemory(cudaExternalMemory_t extMem, unsigned long long offset, unsigned long long size) {
void *ptr = NULL;
cudaExternalMemoryBufferDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.offset = offset;
desc.size = size;
cudaExternalMemoryGetMappedBuffer(&ptr, extMem, &desc);
// Note: 'ptr' must eventually be freed using cudaFree()
return ptr;
}
3.2.16.5.3. 将 Mipmap 数组映射到导入的内存对象
CUDA Mipmap 数组可以映射到导入的内存对象上,如下所示。偏移量、尺寸和格式可以根据分配的 NvSciBufObj
的属性进行填充。所有映射的 Mipmap 数组都必须使用 cudaFreeMipmappedArray()
释放。以下代码示例展示了当将 Mipmap 数组映射到导入的内存对象上时,如何将 NvSciBuf 属性转换为相应的 CUDA 参数。
注意
Mipmap 级别数必须为 1。
cudaMipmappedArray_t mapMipmappedArrayOntoExternalMemory(cudaExternalMemory_t extMem, unsigned long long offset, cudaChannelFormatDesc *formatDesc, cudaExtent *extent, unsigned int flags, unsigned int numLevels) {
cudaMipmappedArray_t mipmap = NULL;
cudaExternalMemoryMipmappedArrayDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.offset = offset;
desc.formatDesc = *formatDesc;
desc.extent = *extent;
desc.flags = flags;
desc.numLevels = numLevels;
// Note: 'mipmap' must eventually be freed using cudaFreeMipmappedArray()
cudaExternalMemoryGetMappedMipmappedArray(&mipmap, extMem, &desc);
return mipmap;
}
3.2.16.5.4. 导入同步对象
可以使用 cudaDeviceGetNvSciSyncAttributes()
生成与给定 CUDA 设备兼容的 NvSciSync 属性。返回的属性列表可用于创建 NvSciSyncObj
,以保证与给定 CUDA 设备的兼容性。
NvSciSyncObj createNvSciSyncObject() {
NvSciSyncObj nvSciSyncObj
int cudaDev0 = 0;
int cudaDev1 = 1;
NvSciSyncAttrList signalerAttrList = NULL;
NvSciSyncAttrList waiterAttrList = NULL;
NvSciSyncAttrList reconciledList = NULL;
NvSciSyncAttrList newConflictList = NULL;
NvSciSyncAttrListCreate(module, &signalerAttrList);
NvSciSyncAttrListCreate(module, &waiterAttrList);
NvSciSyncAttrList unreconciledList[2] = {NULL, NULL};
unreconciledList[0] = signalerAttrList;
unreconciledList[1] = waiterAttrList;
cudaDeviceGetNvSciSyncAttributes(signalerAttrList, cudaDev0, CUDA_NVSCISYNC_ATTR_SIGNAL);
cudaDeviceGetNvSciSyncAttributes(waiterAttrList, cudaDev1, CUDA_NVSCISYNC_ATTR_WAIT);
NvSciSyncAttrListReconcile(unreconciledList, 2, &reconciledList, &newConflictList);
NvSciSyncObjAlloc(reconciledList, &nvSciSyncObj);
return nvSciSyncObj;
}
NvSciSync 对象(如上创建)可以使用 NvSciSyncObj 句柄导入到 CUDA 中,如下所示。请注意,即使在导入后,NvSciSyncObj 句柄的所有权仍然属于应用程序。
cudaExternalSemaphore_t importNvSciSyncObject(void* nvSciSyncObj) {
cudaExternalSemaphore_t extSem = NULL;
cudaExternalSemaphoreHandleDesc desc = {};
memset(&desc, 0, sizeof(desc));
desc.type = cudaExternalSemaphoreHandleTypeNvSciSync;
desc.handle.nvSciSyncObj = nvSciSyncObj;
cudaImportExternalSemaphore(&extSem, &desc);
// Deleting/Freeing the nvSciSyncObj beyond this point will lead to undefined behavior in CUDA
return extSem;
}
3.2.16.5.5. 对导入的同步对象发出信号/等待
可以按照以下概述对导入的 NvSciSyncObj
对象发出信号。对 NvSciSync 支持的信号量对象发出信号会初始化作为输入传递的 fence 参数。此 fence 参数由与上述信号对应的等待操作等待。此外,等待此信号的等待操作必须在此信号发出之后发出。如果标志设置为 cudaExternalSemaphoreSignalSkipNvSciBufMemSync
,则默认情况下作为信号操作一部分执行的内存同步操作(针对此进程中的所有导入的 NvSciBuf)将被跳过。当 NvsciBufGeneralAttrKey_GpuSwNeedCacheCoherency
为 FALSE 时,应设置此标志。
void signalExternalSemaphore(cudaExternalSemaphore_t extSem, cudaStream_t stream, void *fence) {
cudaExternalSemaphoreSignalParams signalParams = {};
memset(&signalParams, 0, sizeof(signalParams));
signalParams.params.nvSciSync.fence = (void*)fence;
signalParams.flags = 0; //OR cudaExternalSemaphoreSignalSkipNvSciBufMemSync
cudaSignalExternalSemaphoresAsync(&extSem, &signalParams, 1, stream);
}
可以按照以下概述等待导入的 NvSciSyncObj
对象。等待 NvSciSync 支持的信号量对象会等待直到输入 fence 参数被相应的信号发送者发出信号。此外,信号必须在等待操作可以发出之前发出。如果标志设置为 cudaExternalSemaphoreWaitSkipNvSciBufMemSync
,则默认情况下作为信号操作一部分执行的内存同步操作(针对此进程中的所有导入的 NvSciBuf)将被跳过。当 NvsciBufGeneralAttrKey_GpuSwNeedCacheCoherency
为 FALSE 时,应设置此标志。
void waitExternalSemaphore(cudaExternalSemaphore_t extSem, cudaStream_t stream, void *fence) {
cudaExternalSemaphoreWaitParams waitParams = {};
memset(&waitParams, 0, sizeof(waitParams));
waitParams.params.nvSciSync.fence = (void*)fence;
waitParams.flags = 0; //OR cudaExternalSemaphoreWaitSkipNvSciBufMemSync
cudaWaitExternalSemaphoresAsync(&extSem, &waitParams, 1, stream);
}
3.3. 版本控制和兼容性
开发人员在开发 CUDA 应用程序时应关注两个版本号:描述计算设备的一般规范和功能的计算能力(请参阅 计算能力)以及描述驱动程序 API 和运行时支持的功能的 CUDA 驱动程序 API 版本。
驱动程序 API 的版本在驱动程序头文件中定义为 CUDA_VERSION
。它允许开发人员检查他们的应用程序是否需要比当前安装的驱动程序更新的设备驱动程序。这一点很重要,因为驱动程序 API 是向后兼容的,这意味着针对特定版本的驱动程序 API 编译的应用程序、插件和库(包括 CUDA 运行时)将继续在后续的设备驱动程序版本上工作,如图 12所示。驱动程序 API 不是向前兼容的,这意味着针对特定版本的驱动程序 API 编译的应用程序、插件和库(包括 CUDA 运行时)将无法在以前版本的设备驱动程序上工作。
重要的是要注意,对支持的版本混合和匹配存在限制
由于系统上一次只能安装一个版本的 CUDA 驱动程序,因此安装的驱动程序版本必须等于或高于任何必须在该系统上运行的应用程序、插件或库所构建的最大驱动程序 API 版本。
除非插件和库静态链接到运行时,否则应用程序使用的所有插件和库都必须使用相同版本的 CUDA 运行时。如果静态链接到运行时,则同一进程空间中可以共存多个版本的运行时。请注意,如果使用
nvcc
链接应用程序,则默认情况下将使用 CUDA 运行时库的静态版本,并且所有 CUDA 工具包库都静态链接到 CUDA 运行时。除非静态链接到那些库,否则应用程序使用的所有插件和库都必须使用相同版本的任何使用运行时的库(例如 cuFFT、cuBLAS 等)。

图 25 驱动程序 API 是向后兼容的,但不是向前兼容的
对于 Tesla GPU 产品,CUDA 10 为 CUDA 驱动程序的用户模式组件引入了新的向前兼容升级路径。此功能在CUDA 兼容性中描述。此处描述的关于 CUDA 驱动程序版本的要求适用于用户模式组件的版本。
3.4. 计算模式
在运行 Windows Server 2008 及更高版本或 Linux 的 Tesla 解决方案上,可以使用 NVIDIA 的系统管理界面 (nvidia-smi) 将系统中的任何设备设置为以下三种模式之一,该工具作为驱动程序的一部分分发
默认计算模式:多个主机线程可以同时使用该设备(在使用运行时 API 时调用此设备上的
cudaSetDevice()
,或者在使用驱动程序 API 时使与设备关联的上下文成为当前上下文)。独占进程计算模式:在系统中的所有进程中,只能在该设备上创建一个 CUDA 上下文。上下文可以是创建该上下文的进程中所需数量的线程的当前上下文。
禁止计算模式:无法在该设备上创建 CUDA 上下文。
这意味着,特别是,如果设备 0 处于禁止模式或独占进程模式并被另一个进程使用,则使用运行时 API 而不显式调用 cudaSetDevice()
的主机线程可能与设备 0 以外的设备关联。cudaSetValidDevices()
可用于从设备优先级列表中设置设备。
另请注意,对于采用 Pascal 架构及更高版本的设备(主修订号为 6 及更高的计算能力),存在对计算抢占的支持。这允许以指令级粒度抢占计算任务,而不是像之前的 Maxwell 和 Kepler GPU 架构那样以线程块粒度抢占,其优点是可以防止长时间运行内核的应用程序垄断系统或超时。但是,计算抢占会带来上下文切换开销,对于支持该功能的设备,计算抢占会自动启用。可以使用具有属性 cudaDevAttrComputePreemptionSupported
的单个属性查询函数 cudaDeviceGetAttribute()
来确定正在使用的设备是否支持计算抢占。希望避免与不同进程相关的上下文切换开销的用户可以确保 GPU 上只有一个进程处于活动状态,方法是选择独占进程模式。
应用程序可以通过检查 computeMode
设备属性来查询设备的计算模式(请参阅设备枚举)。
3.5. 模式切换
具有显示输出的 GPU 会将一些 DRAM 内存专用于所谓的主表面,该主表面用于刷新用户查看的显示设备的输出。当用户通过更改显示器的分辨率或位深度(使用 NVIDIA 控制面板或 Windows 上的显示控制面板)来启动显示器的模式切换时,主表面所需的内存量会发生变化。例如,如果用户将显示分辨率从 1280x1024x32 位更改为 1600x1200x32 位,则系统必须为主表面分配 7.68 MB,而不是 5.24 MB。(启用抗锯齿的全屏图形应用程序可能需要更多显示内存用于主表面。)在 Windows 上,其他可能启动显示模式切换的事件包括启动全屏 DirectX 应用程序、按下 Alt+Tab 键从全屏 DirectX 应用程序切换任务,或按下 Ctrl+Alt+Del 键锁定计算机。
如果模式切换增加了主表面所需的内存量,则系统可能不得不蚕食专用于 CUDA 应用程序的内存分配。因此,模式切换会导致对 CUDA 运行时的任何调用失败并返回无效的上下文错误。
3.6. Windows 的 Tesla 计算集群模式
使用 NVIDIA 的系统管理界面 (nvidia-smi),可以将 Windows 设备驱动程序置于 Tesla 和 Quadro 系列设备的 TCC(Tesla 计算集群)模式。
TCC 模式移除了对任何图形功能的支持。
4. 硬件实现
NVIDIA GPU 架构围绕多线程流式多处理器 (SM) 的可扩展阵列构建。当主机 CPU 上的 CUDA 程序调用内核网格时,网格的块会被枚举并分配给具有可用执行能力的多处理器。线程块的线程在一个多处理器上并发执行,并且多个线程块可以在一个多处理器上并发执行。随着线程块终止,新的块会在空出的多处理器上启动。
多处理器旨在并发执行数百个线程。为了管理如此大量的线程,它采用了一种称为 SIMT (单指令多线程) 的独特架构,该架构在SIMT 架构中进行了描述。指令是流水线式的,利用单个线程内的指令级并行性,以及通过同步硬件多线程实现的广泛的线程级并行性,如硬件多线程中详述。与 CPU 核心不同,它们按顺序发出,并且没有分支预测或推测执行。
SIMT 架构和硬件多线程描述了所有设备通用的流式多处理器的架构特性。计算能力 5.x、计算能力 6.x和计算能力 7.x分别提供了计算能力为 5.x、6.x 和 7.x 的设备的具体信息。
NVIDIA GPU 架构使用小端表示。
4.1. SIMT 架构
多处理器以 32 个并行线程为一组(称为warp)创建、管理、调度和执行线程。组成 warp 的各个线程在相同的程序地址处一起启动,但它们有自己的指令地址计数器和寄存器状态,因此可以自由分支和独立执行。术语 warp 源于编织,这是第一种并行线程技术。半个 warp 是 warp 的前半部分或后半部分。四分之一 warp 是 warp 的第一、第二、第三或第四个四分之一。
当多处理器被赋予一个或多个线程块来执行时,它会将它们划分为 warp,并且每个 warp 都由warp 调度器调度以执行。将块划分为 warp 的方式始终相同;每个 warp 都包含线程 ID 连续递增的线程,其中第一个 warp 包含线程 0。线程层次结构描述了线程 ID 如何与块中的线程索引相关。
一个 warp 一次执行一条公共指令,因此当一个 warp 的所有 32 个线程就其执行路径达成一致时,才能实现完全效率。如果一个 warp 的线程通过数据相关的条件分支发散,则该 warp 会执行所采用的每个分支路径,禁用不在该路径上的线程。分支发散仅在 warp 内发生;不同的 warp 独立执行,无论它们执行的是公共代码路径还是不相交的代码路径。
SIMT 架构类似于 SIMD(单指令多数据)向量组织,因为单条指令控制多个处理元素。一个关键的区别在于,SIMD 向量组织向软件公开 SIMD 宽度,而 SIMT 指令指定单个线程的执行和分支行为。与 SIMD 向量机相比,SIMT 使程序员能够为独立的标量线程编写线程级并行代码,以及为协调线程编写数据并行代码。为了正确性,程序员基本上可以忽略 SIMT 行为;但是,通过注意代码很少需要 warp 中的线程发散,可以实现显着的性能提升。实际上,这类似于传统代码中缓存行的作用:在设计正确性时可以安全地忽略缓存行大小,但在设计峰值性能时必须在代码结构中考虑缓存行大小。另一方面,向量架构要求软件将加载合并到向量中并手动管理发散。
在 NVIDIA Volta 之前,warp 使用在 warp 中所有 32 个线程之间共享的单个程序计数器,以及指定 warp 的活动线程的活动掩码。因此,来自同一 warp 中发散区域或不同执行状态的线程无法相互发出信号或交换数据,并且需要细粒度数据共享(由锁或互斥锁保护)的算法很容易导致死锁,具体取决于争用线程来自哪个 warp。
从 NVIDIA Volta 架构开始,独立线程调度允许线程之间完全并发,而无需考虑 warp。通过独立线程调度,GPU 维护每个线程的执行状态,包括程序计数器和调用堆栈,并且可以以每个线程的粒度让出执行权,以便更好地利用执行资源或允许一个线程等待另一个线程生成的数据。调度优化器确定如何将来自同一 warp 的活动线程分组到 SIMT 单元中。这保留了先前 NVIDIA GPU 中的 SIMT 执行的高吞吐量,但具有更大的灵活性:线程现在可以在子 warp 粒度上发散和重新聚合。
如果开发人员对先前硬件架构的 warp 同步2做出了假设,则独立线程调度可能会导致参与执行代码的线程集与预期的大相径庭。特别是,任何 warp 同步代码(例如,无同步、warp 内缩减)都应重新审视,以确保与 NVIDIA Volta 及更高版本的兼容性。有关更多详细信息,请参阅计算能力 7.x。
注意
参与当前指令的 warp 线程称为活动线程,而不在当前指令上的线程是非活动线程(禁用)。线程可能由于多种原因而处于非活动状态,包括早于其 warp 的其他线程退出、采用与 warp 当前执行的分支路径不同的分支路径,或者是因为线程数不是 warp 大小的倍数的块的最后一个线程。
如果 warp 执行的非原子指令写入全局或共享内存中的相同位置,并且对于 warp 的多个线程,则发生对该位置的序列化写入次数因设备的计算能力而异(请参阅计算能力 5.x、计算能力 6.x和计算能力 7.x),并且哪个线程执行最终写入是未定义的。
如果 warp 执行的原子指令读取、修改并写入全局内存中的相同位置,并且对于 warp 的多个线程,则每次对该位置的读取/修改/写入都会发生,并且它们都是序列化的,但它们发生的顺序是未定义的。
4.2. 硬件多线程
多处理器处理的每个 warp 的执行上下文(程序计数器、寄存器等)在 warp 的整个生命周期内都保持在芯片上。因此,从一个执行上下文切换到另一个执行上下文没有成本,并且在每个指令发布时间,warp 调度器都会选择一个 warp,该 warp 的线程已准备好执行其下一条指令(warp 的活动线程),并将指令发布给这些线程。
特别是,每个多处理器都有一组在 warp 之间分区的 32 位寄存器,以及一个在线程块之间分区的并行数据缓存或共享内存。
给定内核的可以在多处理器上共存并一起处理的块和 warp 的数量取决于内核使用的寄存器和共享内存量,以及多处理器上可用的寄存器和共享内存量。每个多处理器还存在最大驻留块数和最大驻留 warp 数。这些限制以及多处理器上可用的寄存器和共享内存量是设备计算能力的函数,并在计算能力中给出。如果每个多处理器没有足够的寄存器或共享内存来处理至少一个块,则内核将无法启动。
一个块中的 warp 总数如下
\(\text{ceil}\left( \frac{T}{W_{size}},1 \right)\)
T 是每个块的线程数,
Wsize 是 warp 大小,等于 32,
ceil(x, y) 等于 x 向上舍入到 y 的最接近倍数。
块分配的寄存器总数和共享内存总量记录在 CUDA 工具包中提供的 CUDA 占用率计算器中。
- 2
术语 warp 同步是指隐式假定同一 warp 中的线程在每条指令处同步的代码。
5. 性能指南
5.1. 总体性能优化策略
性能优化围绕四个基本策略展开
最大化并行执行以实现最大利用率;
优化内存使用以实现最大内存吞吐量;
优化指令使用以实现最大指令吞吐量;
最小化内存抖动。
哪些策略将为应用程序的特定部分带来最佳性能提升取决于该部分的性能限制因素;例如,优化主要受内存访问限制的内核的指令使用不会产生任何显着的性能提升。因此,优化工作应始终通过测量和监控性能限制因素来指导,例如使用 CUDA 分析器。此外,将特定内核的浮点运算吞吐量或内存吞吐量(无论哪个更有意义)与设备相应的峰值理论吞吐量进行比较,可以表明内核有多少改进空间。
5.2. 最大化利用率
为了最大化利用率,应用程序的结构应使其尽可能地暴露并行性,并将这种并行性有效地映射到系统的各个组件,以使其大部分时间都处于繁忙状态。
5.2.1. 应用层
在高层,应用程序应通过使用异步并发执行中描述的异步函数调用和流,最大化主机、设备以及连接主机和设备的总线之间的并行执行。它应该为每个处理器分配最适合其类型的工作:串行工作负载分配给主机;并行工作负载分配给设备。
对于并行工作负载,在算法中由于某些线程需要同步才能相互共享数据而导致并行性中断的点上,有两种情况:要么这些线程属于同一个块,在这种情况下,它们应该使用 __syncthreads()
并通过同一内核调用中的共享内存共享数据,要么它们属于不同的块,在这种情况下,它们必须使用两个单独的内核调用(一个用于写入全局内存,另一个用于从全局内存读取)通过全局内存共享数据。第二种情况远非最佳,因为它增加了额外内核调用和全局内存流量的开销。因此,应通过以尽可能在单个线程块内执行需要线程间通信的计算的方式将算法映射到 CUDA 编程模型来最大程度地减少其发生。
5.2.2. 设备层
在较低层,应用程序应最大化设备多处理器之间的并行执行。
多个内核可以在设备上并发执行,因此也可以通过使用流来启用足够的内核并发执行来实现最大利用率,如异步并发执行中所述。
5.2.3. 多处理器层
在更低的层,应用程序应最大化多处理器内各个功能单元之间的并行执行。
如硬件多线程中所述,GPU 多处理器主要依靠线程级并行性来最大化其功能单元的利用率。因此,利用率与驻留 warp 的数量直接相关。在每个指令发布时间,warp 调度器都会选择一条准备好执行的指令。此指令可以是同一 warp 的另一条独立指令(利用指令级并行性),或者更常见的是另一 warp 的指令(利用线程级并行性)。如果选择了准备好执行的指令,则会将其发布给 warp 的活动线程。warp 准备好执行其下一条指令所需的时钟周期数称为延迟,当所有 warp 调度器总是在该延迟期间的每个时钟周期都有一些指令要为某些 warp 发布时,或者换句话说,当延迟完全“隐藏”时,才能实现完全利用率。隐藏 L 个时钟周期的延迟所需的指令数取决于这些指令各自的吞吐量(有关各种算术指令的吞吐量,请参阅算术指令)。如果我们假设具有最大吞吐量的指令,则它等于
对于计算能力为 5.x、6.1、6.2、7.x 和 8.x 的设备,4L,因为对于这些设备,如计算能力中所述,多处理器在每个时钟周期为一个 warp 发布一条指令,一次发布四个 warp 的指令。
对于计算能力为 6.0 的设备,2L,因为对于这些设备,每个周期发布的两条指令是两条不同 warp 的指令。
warp 未准备好执行其下一条指令的最常见原因是指令的输入操作数尚不可用。
如果所有输入操作数都是寄存器,则延迟是由寄存器依赖性引起的,即,某些输入操作数由某些先前指令写入,而这些指令的执行尚未完成。在这种情况下,延迟等于先前指令的执行时间,并且 warp 调度器必须在该时间内调度其他 warp 的指令。执行时间因指令而异。在计算能力为 7.x 的设备上,对于大多数算术指令,它通常为 4 个时钟周期。这意味着每个多处理器需要 16 个活动 warp(4 个周期,4 个 warp 调度器)才能隐藏算术指令延迟(假设 warp 执行具有最大吞吐量的指令,否则需要的 warp 更少)。如果单个 warp 表现出指令级并行性,即在其指令流中具有多个独立指令,则需要的 warp 更少,因为可以背靠背地发布来自单个 warp 的多个独立指令。
如果某些输入操作数驻留在片外内存中,则延迟会更高:通常为数百个时钟周期。在这种高延迟期间保持 warp 调度器繁忙所需的 warp 数量取决于内核代码及其指令级并行度。一般来说,如果没有任何片外内存操作数的指令(即,大多数时候是算术指令)的数量与具有片外内存操作数的指令的数量之比很低(此比率通常称为程序的算术强度),则需要更多 warp。
warp 未准备好执行其下一条指令的另一个原因是它正在某个内存栅栏(内存栅栏函数)或同步点(同步函数)处等待。同步点可能会强制多处理器空闲,因为越来越多的 warp 正在等待同一块中的其他 warp 完成同步点之前的指令执行。在这种情况下,每个多处理器拥有多个驻留块可以帮助减少空闲,因为来自不同块的 warp 不需要在同步点相互等待。
给定内核调用的每个多处理器上驻留的块和 warp 的数量取决于调用的执行配置(执行配置)、多处理器的内存资源以及内核的资源需求,如硬件多线程中所述。寄存器和共享内存使用情况由编译器在使用 --ptxas-options=-v
选项编译时报告。
块所需的共享内存总量等于静态分配的共享内存量和动态分配的共享内存量之和。
内核使用的寄存器数量会对驻留 warp 的数量产生重大影响。例如,对于计算能力为 6.x 的设备,如果内核使用 64 个寄存器,并且每个块有 512 个线程,并且只需要很少的共享内存,那么两个块(即 32 个 warp)可以驻留在多处理器上,因为它们需要 2x512x64 个寄存器,这与多处理器上可用的寄存器数量完全匹配。但是,一旦内核多使用一个寄存器,就只能驻留一个块(即 16 个 warp),因为两个块将需要 2x512x65 个寄存器,这比多处理器上可用的寄存器更多。因此,编译器会尝试最小化寄存器使用量,同时将寄存器溢出(请参阅设备内存访问)和指令数量保持在最低限度。可以使用 maxrregcount
编译器选项、启动边界中描述的 __launch_bounds__()
限定符或每个线程的最大寄存器数中描述的 __maxnreg__()
限定符来控制寄存器使用量。
寄存器文件组织为 32 位寄存器。因此,存储在寄存器中的每个变量至少需要一个 32 位寄存器,例如,一个 double
变量使用两个 32 位寄存器。
给定内核调用的执行配置对性能的影响通常取决于内核代码。因此,建议进行实验。应用程序还可以根据寄存器文件大小和共享内存大小来参数化执行配置,这取决于设备的计算能力,以及设备的多处理器数量和内存带宽,所有这些都可以使用运行时查询(请参阅参考手册)。
每个块的线程数应选择为 warp 大小的倍数,以尽可能避免使用未充分填充的 warp 浪费计算资源。
5.2.3.1. 占用率计算器
存在多个 API 函数来帮助程序员根据寄存器和共享内存要求选择线程块大小和集群大小。
占用率计算器 API
cudaOccupancyMaxActiveBlocksPerMultiprocessor
可以根据内核的块大小和共享内存使用情况提供占用率预测。此函数以每个多处理器的并发线程块数报告占用率。请注意,此值可以转换为其他指标。乘以每个块的 warp 数,即可得出每个多处理器的并发 warp 数;进一步将并发 warp 除以每个多处理器的最大 warp 数,即可得出以百分比表示的占用率。
基于占用率的启动配置器 API
cudaOccupancyMaxPotentialBlockSize
和cudaOccupancyMaxPotentialBlockSizeVariableSMem
启发式地计算实现最大多处理器级占用率的执行配置。占用率计算器 API
cudaOccupancyMaxActiveClusters
可以根据内核的集群大小、块大小和共享内存使用情况提供占用率预测。此函数以系统中存在的给定大小的最大活动集群数报告占用率。
以下代码示例计算 MyKernel 的占用率。然后,它报告并发 warp 与每个多处理器的最大 warp 数之间的比率的占用率水平。
// Device code
__global__ void MyKernel(int *d, int *a, int *b)
{
int idx = threadIdx.x + blockIdx.x * blockDim.x;
d[idx] = a[idx] * b[idx];
}
// Host code
int main()
{
int numBlocks; // Occupancy in terms of active blocks
int blockSize = 32;
// These variables are used to convert occupancy to warps
int device;
cudaDeviceProp prop;
int activeWarps;
int maxWarps;
cudaGetDevice(&device);
cudaGetDeviceProperties(&prop, device);
cudaOccupancyMaxActiveBlocksPerMultiprocessor(
&numBlocks,
MyKernel,
blockSize,
0);
activeWarps = numBlocks * blockSize / prop.warpSize;
maxWarps = prop.maxThreadsPerMultiProcessor / prop.warpSize;
std::cout << "Occupancy: " << (double)activeWarps / maxWarps * 100 << "%" << std::endl;
return 0;
}
以下代码示例根据用户输入配置 MyKernel 的基于占用率的内核启动。
// Device code
__global__ void MyKernel(int *array, int arrayCount)
{
int idx = threadIdx.x + blockIdx.x * blockDim.x;
if (idx < arrayCount) {
array[idx] *= array[idx];
}
}
// Host code
int launchMyKernel(int *array, int arrayCount)
{
int blockSize; // The launch configurator returned block size
int minGridSize; // The minimum grid size needed to achieve the
// maximum occupancy for a full device
// launch
int gridSize; // The actual grid size needed, based on input
// size
cudaOccupancyMaxPotentialBlockSize(
&minGridSize,
&blockSize,
(void*)MyKernel,
0,
arrayCount);
// Round up according to array size
gridSize = (arrayCount + blockSize - 1) / blockSize;
MyKernel<<<gridSize, blockSize>>>(array, arrayCount);
cudaDeviceSynchronize();
// If interested, the occupancy can be calculated with
// cudaOccupancyMaxActiveBlocksPerMultiprocessor
return 0;
}
以下代码示例显示如何使用集群占用率 API 查找给定大小的最大活动集群数。下面的示例代码计算大小为 2 且每个块 128 个线程的集群的占用率。
从计算能力 9.0 开始,集群大小为 8 是向前兼容的,但在太小而无法支持 8 个多处理器的 GPU 硬件或 MIG 配置上除外,在这种情况下,最大集群大小将减小。但建议用户在启动集群内核之前查询最大集群大小。可以使用 cudaOccupancyMaxPotentialClusterSize
API 查询最大集群大小。
{
cudaLaunchConfig_t config = {0};
config.gridDim = number_of_blocks;
config.blockDim = 128; // threads_per_block = 128
config.dynamicSmemBytes = dynamic_shared_memory_size;
cudaLaunchAttribute attribute[1];
attribute[0].id = cudaLaunchAttributeClusterDimension;
attribute[0].val.clusterDim.x = 2; // cluster_size = 2
attribute[0].val.clusterDim.y = 1;
attribute[0].val.clusterDim.z = 1;
config.attrs = attribute;
config.numAttrs = 1;
int max_cluster_size = 0;
cudaOccupancyMaxPotentialClusterSize(&max_cluster_size, (void *)kernel, &config);
int max_active_clusters = 0;
cudaOccupancyMaxActiveClusters(&max_active_clusters, (void *)kernel, &config);
std::cout << "Max Active Clusters of size 2: " << max_active_clusters << std::endl;
}
CUDA Nsight Compute 用户界面还在 <CUDA_Toolkit_Path>/include/cuda_occupancy.h
中提供了独立的占用率计算器和启动配置器实现,用于任何不能依赖 CUDA 软件堆栈的用例。Nsight Compute 版本的占用率计算器作为一种学习工具特别有用,它可以可视化参数更改对占用率的影响(块大小、每个线程的寄存器数和每个线程的共享内存)。
5.3. 最大化内存吞吐量
最大化应用程序整体内存吞吐量的第一步是尽量减少低带宽的数据传输。
这意味着尽量减少主机和设备之间的数据传输,如主机和设备之间的数据传输中详述的那样,因为这些传输的带宽远低于全局内存和设备之间的数据传输。
这也意味着通过最大化片上内存(即共享内存和缓存)的使用来尽量减少全局内存和设备之间的数据传输:L1 缓存和 L2 缓存(在计算能力为 2.x 及更高版本的设备上可用)、纹理缓存和常量缓存在所有设备上都可用。
共享内存相当于用户管理的缓存:应用程序显式地分配和访问它。正如CUDA 运行时中所示,一种典型的编程模式是将来自设备内存的数据暂存到共享内存中;换句话说,让一个块的每个线程
从设备内存加载数据到共享内存,
与块的所有其他线程同步,以便每个线程可以安全地读取由不同线程填充的共享内存位置,
处理共享内存中的数据,
如果需要再次同步,以确保共享内存已使用结果更新,
将结果写回设备内存。
对于某些应用程序(例如,全局内存访问模式取决于数据的应用程序),传统的硬件管理缓存更适合利用数据局部性。正如计算能力 7.x、计算能力 8.x 和 计算能力 9.0 中提到的,对于计算能力为 7.x、8.x 和 9.0 的设备,相同的片上内存用于 L1 和共享内存,并且每个内核调用都可以配置多少内存专用于 L1 与共享内存。
内核的内存访问吞吐量可能会因每种内存类型的访问模式而异,差异可达一个数量级。因此,最大化内存吞吐量的下一步是根据设备内存访问中描述的最佳内存访问模式,尽可能优化地组织内存访问。这种优化对于全局内存访问尤为重要,因为与可用的片上带宽和算术指令吞吐量相比,全局内存带宽较低,因此非最佳的全局内存访问通常对性能有很大影响。
5.3.1. 主机和设备之间的数据传输
应用程序应努力尽量减少主机和设备之间的数据传输。实现此目的的一种方法是将更多代码从主机移动到设备,即使这意味着运行的内核没有暴露足够的并行性以在设备上充分高效地执行。中间数据结构可以在设备内存中创建,由设备操作,并在不被主机映射或复制到主机内存的情况下销毁。
此外,由于每次传输都有开销,因此将许多小传输批处理成单个大传输总是比单独进行每次传输性能更好。
在使用前端总线的系统上,通过使用页锁定主机内存中描述的页锁定主机内存,可以实现主机和设备之间更高的数据传输性能。
此外,当使用映射的页锁定内存(映射内存)时,无需分配任何设备内存并在设备内存和主机内存之间显式复制数据。每次内核访问映射内存时,都会隐式执行数据传输。为了获得最佳性能,这些内存访问必须像访问全局内存一样进行合并(请参阅设备内存访问)。假设它们是合并的,并且映射的内存仅读取或写入一次,那么使用映射的页锁定内存而不是设备内存和主机内存之间的显式复制,可以提高性能。
在设备内存和主机内存物理上相同的集成系统上,主机内存和设备内存之间的任何复制都是多余的,应改为使用映射的页锁定内存。应用程序可以查询设备是否是 integrated
,方法是检查 integrated 设备属性(请参阅设备枚举)是否等于 1。
5.3.2. 设备内存访问
访问可寻址内存(即全局内存、本地内存、共享内存、常量内存或纹理内存)的指令可能需要多次重新发布,具体取决于 warp 中线程的内存地址分布。分布如何以这种方式影响指令吞吐量取决于每种内存类型,并在以下章节中描述。例如,对于全局内存,一般来说,地址越分散,吞吐量降低得越多。
全局内存
全局内存驻留在设备内存中,设备内存通过 32 字节、64 字节或 128 字节的内存事务访问。这些内存事务必须是自然对齐的:只有与其大小对齐的设备内存的 32 字节、64 字节或 128 字节段(即,其第一个地址是其大小的倍数)才能通过内存事务读取或写入。
当 warp 执行访问全局内存的指令时,它会根据每个线程访问的字大小和线程的内存地址分布,将 warp 中线程的内存访问合并为一个或多个这些内存事务。一般来说,需要的事务越多,除了线程访问的字之外,传输的未使用字也越多,从而相应地降低指令吞吐量。例如,如果为每个线程的 4 字节访问生成一个 32 字节的内存事务,则吞吐量除以 8。
所需的事务数量以及最终受影响的吞吐量随设备的计算能力而异。计算能力 5.x、计算能力 6.x、计算能力 7.x、计算能力 8.x 和 计算能力 9.0 提供了关于如何处理各种计算能力的全局内存访问的更多详细信息。
因此,为了最大化全局内存吞吐量,重要的是通过以下方式最大化合并:
使用满足下面大小和对齐要求部分中详细说明的大小和对齐要求的数据类型,
在某些情况下填充数据,例如,当访问下面二维数组部分中描述的二维数组时。
大小和对齐要求
全局内存指令支持读取或写入大小等于 1、2、4、8 或 16 字节的字。当且仅当数据类型的大小为 1、2、4、8 或 16 字节,并且数据是自然对齐的(即,其地址是该大小的倍数)时,对驻留在全局内存中的数据(通过变量或指针)的任何访问都会编译为单个全局内存指令。
如果未满足此大小和对齐要求,则访问将编译为多个指令,这些指令具有交错的访问模式,从而阻止这些指令完全合并。因此,建议对驻留在全局内存中的数据使用满足此要求的类型。
对于内置向量类型,自动满足对齐要求。
对于结构,可以使用对齐说明符 __align__(8) 或 __align__(16)
通过编译器强制执行大小和对齐要求,例如
struct __align__(8) {
float x;
float y;
};
或
struct __align__(16) {
float x;
float y;
float z;
};
驻留在全局内存中的变量的任何地址或驱动程序或运行时 API 的内存分配例程返回的任何地址始终至少对齐到 256 字节。
读取非自然对齐的 8 字节或 16 字节字会产生不正确的结果(偏差几个字),因此必须特别注意维护这些类型的任何值或值数组的起始地址的对齐方式。一个容易被忽视的典型情况是使用一些自定义的全局内存分配方案,其中多个数组的分配(通过多次调用 cudaMalloc()
或 cuMemAlloc()
)被替换为单个大型内存块的分配,该内存块被划分为多个数组,在这种情况下,每个数组的起始地址都与块的起始地址偏移。
二维数组
常见的全局内存访问模式是当索引为 (tx,ty)
的每个线程使用以下地址访问宽度为 width
的二维数组的一个元素时,该数组位于地址 BaseAddress
,类型为 type*
(其中 type
满足最大化利用率中描述的要求)
BaseAddress + width * ty + tx
为了使这些访问完全合并,线程块的宽度和数组的宽度都必须是 warp 大小的倍数。
特别是,这意味着如果数组的宽度不是此大小的倍数,则如果实际分配的宽度向上舍入到最接近的此大小的倍数并且其行相应地填充,则访问效率会更高。cudaMallocPitch()
和 cuMemAllocPitch()
函数以及参考手册中描述的相关内存复制函数使程序员能够编写非硬件相关的代码来分配符合这些约束的数组。
本地内存
本地内存访问仅在某些自动变量中发生,如变量内存空间说明符中所述。编译器可能放置在本地内存中的自动变量是
无法确定是否使用常量数量索引的数组,
会消耗过多寄存器空间的大型结构或数组,
如果内核使用的寄存器多于可用寄存器,则任何变量(这也称为寄存器溢出)。
检查 PTX 汇编代码(通过使用 -ptx
或 -keep
选项编译获得)将告诉您变量是否在第一编译阶段已放置在本地内存中,因为它将使用 .local
助记符声明,并使用 ld.local
和 st.local
助记符访问。即使没有,随后的编译阶段可能仍然会另做决定,如果他们发现它为目标架构消耗了过多的寄存器空间:使用 cuobjdump
检查 cubin 对象将告诉您是否是这种情况。此外,当使用 --ptxas-options=-v
选项编译时,编译器会报告每个内核的总本地内存使用量 (lmem
)。请注意,某些数学函数具有可能访问本地内存的实现路径。
本地内存空间驻留在设备内存中,因此本地内存访问与全局内存访问具有相同的高延迟和低带宽,并且受到与设备内存访问中描述的内存合并相同的要求。但是,本地内存的组织方式是,连续的 32 位字由连续的线程 ID 访问。因此,只要 warp 中的所有线程都访问相同的相对地址(例如,数组变量中的相同索引,结构变量中的相同成员),访问就会完全合并。
在计算能力为 5.x 及更高版本的设备上,本地内存访问始终像全局内存访问一样在 L2 中缓存(请参阅计算能力 5.x 和 计算能力 6.x)。
共享内存
由于共享内存是片上的,因此它比本地内存或全局内存具有更高的带宽和更低的延迟。
为了实现高带宽,共享内存被划分为大小相等的内存模块,称为 bank,可以同时访问。因此,对落在 n 个不同内存 bank 中的 n 个地址发出的任何内存读取或写入请求都可以同时得到服务,从而产生比单个模块带宽高 n 倍的整体带宽。
但是,如果内存请求的两个地址落在同一个内存 bank 中,则会发生 bank 冲突,并且访问必须串行化。硬件会将具有 bank 冲突的内存请求拆分为尽可能多的单独的无冲突请求,从而将吞吐量降低的系数等于单独内存请求的数量。如果单独内存请求的数量为 n,则初始内存请求据说会导致 n 路 bank 冲突。
为了获得最佳性能,因此重要的是了解内存地址如何映射到内存 bank,以便调度内存请求,从而最大限度地减少 bank 冲突。这在 计算能力 5.x、计算能力 6.x、计算能力 7.x、计算能力 8.x 和 计算能力 9.0 中进行了描述,分别针对计算能力为 5.x、6.x、7.x、8.x 和 9.0 的设备。
常量内存
常量内存空间驻留在设备内存中,并在常量缓存中缓存。
然后,请求被拆分为与初始请求中不同内存地址一样多的单独请求,从而将吞吐量降低的系数等于单独请求的数量。
如果是缓存命中,则以常量缓存的吞吐量为这些生成的请求提供服务;否则,以设备内存的吞吐量为它们提供服务。
纹理和表面内存
纹理和表面内存空间驻留在设备内存中,并在纹理缓存中缓存,因此只有在缓存未命中时,纹理获取或表面读取才会花费一次从设备内存读取的内存读取,否则,它只花费一次从纹理缓存读取的内存读取。纹理缓存针对二维空间局部性进行了优化,因此,在二维空间中读取纹理或表面地址彼此接近的同一 warp 的线程将获得最佳性能。此外,它专为具有恒定延迟的流式获取而设计;缓存命中会降低 DRAM 带宽需求,但不会降低获取延迟。
通过纹理或表面获取读取设备内存提供了一些好处,这些好处使其成为从全局内存或常量内存读取设备内存的有利替代方案
如果内存读取不遵循全局内存或常量内存读取必须遵循的访问模式才能获得良好的性能,则只要纹理获取或表面读取中存在局部性,就可以实现更高的带宽;
寻址计算由内核外部的专用单元执行;
打包的数据可以在单个操作中广播到单独的变量;
8 位和 16 位整数输入数据可以可选地转换为范围为 [0.0, 1.0] 或 [-1.0, 1.0] 的 32 位浮点值(请参阅纹理内存)。
5.4. 最大化指令吞吐量
为了最大化指令吞吐量,应用程序应
尽量减少使用低吞吐量的算术指令;这包括在不影响最终结果的情况下,以精度换取速度,例如使用 intrinsic 函数而不是常规函数(intrinsic 函数在Intrinsic 函数中列出)、单精度而不是双精度,或将非规范化数刷新为零;
尽量减少由控制流指令引起的发散 warp,如控制流指令中详述的那样
减少指令数量,例如,通过尽可能优化掉同步点(如同步指令中所述)或通过使用__restrict__中描述的受限指针。
在本节中,吞吐量以每个多处理器每个时钟周期的操作数给出。对于 32 的 warp 大小,一条指令对应于 32 个操作,因此如果 N 是每个时钟周期的操作数,则指令吞吐量为每个时钟周期 N/32 条指令。
所有吞吐量都适用于一个多处理器。它们必须乘以设备中多处理器的数量才能获得整个设备的吞吐量。
5.4.1. 算术指令
下表给出了各种计算能力的设备在硬件中本机支持的算术指令的吞吐量。
计算能力 |
5.0, 5.2 |
5.3 |
6.0 |
6.1 |
6.2 |
7.x |
8.0 |
8.6 |
8.9 |
9.0 |
---|---|---|---|---|---|---|---|---|---|---|
16 位浮点加法、乘法、乘加 |
不适用 |
256 |
128 |
2 |
256 |
128 |
2563 |
128 |
256 |
|
32 位浮点加法、乘法、乘加 |
128 |
64 |
128 |
64 |
128 |
|||||
64 位浮点加法、乘法、乘加 |
4 |
32 |
4 |
325 |
32 |
2 |
64 |
|||
32 位浮点倒数、倒数平方根、以 2 为底的对数 ( |
32 |
16 |
32 |
16 |
||||||
32 位整数加法、扩展精度加法、减法、扩展精度减法 |
128 |
64 |
128 |
64 |
||||||
32 位整数乘法、乘加、扩展精度乘加 |
多条指令 |
646 |
||||||||
24 位整数乘法 ( |
多条指令 |
|||||||||
32 位整数移位 |
64 |
32 |
64 |
|||||||
比较、最小值、最大值 |
64 |
32 |
64 |
|||||||
32 位整数位反转 |
64 |
32 |
64 |
16 |
||||||
位字段提取/插入 |
64 |
32 |
64 |
多条指令 |
64 |
|||||
32 位按位与、或、异或 |
128 |
64 |
128 |
64 |
||||||
前导零计数、最高有效非符号位 |
32 |
16 |
32 |
16 |
||||||
人口计数 |
32 |
16 |
32 |
16 |
||||||
warp 洗牌 |
32 |
328 |
32 |
|||||||
warp 归约 |
多条指令 |
16 |
||||||||
warp 投票 |
64 |
|||||||||
绝对差值之和 |
64 |
32 |
64 |
|||||||
SIMD 视频指令 |
多条指令 |
|||||||||
SIMD 视频指令 |
多条指令 |
64 |
||||||||
所有其他 SIMD 视频指令 |
多条指令 |
|||||||||
从 8 位和 16 位整数到 32 位整数类型的类型转换 |
32 |
16 |
32 |
64 |
||||||
与 64 位类型之间以及从 64 位类型到其他类型的类型转换 |
4 |
16 |
4 |
1610 |
16 |
2 |
2 |
16 |
||
所有其他类型转换 |
32 |
16 |
32 |
16 |
||||||
16 位 DPX |
多条指令 |
128 |
||||||||
32 位 DPX |
多条指令 |
64 |
其他指令和函数在本机指令之上实现。对于不同计算能力的设备,实现方式可能不同,并且编译后的本机指令数量可能会随着每个编译器版本而波动。对于复杂的函数,根据输入可能会有多个代码路径。cuobjdump
可用于检查 cubin
对象中的特定实现。
某些函数的实现可以在 CUDA 头文件 (math_functions.h
, device_functions.h
, …) 中轻松获得。
一般来说,使用 -ftz=true
(非规范化数刷新为零)编译的代码比使用 -ftz=false
编译的代码往往具有更高的性能。同样,使用 -prec-div=false
(较低精度的除法)编译的代码往往比使用 -prec-div=true
编译的代码具有更高的性能,并且使用 -prec-sqrt=false
(较低精度的平方根)编译的代码往往比使用 -prec-sqrt=true
编译的代码具有更高的性能。nvcc 用户手册更详细地描述了这些编译标志。
单精度浮点除法
__fdividef(x, y)
(请参阅Intrinsic 函数)提供的单精度浮点除法比除法运算符更快。
单精度浮点倒数平方根
为了保留 IEEE-754 语义,只有当倒数和平方根都是近似值时(即使用 -prec-div=false
和 -prec-sqrt=false
),编译器才能将 1.0/sqrtf()
优化为 rsqrtf()
。因此,建议在需要的地方直接调用 rsqrtf()
。
单精度浮点平方根
单精度浮点平方根实现为倒数平方根后跟倒数,而不是倒数平方根后跟乘法,以便为 0 和无穷大提供正确的结果。
正弦和余弦
sinf(x)
, cosf(x)
, tanf(x)
, sincosf(x)
和相应的双精度指令非常昂贵,如果参数 x 的幅度很大,则更是如此。
更准确地说,参数约简代码(有关实现,请参阅数学函数)包含两个代码路径,分别称为快速路径和慢速路径。
快速路径用于幅度足够小的参数,并且基本上由几个乘加运算组成。慢速路径用于幅度大的参数,并且由实现整个参数范围内的正确结果所需的漫长计算组成。
目前,三角函数的参数约简代码为幅度小于 105615.0f
(对于单精度函数)和小于 2147483648.0
(对于双精度函数)的参数选择快速路径。
由于慢速路径比快速路径需要更多的寄存器,因此已尝试通过将一些中间变量存储在本地内存中来减少慢速路径中的寄存器压力,这可能会由于本地内存的高延迟和带宽而影响性能(请参阅设备内存访问)。目前,单精度函数使用 28 字节的本地内存,双精度函数使用 44 字节的本地内存。但是,确切的数量可能会发生变化。
由于慢速路径中的漫长计算和本地内存的使用,当需要慢速路径约简时,这些三角函数的吞吐量比快速路径约简低一个数量级。
整数算术
整数除法和模运算成本很高,因为它们编译为最多 20 条指令。在某些情况下,可以使用按位运算代替它们:如果 n
是 2 的幂,则 (i/n
) 等效于 (i>>log2(n))
,(i%n
) 等效于 (i&(n-1)
);如果 n
是字面量,编译器将执行这些转换。
__brev
和 __popc
映射到单个指令,__brevll
和 __popcll
映射到几个指令。
__[u]mul24
是传统的 intrinsic 函数,不再有任何理由使用它们。
半精度算术
为了获得 16 位精度浮点加法、乘法或乘加的良好性能,建议对 half
精度使用 half2
数据类型,对 __nv_bfloat16
精度使用 __nv_bfloat162
。然后可以使用向量 intrinsic 函数(例如,__hadd2
、__hsub2
、__hmul2
、__hfma2
)在单个指令中执行两个操作。使用 half2
或 __nv_bfloat162
代替使用 half
或 __nv_bfloat16
的两个调用也可能有助于其他 intrinsic 函数(例如 warp 洗牌)的性能。
提供了 intrinsic 函数 __halves2half2
以将两个 half
精度值转换为 half2
数据类型。
提供了 intrinsic 函数 __halves2bfloat162
以将两个 __nv_bfloat
精度值转换为 __nv_bfloat162
数据类型。
类型转换
有时,编译器必须插入转换指令,从而引入额外的执行周期。以下情况就是如此:
对类型为
char
或short
的变量进行操作的函数,其操作数通常需要转换为int
,用作单精度浮点计算输入的双精度浮点常量(即那些没有类型后缀定义的常量)(根据 C/C++ 标准的要求)。
可以通过使用单精度浮点常量来避免最后一种情况,这些常量使用 f
后缀定义,例如 3.141592653589793f
、1.0f
、0.5f
。
5.4.2. 控制流指令
任何流控制指令(if
、switch
、do
、for
、while
)都可能通过导致同一 warp 的线程发散(即,遵循不同的执行路径)来显着影响有效指令吞吐量。如果发生这种情况,则必须串行化不同的执行路径,从而增加此 warp 执行的指令总数。
为了在控制流取决于线程 ID 的情况下获得最佳性能,应该编写控制条件,以便最大限度地减少发散 warp 的数量。这是可能的,因为 warp 在块中的分布是确定性的,如SIMT 架构中所述。一个简单的例子是当控制条件仅取决于 (threadIdx / warpSize
) 时,其中 warpSize
是 warp 大小。在这种情况下,没有 warp 发散,因为控制条件与 warp 完全对齐。
有时,编译器可能会展开循环,或者它可能会通过使用分支预测来优化掉短 if
或 switch
块,如下详述。在这些情况下,warp 永远不会发散。程序员还可以使用 #pragma unroll
指令控制循环展开(请参阅#pragma unroll)。
当使用分支预测时,不会跳过任何执行取决于控制条件的指令。相反,它们中的每一个都与每个线程的条件代码或谓词相关联,该条件代码或谓词根据控制条件设置为 true 或 false,尽管这些指令中的每一个都被安排执行,但实际上只执行具有 true 谓词的指令。具有 false 谓词的指令不写入结果,也不评估地址或读取操作数。
5.4.3. 同步指令
对于计算能力为 6.0 的设备,__syncthreads()
的吞吐量为每个时钟周期 32 个操作,对于计算能力为 7.x 以及 8.x 的设备,吞吐量为每个时钟周期 16 个操作,对于计算能力为 5.x、6.1 和 6.2 的设备,吞吐量为每个时钟周期 64 个操作。
请注意,__syncthreads()
可能会通过强制多处理器空闲来影响性能,如设备内存访问中所详述的那样。
5.5. 最大限度地减少内存抖动
频繁分配和释放内存的应用程序可能会发现,分配调用往往会随着时间的推移而变慢,直至达到极限。这通常是由于将内存释放回操作系统供其自身使用的性质所预期的。为了在这方面获得最佳性能,我们建议以下几点:
尝试根据手头的问题调整分配的大小。不要尝试使用
cudaMalloc
/cudaMallocHost
/cuMemCreate
分配所有可用内存,因为这会强制内存立即驻留,并阻止其他应用程序使用该内存。这可能会给操作系统调度程序带来更大的压力,或者只是阻止其他使用同一 GPU 的应用程序完全运行。尝试在应用程序早期和仅在应用程序没有使用时分配适当大小的内存。 减少应用程序中
cudaMalloc
+cudaFree
调用的数量,尤其是在性能关键区域。如果应用程序无法分配足够的设备内存,请考虑回退到其他内存类型,例如
cudaMallocHost
或cudaMallocManaged
,这些内存类型性能可能不如cudaMalloc
,但可以使应用程序取得进展。对于支持此功能的平台,
cudaMallocManaged
允许超额订阅,并且启用正确的cudaMemAdvise
策略后,将允许应用程序保留cudaMalloc
的大部分(如果不是全部)性能。cudaMallocManaged
也不会强制分配常驻内存,直到需要或预取它,从而减少操作系统调度程序的总体压力,并更好地支持多租户用例。
6. 支持 CUDA 的 GPU
https://developer.nvidia.com/cuda-gpus 列出了所有支持 CUDA 的设备及其计算能力。
可以使用运行时库查询计算能力、多处理器数量、时钟频率、设备内存总量和其他属性(请参阅参考手册)。
7. C++ 语言扩展
7.1. 函数执行空间限定符
函数执行空间限定符表示函数是在主机上还是在设备上执行,以及它是可以从主机还是从设备调用。
7.1.1. __global__
__global__
执行空间限定符将函数声明为内核函数。 这样的函数是
在设备上执行,
可从主机调用,
对于计算能力为 5.0 或更高的设备,可从设备调用(有关更多详细信息,请参阅 CUDA 动态并行性)。
__global__
函数必须具有 void 返回类型,并且不能是类的成员。
任何对 __global__
函数的调用都必须指定其执行配置,如 执行配置 中所述。
对 __global__
函数的调用是异步的,这意味着它在设备完成执行之前返回。
7.1.2. __device__
__device__
执行空间限定符声明一个函数,该函数
在设备上执行,
仅可从设备调用。
__global__
和 __device__
执行空间限定符不能一起使用。
7.1.3. __host__
__host__
执行空间限定符声明一个函数,该函数
在主机上执行,
仅可从主机调用。
仅使用 __host__
执行空间限定符声明函数,或者在不使用任何 __host__
、__device__
或 __global__
执行空间限定符的情况下声明函数是等效的;在这两种情况下,该函数都仅为主机编译。
__global__
和 __host__
执行空间限定符不能一起使用。
__device__
和 __host__
执行空间限定符可以一起使用,在这种情况下,该函数将为主机和设备编译。 应用程序兼容性 中介绍的 __CUDA_ARCH__
宏可用于区分主机和设备之间的代码路径
__host__ __device__ func()
{
#if __CUDA_ARCH__ >= 800
// Device code path for compute capability 8.x
#elif __CUDA_ARCH__ >= 700
// Device code path for compute capability 7.x
#elif __CUDA_ARCH__ >= 600
// Device code path for compute capability 6.x
#elif __CUDA_ARCH__ >= 500
// Device code path for compute capability 5.x
#elif !defined(__CUDA_ARCH__)
// Host code path
#endif
}
7.1.4. 未定义的行为
当发生以下情况时,“跨执行空间”调用具有未定义的行为
定义了
__CUDA_ARCH__
,从__global__
、__device__
或__host__ __device__
函数内部调用__host__
函数。未定义
__CUDA_ARCH__
,从__host__
函数内部调用__device__
函数。9
7.1.5. __noinline__ 和 __forceinline__
编译器在认为合适时会内联任何 __device__
函数。
__noinline__
函数限定符可以用作编译器的提示,尽可能不要内联该函数。
__forceinline__
函数限定符可用于强制编译器内联该函数。
__noinline__
和 __forceinline__
函数限定符不能一起使用,并且这两个函数限定符都不能应用于内联函数。
7.1.6. __inline_hint__
__inline_hint__
限定符使编译器能够进行更积极的内联。 与 __forceinline__
不同,它并不意味着该函数是内联的。 在使用 LTO 时,它可以用于改进跨模块的内联。
__noinline__
和 __forceinline__
函数限定符都不能与 __inline_hint__
函数限定符一起使用。
7.2. 变量内存空间限定符
变量内存空间限定符表示设备上变量的内存位置。
在设备代码中声明的自动变量,如果没有任何本节中描述的 __device__
、__shared__
和 __constant__
内存空间限定符,通常驻留在寄存器中。 但是,在某些情况下,编译器可能会选择将其放置在本地内存中,这可能会对性能产生不利影响,如 设备内存访问 中详述。
7.2.1. __device__
__device__
内存空间限定符声明一个驻留在设备上的变量。
在接下来的三个小节中定义的其他内存空间限定符中,最多只能有一个与 __device__
一起使用,以进一步表示变量所属的内存空间。 如果没有其中任何一个,则该变量
驻留在全局内存空间中,
具有创建它的 CUDA 上下文的生命周期,
每个设备都有一个不同的对象,
可以通过运行时库
(cudaGetSymbolAddress()
/cudaGetSymbolSize()
/cudaMemcpyToSymbol()
/cudaMemcpyFromSymbol()
) 从网格内的所有线程和主机访问。
7.2.2. __constant__
__constant__
内存空间限定符,可以选择与 __device__
一起使用,声明一个变量,该变量
驻留在常量内存空间中,
具有创建它的 CUDA 上下文的生命周期,
每个设备都有一个不同的对象,
可以通过运行时库 (
cudaGetSymbolAddress()
/cudaGetSymbolSize()
/cudaMemcpyToSymbol()
/cudaMemcpyFromSymbol()
) 从网格内的所有线程和主机访问。
在并发网格访问常量(在该网格生命周期的任何时间点)时,从主机修改常量的行为是未定义的。
7.2.4. __grid_constant__
对于计算架构大于或等于 7.0 的 __grid_constant__
注解,它注解 const
限定的非引用类型的 __global__
函数参数,该参数
具有网格的生命周期,
网格私有,即主机线程和来自其他网格(包括子网格)的线程无法访问该对象,
每个网格都有一个不同的对象,即网格中的所有线程都看到相同的地址,
是只读的,即修改
__grid_constant__
对象或其任何子对象是未定义的行为,包括mutable
成员。
要求
用
__grid_constant__
注解的内核参数必须具有const
限定的非引用类型。所有函数声明在任何
__grid_constant_
参数方面都必须匹配。函数模板特化在任何
__grid_constant__
参数方面都必须与主模板声明匹配。函数模板实例化指令在任何
__grid_constant__
参数方面都必须与主模板声明匹配。
如果获取了 __global__
函数参数的地址,编译器通常会在线程局部内存中创建内核参数的副本,并使用该副本的地址,以部分支持 C++ 语义,这允许每个线程修改其自己的函数参数本地副本。 用 __grid_constant__
注解 __global__
函数参数可确保编译器不会在线程局部内存中创建内核参数的副本,而是使用参数本身的通用地址。 避免本地副本可能会提高性能。
__device__ void unknown_function(S const&);
__global__ void kernel(const __grid_constant__ S s) {
s.x += threadIdx.x; // Undefined Behavior: tried to modify read-only memory
// Compiler will _not_ create a per-thread thread local copy of "s":
unknown_function(s);
}
7.2.5. __managed__
__managed__
内存空间限定符,可以选择与 __device__
一起使用,声明一个变量,该变量
可以从设备代码和主机代码引用,例如,可以获取其地址,或者可以直接从设备或主机函数读取或写入它。
具有应用程序的生命周期。
有关更多详细信息,请参阅 __managed__ 内存空间限定符。
7.2.6. __restrict__
nvcc
通过 __restrict__
关键字支持受限指针。
受限指针是在 C99 中引入的,旨在缓解 C 类型语言中存在的别名问题,该问题会阻止从代码重新排序到公共子表达式消除的各种优化。
这是一个受别名问题影响的示例,其中使用受限指针可以帮助编译器减少指令数量
void foo(const float* a,
const float* b,
float* c)
{
c[0] = a[0] * b[0];
c[1] = a[0] * b[0];
c[2] = a[0] * b[0] * a[1];
c[3] = a[0] * a[1];
c[4] = a[0] * b[0];
c[5] = b[0];
...
}
在 C 类型语言中,指针 a
、b
和 c
可能是别名,因此任何通过 c
的写入都可能修改 a
或 b
的元素。 这意味着为了保证功能正确性,编译器无法将 a[0]
和 b[0]
加载到寄存器中,将它们相乘,并将结果存储到 c[0]
和 c[1]
,因为如果例如 a[0]
与 c[0]
实际上是同一位置,则结果将与抽象执行模型不同。 因此,编译器无法利用公共子表达式。 同样,编译器不能简单地将 c[4]
的计算重新排序到 c[0]
和 c[1]
的计算附近,因为前面写入 c[3]
可能会更改 c[4]
计算的输入。
通过使 a
、b
和 c
成为受限指针,程序员向编译器断言指针实际上没有别名,在这种情况下,这意味着通过 c
的写入永远不会覆盖 a
或 b
的元素。 这将函数原型更改如下
void foo(const float* __restrict__ a,
const float* __restrict__ b,
float* __restrict__ c);
请注意,需要将所有指针参数都设为受限指针,编译器优化器才能获得任何好处。 通过添加 __restrict__
关键字,编译器现在可以随意重新排序和进行公共子表达式消除,同时保留与抽象执行模型相同的功能
void foo(const float* __restrict__ a,
const float* __restrict__ b,
float* __restrict__ c)
{
float t0 = a[0];
float t1 = b[0];
float t2 = t0 * t1;
float t3 = a[1];
c[0] = t2;
c[1] = t2;
c[4] = t2;
c[2] = t2 * t3;
c[3] = t0 * t3;
c[5] = t1;
...
}
这里的影响是减少了内存访问次数和计算次数。 这与由于“缓存”加载和公共子表达式而导致的寄存器压力增加相平衡。
由于寄存器压力在许多 CUDA 代码中是一个关键问题,因此由于占用率降低,使用受限指针可能会对 CUDA 代码产生负面的性能影响。
7.3. 内置向量类型
7.3.1. char、short、int、long、longlong、float、double
这些是从基本整数和浮点类型派生的向量类型。 它们是结构体,可以通过字段 x
、y
、z
和 w
分别访问第 1、2、3 和第 4 个分量。 它们都带有一个 make_<type name>
形式的构造函数; 例如,
int2 make_int2(int x, int y);
它创建一个类型为 int2
的向量,其值为 (x, y)
。
向量类型的对齐要求在下表中详细说明。
类型 |
对齐 |
---|---|
char1, uchar1 |
1 |
char2, uchar2 |
2 |
char3, uchar3 |
1 |
char4, uchar4 |
4 |
short1, ushort1 |
2 |
short2, ushort2 |
4 |
short3, ushort3 |
2 |
short4, ushort4 |
8 |
int1, uint1 |
4 |
int2, uint2 |
8 |
int3, uint3 |
4 |
int4, uint4 |
16 |
long1, ulong1 |
如果 sizeof(long) 等于 sizeof(int) 则为 4,否则为 8 |
long2, ulong2 |
如果 sizeof(long) 等于 sizeof(int) 则为 8,否则为 16 |
long3, ulong3 |
如果 sizeof(long) 等于 sizeof(int) 则为 4,否则为 8 |
long4, ulong4 |
16 |
longlong1, ulonglong1 |
8 |
longlong2, ulonglong2 |
16 |
longlong3, ulonglong3 |
8 |
longlong4, ulonglong4 |
16 |
float1 |
4 |
float2 |
8 |
float3 |
4 |
float4 |
16 |
double1 |
8 |
double2 |
16 |
double3 |
8 |
double4 |
16 |
7.3.2. dim3
此类型是基于 uint3
的整数向量类型,用于指定维度。 定义 dim3
类型的变量时,任何未指定的组件都初始化为 1。
7.4. 内置变量
内置变量指定网格和块维度以及块和线程索引。 它们仅在设备上执行的函数内有效。
7.4.1. gridDim
此变量的类型为 dim3
(请参阅 dim3),并包含网格的维度。
7.4.2. blockIdx
此变量的类型为 uint3
(请参阅 char、short、int、long、longlong、float、double),并包含网格内的块索引。
7.4.3. blockDim
此变量的类型为 dim3
(请参阅 dim3),并包含块的维度。
7.4.4. threadIdx
此变量的类型为 uint3
(请参阅 char、short、int、long、longlong、float、double),并包含块内的线程索引。
7.4.5. warpSize
此变量的类型为 int
,并包含线程中的 Warp 大小(有关 Warp 的定义,请参阅 SIMT 架构)。
7.5. 内存栅栏函数
CUDA 编程模型假设设备具有弱序内存模型,即 CUDA 线程将数据写入共享内存、全局内存、页锁定主机内存或对等设备内存的顺序不一定是另一个 CUDA 或主机线程观察到数据被写入的顺序。 两个线程在没有同步的情况下读取或写入同一内存位置是未定义的行为。
在以下示例中,线程 1 执行 writeXY()
,而线程 2 执行 readXY()
。
__device__ int X = 1, Y = 2;
__device__ void writeXY()
{
X = 10;
Y = 20;
}
__device__ void readXY()
{
int B = Y;
int A = X;
}
两个线程同时从同一内存位置 X
和 Y
读取和写入。 任何数据竞争都是未定义的行为,并且没有定义的语义。 A
和 B
的结果值可以是任何值。
内存栅栏函数可用于强制内存访问的顺序一致性排序。 内存栅栏函数在强制执行排序的作用域中有所不同,但它们与访问的内存空间(共享内存、全局内存、页锁定主机内存和对等设备的内存)无关。
void __threadfence_block();
等效于 cuda::atomic_thread_fence(cuda::memory_order_seq_cst, cuda::thread_scope_block) 并确保
调用线程在调用
__threadfence_block()
之前对所有内存进行的所有写入,都会被调用线程块中的所有线程观察到,发生在调用线程在调用__threadfence_block()
之后对所有内存进行的所有写入之前;调用线程在调用
__threadfence_block()
之前对所有内存进行的所有读取,都排在调用线程在调用__threadfence_block()
之后对所有内存进行的所有读取之前。
void __threadfence();
等效于 cuda::atomic_thread_fence(cuda::memory_order_seq_cst, cuda::thread_scope_device) 并确保调用线程在调用 __threadfence()
之后对所有内存进行的所有写入,都不会被设备中的任何线程观察到,发生在调用线程在调用 __threadfence()
之前对所有内存进行的任何写入之前。
void __threadfence_system();
等效于 cuda::atomic_thread_fence(cuda::memory_order_seq_cst, cuda::thread_scope_system) 并确保调用线程在调用 __threadfence_system()
之前对所有内存进行的所有写入,都会被设备中的所有线程、主机线程和对等设备中的所有线程观察到,发生在调用线程在调用 __threadfence_system()
之后对所有内存进行的所有写入之前。
__threadfence_system()
仅受计算能力为 2.x 及更高版本的设备支持。
在之前的代码示例中,我们可以在代码中插入栅栏,如下所示
__device__ int X = 1, Y = 2;
__device__ void writeXY()
{
X = 10;
__threadfence();
Y = 20;
}
__device__ void readXY()
{
int B = Y;
__threadfence();
int A = X;
}
对于此代码,可以观察到以下结果
A
等于 1,B
等于 2,A
等于 10,B
等于 2,A
等于 10,B
等于 20。
第四个结果是不可能的,因为第一次写入必须在第二次写入之前可见。 如果线程 1 和 2 属于同一块,则使用 __threadfence_block()
足够了。 如果线程 1 和 2 不属于同一块,则如果它们是来自同一设备的 CUDA 线程,则必须使用 __threadfence()
,如果它们是来自两个不同设备的 CUDA 线程,则必须使用 __threadfence_system()
。
一个常见的用例是当线程使用其他线程生成的一些数据时,如下面的内核代码示例所示,该内核在一个调用中计算 N 个数字的数组的总和。 每个块首先对数组的子集求和,并将结果存储在全局内存中。 当所有块都完成后,最后一个完成的块从全局内存中读取每个部分和,并将它们求和以获得最终结果。 为了确定哪个块最后完成,每个块原子地递增一个计数器,以指示它已完成计算和存储其部分和(有关原子函数,请参阅 原子函数)。 最后一个块是接收计数器值等于 gridDim.x-1
的块。 如果在存储部分和与递增计数器之间没有放置栅栏,则计数器可能会在存储部分和之前递增,因此,可能会达到 gridDim.x-1
,并让最后一个块在部分和实际在内存中更新之前开始读取部分和。
内存栅栏函数仅影响线程执行的内存操作的顺序;它们本身并不能确保这些内存操作对其他线程可见(像 __syncthreads()
对于线程块内的线程所做的那样;请参阅同步函数)。在下面的代码示例中,result
变量上的内存操作的可见性通过将其声明为 volatile 来确保(请参阅Volatile 限定符)。
__device__ unsigned int count = 0;
__shared__ bool isLastBlockDone;
__global__ void sum(const float* array, unsigned int N,
volatile float* result)
{
// Each block sums a subset of the input array.
float partialSum = calculatePartialSum(array, N);
if (threadIdx.x == 0) {
// Thread 0 of each block stores the partial sum
// to global memory. The compiler will use
// a store operation that bypasses the L1 cache
// since the "result" variable is declared as
// volatile. This ensures that the threads of
// the last block will read the correct partial
// sums computed by all other blocks.
result[blockIdx.x] = partialSum;
// Thread 0 makes sure that the incrementing
// of the "count" variable is only performed after
// the partial sum has been written to global memory.
__threadfence();
// Thread 0 signals that it is done.
unsigned int value = atomicInc(&count, gridDim.x);
// Thread 0 determines if its block is the last
// block to be done.
isLastBlockDone = (value == (gridDim.x - 1));
}
// Synchronize to make sure that each thread reads
// the correct value of isLastBlockDone.
__syncthreads();
if (isLastBlockDone) {
// The last block sums the partial sums
// stored in result[0 .. gridDim.x-1]
float totalSum = calculateTotalSum(result);
if (threadIdx.x == 0) {
// Thread 0 of last block stores the total sum
// to global memory and resets the count
// variable, so that the next kernel call
// works properly.
result[0] = totalSum;
count = 0;
}
}
}
7.6. 同步函数
void __syncthreads();
等待线程块中的所有线程都到达此点,并且在 __syncthreads()
之前由这些线程进行的所有全局和共享内存访问对于块中的所有线程都可见。
__syncthreads()
用于协调同一线程块的线程之间的通信。当一个块内的一些线程访问共享或全局内存中的相同地址时,对于某些内存访问,可能存在先写后读、先读后写或先写后写的风险。可以通过在这些访问之间同步线程来避免这些数据冒险。
__syncthreads()
允许在条件代码中使用,但前提是该条件在整个线程块中的求值结果完全相同,否则代码执行很可能会挂起或产生意外的副作用。
计算能力为 2.x 及更高版本的设备支持以下描述的 __syncthreads()
的三种变体。
int __syncthreads_count(int predicate);
与 __syncthreads()
相同,但附加功能是它会为线程块的所有线程评估谓词,并返回谓词评估结果为非零的线程数。
int __syncthreads_and(int predicate);
与 __syncthreads()
相同,但附加功能是它会为线程块的所有线程评估谓词,并且当且仅当所有线程的谓词评估结果都为非零时才返回非零值。
int __syncthreads_or(int predicate);
与 __syncthreads()
相同,但附加功能是它会为线程块的所有线程评估谓词,并且当且仅当任何线程的谓词评估结果为非零时才返回非零值。
void __syncwarp(unsigned mask=0xffffffff);
将导致执行线程等待,直到掩码中命名的所有 Warp 通道都执行了 __syncwarp()
(使用相同的掩码)后才恢复执行。每个调用线程都必须在掩码中设置自己的位,并且掩码中命名的所有未退出的线程都必须执行相应的 __syncwarp()
(使用相同的掩码),否则结果未定义。
执行 __syncwarp()
保证了参与屏障的线程之间的内存顺序。因此,希望通过内存进行通信的 Warp 通道内的线程可以存储到内存,执行 __syncwarp()
,然后安全地读取 Warp 通道中其他线程存储的值。
注意
对于 .target sm_6x 或更低版本,掩码中的所有线程必须在收敛中执行相同的 __syncwarp()
,并且掩码中所有值的并集必须等于活动掩码。否则,行为未定义。
7.7. 数学函数
参考手册列出了设备代码中支持的所有 C/C++ 标准库数学函数以及仅在设备代码中支持的所有内置函数。
数学函数 在相关时提供其中一些函数的精度信息。
7.8. 纹理函数
纹理对象在 纹理对象 API 中描述。
纹理获取在 纹理获取 中描述。
7.8.1. 纹理对象 API
7.8.1.1. tex1Dfetch()
template<class T>
T tex1Dfetch(cudaTextureObject_t texObj, int x);
使用整数纹理坐标 x
从一维纹理对象 texObj
指定的线性内存区域中获取数据。tex1Dfetch()
仅适用于非归一化坐标,因此仅支持边框和钳位寻址模式。它不执行任何纹理过滤。对于整数类型,它可以选择将整数提升为单精度浮点数。
7.8.1.2. tex1D()
template<class T>
T tex1D(cudaTextureObject_t texObj, float x);
使用纹理坐标 x
从一维纹理对象 texObj
指定的 CUDA 数组中获取数据。
7.8.1.3. tex1DLod()
template<class T>
T tex1DLod(cudaTextureObject_t texObj, float x, float level);
使用细节级别 level
和纹理坐标 x
从一维纹理对象 texObj
指定的 CUDA 数组中获取数据。
7.8.1.4. tex1DGrad()
template<class T>
T tex1DGrad(cudaTextureObject_t texObj, float x, float dx, float dy);
使用纹理坐标 x
从一维纹理对象 texObj
指定的 CUDA 数组中获取数据。细节级别从 X 梯度 dx
和 Y 梯度 dy
派生而来。
7.8.1.5. tex2D()
template<class T>
T tex2D(cudaTextureObject_t texObj, float x, float y);
使用纹理坐标 (x,y)
从二维纹理对象 texObj
指定的 CUDA 数组或线性内存区域中获取数据。
7.8.1.6. tex2D() 用于稀疏 CUDA 数组
template<class T>
T tex2D(cudaTextureObject_t texObj, float x, float y, bool* isResident);
使用纹理坐标 (x,y)
从二维纹理对象 texObj
指定的 CUDA 数组中获取数据。还会通过 isResident
指针返回纹素是否驻留在内存中。如果不是,则获取的值将为零。
7.8.1.7. tex2Dgather()
template<class T>
T tex2Dgather(cudaTextureObject_t texObj,
float x, float y, int comp = 0);
使用纹理坐标 x
和 y
以及 纹理 Gather 中描述的 comp
参数,从 2D 纹理对象 texObj
指定的 CUDA 数组中获取数据。
7.8.1.8. tex2Dgather() 用于稀疏 CUDA 数组
template<class T>
T tex2Dgather(cudaTextureObject_t texObj,
float x, float y, bool* isResident, int comp = 0);
使用纹理坐标 x
和 y
以及 纹理 Gather 中描述的 comp
参数,从 2D 纹理对象 texObj
指定的 CUDA 数组中获取数据。还会通过 isResident
指针返回纹素是否驻留在内存中。如果不是,则获取的值将为零。
7.8.1.9. tex2DGrad()
template<class T>
T tex2DGrad(cudaTextureObject_t texObj, float x, float y,
float2 dx, float2 dy);
使用纹理坐标 (x,y)
从二维纹理对象 texObj
指定的 CUDA 数组中获取数据。细节级别从 dx
和 dy
梯度派生而来。
7.8.1.10. tex2DGrad() 用于稀疏 CUDA 数组
template<class T>
T tex2DGrad(cudaTextureObject_t texObj, float x, float y,
float2 dx, float2 dy, bool* isResident);
使用纹理坐标 (x,y)
从二维纹理对象 texObj
指定的 CUDA 数组中获取数据。细节级别从 dx
和 dy
梯度派生而来。还会通过 isResident
指针返回纹素是否驻留在内存中。如果不是,则获取的值将为零。
7.8.1.11. tex2DLod()
template<class T>
tex2DLod(cudaTextureObject_t texObj, float x, float y, float level);
使用细节级别 level
和纹理坐标 (x,y)
从二维纹理对象 texObj
指定的 CUDA 数组或线性内存区域中获取数据。
7.8.1.12. tex2DLod() 用于稀疏 CUDA 数组
template<class T>
tex2DLod(cudaTextureObject_t texObj, float x, float y, float level, bool* isResident);
使用细节级别 level
和纹理坐标 (x,y)
从二维纹理对象 texObj
指定的 CUDA 数组中获取数据。还会通过 isResident
指针返回纹素是否驻留在内存中。如果不是,则获取的值将为零。
7.8.1.13. tex3D()
template<class T>
T tex3D(cudaTextureObject_t texObj, float x, float y, float z);
使用纹理坐标 (x,y,z)
从三维纹理对象 texObj
指定的 CUDA 数组中获取数据。
7.8.1.14. tex3D() 用于稀疏 CUDA 数组
template<class T>
T tex3D(cudaTextureObject_t texObj, float x, float y, float z, bool* isResident);
使用纹理坐标 (x,y,z)
从三维纹理对象 texObj
指定的 CUDA 数组中获取数据。还会通过 isResident
指针返回纹素是否驻留在内存中。如果不是,则获取的值将为零。
7.8.1.15. tex3DLod()
template<class T>
T tex3DLod(cudaTextureObject_t texObj, float x, float y, float z, float level);
使用细节级别 level
和纹理坐标 (x,y,z)
从三维纹理对象 texObj
指定的 CUDA 数组或线性内存区域中获取数据。
7.8.1.16. tex3DLod() 用于稀疏 CUDA 数组
template<class T>
T tex3DLod(cudaTextureObject_t texObj, float x, float y, float z, float level, bool* isResident);
使用细节级别 level
和纹理坐标 (x,y,z)
从三维纹理对象 texObj
指定的 CUDA 数组或线性内存区域中获取数据。还会通过 isResident
指针返回纹素是否驻留在内存中。如果不是,则获取的值将为零。
7.8.1.17. tex3DGrad()
template<class T>
T tex3DGrad(cudaTextureObject_t texObj, float x, float y, float z,
float4 dx, float4 dy);
使用纹理坐标 (x,y,z)
从三维纹理对象 texObj
指定的 CUDA 数组中获取数据,细节级别从 X 和 Y 梯度 dx
和 dy
派生而来。
7.8.1.18. tex3DGrad() 用于稀疏 CUDA 数组
template<class T>
T tex3DGrad(cudaTextureObject_t texObj, float x, float y, float z,
float4 dx, float4 dy, bool* isResident);
使用纹理坐标 (x,y,z)
从三维纹理对象 texObj
指定的 CUDA 数组中获取数据,细节级别从 X 和 Y 梯度 dx
和 dy
派生而来。还会通过 isResident
指针返回纹素是否驻留在内存中。如果不是,则获取的值将为零。
7.8.1.19. tex1DLayered()
template<class T>
T tex1DLayered(cudaTextureObject_t texObj, float x, int layer);
使用纹理坐标 x
和索引 layer
从一维纹理对象 texObj
指定的 CUDA 数组中获取数据,如 分层纹理 中所述。
7.8.1.20. tex1DLayeredLod()
template<class T>
T tex1DLayeredLod(cudaTextureObject_t texObj, float x, int layer, float level);
使用细节级别 level
和纹理坐标 x
从层 layer
处的一维分层纹理 指定的 CUDA 数组中获取数据。
7.8.1.21. tex1DLayeredGrad()
template<class T>
T tex1DLayeredGrad(cudaTextureObject_t texObj, float x, int layer,
float dx, float dy);
使用纹理坐标 x
和细节级别(从 dx
和 dy
梯度派生而来)从层 layer
处的一维分层纹理 指定的 CUDA 数组中获取数据。
7.8.1.22. tex2DLayered()
template<class T>
T tex2DLayered(cudaTextureObject_t texObj,
float x, float y, int layer);
使用纹理坐标 (x,y)
和索引 layer
从二维纹理对象 texObj
指定的 CUDA 数组中获取数据,如 分层纹理 中所述。
7.8.1.23. tex2DLayered() 用于稀疏 CUDA 数组
template<class T>
T tex2DLayered(cudaTextureObject_t texObj,
float x, float y, int layer, bool* isResident);
使用纹理坐标 (x,y)
和索引 layer
从二维纹理对象 texObj
指定的 CUDA 数组中获取数据,如 分层纹理 中所述。还会通过 isResident
指针返回纹素是否驻留在内存中。如果不是,则获取的值将为零。
7.8.1.24. tex2DLayeredLod()
template<class T>
T tex2DLayeredLod(cudaTextureObject_t texObj, float x, float y, int layer,
float level);
使用纹理坐标 (x,y)
从层 layer
处的二维分层纹理 指定的 CUDA 数组中获取数据。
7.8.1.25. tex2DLayeredLod() 用于稀疏 CUDA 数组
template<class T>
T tex2DLayeredLod(cudaTextureObject_t texObj, float x, float y, int layer,
float level, bool* isResident);
使用纹理坐标 (x,y)
从层 layer
处的二维分层纹理 指定的 CUDA 数组中获取数据。还会通过 isResident
指针返回纹素是否驻留在内存中。如果不是,则获取的值将为零。
7.8.1.26. tex2DLayeredGrad()
template<class T>
T tex2DLayeredGrad(cudaTextureObject_t texObj, float x, float y, int layer,
float2 dx, float2 dy);
使用纹理坐标 (x,y)
和细节级别(从 dx
和 dy
梯度派生而来)从层 layer
处的二维分层纹理 指定的 CUDA 数组中获取数据。
7.8.1.27. tex2DLayeredGrad() 用于稀疏 CUDA 数组
template<class T>
T tex2DLayeredGrad(cudaTextureObject_t texObj, float x, float y, int layer,
float2 dx, float2 dy, bool* isResident);
使用纹理坐标 (x,y)
和细节级别(从 dx
和 dy
梯度派生而来)从层 layer
处的二维分层纹理 指定的 CUDA 数组中获取数据。还会通过 isResident
指针返回纹素是否驻留在内存中。如果不是,则获取的值将为零。
7.8.1.28. texCubemap()
template<class T>
T texCubemap(cudaTextureObject_t texObj, float x, float y, float z);
使用纹理坐标 (x,y,z)
从立方体贴图纹理对象 texObj
指定的 CUDA 数组中获取数据,如 立方体贴图纹理 中所述。
7.8.1.29. texCubemapGrad()
template<class T>
T texCubemapGrad(cudaTextureObject_t texObj, float x, float, y, float z,
float4 dx, float4 dy);
使用纹理坐标 (x,y,z)
从立方体贴图纹理对象 texObj
指定的 CUDA 数组中获取数据,如 立方体贴图纹理 中所述。使用的细节级别从 dx
和 dy
梯度派生而来。
7.8.1.30. texCubemapLod()
template<class T>
T texCubemapLod(cudaTextureObject_t texObj, float x, float, y, float z,
float level);
使用纹理坐标 (x,y,z)
从立方体贴图纹理对象 texObj
指定的 CUDA 数组中获取数据,如 立方体贴图纹理 中所述。使用的细节级别由 level
给出。
7.8.1.31. texCubemapLayered()
template<class T>
T texCubemapLayered(cudaTextureObject_t texObj,
float x, float y, float z, int layer);
使用纹理坐标 (x,y,z)
和索引 layer
从立方体贴图分层纹理对象 texObj
指定的 CUDA 数组中获取数据,如 立方体贴图分层纹理 中所述。
7.8.1.32. texCubemapLayeredGrad()
template<class T>
T texCubemapLayeredGrad(cudaTextureObject_t texObj, float x, float y, float z,
int layer, float4 dx, float4 dy);
使用纹理坐标 (x,y,z)
和索引 layer
从立方体贴图分层纹理对象 texObj
指定的 CUDA 数组中获取数据,如 立方体贴图分层纹理 中所述,细节级别从 dx
和 dy
梯度派生而来。
7.8.1.33. texCubemapLayeredLod()
template<class T>
T texCubemapLayeredLod(cudaTextureObject_t texObj, float x, float y, float z,
int layer, float level);
使用纹理坐标 (x,y,z)
和索引 layer
从立方体贴图分层纹理对象 texObj
指定的 CUDA 数组中获取数据,如 立方体贴图分层纹理 中所述,细节级别为 level
。
7.9. 表面函数
只有计算能力为 2.0 及更高版本的设备才支持表面函数。
表面对象在 表面对象 API 中描述。
在以下部分中,boundaryMode
指定边界模式,即如何处理超出范围的表面坐标;它等于 cudaBoundaryModeClamp
,在这种情况下,超出范围的坐标将被钳位到有效范围,或者等于 cudaBoundaryModeZero
,在这种情况下,超出范围的读取将返回零,超出范围的写入将被忽略,或者等于 cudaBoundaryModeTrap
,在这种情况下,超出范围的访问会导致内核执行失败。
7.9.1. 表面对象 API
7.9.1.1. surf1Dread()
template<class T>
T surf1Dread(cudaSurfaceObject_t surfObj, int x,
boundaryMode = cudaBoundaryModeTrap);
使用字节坐标 x 读取一维表面对象 surfObj
指定的 CUDA 数组。
7.9.1.2. surf1Dwrite
template<class T>
void surf1Dwrite(T data,
cudaSurfaceObject_t surfObj,
int x,
boundaryMode = cudaBoundaryModeTrap);
将值数据写入到字节坐标 x 处的一维表面对象 surfObj
指定的 CUDA 数组。
7.9.1.3. surf2Dread()
template<class T>
T surf2Dread(cudaSurfaceObject_t surfObj,
int x, int y,
boundaryMode = cudaBoundaryModeTrap);
template<class T>
void surf2Dread(T* data,
cudaSurfaceObject_t surfObj,
int x, int y,
boundaryMode = cudaBoundaryModeTrap);
使用字节坐标 x 和 y 读取二维表面对象 surfObj
指定的 CUDA 数组。
7.9.1.4. surf2Dwrite()
template<class T>
void surf2Dwrite(T data,
cudaSurfaceObject_t surfObj,
int x, int y,
boundaryMode = cudaBoundaryModeTrap);
将值数据写入到字节坐标 x 和 y 处的二维表面对象 surfObj
指定的 CUDA 数组。
7.9.1.5. surf3Dread()
template<class T>
T surf3Dread(cudaSurfaceObject_t surfObj,
int x, int y, int z,
boundaryMode = cudaBoundaryModeTrap);
template<class T>
void surf3Dread(T* data,
cudaSurfaceObject_t surfObj,
int x, int y, int z,
boundaryMode = cudaBoundaryModeTrap);
使用字节坐标 x、y 和 z 读取三维表面对象 surfObj
指定的 CUDA 数组。
7.9.1.6. surf3Dwrite()
template<class T>
void surf3Dwrite(T data,
cudaSurfaceObject_t surfObj,
int x, int y, int z,
boundaryMode = cudaBoundaryModeTrap);
将值数据写入到字节坐标 x、y 和 z 处的三维对象 surfObj
指定的 CUDA 数组。
7.9.1.7. surf1DLayeredread()
template<class T>
T surf1DLayeredread(
cudaSurfaceObject_t surfObj,
int x, int layer,
boundaryMode = cudaBoundaryModeTrap);
template<class T>
void surf1DLayeredread(T data,
cudaSurfaceObject_t surfObj,
int x, int layer,
boundaryMode = cudaBoundaryModeTrap);
使用字节坐标 x 和索引 layer
读取一维分层表面对象 surfObj
指定的 CUDA 数组。
7.9.1.8. surf1DLayeredwrite()
template<class Type>
void surf1DLayeredwrite(T data,
cudaSurfaceObject_t surfObj,
int x, int layer,
boundaryMode = cudaBoundaryModeTrap);
将值数据写入到字节坐标 x 和索引 layer
处的二维分层表面对象 surfObj
指定的 CUDA 数组。
7.9.1.9. surf2DLayeredread()
template<class T>
T surf2DLayeredread(
cudaSurfaceObject_t surfObj,
int x, int y, int layer,
boundaryMode = cudaBoundaryModeTrap);
template<class T>
void surf2DLayeredread(T data,
cudaSurfaceObject_t surfObj,
int x, int y, int layer,
boundaryMode = cudaBoundaryModeTrap);
使用字节坐标 x 和 y 以及索引 layer
读取二维分层表面对象 surfObj
指定的 CUDA 数组。
7.9.1.10. surf2DLayeredwrite()
template<class T>
void surf2DLayeredwrite(T data,
cudaSurfaceObject_t surfObj,
int x, int y, int layer,
boundaryMode = cudaBoundaryModeTrap);
将值数据写入到字节坐标 x 和 y 以及索引 layer
处的一维分层表面对象 surfObj
指定的 CUDA 数组。
7.9.1.11. surfCubemapread()
template<class T>
T surfCubemapread(
cudaSurfaceObject_t surfObj,
int x, int y, int face,
boundaryMode = cudaBoundaryModeTrap);
template<class T>
void surfCubemapread(T data,
cudaSurfaceObject_t surfObj,
int x, int y, int face,
boundaryMode = cudaBoundaryModeTrap);
使用字节坐标 x 和 y 以及面索引 face 读取立方体贴图表面对象 surfObj
指定的 CUDA 数组。
7.9.1.12. surfCubemapwrite()
template<class T>
void surfCubemapwrite(T data,
cudaSurfaceObject_t surfObj,
int x, int y, int face,
boundaryMode = cudaBoundaryModeTrap);
将值数据写入到字节坐标 x 和 y 以及面索引 face 处的立方体贴图对象 surfObj
指定的 CUDA 数组。
7.9.1.13. surfCubemapLayeredread()
template<class T>
T surfCubemapLayeredread(
cudaSurfaceObject_t surfObj,
int x, int y, int layerFace,
boundaryMode = cudaBoundaryModeTrap);
template<class T>
void surfCubemapLayeredread(T data,
cudaSurfaceObject_t surfObj,
int x, int y, int layerFace,
boundaryMode = cudaBoundaryModeTrap);
使用字节坐标 x 和 y 以及索引 layerFace.
读取立方体贴图分层表面对象 surfObj
指定的 CUDA 数组。
7.9.1.14. surfCubemapLayeredwrite()
template<class T>
void surfCubemapLayeredwrite(T data,
cudaSurfaceObject_t surfObj,
int x, int y, int layerFace,
boundaryMode = cudaBoundaryModeTrap);
将值数据写入到字节坐标 x
和 y
以及索引 layerFace
处的立方体贴图分层对象 surfObj
指定的 CUDA 数组。
7.10. 只读数据缓存加载函数
只有计算能力为 5.0 及更高版本的设备才支持只读数据缓存加载函数。
T __ldg(const T* address);
返回位于地址 address
处的类型为 T
的数据,其中 T
为 char
、signed char
、short
、int
、long
、long long
unsigned char
、unsigned short
、unsigned int
、unsigned long
、unsigned long long
、char2
、char4
、short2
、short4
、int2
、int4
、longlong2
uchar2
、uchar4
、ushort2
、ushort4
、uint2
、uint4
、ulonglong2
float
、float2
、float4
、double
或 double2
。包含 cuda_fp16.h
头文件后,T
可以是 __half
或 __half2
。类似地,包含 cuda_bf16.h
头文件后,T
也可以是 __nv_bfloat16
或 __nv_bfloat162
。该操作缓存在只读数据缓存中(请参阅全局内存)。
7.11. 使用缓存提示的加载函数
只有计算能力为 5.0 及更高版本的设备才支持这些加载函数。
T __ldcg(const T* address);
T __ldca(const T* address);
T __ldcs(const T* address);
T __ldlu(const T* address);
T __ldcv(const T* address);
返回位于地址 address
处的类型为 T
的数据,其中 T
为 char
、signed char
、short
、int
、long
、long long
unsigned char
、unsigned short
、unsigned int
、unsigned long
、unsigned long long
、char2
、char4
、short2
、short4
、int2
、int4
、longlong2
uchar2
、uchar4
、ushort2
、ushort4
、uint2
、uint4
、ulonglong2
float
、float2
、float4
、double
或 double2
。包含 cuda_fp16.h
头文件后,T
可以是 __half
或 __half2
。类似地,包含 cuda_bf16.h
头文件后,T
也可以是 __nv_bfloat16
或 __nv_bfloat162
。该操作使用相应的缓存操作符(请参阅 PTX ISA)
7.12. 使用缓存提示的存储函数
只有计算能力为 5.0 及更高版本的设备才支持这些存储函数。
void __stwb(T* address, T value);
void __stcg(T* address, T value);
void __stcs(T* address, T value);
void __stwt(T* address, T value);
将类型为 T
的 value
参数存储到地址为 address
的位置,其中 T
可以是 char
、signed char
、short
、int
、long
、long long
、unsigned char
、unsigned short
、unsigned int
、unsigned long
、unsigned long long
、char2
、char4
、short2
、short4
、int2
、int4
、longlong2
、uchar2
、uchar4
、ushort2
、ushort4
、uint2
、uint4
、ulonglong2
、float
、float2
、float4
、double
或 double2
。 如果包含 cuda_fp16.h
头文件,则 T
可以是 __half
或 __half2
。 类似地,如果包含 cuda_bf16.h
头文件,则 T
也可以是 __nv_bfloat16
或 __nv_bfloat162
。 此操作使用相应的缓存运算符(请参阅 PTX ISA )
7.13. 时间函数
clock_t clock();
long long int clock64();
当在设备代码中执行时,返回每个多处理器计数器的值,该计数器在每个时钟周期递增。 在内核的开始和结束时对此计数器进行采样,取两个样本的差值,并记录每个线程的结果,从而为每个线程提供设备完全执行线程所花费的时钟周期数的度量,但不是设备实际花费执行线程指令的时钟周期数。 前者数字大于后者,因为线程是分时执行的。
7.14. 原子函数
原子函数对驻留在全局或共享内存中的一个 32 位、64 位或 128 位字执行读取-修改-写入原子操作。 对于 float2
或 float4
,读取-修改-写入操作在驻留在全局内存中的向量的每个元素上执行。 例如,atomicAdd()
读取全局或共享内存中某个地址的字,向其添加一个数字,并将结果写回同一地址。 原子函数只能在设备函数中使用。
本节中描述的原子函数具有 cuda::memory_order_relaxed 排序,并且仅在特定的 作用域 内是原子的
带有
_system
后缀的原子 API(例如:atomicAdd_system
)在满足特定条件时,在cuda::thread_scope_system
作用域内是原子的。没有后缀的原子 API(例如:
atomicAdd
)在cuda::thread_scope_device
作用域内是原子的。带有
_block
后缀的原子 API(例如:atomicAdd_block
)在cuda::thread_scope_block
作用域内是原子的。
在以下示例中,CPU 和 GPU 都原子地更新地址 addr
处的整数值
__global__ void mykernel(int *addr) {
atomicAdd_system(addr, 10); // only available on devices with compute capability 6.x
}
void foo() {
int *addr;
cudaMallocManaged(&addr, 4);
*addr = 0;
mykernel<<<...>>>(addr);
__sync_fetch_and_add(addr, 10); // CPU atomic operation
}
请注意,任何原子操作都可以基于 atomicCAS()
(比较并交换)来实现。 例如,对于双精度浮点数的 atomicAdd()
在计算能力低于 6.0 的设备上不可用,但可以按如下方式实现
#if __CUDA_ARCH__ < 600
__device__ double atomicAdd(double* address, double val)
{
unsigned long long int* address_as_ull =
(unsigned long long int*)address;
unsigned long long int old = *address_as_ull, assumed;
do {
assumed = old;
old = atomicCAS(address_as_ull, assumed,
__double_as_longlong(val +
__longlong_as_double(assumed)));
// Note: uses integer comparison to avoid hang in case of NaN (since NaN != NaN)
} while (assumed != old);
return __longlong_as_double(old);
}
#endif
以下设备范围的原子 API 具有系统范围和块范围的变体,但有以下例外
计算能力低于 6.0 的设备仅支持设备范围的原子操作,
计算能力低于 7.2 的 Tegra 设备不支持系统范围的原子操作。
CUDA 12.8 及更高版本支持 CUDA 编译器内置函数,用于具有内存顺序和线程作用域的原子操作。 我们遵循 GNU 的原子内置函数签名,并添加了线程作用域的额外参数。 我们使用以下原子操作内存顺序和线程作用域
enum {
__NV_ATOMIC_RELAXED,
__NV_ATOMIC_CONSUME,
__NV_ATOMIC_ACQUIRE,
__NV_ATOMIC_RELEASE,
__NV_ATOMIC_ACQ_REL,
__NV_ATOMIC_SEQ_CST
};
enum {
__NV_THREAD_SCOPE_THREAD,
__NV_THREAD_SCOPE_BLOCK,
__NV_THREAD_SCOPE_CLUSTER,
__NV_THREAD_SCOPE_DEVICE,
__NV_THREAD_SCOPE_SYSTEM
};
示例
__device__ T __nv_atomic_load_n(T* ptr, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
T 可以是大小为 1、2、4、8 和 16 字节的任何整型类型。
这些原子函数不能在本地内存上操作。 例如
__device__ void foo() {
int a = 1; // defined in local memory
int b;
__nv_atomic_load(&a, &b, __NV_ATOMIC_RELAXED, __NV_THREAD_SCOPE_SYSTEM);
}
这些函数必须仅在 __device__
函数的块作用域内使用。 例如
__device__ void foo() {
__shared__ unsigned int u1 = 1;
__shared__ unsigned int u2 = 2;
__nv_atomic_load(&u1, &u2, __NV_ATOMIC_RELAXED, __NV_THREAD_SCOPE_SYSTEM);
}
并且不能获取这些函数的地址。 以下是三个不支持的示例
// Not permitted to be used in a host function
__host__ void bar() {
__shared__ unsigned int u1 = 1;
__shared__ unsigned int u2 = 2;
__nv_atomic_load(&u1, &u2, __NV_ATOMIC_RELAXED, __NV_THREAD_SCOPE_SYSTEM);
}
// Not permitted to be used as a template default argument.
// The function address cannot be taken.
template<void *F = __nv_atomic_load_n>
class X {
void *f = F;
};
// Not permitted to be called in a constructor initialization list.
class Y {
int a;
public:
__device__ Y(int *b): a(__nv_atomic_load_n(b, __NV_ATOMIC_RELAXED)) {}
};
内存顺序对应于 C++ 标准原子操作的内存顺序。 对于线程作用域,我们遵循 cuda::thread_scope 的 定义。
__NV_ATOMIC_CONSUME
内存顺序目前使用更强的 __NV_ATOMIC_ACQUIRE
内存顺序来实现。
__NV_THREAD_SCOPE_THREAD
线程作用域目前使用更宽的 __NV_THREAD_SCOPE_BLOCK
线程作用域来实现。
对于支持的数据类型,请参考不同原子操作的相应章节。
7.14.1. 算术函数
7.14.1.1. atomicAdd()
int atomicAdd(int* address, int val);
unsigned int atomicAdd(unsigned int* address,
unsigned int val);
unsigned long long int atomicAdd(unsigned long long int* address,
unsigned long long int val);
float atomicAdd(float* address, float val);
double atomicAdd(double* address, double val);
__half2 atomicAdd(__half2 *address, __half2 val);
__half atomicAdd(__half *address, __half val);
__nv_bfloat162 atomicAdd(__nv_bfloat162 *address, __nv_bfloat162 val);
__nv_bfloat16 atomicAdd(__nv_bfloat16 *address, __nv_bfloat16 val);
float2 atomicAdd(float2* address, float2 val);
float4 atomicAdd(float4* address, float4 val);
读取位于全局或共享内存中地址 address
的 16 位、32 位或 64 位 old
值,计算 (old + val)
,并将结果存储回同一地址的内存中。 这三个操作在一个原子事务中执行。 该函数返回 old
。
atomicAdd()
的 32 位浮点版本仅受计算能力为 2.x 及更高版本的设备支持。
atomicAdd()
的 64 位浮点版本仅受计算能力为 6.x 及更高版本的设备支持。
atomicAdd()
的 32 位 __half2
浮点版本仅受计算能力为 6.x 及更高版本的设备支持。 __half2
或 __nv_bfloat162
加法运算的原子性分别针对两个 __half
或 __nv_bfloat16
元素保证;不能保证整个 __half2
或 __nv_bfloat162
作为单个 32 位访问是原子的。
atomicAdd()
的 float2
和 float4
浮点向量版本仅受计算能力为 9.x 及更高版本的设备支持。 float2
或 float4
加法运算的原子性分别针对两个或四个 float
元素保证;不能保证整个 float2
或 float4
作为单个 64 位或 128 位访问是原子的。
atomicAdd()
的 16 位 __half
浮点版本仅受计算能力为 7.x 及更高版本的设备支持。
atomicAdd()
的 16 位 __nv_bfloat16
浮点版本仅受计算能力为 8.x 及更高版本的设备支持。
atomicAdd()
的 float2
和 float4
浮点向量版本仅受计算能力为 9.x 及更高版本的设备支持。
atomicAdd()
的 float2
和 float4
浮点向量版本仅支持全局内存地址。
7.14.1.2. atomicSub()
int atomicSub(int* address, int val);
unsigned int atomicSub(unsigned int* address,
unsigned int val);
读取位于全局或共享内存中地址 address
的 32 位字 old
,计算 (old - val)
,并将结果存储回同一地址的内存中。 这三个操作在一个原子事务中执行。 该函数返回 old
。
7.14.1.3. atomicExch()
int atomicExch(int* address, int val);
unsigned int atomicExch(unsigned int* address,
unsigned int val);
unsigned long long int atomicExch(unsigned long long int* address,
unsigned long long int val);
float atomicExch(float* address, float val);
读取位于全局或共享内存中地址 address
的 32 位或 64 位字 old
,并将 val
存储回同一地址的内存中。 这两个操作在一个原子事务中执行。 该函数返回 old
。
template<typename T> T atomicExch(T* address, T val);
读取位于全局或共享内存中地址 address
的 128 位字 old
,并将 val
存储回同一地址的内存中。 这两个操作在一个原子事务中执行。 该函数返回 old
。 类型 T
必须满足以下要求
sizeof(T) == 16
alignof(T) >= 16
std::is_trivially_copyable<T>::value == true
// for C++03 and older
std::is_default_constructible<T>::value == true
因此,T
必须是 128 位且正确对齐,必须是可平凡复制的,并且在 C++03 或更早版本中,它也必须是默认可构造的。
128 位 atomicExch()
仅受计算能力为 9.x 及更高版本的设备支持。
7.14.1.4. atomicMin()
int atomicMin(int* address, int val);
unsigned int atomicMin(unsigned int* address,
unsigned int val);
unsigned long long int atomicMin(unsigned long long int* address,
unsigned long long int val);
long long int atomicMin(long long int* address,
long long int val);
读取位于全局或共享内存中地址 address
的 32 位或 64 位字 old
,计算 old
和 val
的最小值,并将结果存储回同一地址的内存中。 这三个操作在一个原子事务中执行。 该函数返回 old
。
64 位版本的 atomicMin()
仅受计算能力为 5.0 及更高版本的设备支持。
7.14.1.5. atomicMax()
int atomicMax(int* address, int val);
unsigned int atomicMax(unsigned int* address,
unsigned int val);
unsigned long long int atomicMax(unsigned long long int* address,
unsigned long long int val);
long long int atomicMax(long long int* address,
long long int val);
读取位于全局或共享内存中地址 address
的 32 位或 64 位字 old
,计算 old
和 val
的最大值,并将结果存储回同一地址的内存中。 这三个操作在一个原子事务中执行。 该函数返回 old
。
64 位版本的 atomicMax()
仅受计算能力为 5.0 及更高版本的设备支持。
7.14.1.6. atomicInc()
unsigned int atomicInc(unsigned int* address,
unsigned int val);
读取位于全局或共享内存中地址 address
的 32 位字 old
,计算 ((old >= val) ? 0 : (old+1))
,并将结果存储回同一地址的内存中。 这三个操作在一个原子事务中执行。 该函数返回 old
。
7.14.1.7. atomicDec()
unsigned int atomicDec(unsigned int* address,
unsigned int val);
读取位于全局或共享内存中地址 address
的 32 位字 old
,计算 (((old == 0) || (old > val)) ? val : (old-1)
),并将结果存储回同一地址的内存中。 这三个操作在一个原子事务中执行。 该函数返回 old
。
7.14.1.8. atomicCAS()
int atomicCAS(int* address, int compare, int val);
unsigned int atomicCAS(unsigned int* address,
unsigned int compare,
unsigned int val);
unsigned long long int atomicCAS(unsigned long long int* address,
unsigned long long int compare,
unsigned long long int val);
unsigned short int atomicCAS(unsigned short int *address,
unsigned short int compare,
unsigned short int val);
读取位于全局或共享内存中地址 address
的 16 位、32 位或 64 位字 old
,计算 (old == compare ? val : old)
,并将结果存储回同一地址的内存中。 这三个操作在一个原子事务中执行。 该函数返回 old
(比较并交换)。
template<typename T> T atomicCAS(T* address, T compare, T val);
读取位于全局或共享内存中地址 address
的 128 位字 old
,计算 (old == compare ? val : old)
,并将结果存储回同一地址的内存中。 这三个操作在一个原子事务中执行。 该函数返回 old
(比较并交换)。 类型 T
必须满足以下要求
sizeof(T) == 16
alignof(T) >= 16
std::is_trivially_copyable<T>::value == true
// for C++03 and older
std::is_default_constructible<T>::value == true
因此,T
必须是 128 位且正确对齐,必须是可平凡复制的,并且在 C++03 或更早版本中,它也必须是默认可构造的。
128 位 atomicCAS()
仅受计算能力为 9.x 及更高版本的设备支持。
7.14.1.9. __nv_atomic_exchange()
__device__ void __nv_atomic_exchange(T* ptr, T* val, T *ret, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
此原子函数在 CUDA 12.8 中引入。 它读取 ptr
指向的值,并将该值存储到 ret
指向的位置。 它读取 val
指向的值,并将该值存储到 ptr
指向的位置。
这是一个通用的原子交换,这意味着 T
可以是大小为 4、8 或 16 字节的任何数据类型。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
在架构 sm_90
及更高版本上支持 16 字节数据类型。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整数文字,即参数不能是变量。
7.14.1.10. __nv_atomic_exchange_n()
__device__ T __nv_atomic_exchange_n(T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
此原子函数在 CUDA 12.8 中引入。 它读取 ptr
指向的值,并将此值用作返回值。 它将 val
存储到 ptr
指向的位置。
这是一个非通用的原子交换,这意味着 T
只能是大小为 4、8 或 16 字节的整型类型。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
在架构 sm_90
及更高版本上支持 16 字节数据类型。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整数文字,即参数不能是变量。
7.14.1.11. __nv_atomic_compare_exchange()
__device__ bool __nv_atomic_compare_exchange (T* ptr, T* expected, T* desired, bool weak, int success_order, int failure_order, int scope = __NV_THREAD_SCOPE_SYSTEM);
此原子函数在 CUDA 12.8 中引入。 它读取 ptr
指向的值,并将其与 expected
指向的值进行比较。 如果它们相等,则返回值为 true
,并且 desired
指向的值将存储到 ptr
指向的位置。 否则,它返回 false
,并且 ptr
指向的值将存储到 expected
指向的位置。 参数 weak
被忽略,它会在 success_order
和 failure_order
之间选择更强的内存顺序来执行比较和交换操作。
这是一个通用的原子比较和交换,这意味着 T
可以是大小为 2、4、8 或 16 字节的任何数据类型。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
在架构 sm_90
及更高版本上支持 16 字节数据类型。
在架构 sm_70
及更高版本上支持 2 字节数据类型。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整数文字,即参数不能是变量。
7.14.1.12. __nv_atomic_compare_exchange_n()
__device__ bool __nv_atomic_compare_exchange_n (T* ptr, T* expected, T desired, bool weak, int success_order, int failure_order, int scope = __NV_THREAD_SCOPE_SYSTEM);
此原子函数在 CUDA 12.8 中引入。 它读取 ptr
指向的值,并将其与 expected
指向的值进行比较。 如果它们相等,则返回值为 true
,并且 desired
将存储到 ptr
指向的位置。 否则,它返回 false
,并且 ptr
指向的值将存储到 expected
指向的位置。 参数 weak
被忽略,它会在 success_order
和 failure_order
之间选择更强的内存顺序来执行比较和交换操作。
这是一个非通用的原子比较和交换,这意味着 T
只能是大小为 2、4、8 或 16 字节的整型类型。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
在架构 sm_90
及更高版本上支持 16 字节数据类型。
在架构 sm_70
及更高版本上支持 2 字节数据类型。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整数文字,即参数不能是变量。
7.14.1.13. __nv_atomic_fetch_add() 和 __nv_atomic_add()
__device__ T __nv_atomic_fetch_add (T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_add (T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这两个原子函数在 CUDA 12.8 中引入。 它读取 ptr
指向的值,与 val
相加,并将结果存储回 ptr
指向的位置。 __nv_atomic_fetch_add
返回 ptr
指向的旧值。 __nv_atomic_add
没有返回值。
T
只能是 unsigned int
、int
、unsigned long long
、float
或 double
。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整数文字,即参数不能是变量。
7.14.1.14. __nv_atomic_fetch_sub() 和 __nv_atomic_sub()
__device__ T __nv_atomic_fetch_sub (T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_sub (T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这两个原子函数在 CUDA 12.8 中引入。 它读取 ptr
指向的值,与 val
相减,并将结果存储回 ptr
指向的位置。 __nv_atomic_fetch_sub
返回 ptr
指向的旧值。 __nv_atomic_sub
没有返回值。
T
只能是 unsigned int
、int
、unsigned long long
、float
或 double
。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整数文字,即参数不能是变量。
7.14.1.15. __nv_atomic_fetch_min() 和 __nv_atomic_min()
__device__ T __nv_atomic_fetch_min (T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_min (T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这两个原子函数在 CUDA 12.8 中引入。 它读取 ptr
指向的值,与 val
进行比较,并将较小的值存储回 ptr
指向的位置。 __nv_atomic_fetch_min
返回 ptr
指向的旧值。 __nv_atomic_min
没有返回值。
T
只能是 unsigned int
、int
、unsigned long long
或 long long
。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整数文字,即参数不能是变量。
7.14.1.16. __nv_atomic_fetch_max() 和 __nv_atomic_max()
__device__ T __nv_atomic_fetch_max (T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_max (T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这两个原子函数在 CUDA 12.8 中引入。 它读取 ptr
指向的值,与 val
进行比较,并将较大的值存储回 ptr
指向的位置。 __nv_atomic_fetch_max
返回 ptr
指向的旧值。 __nv_atomic_max
没有返回值。
T
只能是 unsigned int
、int
、unsigned long long
或 long long
。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整数文字,即参数不能是变量。
7.14.2. 位运算函数
7.14.2.1. atomicAnd()
int atomicAnd(int* address, int val);
unsigned int atomicAnd(unsigned int* address,
unsigned int val);
unsigned long long int atomicAnd(unsigned long long int* address,
unsigned long long int val);
读取位于全局或共享内存中地址 address
的 32 位或 64 位字 old
,计算 (old & val
),并将结果存储回同一地址的内存中。 这三个操作在一个原子事务中执行。 该函数返回 old
。
64 位版本的 atomicAnd()
仅受计算能力为 5.0 及更高版本的设备支持。
7.14.2.2. atomicOr()
int atomicOr(int* address, int val);
unsigned int atomicOr(unsigned int* address,
unsigned int val);
unsigned long long int atomicOr(unsigned long long int* address,
unsigned long long int val);
读取位于全局或共享内存中地址 address
的 32 位或 64 位字 old
,计算 (old | val)
,并将结果存储回同一地址的内存中。 这三个操作在一个原子事务中执行。 该函数返回 old
。
64 位版本的 atomicOr()
仅受计算能力为 5.0 及更高版本的设备支持。
7.14.2.3. atomicXor()
int atomicXor(int* address, int val);
unsigned int atomicXor(unsigned int* address,
unsigned int val);
unsigned long long int atomicXor(unsigned long long int* address,
unsigned long long int val);
读取位于全局或共享内存中地址 address
的 32 位或 64 位字 old
,计算 (old ^ val)
,并将结果存储回同一地址的内存中。 这三个操作在一个原子事务中执行。 该函数返回 old
。
64 位版本的 atomicXor()
仅受计算能力为 5.0 及更高版本的设备支持。
7.14.2.4. __nv_atomic_fetch_or() 和 __nv_atomic_or()
__device__ T __nv_atomic_fetch_or (T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_or (T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这两个原子函数在 CUDA 12.8 中引入。它读取 ptr
指向的值,与 val
进行 or
运算,并将结果存储回 ptr
指向的位置。__nv_atomic_fetch_or
返回 ptr
指向的旧值。__nv_atomic_or
没有返回值。
T
只能是大小为 4 或 8 字节的整型。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整数文字,即参数不能是变量。
7.14.2.5. __nv_atomic_fetch_xor() 和 __nv_atomic_xor()
__device__ T __nv_atomic_fetch_xor (T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_xor (T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这两个原子函数在 CUDA 12.8 中引入。它读取 ptr
指向的值,与 val
进行 xor
运算,并将结果存储回 ptr
指向的位置。__nv_atomic_fetch_xor
返回 ptr
指向的旧值。__nv_atomic_xor
没有返回值。
T
只能是大小为 4 或 8 字节的整型。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整数文字,即参数不能是变量。
7.14.2.6. __nv_atomic_fetch_and() 和 __nv_atomic_and()
__device__ T __nv_atomic_fetch_and (T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_and (T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这两个原子函数在 CUDA 12.8 中引入。它读取 ptr
指向的值,与 val
进行 and
运算,并将结果存储回 ptr
指向的位置。__nv_atomic_fetch_and
返回 ptr
指向的旧值。__nv_atomic_and
没有返回值。
T
只能是大小为 4 或 8 字节的整型。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整数文字,即参数不能是变量。
7.14.3. 其他原子函数
7.14.3.1. __nv_atomic_load()
__device__ void __nv_atomic_load(T* ptr, T* ret, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
此原子函数在 CUDA 12.8 中引入。它加载 ptr
指向的值,并将该值写入 ret
指向的位置。
这是一个通用原子加载,这意味着 T
可以是大小为 1、2、4、8 或 16 字节的任何数据类型。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
16 字节数据类型在架构 sm_70
及更高版本上受支持。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整型字面量,即,参数不能是变量。order
不能是 __NV_ATOMIC_RELEASE
或 __NV_ATOMIC_ACQ_REL
。
7.14.3.2. __nv_atomic_load_n()
__device__ T __nv_atomic_load_n(T* ptr, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
此原子函数在 CUDA 12.8 中引入。它加载 ptr
指向的值并返回此值。
这是一个非通用原子加载,这意味着 T
只能是大小为 1、2、4、8 或 16 字节的整型。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
16 字节数据类型在架构 sm_70
及更高版本上受支持。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整型字面量,即,参数不能是变量。order
不能是 __NV_ATOMIC_RELEASE
或 __NV_ATOMIC_ACQ_REL
。
7.14.3.3. __nv_atomic_store()
__device__ void __nv_atomic_store(T* ptr, T* val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
此原子函数在 CUDA 12.8 中引入。它读取 val
指向的值,并存储到 ptr
指向的位置。
这是一个通用原子加载,这意味着 T
可以是大小为 1、2、4、8 或 16 字节的任何数据类型。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
16 字节数据类型在架构 sm_70
及更高版本上受支持。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整型字面量,即,参数不能是变量。order
不能是 __NV_ATOMIC_CONSUME
、__NV_ATOMIC_ACQUIRE
或 __NV_ATOMIC_ACQ_REL
。
7.14.3.4. __nv_atomic_store_n()
__device__ void __nv_atomic_store_n(T* ptr, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
此原子函数在 CUDA 12.8 中引入。它将 val
存储到 ptr
指向的位置。
这是一个非通用原子加载,这意味着 T
只能是大小为 1、2、4、8 或 16 字节的整型。
在架构 sm_60
及更高版本上支持具有内存顺序和线程作用域的原子操作。
16 字节数据类型在架构 sm_70
及更高版本上受支持。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整型字面量,即,参数不能是变量。order
不能是 __NV_ATOMIC_CONSUME
、__NV_ATOMIC_ACQUIRE
或 __NV_ATOMIC_ACQ_REL
。
7.14.3.5. __nv_atomic_thread_fence()
__device__ void __nv_atomic_thread_fence (int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
此原子函数基于指定的内存顺序,在此线程请求的内存访问之间建立顺序。线程作用域参数指定可能观察到此操作排序效果的线程集合。
在架构 sm_90
及更高版本上支持 cluster
的线程作用域。
参数 order
和 scope
需要是整数文字,即参数不能是变量。
7.15. 地址空间谓词函数
如果参数为空指针,则本节中描述的函数的行为未指定。
7.15.1. __isGlobal()
__device__ unsigned int __isGlobal(const void *ptr);
如果 ptr
包含全局内存空间中对象的通用地址,则返回 1,否则返回 0。
7.15.3. __isConstant()
__device__ unsigned int __isConstant(const void *ptr);
如果 ptr
包含常量内存空间中对象的通用地址,则返回 1,否则返回 0。
7.15.4. __isGridConstant()
__device__ unsigned int __isGridConstant(const void *ptr);
如果 ptr
包含使用 __grid_constant__
注释的内核参数的通用地址,则返回 1,否则返回 0。仅支持计算架构 7.x 或更高版本。
7.15.5. __isLocal()
__device__ unsigned int __isLocal(const void *ptr);
如果 ptr
包含本地内存空间中对象的通用地址,则返回 1,否则返回 0。
7.16. 地址空间转换函数
7.16.1. __cvta_generic_to_global()
__device__ size_t __cvta_generic_to_global(const void *ptr);
返回在由 ptr
表示的通用地址上执行 PTX cvta.to.global
指令的结果。
7.16.3. __cvta_generic_to_constant()
__device__ size_t __cvta_generic_to_constant(const void *ptr);
返回在由 ptr
表示的通用地址上执行 PTX cvta.to.const
指令的结果。
7.16.4. __cvta_generic_to_local()
__device__ size_t __cvta_generic_to_local(const void *ptr);
返回在由 ptr
表示的通用地址上执行 PTX cvta.to.local
指令的结果。
7.16.5. __cvta_global_to_generic()
__device__ void * __cvta_global_to_generic(size_t rawbits);
返回通过在 rawbits
提供的值上执行 PTX cvta.global
指令获得的通用指针。
7.16.7. __cvta_constant_to_generic()
__device__ void * __cvta_constant_to_generic(size_t rawbits);
返回通过在 rawbits
提供的值上执行 PTX cvta.const
指令获得的通用指针。
7.16.8. __cvta_local_to_generic()
__device__ void * __cvta_local_to_generic(size_t rawbits);
返回通过在 rawbits
提供的值上执行 PTX cvta.local
指令获得的通用指针。
7.17. Alloca 函数
7.17.1. 概要
__host__ __device__ void * alloca(size_t size);
7.17.2. 描述
alloca()
函数在调用者的堆栈帧中分配 size
字节的内存。返回值是指向已分配内存的指针,当从设备代码调用该函数时,内存的起始地址是 16 字节对齐的。当 alloca()
的调用者返回时,分配的内存会自动释放。
注意
在 Windows 平台上,使用 alloca()
之前必须包含 <malloc.h>
。使用 alloca()
可能会导致堆栈溢出,用户需要相应地调整堆栈大小。
它在计算能力 5.2 或更高版本中受支持。
7.17.3. 示例
__device__ void foo(unsigned int num) {
int4 *ptr = (int4 *)alloca(num * sizeof(int4));
// use of ptr
...
}
7.18. 编译器优化提示函数
本节中描述的函数可用于向编译器优化器提供额外信息。
7.18.1. __builtin_assume_aligned()
void * __builtin_assume_aligned (const void *exp, size_t align)
允许编译器假定参数指针至少按 align
字节对齐,并返回参数指针。
示例
void *res = __builtin_assume_aligned(ptr, 32); // compiler can assume 'res' is
// at least 32-byte aligned
三参数版本
void * __builtin_assume_aligned (const void *exp, size_t align,
<integral type> offset)
允许编译器假定 (char *)exp - offset
至少按 align
字节对齐,并返回参数指针。
示例
void *res = __builtin_assume_aligned(ptr, 32, 8); // compiler can assume
// '(char *)res - 8' is
// at least 32-byte aligned.
7.18.2. __builtin_assume()
void __builtin_assume(bool exp)
允许编译器假定布尔参数为真。如果参数在运行时不为真,则行为未定义。请注意,如果参数具有副作用,则行为未指定。
示例
__device__ int get(int *ptr, int idx) {
__builtin_assume(idx <= 2);
return ptr[idx];
}
7.18.3. __assume()
void __assume(bool exp)
允许编译器假定布尔参数为真。如果参数在运行时不为真,则行为未定义。请注意,如果参数具有副作用,则行为未指定。
示例
__device__ int get(int *ptr, int idx) {
__assume(idx <= 2);
return ptr[idx];
}
7.18.4. __builtin_expect()
long __builtin_expect (long exp, long c)
向编译器指示预期 exp == c
,并返回 exp
的值。通常用于向编译器指示分支预测信息。
示例
// indicate to the compiler that likely "var == 0",
// so the body of the if-block is unlikely to be
// executed at run time.
if (__builtin_expect (var, 0))
doit ();
7.18.5. __builtin_unreachable()
void __builtin_unreachable(void)
向编译器指示控制流永远不会到达调用此函数的位置。如果控制流在运行时实际到达此点,则程序具有未定义的行为。
示例
// indicates to the compiler that the default case label is never reached.
switch (in) {
case 1: return 4;
case 2: return 10;
default: __builtin_unreachable();
}
7.18.6. 限制
仅在使用 cl.exe
主机编译器时才支持 __assume()
。其他函数在所有平台上都受支持,但受以下限制:
如果主机编译器支持该函数,则可以从翻译单元中的任何位置调用该函数。
否则,该函数必须从
__device__
/__global__
函数的主体中调用,或者仅当定义了__CUDA_ARCH__
宏时调用12。
7.19. Warp 投票函数
int __all_sync(unsigned mask, int predicate);
int __any_sync(unsigned mask, int predicate);
unsigned __ballot_sync(unsigned mask, int predicate);
unsigned __activemask();
弃用通知:__any
、__all
和 __ballot
已在 CUDA 9.0 中针对所有设备弃用。
移除通知:当目标设备为计算能力 7.x 或更高版本时,__any
、__all
和 __ballot
不再可用,应使用它们的同步变体。
Warp 投票函数允许给定 warp 的线程执行归约和广播操作。这些函数将 warp 中每个线程的整数 predicate
作为输入,并将这些值与零进行比较。比较结果在 warp 的 活动 线程中组合(归约),并向每个参与线程广播单个返回值
__all_sync(unsigned mask, predicate)
:为
mask
中的所有非退出线程评估predicate
,并且当且仅当predicate
对所有线程的评估结果都为非零时,才返回非零值。__any_sync(unsigned mask, predicate)
:为
mask
中的所有非退出线程评估predicate
,并且当且仅当predicate
对任何线程的评估结果为非零时,才返回非零值。__ballot_sync(unsigned mask, predicate)
:为
mask
中的所有非退出线程评估predicate
,并返回一个整数,当且仅当predicate
对 warp 的第 N 个线程的评估结果为非零且第 N 个线程处于活动状态时,该整数的第 N 位才被设置。__activemask()
:返回调用 warp 中所有当前活动线程的 32 位整数掩码。当调用
__activemask()
时,如果 warp 中的第 N 个 lane 处于活动状态,则设置第 N 位。非活动 线程在返回的掩码中用 0 位表示。已退出程序的线程始终标记为非活动状态。请注意,除非这些指令是同步 warp 内置函数,否则在__activemask()
调用处收敛的线程不能保证在后续指令处收敛。
对于 __all_sync
、__any_sync
和 __ballot_sync
,必须传递一个掩码,用于指定参与调用的线程。必须为每个参与线程设置一个位(表示线程的 lane ID),以确保它们在硬件执行内部函数之前正确收敛。每个调用线程都必须在其掩码中设置自己的位,并且掩码中命名的所有非退出线程都必须执行相同的内部函数和相同的掩码,否则结果未定义。
这些内部函数不暗示内存屏障。它们不保证任何内存排序。
7.20. Warp 匹配函数
__match_any_sync
和 __match_all_sync
在 warp 内的线程之间执行变量的广播和比较操作。
计算能力 7.x 或更高版本的设备支持。
7.20.1. 概要
unsigned int __match_any_sync(unsigned mask, T value);
unsigned int __match_all_sync(unsigned mask, T value, int *pred);
T
可以是 int
、unsigned int
、long
、unsigned long
、long long
、unsigned long long
、float
或 double
。
7.20.2. 描述
__match_sync()
内部函数允许在 mask
中命名的线程同步后,在 warp 中的线程之间广播和比较值 value
。
__match_any_sync
返回在
mask
中具有相同value
值的线程的掩码__match_all_sync
如果
mask
中的所有线程对于value
都具有相同的值,则返回mask
;否则返回 0。如果mask
中的所有线程对于value
都具有相同的值,则谓词pred
设置为 true;否则谓词设置为 false。
新的 *_sync
匹配内部函数接受一个掩码,指示参与调用的线程。必须为每个参与线程设置一个位(表示线程的 lane ID),以确保它们在硬件执行内部函数之前正确收敛。每个调用线程都必须在其掩码中设置自己的位,并且掩码中命名的所有非退出线程都必须执行相同的内部函数和相同的掩码,否则结果未定义。
这些内部函数不暗示内存屏障。它们不保证任何内存排序。
7.21. Warp 归约函数
__reduce_sync(unsigned mask, T value)
内部函数在 mask
中命名的线程同步后,对 value
中提供的数据执行归约操作。对于 {add, min, max} 操作,T 可以是无符号或有符号类型,对于 {and, or, xor} 操作,T 只能是无符号类型。
计算能力 8.x 或更高版本的设备支持。
7.21.1. 概要
// add/min/max
unsigned __reduce_add_sync(unsigned mask, unsigned value);
unsigned __reduce_min_sync(unsigned mask, unsigned value);
unsigned __reduce_max_sync(unsigned mask, unsigned value);
int __reduce_add_sync(unsigned mask, int value);
int __reduce_min_sync(unsigned mask, int value);
int __reduce_max_sync(unsigned mask, int value);
// and/or/xor
unsigned __reduce_and_sync(unsigned mask, unsigned value);
unsigned __reduce_or_sync(unsigned mask, unsigned value);
unsigned __reduce_xor_sync(unsigned mask, unsigned value);
7.21.2. 描述
__reduce_add_sync
、__reduce_min_sync
、__reduce_max_sync
返回对
mask
中命名的每个线程在value
中提供的值应用算术加法、最小值或最大值归约运算的结果。__reduce_and_sync
、__reduce_or_sync
、__reduce_xor_sync
返回对
mask
中命名的每个线程在value
中提供的值应用逻辑 AND、OR 或 XOR 归约运算的结果。
mask
指示参与调用的线程。必须为每个参与线程设置一个位(表示线程的 lane ID),以确保它们在硬件执行内部函数之前正确收敛。每个调用线程都必须在其掩码中设置自己的位,并且掩码中命名的所有非退出线程都必须执行相同的内部函数和相同的掩码,否则结果未定义。
这些内部函数不暗示内存屏障。它们不保证任何内存排序。
7.22. Warp Shuffle 函数
__shfl_sync
、__shfl_up_sync
、__shfl_down_sync
和 __shfl_xor_sync
在 warp 内的线程之间交换变量。
计算能力 5.0 或更高版本的设备支持。
弃用通知:__shfl
、__shfl_up
、__shfl_down
和 __shfl_xor
已在 CUDA 9.0 中针对所有设备弃用。
移除通知:当目标设备为计算能力 7.x 或更高版本时,__shfl
、__shfl_up
、__shfl_down
和 __shfl_xor
不再可用,应使用它们的同步变体。
7.22.1. 概要
T __shfl_sync(unsigned mask, T var, int srcLane, int width=warpSize);
T __shfl_up_sync(unsigned mask, T var, unsigned int delta, int width=warpSize);
T __shfl_down_sync(unsigned mask, T var, unsigned int delta, int width=warpSize);
T __shfl_xor_sync(unsigned mask, T var, int laneMask, int width=warpSize);
T
可以是 int
、unsigned int
、long
、unsigned long
、long long
、unsigned long long
、float
或 double
。包含 cuda_fp16.h
头文件后,T
也可以是 __half
或 __half2
。类似地,包含 cuda_bf16.h
头文件后,T
也可以是 __nv_bfloat16
或 __nv_bfloat162
。
7.22.2. 描述
__shfl_sync()
内部函数允许在 warp 内的线程之间交换变量,而无需使用共享内存。对于 warp 内的所有 活动 线程(和 mask
中命名的线程),交换同时发生,每个线程移动 4 或 8 字节的数据,具体取决于类型。
Warp 中的线程称为 lane,其索引可能在 0 到 warpSize-1
(包括端点)之间。支持四种源 lane 寻址模式
__shfl_sync()
从索引 lane 直接复制
__shfl_up_sync()
从 lane 复制,其 ID 相对于调用者较低
__shfl_down_sync()
从 lane 复制,其 ID 相对于调用者较高
__shfl_xor_sync()
基于自身 lane ID 的按位 XOR,从 lane 复制
线程只能从另一个线程读取数据,该线程正在积极参与 __shfl_sync()
命令。如果目标线程 非活动,则检索到的值未定义。
所有 __shfl_sync()
内部函数都接受一个可选的 width
参数,该参数会改变内部函数的行为。width
的值必须是 [1, warpSize] 范围内的 2 的幂(即 1、2、4、8、16 或 32)。对于其他值,结果未定义。
__shfl_sync()
返回由 ID 为 srcLane
的线程持有的 var
值。如果 width 小于 warpSize
,则 warp 的每个子部分的行为都像一个独立的实体,起始逻辑 lane ID 为 0。如果 srcLane
超出范围 [0:width-1]
,则返回的值对应于 srcLane modulo width
(即在同一子部分内)持有的 var 值。
__shfl_up_sync()
通过从调用者的 lane ID 中减去 delta
来计算源 lane ID。返回结果 lane ID 持有的 var
值:实际上,var
在 warp 中向上移动 delta
个 lane。如果 width 小于 warpSize
,则 warp 的每个子部分的行为都像一个独立的实体,起始逻辑 lane ID 为 0。源 lane 索引不会环绕 width
的值,因此实际上较低的 delta
个 lane 将保持不变。
__shfl_down_sync()
通过将 delta
添加到调用者的 lane ID 来计算源 lane ID。返回结果 lane ID 持有的 var
值:这具有将 var
在 warp 中向下移动 delta
个 lane 的效果。如果 width 小于 warpSize
,则 warp 的每个子部分的行为都像一个独立的实体,起始逻辑 lane ID 为 0。与 __shfl_up_sync()
相同,源 lane 的 ID 号不会环绕 width 的值,因此较高的 delta
个 lane 将保持不变。
__shfl_xor_sync()
通过对调用者的 lane ID 与 laneMask
执行按位 XOR 来计算源 line ID:返回结果 lane ID 持有的 var
值。如果 width
小于 warpSize
,则每组 width
个连续线程都能够访问来自较早线程组的元素,但是,如果它们尝试访问来自较晚线程组的元素,则将返回它们自己的 var
值。此模式实现了一种 butterfly 寻址模式,例如树归约和广播中使用的模式。
新的 *_sync
shfl 内部函数接受一个掩码,指示参与调用的线程。必须为每个参与线程设置一个位(表示线程的 lane ID),以确保它们在硬件执行内部函数之前正确收敛。每个调用线程都必须在其掩码中设置自己的位,并且掩码中命名的所有非退出线程都必须执行相同的内部函数和相同的掩码,否则结果未定义。
线程只能从另一个线程读取数据,该线程正在积极参与 __shfl_sync()
命令。如果目标线程非活动,则检索到的值未定义。
这些内部函数不暗示内存屏障。它们不保证任何内存排序。
7.22.3. 示例
7.22.3.1. 跨 warp 广播单个值
#include <stdio.h>
__global__ void bcast(int arg) {
int laneId = threadIdx.x & 0x1f;
int value;
if (laneId == 0) // Note unused variable for
value = arg; // all threads except lane 0
value = __shfl_sync(0xffffffff, value, 0); // Synchronize all threads in warp, and get "value" from lane 0
if (value != arg)
printf("Thread %d failed.\n", threadIdx.x);
}
int main() {
bcast<<< 1, 32 >>>(1234);
cudaDeviceSynchronize();
return 0;
}
7.22.3.2. 跨 8 个线程的子分区进行包含性加法扫描
#include <stdio.h>
__global__ void scan4() {
int laneId = threadIdx.x & 0x1f;
// Seed sample starting value (inverse of lane ID)
int value = 31 - laneId;
// Loop to accumulate scan within my partition.
// Scan requires log2(n) == 3 steps for 8 threads
// It works by an accumulated sum up the warp
// by 1, 2, 4, 8 etc. steps.
for (int i=1; i<=4; i*=2) {
// We do the __shfl_sync unconditionally so that we
// can read even from threads which won't do a
// sum, and then conditionally assign the result.
int n = __shfl_up_sync(0xffffffff, value, i, 8);
if ((laneId & 7) >= i)
value += n;
}
printf("Thread %d final value = %d\n", threadIdx.x, value);
}
int main() {
scan4<<< 1, 32 >>>();
cudaDeviceSynchronize();
return 0;
}
7.22.3.3. 跨 warp 归约
#include <stdio.h>
__global__ void warpReduce() {
int laneId = threadIdx.x & 0x1f;
// Seed starting value as inverse lane ID
int value = 31 - laneId;
// Use XOR mode to perform butterfly reduction
for (int i=16; i>=1; i/=2)
value += __shfl_xor_sync(0xffffffff, value, i, 32);
// "value" now contains the sum across all threads
printf("Thread %d final value = %d\n", threadIdx.x, value);
}
int main() {
warpReduce<<< 1, 32 >>>();
cudaDeviceSynchronize();
return 0;
}
7.23. Nanosleep 函数
7.23.1. 概要
void __nanosleep(unsigned ns);
7.23.2. 描述
__nanosleep(ns)
将线程挂起大约 ns
纳秒的睡眠持续时间。最大睡眠持续时间约为 1 毫秒。
它在计算能力 7.0 或更高版本中受支持。
7.23.3. 示例
以下代码实现了一个具有指数退避的互斥锁。
__device__ void mutex_lock(unsigned int *mutex) {
unsigned int ns = 8;
while (atomicCAS(mutex, 0, 1) == 1) {
__nanosleep(ns);
if (ns < 256) {
ns *= 2;
}
}
}
__device__ void mutex_unlock(unsigned int *mutex) {
atomicExch(mutex, 0);
}
7.24. Warp 矩阵函数
C++ warp 矩阵运算利用 Tensor Core 来加速 D=A*B+C
形式的矩阵问题。这些操作在计算能力 7.0 或更高版本的设备上,针对混合精度浮点数据受支持。这需要 warp 中所有线程的协同。此外,只有当条件在整个 warp 中以相同方式评估时,才允许在条件代码中使用这些操作,否则代码执行可能会挂起。
7.24.1. 描述
以下所有函数和类型均在命名空间 nvcuda::wmma
中定义。亚字节操作被视为预览功能,即用于它们的数据结构和 API 可能会发生更改,并且可能与未来的版本不兼容。此额外功能在 nvcuda::wmma::experimental
命名空间中定义。
template<typename Use, int m, int n, int k, typename T, typename Layout=void> class fragment;
void load_matrix_sync(fragment<...> &a, const T* mptr, unsigned ldm);
void load_matrix_sync(fragment<...> &a, const T* mptr, unsigned ldm, layout_t layout);
void store_matrix_sync(T* mptr, const fragment<...> &a, unsigned ldm, layout_t layout);
void fill_fragment(fragment<...> &a, const T& v);
void mma_sync(fragment<...> &d, const fragment<...> &a, const fragment<...> &b, const fragment<...> &c, bool satf=false);
fragment(片段)
一个重载类,包含一个矩阵的一部分,该部分分布在 warp 中的所有线程中。矩阵元素到
fragment
内部存储的映射是未指定的,并且在未来的架构中可能会发生更改。
仅允许使用某些模板参数组合。第一个模板参数指定 fragment 将如何参与矩阵运算。Use
的可接受值包括
当 fragment 用作第一个乘数
A
时,使用matrix_a
,当 fragment 用作第二个乘数
B
时,使用matrix_b
,或者当 fragment 用作源或目标累加器(分别为
C
或D
)时,使用accumulator
。m
、n
和k
大小描述了参与乘-累加运算的 warp 范围矩阵瓦片的形状。每个瓦片的维度取决于其角色。对于matrix_a
,瓦片采用维度m x k
;对于matrix_b
,维度为k x n
,而accumulator
瓦片为m x n
。数据类型
T
可以是double
、float
、__half
、__nv_bfloat16
、char
或unsigned char
(用于乘数),以及double
、float
、int
或__half
(用于累加器)。如 元素类型和矩阵大小 中所述,仅支持累加器和乘数类型的有限组合。必须为matrix_a
和matrix_b
片段指定 Layout 参数。row_major
或col_major
表示矩阵行或列中的元素在内存中是连续的。accumulator
矩阵的Layout
参数应保留默认值void
。仅当如下所述加载或存储累加器时,才指定行或列布局。
load_matrix_sync
等待所有 warp 通道到达 load_matrix_sync,然后从内存加载矩阵片段 a。
mptr
必须是指向内存中矩阵第一个元素的 256 位对齐指针。ldm
描述连续行(对于行主序布局)或列(对于列主序布局)之间元素的步幅,并且对于__half
元素类型必须是 8 的倍数,对于float
元素类型必须是 4 的倍数(即,两种情况下都是 16 字节的倍数)。如果片段是accumulator
,则必须将layout
参数指定为mem_row_major
或mem_col_major
。对于matrix_a
和matrix_b
片段,布局从片段的layout
参数推断得出。mptr
、ldm
、layout
的值以及a
的所有模板参数对于 warp 中的所有线程都必须相同。此函数必须由 warp 中的所有线程调用,否则结果未定义。store_matrix_sync
等待所有 warp 通道到达 store_matrix_sync,然后将矩阵片段 a 存储到内存。
mptr
必须是指向内存中矩阵第一个元素的 256 位对齐指针。ldm
描述连续行(对于行主序布局)或列(对于列主序布局)之间元素的步幅,并且对于__half
元素类型必须是 8 的倍数,对于float
元素类型必须是 4 的倍数(即,两种情况下都是 16 字节的倍数)。输出矩阵的布局必须指定为mem_row_major
或mem_col_major
。mptr
、ldm
、layout
的值以及 a 的所有模板参数对于 warp 中的所有线程都必须相同。fill_fragment
用常数值
v
填充矩阵片段。由于矩阵元素到每个片段的映射是未指定的,因此此函数通常由 warp 中的所有线程使用v
的公共值调用。mma_sync
等待所有 warp 通道到达 mma_sync,然后执行 warp 同步矩阵乘-累加运算
D=A*B+C
。也支持就地运算C=A*B+C
。satf
的值和每个矩阵片段的模板参数对于 warp 中的所有线程都必须相同。此外,片段A
、B
、C
和D
之间的模板参数m
、n
和k
必须匹配。此函数必须由 warp 中的所有线程调用,否则结果未定义。
如果 satf
(饱和到有限值)模式为 true
,则以下附加数值属性适用于目标累加器
如果元素结果为 +Infinity,则对应的累加器将包含
+MAX_NORM
如果元素结果为 -Infinity,则对应的累加器将包含
-MAX_NORM
如果元素结果为 NaN,则对应的累加器将包含
+0
由于矩阵元素到每个线程的 fragment
的映射是未指定的,因此在调用 store_matrix_sync
后,必须从内存(共享或全局)访问各个矩阵元素。在所有 warp 中的线程将元素级操作统一应用于所有片段元素这种特殊情况下,可以使用以下 fragment
类成员来实现直接元素访问。
enum fragment<Use, m, n, k, T, Layout>::num_elements;
T fragment<Use, m, n, k, T, Layout>::x[num_elements];
例如,以下代码将 accumulator
矩阵瓦片缩放一半。
wmma::fragment<wmma::accumulator, 16, 16, 16, float> frag;
float alpha = 0.5f; // Same value for all threads in warp
/*...*/
for(int t=0; t<frag.num_elements; t++)
frag.x[t] *= alpha;
7.24.2. 备用浮点
在计算能力为 8.0 及更高版本的设备上,Tensor Core 支持备用类型的浮点运算。
__nv_bfloat16
此数据格式是一种备用 fp16 格式,它具有与 f32 相同的范围,但精度降低(7 位)。您可以将此数据格式直接与
cuda_bf16.h
中提供的__nv_bfloat16
类型一起使用。具有__nv_bfloat16
数据类型的矩阵片段需要与float
类型的累加器组合。支持的形状和操作与__half
相同。tf32
此数据格式是 Tensor Core 支持的一种特殊浮点格式,它具有与 f32 相同的范围和降低的精度(>=10 位)。此格式的内部布局是实现定义的。为了将此浮点格式与 WMMA 操作一起使用,必须手动将输入矩阵转换为 tf32 精度。
为了方便转换,提供了一个新的内联函数
__float_to_tf32
。虽然内联函数的输入和输出参数都是float
类型,但输出在数值上将是tf32
。这种新的精度旨在仅与 Tensor Core 一起使用,如果与其他float
类型操作混合使用,则结果的精度和范围将是未定义的。一旦输入矩阵(
matrix_a
或matrix_b
)转换为 tf32 精度,fragment
与precision::tf32
精度以及float
数据类型组合以load_matrix_sync
将利用此新功能。累加器片段都必须具有float
数据类型。唯一支持的矩阵大小是 16x16x8 (m-n-k)。片段的元素表示为
float
,因此从element_type<T>
到storage_element_type<T>
的映射是precision::tf32 -> float
7.24.3. 双精度
在计算能力为 8.0 及更高版本的设备上,Tensor Core 支持双精度浮点运算。要使用此新功能,必须使用类型为 double
的 fragment
。mma_sync
操作将使用 .rn(舍入到最接近的偶数)舍入修饰符执行。
7.24.4. 亚字节操作
亚字节 WMMA 操作提供了一种访问 Tensor Core 低精度功能的方法。它们被视为预览功能,即用于它们的数据结构和 API 可能会发生更改,并且可能与未来的版本不兼容。此功能通过 nvcuda::wmma::experimental
命名空间提供
namespace experimental {
namespace precision {
struct u4; // 4-bit unsigned
struct s4; // 4-bit signed
struct b1; // 1-bit
}
enum bmmaBitOp {
bmmaBitOpXOR = 1, // compute_75 minimum
bmmaBitOpAND = 2 // compute_80 minimum
};
enum bmmaAccumulateOp { bmmaAccumulateOpPOPC = 1 };
}
对于 4 位精度,可用的 API 保持不变,但您必须指定 experimental::precision::u4
或 experimental::precision::s4
作为片段数据类型。由于片段的元素打包在一起,因此对于该片段,num_storage_elements
将小于 num_elements
。num_elements
变量对于亚字节片段,因此返回亚字节类型 element_type<T>
的元素数。这对于单比特精度也是如此,在这种情况下,从 element_type<T>
到 storage_element_type<T>
的映射如下
experimental::precision::u4 -> unsigned (8 elements in 1 storage element)
experimental::precision::s4 -> int (8 elements in 1 storage element)
experimental::precision::b1 -> unsigned (32 elements in 1 storage element)
T -> T //all other types
亚字节片段的允许布局始终是 matrix_a
的 row_major
和 matrix_b
的 col_major
。
对于亚字节操作,load_matrix_sync
中 ldm
的值对于元素类型 experimental::precision::u4
和 experimental::precision::s4
应为 32 的倍数,或者对于元素类型 experimental::precision::b1
应为 128 的倍数(即,两种情况下都是 16 字节的倍数)。
注意
对 MMA 指令的以下变体的支持已弃用,将在 sm_90 中删除
experimental::precision::u4
experimental::precision::s4
设置为
bmmaBitOpXOR
的bmmaBitOp
的experimental::precision::b1
bmma_sync
等待所有 warp 通道执行 bmma_sync,然后执行 warp 同步位矩阵乘-累加运算
D = (A op B) + C
,其中op
由逻辑运算bmmaBitOp
和bmmaAccumulateOp
定义的累加组成。可用的操作包括bmmaBitOpXOR
,matrix_a
中一行的 128 位 XOR 与matrix_b
的 128 位列bmmaBitOpAND
,matrix_a
中一行的 128 位 AND 与matrix_b
的 128 位列,在计算能力为 8.0 及更高版本的设备上可用。累加运算始终是
bmmaAccumulateOpPOPC
,它计算设置位的数量。
7.24.5. 限制
张量核心所需的特殊格式可能因每个主要和次要设备架构而异。由于线程仅持有整个矩阵的一个片段(不透明的架构特定 ABI 数据结构),并且不允许开发人员对各个参数如何映射到参与矩阵乘-累加的寄存器进行假设,这使得情况更加复杂。
由于片段是架构特定的,因此如果函数 A 和函数 B 是针对不同的链接兼容架构编译并链接到同一个设备可执行文件中,则将片段从函数 A 传递到函数 B 是不安全的。在这种情况下,片段的大小和布局将特定于一个架构,而在另一个架构中使用 WMMA API 将导致不正确的结果,甚至可能导致损坏。
片段布局不同的两个链接兼容架构的示例是 sm_70 和 sm_75。
fragA.cu: void foo() { wmma::fragment<...> mat_a; bar(&mat_a); }
fragB.cu: void bar(wmma::fragment<...> *mat_a) { // operate on mat_a }
// sm_70 fragment layout
$> nvcc -dc -arch=compute_70 -code=sm_70 fragA.cu -o fragA.o
// sm_75 fragment layout
$> nvcc -dc -arch=compute_75 -code=sm_75 fragB.cu -o fragB.o
// Linking the two together
$> nvcc -dlink -arch=sm_75 fragA.o fragB.o -o frag.o
这种未定义的行为也可能在编译时和运行时通过工具检测不到,因此需要格外小心以确保片段的布局是一致的。当与为不同的链接兼容架构构建并期望传递 WMMA 片段的旧库链接时,最有可能出现这种链接危害。
请注意,在弱链接的情况下(例如,CUDA C++ 内联函数),链接器可以选择任何可用的函数定义,这可能会导致编译单元之间的隐式传递。
为了避免这些类型的问题,矩阵应始终存储到内存中以通过外部接口传输(例如,wmma::store_matrix_sync(dst, …);
),然后可以将其安全地作为指针类型传递给 bar()
[例如 float *dst
]。
请注意,由于 sm_70 可以在 sm_75 上运行,因此可以将上面的示例 sm_75 代码更改为 sm_70,并在 sm_75 上正确工作。但是,建议在与其他单独编译的 sm_75 二进制文件链接时,在您的应用程序中使用 sm_75 本机代码。
7.24.6. 元素类型和矩阵大小
Tensor Core 支持各种元素类型和矩阵大小。下表列出了支持的 matrix_a
、matrix_b
和 accumulator
矩阵的各种组合
矩阵 A |
矩阵 B |
累加器 |
矩阵大小 (m-n-k) |
---|---|---|---|
__half |
__half |
float |
16x16x16 |
__half |
__half |
float |
32x8x16 |
__half |
__half |
float |
8x32x16 |
__half |
__half |
__half |
16x16x16 |
__half |
__half |
__half |
32x8x16 |
__half |
__half |
__half |
8x32x16 |
unsigned char |
unsigned char |
int |
16x16x16 |
unsigned char |
unsigned char |
int |
32x8x16 |
unsigned char |
unsigned char |
int |
8x32x16 |
signed char |
signed char |
int |
16x16x16 |
signed char |
signed char |
int |
32x8x16 |
signed char |
signed char |
int |
8x32x16 |
备用浮点支持
矩阵 A |
矩阵 B |
累加器 |
矩阵大小 (m-n-k) |
---|---|---|---|
__nv_bfloat16 |
__nv_bfloat16 |
float |
16x16x16 |
__nv_bfloat16 |
__nv_bfloat16 |
float |
32x8x16 |
__nv_bfloat16 |
__nv_bfloat16 |
float |
8x32x16 |
precision::tf32 |
precision::tf32 |
float |
16x16x8 |
双精度支持
矩阵 A |
矩阵 B |
累加器 |
矩阵大小 (m-n-k) |
---|---|---|---|
double |
double |
double |
8x8x4 |
亚字节操作的实验性支持
矩阵 A |
矩阵 B |
累加器 |
矩阵大小 (m-n-k) |
---|---|---|---|
precision::u4 |
precision::u4 |
int |
8x8x32 |
precision::s4 |
precision::s4 |
int |
8x8x32 |
precision::b1 |
precision::b1 |
int |
8x8x128 |
7.24.7. 示例
以下代码在单个 warp 中实现 16x16x16 矩阵乘法。
#include <mma.h>
using namespace nvcuda;
__global__ void wmma_ker(half *a, half *b, float *c) {
// Declare the fragments
wmma::fragment<wmma::matrix_a, 16, 16, 16, half, wmma::col_major> a_frag;
wmma::fragment<wmma::matrix_b, 16, 16, 16, half, wmma::row_major> b_frag;
wmma::fragment<wmma::accumulator, 16, 16, 16, float> c_frag;
// Initialize the output to zero
wmma::fill_fragment(c_frag, 0.0f);
// Load the inputs
wmma::load_matrix_sync(a_frag, a, 16);
wmma::load_matrix_sync(b_frag, b, 16);
// Perform the matrix multiplication
wmma::mma_sync(c_frag, a_frag, b_frag, c_frag);
// Store the output
wmma::store_matrix_sync(c, c_frag, 16, wmma::mem_row_major);
}
7.25. DPX
DPX 是一组函数,可以为最多三个 16 位和 32 位有符号或无符号整数参数查找最小值和最大值,以及融合加法和最小值/最大值,并可选择 ReLU(钳位到零)
三个参数:
__vimax3_s32
,__vimax3_s16x2
,__vimax3_u32
,__vimax3_u16x2
,__vimin3_s32
,__vimin3_s16x2
,__vimin3_u32
,__vimin3_u16x2
两个参数,带 ReLU:
__vimax_s32_relu
,__vimax_s16x2_relu
,__vimin_s32_relu
,__vimin_s16x2_relu
三个参数,带 ReLU:
__vimax3_s32_relu
,__vimax3_s16x2_relu
,__vimin3_s32_relu
,__vimin3_s16x2_relu
两个参数,也返回哪个参数更小/更大:
__vibmax_s32
,__vibmax_u32
,__vibmin_s32
,__vibmin_u32
,__vibmax_s16x2
,__vibmax_u16x2
,__vibmin_s16x2
,__vibmin_u16x2
三个参数,比较(第一个 + 第二个)与第三个:
__viaddmax_s32
,__viaddmax_s16x2
,__viaddmax_u32
,__viaddmax_u16x2
,__viaddmin_s32
,__viaddmin_s16x2
,__viaddmin_u32
,__viaddmin_u16x2
三个参数,带 ReLU,比较(第一个 + 第二个)与第三个和一个零:
__viaddmax_s32_relu
,__viaddmax_s16x2_relu
,__viaddmin_s32_relu
,__viaddmin_s16x2_relu
这些指令在计算能力为 9 及更高版本的设备上是硬件加速的,而在较旧的设备上是软件模拟的。
完整的 API 可以在 CUDA Math API 文档中找到。
在实现动态规划算法时,DPX 非常有用,例如基因组学中的 Smith-Waterman 或 Needleman-Wunsch 以及路由优化中的 Floyd-Warshall。
7.25.1. 示例
三个有符号 32 位整数的最大值,带 ReLU
const int a = -15;
const int b = 8;
const int c = 5;
int max_value_0 = __vimax3_s32_relu(a, b, c); // max(-15, 8, 5, 0) = 8
const int d = -2;
const int e = -4;
int max_value_1 = __vimax3_s32_relu(a, d, e); // max(-15, -2, -4, 0) = 0
两个 32 位有符号整数之和的最小值、另一个 32 位有符号整数和一个零 (ReLU)
const int a = -5;
const int b = 6;
const int c = -2;
int max_value_0 = __viaddmax_s32_relu(a, b, c); // max(-5 + 6, -2, 0) = max(1, -2, 0) = 1
const int d = 4;
int max_value_1 = __viaddmax_s32_relu(a, d, c); // max(-5 + 4, -2, 0) = max(-1, -2, 0) = 0
两个无符号 32 位整数的最小值,并确定哪个值更小
const unsigned int a = 9;
const unsigned int b = 6;
bool smaller_value;
unsigned int min_value = __vibmin_u32(a, b, &smaller_value); // min_value is 6, smaller_value is true
三对无符号 16 位整数的最大值
const unsigned a = 0x00050002;
const unsigned b = 0x00070004;
const unsigned c = 0x00020006;
unsigned int max_value = __vimax3_u16x2(a, b, c); // max(5, 7, 2) and max(2, 4, 6), so max_value is 0x00070006
7.26. 异步屏障
NVIDIA C++ 标准库引入了 std::barrier 的 GPU 实现。除了 std::barrier
的实现之外,该库还提供了扩展,允许用户指定屏障对象的范围。屏障 API 范围在 线程范围下记录。计算能力为 8.0 或更高版本的设备为屏障操作提供硬件加速,并将这些屏障与 memcpy_async 功能集成。在计算能力低于 8.0 但起始于 7.0 的设备上,这些屏障在没有硬件加速的情况下可用。
nvcuda::experimental::awbarrier
已弃用,推荐使用 cuda::barrier
。
7.26.1. 简单同步模式
在没有到达/等待屏障的情况下,同步是使用 __syncthreads()
(同步块中的所有线程)或在使用 协作组 时使用 group.sync()
来实现的。
#include <cooperative_groups.h>
__global__ void simple_sync(int iteration_count) {
auto block = cooperative_groups::this_thread_block();
for (int i = 0; i < iteration_count; ++i) {
/* code before arrive */
block.sync(); /* wait for all threads to arrive here */
/* code after wait */
}
}
线程在同步点 (block.sync()
) 处被阻塞,直到所有线程都到达同步点。此外,在同步点之前发生的内存更新保证在同步点之后对块中的所有线程可见,即等效于 atomic_thread_fence(memory_order_seq_cst, thread_scope_block)
以及 sync
。
此模式有三个阶段
在同步之前的代码执行将在之后同步读取的内存更新。
同步点
在同步点之后的代码,可以查看在同步点之前发生的内存更新。
7.26.2. 时间分割和同步的五个阶段
使用 std::barrier
的时间分割同步模式如下。
#include <cuda/barrier>
#include <cooperative_groups.h>
__device__ void compute(float* data, int curr_iteration);
__global__ void split_arrive_wait(int iteration_count, float *data) {
using barrier = cuda::barrier<cuda::thread_scope_block>;
__shared__ barrier bar;
auto block = cooperative_groups::this_thread_block();
if (block.thread_rank() == 0) {
init(&bar, block.size()); // Initialize the barrier with expected arrival count
}
block.sync();
for (int curr_iter = 0; curr_iter < iteration_count; ++curr_iter) {
/* code before arrive */
barrier::arrival_token token = bar.arrive(); /* this thread arrives. Arrival does not block a thread */
compute(data, curr_iter);
bar.wait(std::move(token)); /* wait for all threads participating in the barrier to complete bar.arrive()*/
/* code after wait */
}
}
在此模式中,同步点 (block.sync()
) 被拆分为到达点 (bar.arrive()
) 和等待点 (bar.wait(std::move(token))
)。线程通过首次调用 bar.arrive()
开始参与 cuda::barrier
。当线程调用 bar.wait(std::move(token))
时,它将被阻塞,直到参与线程完成 bar.arrive()
的预期次数,如传递给 init()
的预期到达计数参数所指定的那样。在参与线程调用 bar.arrive()
之前发生的内存更新保证在参与线程调用 bar.wait(std::move(token))
之后对参与线程可见。请注意,调用 bar.arrive()
不会阻塞线程,它可以继续执行不依赖于在其他参与线程调用 bar.arrive()
之前发生的内存更新的其他工作。
到达然后等待模式有五个阶段,可以迭代重复
在到达之前的代码执行将在之后等待读取的内存更新。
到达点,带有隐式内存栅栏(即,等效于
atomic_thread_fence(memory_order_seq_cst, thread_scope_block)
)。在到达和等待之间的代码。
等待点。
在等待之后的代码,可以查看在到达之前执行的更新。
7.26.3. 引导初始化、预期到达计数和参与
初始化必须在任何线程开始参与 cuda::barrier
之前发生。
#include <cuda/barrier>
#include <cooperative_groups.h>
__global__ void init_barrier() {
__shared__ cuda::barrier<cuda::thread_scope_block> bar;
auto block = cooperative_groups::this_thread_block();
if (block.thread_rank() == 0) {
init(&bar, block.size()); // Single thread initializes the total expected arrival count.
}
block.sync();
}
在任何线程可以参与 cuda::barrier
之前,必须使用 init()
和预期到达计数(在本例中为 block.size()
)来初始化屏障。初始化必须在任何线程调用 bar.arrive()
之前发生。这提出了一个引导挑战,即线程必须在参与 cuda::barrier
之前同步,但线程正在创建 cuda::barrier
以进行同步。在此示例中,将参与的线程是协作组的一部分,并使用 block.sync()
来引导初始化。在此示例中,整个线程块都参与初始化,因此也可以使用 __syncthreads()
。
init()
的第二个参数是预期到达计数,即参与线程在参与线程从其调用 bar.wait(std::move(token))
解除阻塞之前将调用 bar.arrive()
的次数。在前面的示例中,cuda::barrier
使用线程块中的线程数(即 cooperative_groups::this_thread_block().size()
)进行初始化,并且线程块中的所有线程都参与屏障。
cuda::barrier
在指定线程如何参与(分割到达/等待)以及哪些线程参与方面是灵活的。相比之下,来自协作组的 this_thread_block.sync()
或 __syncthreads()
适用于整个线程块,而 __syncwarp(mask)
是 warp 的指定子集。如果用户的意图是同步完整的线程块或完整的 warp,我们建议出于性能原因分别使用 __syncthreads()
和 __syncwarp(mask)
。
7.26.4. 屏障的阶段:到达、倒计时、完成和重置
一个 cuda::barrier
会从预期的到达计数递减至零,当参与线程调用 bar.arrive()
时。当倒计时达到零时,当前阶段的 cuda::barrier
完成。当最后一次调用 bar.arrive()
导致倒计时达到零时,倒计时会自动且原子地重置。重置将倒计时赋值为预期的到达计数,并将 cuda::barrier
移至下一阶段。
类 cuda::barrier::arrival_token
的 token
对象,如从 token=bar.arrive()
返回的对象,与 barrier 的当前阶段关联。当 cuda::barrier
处于当前阶段时,即当与 token 关联的阶段与 cuda::barrier
的阶段匹配时,调用 bar.wait(std::move(token))
会阻塞调用线程。如果在调用 bar.wait(std::move(token))
之前阶段已推进(因为倒计时达到零),则线程不会阻塞;如果在线程在 bar.wait(std::move(token))
中被阻塞时阶段已推进,则线程会被解除阻塞。
务必了解何时可能或不可能发生重置,尤其是在非平凡的到达/等待同步模式中。
线程对
token=bar.arrive()
和bar.wait(std::move(token))
的调用必须按顺序执行,以便token=bar.arrive()
在cuda::barrier
的当前阶段发生,而bar.wait(std::move(token))
在相同或下一阶段发生。线程对
bar.arrive()
的调用必须在 barrier 的计数器非零时发生。在 barrier 初始化之后,如果线程对bar.arrive()
的调用导致倒计时达到零,则必须在 barrier 可以重用于后续对bar.arrive()
的调用之前调用bar.wait(std::move(token))
。bar.wait()
必须仅使用当前阶段或紧邻的前一阶段的token
对象调用。对于token
对象的任何其他值,行为是未定义的。
对于简单的到达/等待同步模式,遵守这些使用规则非常简单。
7.26.5. 空间分区(也称为 Warp 特化)
线程块可以进行空间分区,以便 warp 专门执行独立的计算。空间分区用于生产者或消费者模式,其中线程的一个子集生成数据,而线程的另一个(不相交)子集并发地消耗数据。
生产者/消费者空间分区模式需要两次单边同步来管理生产者和消费者之间的数据缓冲区。
生产者 |
消费者 |
---|---|
等待缓冲区准备好被填充 |
发出缓冲区准备好被填充的信号 |
生成数据并填充缓冲区 |
|
发出缓冲区已填充的信号 |
等待缓冲区被填充 |
消耗已填充缓冲区中的数据 |
生产者线程等待消费者线程发出缓冲区准备好被填充的信号;但是,消费者线程不等待此信号。消费者线程等待生产者线程发出缓冲区已填充的信号;但是,生产者线程不等待此信号。对于完全的生产者/消费者并发,此模式具有(至少)双缓冲,其中每个缓冲区需要两个 cuda::barrier
。
#include <cuda/barrier>
#include <cooperative_groups.h>
using barrier = cuda::barrier<cuda::thread_scope_block>;
__device__ void producer(barrier ready[], barrier filled[], float* buffer, float* in, int N, int buffer_len)
{
for (int i = 0; i < (N/buffer_len); ++i) {
ready[i%2].arrive_and_wait(); /* wait for buffer_(i%2) to be ready to be filled */
/* produce, i.e., fill in, buffer_(i%2) */
barrier::arrival_token token = filled[i%2].arrive(); /* buffer_(i%2) is filled */
}
}
__device__ void consumer(barrier ready[], barrier filled[], float* buffer, float* out, int N, int buffer_len)
{
barrier::arrival_token token1 = ready[0].arrive(); /* buffer_0 is ready for initial fill */
barrier::arrival_token token2 = ready[1].arrive(); /* buffer_1 is ready for initial fill */
for (int i = 0; i < (N/buffer_len); ++i) {
filled[i%2].arrive_and_wait(); /* wait for buffer_(i%2) to be filled */
/* consume buffer_(i%2) */
barrier::arrival_token token = ready[i%2].arrive(); /* buffer_(i%2) is ready to be re-filled */
}
}
//N is the total number of float elements in arrays in and out
__global__ void producer_consumer_pattern(int N, int buffer_len, float* in, float* out) {
// Shared memory buffer declared below is of size 2 * buffer_len
// so that we can alternatively work between two buffers.
// buffer_0 = buffer and buffer_1 = buffer + buffer_len
__shared__ extern float buffer[];
// bar[0] and bar[1] track if buffers buffer_0 and buffer_1 are ready to be filled,
// while bar[2] and bar[3] track if buffers buffer_0 and buffer_1 are filled-in respectively
__shared__ barrier bar[4];
auto block = cooperative_groups::this_thread_block();
if (block.thread_rank() < 4)
init(bar + block.thread_rank(), block.size());
block.sync();
if (block.thread_rank() < warpSize)
producer(bar, bar+2, buffer, in, N, buffer_len);
else
consumer(bar, bar+2, buffer, out, N, buffer_len);
}
在此示例中,第一个 warp 专门用作生产者,其余 warp 专门用作消费者。所有生产者和消费者线程都参与(调用 bar.arrive()
或 bar.arrive_and_wait()
)四个 cuda::barrier
中的每一个,因此预期的到达计数等于 block.size()
。
生产者线程等待消费者线程发出共享内存缓冲区可以填充的信号。为了等待 cuda::barrier
,生产者线程必须首先到达 ready[i%2].arrive()
以获取 token,然后使用该 token ready[i%2].wait(token)
。为了简单起见,ready[i%2].arrive_and_wait()
组合了这些操作。
bar.arrive_and_wait();
/* is equivalent to */
bar.wait(bar.arrive());
生产者线程计算并填充就绪缓冲区,然后通过到达已填充的 barrier filled[i%2].arrive()
来发出缓冲区已填充的信号。生产者线程此时不等待,而是等待直到下一个迭代的缓冲区(双缓冲)准备好被填充。
消费者线程首先发出两个缓冲区都准备好被填充的信号。消费者线程此时不等待,而是等待此迭代的缓冲区被填充,filled[i%2].arrive_and_wait()
。在消费者线程消耗缓冲区后,它们会发出缓冲区再次准备好被填充的信号,ready[i%2].arrive()
,然后等待下一个迭代的缓冲区被填充。
7.26.6. 提前退出(退出参与)
当参与同步序列的线程必须从该序列提前退出时,该线程必须在退出之前显式退出参与。其余参与线程可以正常进行后续的 cuda::barrier
到达和等待操作。
#include <cuda/barrier>
#include <cooperative_groups.h>
__device__ bool condition_check();
__global__ void early_exit_kernel(int N) {
using barrier = cuda::barrier<cuda::thread_scope_block>;
__shared__ barrier bar;
auto block = cooperative_groups::this_thread_block();
if (block.thread_rank() == 0)
init(&bar , block.size());
block.sync();
for (int i = 0; i < N; ++i) {
if (condition_check()) {
bar.arrive_and_drop();
return;
}
/* other threads can proceed normally */
barrier::arrival_token token = bar.arrive();
/* code between arrive and wait */
bar.wait(std::move(token)); /* wait for all threads to arrive */
/* code after wait */
}
}
此操作到达 cuda::barrier
以履行参与线程在当前阶段到达的义务,然后递减下一阶段的预期到达计数,以便不再期望此线程到达 barrier。
7.26.7. 完成函数
cuda::barrier<Scope, CompletionFunction>
的 CompletionFunction
在每个阶段执行一次,在最后一个线程到达之后以及任何线程从 wait
解除阻塞之前执行。CompletionFunction
执行线程可以看到在该阶段到达 barrier
的线程执行的内存操作,并且 CompletionFunction
中执行的所有内存操作在等待在 barrier
的所有线程从 wait
解除阻塞后都可见。
#include <cuda/barrier>
#include <cooperative_groups.h>
#include <functional>
namespace cg = cooperative_groups;
__device__ int divergent_compute(int*, int);
__device__ int independent_computation(int*, int);
__global__ void psum(int* data, int n, int* acc) {
auto block = cg::this_thread_block();
constexpr int BlockSize = 128;
__shared__ int smem[BlockSize];
assert(BlockSize == block.size());
assert(n % 128 == 0);
auto completion_fn = [&] {
int sum = 0;
for (int i = 0; i < 128; ++i) sum += smem[i];
*acc += sum;
};
// Barrier storage
// Note: the barrier is not default-constructible because
// completion_fn is not default-constructible due
// to the capture.
using completion_fn_t = decltype(completion_fn);
using barrier_t = cuda::barrier<cuda::thread_scope_block,
completion_fn_t>;
__shared__ std::aligned_storage<sizeof(barrier_t),
alignof(barrier_t)> bar_storage;
// Initialize barrier:
barrier_t* bar = (barrier_t*)&bar_storage;
if (block.thread_rank() == 0) {
assert(*acc == 0);
assert(blockDim.x == blockDim.y == blockDim.y == 1);
new (bar) barrier_t{block.size(), completion_fn};
// equivalent to: init(bar, block.size(), completion_fn);
}
block.sync();
// Main loop
for (int i = 0; i < n; i += block.size()) {
smem[block.thread_rank()] = data[i] + *acc;
auto t = bar->arrive();
// We can do independent computation here
bar->wait(std::move(t));
// shared-memory is safe to re-use in the next iteration
// since all threads are done with it, including the one
// that did the reduction
}
}
7.26.8. 内存 Barrier 原语接口
内存 barrier 原语是 cuda::barrier
功能的类似 C 的接口。这些原语通过包含 <cuda_awbarrier_primitives.h>
头文件获得。
7.26.8.1. 数据类型
typedef /* implementation defined */ __mbarrier_t;
typedef /* implementation defined */ __mbarrier_token_t;
7.26.8.2. 内存 Barrier 原语 API
uint32_t __mbarrier_maximum_count();
void __mbarrier_init(__mbarrier_t* bar, uint32_t expected_count);
bar
必须是指向__shared__
内存的指针。expected_count <= __mbarrier_maximum_count()
将
*bar
的当前阶段和下一阶段的预期到达计数初始化为expected_count
。
void __mbarrier_inval(__mbarrier_t* bar);
bar
必须是指向驻留在共享内存中的 mbarrier 对象的指针。在可以重新利用相应的共享内存之前,需要使
*bar
失效。
__mbarrier_token_t __mbarrier_arrive(__mbarrier_t* bar);
必须在此调用之前初始化
*bar
。挂起计数不能为零。
原子地递减 barrier 当前阶段的挂起计数。
返回与紧接在递减之前的 barrier 状态关联的到达 token。
__mbarrier_token_t __mbarrier_arrive_and_drop(__mbarrier_t* bar);
必须在此调用之前初始化
*bar
。挂起计数不能为零。
原子地递减 barrier 当前阶段的挂起计数和下一阶段的预期计数。
返回与紧接在递减之前的 barrier 状态关联的到达 token。
bool __mbarrier_test_wait(__mbarrier_t* bar, __mbarrier_token_t token);
token
必须与*this
的紧邻的前一阶段或当前阶段关联。如果
token
与*bar
的紧邻的前一阶段关联,则返回true
,否则返回false
。
//Note: This API has been deprecated in CUDA 11.1
uint32_t __mbarrier_pending_count(__mbarrier_token_t token);
7.27. 异步数据复制
CUDA 11 引入了异步数据操作和 memcpy_async
API,以允许设备代码显式管理数据的异步复制。memcpy_async
功能使 CUDA 内核能够将计算与数据移动重叠。
7.27.1. memcpy_async
API
memcpy_async
API 在 cuda/barrier
、cuda/pipeline
和 cooperative_groups/memcpy_async.h
头文件中提供。
cuda::memcpy_async
API 与 cuda::barrier
和 cuda::pipeline
同步原语一起使用,而 cooperative_groups::memcpy_async
使用 coopertive_groups::wait
进行同步。
这些 API 具有非常相似的语义:将对象从 src
复制到 dst
,如同由另一个线程执行一样,该线程在复制完成时可以通过 cuda::pipeline
、cuda::barrier
或 cooperative_groups::wait
进行同步。
有关 cuda::barrier
和 cuda::pipeline
的 cuda::memcpy_async
重载的完整 API 文档,请参见 libcudacxx API 文档以及一些示例。
有关 cooperative_groups::memcpy_async 的 API 文档,请参见 协作组 部分。
使用 cuda::barrier 和 cuda::pipeline
的 memcpy_async
API 需要计算能力 7.0 或更高版本。在计算能力为 8.0 或更高的设备上,从全局内存到共享内存的 memcpy_async
操作可以受益于硬件加速。
7.27.3. 不使用 memcpy_async
不使用 memcpy_async
,复制和计算模式的复制阶段表示为 shared[local_idx] = global[global_idx]
。此全局到共享内存的复制扩展为从全局内存读取到寄存器,然后从寄存器写入到共享内存。
当此模式在迭代算法中发生时,每个线程块需要在 shared[local_idx] = global[global_idx]
赋值之后进行同步,以确保在计算阶段开始之前,所有写入共享内存的操作都已完成。线程块还需要在计算阶段之后再次同步,以防止在所有线程完成其计算之前覆盖共享内存。以下代码段说明了此模式。
#include <cooperative_groups.h>
__device__ void compute(int* global_out, int const* shared_in) {
// Computes using all values of current batch from shared memory.
// Stores this thread's result back to global memory.
}
__global__ void without_memcpy_async(int* global_out, int const* global_in, size_t size, size_t batch_sz) {
auto grid = cooperative_groups::this_grid();
auto block = cooperative_groups::this_thread_block();
assert(size == batch_sz * grid.size()); // Exposition: input size fits batch_sz * grid_size
extern __shared__ int shared[]; // block.size() * sizeof(int) bytes
size_t local_idx = block.thread_rank();
for (size_t batch = 0; batch < batch_sz; ++batch) {
// Compute the index of the current batch for this block in global memory:
size_t block_batch_idx = block.group_index().x * block.size() + grid.size() * batch;
size_t global_idx = block_batch_idx + threadIdx.x;
shared[local_idx] = global_in[global_idx];
block.sync(); // Wait for all copies to complete
compute(global_out + block_batch_idx, shared); // Compute and write result to global memory
block.sync(); // Wait for compute using shared memory to finish
}
}
7.27.4. 使用 memcpy_async
使用 memcpy_async
,从全局内存到共享内存的赋值
shared[local_idx] = global_in[global_idx];
被来自 协作组 的异步复制操作替换
cooperative_groups::memcpy_async(group, shared, global_in + batch_idx, sizeof(int) * block.size());
cooperative_groups::memcpy_async API 从全局内存复制 sizeof(int) * block.size()
字节,起始地址为 global_in + batch_idx
,目标地址为 shared
数据。此操作如同由另一个线程执行,该线程在复制完成后与当前线程对 cooperative_groups::wait 的调用同步。在复制操作完成之前,修改全局数据或读取或写入共享数据会引入数据竞争。
在计算能力为 8.0 或更高的设备上,从全局内存到共享内存的 memcpy_async
传输可以受益于硬件加速,这避免了通过中间寄存器传输数据。
#include <cooperative_groups.h>
#include <cooperative_groups/memcpy_async.h>
__device__ void compute(int* global_out, int const* shared_in);
__global__ void with_memcpy_async(int* global_out, int const* global_in, size_t size, size_t batch_sz) {
auto grid = cooperative_groups::this_grid();
auto block = cooperative_groups::this_thread_block();
assert(size == batch_sz * grid.size()); // Exposition: input size fits batch_sz * grid_size
extern __shared__ int shared[]; // block.size() * sizeof(int) bytes
for (size_t batch = 0; batch < batch_sz; ++batch) {
size_t block_batch_idx = block.group_index().x * block.size() + grid.size() * batch;
// Whole thread-group cooperatively copies whole batch to shared memory:
cooperative_groups::memcpy_async(block, shared, global_in + block_batch_idx, sizeof(int) * block.size());
cooperative_groups::wait(block); // Joins all threads, waits for all copies to complete
compute(global_out + block_batch_idx, shared);
block.sync();
}
}}
7.27.5. 使用 cuda::barrier
的异步数据复制
用于 cuda::barrier 的 cuda::memcpy_async
重载允许使用 barrier
同步异步数据传输。此重载执行复制操作,如同由另一个线程执行,该线程通过以下方式绑定到 barrier:在创建时递增当前阶段的预期计数,并在复制操作完成时递减它,这样,只有当参与 barrier 的所有线程都到达,并且绑定到 barrier 当前阶段的所有 memcpy_async
都已完成时,barrier
的阶段才会前进。以下示例使用块范围的 barrier
,其中所有块线程都参与,并将等待操作与 barrier arrive_and_wait
交换,同时提供与先前示例相同的功能
#include <cooperative_groups.h>
#include <cuda/barrier>
__device__ void compute(int* global_out, int const* shared_in);
__global__ void with_barrier(int* global_out, int const* global_in, size_t size, size_t batch_sz) {
auto grid = cooperative_groups::this_grid();
auto block = cooperative_groups::this_thread_block();
assert(size == batch_sz * grid.size()); // Assume input size fits batch_sz * grid_size
extern __shared__ int shared[]; // block.size() * sizeof(int) bytes
// Create a synchronization object (C++20 barrier)
__shared__ cuda::barrier<cuda::thread_scope::thread_scope_block> barrier;
if (block.thread_rank() == 0) {
init(&barrier, block.size()); // Friend function initializes barrier
}
block.sync();
for (size_t batch = 0; batch < batch_sz; ++batch) {
size_t block_batch_idx = block.group_index().x * block.size() + grid.size() * batch;
cuda::memcpy_async(block, shared, global_in + block_batch_idx, sizeof(int) * block.size(), barrier);
barrier.arrive_and_wait(); // Waits for all copies to complete
compute(global_out + block_batch_idx, shared);
block.sync();
}
}
7.27.6. memcpy_async
的性能指导
对于计算能力 8.x,pipeline 机制在同一 CUDA warp 中的 CUDA 线程之间共享。这种共享会导致 memcpy_async
批处理在 warp 内纠缠在一起,这在某些情况下可能会影响性能。
本节重点介绍 warp 纠缠对 commit、wait 和 arrive 操作的影响。有关各个操作的概述,请参阅 Pipeline 接口 和 Pipeline 原语接口。
7.27.6.1. 对齐
在计算能力为 8.0 的设备上,cp.async 指令系列 允许异步地将数据从全局内存复制到共享内存。这些指令支持一次复制 4、8 和 16 个字节。如果提供给 memcpy_async
的大小是 4、8 或 16 的倍数,并且传递给 memcpy_async
的两个指针都与 4、8 或 16 对齐边界对齐,则可以使用完全异步的内存操作来实现 memcpy_async
。
此外,为了在使用 memcpy_async
API 时获得最佳性能,共享内存和全局内存都需要 128 字节的对齐。
对于指向对齐要求为 1 或 2 的类型的值的指针,通常不可能证明指针始终与更高的对齐边界对齐。确定是否可以使用 cp.async
指令必须延迟到运行时。执行此类运行时对齐检查会增加代码大小并增加运行时开销。
cuda::aligned_size_t<size_t Align>(size_t size)Shape 可用于提供证明,证明传递给 memcpy_async
的两个指针都与 Align
对齐边界对齐,并且 size
是 Align
的倍数,方法是将它作为 memcpy_async
API 期望 Shape
的参数传递。
cuda::memcpy_async(group, dst, src, cuda::aligned_size_t<16>(N * block.size()), pipeline);
如果证明不正确,则行为是未定义的。
7.27.6.2. 可平凡复制
在计算能力为 8.0 的设备上,cp.async 指令系列 允许异步地将数据从全局内存复制到共享内存。如果传递给 memcpy_async
的指针类型不指向 TriviallyCopyable 类型,则需要调用每个输出元素的复制构造函数,并且这些指令不能用于加速 memcpy_async
。
7.27.6.3. Warp 纠缠 - Commit
memcpy_async
批处理的序列在 warp 中共享。commit 操作被合并,以便为所有调用 commit 操作的收敛线程将序列递增一次。如果 warp 完全收敛,则序列递增 1;如果 warp 完全发散,则序列递增 32。
设 PB 为 warp 共享的 pipeline 的实际批处理序列。
PB = {BP0, BP1, BP2, …, BPL}
设 TB 为线程的感知批处理序列,就好像序列仅由此线程调用 commit 操作递增一样。
TB = {BT0, BT1, BT2, …, BTL}
pipeline::producer_commit()
返回值来自线程的感知批处理序列。线程感知序列中的索引始终与实际 warp 共享序列中的相等或更大的索引对齐。仅当所有 commit 操作都从收敛线程调用时,序列才相等。
BTn ≡ BPm
其中n <= m
例如,当 warp 完全发散时
warp 共享的 pipeline 的实际序列将为:
PB = {0, 1, 2, 3, ..., 31}
(PL=31
)。此 warp 的每个线程的感知序列将为
线程 0:
TB = {0}
(TL=0
)线程 1:
TB = {0}
(TL=0
)…
线程 31:
TB = {0}
(TL=0
)
7.27.6.4. Warp 纠缠 - Wait
CUDA 线程调用 pipeline_consumer_wait_prior<N>()
或 pipeline::consumer_wait()
以等待感知序列 TB
中的批处理完成。请注意,pipeline::consumer_wait()
等效于 pipeline_consumer_wait_prior<N>()
,其中 N = PL
。
pipeline_consumer_wait_prior<N>()
函数等待实际序列中至少到 PL-N
的批处理。由于 TL <= PL
,因此等待直到并包括 PL-N
的批处理包括等待批处理 TL-N
。因此,当 TL < PL
时,线程将无意中等待额外的、更新的批处理。
在上面极端的完全发散 warp 示例中,每个线程都可能等待所有 32 个批处理。
7.27.6.5. Warp 纠缠 - Arrive-On
Warp 发散会影响 arrive_on(bar)
操作更新 barrier 的次数。如果调用的 warp 完全收敛,则 barrier 会更新一次。如果调用的 warp 完全发散,则会对 barrier 应用 32 个单独的更新。
7.27.6.6. 保持 Commit 和 Arrive-On 操作收敛
建议 commit 和 arrive-on 调用由收敛线程发出
通过使线程的感知批处理序列与实际序列对齐,避免过度等待,以及
最大程度地减少对 barrier 对象的更新。
当在这些操作之前的代码使线程发散时,则应在调用 commit 或 arrive-on 操作之前通过 __syncwarp
重新收敛 warp。
7.28. 使用 cuda::pipeline
的异步数据拷贝
CUDA 提供了 cuda::pipeline
同步对象来管理和重叠异步数据移动与计算。
关于 cuda::pipeline
的 API 文档,请参考 libcudacxx API。pipeline 对象是一个双端 N 阶段队列,具有头部和尾部,用于以先进先出 (FIFO) 的顺序处理工作。pipeline 对象具有以下成员函数来管理 pipeline 的阶段。
Pipeline 类成员函数 |
描述 |
---|---|
|
在 pipeline 内部队列中获取一个可用阶段。 |
|
提交在调用 |
|
等待 pipeline 最老阶段上的所有异步操作完成。 |
|
将 pipeline 最老阶段释放回 pipeline 对象以供重用。然后,生产者可以获取释放的阶段。 |
7.28.1. 使用 cuda::pipeline
的单阶段异步数据拷贝
在之前的示例中,我们展示了如何使用 cooperative_groups 和 cuda::barrier 来执行异步数据传输。在本节中,我们将使用单阶段的 cuda::pipeline
API 来调度异步拷贝。稍后我们将扩展此示例,以展示多阶段重叠计算和拷贝。
#include <cooperative_groups/memcpy_async.h>
#include <cuda/pipeline>
__device__ void compute(int* global_out, int const* shared_in);
__global__ void with_single_stage(int* global_out, int const* global_in, size_t size, size_t batch_sz) {
auto grid = cooperative_groups::this_grid();
auto block = cooperative_groups::this_thread_block();
assert(size == batch_sz * grid.size()); // Assume input size fits batch_sz * grid_size
constexpr size_t stages_count = 1; // Pipeline with one stage
// One batch must fit in shared memory:
extern __shared__ int shared[]; // block.size() * sizeof(int) bytes
// Allocate shared storage for a single stage cuda::pipeline:
__shared__ cuda::pipeline_shared_state<
cuda::thread_scope::thread_scope_block,
stages_count
> shared_state;
auto pipeline = cuda::make_pipeline(block, &shared_state);
// Each thread processes `batch_sz` elements.
// Compute offset of the batch `batch` of this thread block in global memory:
auto block_batch = [&](size_t batch) -> int {
return block.group_index().x * block.size() + grid.size() * batch;
};
for (size_t batch = 0; batch < batch_sz; ++batch) {
size_t global_idx = block_batch(batch);
// Collectively acquire the pipeline head stage from all producer threads:
pipeline.producer_acquire();
// Submit async copies to the pipeline's head stage to be
// computed in the next loop iteration
cuda::memcpy_async(block, shared, global_in + global_idx, sizeof(int) * block.size(), pipeline);
// Collectively commit (advance) the pipeline's head stage
pipeline.producer_commit();
// Collectively wait for the operations committed to the
// previous `compute` stage to complete:
pipeline.consumer_wait();
// Computation overlapped with the memcpy_async of the "copy" stage:
compute(global_out + global_idx, shared);
// Collectively release the stage resources
pipeline.consumer_release();
}
}
7.28.2. 使用 cuda::pipeline
的多阶段异步数据拷贝
在之前使用 cooperative_groups::wait 和 cuda::barrier 的示例中,内核线程会立即等待数据传输到共享内存完成。这避免了从全局内存到寄存器的数据传输,但没有通过重叠计算来隐藏 memcpy_async
操作的延迟。
为此,我们在以下示例中使用 CUDA pipeline 功能。它提供了一种机制来管理 memcpy_async
批次的序列,使 CUDA 内核能够将内存传输与计算重叠。以下示例实现了一个两阶段 pipeline,该 pipeline 将数据传输与计算重叠。它
初始化 pipeline 共享状态(更多内容见下文)
通过为第一批调度
memcpy_async
来启动 pipeline。循环遍历所有批次:它为下一批调度
memcpy_async
,阻塞所有线程等待前一批memcpy_async
完成,然后将前一批的计算与下一批内存的异步拷贝重叠。最后,它通过对最后一批执行计算来清空 pipeline。
请注意,为了与 cuda::pipeline
互操作,此处使用了来自 cuda/pipeline
头文件的 cuda::memcpy_async
。
#include <cooperative_groups/memcpy_async.h>
#include <cuda/pipeline>
__device__ void compute(int* global_out, int const* shared_in);
__global__ void with_staging(int* global_out, int const* global_in, size_t size, size_t batch_sz) {
auto grid = cooperative_groups::this_grid();
auto block = cooperative_groups::this_thread_block();
assert(size == batch_sz * grid.size()); // Assume input size fits batch_sz * grid_size
constexpr size_t stages_count = 2; // Pipeline with two stages
// Two batches must fit in shared memory:
extern __shared__ int shared[]; // stages_count * block.size() * sizeof(int) bytes
size_t shared_offset[stages_count] = { 0, block.size() }; // Offsets to each batch
// Allocate shared storage for a two-stage cuda::pipeline:
__shared__ cuda::pipeline_shared_state<
cuda::thread_scope::thread_scope_block,
stages_count
> shared_state;
auto pipeline = cuda::make_pipeline(block, &shared_state);
// Each thread processes `batch_sz` elements.
// Compute offset of the batch `batch` of this thread block in global memory:
auto block_batch = [&](size_t batch) -> int {
return block.group_index().x * block.size() + grid.size() * batch;
};
// Initialize first pipeline stage by submitting a `memcpy_async` to fetch a whole batch for the block:
if (batch_sz == 0) return;
pipeline.producer_acquire();
cuda::memcpy_async(block, shared + shared_offset[0], global_in + block_batch(0), sizeof(int) * block.size(), pipeline);
pipeline.producer_commit();
// Pipelined copy/compute:
for (size_t batch = 1; batch < batch_sz; ++batch) {
// Stage indices for the compute and copy stages:
size_t compute_stage_idx = (batch - 1) % 2;
size_t copy_stage_idx = batch % 2;
size_t global_idx = block_batch(batch);
// Collectively acquire the pipeline head stage from all producer threads:
pipeline.producer_acquire();
// Submit async copies to the pipeline's head stage to be
// computed in the next loop iteration
cuda::memcpy_async(block, shared + shared_offset[copy_stage_idx], global_in + global_idx, sizeof(int) * block.size(), pipeline);
// Collectively commit (advance) the pipeline's head stage
pipeline.producer_commit();
// Collectively wait for the operations commited to the
// previous `compute` stage to complete:
pipeline.consumer_wait();
// Computation overlapped with the memcpy_async of the "copy" stage:
compute(global_out + global_idx, shared + shared_offset[compute_stage_idx]);
// Collectively release the stage resources
pipeline.consumer_release();
}
// Compute the data fetch by the last iteration
pipeline.consumer_wait();
compute(global_out + block_batch(batch_sz-1), shared + shared_offset[(batch_sz - 1) % 2]);
pipeline.consumer_release();
}
pipeline 对象 是一个具有头部和尾部的双端队列,用于以先进先出 (FIFO) 的顺序处理工作。生产者线程将工作提交到 pipeline 的头部,而消费者线程从 pipeline 的尾部拉取工作。在上面的示例中,所有线程既是生产者线程又是消费者线程。线程首先提交 memcpy_async
操作以获取下一批,同时它们等待前一批 memcpy_async
操作完成。
将工作提交到 pipeline 阶段涉及
使用
pipeline.producer_acquire()
从一组生产者线程集体获取 pipeline 头部。将
memcpy_async
操作提交到 pipeline 头部。使用
pipeline.producer_commit()
集体提交(推进)pipeline 头部。
使用先前提交的阶段涉及
集体等待阶段完成,例如,使用
pipeline.consumer_wait()
等待尾部(最老)阶段。使用
pipeline.consumer_release()
集体释放阶段。
cuda::pipeline_shared_state<scope, count>
封装了有限的资源,这些资源允许 pipeline 处理最多 count
个并发阶段。如果所有资源都在使用中,pipeline.producer_acquire()
会阻塞生产者线程,直到消费者线程释放下一个 pipeline 阶段的资源。
此示例可以通过将循环的 prolog 和 epilog 与循环本身合并,以更简洁的方式编写,如下所示
template <size_t stages_count = 2 /* Pipeline with stages_count stages */>
__global__ void with_staging_unified(int* global_out, int const* global_in, size_t size, size_t batch_sz) {
auto grid = cooperative_groups::this_grid();
auto block = cooperative_groups::this_thread_block();
assert(size == batch_sz * grid.size()); // Assume input size fits batch_sz * grid_size
extern __shared__ int shared[]; // stages_count * block.size() * sizeof(int) bytes
size_t shared_offset[stages_count];
for (int s = 0; s < stages_count; ++s) shared_offset[s] = s * block.size();
__shared__ cuda::pipeline_shared_state<
cuda::thread_scope::thread_scope_block,
stages_count
> shared_state;
auto pipeline = cuda::make_pipeline(block, &shared_state);
auto block_batch = [&](size_t batch) -> int {
return block.group_index().x * block.size() + grid.size() * batch;
};
// compute_batch: next batch to process
// fetch_batch: next batch to fetch from global memory
for (size_t compute_batch = 0, fetch_batch = 0; compute_batch < batch_sz; ++compute_batch) {
// The outer loop iterates over the computation of the batches
for (; fetch_batch < batch_sz && fetch_batch < (compute_batch + stages_count); ++fetch_batch) {
// This inner loop iterates over the memory transfers, making sure that the pipeline is always full
pipeline.producer_acquire();
size_t shared_idx = fetch_batch % stages_count;
size_t batch_idx = fetch_batch;
size_t block_batch_idx = block_batch(batch_idx);
cuda::memcpy_async(block, shared + shared_offset[shared_idx], global_in + block_batch_idx, sizeof(int) * block.size(), pipeline);
pipeline.producer_commit();
}
pipeline.consumer_wait();
int shared_idx = compute_batch % stages_count;
int batch_idx = compute_batch;
compute(global_out + block_batch(batch_idx), shared + shared_offset[shared_idx]);
pipeline.consumer_release();
}
}
上面使用的 pipeline<thread_scope_block>
原语非常灵活,并支持我们的上述示例未使用的两个功能:块中任何任意线程子集都可以参与 pipeline
,并且在参与的线程中,任何子集都可以是生产者、消费者或两者兼而有之。在以下示例中,线程秩为“偶数”的线程是生产者,而其他线程是消费者
__device__ void compute(int* global_out, int shared_in);
template <size_t stages_count = 2>
__global__ void with_specialized_staging_unified(int* global_out, int const* global_in, size_t size, size_t batch_sz) {
auto grid = cooperative_groups::this_grid();
auto block = cooperative_groups::this_thread_block();
// In this example, threads with "even" thread rank are producers, while threads with "odd" thread rank are consumers:
const cuda::pipeline_role thread_role
= block.thread_rank() % 2 == 0? cuda::pipeline_role::producer : cuda::pipeline_role::consumer;
// Each thread block only has half of its threads as producers:
auto producer_threads = block.size() / 2;
// Map adjacent even and odd threads to the same id:
const int thread_idx = block.thread_rank() / 2;
auto elements_per_batch = size / batch_sz;
auto elements_per_batch_per_block = elements_per_batch / grid.group_dim().x;
extern __shared__ int shared[]; // stages_count * elements_per_batch_per_block * sizeof(int) bytes
size_t shared_offset[stages_count];
for (int s = 0; s < stages_count; ++s) shared_offset[s] = s * elements_per_batch_per_block;
__shared__ cuda::pipeline_shared_state<
cuda::thread_scope::thread_scope_block,
stages_count
> shared_state;
cuda::pipeline pipeline = cuda::make_pipeline(block, &shared_state, thread_role);
// Each thread block processes `batch_sz` batches.
// Compute offset of the batch `batch` of this thread block in global memory:
auto block_batch = [&](size_t batch) -> int {
return elements_per_batch * batch + elements_per_batch_per_block * blockIdx.x;
};
for (size_t compute_batch = 0, fetch_batch = 0; compute_batch < batch_sz; ++compute_batch) {
// The outer loop iterates over the computation of the batches
for (; fetch_batch < batch_sz && fetch_batch < (compute_batch + stages_count); ++fetch_batch) {
// This inner loop iterates over the memory transfers, making sure that the pipeline is always full
if (thread_role == cuda::pipeline_role::producer) {
// Only the producer threads schedule asynchronous memcpys:
pipeline.producer_acquire();
size_t shared_idx = fetch_batch % stages_count;
size_t batch_idx = fetch_batch;
size_t global_batch_idx = block_batch(batch_idx) + thread_idx;
size_t shared_batch_idx = shared_offset[shared_idx] + thread_idx;
cuda::memcpy_async(shared + shared_batch_idx, global_in + global_batch_idx, sizeof(int), pipeline);
pipeline.producer_commit();
}
}
if (thread_role == cuda::pipeline_role::consumer) {
// Only the consumer threads compute:
pipeline.consumer_wait();
size_t shared_idx = compute_batch % stages_count;
size_t global_batch_idx = block_batch(compute_batch) + thread_idx;
size_t shared_batch_idx = shared_offset[shared_idx] + thread_idx;
compute(global_out + global_batch_idx, *(shared + shared_batch_idx));
pipeline.consumer_release();
}
}
}
pipeline
执行了一些优化,例如,当所有线程既是生产者又是消费者时,但在一般情况下,完全消除支持所有这些功能的成本是不可能的。例如,pipeline
在共享内存中存储和使用一组屏障用于同步,如果块中的所有线程都参与 pipeline,则这实际上是不必要的。
对于块中的所有线程都参与 pipeline
的特定情况,我们可以通过使用 pipeline<thread_scope_thread>
结合 __syncthreads()
来做得更好,而不是使用 pipeline<thread_scope_block>
template<size_t stages_count>
__global__ void with_staging_scope_thread(int* global_out, int const* global_in, size_t size, size_t batch_sz) {
auto grid = cooperative_groups::this_grid();
auto block = cooperative_groups::this_thread_block();
auto thread = cooperative_groups::this_thread();
assert(size == batch_sz * grid.size()); // Assume input size fits batch_sz * grid_size
extern __shared__ int shared[]; // stages_count * block.size() * sizeof(int) bytes
size_t shared_offset[stages_count];
for (int s = 0; s < stages_count; ++s) shared_offset[s] = s * block.size();
// No pipeline::shared_state needed
cuda::pipeline<cuda::thread_scope_thread> pipeline = cuda::make_pipeline();
auto block_batch = [&](size_t batch) -> int {
return block.group_index().x * block.size() + grid.size() * batch;
};
for (size_t compute_batch = 0, fetch_batch = 0; compute_batch < batch_sz; ++compute_batch) {
for (; fetch_batch < batch_sz && fetch_batch < (compute_batch + stages_count); ++fetch_batch) {
pipeline.producer_acquire();
size_t shared_idx = fetch_batch % stages_count;
size_t batch_idx = fetch_batch;
// Each thread fetches its own data:
size_t thread_batch_idx = block_batch(batch_idx) + threadIdx.x;
// The copy is performed by a single `thread` and the size of the batch is now that of a single element:
cuda::memcpy_async(thread, shared + shared_offset[shared_idx] + threadIdx.x, global_in + thread_batch_idx, sizeof(int), pipeline);
pipeline.producer_commit();
}
pipeline.consumer_wait();
block.sync(); // __syncthreads: All memcpy_async of all threads in the block for this stage have completed here
int shared_idx = compute_batch % stages_count;
int batch_idx = compute_batch;
compute(global_out + block_batch(batch_idx), shared + shared_offset[shared_idx]);
pipeline.consumer_release();
}
}
如果 compute
操作仅读取与当前线程在同一 warp 中的其他线程写入的共享内存,则 __syncwarp()
就足够了。
7.28.3. Pipeline 接口
关于 cuda::memcpy_async
的完整 API 文档,请参考 libcudacxx API 文档以及一些示例。
pipeline
接口要求
至少 CUDA 11.0,
至少 ISO C++ 2011 兼容性,例如,使用
-std=c++11
编译,以及#include <cuda/pipeline>
.
对于类似 C 的接口,当在没有 ISO C++ 2011 兼容性的情况下编译时,请参阅 Pipeline 原语接口。
7.28.4. Pipeline 原语接口
Pipeline 原语是 memcpy_async
功能的类似 C 的接口。pipeline 原语接口可通过包含 <cuda_pipeline.h>
头文件获得。当在没有 ISO C++ 2011 兼容性的情况下编译时,请包含 <cuda_pipeline_primitives.h>
头文件。
7.28.4.1. memcpy_async
原语
void __pipeline_memcpy_async(void* __restrict__ dst_shared,
const void* __restrict__ src_global,
size_t size_and_align,
size_t zfill=0);
请求提交以下操作以进行异步评估
size_t i = 0; for (; i < size_and_align - zfill; ++i) ((char*)dst_shared)[i] = ((char*)src_global)[i]; /* copy */ for (; i < size_and_align; ++i) ((char*)dst_shared)[i] = 0; /* zero-fill */
要求
dst_shared
必须是指向memcpy_async
的共享内存目标的指针。src_global
必须是指向memcpy_async
的全局内存源的指针。size_and_align
必须为 4、8 或 16。zfill <= size_and_align
.size_and_align
必须是dst_shared
和src_global
的对齐方式。
在等待
memcpy_async
操作完成之前,任何线程修改源内存或观察目标内存都是竞争条件。在提交memcpy_async
操作和等待其完成之间,任何以下操作都会引入竞争条件从
dst_shared
加载。存储到
dst_shared
或src_global
。对
dst_shared
或src_global
应用原子更新。
7.28.4.2. Commit 原语
void __pipeline_commit();
将提交的
memcpy_async
作为当前批次提交到 pipeline。
7.28.4.3. Wait 原语
void __pipeline_wait_prior(size_t N);
令
{0, 1, 2, ..., L}
为给定线程调用__pipeline_commit()
关联的索引序列。等待至少到
L-N
(包括L-N
)的批次完成。
7.28.4.4. Arrive On Barrier 原语
void __pipeline_arrive_on(__mbarrier_t* bar);
bar
指向共享内存中的屏障。将屏障到达计数增加 1,当在此调用之前排序的所有 memcpy_async 操作都已完成时,到达计数将减少 1,因此对到达计数的净影响为零。用户有责任确保到达计数的增量不超过
__mbarrier_maximum_count()
。
7.29. 使用张量内存加速器 (TMA) 的异步数据拷贝
许多应用程序需要从全局内存移动大量数据。通常,数据在全局内存中以具有非顺序数据访问模式的多维数组形式布局。为了减少全局内存使用量,在计算之前,此类数组的子瓦片被复制到共享内存。加载和存储涉及地址计算,这些计算可能容易出错且重复。为了卸载这些计算,计算能力 9.0 引入了张量内存加速器 (TMA)。TMA 的主要目标是为多维数组提供从全局内存到共享内存的高效数据传输机制。
命名。张量内存加速器 (TMA) 是一个广泛的术语,用于指代本节中描述的功能。为了向前兼容性并减少与 PTX ISA 的差异,本节中的文本将 TMA 操作称为批量异步拷贝或批量张量异步拷贝,具体取决于所使用的拷贝类型。“批量”一词用于将这些操作与上一节中描述的异步内存操作区分开来。
维度。TMA 支持拷贝一维和多维数组(最多 5 维)。一维连续数组的批量异步拷贝的编程模型与多维数组的批量张量异步拷贝的编程模型不同。要执行多维数组的批量张量异步拷贝,硬件需要一个 张量映射。此对象描述了多维数组在全局内存和共享内存中的布局。张量映射通常在主机上使用 cuTensorMapEncode API 创建,然后作为带有 __grid_constant__
注解的 const
内核参数从主机传输到设备。张量映射作为带有 __grid_constant__
注解的 const
内核参数从主机传输到设备,并且可以在设备上用于在共享内存和全局内存之间拷贝数据瓦片。相比之下,执行连续一维数组的批量异步拷贝不需要张量映射:它可以在设备上使用指针和大小参数执行。
源和目标。批量异步拷贝操作的源地址和目标地址可以在共享内存或全局内存中。这些操作可以从全局内存读取数据到共享内存,将数据从共享内存写入到全局内存,还可以从共享内存拷贝到同一集群中另一个块的 分布式共享内存。此外,当在集群中时,可以将批量异步操作指定为组播。在这种情况下,数据可以从全局内存传输到集群内多个块的共享内存。组播功能针对目标架构 sm_90a
进行了优化,并且在其他目标上可能具有 显著降低的性能。因此,建议与 计算架构 sm_90a
一起使用。
异步。使用 TMA 的数据传输是 异步的。这允许启动线程在硬件异步拷贝数据时继续计算。数据传输在实践中是否异步发生取决于硬件实现,并且将来可能会发生变化。批量异步操作可以使用多种 完成机制 来指示它们已完成。当操作从全局内存读取到共享内存时,块中的任何线程都可以通过等待 共享内存屏障 来等待数据在共享内存中变为可读。当批量异步操作将数据从共享内存写入到全局内存或分布式共享内存时,只有启动线程可以等待操作完成。这是通过基于批量异步组的完成机制来实现的。描述完成机制的表格可以在下面和 PTX ISA 中找到。
方向 |
完成机制 |
||
---|---|---|---|
目标 |
源 |
异步拷贝 |
批量异步拷贝 (TMA) |
全局 |
全局 |
||
全局 |
Shared::cta |
批量异步组 |
|
Shared::cta |
全局 |
异步组,mbarrier |
Mbarrier |
Shared::cluster |
全局 |
Mbarrier (组播) |
|
Shared::cta |
Shared::cluster |
Mbarrier |
|
Shared::cta |
Shared::cta |
7.29.1. 使用 TMA 传输一维数组
本节演示如何编写一个简单的内核,该内核使用 TMA 读取-修改-写入一维数组。这展示了如何使用批量异步拷贝加载和存储数据,以及如何使用这些拷贝同步执行线程。
内核代码包含在下面。某些功能需要通过 libcu++ 当前提供的内联 PTX 汇编。可以使用以下代码检查这些包装器的可用性
#if defined(__CUDA_MINIMUM_ARCH__) && __CUDA_MINIMUM_ARCH__ < 900
static_assert(false, "Device code is being compiled with older architectures that are incompatible with TMA.");
#endif // __CUDA_MINIMUM_ARCH__
内核经历以下阶段
初始化共享内存屏障。
启动从全局内存到共享内存的内存块的批量异步拷贝。
到达并等待共享内存屏障。
增加共享内存缓冲区值。
等待共享内存写入对后续批量异步拷贝可见,即在下一步之前在 异步代理 中对共享内存写入进行排序。
启动将共享内存中的缓冲区批量异步拷贝到全局内存。
在内核末尾等待批量异步拷贝完成读取共享内存。
#include <cuda/barrier>
#include <cuda/ptx>
using barrier = cuda::barrier<cuda::thread_scope_block>;
namespace ptx = cuda::ptx;
static constexpr size_t buf_len = 1024;
__global__ void add_one_kernel(int* data, size_t offset)
{
// Shared memory buffer. The destination shared memory buffer of
// a bulk operations should be 16 byte aligned.
__shared__ alignas(16) int smem_data[buf_len];
// 1. a) Initialize shared memory barrier with the number of threads participating in the barrier.
// b) Make initialized barrier visible in async proxy.
#pragma nv_diag_suppress static_var_with_dynamic_init
__shared__ barrier bar;
if (threadIdx.x == 0) {
init(&bar, blockDim.x); // a)
ptx::fence_proxy_async(ptx::space_shared); // b)
}
__syncthreads();
// 2. Initiate TMA transfer to copy global to shared memory.
if (threadIdx.x == 0) {
// 3a. cuda::memcpy_async arrives on the barrier and communicates
// how many bytes are expected to come in (the transaction count)
cuda::memcpy_async(
smem_data,
data + offset,
cuda::aligned_size_t<16>(sizeof(smem_data)),
bar
);
}
// 3b. All threads arrive on the barrier
barrier::arrival_token token = bar.arrive();
// 3c. Wait for the data to have arrived.
bar.wait(std::move(token));
// 4. Compute saxpy and write back to shared memory
for (int i = threadIdx.x; i < buf_len; i += blockDim.x) {
smem_data[i] += 1;
}
// 5. Wait for shared memory writes to be visible to TMA engine.
ptx::fence_proxy_async(ptx::space_shared); // b)
__syncthreads();
// After syncthreads, writes by all threads are visible to TMA engine.
// 6. Initiate TMA transfer to copy shared memory to global memory
if (threadIdx.x == 0) {
ptx::cp_async_bulk(
ptx::space_global,
ptx::space_shared,
data + offset, smem_data, sizeof(smem_data));
// 7. Wait for TMA transfer to have finished reading shared memory.
// Create a "bulk async-group" out of the previous bulk copy operation.
ptx::cp_async_bulk_commit_group();
// Wait for the group to have completed reading from shared memory.
ptx::cp_async_bulk_wait_group_read(ptx::n32_t<0>());
}
}
屏障初始化。屏障使用参与块的线程数进行初始化。因此,只有当所有线程都到达此屏障时,屏障才会翻转。使用 cuda::barrier 的异步数据拷贝 中更详细地描述了共享内存屏障。为了使初始化的屏障对后续批量异步拷贝可见,使用了 fence.proxy.async.shared::cta
指令。此指令确保后续批量异步拷贝操作在初始化的屏障上运行。
TMA 读取。批量异步拷贝指令指示硬件将大数据块拷贝到共享内存中,并在读取完成后更新共享内存屏障的 事务计数。通常,发出尽可能少且尺寸尽可能大的批量拷贝可获得最佳性能。由于拷贝可以由硬件异步执行,因此无需将拷贝拆分为较小的块。
启动批量异步拷贝操作的线程使用 mbarrier.expect_tx
到达屏障。这由 cuda::memcpy_async
自动执行。这告诉屏障线程已到达,以及预计到达的字节数(tx / 事务)。只需要单个线程更新预期事务计数。如果多个线程更新事务计数,则预期事务将是更新的总和。只有当所有线程都已到达并且所有字节都已到达时,屏障才会翻转。一旦屏障翻转,字节就可以安全地从共享内存中读取,无论是线程还是后续批量异步拷贝都可以读取。有关屏障事务计数的更多信息,请参见 PTX ISA。
屏障等待。等待屏障翻转是使用 mbarrier.try_wait
完成的。它可以返回 true,表示等待结束,也可以返回 false,这可能表示等待超时。while 循环等待完成,并在超时时重试。
SMEM 写入和同步。缓冲区值的递增操作会读取和写入共享内存。为了使写入对后续批量异步拷贝可见,使用了 fence.proxy.async.shared::cta
指令。这在批量异步拷贝操作的后续读取之前对共享内存的写入进行排序,这些操作通过异步代理读取。因此,每个线程首先通过 fence.proxy.async.shared::cta
在异步代理中对共享内存中对象的写入进行排序,并且所有线程的这些操作都在线程 0 中使用 __syncthreads()
执行的异步操作之前排序。
TMA 写入和同步。从共享内存到全局内存的写入再次由单个线程启动。写入的完成不由共享内存屏障跟踪。而是使用线程本地机制。可以将多个写入批处理到一个所谓的批量异步组中。之后,线程可以等待此组中的所有操作完成从共享内存的读取(如上面的代码中所示)或完成写入到全局内存,从而使写入对启动线程可见。有关更多信息,请参阅 cp.async.bulk.wait_group 的 PTX ISA 文档。请注意,批量异步和非批量异步拷贝指令具有不同的异步组:既存在 cp.async.wait_group
指令,也存在 cp.async.bulk.wait_group
指令。
批量异步指令对其源地址和目标地址具有特定的对齐要求。更多信息可以在下表中找到。
地址 / 大小 |
对齐 |
---|---|
全局内存地址 |
必须是 16 字节对齐。 |
共享内存地址 |
必须是 16 字节对齐。 |
共享内存屏障地址 |
必须是 8 字节对齐(这由 |
传输大小 |
必须是 16 字节的倍数。 |
7.29.2. 使用 TMA 传输多维数组
一维和多维情况之间的主要区别在于,必须在主机上创建张量映射并将其传递给 CUDA 内核。本节介绍如何使用 CUDA 驱动程序 API 创建张量映射,如何将其传递到设备,以及如何在设备上使用它。
驱动程序 API。张量映射是使用 cuTensorMapEncodeTiled 驱动程序 API 创建的。可以通过直接链接到驱动程序 (-lcuda
) 或使用 cudaGetDriverEntryPoint API 来访问此 API。下面,我们展示了如何获取指向 cuTensorMapEncodeTiled
API 的指针。有关更多信息,请参阅 驱动程序入口点访问。
#include <cudaTypedefs.h> // PFN_cuTensorMapEncodeTiled, CUtensorMap
PFN_cuTensorMapEncodeTiled_v12000 get_cuTensorMapEncodeTiled() {
// Get pointer to cuTensorMapEncodeTiled
cudaDriverEntryPointQueryResult driver_status;
void* cuTensorMapEncodeTiled_ptr = nullptr;
CUDA_CHECK(cudaGetDriverEntryPointByVersion("cuTensorMapEncodeTiled", &cuTensorMapEncodeTiled_ptr, 12000, cudaEnableDefault, &driver_status));
assert(driver_status == cudaDriverEntryPointSuccess);
return reinterpret_cast<PFN_cuTensorMapEncodeTiled_v12000>(cuTensorMapEncodeTiled_ptr);
}
创建。创建张量映射需要许多参数。其中包括全局内存中数组的基指针、数组的大小(以元素数为单位)、从一行到下一行的步幅(以字节为单位)、共享内存缓冲区的大小(以元素数为单位)。下面的代码创建了一个张量映射,用于描述大小为 GMEM_HEIGHT x GMEM_WIDTH
的二维行优先数组。请注意参数的顺序:移动最快的维度排在第一位。
CUtensorMap tensor_map{};
// rank is the number of dimensions of the array.
constexpr uint32_t rank = 2;
uint64_t size[rank] = {GMEM_WIDTH, GMEM_HEIGHT};
// The stride is the number of bytes to traverse from the first element of one row to the next.
// It must be a multiple of 16.
uint64_t stride[rank - 1] = {GMEM_WIDTH * sizeof(int)};
// The box_size is the size of the shared memory buffer that is used as the
// destination of a TMA transfer.
uint32_t box_size[rank] = {SMEM_WIDTH, SMEM_HEIGHT};
// The distance between elements in units of sizeof(element). A stride of 2
// can be used to load only the real component of a complex-valued tensor, for instance.
uint32_t elem_stride[rank] = {1, 1};
// Get a function pointer to the cuTensorMapEncodeTiled driver API.
auto cuTensorMapEncodeTiled = get_cuTensorMapEncodeTiled();
// Create the tensor descriptor.
CUresult res = cuTensorMapEncodeTiled(
&tensor_map, // CUtensorMap *tensorMap,
CUtensorMapDataType::CU_TENSOR_MAP_DATA_TYPE_INT32,
rank, // cuuint32_t tensorRank,
tensor_ptr, // void *globalAddress,
size, // const cuuint64_t *globalDim,
stride, // const cuuint64_t *globalStrides,
box_size, // const cuuint32_t *boxDim,
elem_stride, // const cuuint32_t *elementStrides,
// Interleave patterns can be used to accelerate loading of values that
// are less than 4 bytes long.
CUtensorMapInterleave::CU_TENSOR_MAP_INTERLEAVE_NONE,
// Swizzling can be used to avoid shared memory bank conflicts.
CUtensorMapSwizzle::CU_TENSOR_MAP_SWIZZLE_NONE,
// L2 Promotion can be used to widen the effect of a cache-policy to a wider
// set of L2 cache lines.
CUtensorMapL2promotion::CU_TENSOR_MAP_L2_PROMOTION_NONE,
// Any element that is outside of bounds will be set to zero by the TMA transfer.
CUtensorMapFloatOOBfill::CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE
);
主机到设备传输。有三种方法可以使张量映射可供设备代码访问。推荐的方法是将张量映射作为 const __grid_constant__
参数传递给内核。其他可能性是使用 cudaMemcpyToSymbol
将张量映射拷贝到设备 __constant__
内存中,或通过全局内存访问它。当将张量映射作为参数传递时,某些版本的 GCC C++ 编译器会发出警告“GCC 4.6 中已更改了用于传递具有 64 字节对齐的参数的 ABI”。可以忽略此警告。
#include <cuda.h>
__global__ void kernel(const __grid_constant__ CUtensorMap tensor_map)
{
// Use tensor_map here.
}
int main() {
CUtensorMap map;
// [ ..Initialize map.. ]
kernel<<<1, 1>>>(map);
}
作为 __grid_constant__
内核参数的替代方案,可以使用全局 常量 变量。下面包含一个示例。
#include <cuda.h>
__constant__ CUtensorMap global_tensor_map;
__global__ void kernel()
{
// Use global_tensor_map here.
}
int main() {
CUtensorMap local_tensor_map;
// [ ..Initialize map.. ]
cudaMemcpyToSymbol(global_tensor_map, &local_tensor_map, sizeof(CUtensorMap));
kernel<<<1, 1>>>();
}
最后,可以将张量映射拷贝到全局内存。在每个线程块中使用指向全局设备内存中张量映射的指针之前,需要一个 fence。除非再次修改张量映射,否则该线程块对张量映射的进一步使用不需要 fence。请注意,此机制可能比上述两种机制慢。
#include <cuda.h>
#include <cuda/ptx>
namespace ptx = cuda::ptx;
__device__ CUtensorMap global_tensor_map;
__global__ void kernel(CUtensorMap *tensor_map)
{
// Fence acquire tensor map:
ptx::n32_t<128> size_bytes;
// Since the tensor map was modified from the host using cudaMemcpy,
// the scope should be .sys.
ptx::fence_proxy_tensormap_generic(
ptx::sem_acquire, ptx::scope_sys, tensor_map, size_bytes
);
// Safe to use tensor_map after fence inside this thread..
}
int main() {
CUtensorMap local_tensor_map;
// [ ..Initialize map.. ]
cudaMemcpy(&global_tensor_map, &local_tensor_map, sizeof(CUtensorMap), cudaMemcpyHostToDevice);
kernel<<<1, 1>>>(global_tensor_map);
}
使用。下面的内核从更大的二维数组加载大小为 SMEM_HEIGHT x SMEM_WIDTH
的二维瓦片。瓦片的左上角由索引 x
和 y
指示。瓦片被加载到共享内存中,修改后,再写回全局内存。
#include <cuda.h> // CUtensormap
#include <cuda/barrier>
using barrier = cuda::barrier<cuda::thread_scope_block>;
namespace cde = cuda::device::experimental;
__global__ void kernel(const __grid_constant__ CUtensorMap tensor_map, int x, int y) {
// The destination shared memory buffer of a bulk tensor operation should be
// 128 byte aligned.
__shared__ alignas(128) int smem_buffer[SMEM_HEIGHT][SMEM_WIDTH];
// Initialize shared memory barrier with the number of threads participating in the barrier.
#pragma nv_diag_suppress static_var_with_dynamic_init
__shared__ barrier bar;
if (threadIdx.x == 0) {
// Initialize barrier. All `blockDim.x` threads in block participate.
init(&bar, blockDim.x);
// Make initialized barrier visible in async proxy.
cde::fence_proxy_async_shared_cta();
}
// Syncthreads so initialized barrier is visible to all threads.
__syncthreads();
barrier::arrival_token token;
if (threadIdx.x == 0) {
// Initiate bulk tensor copy.
cde::cp_async_bulk_tensor_2d_global_to_shared(&smem_buffer, &tensor_map, x, y, bar);
// Arrive on the barrier and tell how many bytes are expected to come in.
token = cuda::device::barrier_arrive_tx(bar, 1, sizeof(smem_buffer));
} else {
// Other threads just arrive.
token = bar.arrive();
}
// Wait for the data to have arrived.
bar.wait(std::move(token));
// Symbolically modify a value in shared memory.
smem_buffer[0][threadIdx.x] += threadIdx.x;
// Wait for shared memory writes to be visible to TMA engine.
cde::fence_proxy_async_shared_cta();
__syncthreads();
// After syncthreads, writes by all threads are visible to TMA engine.
// Initiate TMA transfer to copy shared memory to global memory
if (threadIdx.x == 0) {
cde::cp_async_bulk_tensor_2d_shared_to_global(&tensor_map, x, y, &smem_buffer);
// Wait for TMA transfer to have finished reading shared memory.
// Create a "bulk async-group" out of the previous bulk copy operation.
cde::cp_async_bulk_commit_group();
// Wait for the group to have completed reading from shared memory.
cde::cp_async_bulk_wait_group_read<0>();
}
// Destroy barrier. This invalidates the memory region of the barrier. If
// further computations were to take place in the kernel, this allows the
// memory location of the shared memory barrier to be reused.
if (threadIdx.x == 0) {
(&bar)->~barrier();
}
}
负索引和越界。当从全局内存读取到共享内存的瓦片部分越界时,对应于越界区域的共享内存将被零填充。瓦片的左上角索引也可能为负数。当从共享内存写入到全局内存时,瓦片的部分可能越界,但左上角不能有任何负索引。
大小和步幅。张量的大小是沿一个维度的元素数。所有大小都必须大于 1。步幅是同一维度的元素之间的字节数。例如,4 x 4 整数矩阵的大小为 4 和 4。由于每个元素占用 4 个字节,因此步幅为 4 和 16 字节。由于对齐要求,4 x 3 行优先整数矩阵也必须具有 4 和 16 字节的步幅。每行都填充了额外的 4 个字节,以确保下一行的开头与 16 字节对齐。有关对齐的更多信息,请参阅表 计算能力 9.0 中多维批量张量异步拷贝操作的对齐要求。。
地址 / 大小 |
对齐 |
---|---|
全局内存地址 |
必须是 16 字节对齐。 |
全局内存大小 |
必须大于或等于 1。不必是 16 字节的倍数。 |
全局内存步幅 |
必须是 16 字节的倍数。 |
共享内存地址 |
必须是 128 字节对齐。 |
共享内存屏障地址 |
必须是 8 字节对齐(这由 |
传输大小 |
必须是 16 字节的倍数。 |
7.29.2.1. 多维 TMA PTX 包装器
下面,PTX 指令按其在上面的示例代码中的使用顺序排列。
cp.async.bulk.tensor 指令启动全局内存和共享内存之间的批量张量异步拷贝。下面的包装器从全局内存读取到共享内存,并从共享内存写入到全局内存。
// https://docs.nvda.net.cn/cuda/parallel-thread-execution/index.html#data-movement-and-conversion-instructions-cp-async-bulk-tensor
inline __device__
void cuda::device::experimental::cp_async_bulk_tensor_1d_global_to_shared(
void *dest, const CUtensorMap *tensor_map , int c0, cuda::barrier<cuda::thread_scope_block> &bar
);
// https://docs.nvda.net.cn/cuda/parallel-thread-execution/index.html#data-movement-and-conversion-instructions-cp-async-bulk-tensor
inline __device__
void cuda::device::experimental::cp_async_bulk_tensor_2d_global_to_shared(
void *dest, const CUtensorMap *tensor_map , int c0, int c1, cuda::barrier<cuda::thread_scope_block> &bar
);
// https://docs.nvda.net.cn/cuda/parallel-thread-execution/index.html#data-movement-and-conversion-instructions-cp-async-bulk-tensor
inline __device__
void cuda::device::experimental::cp_async_bulk_tensor_3d_global_to_shared(
void *dest, const CUtensorMap *tensor_map, int c0, int c1, int c2, cuda::barrier<cuda::thread_scope_block> &bar
);
// https://docs.nvda.net.cn/cuda/parallel-thread-execution/index.html#data-movement-and-conversion-instructions-cp-async-bulk-tensor
inline __device__
void cuda::device::experimental::cp_async_bulk_tensor_4d_global_to_shared(
void *dest, const CUtensorMap *tensor_map , int c0, int c1, int c2, int c3, cuda::barrier<cuda::thread_scope_block> &bar
);
// https://docs.nvda.net.cn/cuda/parallel-thread-execution/index.html#data-movement-and-conversion-instructions-cp-async-bulk-tensor
inline __device__
void cuda::device::experimental::cp_async_bulk_tensor_5d_global_to_shared(
void *dest, const CUtensorMap *tensor_map , int c0, int c1, int c2, int c3, int c4, cuda::barrier<cuda::thread_scope_block> &bar
);
// https://docs.nvda.net.cn/cuda/parallel-thread-execution/index.html#data-movement-and-conversion-instructions-cp-async-bulk-tensor
inline __device__
void cuda::device::experimental::cp_async_bulk_tensor_1d_shared_to_global(
const CUtensorMap *tensor_map, int c0, const void *src
);
// https://docs.nvda.net.cn/cuda/parallel-thread-execution/index.html#data-movement-and-conversion-instructions-cp-async-bulk-tensor
inline __device__
void cuda::device::experimental::cp_async_bulk_tensor_2d_shared_to_global(
const CUtensorMap *tensor_map, int c0, int c1, const void *src
);
// https://docs.nvda.net.cn/cuda/parallel-thread-execution/index.html#data-movement-and-conversion-instructions-cp-async-bulk-tensor
inline __device__
void cuda::device::experimental::cp_async_bulk_tensor_3d_shared_to_global(
const CUtensorMap *tensor_map, int c0, int c1, int c2, const void *src
);
// https://docs.nvda.net.cn/cuda/parallel-thread-execution/index.html#data-movement-and-conversion-instructions-cp-async-bulk-tensor
inline __device__
void cuda::device::experimental::cp_async_bulk_tensor_4d_shared_to_global(
const CUtensorMap *tensor_map, int c0, int c1, int c2, int c3, const void *src
);
// https://docs.nvda.net.cn/cuda/parallel-thread-execution/index.html#data-movement-and-conversion-instructions-cp-async-bulk-tensor
inline __device__
void cuda::device::experimental::cp_async_bulk_tensor_5d_shared_to_global(
const CUtensorMap *tensor_map, int c0, int c1, int c2, int c3, int c4, const void *src
);
7.29.3. TMA 混洗
默认情况下,TMA 引擎以与数据在全局内存中布局相同的顺序将数据加载到共享内存中。但是,对于某些共享内存访问模式,此布局可能不是最佳的,因为它可能导致共享内存存储体冲突。为了提高性能并减少存储体冲突,我们可以通过应用“混洗模式”来更改共享内存布局。
共享内存有 32 个存储体,这些存储体的组织方式使得连续的 32 位字映射到连续的存储体。每个存储体每个时钟周期的带宽为 32 位。在加载和存储共享内存时,如果在事务中多次使用同一存储体,则会发生存储体冲突,从而导致带宽降低。请参阅 共享内存,存储体冲突。
为了确保数据以用户代码可以避免共享内存存储体冲突的方式布局在共享内存中,可以指示 TMA 引擎在将数据存储到共享内存之前“混洗”数据,并在将数据从共享内存拷贝回全局内存时“取消混洗”数据。张量映射编码了“混洗模式”,指示使用了哪种混洗模式。
7.29.3.1. 示例“矩阵转置”
一个示例是矩阵的转置,其中数据从行映射到列优先访问。数据以行优先方式存储在全局内存中,但我们也希望在共享内存中按列访问它,这会导致存储体冲突。但是,通过使用 128 字节的“混洗”模式和新的共享内存索引,可以消除它们。
在该示例中,我们将类型为 int4
的 8x8 矩阵(以行优先方式存储在全局内存中)加载到共享内存中。然后,每组八个线程从共享内存缓冲区加载一行,并将其存储到单独的转置共享内存缓冲区中的一列。这在存储时导致八路存储体冲突。最后,将转置缓冲区写回全局内存。
为了避免存储体冲突,可以使用 CU_TENSOR_MAP_SWIZZLE_128B
布局。此布局与 128 字节的行长度匹配,并以这样一种方式更改共享内存布局,即按列和按行访问都不需要每个事务使用相同的存储体。
下方的两个表,图 26 和 图 27,展示了类型为 int4
的 8x8 矩阵及其转置矩阵的正常和混合(swizzled)共享内存布局。颜色表示矩阵元素映射到的八组bank中的哪一组,页边行和页边列列出了全局内存的行和列索引。条目显示了 16 字节矩阵元素的共享内存索引。

图 26 在没有混合的共享内存数据布局中,共享内存索引等同于全局内存索引。每次加载指令,读取一行并存储在转置缓冲区的列中。由于转置中列的所有矩阵元素都落在同一个bank中,因此存储必须串行化,从而导致八次存储事务,每个存储列产生八路bank冲突。

图 27 使用 CU_TENSOR_MAP_SWIZZLE_128B
混合的共享内存数据布局。一行存储在一列中,行和列的每个矩阵元素都来自不同的bank,因此没有任何bank冲突。
__global__ void kernel_tma(const __grid_constant__ CUtensorMap tensor_map) {
// The destination shared memory buffer of a bulk tensor operation
// with the 128-byte swizzle mode, it should be 1024 bytes aligned.
__shared__ alignas(1024) int4 smem_buffer[8][8];
__shared__ alignas(1024) int4 smem_buffer_tr[8][8];
// Initialize shared memory barrier
#pragma nv_diag_suppress static_var_with_dynamic_init
__shared__ barrier bar;
if (threadIdx.x == 0) {
init(&bar, blockDim.x);
cde::fence_proxy_async_shared_cta();
}
__syncthreads();
barrier::arrival_token token;
if (threadIdx.x == 0) {
// Initiate bulk tensor copy from global to shared memory,
// in the same way as without swizzle.
cde::cp_async_bulk_tensor_2d_global_to_shared(&smem_buffer, &tensor_map, 0, 0, bar);
token = cuda::device::barrier_arrive_tx(bar, 1, sizeof(smem_buffer));
} else {
token = bar.arrive();
}
bar.wait(std::move(token));
/* Matrix transpose
* When using the normal shared memory layout, there are eight
* 8-way shared memory bank conflict when storing to the transpose.
* When enabling the 128-byte swizzle pattern and using the according access pattern,
* they are eliminated both for load and store. */
for(int sidx_j =threadIdx.x; sidx_j < 8; sidx_j+= blockDim.x){
for(int sidx_i = 0; sidx_i < 8; ++sidx_i){
const int swiz_j_idx = (sidx_i % 8) ^ sidx_j;
const int swiz_i_idx_tr = (sidx_j % 8) ^ sidx_i;
smem_buffer_tr[sidx_j][swiz_i_idx_tr] = smem_buffer[sidx_i][swiz_j_idx];
}
}
// Wait for shared memory writes to be visible to TMA engine.
cde::fence_proxy_async_shared_cta();
__syncthreads();
/* Initiate TMA transfer to copy the transposed shared memory buffer back to global memory,
* it will 'unswizzle' the data. */
if (threadIdx.x == 0) {
cde::cp_async_bulk_tensor_2d_shared_to_global(&tensor_map, 0, 0, &smem_buffer_tr);
cde::cp_async_bulk_commit_group();
cde::cp_async_bulk_wait_group_read<0>();
}
// Destroy barrier
if (threadIdx.x == 0) {
(&bar)->~barrier();
}
}
// --------------------------------- main ----------------------------------------
int main(){
...
void* tensor_ptr = d_data;
CUtensorMap tensor_map{};
// rank is the number of dimensions of the array.
constexpr uint32_t rank = 2;
// global memory size
uint64_t size[rank] = {4*8, 8};
// global memory stride, must be a multiple of 16.
uint64_t stride[rank - 1] = {8 * sizeof(int4)};
// The inner shared memory box dimension in bytes, equal to the swizzle span.
uint32_t box_size[rank] = {4*8, 8};
uint32_t elem_stride[rank] = {1, 1};
// Create the tensor descriptor.
CUresult res = cuTensorMapEncodeTiled(
&tensor_map, // CUtensorMap *tensorMap,
CUtensorMapDataType::CU_TENSOR_MAP_DATA_TYPE_INT32,
rank, // cuuint32_t tensorRank,
tensor_ptr, // void *globalAddress,
size, // const cuuint64_t *globalDim,
stride, // const cuuint64_t *globalStrides,
box_size, // const cuuint32_t *boxDim,
elem_stride, // const cuuint32_t *elementStrides,
CUtensorMapInterleave::CU_TENSOR_MAP_INTERLEAVE_NONE,
// Using a swizzle pattern of 128 bytes.
CUtensorMapSwizzle::CU_TENSOR_MAP_SWIZZLE_128B,
CUtensorMapL2promotion::CU_TENSOR_MAP_L2_PROMOTION_NONE,
CUtensorMapFloatOOBfill::CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE
);
kernel_tma<<<1, 8>>>(tensor_map);
...
}
备注。 此示例旨在展示混合的用法, ‘按原样’ 既不高效,也无法扩展到给定的维度之外。
解释。 在数据传输期间,TMA 引擎根据混合模式对数据进行洗牌,如下表所述。这些混合模式定义了沿混合宽度到四个bank子组的 16 字节块的映射。它的类型为 CUtensorMapSwizzle
,有四个选项:none、32 字节、64 字节和 128 字节。请注意,共享内存框的内维度必须小于或等于混合模式的跨度。
7.29.3.2. 混合模式
如前所述,有四种混合模式。下表显示了不同的混合模式,包括新的共享内存索引的关系。这些表定义了沿 128 字节到八个四个bank子组的 16 字节块的映射。

图 28 TMA 混合模式概述
注意事项。 应用 TMA 混合模式时,必须遵守特定的内存要求
全局内存对齐:全局内存必须对齐到 128 字节。
共享内存对齐:共享内存必须根据混合模式重复后的字节数对齐。如果未保持此对齐,则混合索引计算将导致未定义的映射。为了处理这种情况,您可以将内存对齐到 128 字节,并在计算中添加偏移量。请参阅下面的备注。
内维度:共享内存块的内维度必须满足表 表 9 中指定的大小要求。如果未满足这些要求,则该指令被视为无效。此外,如果混合宽度超过内维度,请确保分配共享内存以容纳完整的混合宽度。
粒度:混合映射的粒度固定为 16 字节。这意味着数据以 16 字节的块进行组织和访问,在规划内存布局和访问模式时必须考虑到这一点。
备注,基于指针的方法访问混合数据。 当共享内存缓冲区未按混合模式重复自身的字节数对齐时,混合模式具有偏移量。在这里,我们描述如何确定此偏移量。使用 TMA 时,共享内存需要对齐到 128 字节。要找到共享内存缓冲区偏移了多少次,请应用
data_t* ptr = &smem_buffer[0][0];
int offset = (reinterpret_cast<uintptr_t>(ptr) >> 0x7) & 0x7;
在 图 28 中,此偏移量表示初始行偏移量,因此,在混合索引计算中,它被添加到行索引 y
中,例如,在 CU_TENSOR_MAP_SWIZZLE_128B
模式下,索引关系变为 smem[y][x] <-> smem[y][((y+offset)%8)^x]
。
总结。 下表 Compute Capability 9 的不同混合模式的要求和属性 总结了 Compute Capability 9 的不同混合模式的要求和属性。
模式 |
混合宽度 |
共享框的内维度 |
重复周期 |
共享内存对齐 |
全局内存对齐 |
---|---|---|---|---|---|
CU_TENSOR_MAP_SWIZZLE_128B |
128 字节 |
<=128 字节 |
1024 字节 |
128 字节 |
128 字节 |
CU_TENSOR_MAP_SWIZZLE_64B |
64 字节 |
<=64 字节 |
512 字节 |
128 字节 |
128 字节 |
CU_TENSOR_MAP_SWIZZLE_32B |
32 字节 |
<=32 字节 |
256 字节 |
128 字节 |
128 字节 |
CU_TENSOR_MAP_SWIZZLE_NONE (默认) |
128 字节 |
16 字节 |
7.30. 在设备上编码张量映射
前面的章节描述了如何在主机上使用 CUDA 驱动程序 API 创建张量映射。
本节介绍如何在设备上编码平铺类型的张量映射。这在以下情况下很有用:当以典型方式传输张量映射(使用 const __grid_constant__
内核参数)不可取时,例如,在单个内核启动中处理一批不同大小的张量时。
推荐的模式如下
使用主机上的 Driver API 创建张量映射“模板”,
template_tensor_map
。在设备内核中,复制
template_tensor_map
,修改副本,存储在全局内存中,并适当设置栅栏。在内核中使用张量映射,并进行适当的栅栏设置。
高级代码结构如下
// Initialize device context:
CUDA_CHECK(cudaDeviceSynchronize());
// Create a tensor map template using the cuTensorMapEncodeTiled driver function
CUtensorMap template_tensor_map = make_tensormap_template();
// Allocate tensor map and tensor in global memory
CUtensorMap* global_tensor_map;
CUDA_CHECK(cudaMalloc(&global_tensor_map, sizeof(CUtensorMap)));
char* global_buf;
CUDA_CHECK(cudaMalloc(&global_buf, 8 * 256));
// Fill global buffer with data.
fill_global_buf<<<1, 1>>>(global_buf);
// Define the parameters of the tensor map that will be created on device.
tensormap_params p{};
p.global_address = global_buf;
p.rank = 2;
p.box_dim[0] = 128; // The box in shared memory has half the width of the full buffer
p.box_dim[1] = 4; // The box in shared memory has half the height of the full buffer
p.global_dim[0] = 256; //
p.global_dim[1] = 8; //
p.global_stride[0] = 256; //
p.element_stride[0] = 1; //
p.element_stride[1] = 1; //
// Encode global_tensor_map on device:
encode_tensor_map<<<1, 32>>>(template_tensor_map, p, global_tensor_map);
// Use it from another kernel:
consume_tensor_map<<<1, 1>>>(global_tensor_map);
// Check for errors:
CUDA_CHECK(cudaDeviceSynchronize());
以下各节描述了高级步骤。在整个示例中,以下 tensormap_params
结构包含要更新的字段的新值。此处包含它是为了在阅读示例时引用。
struct tensormap_params {
void* global_address;
int rank;
uint32_t box_dim[5];
uint64_t global_dim[5];
size_t global_stride[4];
uint32_t element_stride[5];
};
7.30.1. 张量映射的设备端编码和修改
在全局内存中编码张量映射的推荐过程如下。
将现有的张量映射
template_tensor_map
传递给内核。与在cp.async.bulk.tensor
指令中使用张量映射的内核不同,这可以通过任何方式完成:指向全局内存的指针、内核参数、__const___
变量等等。使用 template_tensor_map 值在共享内存中复制初始化张量映射。
使用 cuda::ptx::tensormap_replace 函数修改共享内存中的张量映射。这些函数包装了 tensormap.replace PTX 指令,该指令可用于修改平铺类型张量映射的任何字段,包括基地址、大小、步幅等等。
使用 cuda::ptx::tensormap_copy_fenceproxy 函数,将修改后的张量映射从共享内存复制到全局内存,并执行任何必要的栅栏操作。
以下代码包含一个遵循这些步骤的内核。为了完整性,它修改了张量映射的所有字段。通常,内核只会修改几个字段。
在此内核中,template_tensor_map
作为内核参数传递。这是将 template_tensor_map
从主机移动到设备的首选方式。如果内核旨在更新设备内存中现有的张量映射,它可以获取指向现有张量映射的指针以进行修改。
注意
张量映射的格式可能会随时间而变化。因此,cuda::ptx::tensormap_replace 函数和相应的 tensormap.replace.tile PTX 指令被标记为特定于 sm_90a。要使用它们,请使用 nvcc -arch sm_90a ....
进行编译。
提示
在 sm_90a 上,共享内存中零初始化的缓冲区也可以用作初始张量映射值。这使得完全可以在设备上编码张量映射,而无需使用驱动程序 API 来编码 template_tensor_map value
。
注意
设备上修改仅支持平铺类型张量映射;其他张量映射类型无法在设备上修改。有关张量映射类型的更多信息,请参阅 Driver API 参考。
#include <cuda/ptx>
namespace ptx = cuda::ptx;
// launch with 1 warp.
__launch_bounds__(32)
__global__ void encode_tensor_map(const __grid_constant__ CUtensorMap template_tensor_map, tensormap_params p, CUtensorMap* out) {
__shared__ alignas(128) CUtensorMap smem_tmap;
if (threadIdx.x == 0) {
// Copy template to shared memory:
smem_tmap = template_tensor_map;
const auto space_shared = ptx::space_shared;
ptx::tensormap_replace_global_address(space_shared, &smem_tmap, p.global_address);
// For field .rank, the operand new_val must be ones less than the desired
// tensor rank as this field uses zero-based numbering.
ptx::tensormap_replace_rank(space_shared, &smem_tmap, p.rank - 1);
// Set box dimensions:
if (0 < p.rank) { ptx::tensormap_replace_box_dim(space_shared, &smem_tmap, ptx::n32_t<0>{}, p.box_dim[0]); }
if (1 < p.rank) { ptx::tensormap_replace_box_dim(space_shared, &smem_tmap, ptx::n32_t<1>{}, p.box_dim[1]); }
if (2 < p.rank) { ptx::tensormap_replace_box_dim(space_shared, &smem_tmap, ptx::n32_t<2>{}, p.box_dim[2]); }
if (3 < p.rank) { ptx::tensormap_replace_box_dim(space_shared, &smem_tmap, ptx::n32_t<3>{}, p.box_dim[3]); }
if (4 < p.rank) { ptx::tensormap_replace_box_dim(space_shared, &smem_tmap, ptx::n32_t<4>{}, p.box_dim[4]); }
// Set global dimensions:
if (0 < p.rank) { ptx::tensormap_replace_global_dim(space_shared, &smem_tmap, ptx::n32_t<0>{}, (uint32_t) p.global_dim[0]); }
if (1 < p.rank) { ptx::tensormap_replace_global_dim(space_shared, &smem_tmap, ptx::n32_t<1>{}, (uint32_t) p.global_dim[1]); }
if (2 < p.rank) { ptx::tensormap_replace_global_dim(space_shared, &smem_tmap, ptx::n32_t<2>{}, (uint32_t) p.global_dim[2]); }
if (3 < p.rank) { ptx::tensormap_replace_global_dim(space_shared, &smem_tmap, ptx::n32_t<3>{}, (uint32_t) p.global_dim[3]); }
if (4 < p.rank) { ptx::tensormap_replace_global_dim(space_shared, &smem_tmap, ptx::n32_t<4>{}, (uint32_t) p.global_dim[4]); }
// Set global stride:
if (1 < p.rank) { ptx::tensormap_replace_global_stride(space_shared, &smem_tmap, ptx::n32_t<0>{}, p.global_stride[0]); }
if (2 < p.rank) { ptx::tensormap_replace_global_stride(space_shared, &smem_tmap, ptx::n32_t<1>{}, p.global_stride[1]); }
if (3 < p.rank) { ptx::tensormap_replace_global_stride(space_shared, &smem_tmap, ptx::n32_t<2>{}, p.global_stride[2]); }
if (4 < p.rank) { ptx::tensormap_replace_global_stride(space_shared, &smem_tmap, ptx::n32_t<3>{}, p.global_stride[3]); }
// Set element stride:
if (0 < p.rank) { ptx::tensormap_replace_element_size(space_shared, &smem_tmap, ptx::n32_t<0>{}, p.element_stride[0]); }
if (1 < p.rank) { ptx::tensormap_replace_element_size(space_shared, &smem_tmap, ptx::n32_t<1>{}, p.element_stride[1]); }
if (2 < p.rank) { ptx::tensormap_replace_element_size(space_shared, &smem_tmap, ptx::n32_t<2>{}, p.element_stride[2]); }
if (3 < p.rank) { ptx::tensormap_replace_element_size(space_shared, &smem_tmap, ptx::n32_t<3>{}, p.element_stride[3]); }
if (4 < p.rank) { ptx::tensormap_replace_element_size(space_shared, &smem_tmap, ptx::n32_t<4>{}, p.element_stride[4]); }
// These constants are documented in this table:
// https://docs.nvda.net.cn/cuda/parallel-thread-execution/index.html#tensormap-new-val-validity
auto u8_elem_type = ptx::n32_t<0>{};
ptx::tensormap_replace_elemtype(space_shared, &smem_tmap, u8_elem_type);
auto no_interleave = ptx::n32_t<0>{};
ptx::tensormap_replace_interleave_layout(space_shared, &smem_tmap, no_interleave);
auto no_swizzle = ptx::n32_t<0>{};
ptx::tensormap_replace_swizzle_mode(space_shared, &smem_tmap, no_swizzle);
auto zero_fill = ptx::n32_t<0>{};
ptx::tensormap_replace_fill_mode(space_shared, &smem_tmap, zero_fill);
}
// Synchronize the modifications with other threads in warp
__syncwarp();
// Copy the tensor map to global memory collectively with threads in the warp.
// In addition: make the updated tensor map visible to other threads on device that
// for use with cp.async.bulk.
ptx::n32_t<128> bytes_128;
ptx::tensormap_cp_fenceproxy(ptx::sem_release, ptx::scope_gpu, out, &smem_tmap, bytes_128);
}
7.30.2. 修改后的张量映射的用法
与使用作为 const __grid_constant__
内核参数传递的张量映射相反,在全局内存中使用张量映射需要在修改张量映射的线程和使用它的线程之间的张量映射代理中显式建立发布-获取模式。
模式的发布部分已在上一节中显示。它是使用 cuda::ptx::tensormap.cp_fenceproxy 函数完成的。
获取部分是使用 cuda::ptx::fence_proxy_tensormap_generic 函数完成的,该函数包装了 fence.proxy.tensormap::generic.acquire 指令。如果参与发布-获取模式的两个线程位于同一设备上,则 .gpu
作用域就足够了。如果线程位于不同的设备上,则必须使用 .sys
作用域。一旦一个线程获取了张量映射,同一块中的其他线程就可以在使用足够的同步之后使用它,例如,使用 __syncthreads()
。使用张量映射的线程和执行栅栏操作的线程必须在同一个块中。也就是说,如果线程位于例如同一集群的不同线程块、同一网格或不同的内核中,则诸如 cooperative_groups::cluster
或 grid_group::sync()
或流顺序同步之类的同步 API 不足以建立张量映射更新的顺序,也就是说,这些其他线程块中的线程仍然需要在使用更新后的张量映射之前在正确的作用域中获取张量映射代理。如果没有中间修改,则不必在每个 cp.async.bulk.tensor
指令之前重复栅栏操作。
fence
和随后张量映射的用法在以下示例中显示。
// Consumer of tensor map in global memory:
__global__ void consume_tensor_map(CUtensorMap* tensor_map) {
// Fence acquire tensor map:
ptx::n32_t<128> size_bytes;
ptx::fence_proxy_tensormap_generic(ptx::sem_acquire, ptx::scope_sys, tensor_map, size_bytes);
// Safe to use tensor_map after fence..
__shared__ uint64_t bar;
__shared__ alignas(128) char smem_buf[4][128];
if (threadIdx.x == 0) {
// Initialize barrier
ptx::mbarrier_init(&bar, 1);
// Make barrier init visible in async proxy, i.e., to TMA engine
ptx::fence_proxy_async(ptx::space_shared);
// Issue TMA request
ptx::cp_async_bulk_tensor(ptx::space_cluster, ptx::space_global, smem_buf, tensor_map, {0, 0}, &bar);
// Arrive on barrier. Expect 4 * 128 bytes.
ptx::mbarrier_arrive_expect_tx(ptx::sem_release, ptx::scope_cta, ptx::space_shared, &bar, sizeof(smem_buf));
}
const int parity = 0;
// Wait for load to have completed
while (!ptx::mbarrier_try_wait_parity(&bar, parity)) {}
// print items:
printf("Got:\n\n");
for (int j = 0; j < 4; ++j) {
for (int i = 0; i < 128; ++i) {
printf("%3d ", smem_buf[j][i]);
if (i % 32 == 31) { printf("\n"); };
}
printf("\n");
}
}
7.30.3. 使用 Driver API 创建模板张量映射值
以下代码创建了一个最小的平铺类型张量映射,可以在设备上随后对其进行修改。
CUtensorMap make_tensormap_template() {
CUtensorMap template_tensor_map{};
auto cuTensorMapEncodeTiled = get_cuTensorMapEncodeTiled();
uint32_t dims_32 = 16;
uint64_t dims_strides_64 = 16;
uint32_t elem_strides = 1;
// Create the tensor descriptor.
CUresult res = cuTensorMapEncodeTiled(
&template_tensor_map, // CUtensorMap *tensorMap,
CUtensorMapDataType::CU_TENSOR_MAP_DATA_TYPE_UINT8,
1, // cuuint32_t tensorRank,
nullptr, // void *globalAddress,
&dims_strides_64, // const cuuint64_t *globalDim,
&dims_strides_64, // const cuuint64_t *globalStrides,
&dims_32, // const cuuint32_t *boxDim,
&elem_strides, // const cuuint32_t *elementStrides,
CUtensorMapInterleave::CU_TENSOR_MAP_INTERLEAVE_NONE,
CUtensorMapSwizzle::CU_TENSOR_MAP_SWIZZLE_NONE,
CUtensorMapL2promotion::CU_TENSOR_MAP_L2_PROMOTION_NONE,
CUtensorMapFloatOOBfill::CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE);
CU_CHECK(res);
return template_tensor_map;
}
7.31. 分析器计数器函数
每个多处理器都有一组十六个硬件计数器,应用程序可以通过调用 __prof_trigger()
函数使用单个指令来递增这些计数器。
void __prof_trigger(int counter);
每个 warp 将索引为 counter
的每个多处理器硬件计数器递增一。计数器 8 到 15 是保留的,不应被应用程序使用。
计数器 0、1、…、7 的值可以通过 nvprof
使用 nvprof --events prof_trigger_0x
获得,其中 x
为 0、1、…、7。所有计数器在每次内核启动之前都会重置(请注意,当收集计数器时,内核启动是同步的,如 主机和设备之间的并发执行 中所述)。
7.32. 断言
只有计算能力为 2.x 及更高版本的设备才支持断言。
void assert(int expression);
如果 expression
等于零,则停止内核执行。如果在调试器中运行程序,这将触发断点,并且可以使用调试器来检查设备的当前状态。否则,对于 expression
等于零的每个线程,在通过 cudaDeviceSynchronize()
、cudaStreamSynchronize()
或 cudaEventSynchronize()
与主机同步后,会向stderr打印一条消息。此消息的格式如下
<filename>:<line number>:<function>:
block: [blockId.x,blockId.x,blockIdx.z],
thread: [threadIdx.x,threadIdx.y,threadIdx.z]
Assertion `<expression>` failed.
对同一设备进行的任何后续主机端同步调用都将返回 cudaErrorAssert
。在调用 cudaDeviceReset()
以重新初始化设备之前,无法向此设备发送更多命令。
如果 expression
不为零,则内核执行不受影响。
例如,来自源文件 test.cu 的以下程序
#include <assert.h>
__global__ void testAssert(void)
{
int is_one = 1;
int should_be_one = 0;
// This will have no effect
assert(is_one);
// This will halt kernel execution
assert(should_be_one);
}
int main(int argc, char* argv[])
{
testAssert<<<1,1>>>();
cudaDeviceSynchronize();
return 0;
}
将输出
test.cu:19: void testAssert(): block: [0,0,0], thread: [0,0,0] Assertion `should_be_one` failed.
断言用于调试目的。它们可能会影响性能,因此建议在生产代码中禁用它们。可以通过在包含 assert.h
之前定义 NDEBUG
预处理器宏在编译时禁用它们。请注意,expression
不应是具有副作用的表达式(例如 (++i > 0)
之类的表达式),否则禁用断言将影响代码的功能。
7.33. 陷阱函数
可以通过从任何设备线程调用 __trap()
函数来启动陷阱操作。
void __trap();
内核的执行被中止,并在主机程序中引发中断。
7.34. 断点函数
可以通过从任何设备线程调用 __brkpt()
函数来暂停内核函数的执行。
void __brkpt();
7.35. 格式化输出
只有计算能力为 2.x 及更高版本的设备才支持格式化输出。
int printf(const char *format[, arg, ...]);
从内核向主机端输出流打印格式化输出。
内核中的 printf()
函数的行为类似于标准 C 库 printf()
函数,用户可以参考主机系统的手册页以获得 printf()
行为的完整描述。本质上,作为 format
传入的字符串将输出到主机上的流,并在遇到格式说明符时从参数列表中进行替换。下面列出了支持的格式说明符。
printf()
命令作为任何其他设备端函数执行:每个线程执行,并在调用线程的上下文中执行。从多线程内核来看,这意味着对 printf()
的直接调用将由每个线程执行,并使用该线程的数据,如指定的那样。然后,输出字符串的多个版本将出现在主机流中,每个遇到 printf()
的线程一次。
如果只需要单个输出字符串,则由程序员来限制输出到单个线程(有关说明性示例,请参阅 示例)。
与 C 标准 printf()
返回打印的字符数不同,CUDA 的 printf()
返回解析的参数数量。如果格式字符串后没有参数,则返回 0。如果格式字符串为 NULL,则返回 -1。如果发生内部错误,则返回 -2。
7.35.1. 格式说明符
与标准 printf()
一样,格式说明符采用以下形式:%[flags][width][.precision][size]type
支持以下字段(有关所有行为的完整描述,请参阅广泛提供的文档)
标志:
'#' ' ' '0' '+' '-'
宽度:
'*' '0-9'
精度:
'0-9'
大小:
'h' 'l' 'll'
类型:
"%cdiouxXpeEfgGaAs"
请注意,CUDA 的 printf()
将接受标志、宽度、精度、大小和类型的任何组合,无论它们总体上是否构成有效的格式说明符。“%hd
” 将被接受,并且 printf 将期望在参数列表中相应位置使用双精度变量。
7.35.2. 限制
printf()
输出的最终格式化发生在主机系统上。这意味着格式字符串必须被主机系统的编译器和 C 库理解。已尽一切努力确保 CUDA 的 printf 函数支持的格式说明符构成最常见的主机编译器的通用子集,但确切的行为将取决于主机操作系统。
如 格式说明符 中所述,printf()
将接受有效标志和类型的所有组合。这是因为它无法确定在最终输出格式化的主机系统上哪些组合有效,哪些组合无效。这样做的效果是,如果程序发出的格式字符串包含无效组合,则输出可能是未定义的。
printf()
命令最多可以接受 32 个参数,外加格式字符串。超出此范围的其他参数将被忽略,格式说明符将按原样输出。
由于 64 位 Windows 平台上的 long
类型的大小不同(在 64 位 Windows 平台上为 4 字节,在其他 64 位平台上为 8 字节),因此在非 Windows 64 位机器上编译但在 win64 机器上运行的内核将看到所有包含 “%ld
” 的格式字符串的输出损坏。建议编译平台与执行平台匹配以确保安全。
printf()
的输出缓冲区在内核启动之前设置为固定大小(请参阅 关联的主机端 API)。它是循环的,如果在内核执行期间产生的输出多于缓冲区可以容纳的输出,则较旧的输出将被覆盖。仅当执行以下操作之一时,它才会被刷新
通过
<<<>>>
或cuLaunchKernel()
进行内核启动(在启动开始时,以及如果 CUDA_LAUNCH_BLOCKING 环境变量设置为 1,则在启动结束时),通过
cudaDeviceSynchronize()
、cuCtxSynchronize()
、cudaStreamSynchronize()
、cuStreamSynchronize()
、cudaEventSynchronize()
或cuEventSynchronize()
进行同步,通过任何阻塞版本的
cudaMemcpy*()
或cuMemcpy*()
进行内存复制,通过
cuModuleLoad()
或cuModuleUnload()
进行模块加载/卸载,通过
cudaDeviceReset()
或cuCtxDestroy()
进行上下文销毁。在执行由
cudaStreamAddCallback
或cuStreamAddCallback
添加的流回调之前。
请注意,缓冲区在程序退出时不会自动刷新。用户必须显式调用 cudaDeviceReset()
或 cuCtxDestroy()
,如下面的示例所示。
在内部,printf()
使用共享数据结构,因此调用 printf()
可能会更改线程的执行顺序。特别是,调用 printf()
的线程可能比不调用 printf()
的线程采用更长的执行路径,并且该路径长度取决于 printf()
的参数。但是,请注意,CUDA 不保证线程执行顺序,除非在显式 __syncthreads()
屏障处,因此无法判断执行顺序是被 printf()
还是被硬件中的其他调度行为修改了。
7.35.3. 关联的主机端 API
以下 API 函数获取和设置用于将 printf()
参数和内部元数据传输到主机的缓冲区大小(默认为 1 兆字节)
cudaDeviceGetLimit(size_t* size,cudaLimitPrintfFifoSize)
cudaDeviceSetLimit(cudaLimitPrintfFifoSize, size_t size)
7.35.4. 示例
以下代码示例
#include <stdio.h>
__global__ void helloCUDA(float f)
{
printf("Hello thread %d, f=%f\n", threadIdx.x, f);
}
int main()
{
helloCUDA<<<1, 5>>>(1.2345f);
cudaDeviceSynchronize();
return 0;
}
将输出
Hello thread 2, f=1.2345
Hello thread 1, f=1.2345
Hello thread 4, f=1.2345
Hello thread 0, f=1.2345
Hello thread 3, f=1.2345
请注意,每个线程都遇到 printf()
命令,因此输出行数与网格中启动的线程数相同。正如预期的那样,全局值(即 float f
)在所有线程之间是通用的,而局部值(即 threadIdx.x
)对于每个线程是不同的。
以下代码示例
#include <stdio.h>
__global__ void helloCUDA(float f)
{
if (threadIdx.x == 0)
printf("Hello thread %d, f=%f\n", threadIdx.x, f) ;
}
int main()
{
helloCUDA<<<1, 5>>>(1.2345f);
cudaDeviceSynchronize();
return 0;
}
将输出
Hello thread 0, f=1.2345
不言而喻,if()
语句限制了哪些线程将调用 printf
,因此只会看到单行输出。
7.36. 动态全局内存分配和操作
只有计算能力为 2.x 及更高版本的设备才支持动态全局内存分配和操作。
__host__ __device__ void* malloc(size_t size);
__device__ void *__nv_aligned_device_malloc(size_t size, size_t align);
__host__ __device__ void free(void* ptr);
从全局内存中的固定大小堆动态分配和释放内存。
__host__ __device__ void* memcpy(void* dest, const void* src, size_t size);
将 size
字节从 src
指向的内存位置复制到 dest
指向的内存位置。
__host__ __device__ void* memset(void* ptr, int value, size_t size);
将 ptr
指向的内存块的 size
字节设置为 value
(解释为无符号字符)。
CUDA 内核中的 malloc()
函数从设备堆中分配至少 size
字节,并返回指向已分配内存的指针,如果内存不足以满足请求,则返回 NULL。返回的指针保证与 16 字节边界对齐。
CUDA 内核中的 __nv_aligned_device_malloc()
函数从设备堆中分配至少 size
字节,并返回指向已分配内存的指针,如果内存不足以满足请求的大小或对齐方式,则返回 NULL。已分配内存的地址将是 align
的倍数。align
必须是非零的 2 的幂。
CUDA 内核中的 free()
函数释放 ptr
指向的内存,该内存必须是先前调用 malloc()
或 __nv_aligned_device_malloc()
返回的。如果 ptr
为 NULL,则对 free()
的调用将被忽略。使用相同的 ptr
重复调用 free()
具有未定义的行为。
给定的 CUDA 线程通过 malloc()
或 __nv_aligned_device_malloc()
分配的内存,在其 CUDA 上下文的生命周期内保持分配状态,或者直到通过调用 free()
显式释放它为止。它可以被任何其他 CUDA 线程使用,即使来自后续的内核启动。任何 CUDA 线程都可以释放由另一个线程分配的内存,但应注意确保同一指针不会被释放多次。
7.36.1. 堆内存分配
设备内存堆具有固定大小,必须在使用 malloc()
、__nv_aligned_device_malloc()
或 free()
的任何程序加载到上下文中之前指定。如果任何程序在没有显式指定堆大小的情况下使用 malloc()
或 __nv_aligned_device_malloc()
,则会分配一个八兆字节的默认堆。
以下 API 函数用于获取和设置堆大小
cudaDeviceGetLimit(size_t* size, cudaLimitMallocHeapSize)
cudaDeviceSetLimit(cudaLimitMallocHeapSize, size_t size)
授予的堆大小将至少为 size
字节。cuCtxGetLimit()
和 cudaDeviceGetLimit()
返回当前请求的堆大小。
堆的实际内存分配发生在模块加载到上下文中时,可以通过 CUDA 驱动程序 API 显式加载(请参阅 模块),也可以通过 CUDA 运行时 API 隐式加载(请参阅 CUDA 运行时)。如果内存分配失败,模块加载将生成 CUDA_ERROR_SHARED_OBJECT_INIT_FAILED
错误。
一旦发生模块加载,堆大小就无法更改,并且不会根据需要动态调整大小。
为设备堆保留的内存是主机端 CUDA API 调用(例如 cudaMalloc()
)分配的内存之外的内存。
7.36.2. 与主机内存 API 的互操作性
通过设备 malloc()
或 __nv_aligned_device_malloc()
分配的内存不能使用运行时释放(即,通过从 设备内存 调用任何释放内存的函数)。
同样,通过运行时分配的内存(即,通过从 设备内存 调用任何内存分配函数)不能通过 free()
释放。
此外,在设备代码中通过调用 malloc()
或 __nv_aligned_device_malloc()
分配的内存不能在任何运行时或驱动程序 API 调用中使用(例如 cudaMemcpy、cudaMemset 等)。
7.36.3. 示例
7.36.3.1. 每个线程的分配
以下代码示例
#include <stdlib.h>
#include <stdio.h>
__global__ void mallocTest()
{
size_t size = 123;
char* ptr = (char*)malloc(size);
memset(ptr, 0, size);
printf("Thread %d got pointer: %p\n", threadIdx.x, ptr);
free(ptr);
}
int main()
{
// Set a heap size of 128 megabytes. Note that this must
// be done before any kernel is launched.
cudaDeviceSetLimit(cudaLimitMallocHeapSize, 128*1024*1024);
mallocTest<<<1, 5>>>();
cudaDeviceSynchronize();
return 0;
}
将输出
Thread 0 got pointer: 00057020
Thread 1 got pointer: 0005708c
Thread 2 got pointer: 000570f8
Thread 3 got pointer: 00057164
Thread 4 got pointer: 000571d0
请注意,每个线程如何遇到 malloc()
和 memset()
命令,从而接收并初始化自己的分配。(确切的指针值会有所不同:这些只是说明性的。)
7.36.3.2. 每个线程块的分配
#include <stdlib.h>
__global__ void mallocTest()
{
__shared__ int* data;
// The first thread in the block does the allocation and then
// shares the pointer with all other threads through shared memory,
// so that access can easily be coalesced.
// 64 bytes per thread are allocated.
if (threadIdx.x == 0) {
size_t size = blockDim.x * 64;
data = (int*)malloc(size);
}
__syncthreads();
// Check for failure
if (data == NULL)
return;
// Threads index into the memory, ensuring coalescence
int* ptr = data;
for (int i = 0; i < 64; ++i)
ptr[i * blockDim.x + threadIdx.x] = threadIdx.x;
// Ensure all threads complete before freeing
__syncthreads();
// Only one thread may free the memory!
if (threadIdx.x == 0)
free(data);
}
int main()
{
cudaDeviceSetLimit(cudaLimitMallocHeapSize, 128*1024*1024);
mallocTest<<<10, 128>>>();
cudaDeviceSynchronize();
return 0;
}
7.36.3.3. 内核启动之间持久存在的分配
#include <stdlib.h>
#include <stdio.h>
#define NUM_BLOCKS 20
__device__ int* dataptr[NUM_BLOCKS]; // Per-block pointer
__global__ void allocmem()
{
// Only the first thread in the block does the allocation
// since we want only one allocation per block.
if (threadIdx.x == 0)
dataptr[blockIdx.x] = (int*)malloc(blockDim.x * 4);
__syncthreads();
// Check for failure
if (dataptr[blockIdx.x] == NULL)
return;
// Zero the data with all threads in parallel
dataptr[blockIdx.x][threadIdx.x] = 0;
}
// Simple example: store thread ID into each element
__global__ void usemem()
{
int* ptr = dataptr[blockIdx.x];
if (ptr != NULL)
ptr[threadIdx.x] += threadIdx.x;
}
// Print the content of the buffer before freeing it
__global__ void freemem()
{
int* ptr = dataptr[blockIdx.x];
if (ptr != NULL)
printf("Block %d, Thread %d: final value = %d\n",
blockIdx.x, threadIdx.x, ptr[threadIdx.x]);
// Only free from one thread!
if (threadIdx.x == 0)
free(ptr);
}
int main()
{
cudaDeviceSetLimit(cudaLimitMallocHeapSize, 128*1024*1024);
// Allocate memory
allocmem<<< NUM_BLOCKS, 10 >>>();
// Use memory
usemem<<< NUM_BLOCKS, 10 >>>();
usemem<<< NUM_BLOCKS, 10 >>>();
usemem<<< NUM_BLOCKS, 10 >>>();
// Free memory
freemem<<< NUM_BLOCKS, 10 >>>();
cudaDeviceSynchronize();
return 0;
}
7.37. 执行配置
对 __global__
函数的任何调用都必须指定该调用的执行配置。执行配置定义了将在设备上执行该函数的网格和块的维度,以及关联的流(有关流的描述,请参阅 CUDA 运行时)。
执行配置通过在函数名称和带括号的参数列表之间插入 <<< Dg, Db, Ns, S >>>
形式的表达式来指定,其中
Dg
的类型为dim3
(请参阅 dim3),并指定网格的维度和大小,使得Dg.x * Dg.y * Dg.z
等于要启动的块数;Db
的类型为dim3
(请参阅 dim3),并指定每个块的维度和大小,使得Db.x * Db.y * Db.z
等于每个块的线程数;Ns
的类型为size_t
,并指定除了静态分配的内存之外,在此调用中每个块动态分配的共享内存的字节数;此动态分配的内存由声明为外部数组的任何变量使用,如 __shared__ 中所述;Ns
是一个可选参数,默认为 0;S
的类型为cudaStream_t
,并指定关联的流;S
是一个可选参数,默认为 0。
例如,声明为如下的函数
__global__ void Func(float* parameter);
必须像这样调用
Func<<< Dg, Db, Ns >>>(parameter);
执行配置的参数在实际函数参数之前进行评估。
如果 Dg
或 Db
大于 计算能力 中指定的设备允许的最大大小,或者如果 Ns
大于设备上可用的最大共享内存量(减去静态分配所需的共享内存量),则函数调用将失败。
计算能力 9.0 及以上版本允许用户指定编译时线程块集群维度,以便内核可以在 CUDA 中使用集群层次结构。编译时集群维度可以使用 __cluster_dims__([x, [y, [z]]])
指定。下面的示例显示了 X 维度为 2,Y 和 Z 维度为 1 的编译时集群大小。
__global__ void __cluster_dims__(2, 1, 1) Func(float* parameter);
__cluster_dims__()
的默认形式指定内核将作为集群网格启动。通过不指定集群维度,用户可以自由地在启动时指定维度。在启动时不指定维度将导致启动时错误。
线程块集群维度也可以在运行时指定,并且可以使用 cudaLaunchKernelEx
API 启动具有集群的内核。该 API 接受类型为 cudaLaunchConfig_t
的配置参数、内核函数指针和内核参数。运行时内核配置在下面的示例中显示。
__global__ void Func(float* parameter);
// 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 = Dg;
config.blockDim = Db;
config.dynamicSmemBytes = Ns;
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;
float* parameter;
cudaLaunchKernelEx(&config, Func, parameter);
}
7.38. 启动边界
正如在 多处理器级别 中详细讨论的那样,内核使用的寄存器越少,可能驻留在多处理器上的线程和线程块就越多,这可以提高性能。
因此,编译器使用启发式方法来最小化寄存器使用量,同时将寄存器溢出(请参阅 设备内存访问)和指令计数保持在最低限度。应用程序可以选择通过提供额外的启动边界信息来帮助这些启发式方法,这些启动边界信息使用 __launch_bounds__()
限定符在 __global__
函数的定义中指定
__global__ void
__launch_bounds__(maxThreadsPerBlock, minBlocksPerMultiprocessor, maxBlocksPerCluster)
MyKernel(...)
{
...
}
maxThreadsPerBlock
指定应用程序将永远启动MyKernel()
的每个块的最大线程数;它编译为.maxntid
PTX 指令。minBlocksPerMultiprocessor
是可选的,并指定每个多处理器所需的最小常驻块数;它编译为.minnctapersm
PTX 指令。maxBlocksPerCluster
是可选的,并指定应用程序将永远启动MyKernel()
的每个集群所需的最大线程块数;它编译为.maxclusterrank
PTX 指令。
如果指定了启动边界,编译器首先从中导出内核应使用的寄存器数量的上限 L,以确保 minBlocksPerMultiprocessor
个块(如果未指定 minBlocksPerMultiprocessor
,则为单个块)的 maxThreadsPerBlock
个线程可以驻留在多处理器上(有关内核使用的寄存器数量与每个块分配的寄存器数量之间的关系,请参阅 硬件多线程)。然后,编译器按以下方式优化寄存器使用量
如果初始寄存器使用量高于 L,则编译器会进一步减少它,直到它变得小于或等于 L,通常以增加本地内存使用量和/或更高的指令数为代价;
如果初始寄存器使用量低于 L
如果指定了
maxThreadsPerBlock
并且未指定minBlocksPerMultiprocessor
,则编译器使用maxThreadsPerBlock
来确定n
和n+1
个常驻块之间转换的寄存器使用阈值(即,当使用更少的寄存器为额外的常驻块腾出空间时,如 多处理器级别 的示例中所示),然后应用与未指定启动边界时类似的启发式方法;如果同时指定了
minBlocksPerMultiprocessor
和maxThreadsPerBlock
,则编译器可能会将寄存器使用量增加到 L,以减少指令数并更好地隐藏单线程指令延迟。
如果内核执行时每个块的线程数超过其启动边界 maxThreadsPerBlock
,则内核将无法启动。
如果内核执行时每个集群的线程块数超过其启动边界 maxBlocksPerCluster
,则内核将无法启动。
CUDA 内核所需的每个线程资源可能会以不希望的方式限制最大块大小。为了保持与未来硬件和工具包的向前兼容性,并确保至少一个线程块可以在 SM 上运行,开发人员应包含单参数 __launch_bounds__(maxThreadsPerBlock)
,该参数指定内核将启动的最大块大小。如果不这样做,可能会导致“请求启动的资源过多”错误。提供双参数版本的 __launch_bounds__(maxThreadsPerBlock,minBlocksPerMultiprocessor)
在某些情况下可以提高性能。minBlocksPerMultiprocessor
的正确值应使用详细的每个内核分析来确定。
给定内核的最佳启动边界通常在主要架构修订之间有所不同。下面的示例代码显示了通常如何在设备代码中使用 应用程序兼容性 中引入的 __CUDA_ARCH__
宏来处理这种情况。
#define THREADS_PER_BLOCK 256
#if __CUDA_ARCH__ >= 200
#define MY_KERNEL_MAX_THREADS (2 * THREADS_PER_BLOCK)
#define MY_KERNEL_MIN_BLOCKS 3
#else
#define MY_KERNEL_MAX_THREADS THREADS_PER_BLOCK
#define MY_KERNEL_MIN_BLOCKS 2
#endif
// Device code
__global__ void
__launch_bounds__(MY_KERNEL_MAX_THREADS, MY_KERNEL_MIN_BLOCKS)
MyKernel(...)
{
...
}
在 MyKernel
以每个块的最大线程数(指定为 __launch_bounds__()
的第一个参数)调用的一般情况下,很容易在执行配置中使用 MY_KERNEL_MAX_THREADS
作为每个块的线程数
// Host code
MyKernel<<<blocksPerGrid, MY_KERNEL_MAX_THREADS>>>(...);
但是,这将不起作用,因为 __CUDA_ARCH__
在主机代码中未定义,如 应用程序兼容性 中所述,因此即使 __CUDA_ARCH__
大于或等于 200,MyKernel
也将以每个块 256 个线程启动。相反,每个块的线程数应确定
可以在编译时使用不依赖于
__CUDA_ARCH__
的宏,例如// Host code MyKernel<<<blocksPerGrid, THREADS_PER_BLOCK>>>(...);
或者在运行时基于计算能力
// Host code cudaGetDeviceProperties(&deviceProp, device); int threadsPerBlock = (deviceProp.major >= 2 ? 2 * THREADS_PER_BLOCK : THREADS_PER_BLOCK); MyKernel<<<blocksPerGrid, threadsPerBlock>>>(...);
寄存器使用情况由 --ptxas-options=-v
编译器选项报告。常驻块的数量可以从 CUDA 分析器报告的占用率中推导出来(有关占用率的定义,请参阅 设备内存访问)。
7.39. 每个线程的最大寄存器数
为了提供低级性能调整机制,CUDA C++ 提供了 __maxnreg__()
函数限定符,以将性能调整信息传递给后端优化编译器。__maxnreg__()
限定符指定要分配给线程块中单个线程的最大寄存器数。在 __global__
函数的定义中
__global__ void
__maxnreg__(maxNumberRegistersPerThread)
MyKernel(...)
{
...
}
maxNumberRegistersPerThread
指定要分配给内核MyKernel()
的线程块中单个线程的最大寄存器数;它编译为.maxnreg
PTX 指令。
__launch_bounds__()
和 __maxnreg__()
限定符不能应用于同一内核。
也可以使用 maxrregcount
编译器选项控制文件中所有 __global__
函数的寄存器使用情况。对于带有 __maxnreg__
限定符的函数,maxrregcount
的值将被忽略。
7.40. #pragma unroll
默认情况下,编译器会展开行程计数已知的小循环。但是,#pragma unroll
指令可用于控制任何给定循环的展开。它必须紧靠循环之前放置,并且仅适用于该循环。它可以选择后跟一个整数常量表达式 (ICE)13。如果 ICE 缺失,则如果其行程计数为常量,则循环将完全展开。如果 ICE 的计算结果为 1,则编译器将不会展开循环。如果 pragma 的 ICE 的计算结果为非正整数或大于 int
数据类型可表示的最大值的整数,则该 pragma 将被忽略。
示例
struct S1_t { static const int value = 4; };
template <int X, typename T2>
__device__ void foo(int *p1, int *p2) {
// no argument specified, loop will be completely unrolled
#pragma unroll
for (int i = 0; i < 12; ++i)
p1[i] += p2[i]*2;
// unroll value = 8
#pragma unroll (X+1)
for (int i = 0; i < 12; ++i)
p1[i] += p2[i]*4;
// unroll value = 1, loop unrolling disabled
#pragma unroll 1
for (int i = 0; i < 12; ++i)
p1[i] += p2[i]*8;
// unroll value = 4
#pragma unroll (T2::value)
for (int i = 0; i < 12; ++i)
p1[i] += p2[i]*16;
}
__global__ void bar(int *p1, int *p2) {
foo<7, S1_t>(p1, p2);
}
7.41. SIMD 视频指令
PTX ISA 版本 3.0 包括 SIMD(单指令,多数据)视频指令,这些指令对成对的 16 位值和四字节的 8 位值进行操作。这些指令在计算能力为 3.0 的设备上可用。
SIMD 视频指令包括
vadd2, vadd4
vsub2, vsub4
vavrg2, vavrg4
vabsdiff2, vabsdiff4
vmin2, vmin4
vmax2, vmax4
vset2, vset4
PTX 指令(例如 SIMD 视频指令)可以通过汇编器 asm()
语句包含在 CUDA 程序中。
asm()
语句的基本语法是
asm("template-string" : "constraint"(output) : "constraint"(input)"));
使用 vabsdiff4
PTX 指令的示例是
asm("vabsdiff4.u32.u32.u32.add" " %0, %1, %2, %3;": "=r" (result):"r" (A), "r" (B), "r" (C));
这使用 vabsdiff4
指令来计算整数四字节 SIMD 绝对差值和。以 SIMD 方式计算无符号整数 A 和 B 的每个字节的绝对差值。可选的累加操作 (.add
) 被指定为对这些差值求和。
有关在代码中使用汇编语句的详细信息,请参阅文档“在 CUDA 中使用内联 PTX 汇编”。有关您正在使用的 PTX 版本的 PTX 指令的详细信息,请参阅 PTX ISA 文档(例如“并行线程执行 ISA 版本 3.0”)。
7.42. 诊断 Pragma
以下 pragma 可用于控制在发出给定诊断消息时使用的错误严重性。
#pragma nv_diag_suppress
#pragma nv_diag_warning
#pragma nv_diag_error
#pragma nv_diag_default
#pragma nv_diag_once
这些 pragma 的使用形式如下
#pragma nv_diag_xxx error_number, error_number ...
受影响的诊断使用警告消息中显示的错误编号指定。任何诊断都可以被覆盖为错误,但只有警告可以抑制其严重性或在升级为错误后恢复为警告。nv_diag_default
pragma 用于将诊断的严重性返回到在发出任何 pragma 之前生效的严重性(即,消息的正常严重性,由任何命令行选项修改)。以下示例抑制了 foo
的声明中的 "declared but never referenced"
警告
#pragma nv_diag_suppress 177
void foo()
{
int i=0;
}
#pragma nv_diag_default 177
void bar()
{
int i=0;
}
以下 pragma 可用于保存和恢复当前的诊断 pragma 状态
#pragma nv_diagnostic push
#pragma nv_diagnostic pop
示例
#pragma nv_diagnostic push
#pragma nv_diag_suppress 177
void foo()
{
int i=0;
}
#pragma nv_diagnostic pop
void bar()
{
int i=0;
}
请注意,pragma 仅影响 nvcc CUDA 前端编译器;它们对主机编译器没有影响。
移除通知:CUDA 12.0 中移除了对不带 nv_
前缀的诊断 pragma 的支持,如果 pragma 在设备代码内部,则会发出警告 unrecognized #pragma in device code
,否则它们将传递给主机编译器。如果它们用于 CUDA 代码,请改用带有 nv_
前缀的 pragma。
7.43. 自定义 ABI Pragma
#pragma nv_abi
指令使在单独编译模式下编译的应用程序能够实现与全程序编译类似的性能。
使用此 pragma 的语法如下,其中 ICE 指的是任何整数常量表达式 (ICE):13。
#pragma nv_abi preserve_n_data(ICE) preserve_n_control(ICE)
请注意,#pragma nv_abi
后面的参数是可选的,可以按任何顺序提供;但是,至少需要一个参数。
preserve_n
参数设置函数调用期间保留的寄存器数量的限制
preserve_n_data(ICE)
限制数据寄存器的数量,并且preserve_n_control(ICE)
限制控制寄存器的数量。
#pragma nv_abi
可以紧靠设备函数声明或定义之前放置。或者,它可以直接放置在设备函数内部的 C++ 表达式语句中的间接函数调用之前。请注意,支持对自由函数的间接函数调用,但不支持通过函数参数引用或类成员函数的间接调用。
当 pragma 应用于设备函数声明或定义时,它会修改对该函数的任何调用的自定义 ABI 属性。当放置在间接函数调用站点时,pragma 会影响该间接函数调用的 ABI 属性。关键点是,与可以直接放置 pragma 在函数声明或定义之前的直接函数调用不同,#pragma nv_abi
仅在 pragma 放置在调用站点之前时才影响间接函数调用。
如下面的示例所示,我们有两个设备函数 foo()
和 bar()
。在此示例中,pragma 放置在函数指针 fptr 的调用站点之前,以修改间接函数调用的 ABI 属性。请注意,将 pragma 放置在直接调用之前不会影响调用的 ABI 属性。要更改直接函数调用的 ABI 属性,必须将 pragma 放置在函数声明或定义之前。
__device__ int foo()
{
int value{0};
...
return value;
}
__device__ int bar()
{
int value{0};
...
return value;
}
__device__ void baz()
{
int result{0};
int (*fptr)() = foo; // function pointer
#pragma nv_abi preserve_n_data(16) preserve_n_control(8)
result = fptr(); // The pragma affects the indirect call to foo() via fptr
#pragma nv_abi preserve_n_data(16) preserve_n_control(8)
result = (*fptr)(); // Alternate syntax for the indirect call to foo()
#pragma nv_abi preserve_n_data(16) preserve_n_control(8)
result += bar(); // The pragma does NOT affect the direct call to bar()
}
如下面的示例所示,要修改直接函数调用,必须将 pragma 应用于函数声明或定义。
#pragma nv_abi preserve_n_data(16)
__device__ void foo();
请注意,如果函数声明及其对应定义的 pragma 参数不匹配,则程序格式不正确。
8. 协作组
8.1. 简介
协作组是 CUDA 编程模型的扩展,在 CUDA 9 中引入,用于组织通信线程组。协作组允许开发人员表达线程通信的粒度,帮助他们表达更丰富、更高效的并行分解。
从历史上看,CUDA 编程模型提供了一个简单的构造来同步协作线程:线程块的所有线程之间的屏障,通过 __syncthreads()
内在函数实现。但是,程序员希望定义和同步其他粒度的线程组,以便以“集体”组范围函数接口的形式实现更高的性能、设计灵活性和软件重用。为了表达更广泛的并行交互模式,许多面向性能的程序员已求助于编写自己的临时和不安全的基元,用于同步单个 Warp 内的线程或跨在单个 GPU 上运行的线程块集。虽然实现的性能改进通常很有价值,但这导致了不断增长的脆弱代码集合,这些代码在时间和跨 GPU 世代中编写、调整和维护的成本很高。协作组通过提供安全且面向未来的机制来解决此问题,从而实现高性能代码。
8.2. 协作组的新功能
8.2.1. CUDA 12.2
为 grid_group 和 thread_block 添加了
barrier_arrive
和barrier_wait
成员函数。API 的描述在此处 此处 提供。
8.2.2. CUDA 12.1
添加了 invoke_one 和 invoke_one_broadcast API。
8.2.3. CUDA 12.0
以下实验性 API 现在已移至主命名空间
在 CUDA 11.7 中添加了异步 reduce 和 scan 更新
在 CUDA 11.1 中添加了大于 32 的
thread_block_tile
不再需要使用
block_tile_memory
对象来提供内存,以便在计算能力 8.0 或更高的版本上创建这些大型 tile。
8.3. 编程模型概念
协作组编程模型描述了 CUDA 线程块内部和跨 CUDA 线程块的同步模式。它既提供了应用程序定义自己的线程组的方法,又提供了同步它们的接口。它还提供了新的启动 API,这些 API 强制执行某些限制,因此可以保证同步将起作用。这些基元在 CUDA 中实现了新的协作并行模式,包括生产者-消费者并行性、机会并行性和跨整个网格的全局同步。
协作组编程模型包含以下元素
用于表示协作线程组的数据类型;
用于获取由 CUDA 启动 API 定义的隐式组(例如,线程块)的操作;
用于将现有组划分为新组的集合;
用于数据移动和操作的集合算法(例如 memcpy_async、reduce、scan);
用于同步组内所有线程的操作;
用于检查组属性的操作;
公开低级、特定于组且通常是硬件加速的操作的集合。
协作组的主要概念是命名属于它的线程集的对象。组作为第一类程序对象的这种表达方式改进了软件组合,因为集合函数可以接收表示参与线程组的显式对象。此对象还使程序员的意图明确,从而消除了导致脆弱代码、对编译器优化的不良限制以及与新 GPU 世代更好的兼容性的不健全的架构假设。
要编写高效的代码,最好使用专用组(通用化会损失很多编译时优化),并通过引用将这些组对象传递给打算以某种协作方式使用这些线程的函数。
协作组需要 CUDA 9.0 或更高版本。要使用协作组,请包含头文件
// Primary header is compatible with pre-C++11, collective algorithm headers require C++11
#include <cooperative_groups.h>
// Optionally include for memcpy_async() collective
#include <cooperative_groups/memcpy_async.h>
// Optionally include for reduce() collective
#include <cooperative_groups/reduce.h>
// Optionally include for inclusive_scan() and exclusive_scan() collectives
#include <cooperative_groups/scan.h>
并使用协作组命名空间
using namespace cooperative_groups;
// Alternatively use an alias to avoid polluting the namespace with collective algorithms
namespace cg = cooperative_groups;
可以使用 nvcc 以正常方式编译代码,但是,如果您希望使用 memcpy_async、reduce 或 scan 功能,并且您的主机编译器的默认方言不是 C++11 或更高版本,则必须将 --std=c++11
添加到命令行。
8.3.1. 组合示例
为了说明组的概念,此示例尝试执行块范围的总和归约。以前,在编写此代码时,实现存在隐藏的约束
__device__ int sum(int *x, int n) {
// ...
__syncthreads();
return total;
}
__global__ void parallel_kernel(float *x) {
// ...
// Entire thread block must call sum
sum(x, n);
}
线程块中的所有线程都必须到达 __syncthreads()
屏障,但是,此约束对于可能想要使用 sum(…)
的开发人员是隐藏的。使用协作组,编写此代码的更好方法是
__device__ int sum(const thread_block& g, int *x, int n) {
// ...
g.sync()
return total;
}
__global__ void parallel_kernel(...) {
// ...
// Entire thread block must call sum
thread_block tb = this_thread_block();
sum(tb, x, n);
// ...
}
8.4. 组类型
8.4.1. 隐式组
隐式组表示内核的启动配置。无论您的内核如何编写,它始终具有一组线程、块和块维度,以及单个网格和网格维度。此外,如果使用多设备协作启动 API,则它可以具有多个网格(每个设备一个网格)。这些组为分解为更细粒度的组提供了起点,这些组通常是硬件加速的,并且更专门用于开发人员正在解决的问题。
虽然你可以在代码中的任何位置创建隐式组,但这样做是危险的。为隐式组创建句柄是一个集体操作——组中的所有线程都必须参与。如果组是在并非所有线程都能到达的条件分支中创建的,则可能导致死锁或数据损坏。因此,建议您预先为隐式组创建句柄(尽可能早,在任何分支发生之前),并在整个内核中使用该句柄。出于同样的原因,组句柄必须在声明时初始化(没有默认构造函数),并且不鼓励复制构造它们。
8.4.1.1. 线程块组
任何 CUDA 程序员都已经熟悉某种线程组:线程块。 Cooperative Groups 扩展引入了一种新的数据类型 thread_block
,以在内核中显式表示这个概念。
class thread_block;
通过以下方式构造
thread_block g = this_thread_block();
公共成员函数
static void sync()
: 同步组中命名的线程,等效于 g.barrier_wait(g.barrier_arrive())
thread_block::arrival_token barrier_arrive()
: 到达 thread_block 栅栏,返回一个令牌,该令牌需要传递到 barrier_wait()
中。更多详情请参考 此处
void barrier_wait(thread_block::arrival_token&& t)
: 等待 thread_block
栅栏,接受从 barrier_arrive()
返回的到达令牌作为右值引用。更多详情请参考 此处
static unsigned int thread_rank()
: 调用线程在 [0, num_threads) 中的秩
static dim3 group_index()
: 启动网格中块的 3 维索引
static dim3 thread_index()
: 启动块中线程的 3 维索引
static dim3 dim_threads()
: 以线程为单位的启动块的维度
static unsigned int num_threads()
: 组中线程的总数
旧版成员函数(别名)
static unsigned int size()
: 组中线程的总数(num_threads()
的别名)
static dim3 group_dim()
: 启动块的维度(dim_threads()
的别名)
示例
/// Loading an integer from global into shared memory
__global__ void kernel(int *globalInput) {
__shared__ int x;
thread_block g = this_thread_block();
// Choose a leader in the thread block
if (g.thread_rank() == 0) {
// load from global into shared for all threads to work with
x = (*globalInput);
}
// After loading data into shared memory, you want to synchronize
// if all threads in your thread block need to see it
g.sync(); // equivalent to __syncthreads();
}
注意: 组中的所有线程都必须参与集体操作,否则行为未定义。
相关: thread_block
数据类型派生自更通用的 thread_group
数据类型,后者可用于表示更广泛的组。
8.4.1.2. 集群组
此组对象表示在单个集群中启动的所有线程。请参考 线程块集群。这些 API 在计算能力为 9.0+ 的所有硬件上均可用。在这种情况下,当启动非集群网格时,API 假定为 1x1x1 集群。
class cluster_group;
通过以下方式构造
cluster_group g = this_cluster();
公共成员函数
static void sync()
: 同步组中命名的线程,等效于 g.barrier_wait(g.barrier_arrive())
static cluster_group::arrival_token barrier_arrive()
: 到达集群栅栏,返回一个令牌,该令牌需要传递到 barrier_wait()
中。更多详情请参考 此处
static void barrier_wait(cluster_group::arrival_token&& t)
: 等待集群栅栏,接受从 barrier_arrive()
返回的到达令牌作为右值引用。更多详情请参考 此处
static unsigned int thread_rank()
: 调用线程在 [0, num_threads) 中的秩
static unsigned int block_rank()
: 调用块在 [0, num_blocks) 中的秩
static unsigned int num_threads()
: 组中线程的总数
static unsigned int num_blocks()
: 组中块的总数
static dim3 dim_threads()
: 以线程为单位的启动集群的维度
static dim3 dim_blocks()
: 以块为单位的启动集群的维度
static dim3 block_index()
: 启动集群中调用块的 3 维索引
static unsigned int query_shared_rank(const void *addr)
: 获取共享内存地址所属的块秩
static T* map_shared_rank(T *addr, int rank)
: 获取集群中另一个块的共享内存变量的地址
旧版成员函数(别名)
static unsigned int size()
: 组中线程的总数(num_threads()
的别名)
8.4.1.3. 网格组
此组对象表示在单个网格中启动的所有线程。除了 sync()
之外的 API 始终可用,但为了能够跨网格同步,您需要使用协同启动 API。
class grid_group;
通过以下方式构造
grid_group g = this_grid();
公共成员函数
bool is_valid() const
: 返回 grid_group 是否可以同步
void sync() const
: 同步组中命名的线程,等效于 g.barrier_wait(g.barrier_arrive())
grid_group::arrival_token barrier_arrive()
: 到达网格栅栏,返回一个令牌,该令牌需要传递到 barrier_wait()
中。更多详情请参考 此处
void barrier_wait(grid_group::arrival_token&& t)
: 等待网格栅栏,接受从 barrier_arrive()
返回的到达令牌作为右值引用。更多详情请参考 此处
static unsigned long long thread_rank()
: 调用线程在 [0, num_threads) 中的秩
static unsigned long long block_rank()
: 调用块在 [0, num_blocks) 中的秩
static unsigned long long cluster_rank()
: 调用集群在 [0, num_clusters) 中的秩
static unsigned long long num_threads()
: 组中线程的总数
static unsigned long long num_blocks()
: 组中块的总数
static unsigned long long num_clusters()
: 组中集群的总数
static dim3 dim_blocks()
: 以块为单位的启动网格的维度
static dim3 dim_clusters()
: 以集群为单位的启动网格的维度
static dim3 block_index()
: 启动网格中块的 3 维索引
static dim3 cluster_index()
: 启动网格中集群的 3 维索引
旧版成员函数(别名)
static unsigned long long size()
: 组中线程的总数(num_threads()
的别名)
static dim3 group_dim()
: 启动网格的维度(dim_blocks()
的别名)
8.4.1.4. 多网格组
此组对象表示跨多设备协同启动的所有设备启动的所有线程。与 grid.group 不同,所有 API 都要求您已使用适当的启动 API。
class multi_grid_group;
通过以下方式构造
// Kernel must be launched with the cooperative multi-device API
multi_grid_group g = this_multi_grid();
公共成员函数
bool is_valid() const
: 返回 multi_grid_group
是否可以使用
void sync() const
: 同步组中命名的线程
unsigned long long num_threads() const
: 组中线程的总数
unsigned long long thread_rank() const
: 调用线程在 [0, num_threads) 中的秩
unsigned int grid_rank() const
: 网格在 [0,num_grids] 中的秩
unsigned int num_grids() const
: 启动的网格总数
旧版成员函数(别名)
unsigned long long size() const
: 组中线程的总数(num_threads()
的别名)
弃用通知:multi_grid_group
已在 CUDA 11.3 中针对所有设备弃用。
8.4.2. 显式组
8.4.2.1. 线程块 Tile
Tile 组的模板化版本,其中模板参数用于指定 Tile 的大小 - 由于在编译时已知这一点,因此有可能实现更优的执行。
template <unsigned int Size, typename ParentT = void>
class thread_block_tile;
通过以下方式构造
template <unsigned int Size, typename ParentT>
_CG_QUALIFIER thread_block_tile<Size, ParentT> tiled_partition(const ParentT& g)
Size
必须是 2 的幂,并且小于或等于 1024。注释部分描述了在计算能力为 7.5 或更低的硬件上创建大于 32 的 Tile 所需的额外步骤。
ParentT
是从中分区此组的父类型。它是自动推断的,但是 void 值会将此信息存储在组句柄中,而不是类型中。
公共成员函数
void sync() const
: 同步组中命名的线程
unsigned long long num_threads() const
: 组中线程的总数
unsigned long long thread_rank() const
: 调用线程在 [0, num_threads) 中的秩
unsigned long long meta_group_size() const
: 返回分区父组时创建的组数。
unsigned long long meta_group_rank() const
: 从父组分区的 Tile 集中组的线性秩(受 meta_group_size 限制)
T shfl(T var, unsigned int src_rank) const
: 请参考 Warp Shuffle 函数,注意:对于大于 32 的大小,组中的所有线程都必须指定相同的 src_rank,否则行为未定义。
T shfl_up(T var, int delta) const
: 请参考 Warp Shuffle 函数,仅适用于大小小于或等于 32 的情况。
T shfl_down(T var, int delta) const
: 请参考 Warp Shuffle 函数,仅适用于大小小于或等于 32 的情况。
T shfl_xor(T var, int delta) const
: 请参考 Warp Shuffle 函数,仅适用于大小小于或等于 32 的情况。
int any(int predicate) const
: 请参考 Warp Vote 函数
int all(int predicate) const
: 请参考 Warp Vote 函数
unsigned int ballot(int predicate) const
: 请参考 Warp Vote 函数,仅适用于大小小于或等于 32 的情况。
unsigned int match_any(T val) const
: 请参考 Warp Match 函数,仅适用于大小小于或等于 32 的情况。
unsigned int match_all(T val, int &pred) const
: 请参考 Warp Match 函数,仅适用于大小小于或等于 32 的情况。
旧版成员函数(别名)
unsigned long long size() const
: 组中线程的总数(num_threads()
的别名)
注释
thread_block_tile
模板化数据结构在此处使用,组的大小作为模板参数而不是参数传递给tiled_partition
调用。shfl, shfl_up, shfl_down, and shfl_xor
函数在使用 C++11 或更高版本编译时接受任何类型的对象。这意味着只要它们满足以下约束,就可以 Shuffle 非整型类型符合可平凡复制的条件,即
is_trivially_copyable<T>::value == true
对于小于或等于 32 的 Tile 大小,
sizeof(T) <= 32
,对于较大的 Tile,sizeof(T) <= 8
在计算能力为 7.5 或更低的硬件上,大于 32 的 Tile 需要少量为其保留的内存。这可以使用
cooperative_groups::block_tile_memory
结构模板来完成,该模板必须驻留在共享或全局内存中。template <unsigned int MaxBlockSize = 1024> struct block_tile_memory;
MaxBlockSize
指定当前线程块中的最大线程数。此参数可用于在仅使用较小线程计数启动的内核中最小化block_tile_memory
的共享内存使用量。然后需要将此
block_tile_memory
传递到cooperative_groups::this_thread_block
中,从而允许将生成的thread_block
分区为大于 32 的 Tile。接受block_tile_memory
参数的this_thread_block
重载是一个集体操作,必须由thread_block
中的所有线程调用。block_tile_memory
可以在计算能力为 8.0 或更高的硬件上使用,以便能够编写一个针对多个不同计算能力来源的代码。在不需要它的情况下,当在共享内存中实例化时,它应该不消耗内存。
示例
/// The following code will create two sets of tiled groups, of size 32 and 4 respectively:
/// The latter has the provenance encoded in the type, while the first stores it in the handle
thread_block block = this_thread_block();
thread_block_tile<32> tile32 = tiled_partition<32>(block);
thread_block_tile<4, thread_block> tile4 = tiled_partition<4>(block);
/// The following code will create tiles of size 128 on all Compute Capabilities.
/// block_tile_memory can be omitted on Compute Capability 8.0 or higher.
__global__ void kernel(...) {
// reserve shared memory for thread_block_tile usage,
// specify that block size will be at most 256 threads.
__shared__ block_tile_memory<256> shared;
thread_block thb = this_thread_block(shared);
// Create tiles with 128 threads.
auto tile = tiled_partition<128>(thb);
// ...
}
8.4.2.1.1. Warp 同步代码模式
开发人员可能已经有了 Warp 同步代码,他们之前对 Warp 大小进行了隐式假设,并围绕该数字进行编码。现在需要显式指定这一点。
__global__ void cooperative_kernel(...) {
// obtain default "current thread block" group
thread_block my_block = this_thread_block();
// subdivide into 32-thread, tiled subgroups
// Tiled subgroups evenly partition a parent group into
// adjacent sets of threads - in this case each one warp in size
auto my_tile = tiled_partition<32>(my_block);
// This operation will be performed by only the
// first 32-thread tile of each block
if (my_tile.meta_group_rank() == 0) {
// ...
my_tile.sync();
}
}
8.4.2.1.2. 单线程组
表示当前线程的组可以从 this_thread
函数获得
thread_block_tile<1> this_thread();
以下 memcpy_async
API 使用 thread_group
,将 int 元素从源复制到目标
#include <cooperative_groups.h>
#include <cooperative_groups/memcpy_async.h>
cooperative_groups::memcpy_async(cooperative_groups::this_thread(), dest, src, sizeof(int));
使用 this_thread
执行异步复制的更详细示例可以在 使用 cuda::pipeline 的单阶段异步数据复制 和 使用 cuda::pipeline 的多阶段异步数据复制 部分找到。
8.4.2.2. 合并组
在 CUDA 的 SIMT 架构中,在硬件级别,多处理器以 32 个线程的组(称为 Warp)执行线程。如果应用程序代码中存在数据相关的条件分支,导致 Warp 内的线程发散,则 Warp 会串行执行每个分支,禁用不在该路径上的线程。在该路径上保持活动的线程称为合并线程。 Cooperative Groups 具有发现和创建包含所有合并线程的组的功能。
通过 coalesced_threads()
构造组句柄是机会性的。它返回该时间点的活动线程集,并且不保证返回哪些线程(只要它们处于活动状态)或它们将在整个执行过程中保持合并状态(它们将被带回一起执行集体操作,但之后可能会再次发散)。
class coalesced_group;
通过以下方式构造
coalesced_group active = coalesced_threads();
公共成员函数
void sync() const
: 同步组中命名的线程
unsigned long long num_threads() const
: 组中线程的总数
unsigned long long thread_rank() const
: 调用线程在 [0, num_threads) 中的秩
unsigned long long meta_group_size() const
: 返回分区父组时创建的组数。如果此组是通过查询活动线程集创建的,例如 coalesced_threads()
,则 meta_group_size()
的值将为 1。
unsigned long long meta_group_rank() const
: 从父组分区的 Tile 集中组的线性秩(受 meta_group_size 限制)。如果此组是通过查询活动线程集创建的,例如 coalesced_threads()
,则 meta_group_rank()
的值将始终为 0。
T shfl(T var, unsigned int src_rank) const
: 请参考 Warp Shuffle 函数
T shfl_up(T var, int delta) const
: 请参考 Warp Shuffle 函数
T shfl_down(T var, int delta) const
: 请参考 Warp Shuffle 函数
int any(int predicate) const
: 请参考 Warp Vote 函数
int all(int predicate) const
: 请参考 Warp Vote 函数
unsigned int ballot(int predicate) const
: 请参考 Warp Vote 函数
unsigned int match_any(T val) const
: 请参考 Warp Match 函数
unsigned int match_all(T val, int &pred) const
: 请参考 Warp Match 函数
旧版成员函数(别名)
unsigned long long size() const
: 组中线程的总数(num_threads()
的别名)
注释
shfl, shfl_up, and shfl_down
函数在使用 C++11 或更高版本编译时接受任何类型的对象。这意味着只要它们满足以下约束,就可以 Shuffle 非整型类型
符合可平凡复制的条件,即
is_trivially_copyable<T>::value == true
sizeof(T) <= 32
示例
/// Consider a situation whereby there is a branch in the
/// code in which only the 2nd, 4th and 8th threads in each warp are
/// active. The coalesced_threads() call, placed in that branch, will create (for each
/// warp) a group, active, that has three threads (with
/// ranks 0-2 inclusive).
__global__ void kernel(int *globalInput) {
// Lets say globalInput says that threads 2, 4, 8 should handle the data
if (threadIdx.x == *globalInput) {
coalesced_group active = coalesced_threads();
// active contains 0-2 inclusive
active.sync();
}
}
8.4.2.2.1. 发现模式
通常,开发人员需要使用当前活动的线程集。不假设存在哪些线程,而是开发人员使用碰巧在那里的线程。这在以下“跨 Warp 聚合原子增量”示例中可以看出(使用正确的 CUDA 9.0 指令集编写)
{
unsigned int writemask = __activemask();
unsigned int total = __popc(writemask);
unsigned int prefix = __popc(writemask & __lanemask_lt());
// Find the lowest-numbered active lane
int elected_lane = __ffs(writemask) - 1;
int base_offset = 0;
if (prefix == 0) {
base_offset = atomicAdd(p, total);
}
base_offset = __shfl_sync(writemask, base_offset, elected_lane);
int thread_offset = prefix + base_offset;
return thread_offset;
}
这可以使用 Cooperative Groups 重写如下
{
cg::coalesced_group g = cg::coalesced_threads();
int prev;
if (g.thread_rank() == 0) {
prev = atomicAdd(p, g.num_threads());
}
prev = g.thread_rank() + g.shfl(prev, 0);
return prev;
}
8.5. 组分区
8.5.1. tiled_partition
template <unsigned int Size, typename ParentT>
thread_block_tile<Size, ParentT> tiled_partition(const ParentT& g);
thread_group tiled_partition(const thread_group& parent, unsigned int tilesz);
tiled_partition
方法是一种集体操作,它将父组分区为子组的一维行主序 Tile 划分。总共将创建 ((size(parent)/tilesz) 个子组,因此父组大小必须可以被 Size
整除。允许的父组为 thread_block
或 thread_block_tile
。
实现可能会导致调用线程等待,直到父组的所有成员都调用了该操作,然后才能恢复执行。功能仅限于本机硬件大小 1/2/4/8/16/32,并且 cg::size(parent)
必须大于 Size
参数。 tiled_partition
的模板化版本也支持 64/128/256/512 大小,但在计算能力为 7.5 或更低的硬件上需要一些额外的步骤,请参考 线程块 Tile 获取详细信息。
代码生成要求: 最低计算能力 5.0,对于大于 32 的大小需要 C++11
示例
/// The following code will create a 32-thread tile
thread_block block = this_thread_block();
thread_block_tile<32> tile32 = tiled_partition<32>(block);
我们可以将这些组中的每一个分区为更小的组,每个组的大小为 4 个线程
auto tile4 = tiled_partition<4>(tile32);
// or using a general group
// thread_group tile4 = tiled_partition(tile32, 4);
例如,如果我们要包含以下代码行
if (tile4.thread_rank()==0) printf("Hello from tile4 rank 0\n");
那么该语句将由块中每四个线程打印:每个 tile4
组中秩为 0 的线程,它们对应于 block
组中秩为 0,4,8,12 等的线程。
8.5.2. labeled_partition
template <typename Label>
coalesced_group labeled_partition(const coalesced_group& g, Label label);
template <unsigned int Size, typename Label>
coalesced_group labeled_partition(const thread_block_tile<Size>& g, Label label);
labeled_partition
方法是一种集体操作,它将父组分区为一维子组,在这些子组中线程是合并的。该实现将评估条件标签,并将标签值相同的线程分配到同一组中。
Label
可以是任何整型类型。
实现可能会导致调用线程等待,直到父组的所有成员都调用了该操作,然后才能恢复执行。
注意: 此功能仍在评估中,将来可能会略有变化。
代码生成要求: 最低计算能力 7.0,C++11
8.5.3. binary_partition
coalesced_group binary_partition(const coalesced_group& g, bool pred);
template <unsigned int Size>
coalesced_group binary_partition(const thread_block_tile<Size>& g, bool pred);
binary_partition()
方法是一种集体操作,它将父组分区为一维子组,在这些子组中线程是合并的。该实现将评估一个谓词,并将具有相同值的线程分配到同一组中。这是 labeled_partition()
的一种特殊形式,其中标签只能为 0 或 1。
实现可能会导致调用线程等待,直到父组的所有成员都调用了该操作,然后才能恢复执行。
注意: 此功能仍在评估中,将来可能会略有变化。
代码生成要求: 最低计算能力 7.0,C++11
示例
/// This example divides a 32-sized tile into a group with odd
/// numbers and a group with even numbers
_global__ void oddEven(int *inputArr) {
auto block = cg::this_thread_block();
auto tile32 = cg::tiled_partition<32>(block);
// inputArr contains random integers
int elem = inputArr[block.thread_rank()];
// after this, tile32 is split into 2 groups,
// a subtile where elem&1 is true and one where its false
auto subtile = cg::binary_partition(tile32, (elem & 1));
}
8.6. 组集体操作
Cooperative Groups 库提供了一组可以由线程组执行的集体操作。这些操作需要指定组中所有线程的参与才能完成操作。组中的所有线程都需要为每个集体调用传递对应参数的相同值,除非参数描述中明确允许不同的值。否则,调用的行为是未定义的。
8.6.1. 同步
8.6.1.1. barrier_arrive
和 barrier_wait
T::arrival_token T::barrier_arrive();
void T::barrier_wait(T::arrival_token&&);
barrier_arrive
和 barrier_wait
成员函数提供类似于 cuda::barrier
(阅读更多) 的同步 API。 Cooperative Groups 自动初始化组栅栏,但到达和等待操作有一个额外的限制,这是由这些操作的集体性质引起的:组中的所有线程必须在每个阶段到达和等待一次栅栏。当使用组调用 barrier_arrive
时,在该组的栅栏阶段完成之前,调用任何集体操作或与该组的另一个栅栏到达的结果是未定义的,栅栏阶段通过 barrier_wait
调用观察到完成。在组中的所有线程都调用 barrier_arrive
之后,阻塞在 barrier_wait
上的线程可能会在其他线程调用 barrier_wait
之前从同步中释放,。组类型 T
可以是任何 隐式组。这允许线程在到达之后和等待同步解决之前执行独立的工作,从而隐藏一些同步延迟。barrier_arrive
返回一个 arrival_token
对象,该对象必须传递到相应的 barrier_wait
中。令牌以这种方式被消耗,不能用于另一个 barrier_wait
调用。
barrier_arrive 和 barrier_wait 用于同步集群中共享内存初始化的示例
#include <cooperative_groups.h>
using namespace cooperative_groups;
void __device__ init_shared_data(const thread_block& block, int *data);
void __device__ local_processing(const thread_block& block);
void __device__ process_shared_data(const thread_block& block, int *data);
__global__ void cluster_kernel() {
extern __shared__ int array[];
auto cluster = this_cluster();
auto block = this_thread_block();
// Use this thread block to initialize some shared state
init_shared_data(block, &array[0]);
auto token = cluster.barrier_arrive(); // Let other blocks know this block is running and data was initialized
// Do some local processing to hide the synchronization latency
local_processing(block);
// Map data in shared memory from the next block in the cluster
int *dsmem = cluster.map_shared_rank(&array[0], (cluster.block_rank() + 1) % cluster.num_blocks());
// Make sure all other blocks in the cluster are running and initialized shared data before accessing dsmem
cluster.barrier_wait(std::move(token));
// Consume data in distributed shared memory
process_shared_data(block, dsmem);
cluster.sync();
}
8.6.1.2. sync
static void T::sync();
template <typename T>
void sync(T& group);
8.6.2. 数据传输
8.6.2.1. memcpy_async
用法 1
template <typename TyGroup, typename TyElem, typename TyShape>
void memcpy_async(
const TyGroup &group,
TyElem *__restrict__ _dst,
const TyElem *__restrict__ _src,
const TyShape &shape
);
执行 ``shape`` 字节的复制。
用法 2
template <typename TyGroup, typename TyElem, typename TyDstLayout, typename TySrcLayout>
void memcpy_async(
const TyGroup &group,
TyElem *__restrict__ dst,
const TyDstLayout &dstLayout,
const TyElem *__restrict__ src,
const TySrcLayout &srcLayout
);
执行 ``min(dstLayout, srcLayout)`` 个元素的复制。如果布局类型为 cuda::aligned_size_t<N>
,则两者都必须指定相同的对齐方式。
勘误 在 CUDA 11.1 中引入的 memcpy_async
API,同时具有 src 和 dst 输入布局,期望布局以元素而不是字节为单位提供。元素类型从 TyElem
推断,大小为 sizeof(TyElem)
。如果 cuda::aligned_size_t<N>
类型用作布局,则指定的元素数量乘以 sizeof(TyElem)
必须是 N 的倍数,建议使用 std::byte
或 char
作为元素类型。
如果指定的复制形状或布局的类型为 cuda::aligned_size_t<N>
,则对齐将保证至少为 min(16, N)
。在这种情况下,dst
和 src
指针都需要对齐到 N 字节,并且复制的字节数需要是 N 的倍数。
代码生成要求: 计算能力 5.0 最低,计算能力 8.0 用于异步性,C++11
需要包含 cooperative_groups/memcpy_async.h
头文件。
示例
/// This example streams elementsPerThreadBlock worth of data from global memory
/// into a limited sized shared memory (elementsInShared) block to operate on.
#include <cooperative_groups.h>
#include <cooperative_groups/memcpy_async.h>
namespace cg = cooperative_groups;
__global__ void kernel(int* global_data) {
cg::thread_block tb = cg::this_thread_block();
const size_t elementsPerThreadBlock = 16 * 1024;
const size_t elementsInShared = 128;
__shared__ int local_smem[elementsInShared];
size_t copy_count;
size_t index = 0;
while (index < elementsPerThreadBlock) {
cg::memcpy_async(tb, local_smem, elementsInShared, global_data + index, elementsPerThreadBlock - index);
copy_count = min(elementsInShared, elementsPerThreadBlock - index);
cg::wait(tb);
// Work with local_smem
index += copy_count;
}
}
8.6.2.2. wait 和 wait_prior
template <typename TyGroup>
void wait(TyGroup & group);
template <unsigned int NumStages, typename TyGroup>
void wait_prior(TyGroup & group);
代码生成要求: 计算能力 5.0 最低,计算能力 8.0 用于异步性,C++11
需要包含 cooperative_groups/memcpy_async.h
头文件。
示例
/// This example streams elementsPerThreadBlock worth of data from global memory
/// into a limited sized shared memory (elementsInShared) block to operate on in
/// multiple (two) stages. As stage N is kicked off, we can wait on and operate on stage N-1.
#include <cooperative_groups.h>
#include <cooperative_groups/memcpy_async.h>
namespace cg = cooperative_groups;
__global__ void kernel(int* global_data) {
cg::thread_block tb = cg::this_thread_block();
const size_t elementsPerThreadBlock = 16 * 1024 + 64;
const size_t elementsInShared = 128;
__align__(16) __shared__ int local_smem[2][elementsInShared];
int stage = 0;
// First kick off an extra request
size_t copy_count = elementsInShared;
size_t index = copy_count;
cg::memcpy_async(tb, local_smem[stage], elementsInShared, global_data, elementsPerThreadBlock - index);
while (index < elementsPerThreadBlock) {
// Now we kick off the next request...
cg::memcpy_async(tb, local_smem[stage ^ 1], elementsInShared, global_data + index, elementsPerThreadBlock - index);
// ... but we wait on the one before it
cg::wait_prior<1>(tb);
// Its now available and we can work with local_smem[stage] here
// (...)
//
// Calculate the amount fo data that was actually copied, for the next iteration.
copy_count = min(elementsInShared, elementsPerThreadBlock - index);
index += copy_count;
// A cg::sync(tb) might be needed here depending on whether
// the work done with local_smem[stage] can release threads to race ahead or not
// Wrap to the next stage
stage ^= 1;
}
cg::wait(tb);
// The last local_smem[stage] can be handled here
}
8.6.3. 数据操作
8.6.3.1. reduce
template <typename TyGroup, typename TyArg, typename TyOp>
auto reduce(const TyGroup& group, TyArg&& val, TyOp&& op) -> decltype(op(val, val));
group
: 有效的组类型为 coalesced_group
和 thread_block_tile
。
val
: 满足以下要求的任何类型
符合可平凡复制的条件,即
is_trivially_copyable<TyArg>::value == true
sizeof(T) <= 32
对于coalesced_group
和尺寸小于或等于 32 的 tile,sizeof(T) <= 8
对于更大的 tile对于给定的函数对象,具有合适的算术或比较运算符。
注意: 组中不同的线程可以为此参数传递不同的值。
op
: 对于整数类型,将提供硬件加速的有效函数对象为 plus(), less(), greater(), bit_and(), bit_xor(), bit_or()
。这些必须构造,因此需要 TyVal 模板参数,即 plus<int>()
。Reduce 还支持可以使用 operator()
调用的 lambda 和其他函数对象
异步归约
template <typename TyGroup, typename TyArg, typename TyAtomic, typename TyOp>
void reduce_update_async(const TyGroup& group, TyAtomic& atomic, TyArg&& val, TyOp&& op);
template <typename TyGroup, typename TyArg, typename TyAtomic, typename TyOp>
void reduce_store_async(const TyGroup& group, TyAtomic& atomic, TyArg&& val, TyOp&& op);
template <typename TyGroup, typename TyArg, typename TyOp>
void reduce_store_async(const TyGroup& group, TyArg* ptr, TyArg&& val, TyOp&& op);
API 的 *_async
变体异步计算结果,以存储到或更新参与线程之一指定的目的地,而不是由每个线程返回。为了观察这些异步调用的效果,需要同步调用线程组或包含它们的更大的组。
在原子存储或更新变体的情况下,
atomic
参数可以是 CUDA C++ 标准库 中可用的cuda::atomic
或cuda::atomic_ref
中的任何一个。此 API 变体仅在平台和设备上可用,其中这些类型受 CUDA C++ 标准库支持。归约的结果用于根据指定的op
原子地更新原子对象,例如,在cg::plus()
的情况下,结果原子地添加到原子对象。由atomic
持有的类型必须与TyArg
的类型匹配。原子对象的范围必须包括组中的所有线程,如果多个组同时使用相同的原子对象,则范围必须包括使用它的所有组中的所有线程。原子更新以宽松的内存顺序执行。在指针存储变体的情况下,归约的结果将弱存储到
dst
指针中。
代码生成要求: 计算能力 5.0 最低,计算能力 8.0 用于硬件加速,C++11。
需要包含 cooperative_groups/reduce.h
头文件。
整数向量的近似标准差示例
#include <cooperative_groups.h>
#include <cooperative_groups/reduce.h>
namespace cg = cooperative_groups;
/// Calculate approximate standard deviation of integers in vec
__device__ int std_dev(const cg::thread_block_tile<32>& tile, int *vec, int length) {
int thread_sum = 0;
// calculate average first
for (int i = tile.thread_rank(); i < length; i += tile.num_threads()) {
thread_sum += vec[i];
}
// cg::plus<int> allows cg::reduce() to know it can use hardware acceleration for addition
int avg = cg::reduce(tile, thread_sum, cg::plus<int>()) / length;
int thread_diffs_sum = 0;
for (int i = tile.thread_rank(); i < length; i += tile.num_threads()) {
int diff = vec[i] - avg;
thread_diffs_sum += diff * diff;
}
// temporarily use floats to calculate the square root
float diff_sum = static_cast<float>(cg::reduce(tile, thread_diffs_sum, cg::plus<int>())) / length;
return static_cast<int>(sqrtf(diff_sum));
}
块范围归约示例
#include <cooperative_groups.h>
#include <cooperative_groups/reduce.h>
namespace cg=cooperative_groups;
/// The following example accepts input in *A and outputs a result into *sum
/// It spreads the data equally within the block
__device__ void block_reduce(const int* A, int count, cuda::atomic<int, cuda::thread_scope_block>& total_sum) {
auto block = cg::this_thread_block();
auto tile = cg::tiled_partition<32>(block);
int thread_sum = 0;
// Stride loop over all values, each thread accumulates its part of the array.
for (int i = block.thread_rank(); i < count; i += block.size()) {
thread_sum += A[i];
}
// reduce thread sums across the tile, add the result to the atomic
// cg::plus<int> allows cg::reduce() to know it can use hardware acceleration for addition
cg::reduce_update_async(tile, total_sum, thread_sum, cg::plus<int>());
// synchronize the block, to ensure all async reductions are ready
block.sync();
}
8.6.3.2. Reduce
运算符
以下是可以使用 reduce
完成的一些基本操作的函数对象的原型
namespace cooperative_groups {
template <typename Ty>
struct cg::plus;
template <typename Ty>
struct cg::less;
template <typename Ty>
struct cg::greater;
template <typename Ty>
struct cg::bit_and;
template <typename Ty>
struct cg::bit_xor;
template <typename Ty>
struct cg::bit_or;
}
归约仅限于编译时实现可用的信息。因此,为了利用 CC 8.0 中引入的内在函数,cg::
命名空间公开了几个反映硬件的功能对象。这些对象看起来类似于 C++ STL 中提供的对象,但 less/greater
除外。与 STL 存在任何差异的原因是,这些函数对象旨在实际反映硬件内在函数的操作。
功能描述
cg::plus:
接受两个值,并使用 operator+ 返回两者的和。cg::less:
接受两个值,并使用 operator< 返回较小的值。不同之处在于,它返回的是较小的值,而不是布尔值。cg::greater:
接受两个值,并使用 operator< 返回较大的值。不同之处在于,它返回的是较大的值,而不是布尔值。cg::bit_and:
接受两个值,并返回 operator& 的结果。cg::bit_xor:
接受两个值,并返回 operator^ 的结果。cg::bit_or:
接受两个值,并返回 operator| 的结果。
示例
{
// cg::plus<int> is specialized within cg::reduce and calls __reduce_add_sync(...) on CC 8.0+
cg::reduce(tile, (int)val, cg::plus<int>());
// cg::plus<float> fails to match with an accelerator and instead performs a standard shuffle based reduction
cg::reduce(tile, (float)val, cg::plus<float>());
// While individual components of a vector are supported, reduce will not use hardware intrinsics for the following
// It will also be necessary to define a corresponding operator for vector and any custom types that may be used
int4 vec = {...};
cg::reduce(tile, vec, cg::plus<int4>())
// Finally lambdas and other function objects cannot be inspected for dispatch
// and will instead perform shuffle based reductions using the provided function object.
cg::reduce(tile, (int)val, [](int l, int r) -> int {return l + r;});
}
8.6.3.3. inclusive_scan
和 exclusive_scan
template <typename TyGroup, typename TyVal, typename TyFn>
auto inclusive_scan(const TyGroup& group, TyVal&& val, TyFn&& op) -> decltype(op(val, val));
template <typename TyGroup, typename TyVal>
TyVal inclusive_scan(const TyGroup& group, TyVal&& val);
template <typename TyGroup, typename TyVal, typename TyFn>
auto exclusive_scan(const TyGroup& group, TyVal&& val, TyFn&& op) -> decltype(op(val, val));
template <typename TyGroup, typename TyVal>
TyVal exclusive_scan(const TyGroup& group, TyVal&& val);
group
: 有效的组类型为 coalesced_group
和 thread_block_tile
。
val
: 满足以下要求的任何类型
符合可平凡复制的条件,即
is_trivially_copyable<TyArg>::value == true
sizeof(T) <= 32
对于coalesced_group
和尺寸小于或等于 32 的 tile,sizeof(T) <= 8
对于更大的 tile对于给定的函数对象,具有合适的算术或比较运算符。
注意: 组中不同的线程可以为此参数传递不同的值。
op
: 为方便起见定义的函数对象为 plus(), less(), greater(), bit_and(), bit_xor(), bit_or()
,在 Reduce 运算符 中描述。这些必须构造,因此需要 TyVal 模板参数,即 plus<int>()
。inclusive_scan
和 exclusive_scan
还支持可以使用 operator()
调用的 lambda 和其他函数对象。没有此参数的重载使用 cg::plus<TyVal>()
。
扫描更新
template <typename TyGroup, typename TyAtomic, typename TyVal, typename TyFn>
auto inclusive_scan_update(const TyGroup& group, TyAtomic& atomic, TyVal&& val, TyFn&& op) -> decltype(op(val, val));
template <typename TyGroup, typename TyAtomic, typename TyVal>
TyVal inclusive_scan_update(const TyGroup& group, TyAtomic& atomic, TyVal&& val);
template <typename TyGroup, typename TyAtomic, typename TyVal, typename TyFn>
auto exclusive_scan_update(const TyGroup& group, TyAtomic& atomic, TyVal&& val, TyFn&& op) -> decltype(op(val, val));
template <typename TyGroup, typename TyAtomic, typename TyVal>
TyVal exclusive_scan_update(const TyGroup& group, TyAtomic& atomic, TyVal&& val);
以下伪代码说明了扫描更新变体的工作方式
/*
inclusive_scan_update behaves as the following block,
except both reduce and inclusive_scan is calculated simultaneously.
auto total = reduce(group, val, op);
TyVal old;
if (group.thread_rank() == selected_thread) {
atomicaly {
old = atomic.load();
atomic.store(op(old, total));
}
}
old = group.shfl(old, selected_thread);
return op(inclusive_scan(group, val, op), old);
*/
代码生成要求: 计算能力 5.0 最低,C++11。
需要包含 cooperative_groups/scan.h
头文件。
示例
#include <stdio.h>
#include <cooperative_groups.h>
#include <cooperative_groups/scan.h>
namespace cg = cooperative_groups;
__global__ void kernel() {
auto thread_block = cg::this_thread_block();
auto tile = cg::tiled_partition<8>(thread_block);
unsigned int val = cg::inclusive_scan(tile, tile.thread_rank());
printf("%u: %u\n", tile.thread_rank(), val);
}
/* prints for each group:
0: 0
1: 1
2: 3
3: 6
4: 10
5: 15
6: 21
7: 28
*/
使用 exclusive_scan 的流压缩示例
#include <cooperative_groups.h>
#include <cooperative_groups/scan.h>
namespace cg = cooperative_groups;
// put data from input into output only if it passes test_fn predicate
template<typename Group, typename Data, typename TyFn>
__device__ int stream_compaction(Group &g, Data *input, int count, TyFn&& test_fn, Data *output) {
int per_thread = count / g.num_threads();
int thread_start = min(g.thread_rank() * per_thread, count);
int my_count = min(per_thread, count - thread_start);
// get all passing items from my part of the input
// into a contagious part of the array and count them.
int i = thread_start;
while (i < my_count + thread_start) {
if (test_fn(input[i])) {
i++;
}
else {
my_count--;
input[i] = input[my_count + thread_start];
}
}
// scan over counts from each thread to calculate my starting
// index in the output
int my_idx = cg::exclusive_scan(g, my_count);
for (i = 0; i < my_count; ++i) {
output[my_idx + i] = input[thread_start + i];
}
// return the total number of items in the output
return g.shfl(my_idx + my_count, g.num_threads() - 1);
}
使用 exclusive_scan_update 的动态缓冲区空间分配示例
#include <cooperative_groups.h>
#include <cooperative_groups/scan.h>
namespace cg = cooperative_groups;
// Buffer partitioning is static to make the example easier to follow,
// but any arbitrary dynamic allocation scheme can be implemented by replacing this function.
__device__ int calculate_buffer_space_needed(cg::thread_block_tile<32>& tile) {
return tile.thread_rank() % 2 + 1;
}
__device__ int my_thread_data(int i) {
return i;
}
__global__ void kernel() {
__shared__ extern int buffer[];
__shared__ cuda::atomic<int, cuda::thread_scope_block> buffer_used;
auto block = cg::this_thread_block();
auto tile = cg::tiled_partition<32>(block);
buffer_used = 0;
block.sync();
// each thread calculates buffer size it needs
int buf_needed = calculate_buffer_space_needed(tile);
// scan over the needs of each thread, result for each thread is an offset
// of that thread’s part of the buffer. buffer_used is atomically updated with
// the sum of all thread's inputs, to correctly offset other tile’s allocations
int buf_offset =
cg::exclusive_scan_update(tile, buffer_used, buf_needed);
// each thread fills its own part of the buffer with thread specific data
for (int i = 0 ; i < buf_needed ; ++i) {
buffer[buf_offset + i] = my_thread_data(i);
}
block.sync();
// buffer_used now holds total amount of memory allocated
// buffer is {0, 0, 1, 0, 0, 1 ...};
}
8.6.4. 执行控制
8.6.4.1. invoke_one
和 invoke_one_broadcast
template<typename Group, typename Fn, typename... Args>
void invoke_one(const Group& group, Fn&& fn, Args&&... args);
template<typename Group, typename Fn, typename... Args>
auto invoke_one_broadcast(const Group& group, Fn&& fn, Args&&... args) -> decltype(fn(args...));
group
: 所有组类型对于 invoke_one
都是有效的,coalesced_group
和 thread_block_tile
对于 invoke_one_broadcast
是有效的。
fn
: 可以使用 operator()
调用的函数或对象。
args
: 与提供的可调用对象 fn
的参数类型匹配的类型参数包。
在 invoke_one_broadcast
的情况下,提供的可调用对象 fn
的返回类型必须满足以下要求
符合可平凡复制的条件,即
is_trivially_copyable<T>::value == true
sizeof(T) <= 32
对于coalesced_group
和尺寸小于或等于 32 的 tile,sizeof(T) <= 8
对于更大的 tile
代码生成要求: 计算能力 5.0 最低,计算能力 9.0 用于硬件加速,C++11。
来自 发现模式部分 的聚合原子示例,已重写为使用 invoke_one_broadcast:
#include <cooperative_groups.h>
#include <cuda/atomic>
namespace cg = cooperative_groups;
template<cuda::thread_scope Scope>
__device__ unsigned int atomicAddOneRelaxed(cuda::atomic<unsigned int, Scope>& atomic) {
auto g = cg::coalesced_threads();
auto prev = cg::invoke_one_broadcast(g, [&] () {
return atomic.fetch_add(g.num_threads(), cuda::memory_order_relaxed);
});
return prev + g.thread_rank();
}
8.7. 网格同步
在引入协同组之前,CUDA 编程模型仅允许在内核完成边界处在线程块之间进行同步。内核边界带有状态的隐式失效,以及潜在的性能影响。
例如,在某些用例中,应用程序有大量的小内核,每个内核代表处理管道中的一个阶段。当前 CUDA 编程模型需要这些内核的存在,以确保在一个管道阶段上运行的线程块在下一个管道阶段上运行的线程块准备好使用数据之前生成数据。在这种情况下,提供全局线程块间同步的能力将允许应用程序重构为具有持久线程块,这些线程块能够在给定阶段完成时在设备上同步。
要在内核内部跨网格同步,您只需使用 grid.sync()
函数
grid_group grid = this_grid();
grid.sync();
并且在启动内核时,需要使用 cudaLaunchCooperativeKernel
CUDA 运行时启动 API 或 CUDA 驱动程序等效项
,而不是 <<<...>>>
执行配置语法。
示例
为了保证线程块在 GPU 上的共驻留,需要仔细考虑启动的块数。例如,可以启动与 SM 数量一样多的块,如下所示
int dev = 0;
cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, dev);
// initialize, then launch
cudaLaunchCooperativeKernel((void*)my_kernel, deviceProp.multiProcessorCount, numThreads, args);
或者,您可以使用占用率计算器计算每个 SM 可以同时容纳多少个块,从而最大化暴露的并行性,如下所示
/// This will launch a grid that can maximally fill the GPU, on the default stream with kernel arguments
int numBlocksPerSm = 0;
// Number of threads my_kernel will be launched with
int numThreads = 128;
cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, dev);
cudaOccupancyMaxActiveBlocksPerMultiprocessor(&numBlocksPerSm, my_kernel, numThreads, 0);
// launch
void *kernelArgs[] = { /* add kernel args */ };
dim3 dimBlock(numThreads, 1, 1);
dim3 dimGrid(deviceProp.multiProcessorCount*numBlocksPerSm, 1, 1);
cudaLaunchCooperativeKernel((void*)my_kernel, dimGrid, dimBlock, kernelArgs);
良好的做法是首先通过查询设备属性 cudaDevAttrCooperativeLaunch
来确保设备支持协同启动
int dev = 0;
int supportsCoopLaunch = 0;
cudaDeviceGetAttribute(&supportsCoopLaunch, cudaDevAttrCooperativeLaunch, dev);
这将设置 supportsCoopLaunch
为 1,如果设备 0 上支持该属性。仅支持计算能力为 6.0 及更高的设备。此外,您需要运行在以下任一平台上
没有 MPS 的 Linux 平台
具有 MPS 的 Linux 平台以及计算能力为 7.0 或更高的设备上
最新的 Windows 平台
8.8. 多设备同步
为了在协同组中实现跨多个设备的同步,需要使用 cudaLaunchCooperativeKernelMultiDevice
CUDA API。这与现有的 CUDA API 有显著不同,它将允许单个主机线程跨多个设备启动内核。除了 cudaLaunchCooperativeKernel
所做的约束和保证之外,此 API 还有额外的语义
此 API 将确保启动是原子的,即,如果 API 调用成功,则提供的线程块数将在所有指定的设备上启动。
通过此 API 启动的函数必须相同。驱动程序在此方面不进行显式检查,因为这在很大程度上是不可行的。应用程序有责任确保这一点。
提供的
cudaLaunchParams
中的任何两个条目都不得映射到同一设备。此启动所针对的所有设备的计算能力必须相同 - 主版本和次版本。
所有设备上的块大小、网格大小和每个网格的共享内存量必须相同。请注意,这意味着每个设备可以启动的最大块数将受到 SM 数量最少的设备的限制。
拥有要启动的 CUfunction 的模块中存在的任何用户定义的
__device__
、__constant__
或__managed__
设备全局变量都在每个设备上独立实例化。用户负责适当地初始化此类设备全局变量。
弃用通知:cudaLaunchCooperativeKernelMultiDevice
已在 CUDA 11.3 中针对所有设备弃用。多设备共轭梯度示例中可以找到替代方法的示例。
通过为所有参与设备启用对等访问(通过 cuCtxEnablePeerAccess
或 cudaDeviceEnablePeerAccess
),可以实现多设备同步的最佳性能。
启动参数应使用结构数组(每个设备一个)定义,并使用 cudaLaunchCooperativeKernelMultiDevice
启动
示例
cudaDeviceProp deviceProp;
cudaGetDeviceCount(&numGpus);
// Per device launch parameters
cudaLaunchParams *launchParams = (cudaLaunchParams*)malloc(sizeof(cudaLaunchParams) * numGpus);
cudaStream_t *streams = (cudaStream_t*)malloc(sizeof(cudaStream_t) * numGpus);
// The kernel arguments are copied over during launch
// Its also possible to have individual copies of kernel arguments per device, but
// the signature and name of the function/kernel must be the same.
void *kernelArgs[] = { /* Add kernel arguments */ };
for (int i = 0; i < numGpus; i++) {
cudaSetDevice(i);
// Per device stream, but its also possible to use the default NULL stream of each device
cudaStreamCreate(&streams[i]);
// Loop over other devices and cudaDeviceEnablePeerAccess to get a faster barrier implementation
}
// Since all devices must be of the same compute capability and have the same launch configuration
// it is sufficient to query device 0 here
cudaGetDeviceProperties(&deviceProp[i], 0);
dim3 dimBlock(numThreads, 1, 1);
dim3 dimGrid(deviceProp.multiProcessorCount, 1, 1);
for (int i = 0; i < numGpus; i++) {
launchParamsList[i].func = (void*)my_kernel;
launchParamsList[i].gridDim = dimGrid;
launchParamsList[i].blockDim = dimBlock;
launchParamsList[i].sharedMem = 0;
launchParamsList[i].stream = streams[i];
launchParamsList[i].args = kernelArgs;
}
cudaLaunchCooperativeKernelMultiDevice(launchParams, numGpus);
此外,与网格范围同步一样,生成的设备代码看起来非常相似
multi_grid_group multi_grid = this_multi_grid();
multi_grid.sync();
但是,代码需要在单独编译中编译,方法是将 -rdc=true
传递给 nvcc。
良好的做法是首先通过查询设备属性 cudaDevAttrCooperativeMultiDeviceLaunch
来确保设备支持多设备协同启动
int dev = 0;
int supportsMdCoopLaunch = 0;
cudaDeviceGetAttribute(&supportsMdCoopLaunch, cudaDevAttrCooperativeMultiDeviceLaunch, dev);
这将设置 supportsMdCoopLaunch
为 1,如果设备 0 上支持该属性。仅支持计算能力为 6.0 及更高的设备。此外,您需要运行在 Linux 平台(没有 MPS)或当前版本的 Windows 上,且设备处于 TCC 模式。
有关更多信息,请参阅 cudaLaunchCooperativeKernelMultiDevice
API 文档。
9. CUDA 动态并行
9.1. 简介
9.1.1. 概述
动态并行性仅受计算能力 3.5 及更高版本的设备支持。
9.1.2. 术语表
本指南中使用的术语的定义。
- 网格
网格是 线程 的集合。网格中的线程执行 内核函数,并分为 线程块。
- 线程块
线程块是在同一多处理器 (SM) 上执行的线程组。线程块内的线程可以访问共享内存,并且可以显式同步。
- 内核函数
内核函数是一个隐式并行子例程,它在 CUDA 执行和内存模型下为网格中的每个线程执行。
- 主机
主机是指最初调用 CUDA 的执行环境。通常是在系统 CPU 处理器上运行的线程。
- 父级
父级线程、线程块或网格是启动了新网格(子级 网格)的线程、线程块或网格。在所有其启动的子级网格也完成之前,父级不被视为已完成。
- 子级
子级线程、块或网格是由父级网格启动的线程、块或网格。子级网格必须在父级线程、线程块或网格被视为完成之前完成。
- 线程块作用域
具有线程块作用域的对象具有单个线程块的生命周期。它们仅在由创建对象的线程块中的线程操作时才具有定义的行为,并在创建它们的线程块完成时销毁。
- 设备运行时
设备运行时是指可用的运行时系统和 API,以使内核函数能够使用动态并行性。
9.2. 执行环境和内存模型
9.2.1. 执行环境
CUDA 执行模型基于线程、线程块和网格的基元,内核函数定义了线程块和网格中各个线程执行的程序。当调用内核函数时,网格的属性由执行配置描述,该配置在 CUDA 中具有特殊的语法。CUDA 中对动态并行性的支持扩展了配置、启动和隐式同步新网格的能力,使其能够由设备上运行的线程执行。
9.2.1.1. 父级和子级网格
配置和启动新网格的设备线程属于父级网格,而由调用创建的网格是子级网格。
子级网格的调用和完成是正确嵌套的,这意味着在父级网格创建的所有子级网格完成之前,父级网格不被视为已完成,并且运行时保证父级和子级之间的隐式同步。

图 29 父子启动嵌套
9.2.1.2. CUDA 原语的范围
在主机和设备上,CUDA 运行时都提供了一个 API,用于启动内核以及通过流和事件跟踪启动之间的依赖关系。在主机系统上,启动的状态和引用流和事件的 CUDA 原语在进程内的所有线程之间共享;但是,进程独立执行,可能不共享 CUDA 对象。
在设备上,启动的内核和 CUDA 对象对网格中的所有线程可见。这意味着,例如,一个流可以由一个线程创建,并由网格中的任何其他线程使用。
9.2.1.3. 同步
警告
在 CUDA 11.6 中,已弃用从父块显式同步子内核(即在设备代码中使用 cudaDeviceSynchronize()
),并在 compute_90+ 编译中移除。对于计算能力 < 9.0,需要通过指定 -DCUDA_FORCE_CDP1_IF_SUPPORTED
进行编译时选择加入,才能继续在设备代码中使用 cudaDeviceSynchronize()
。请注意,这计划在未来的 CUDA 版本中完全移除。
来自任何线程的 CUDA 运行时操作,包括内核启动,在网格中的所有线程中都是可见的。这意味着父网格中的调用线程可以执行同步,以控制由网格中任何线程在网格中任何线程创建的流上启动的网格的启动顺序。在网格中的所有线程完成所有启动之前,网格的执行不被视为完成。如果网格中的所有线程在所有子启动完成之前退出,则将自动触发隐式同步操作。
9.2.1.4. 流和事件
CUDA 流和事件允许控制网格启动之间的依赖关系:启动到同一流中的网格按顺序执行,事件可以用于创建流之间的依赖关系。在设备上创建的流和事件具有完全相同的目的。
在网格内创建的流和事件存在于网格范围内,但在创建它们的网格之外使用时,行为未定义。如上所述,当网格退出时,网格启动的所有工作都隐式同步;启动到流中的工作也包含在内,所有依赖关系都得到适当解决。在网格范围外修改的流上进行操作的行为是未定义的。
在主机上创建的流和事件在任何内核中使用时,行为都是未定义的,就像父网格创建的流和事件在子网格中使用时行为未定义一样。
9.2.1.5. 排序和并发
设备运行时内核启动的排序遵循 CUDA 流排序语义。在一个网格内,所有启动到同一流中的内核(稍后讨论的即发即弃流除外)都按顺序执行。当同一网格中的多个线程启动到同一流中时,流内的排序取决于网格内的线程调度,这可以使用诸如 __syncthreads()
之类的同步原语来控制。
请注意,虽然命名流在网格内的所有线程之间共享,但隐式 NULL 流仅在线程块内的所有线程之间共享。如果一个线程块中的多个线程启动到隐式流中,则这些启动将按顺序执行。如果不同线程块中的多个线程启动到隐式流中,则这些启动可以并发执行。如果希望线程块内多个线程的启动具有并发性,则应使用显式命名流。
动态并行使程序内更容易表达并发性;但是,设备运行时在 CUDA 执行模型中没有引入新的并发性保证。不能保证设备上任意数量的不同线程块之间并发执行。
缺乏并发性保证也扩展到父网格及其子网格。当父网格启动子网格时,子网格可以在流依赖关系得到满足且硬件资源可用于托管子网格后开始执行,但不保证在父网格到达隐式同步点之前开始执行。
虽然通常很容易实现并发,但它可能随设备配置、应用程序工作负载和运行时调度而变化。因此,依赖不同线程块之间的任何并发是不安全的。
9.2.1.6. 设备管理
设备运行时不支持多 GPU;设备运行时只能在其当前执行的设备上运行。但是,允许查询系统中任何支持 CUDA 的设备的属性。
9.2.2. 内存模型
父网格和子网格共享相同的全局和常量内存存储,但具有不同的本地和共享内存。
9.2.2.1. 一致性和连贯性
9.2.2.1.1. 全局内存
父网格和子网格对全局内存具有连贯的访问,子网格和父网格之间具有弱一致性保证。在子网格执行期间,只有一个时间点其内存视图与父线程完全一致:即子网格被父网格调用的点。
子网格调用之前父线程中的所有全局内存操作对子网格都是可见的。随着 cudaDeviceSynchronize()
的移除,不再可能从父网格访问子网格中线程所做的修改。在父网格退出之前访问子网格中线程所做修改的唯一方法是通过启动到 cudaStreamTailLaunch
流中的内核。
在以下示例中,执行 child_launch
的子网格仅保证看到在子网格启动之前对 data
所做的修改。由于父线程 0 正在执行启动,因此子网格将与父线程 0 看到的内存一致。由于第一次 __syncthreads()
调用,子网格将看到 data[0]=0
, data[1]=1
, …, data[255]=255
(如果没有 __syncthreads()
调用,则仅保证子网格看到 data[0]=0
)。子网格仅保证在隐式同步时返回。这意味着子网格中线程所做的修改永远不能保证对父网格可用。要访问 child_launch
所做的修改,需要将 tail_launch
内核启动到 cudaStreamTailLaunch
流中。
__global__ void tail_launch(int *data) {
data[threadIdx.x] = data[threadIdx.x]+1;
}
__global__ void child_launch(int *data) {
data[threadIdx.x] = data[threadIdx.x]+1;
}
__global__ void parent_launch(int *data) {
data[threadIdx.x] = threadIdx.x;
__syncthreads();
if (threadIdx.x == 0) {
child_launch<<< 1, 256 >>>(data);
tail_launch<<< 1, 256, 0, cudaStreamTailLaunch >>>(data);
}
}
void host_launch(int *data) {
parent_launch<<< 1, 256 >>>(data);
}
9.2.2.1.2. 零拷贝内存
零拷贝系统内存具有与全局内存相同的一致性和连贯性保证,并遵循上面详述的语义。内核可能无法分配或释放零拷贝内存,但可以使用从主机程序传入的零拷贝指针。
9.2.2.1.3. 常量内存
常量不能从设备修改。它们只能从主机修改,但是在并发网格在其生命周期内的任何时候访问该常量时,从主机修改常量的行为是未定义的。
9.2.2.1.5. 本地内存
本地内存是执行线程的私有存储,在该线程之外不可见。将本地内存的指针作为启动参数启动子内核是非法的。从子内核解引用此类本地内存指针的结果将是未定义的。
例如,以下是非法的,如果 child_launch
访问 x_array
,则行为未定义
int x_array[10]; // Creates x_array in parent's local memory
child_launch<<< 1, 1 >>>(x_array);
程序员有时很难意识到变量何时被编译器放置到本地内存中。一般规则是,传递给子内核的所有存储都应从全局内存堆中显式分配,可以使用 cudaMalloc()
、new()
或通过在全局作用域中声明 __device__
存储。例如
// Correct - "value" is global storage
__device__ int value;
__device__ void x() {
value = 5;
child<<< 1, 1 >>>(&value);
}
// Invalid - "value" is local storage
__device__ void y() {
int value = 5;
child<<< 1, 1 >>>(&value);
}
9.2.2.1.6. 纹理内存
对纹理映射到的全局内存区域的写入与纹理访问不连贯。纹理内存的连贯性在子网格的调用和子网格完成时强制执行。这意味着在子内核启动之前对内存的写入会反映在子内核的纹理内存访问中。与上面的全局内存类似,子内核对内存的写入永远不能保证反映在父内核的纹理内存访问中。在父网格退出之前访问子网格中线程所做修改的唯一方法是通过启动到 cudaStreamTailLaunch
流中的内核。父内核和子内核的并发访问可能会导致数据不一致。
9.3. 编程接口
9.3.1. CUDA C++ 参考
本节介绍 CUDA C++ 语言扩展中为支持动态并行而进行的更改和添加。
使用 CUDA C++ 进行动态并行性的 CUDA 内核可用的语言接口和 API,称为设备运行时,与主机上可用的 CUDA 运行时 API 非常相似。在可能的情况下,CUDA 运行时 API 的语法和语义已保留,以便于代码重用于可能在主机或设备环境中运行的例程。
与 CUDA C++ 中的所有代码一样,此处概述的 API 和代码是按线程代码。这使每个线程能够对接下来要执行的内核或操作做出唯一的动态决策。块内的线程之间没有同步要求来执行任何提供的设备运行时 API,这使得设备运行时 API 函数可以在任意发散的内核代码中调用而不会死锁。
9.3.1.1. 设备端内核启动
可以使用标准 CUDA <<< >>> 语法从设备启动内核
kernel_name<<< Dg, Db, Ns, S >>>([kernel arguments]);
Dg
的类型为dim3
,并指定网格的维度和大小Db
的类型为dim3
,并指定每个线程块的维度和大小Ns
的类型为size_t
,并指定为此调用动态分配给每个线程块的共享内存字节数,此外还有静态分配的内存。Ns
是一个可选参数,默认为 0。S
的类型为cudaStream_t
,并指定与此调用关联的流。流必须在进行调用的同一网格中分配。S
是一个可选参数,默认为 NULL 流。
9.3.1.1.1. 启动是异步的
与主机端启动相同,所有设备端内核启动都相对于启动线程是异步的。也就是说,<<<>>>
启动命令将立即返回,并且启动线程将继续执行,直到它遇到隐式启动同步点(例如,在启动到 cudaStreamTailLaunch
流中的内核处)。
子网格启动被发布到设备,并将独立于父线程执行。子网格可以在启动后的任何时间开始执行,但不保证在启动线程到达隐式启动同步点之前开始执行。
9.3.1.1.2. 启动环境配置
所有全局设备配置设置(例如,从 cudaDeviceGetCacheConfig()
返回的共享内存和 L1 缓存大小,以及从 cudaDeviceGetLimit()
返回的设备限制)将从父级继承。同样,设备限制(例如堆栈大小)将保持配置状态。
对于主机启动的内核,从主机设置的每个内核配置将优先于全局设置。当从设备启动内核时,也将使用这些配置。无法从设备重新配置内核的环境。
9.3.1.2. 流
命名和未命名 (NULL) 流都可从设备运行时获得。命名流可以由网格内的任何线程使用,但流句柄不能传递给其他子/父内核。换句话说,流应被视为对其创建所在的网格私有。
与主机端启动类似,启动到单独流中的工作可以并发运行,但实际的并发性不能保证。CUDA 编程模型不支持依赖子内核之间并发性的程序,并且将具有未定义的行为。
设备上不支持主机端 NULL 流的跨流屏障语义(有关详细信息,请参见下文)。为了保留与主机运行时的语义兼容性,所有设备流都必须使用 cudaStreamCreateWithFlags()
API 创建,并传递 cudaStreamNonBlocking
标志。cudaStreamCreate()
调用是仅主机运行时的 API,并且将无法为设备编译。
由于设备运行时不支持 cudaStreamSynchronize()
和 cudaStreamQuery()
,因此当应用程序需要知道流启动的子内核已完成时,应使用启动到 cudaStreamTailLaunch
流中的内核。
9.3.1.2.1. 隐式 (NULL) 流
在主机程序中,未命名 (NULL) 流与其他流具有额外的屏障同步语义(有关详细信息,请参见 默认流)。设备运行时提供一个隐式、未命名的流,该流在线程块中的所有线程之间共享,但由于所有命名流都必须使用 cudaStreamNonBlocking
标志创建,因此启动到 NULL 流中的工作不会插入对任何其他流(包括其他线程块的 NULL 流)中待处理工作的隐式依赖关系。
9.3.1.2.2. 即发即弃流
即发即弃命名流 (cudaStreamFireAndForget
) 允许用户启动即发即弃工作,减少样板代码,并避免流跟踪开销。它在功能上与为每次启动创建新流并启动到该流中相同,但速度更快。
即发即弃启动会立即安排启动,而不依赖于先前启动的网格的完成。除非通过父网格末尾的隐式同步,否则没有其他网格启动可以依赖于即发即弃启动的完成。因此,在父网格的即发即弃工作完成之前,尾部启动或父网格流中的下一个网格不会启动。
// In this example, C2's launch will not wait for C1's completion
__global__ void P( ... ) {
C1<<< ... , cudaStreamFireAndForget >>>( ... );
C2<<< ... , cudaStreamFireAndForget >>>( ... );
}
即发即弃流不能用于记录或等待事件。尝试这样做会导致 cudaErrorInvalidValue
。当使用定义的 CUDA_FORCE_CDP1_IF_SUPPORTED
编译时,不支持即发即弃流。即发即弃流的使用需要以 64 位模式编译。
9.3.1.2.3. 尾部启动流
尾部启动命名流 (cudaStreamTailLaunch
) 允许网格在完成之后调度新网格进行启动。在大多数情况下,应该可以使用尾部启动来实现与 cudaDeviceSynchronize()
相同的功能。
每个网格都有自己的尾部启动流。网格启动的所有非尾部启动工作在启动尾部流之前隐式同步。即,父网格的尾部启动在父网格以及父网格启动到普通流或每个线程或即发即弃流的所有工作完成之前不会启动。如果两个网格启动到同一网格的尾部启动流,则后一个网格在先前的网格及其所有后代工作完成之前不会启动。
// In this example, C2 will only launch after C1 completes.
__global__ void P( ... ) {
C1<<< ... , cudaStreamTailLaunch >>>( ... );
C2<<< ... , cudaStreamTailLaunch >>>( ... );
}
启动到尾部启动流中的网格在父网格完成所有工作(包括父网格在所有非尾部启动流中启动的所有其他网格(及其后代),包括在尾部启动之后执行或启动的工作)之前不会启动。
// In this example, C will only launch after all X, F and P complete.
__global__ void P( ... ) {
C<<< ... , cudaStreamTailLaunch >>>( ... );
X<<< ... , cudaStreamPerThread >>>( ... );
F<<< ... , cudaStreamFireAndForget >>>( ... )
}
父网格流中的下一个网格在父网格的尾部启动工作完成之前不会启动。换句话说,尾部启动流的行为就好像它插入在其父网格及其父网格流中的下一个网格之间一样。
// In this example, P2 will only launch after C completes.
__global__ void P1( ... ) {
C<<< ... , cudaStreamTailLaunch >>>( ... );
}
__global__ void P2( ... ) {
}
int main ( ... ) {
...
P1<<< ... >>>( ... );
P2<<< ... >>>( ... );
...
}
每个网格只有一个尾部启动流。要尾部启动并发网格,可以像下面的示例一样完成。
// In this example, C1 and C2 will launch concurrently after P's completion
__global__ void T( ... ) {
C1<<< ... , cudaStreamFireAndForget >>>( ... );
C2<<< ... , cudaStreamFireAndForget >>>( ... );
}
__global__ void P( ... ) {
...
T<<< ... , cudaStreamTailLaunch >>>( ... );
}
尾部启动流不能用于记录或等待事件。尝试这样做会导致 cudaErrorInvalidValue
。当使用定义的 CUDA_FORCE_CDP1_IF_SUPPORTED
编译时,不支持尾部启动流。尾部启动流的使用需要以 64 位模式编译。
9.3.1.3. 事件
仅支持 CUDA 事件的流间同步功能。这意味着支持 cudaStreamWaitEvent()
,但不支持 cudaEventSynchronize()
、cudaEventElapsedTime()
和 cudaEventQuery()
。由于不支持 cudaEventElapsedTime()
,因此 cudaEvents 必须通过 cudaEventCreateWithFlags()
创建,并传递 cudaEventDisableTiming
标志。
与命名流一样,事件对象可以在创建它们的网格内的所有线程之间共享,但对于该网格是本地的,不能传递给其他内核。事件句柄不能保证在网格之间是唯一的,因此在未使用创建它的网格内的事件句柄将导致未定义的行为。
9.3.1.4. 同步
如果调用线程打算与从其他线程调用的子网格同步,则由程序执行足够的线程间同步,例如通过 CUDA 事件。
由于无法从父线程显式同步子工作,因此无法保证子网格中发生的更改对父网格中的线程可见。
9.3.1.5. 设备管理
只有内核正在运行的设备才能从该内核进行控制。这意味着设备运行时不支持诸如 cudaSetDevice()
之类的设备 API。从 GPU 看到的活动设备(从 cudaGetDevice()
返回)将具有与从主机系统看到的设备编号相同的设备编号。cudaDeviceGetAttribute()
调用可以请求有关另一个设备的信息,因为此 API 允许指定设备 ID 作为调用的参数。请注意,设备运行时不提供通用的 cudaGetDeviceProperties()
API - 必须单独查询属性。
9.3.1.6. 内存声明
9.3.1.6.1. 设备内存和常量内存
在使用设备运行时时,使用 __device__
或 __constant__
内存空间说明符在文件作用域中声明的内存的行为相同。所有内核都可以读取或写入设备变量,无论内核最初是由主机还是设备运行时启动的。同样,所有内核都将具有与模块作用域中声明的 __constant__
相同的视图。
9.3.1.6.2. 纹理和表面
CUDA 支持动态创建的纹理和表面对象14,其中纹理对象可以在主机上创建,传递给内核,由该内核使用,然后从主机销毁。设备运行时不允许从设备代码内部创建或销毁纹理或表面对象,但是从主机创建的纹理和表面对象可以在设备上自由使用和传递。无论在何处创建,动态创建的纹理对象始终有效,并且可以从父内核传递给子内核。
注意
设备运行时不支持从设备启动的内核中的传统模块作用域(即,Fermi 风格)纹理和表面。模块作用域(传统)纹理可以从主机创建并在设备代码中使用,就像任何内核一样,但只能由顶层内核(即,从主机启动的内核)使用。
9.3.1.6.4. 符号地址
设备端符号(即,标记为 __device__
的符号)可以从内核内部简单地通过 &
运算符引用,因为所有全局作用域设备变量都在内核的可见地址空间中。这也适用于 __constant__
符号,尽管在这种情况下,指针将引用只读数据。
鉴于可以直接引用设备端符号,因此引用符号的那些 CUDA 运行时 API(例如,cudaMemcpyToSymbol()
或 cudaGetSymbolAddress()
)是冗余的,因此设备运行时不支持。请注意,这意味着常量数据不能从运行中的内核内部更改,即使在子内核启动之前也不行,因为对 __constant__
空间的引用是只读的。
9.3.1.7. API 错误和启动失败
与 CUDA 运行时通常一样,任何函数都可能返回错误代码。记录返回的最后一个错误代码,并且可以通过 cudaGetLastError()
调用检索。错误是按线程记录的,因此每个线程都可以识别它生成的最新错误。错误代码的类型为 cudaError_t
。
与主机端启动类似,设备端启动可能因多种原因(无效参数等)而失败。用户必须调用 cudaGetLastError()
来确定启动是否生成错误,但是启动后没有错误并不意味着子内核已成功完成。
对于设备端异常,例如,访问无效地址,子网格中的错误将返回给主机。
9.3.1.7.1. 启动设置 API
内核启动是一种系统级机制,通过设备运行时库公开,因此可以直接从 PTX 通过底层的 cudaGetParameterBuffer()
和 cudaLaunchDevice()
API 访问。CUDA 应用程序可以自行调用这些 API,其要求与 PTX 相同。在这两种情况下,用户都需要负责根据规范以正确的格式正确填充所有必要的数据结构。这些数据结构中保证向后兼容性。
与主机端启动一样,设备端操作符 <<<>>>
映射到下层的内核启动 API。这是为了让以 PTX 为目标的用户能够执行启动,并使编译器前端可以将 <<<>>>
转换为这些调用。
运行时 API 启动函数 |
与主机运行时行为差异的描述(如果没有描述,则行为相同) |
---|---|
|
从 |
|
从 |
这些启动函数的 API 与 CUDA 运行时 API 的 API 不同,定义如下
extern device cudaError_t cudaGetParameterBuffer(void **params);
extern __device__ cudaError_t cudaLaunchDevice(void *kernel,
void *params, dim3 gridDim,
dim3 blockDim,
unsigned int sharedMemSize = 0,
cudaStream_t stream = 0);
9.3.1.8. API 参考
此处详细介绍了设备运行时中支持的 CUDA 运行时 API 的部分。主机和设备运行时 API 具有相同的语法;语义相同,除非另有说明。下表概述了相对于主机提供的版本的 API。
运行时 API 函数 |
详细信息 |
---|---|
|
|
|
|
|
最后一个错误是每线程状态,而不是每块状态 |
|
|
|
|
|
|
|
将返回任何设备的属性 |
|
始终返回当前设备 ID,如同从主机端看到的那样 |
|
必须传递 |
|
|
|
|
|
必须传递 |
|
|
|
|
|
|
|
关于所有
|
|
|
|
|
|
|
|
|
|
|
|
|
|
可能不会在设备上对主机上创建的指针调用 |
|
|
|
|
|
|
|
9.3.2. 从 PTX 进行设备端启动
本节适用于以并行线程执行 (PTX) 为目标并计划在其语言中支持动态并行性的编程语言和编译器实现者。它提供了与在 PTX 级别支持内核启动相关的底层细节。
9.3.2.1. 内核启动 API
可以使用以下两个可从 PTX 访问的 API 实现设备端内核启动:cudaLaunchDevice()
和 cudaGetParameterBuffer()
。cudaLaunchDevice()
使用通过调用 cudaGetParameterBuffer()
获取并填充了已启动内核参数的参数缓冲区来启动指定的内核。如果启动的内核不带任何参数,则参数缓冲区可以为 NULL,即无需调用 cudaGetParameterBuffer()
。
9.3.2.1.1. cudaLaunchDevice
在 PTX 级别,cudaLaunchDevice()
需要在使用前以下面两种形式之一声明。
// PTX-level Declaration of cudaLaunchDevice() when .address_size is 64
.extern .func(.param .b32 func_retval0) cudaLaunchDevice
(
.param .b64 func,
.param .b64 parameterBuffer,
.param .align 4 .b8 gridDimension[12],
.param .align 4 .b8 blockDimension[12],
.param .b32 sharedMemSize,
.param .b64 stream
)
;
下面的 CUDA 级别声明映射到上述 PTX 级别声明之一,并在系统头文件 cuda_device_runtime_api.h
中找到。该函数在 cudadevrt
系统库中定义,该库必须与程序链接才能使用设备端内核启动功能。
// CUDA-level declaration of cudaLaunchDevice()
extern "C" __device__
cudaError_t cudaLaunchDevice(void *func, void *parameterBuffer,
dim3 gridDimension, dim3 blockDimension,
unsigned int sharedMemSize,
cudaStream_t stream);
第一个参数是指向要启动的内核的指针,第二个参数是保存已启动内核实际参数的参数缓冲区。参数缓冲区的布局在下面的参数缓冲区布局中进行了解释。其他参数指定启动配置,即网格维度、块维度、共享内存大小以及与启动关联的流(有关启动配置的详细描述,请参阅执行配置)。
9.3.2.1.2. cudaGetParameterBuffer
cudaGetParameterBuffer()
需要在 PTX 级别使用前声明。PTX 级别声明必须采用下面给出的两种形式之一,具体取决于地址大小
// PTX-level Declaration of cudaGetParameterBuffer() when .address_size is 64
.extern .func(.param .b64 func_retval0) cudaGetParameterBuffer
(
.param .b64 alignment,
.param .b64 size
)
;
以下 cudaGetParameterBuffer()
的 CUDA 级别声明映射到上述 PTX 级别声明
// CUDA-level Declaration of cudaGetParameterBuffer()
extern "C" __device__
void *cudaGetParameterBuffer(size_t alignment, size_t size);
第一个参数指定参数缓冲区的对齐要求,第二个参数指定字节大小要求。在当前实现中,cudaGetParameterBuffer()
返回的参数缓冲区始终保证为 64 字节对齐,并且对齐要求参数将被忽略。但是,建议将正确的对齐要求值(即要放置在参数缓冲区中的任何参数的最大对齐方式)传递给 cudaGetParameterBuffer()
,以确保未来的可移植性。
9.3.2.2. 参数缓冲区布局
禁止在参数缓冲区中进行参数重新排序,并且参数缓冲区中放置的每个单独参数都需要对齐。也就是说,每个参数都必须放置在参数缓冲区中的第 nth 个字节处,其中 n 是参数大小的最小倍数,该倍数大于前一个参数占用的最后一个字节的偏移量。参数缓冲区的最大大小为 4KB。
有关 CUDA 编译器生成的 PTX 代码的更详细描述,请参阅 PTX-3.5 规范。
9.3.3. 动态并行性的工具包支持
9.3.3.1. 在 CUDA 代码中包含设备运行时 API
与主机端运行时 API 类似,CUDA 设备运行时 API 的原型在程序编译期间自动包含。无需显式包含 cuda_device_runtime_api.h
。
9.3.3.2. 编译和链接
当使用 nvcc
编译和链接使用动态并行性的 CUDA 程序时,该程序将自动链接到静态设备运行时库 libcudadevrt
。
设备运行时作为静态库(Windows 上的 cudadevrt.lib
,Linux 下的 libcudadevrt.a
)提供,使用设备运行时的 GPU 应用程序必须链接到该库。设备库的链接可以通过 nvcc
和/或 nvlink
完成。下面显示了两个简单的示例。
如果可以从命令行指定所有必需的源文件,则可以在一个步骤中编译和链接设备运行时程序
$ nvcc -arch=sm_75 -rdc=true hello_world.cu -o hello -lcudadevrt
也可以先将 CUDA .cu 源文件编译为目标文件,然后在两阶段过程中将它们链接在一起
$ nvcc -arch=sm_75 -dc hello_world.cu -o hello_world.o
$ nvcc -arch=sm_75 -rdc=true hello_world.o -o hello -lcudadevrt
有关更多详细信息,请参阅《CUDA 驱动程序编译器 NVCC 指南》的“使用单独编译”部分。
9.4. 编程指南
9.4.1. 基础知识
设备运行时是主机运行时的功能子集。API 级别的设备管理、内核启动、设备 memcpy、流管理和事件管理都从设备运行时公开。
对于已经有 CUDA 经验的人来说,设备运行时的编程应该很熟悉。设备运行时语法和语义与主机 API 的语法和语义基本相同,任何例外情况都在本文档前面详细说明。
以下示例显示了一个简单的 Hello World 程序,其中包含动态并行性
#include <stdio.h>
__global__ void childKernel()
{
printf("Hello ");
}
__global__ void tailKernel()
{
printf("World!\n");
}
__global__ void parentKernel()
{
// launch child
childKernel<<<1,1>>>();
if (cudaSuccess != cudaGetLastError()) {
return;
}
// launch tail into cudaStreamTailLaunch stream
// implicitly synchronizes: waits for child to complete
tailKernel<<<1,1,0,cudaStreamTailLaunch>>>();
}
int main(int argc, char *argv[])
{
// launch parent
parentKernel<<<1,1>>>();
if (cudaSuccess != cudaGetLastError()) {
return 1;
}
// wait for parent to complete
if (cudaSuccess != cudaDeviceSynchronize()) {
return 2;
}
return 0;
}
此程序可以从命令行在一个步骤中构建,如下所示
$ nvcc -arch=sm_75 -rdc=true hello_world.cu -o hello -lcudadevrt
9.4.2. 性能
9.4.2.1. 启用动态并行性的内核开销
在控制动态启动时处于活动状态的系统软件可能会对当时正在运行的任何内核施加开销,无论它是否调用自己的内核启动。这种开销来自设备运行时的执行跟踪和管理软件,并可能导致性能下降。一般来说,对于链接到设备运行时库的应用程序,会产生这种开销。
9.4.3. 实现限制和局限性
动态并行性 保证本文档中描述的所有语义,但是,某些硬件和软件资源是实现相关的,并且限制了使用设备运行时的程序的规模、性能和其他属性。
9.4.3.1. 运行时
9.4.3.1.1. 内存占用
设备运行时系统软件为各种管理目的保留内存,特别是为跟踪挂起的网格启动保留内存。配置控件可用于减少此保留的大小,以换取某些启动限制。有关详细信息,请参阅下面的配置选项。
9.4.3.1.2. 挂起的内核启动
当内核启动时,所有相关的配置和参数数据都会被跟踪,直到内核完成。此数据存储在系统管理的启动池中。
固定大小的启动池的大小可以通过从主机调用 cudaDeviceSetLimit()
并指定 cudaLimitDevRuntimePendingLaunchCount
来配置。
9.4.3.1.3. 配置选项
设备运行时系统软件的资源分配通过主机程序中的 cudaDeviceSetLimit()
API 控制。限制必须在任何内核启动之前设置,并且在 GPU 正在积极运行程序时不得更改。
可以设置以下命名限制
限制 |
行为 |
---|---|
|
控制为缓冲尚未开始执行的内核启动和事件而预留的内存量,这可能是由于未解决的依赖关系或缺乏执行资源。当缓冲区已满时,在设备端内核启动期间尝试分配启动槽将失败并返回 |
|
控制每个 GPU 线程的堆栈大小(以字节为单位)。CUDA 驱动程序会根据需要自动增加每次内核启动的每线程堆栈大小。此大小在每次启动后不会重置回原始值。要将每线程堆栈大小设置为不同的值,可以调用 |
9.4.3.1.4. 内存分配和生命周期
cudaMalloc()
和 cudaFree()
在主机和设备环境之间具有不同的语义。当从主机调用时,cudaMalloc()
从未使用的设备内存中分配一个新的区域。当从设备运行时调用时,这些函数映射到设备端 malloc()
和 free()
。这意味着在设备环境中,总的可分配内存受到设备 malloc()
堆大小的限制,这可能小于可用的未使用设备内存。此外,从主机程序在设备上通过 cudaMalloc()
分配的指针上调用 cudaFree()
是错误的,反之亦然。
主机上的 cudaMalloc() |
设备上的 cudaMalloc() |
|
---|---|---|
主机上的 cudaFree() |
支持 |
不支持 |
设备上的 cudaFree() |
不支持 |
支持 |
分配限制 |
释放设备内存 |
|
9.4.3.1.5. SM ID 和 Warp ID
请注意,在 PTX 中,%smid
和 %warpid
被定义为 volatile 值。设备运行时可能会将线程块重新调度到不同的 SM 上,以便更有效地管理资源。因此,依赖 %smid
或 %warpid
在线程或线程块的生命周期内保持不变是不安全的。
9.4.3.1.6. ECC 错误
CUDA 内核中的代码无法获得 ECC 错误的通知。一旦整个启动树完成,ECC 错误将在主机端报告。在嵌套程序执行期间发生的任何 ECC 错误都将生成异常或继续执行(取决于错误和配置)。
9.5. CDP2 与 CDP1
本节总结了新的 (CDP2) 和旧版 (CDP1) CUDA 动态并行性接口之间的差异,以及它们的兼容性和互操作性。它还展示了如何在计算能力低于 9.0 的设备上选择退出 CDP2 接口。
9.5.1. CDP1 与 CDP2 之间的差异
使用 CDP2 或在计算能力为 9.0 或更高的设备上,设备端显式同步不再可能。必须改用隐式同步(例如尾部启动)。
尝试使用 CDP2 或在计算能力为 9.0 或更高的设备上查询或设置 cudaLimitDevRuntimeSyncDepth
(或 CU_LIMIT_DEV_RUNTIME_SYNC_DEPTH
)会导致 cudaErrorUnsupportedLimit
。
CDP2 不再具有用于不适合固定大小池的挂起启动的虚拟化池。cudaLimitDevRuntimePendingLaunchCount
必须设置得足够大,以避免启动槽耗尽。
对于 CDP2,一次存在的事件总数有限制(请注意,事件仅在启动完成后销毁),等于挂起启动计数的两倍。cudaLimitDevRuntimePendingLaunchCount
必须设置得足够大,以避免事件槽耗尽。
使用 CDP2 或在计算能力为 9.0 或更高的设备上,流按网格跟踪,而不是按线程块跟踪。这允许将工作启动到由另一个线程块创建的流中。尝试使用 CDP1 执行此操作会导致 cudaErrorInvalidValue
。
CDP2 引入了尾部启动 (cudaStreamTailLaunch
) 和 fire-and-forget (cudaStreamFireAndForget
) 命名流。
CDP2 仅在 64 位编译模式下受支持。
9.5.2. 兼容性和互操作性
CDP2 是默认设置。可以使用 -DCUDA_FORCE_CDP1_IF_SUPPORTED
编译函数,以在计算能力低于 9.0 的设备上选择退出使用 CDP2。
使用 CUDA 12.0 及更高版本(默认)的函数编译器 |
使用 CUDA 12.0 之前版本或 CUDA 12.0 及更高版本以及指定的 |
|
---|---|---|
编译 |
如果设备代码引用 |
如果代码引用 |
计算能力 < 9.0 |
使用新接口。 |
使用旧接口。 |
计算能力 9.0 及更高 |
使用新接口。 |
使用新接口。如果函数在设备代码中引用 |
使用 CDP1 和 CDP2 的函数可以在同一上下文中同时加载和运行。CDP1 函数能够使用 CDP1 特定的功能(例如 cudaDeviceSynchronize
),CDP2 函数能够使用 CDP2 特定的功能(例如尾部启动和 fire-and-forget 启动)。
使用 CDP1 的函数无法启动使用 CDP2 的函数,反之亦然。如果将使用 CDP1 的函数在其调用图中包含将使用 CDP2 的函数,反之亦然,则在函数加载期间将导致 cudaErrorCdpVersionMismatch
。
9.6. 旧版 CUDA 动态并行性 (CDP1)
有关文档的 CDP2 版本,请参阅上面的CUDA 动态并行性。
9.6.1. 执行环境和内存模型 (CDP1)
有关文档的 CDP2 版本,请参阅上面的执行环境和内存模型。
9.6.1.1. 执行环境 (CDP1)
有关文档的 CDP2 版本,请参阅上面的执行环境。
CUDA 执行模型基于线程、线程块和网格的原语,内核函数定义了线程块和网格内各个线程执行的程序。当调用内核函数时,网格的属性由执行配置描述,该执行配置在 CUDA 中具有特殊的语法。CUDA 中对动态并行性的支持扩展了配置、启动和同步新网格到在设备上运行的线程的能力。
警告
在 CUDA 11.6 中,不推荐使用从父块显式同步子内核(即在设备代码中使用 cudaDeviceSynchronize()
块),对于 compute_90+ 编译已删除,并计划在未来的 CUDA 版本中完全删除。
9.6.1.1.1. 父网格和子网格 (CDP1)
有关文档的 CDP2 版本,请参阅上面的父网格和子网格。
配置和启动新网格的设备线程属于父级网格,而由调用创建的网格是子级网格。
子网格的调用和完成是正确嵌套的,这意味着在由其线程创建的所有子网格完成之前,父网格不被视为完成。即使调用线程没有显式同步启动的子网格,运行时也保证父网格和子网格之间存在隐式同步。
警告
在 CUDA 11.6 中,不推荐使用从父块显式同步子内核(即在设备代码中使用 cudaDeviceSynchronize()
) ,对于 compute_90+ 编译已删除,并计划在未来的 CUDA 版本中完全删除。

图 30 父子启动嵌套
9.6.1.1.2. CUDA 原语的范围 (CDP1)
有关文档的 CDP2 版本,请参阅上面的CUDA 原语的范围。
在主机和设备上,CUDA 运行时都提供 API 用于启动内核、等待启动的工作完成以及通过流和事件跟踪启动之间的依赖关系。在主机系统中,启动状态和引用流和事件的 CUDA 原语由进程内的所有线程共享;但是,进程独立执行,可能不共享 CUDA 对象。
设备上存在类似的层次结构:启动的内核和 CUDA 对象对线程块中的所有线程都可见,但在线程块之间是独立的。这意味着,例如,流可以由一个线程创建,并由同一线程块中的任何其他线程使用,但不能与任何其他线程块中的线程共享。
9.6.1.1.3. 同步 (CDP1)
有关文档的 CDP2 版本,请参阅上面的同步。
警告
在 CUDA 11.6 中,不推荐使用从父块显式同步子内核(即在设备代码中使用 cudaDeviceSynchronize()
) ,对于 compute_90+ 编译已删除,并计划在未来的 CUDA 版本中完全删除。
来自任何线程的 CUDA 运行时操作(包括内核启动)在线程块中可见。这意味着父网格中的调用线程可以对该线程、线程块中的其他线程启动的网格或在同一线程块内创建的流执行同步。线程块的执行在块中所有线程的所有启动完成之前不被视为完成。如果块中的所有线程在所有子启动完成之前退出,则将自动触发同步操作。
9.6.1.1.4. 流和事件 (CDP1)
有关文档的 CDP2 版本,请参阅上面的流和事件。
CUDA 流和事件允许控制网格启动之间的依赖关系:启动到同一流中的网格按顺序执行,事件可以用于创建流之间的依赖关系。在设备上创建的流和事件具有完全相同的目的。
在网格内创建的流和事件存在于线程块范围内,但在线程块范围外使用时具有未定义的行为。如上所述,线程块启动的所有工作在块退出时都隐式同步;启动到流中的工作也包含在其中,所有依赖关系都得到适当解决。对在线程块范围外修改的流执行操作的行为是未定义的。
在主机上创建的流和事件在任何内核中使用时,行为都是未定义的,就像父网格创建的流和事件在子网格中使用时行为未定义一样。
9.6.1.1.5. 排序和并发 (CDP1)
有关文档的 CDP2 版本,请参阅上面的排序和并发。
来自设备运行时的内核启动排序遵循 CUDA 流排序语义。在线程块中,所有启动到同一流中的内核都按顺序执行。在同一线程块中的多个线程启动到同一流中的情况下,流内的排序取决于块内的线程调度,这可以通过同步原语(如 __syncthreads()
)来控制。
请注意,由于流由线程块内的所有线程共享,因此隐式 NULL 流也被共享。如果线程块中的多个线程启动到隐式流中,则这些启动将按顺序执行。如果需要并发,则应使用显式命名流。
动态并行使程序内更容易表达并发性;但是,设备运行时在 CUDA 执行模型中没有引入新的并发性保证。不能保证设备上任意数量的不同线程块之间并发执行。
缺乏并发保证也扩展到父线程块及其子网格。当父线程块启动子网格时,除非父线程块到达显式同步点(例如 cudaDeviceSynchronize()
),否则不保证子网格开始执行。
警告
在 CUDA 11.6 中,不推荐使用从父块显式同步子内核(即在设备代码中使用 cudaDeviceSynchronize()
) ,对于 compute_90+ 编译已删除,并计划在未来的 CUDA 版本中完全删除。
虽然并发通常很容易实现,但它可能会作为设备配置、应用程序工作负载和运行时调度的函数而变化。因此,依赖不同线程块之间的任何并发是不安全的。
9.6.1.1.6. 设备管理 (CDP1)
有关文档的 CDP2 版本,请参阅上面的设备管理。
设备运行时不支持多 GPU;设备运行时只能在其当前执行的设备上运行。但是,允许查询系统中任何支持 CUDA 的设备的属性。
9.6.1.2. 内存模型 (CDP1)
请参阅上文内存模型,了解 CDP2 版本的文档。
父网格和子网格共享相同的全局和常量内存存储,但具有不同的本地和共享内存。
9.6.1.2.1. 一致性和连贯性 (CDP1)
请参阅上文一致性和连贯性,了解 CDP2 版本的文档。
9.6.1.2.1.1. 全局内存 (CDP1)
请参阅上文全局内存,了解 CDP2 版本的文档。
父网格和子网格可以连贯地访问全局内存,子网格和父网格之间具有弱一致性保证。在子网格执行过程中,有两个时间点其内存视图与父线程完全一致:子网格被父网格调用时,以及子网格完成时(由父线程中的同步 API 调用发出信号)。
警告
在 CUDA 11.6 中,不推荐使用从父块显式同步子内核(即在设备代码中使用 cudaDeviceSynchronize()
) ,对于 compute_90+ 编译已删除,并计划在未来的 CUDA 版本中完全删除。
在子网格调用之前,父线程中的所有全局内存操作对子网格都是可见的。在父网格同步子网格完成后,子网格的所有内存操作对父网格都是可见的。
在以下示例中,执行 child_launch
的子网格仅保证看到在子网格启动之前对 data
所做的修改。由于父线程的线程 0 执行启动,因此子网格将与父线程的线程 0 所见的内存保持一致。由于第一个 __syncthreads()
调用,子网格将看到 data[0]=0
、data[1]=1
、…、data[255]=255
(如果没有 __syncthreads()
调用,则只有 data[0]
保证被子网格看到)。当子网格返回时,线程 0 保证看到其子网格中的线程所做的修改。这些修改只有在第二次 __syncthreads()
调用后才对父网格的其他线程可用。
__global__ void child_launch(int *data) {
data[threadIdx.x] = data[threadIdx.x]+1;
}
__global__ void parent_launch(int *data) {
data[threadIdx.x] = threadIdx.x;
__syncthreads();
if (threadIdx.x == 0) {
child_launch<<< 1, 256 >>>(data);
cudaDeviceSynchronize();
}
__syncthreads();
}
void host_launch(int *data) {
parent_launch<<< 1, 256 >>>(data);
}
9.6.1.2.1.2. 零拷贝内存 (CDP1)
请参阅上文零拷贝内存,了解 CDP2 版本的文档。
零拷贝系统内存具有与全局内存相同的一致性和连贯性保证,并遵循上面详述的语义。内核可能无法分配或释放零拷贝内存,但可以使用从主机程序传入的零拷贝指针。
9.6.1.2.1.3. 常量内存 (CDP1)
请参阅上文常量内存,了解 CDP2 版本的文档。
常量是不可变的,即使在父网格和子网格启动之间也不能从设备修改。也就是说,所有 __constant__
变量的值必须在启动之前从主机设置。常量内存由所有子内核从其各自的父内核自动继承。
从内核线程内部获取常量内存对象的地址与所有 CUDA 程序具有相同的语义,并且自然支持将该指针从父内核传递到子内核或从子内核传递到父内核。
9.6.1.2.1.5. 本地内存 (CDP1)
请参阅上文本地内存,了解 CDP2 版本的文档。
本地内存是执行线程的私有存储,在该线程之外不可见。将本地内存的指针作为启动参数启动子内核是非法的。从子内核解引用此类本地内存指针的结果将是未定义的。
例如,以下是非法的,如果 child_launch
访问 x_array
,则行为未定义
int x_array[10]; // Creates x_array in parent's local memory
child_launch<<< 1, 1 >>>(x_array);
程序员有时很难意识到变量何时被编译器放置到本地内存中。一般规则是,传递给子内核的所有存储都应从全局内存堆中显式分配,可以使用 cudaMalloc()
、new()
或通过在全局作用域中声明 __device__
存储。例如
// Correct - "value" is global storage
__device__ int value;
__device__ void x() {
value = 5;
child<<< 1, 1 >>>(&value);
}
// Invalid - "value" is local storage
__device__ void y() {
int value = 5;
child<<< 1, 1 >>>(&value);
}
9.6.1.2.1.6. 纹理内存 (CDP1)
请参阅上文纹理内存,了解 CDP2 版本的文档。
对纹理映射到的全局内存区域的写入与纹理访问是不连贯的。纹理内存的连贯性在子网格的调用和子网格完成时强制执行。这意味着在子内核启动之前对内存的写入会反映在子内核的纹理内存访问中。同样,子内核对内存的写入将反映在父内核的纹理内存访问中,但仅在父内核同步子内核完成后才反映。父内核和子内核的并发访问可能导致数据不一致。
警告
在 CUDA 11.6 中,不推荐使用从父块显式同步子内核(即在设备代码中使用 cudaDeviceSynchronize()
) ,对于 compute_90+ 编译已删除,并计划在未来的 CUDA 版本中完全删除。
9.6.2. 编程接口 (CDP1)
请参阅上文编程接口,了解 CDP2 版本的文档。
9.6.2.1. CUDA C++ 参考 (CDP1)
请参阅上文CUDA C++ 参考,了解 CDP2 版本的文档。
本节介绍 CUDA C++ 语言扩展中为支持动态并行而进行的更改和添加。
使用 CUDA C++ 进行动态并行性的 CUDA 内核可用的语言接口和 API,称为设备运行时,与主机上可用的 CUDA 运行时 API 非常相似。在可能的情况下,CUDA 运行时 API 的语法和语义已保留,以便于代码重用于可能在主机或设备环境中运行的例程。
与 CUDA C++ 中的所有代码一样,此处概述的 API 和代码是按线程代码。这使每个线程能够对接下来要执行的内核或操作做出唯一的动态决策。块内的线程之间没有同步要求来执行任何提供的设备运行时 API,这使得设备运行时 API 函数可以在任意发散的内核代码中调用而不会死锁。
9.6.2.1.1. 设备端内核启动 (CDP1)
请参阅上文内核启动 API,了解 CDP2 版本的文档。
可以使用标准 CUDA <<< >>> 语法从设备启动内核
kernel_name<<< Dg, Db, Ns, S >>>([kernel arguments]);
Dg
的类型为dim3
,并指定网格的维度和大小Db
的类型为dim3
,并指定每个线程块的维度和大小Ns
的类型为size_t
,指定每个线程块为此调用动态分配的共享内存字节数,并添加到静态分配的内存中。Ns
是一个可选参数,默认为 0。S
的类型为cudaStream_t
,指定与此调用关联的流。流必须在进行调用的同一线程块中分配。S
是一个可选参数,默认为 0。
9.6.2.1.1.1. 启动是异步的 (CDP1)
请参阅上文启动是异步的,了解 CDP2 版本的文档。
与主机端启动相同,所有设备端内核启动相对于启动线程都是异步的。也就是说,<<<>>>
启动命令将立即返回,并且启动线程将继续执行,直到它遇到显式的启动同步点,例如 cudaDeviceSynchronize()
。
警告
在 CUDA 11.6 中,不推荐使用从父块显式同步子内核(即在设备代码中使用 cudaDeviceSynchronize()
) ,对于 compute_90+ 编译已删除,并计划在未来的 CUDA 版本中完全删除。
网格启动被发布到设备,并将独立于父线程执行。子网格可能在启动后的任何时间开始执行,但不保证在启动线程到达显式的启动同步点之前开始执行。
9.6.2.1.1.2. 启动环境配置 (CDP1)
请参阅上文启动环境配置,了解 CDP2 版本的文档。
所有全局设备配置设置(例如,从 cudaDeviceGetCacheConfig()
返回的共享内存和 L1 缓存大小,以及从 cudaDeviceGetLimit()
返回的设备限制)将从父级继承。同样,设备限制(例如堆栈大小)将保持配置状态。
对于主机启动的内核,从主机设置的每个内核配置将优先于全局设置。当从设备启动内核时,也将使用这些配置。无法从设备重新配置内核的环境。
9.6.2.1.2. 流 (CDP1)
请参阅上文流,了解 CDP2 版本的文档。
设备运行时同时提供命名流和未命名 (NULL) 流。命名流可以被线程块内的任何线程使用,但是流句柄不能传递给其他块或子/父内核。换句话说,流应被视为创建它的块的私有流。流句柄不能保证在块之间是唯一的,因此在未分配它的块中使用流句柄将导致未定义的行为。
与主机端启动类似,启动到单独流中的工作可以并发运行,但实际的并发性不能保证。CUDA 编程模型不支持依赖子内核之间并发性的程序,并且将具有未定义的行为。
设备上不支持主机端 NULL 流的跨流屏障语义(有关详细信息,请参见下文)。为了保留与主机运行时的语义兼容性,所有设备流都必须使用 cudaStreamCreateWithFlags()
API 创建,并传递 cudaStreamNonBlocking
标志。cudaStreamCreate()
调用是仅主机运行时的 API,并且将无法为设备编译。
由于设备运行时不支持 cudaStreamSynchronize()
和 cudaStreamQuery()
,因此当应用程序需要知道流启动的子内核已完成时,应改用 cudaDeviceSynchronize()
。
警告
在 CUDA 11.6 中,不推荐使用从父块显式同步子内核(即在设备代码中使用 cudaDeviceSynchronize()
) ,对于 compute_90+ 编译已删除,并计划在未来的 CUDA 版本中完全删除。
9.6.2.1.2.1. 隐式 (NULL) 流 (CDP1)
请参阅上文隐式 (NULL) 流,了解 CDP2 版本的文档。
在主机程序中,未命名 (NULL) 流与其他流具有额外的屏障同步语义(有关详细信息,请参阅默认流)。设备运行时提供一个在块中所有线程之间共享的单个隐式未命名流,但是由于所有命名流都必须使用 cudaStreamNonBlocking
标志创建,因此启动到 NULL 流中的工作不会插入对任何其他流(包括其他线程块的 NULL 流)中待处理工作的隐式依赖。
9.6.2.1.3. 事件 (CDP1)
请参阅上文事件,了解 CDP2 版本的文档。
仅支持 CUDA 事件的流间同步功能。这意味着支持 cudaStreamWaitEvent()
,但不支持 cudaEventSynchronize()
、cudaEventElapsedTime()
和 cudaEventQuery()
。由于不支持 cudaEventElapsedTime()
,因此 cudaEvents 必须通过 cudaEventCreateWithFlags()
创建,并传递 cudaEventDisableTiming
标志。
与所有设备运行时对象一样,事件对象可以在创建它们的线程块内的所有线程之间共享,但对于该块是本地的,并且不能传递给其他内核,或同一内核中的块之间。事件句柄不能保证在块之间是唯一的,因此在未创建它的块中使用事件句柄将导致未定义的行为。
9.6.2.1.4. 同步 (CDP1)
请参阅上文同步,了解 CDP2 版本的文档。
警告
在 CUDA 11.6 中,不推荐使用从父块显式同步子内核(即在设备代码中使用 cudaDeviceSynchronize()
) ,对于 compute_90+ 编译已删除,并计划在未来的 CUDA 版本中完全删除。
cudaDeviceSynchronize()
函数将同步线程块中任何线程在调用 cudaDeviceSynchronize()
之前启动的所有工作。请注意,可以从发散代码中调用 cudaDeviceSynchronize()
(请参阅块范围同步 (CDP1))。
如果调用线程旨在与从其他线程调用的子网格同步,则程序有责任执行足够的额外线程间同步,例如通过调用 __syncthreads()
。
9.6.2.1.4.1. 块范围同步 (CDP1)
有关文档的 CDP2 版本,请参阅上面的CUDA 动态并行性。
cudaDeviceSynchronize()
函数不暗示块内同步。特别是,如果没有通过 __syncthreads()
指令进行显式同步,则调用线程不能对除自身之外的任何线程已启动的工作做出任何假设。例如,如果一个块内的多个线程都在启动工作,并且希望一次同步所有这些工作(可能是因为基于事件的依赖关系),则程序有责任保证在调用 cudaDeviceSynchronize()
之前,所有线程都提交了这项工作。
由于实现允许同步块中任何线程的启动,因此多个线程同时调用 cudaDeviceSynchronize()
很可能会在第一次调用中耗尽所有工作,然后在后续调用中不起作用。
9.6.2.1.5. 设备管理 (CDP1)
有关文档的 CDP2 版本,请参阅上面的设备管理。
只有内核正在运行的设备才能从该内核进行控制。这意味着设备运行时不支持诸如 cudaSetDevice()
之类的设备 API。从 GPU 看到的活动设备(从 cudaGetDevice()
返回)将具有与从主机系统看到的设备编号相同的设备编号。cudaDeviceGetAttribute()
调用可以请求有关另一个设备的信息,因为此 API 允许指定设备 ID 作为调用的参数。请注意,设备运行时不提供通用的 cudaGetDeviceProperties()
API - 必须单独查询属性。
9.6.2.1.6. 内存声明 (CDP1)
请参阅上文内存声明,了解 CDP2 版本的文档。
9.6.2.1.6.1. 设备内存和常量内存 (CDP1)
请参阅上文设备内存和常量内存,了解 CDP2 版本的文档。
在使用设备运行时时,使用 __device__
或 __constant__
内存空间说明符在文件作用域中声明的内存的行为相同。所有内核都可以读取或写入设备变量,无论内核最初是由主机还是设备运行时启动的。同样,所有内核都将具有与模块作用域中声明的 __constant__
相同的视图。
9.6.2.1.6.2. 纹理和表面 (CDP1)
请参阅上文纹理和表面,了解 CDP2 版本的文档。
CUDA 支持动态创建的纹理和表面对象14,其中纹理对象可以在主机上创建,传递给内核,由该内核使用,然后从主机销毁。设备运行时不允许从设备代码内部创建或销毁纹理或表面对象,但是从主机创建的纹理和表面对象可以在设备上自由使用和传递。无论在哪里创建,动态创建的纹理对象始终有效,并且可以从父内核传递到子内核。
注意
设备运行时不支持从设备启动的内核中的传统模块作用域(即,Fermi 风格)纹理和表面。模块作用域(传统)纹理可以从主机创建并在设备代码中使用,就像任何内核一样,但只能由顶层内核(即,从主机启动的内核)使用。
9.6.2.1.6.4. 符号地址 (CDP1)
请参阅上文符号地址,了解 CDP2 版本的文档。
设备端符号(即,标记为 __device__
的符号)可以从内核内部简单地通过 &
运算符引用,因为所有全局作用域设备变量都在内核的可见地址空间中。这也适用于 __constant__
符号,尽管在这种情况下,指针将引用只读数据。
鉴于设备端符号可以直接引用,因此引用符号的 CUDA 运行时 API(例如,cudaMemcpyToSymbol()
或 cudaGetSymbolAddress()
)是冗余的,因此设备运行时不支持。请注意,这意味着即使在子内核启动之前,也不能从正在运行的内核内部更改常量数据,因为对 __constant__
空间的引用是只读的。
9.6.2.1.7. API 错误和启动失败 (CDP1)
请参阅上文API 错误和启动失败,了解 CDP2 版本的文档。
与 CUDA 运行时通常一样,任何函数都可能返回错误代码。记录返回的最后一个错误代码,并且可以通过 cudaGetLastError()
调用检索。错误是按线程记录的,因此每个线程都可以识别它生成的最新错误。错误代码的类型为 cudaError_t
。
与主机端启动类似,设备端启动可能因多种原因而失败(无效参数等等)。用户必须调用 cudaGetLastError()
来确定启动是否生成了错误,但是启动后没有错误并不意味着子内核已成功完成。
对于设备端异常,例如,访问无效地址,子网格中的错误将返回给主机,而不是由父内核调用 cudaDeviceSynchronize()
返回。
9.6.2.1.7.1. 启动设置 API (CDP1)
请参阅上文启动设置 API,了解 CDP2 版本的文档。
内核启动是一种系统级机制,通过设备运行时库公开,因此可以直接从 PTX 通过底层的 cudaGetParameterBuffer()
和 cudaLaunchDevice()
API 访问。CUDA 应用程序可以自行调用这些 API,其要求与 PTX 相同。在这两种情况下,用户都需要负责根据规范以正确的格式正确填充所有必要的数据结构。这些数据结构中保证向后兼容性。
与主机端启动一样,设备端操作符 <<<>>>
映射到下层的内核启动 API。这是为了让以 PTX 为目标的用户能够执行启动,并使编译器前端可以将 <<<>>>
转换为这些调用。
运行时 API 启动函数 |
与主机运行时行为差异的描述(如果没有描述,则行为相同) |
---|---|
|
从 |
|
从 |
这些启动函数的 API 与 CUDA 运行时 API 的 API 不同,定义如下
extern device cudaError_t cudaGetParameterBuffer(void **params);
extern __device__ cudaError_t cudaLaunchDevice(void *kernel,
void *params, dim3 gridDim,
dim3 blockDim,
unsigned int sharedMemSize = 0,
cudaStream_t stream = 0);
9.6.2.1.8. API 参考 (CDP1)
请参阅上文API 参考,了解 CDP2 版本的文档。
此处详细介绍了设备运行时中支持的 CUDA 运行时 API 的部分。主机和设备运行时 API 具有相同的语法;语义相同,除非另有说明。下表概述了相对于主机可用版本的 API。
运行时 API 函数 |
详细信息 |
---|---|
|
仅同步从线程自身块启动的工作。 警告:请注意,从设备代码调用此 API 在 CUDA 11.6 中已弃用,为 compute_90+ 编译而删除,并计划在未来的 CUDA 版本中完全删除。 |
|
|
|
|
|
最后一个错误是每线程状态,而不是每块状态 |
|
|
|
|
|
|
|
将返回任何设备的属性 |
|
始终返回当前设备 ID,如同从主机端看到的那样 |
|
必须传递 |
|
|
|
|
|
必须传递 |
|
|
|
|
|
|
|
关于所有
|
|
|
|
|
|
|
|
|
|
|
|
|
|
可能不会在设备上对主机上创建的指针调用 |
|
|
|
|
|
|
|
9.6.2.2. 从 PTX 进行设备端启动 (CDP1)
请参阅上文从 PTX 进行设备端启动,了解 CDP2 版本的文档。
本节适用于以并行线程执行 (PTX) 为目标并计划在其语言中支持动态并行性的编程语言和编译器实现者。它提供了与在 PTX 级别支持内核启动相关的底层细节。
9.6.2.2.1. 内核启动 API (CDP1)
请参阅上文内核启动 API,了解 CDP2 版本的文档。
可以使用以下两个可从 PTX 访问的 API 实现设备端内核启动:cudaLaunchDevice()
和 cudaGetParameterBuffer()
。cudaLaunchDevice()
使用通过调用 cudaGetParameterBuffer()
获取并填充了已启动内核参数的参数缓冲区来启动指定的内核。如果启动的内核不带任何参数,则参数缓冲区可以为 NULL,即无需调用 cudaGetParameterBuffer()
。
9.6.2.2.1.1. cudaLaunchDevice (CDP1)
请参阅上文cudaLaunchDevice,了解 CDP2 版本的文档。
在 PTX 级别,cudaLaunchDevice()
需要在使用前以下面两种形式之一声明。
// PTX-level Declaration of cudaLaunchDevice() when .address_size is 64
.extern .func(.param .b32 func_retval0) cudaLaunchDevice
(
.param .b64 func,
.param .b64 parameterBuffer,
.param .align 4 .b8 gridDimension[12],
.param .align 4 .b8 blockDimension[12],
.param .b32 sharedMemSize,
.param .b64 stream
)
;
// PTX-level Declaration of cudaLaunchDevice() when .address_size is 32
.extern .func(.param .b32 func_retval0) cudaLaunchDevice
(
.param .b32 func,
.param .b32 parameterBuffer,
.param .align 4 .b8 gridDimension[12],
.param .align 4 .b8 blockDimension[12],
.param .b32 sharedMemSize,
.param .b32 stream
)
;
下面的 CUDA 级别声明映射到上述 PTX 级别声明之一,并在系统头文件 cuda_device_runtime_api.h
中找到。该函数在 cudadevrt
系统库中定义,该库必须与程序链接才能使用设备端内核启动功能。
// CUDA-level declaration of cudaLaunchDevice()
extern "C" __device__
cudaError_t cudaLaunchDevice(void *func, void *parameterBuffer,
dim3 gridDimension, dim3 blockDimension,
unsigned int sharedMemSize,
cudaStream_t stream);
第一个参数是指向要启动的内核的指针,第二个参数是参数缓冲区,其中包含启动内核的实际参数。参数缓冲区的布局在下文参数缓冲区布局 (CDP1)中进行了解释。其他参数指定启动配置,即网格维度、块维度、共享内存大小以及与启动关联的流(有关启动配置的详细描述,请参阅执行配置)。
9.6.2.2.1.2. cudaGetParameterBuffer (CDP1)
请参阅上文cudaGetParameterBuffer,了解 CDP2 版本的文档。
cudaGetParameterBuffer()
需要在 PTX 级别使用前声明。PTX 级别声明必须采用下面给出的两种形式之一,具体取决于地址大小
// PTX-level Declaration of cudaGetParameterBuffer() when .address_size is 64
// When .address_size is 64
.extern .func(.param .b64 func_retval0) cudaGetParameterBuffer
(
.param .b64 alignment,
.param .b64 size
)
;
// PTX-level Declaration of cudaGetParameterBuffer() when .address_size is 32
.extern .func(.param .b32 func_retval0) cudaGetParameterBuffer
(
.param .b32 alignment,
.param .b32 size
)
;
以下 cudaGetParameterBuffer()
的 CUDA 级别声明映射到上述 PTX 级别声明
// CUDA-level Declaration of cudaGetParameterBuffer()
extern "C" __device__
void *cudaGetParameterBuffer(size_t alignment, size_t size);
第一个参数指定参数缓冲区的对齐要求,第二个参数指定字节大小要求。在当前实现中,cudaGetParameterBuffer()
返回的参数缓冲区始终保证为 64 字节对齐,并且对齐要求参数将被忽略。但是,建议将正确的对齐要求值(即要放置在参数缓冲区中的任何参数的最大对齐方式)传递给 cudaGetParameterBuffer()
,以确保未来的可移植性。
9.6.2.2.2. 参数缓冲区布局 (CDP1)
请参阅上文参数缓冲区布局,了解 CDP2 版本的文档。
禁止在参数缓冲区中进行参数重新排序,并且参数缓冲区中放置的每个单独参数都需要对齐。也就是说,每个参数都必须放置在参数缓冲区中的第 nth 个字节处,其中 n 是参数大小的最小倍数,该倍数大于前一个参数占用的最后一个字节的偏移量。参数缓冲区的最大大小为 4KB。
有关 CUDA 编译器生成的 PTX 代码的更详细描述,请参阅 PTX-3.5 规范。
9.6.2.3. 动态并行性的工具包支持 (CDP1)
请参阅上文动态并行性的工具包支持,了解 CDP2 版本的文档。
9.6.2.3.1. 在 CUDA 代码中包含设备运行时 API (CDP1)
请参阅上文在 CUDA 代码中包含设备运行时 API,了解 CDP2 版本的文档。
与主机端运行时 API 类似,CUDA 设备运行时 API 的原型在程序编译期间自动包含。无需显式包含 cuda_device_runtime_api.h
。
9.6.2.3.2. 编译和链接 (CDP1)
请参阅上文编译和链接,了解 CDP2 版本的文档。
当使用 nvcc
编译和链接使用动态并行性的 CUDA 程序时,该程序将自动链接到静态设备运行时库 libcudadevrt
。
设备运行时作为静态库(Windows 上的 cudadevrt.lib
,Linux 下的 libcudadevrt.a
)提供,使用设备运行时的 GPU 应用程序必须链接到该库。设备库的链接可以通过 nvcc
和/或 nvlink
完成。下面显示了两个简单的示例。
如果可以从命令行指定所有必需的源文件,则可以在一个步骤中编译和链接设备运行时程序
$ nvcc -arch=sm_75 -rdc=true hello_world.cu -o hello -lcudadevrt
也可以先将 CUDA .cu 源文件编译为目标文件,然后在两阶段过程中将它们链接在一起
$ nvcc -arch=sm_75 -dc hello_world.cu -o hello_world.o
$ nvcc -arch=sm_75 -rdc=true hello_world.o -o hello -lcudadevrt
有关更多详细信息,请参阅《CUDA 驱动程序编译器 NVCC 指南》的“使用单独编译”部分。
9.6.3. 编程指南 (CDP1)
请参阅上文编程指南,了解 CDP2 版本的文档。
9.6.3.1. 基础知识 (CDP1)
请参阅上文基础知识,了解 CDP2 版本的文档。
设备运行时是主机运行时的功能子集。API 级别的设备管理、内核启动、设备 memcpy、流管理和事件管理都从设备运行时公开。
对于已经有 CUDA 经验的人来说,设备运行时的编程应该很熟悉。设备运行时语法和语义与主机 API 的语法和语义基本相同,任何例外情况都在本文档前面详细说明。
警告
在 CUDA 11.6 中,不推荐使用从父块显式同步子内核(即在设备代码中使用 cudaDeviceSynchronize()
) ,对于 compute_90+ 编译已删除,并计划在未来的 CUDA 版本中完全删除。
以下示例显示了一个简单的 Hello World 程序,其中包含动态并行性
#include <stdio.h>
__global__ void childKernel()
{
printf("Hello ");
}
__global__ void parentKernel()
{
// launch child
childKernel<<<1,1>>>();
if (cudaSuccess != cudaGetLastError()) {
return;
}
// wait for child to complete
if (cudaSuccess != cudaDeviceSynchronize()) {
return;
}
printf("World!\n");
}
int main(int argc, char *argv[])
{
// launch parent
parentKernel<<<1,1>>>();
if (cudaSuccess != cudaGetLastError()) {
return 1;
}
// wait for parent to complete
if (cudaSuccess != cudaDeviceSynchronize()) {
return 2;
}
return 0;
}
此程序可以从命令行在一个步骤中构建,如下所示
$ nvcc -arch=sm_75 -rdc=true hello_world.cu -o hello -lcudadevrt
9.6.3.2. 性能 (CDP1)
请参阅上文性能,了解 CDP2 版本的文档。
9.6.3.2.1. 同步 (CDP1)
有关文档的 CDP2 版本,请参阅上面的CUDA 动态并行性。
警告
从父块显式同步子内核(例如在设备代码中使用 cudaDeviceSynchronize()
)在 CUDA 11.6 中已弃用,为 compute_90+ 编译而删除,并计划在未来的 CUDA 版本中完全删除。
一个线程的同步可能会影响同一线程块中其他线程的性能,即使那些其他线程自身不调用 cudaDeviceSynchronize()
。这种影响将取决于底层实现。一般来说,与显式调用 cudaDeviceSynchronize()
相比,线程块结束时完成的子内核的隐式同步效率更高。因此,建议仅在需要在线程块结束之前与子内核同步时才调用 cudaDeviceSynchronize()
。
9.6.3.2.2. 启用动态并行性的内核开销 (CDP1)
请参阅上文启用动态并行性的内核开销,了解 CDP2 版本的文档。
在控制动态启动时处于活动状态的系统软件可能会对当时正在运行的任何内核施加开销,无论它是否调用自身的内核启动。这种开销来自设备运行时的执行跟踪和管理软件,并可能导致性能下降,例如,与从主机端进行库调用相比,从设备进行库调用时性能下降。一般来说,对于链接到设备运行时库的应用程序,会产生这种开销。
9.6.3.3. 实现限制和局限性 (CDP1)
请参阅上文实现限制和局限性,了解 CDP2 版本的文档。
动态并行性 保证本文档中描述的所有语义,但是,某些硬件和软件资源是实现相关的,并且限制了使用设备运行时的程序的规模、性能和其他属性。
9.6.3.3.1. 运行时 (CDP1)
请参阅上文运行时,了解 CDP2 版本的文档。
9.6.3.3.1.1. 内存占用 (CDP1)
请参阅上文内存占用,了解 CDP2 版本的文档。
设备运行时系统软件为各种管理目的保留内存,特别是用于在同步期间保存父网格状态的一个预留,以及用于跟踪待处理网格启动的第二个预留。配置控件可用于减少这些预留的大小,以换取某些启动限制。有关详细信息,请参阅下文配置选项 (CDP1)。
大多数保留内存作为父内核状态的后备存储分配,用于同步子启动时。保守地讲,此内存必须支持存储设备上可能存在的最大活动线程数的状态。这意味着每个可以调用 cudaDeviceSynchronize()
的父代都可能需要高达 860MB 的设备内存,具体取决于设备配置,即使并非所有内存都被消耗,程序也无法使用。
9.6.3.3.1.2. 嵌套和同步深度 (CDP1)
有关文档的 CDP2 版本,请参阅上面的CUDA 动态并行性。
使用设备运行时,一个内核可以启动另一个内核,而该内核又可以启动另一个内核,依此类推。每个从属启动都被视为一个新的嵌套级别,级别的总数是程序的嵌套深度。同步深度定义为程序将显式同步子启动的最深级别。通常,这比程序的嵌套深度小 1,但是如果程序不需要在所有级别都调用 cudaDeviceSynchronize()
,则同步深度可能与嵌套深度大不相同。
警告
在 CUDA 11.6 中,不推荐使用从父块显式同步子内核(即在设备代码中使用 cudaDeviceSynchronize()
) ,对于 compute_90+ 编译已删除,并计划在未来的 CUDA 版本中完全删除。
总的最大嵌套深度限制为 24,但实际上,真正的限制将是系统为每个新级别所需的内存量(请参阅上文内存占用 (CDP1))。任何导致内核深度超过最大深度的启动都会失败。请注意,这也可能适用于 cudaMemcpyAsync()
,它本身可能会生成内核启动。有关详细信息,请参阅配置选项 (CDP1)。
默认情况下,为两个同步级别保留了足够的存储空间。可以通过调用 cudaDeviceSetLimit()
并指定 cudaLimitDevRuntimeSyncDepth
来控制此最大同步深度(以及因此保留的存储空间)。必须在从主机启动顶级内核之前配置要支持的级别数,以保证嵌套程序成功执行。在大于指定最大同步深度的深度调用 cudaDeviceSynchronize()
将返回错误。
允许进行优化,即在父内核从不调用 cudaDeviceSynchronize()
的情况下,系统检测到它不需要为父内核的状态保留空间。在这种情况下,由于永远不会发生显式的父/子同步,因此程序所需的内存占用将远小于保守的最大值。这样的程序可以指定较浅的最大同步深度,以避免过度分配后备存储。
9.6.3.3.1.3. 待处理内核启动 (CDP1)
请参阅上文待处理内核启动,了解 CDP2 版本的文档。
当内核启动时,所有相关的配置和参数数据都会被跟踪,直到内核完成。此数据存储在系统管理的启动池中。
启动池分为固定大小池和性能较低的虚拟化池。设备运行时系统软件将首先尝试在固定大小池中跟踪启动数据。当固定大小池已满时,虚拟化池将用于跟踪新的启动。
固定大小的启动池的大小可以通过从主机调用 cudaDeviceSetLimit()
并指定 cudaLimitDevRuntimePendingLaunchCount
来配置。
9.6.3.3.1.4. 配置选项 (CDP1)
请参阅上文配置选项,了解 CDP2 版本的文档。
设备运行时系统软件的资源分配通过主机程序中的 cudaDeviceSetLimit()
API 控制。限制必须在任何内核启动之前设置,并且在 GPU 正在积极运行程序时不得更改。
警告
在 CUDA 11.6 中,不推荐使用从父块显式同步子内核(即在设备代码中使用 cudaDeviceSynchronize()
) ,对于 compute_90+ 编译已删除,并计划在未来的 CUDA 版本中完全删除。
可以设置以下命名限制
限制 |
行为 |
---|---|
|
设置可以调用 |
|
控制为缓冲尚未开始执行的内核启动而预留的内存量,这可能是由于未解决的依赖关系或缺乏执行资源。当缓冲区已满时,设备运行时系统软件将尝试在性能较低的虚拟化缓冲区中跟踪新的待处理启动。如果虚拟化缓冲区也已满,即当所有可用的堆空间都被消耗时,启动将不会发生,并且线程的最后一个错误将被设置为 |
|
控制每个 GPU 线程的堆栈大小(以字节为单位)。CUDA 驱动程序会根据需要自动增加每次内核启动的每线程堆栈大小。此大小在每次启动后不会重置回原始值。要将每线程堆栈大小设置为不同的值,可以调用 |
9.6.3.3.1.5. 内存分配和生命周期 (CDP1)
请参阅上文内存分配和生命周期,了解 CDP2 版本的文档。
cudaMalloc()
和 cudaFree()
在主机和设备环境之间具有不同的语义。当从主机调用时,cudaMalloc()
从未使用的设备内存中分配一个新的区域。当从设备运行时调用时,这些函数映射到设备端 malloc()
和 free()
。这意味着在设备环境中,总的可分配内存受到设备 malloc()
堆大小的限制,这可能小于可用的未使用设备内存。此外,从主机程序在设备上通过 cudaMalloc()
分配的指针上调用 cudaFree()
是错误的,反之亦然。
主机上的 cudaMalloc() |
设备上的 cudaMalloc() |
|
---|---|---|
主机上的 cudaFree() |
支持 |
不支持 |
设备上的 cudaFree() |
不支持 |
支持 |
分配限制 |
释放设备内存 |
|
9.6.3.3.1.6. SM Id 和 Warp Id (CDP1)
请参阅上文SM Id 和 Warp Id,了解 CDP2 版本的文档。
请注意,在 PTX 中,%smid
和 %warpid
被定义为 volatile 值。设备运行时可能会将线程块重新调度到不同的 SM 上,以便更有效地管理资源。因此,依赖 %smid
或 %warpid
在线程或线程块的生命周期内保持不变是不安全的。
9.6.3.3.1.7. ECC 错误 (CDP1)
有关 CDP2 版本的文档,请参阅上文的 ECC 错误。
CUDA 内核中的代码无法获得 ECC 错误的通知。一旦整个启动树完成,ECC 错误将在主机端报告。在嵌套程序执行期间发生的任何 ECC 错误都将生成异常或继续执行(取决于错误和配置)。
10. 虚拟内存管理
10.1. 简介
虚拟内存管理 API 提供了一种方法,使应用程序可以直接管理 CUDA 提供的统一虚拟地址空间,从而将物理内存映射到 GPU 可访问的虚拟地址。这些 API 在 CUDA 10.2 中引入,还提供了一种与其他进程和图形 API(如 OpenGL 和 Vulkan)进行互操作的新方法,以及用户可以调整以适应其应用程序的更新的内存属性。
从历史上看,CUDA 编程模型中的内存分配调用(例如 cudaMalloc()
)返回的内存地址指向 GPU 内存。由此获得的地址可以与任何 CUDA API 或设备内核内部一起使用。但是,已分配的内存无法根据用户的内存需求调整大小。为了增加分配的大小,用户必须显式分配更大的缓冲区,从初始分配中复制数据,释放它,然后继续跟踪较新分配的地址。这通常会导致应用程序的性能降低和峰值内存利用率更高。本质上,用户拥有一个类似 malloc 的接口来分配 GPU 内存,但没有相应的 realloc 来补充它。虚拟内存管理 API 将地址和内存的概念分离,并允许应用程序分别处理它们。这些 API 允许应用程序根据需要从虚拟地址范围映射和取消映射内存。
在启用对等设备访问内存分配(通过使用 cudaEnablePeerAccess
)的情况下,所有过去和未来的用户分配都将映射到目标对等设备。这导致用户不知不觉地为将所有 cudaMalloc 分配映射到对等设备的运行时成本付费。但是,在大多数情况下,应用程序仅通过与其他设备共享少量分配进行通信,并非所有分配都需要映射到所有设备。通过虚拟内存管理,应用程序可以专门选择某些分配以使其可以从目标设备访问。
CUDA 虚拟内存管理 API 向用户公开了对应用程序中 GPU 内存进行精细控制的功能。它提供了 API,使用户可以
将分配在不同设备上的内存放置到连续的 VA 范围中。
使用特定于平台的机制执行用于内存共享的进程间通信。
选择支持它们的设备上的较新内存类型。
为了分配内存,虚拟内存管理编程模型公开了以下功能
分配物理内存。
保留 VA 范围。
将已分配的内存映射到 VA 范围。
控制映射范围的访问权限。
请注意,本节中描述的 API 套件需要支持 UVA 的系统。
10.2. 查询支持
在尝试使用虚拟内存管理 API 之前,应用程序必须确保它们要使用的设备支持 CUDA 虚拟内存管理。以下代码示例显示了查询虚拟内存管理支持
int deviceSupportsVmm;
CUresult result = cuDeviceGetAttribute(&deviceSupportsVmm, CU_DEVICE_ATTRIBUTE_VIRTUAL_MEMORY_MANAGEMENT_SUPPORTED, device);
if (deviceSupportsVmm != 0) {
// `device` supports Virtual Memory Management
}
10.3. 分配物理内存
使用虚拟内存管理 API 进行内存分配的第一步是创建一个物理内存块,该块将为分配提供后备。为了分配物理内存,应用程序必须使用 cuMemCreate
API。此函数创建的分配没有任何设备或主机映射。函数参数 CUmemGenericAllocationHandle
描述了要分配的内存的属性,例如分配的位置、分配是否要共享到另一个进程(或其他图形 API),或要分配的内存的物理属性。用户必须确保请求的分配大小必须与适当的粒度对齐。有关分配粒度要求的信息可以使用 cuMemGetAllocationGranularity
查询。以下代码片段显示了使用 cuMemCreate
分配物理内存
CUmemGenericAllocationHandle allocatePhysicalMemory(int device, size_t size) {
CUmemAllocationProp prop = {};
prop.type = CU_MEM_ALLOCATION_TYPE_PINNED;
prop.location.type = CU_MEM_LOCATION_TYPE_DEVICE;
prop.location.id = device;
size_t granularity = 0;
cuMemGetAllocationGranularity(&granularity, &prop, CU_MEM_ALLOC_GRANULARITY_MINIMUM);
// Ensure size matches granularity requirements for the allocation
size_t padded_size = ROUND_UP(size, granularity);
// Allocate physical memory
CUmemGenericAllocationHandle allocHandle;
cuMemCreate(&allocHandle, padded_size, &prop, 0);
return allocHandle;
}
cuMemCreate
分配的内存由它返回的 CUmemGenericAllocationHandle
引用。这与 cudaMalloc 风格的分配不同,后者返回指向 GPU 内存的指针,CUDA 内核可以直接在设备上访问该指针。分配的内存不能用于任何操作,除了使用 cuMemGetAllocationPropertiesFromHandle
查询属性。为了使此内存可访问,应用程序必须将此内存映射到由 cuMemAddressReserve
保留的 VA 范围中,并为其提供适当的访问权限。应用程序必须使用 cuMemRelease
API 释放已分配的内存。
10.3.2. 内存类型
在 CUDA 10.2 之前,应用程序无法通过用户控制的方式分配某些设备可能支持的任何特殊类型的内存。通过 cuMemCreate
,应用程序还可以使用 CUmemAllocationProp::allocFlags
指定内存类型要求,以选择任何特定的内存功能。应用程序还必须确保请求的内存类型在分配设备上受支持。
10.3.2.1. 可压缩内存
可压缩内存可用于加速访问具有非结构化稀疏性和其他可压缩数据模式的数据。根据正在操作的数据,压缩可以节省 DRAM 带宽、L2 读取带宽和 L2 容量。想要在支持计算数据压缩的设备上分配可压缩内存的应用程序可以通过将 CUmemAllocationProp::allocFlags::compressionType
设置为 CU_MEM_ALLOCATION_COMP_GENERIC
来实现。用户必须使用 CU_DEVICE_ATTRIBUTE_GENERIC_COMPRESSION_SUPPORTED
查询设备是否支持计算数据压缩。以下代码片段说明了查询可压缩内存支持 cuDeviceGetAttribute
。
int compressionSupported = 0;
cuDeviceGetAttribute(&compressionSupported, CU_DEVICE_ATTRIBUTE_GENERIC_COMPRESSION_SUPPORTED, device);
在支持计算数据压缩的设备上,用户必须在分配时选择加入,如下所示
prop.allocFlags.compressionType = CU_MEM_ALLOCATION_COMP_GENERIC;
由于各种原因(例如有限的硬件资源),分配可能没有压缩属性,因此用户应使用 cuMemGetAllocationPropertiesFromHandle
查询回已分配内存的属性,并检查压缩属性。
CUmemAllocationProp allocationProp = {};
cuMemGetAllocationPropertiesFromHandle(&allocationProp, allocationHandle);
if (allocationProp.allocFlags.compressionType == CU_MEM_ALLOCATION_COMP_GENERIC)
{
// Obtained compressible memory allocation
}
10.4. 保留虚拟地址范围
由于通过虚拟内存管理,地址和内存的概念是不同的,因此应用程序必须划分出一个地址范围,该范围可以容纳由 cuMemCreate
进行的内存分配。保留的地址范围必须至少与用户计划放置在其中的所有物理内存分配的大小总和一样大。
应用程序可以通过将适当的参数传递给 cuMemAddressReserve
来保留虚拟地址范围。获得的地址范围将没有任何设备或主机物理内存与之关联。保留的虚拟地址范围可以映射到系统中任何设备的内存块,从而为应用程序提供由属于不同设备的内存支持和映射的连续 VA 范围。应用程序应使用 cuMemAddressFree
将虚拟地址范围返回给 CUDA。用户必须确保在调用 cuMemAddressFree
之前取消映射整个 VA 范围。这些函数在概念上类似于 mmap/munmap(在 Linux 上)或 VirtualAlloc/VirtualFree(在 Windows 上)函数。以下代码片段说明了该函数的用法
CUdeviceptr ptr;
// `ptr` holds the returned start of virtual address range reserved.
CUresult result = cuMemAddressReserve(&ptr, size, 0, 0, 0); // alignment = 0 for default alignment
10.5. 虚拟别名支持
虚拟内存管理 API 提供了一种通过多次调用具有不同虚拟地址的 cuMemMap
来创建同一分配的多个虚拟内存映射或“代理”的方法,即所谓的虚拟别名。除非 PTX ISA 中另有说明,否则对分配的一个代理的写入被认为与同一内存的任何其他代理不一致且不连贯,直到写入设备操作(网格启动、memcpy、memset 等)完成。在写入设备操作之前存在于 GPU 上但在写入设备操作完成后读取的网格也被认为具有不一致且不连贯的代理。
例如,以下代码片段被认为是未定义的,假设设备指针 A 和 B 是同一内存分配的虚拟别名
__global__ void foo(char *A, char *B) {
*A = 0x1;
printf("%d\n", *B); // Undefined behavior! *B can take on either
// the previous value or some value in-between.
}
以下是已定义的行为,假设这两个内核按单调顺序排列(按流或事件)。
__global__ void foo1(char *A) {
*A = 0x1;
}
__global__ void foo2(char *B) {
printf("%d\n", *B); // *B == *A == 0x1 assuming foo2 waits for foo1
// to complete before launching
}
cudaMemcpyAsync(B, input, size, stream1); // Aliases are allowed at
// operation boundaries
foo1<<<1,1,0,stream1>>>(A); // allowing foo1 to access A.
cudaEventRecord(event, stream1);
cudaStreamWaitEvent(stream2, event);
foo2<<<1,1,0,stream2>>>(B);
cudaStreamWaitEvent(stream3, event);
cudaMemcpyAsync(output, B, size, stream3); // Both launches of foo2 and
// cudaMemcpy (which both
// read) wait for foo1 (which writes)
// to complete before proceeding
如果需要在同一内核中通过不同的“代理”访问同一分配,则可以在两次访问之间使用 fence.proxy.alias
。因此,可以使用内联 PTX 汇编使上述示例合法
__global__ void foo(char *A, char *B) {
*A = 0x1;
asm volatile ("fence.proxy.alias;" ::: "memory");
printf("%d\n", *B); // *B == *A == 0x1
}
10.6. 映射内存
前两节中分配的物理内存和划分的虚拟地址空间表示虚拟内存管理 API 引入的内存和地址的区别。为了使分配的内存可用,用户必须首先将内存放置在地址空间中。从 cuMemAddressReserve
获得的地址范围和从 cuMemCreate
或 cuMemImportFromShareableHandle
获得的物理分配必须通过使用 cuMemMap
相互关联。
只要用户划分了足够的地址空间,用户就可以将来自多个设备的分配关联起来以驻留在连续的虚拟地址范围中。为了分离物理分配和地址范围,用户必须使用 cuMemUnmap
取消映射映射的地址。用户可以根据需要多次将内存映射和取消映射到同一地址范围,只要他们确保不尝试在已映射的 VA 范围保留上创建映射即可。以下代码片段说明了该函数的用法
CUdeviceptr ptr;
// `ptr`: address in the address range previously reserved by cuMemAddressReserve.
// `allocHandle`: CUmemGenericAllocationHandle obtained by a previous call to cuMemCreate.
CUresult result = cuMemMap(ptr, size, 0, allocHandle, 0);
10.7. 控制访问权限
虚拟内存管理 API 使应用程序能够使用访问控制机制显式保护其 VA 范围。使用 cuMemMap
将分配映射到地址范围的区域不会使地址可访问,如果 CUDA 内核访问该地址,将导致程序崩溃。用户必须专门使用 cuMemSetAccess
函数选择访问控制,该函数允许或限制特定设备对映射地址范围的访问。以下代码片段说明了该函数的用法
void setAccessOnDevice(int device, CUdeviceptr ptr, size_t size) {
CUmemAccessDesc accessDesc = {};
accessDesc.location.type = CU_MEM_LOCATION_TYPE_DEVICE;
accessDesc.location.id = device;
accessDesc.flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE;
// Make the address accessible
cuMemSetAccess(ptr, size, &accessDesc, 1);
}
通过虚拟内存管理公开的访问控制机制允许用户显式声明他们想要与其他对等设备共享系统上的哪些分配。如前所述,cudaEnablePeerAccess
强制将所有先前和未来的 cudaMalloc'd 分配映射到目标对等设备。在许多情况下,这可能很方便,因为用户不必担心跟踪系统中每个设备上每个分配的映射状态。但是对于关注其应用程序性能的用户来说,这种方法具有性能影响。通过分配粒度的访问控制,虚拟内存管理公开了一种以最小开销进行对等映射的机制。
vectorAddMMAP
示例可以用作使用虚拟内存管理 API 的示例。
10.8. Fabric 内存
CUDA 12.4 引入了一种新的 VMM 分配句柄类型 CU_MEM_HANDLE_TYPE_FABRIC
。在受支持的平台上,并且在 NVIDIA IMEX 守护程序正在运行的情况下,此分配句柄类型不仅可以在节点内通过任何通信机制(例如 MPI)共享分配,还可以在节点间共享分配。这允许多节点 NVLINK 系统中的 GPU 映射同一 NVLINK Fabric 中所有其他 GPU 的内存,即使它们位于不同的节点中,也大大提高了使用 NVLINK 的多 GPU 编程的规模。
10.8.1. 查询支持
在尝试使用 Fabric 内存之前,应用程序必须确保它们要使用的设备支持 Fabric 内存。以下代码示例显示了查询 Fabric 内存支持
int deviceSupportsFabricMem;
CUresult result = cuDeviceGetAttribute(&deviceSupportsFabricMem, CU_DEVICE_ATTRIBUTE_HANDLE_TYPE_FABRIC_SUPPORTED, device);
if (deviceSupportsFabricMem != 0) {
// `device` supports Fabric Memory
}
除了使用 CU_MEM_HANDLE_TYPE_FABRIC
作为句柄类型,并且不需要 OS 本机进程间通信机制来交换可共享句柄之外,使用 Fabric 内存与使用其他分配句柄类型没有区别。
10.9. 多播支持
多播对象管理 API 提供了一种方法,使应用程序可以创建多播对象,并结合上面描述的 虚拟内存管理 API,允许应用程序在支持的 NVLINK 连接的 GPU 上利用 NVLINK SHARP(如果它们与 NVSWITCH 连接)。NVLINK SHARP 允许 CUDA 应用程序利用 Fabric 内计算来加速使用 NVSWITCH 连接的 GPU 之间的广播和归约等操作。为此,多个 NVLINK 连接的 GPU 形成一个多播团队,团队中的每个 GPU 都使用物理内存备份一个多播对象。因此,由 N 个 GPU 组成的多播团队具有 N 个多播对象的物理副本,每个副本都位于一个参与 GPU 本地。multimem PTX 指令 使用多播对象的映射与多播对象的所有副本一起工作。
要使用多播对象,应用程序需要
查询多播支持
使用
cuMulticastCreate
创建多播句柄。与控制应参与多播团队的 GPU 的所有进程共享多播句柄。这可以使用上面描述的
cuMemExportToShareableHandle
来实现。使用
cuMulticastAddDevice
添加应参与多播团队的所有 GPU。对于每个参与的 GPU,将使用
cuMemCreate
分配的物理内存绑定到多播句柄,如上所述。在任何设备上绑定内存之前,需要将所有设备添加到多播团队。保留地址范围,映射多播句柄并设置访问权限,如上文针对常规单播映射所述。相同的物理内存可以进行单播和多播映射。请参阅上面的虚拟别名支持部分,了解如何确保同一物理内存的多个映射之间的一致性。
将 multimem PTX 指令 与多播映射一起使用。
多 GPU 编程模型 GitHub 存储库中的 multi_node_p2p
示例包含一个完整的示例,该示例使用 Fabric 内存(包括多播对象)来利用 NVLINK SHARP。请注意,此示例适用于 NCCL 或 NVSHMEM 等库的开发人员。它展示了 NVSHMEM 等更高级别的编程模型如何在(多节点)NVLINK 域内工作。应用程序开发人员通常应使用更高级别的 MPI、NCCL 或 NVSHMEM 接口,而不是此 API。
10.9.1. 查询支持
在尝试使用多播对象之前,应用程序必须确保它们要使用的设备支持多播对象。以下代码示例显示了查询 Fabric 内存支持
int deviceSupportsMultiCast;
CUresult result = cuDeviceGetAttribute(&deviceSupportsMultiCast, CU_DEVICE_ATTRIBUTE_MULTICAST_SUPPORTED, device);
if (deviceSupportsMultiCast != 0) {
// `device` supports Multicast Objects
}
10.9.2. 分配多播对象
可以使用 cuMulticastCreate
创建多播对象
CUmemGenericAllocationHandle createMCHandle(int numDevices, size_t size) {
CUmemAllocationProp mcProp = {};
mcProp.numDevices = numDevices;
mcProp.handleTypes = CU_MEM_HANDLE_TYPE_FABRIC; // or on single node CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR
size_t granularity = 0;
cuMulticastGetGranularity(&granularity, &mcProp, CU_MEM_ALLOC_GRANULARITY_MINIMUM);
// Ensure size matches granularity requirements for the allocation
size_t padded_size = ROUND_UP(size, granularity);
mcProp.size = padded_size;
// Create Multicast Object this has no devices and no physical memory associated yet
CUmemGenericAllocationHandle mcHandle;
cuMulticastCreate(&mcHandle, &mcProp);
return mcHandle;
}
10.9.3. 将设备添加到多播对象
可以使用 cuMulticastAddDevice
将设备添加到多播团队
cuMulticastAddDevice(&mcHandle, device);
在将任何设备上的内存绑定到多播对象之前,需要在控制应参与多播团队的设备的所有进程上完成此步骤。
10.9.4. 将内存绑定到多播对象
在创建多播对象并将所有参与设备添加到多播对象后,需要使用 cuMemCreate
为每个设备分配的物理内存来支持它
cuMulticastBindMem(mcHandle, mcOffset, memHandle, memOffset, size, 0 /*flags*/);
10.9.5. 使用多播映射
要在 CUDA C++ 中使用多播映射,需要将 multimem PTX 指令 与内联 PTX 汇编一起使用
__global__ void all_reduce_norm_barrier_kernel(float* l2_norm,
float* partial_l2_norm_mc,
unsigned int* arrival_counter_uc, unsigned int* arrival_counter_mc,
const unsigned int expected_count) {
assert( 1 == blockDim.x * blockDim.y * blockDim.z * gridDim.x * gridDim.y * gridDim.z );
float l2_norm_sum = 0.0;
#if __CUDA_ARCH__ >= 900
// atomic reduction to all replicas
// this can be conceptually thought of as __threadfence_system(); atomicAdd_system(arrival_counter_mc, 1);
asm volatile ("multimem.red.release.sys.global.add.u32 [%0], %1;" :: "l"(arrival_counter_mc), "n"(1) : "memory");
// Need a fence between Multicast (mc) and Unicast (uc) access to the same memory `arrival_counter_uc` and `arrival_counter_mc`:
// - fence.proxy instructions establish an ordering between memory accesses that may happen through different proxies
// - Value .alias of the .proxykind qualifier refers to memory accesses performed using virtually aliased addresses to the same memory location.
// from https://docs.nvda.net.cn/cuda/parallel-thread-execution/#parallel-synchronization-and-communication-instructions-membar
asm volatile ("fence.proxy.alias;" ::: "memory");
// spin wait with acquire ordering on UC mapping till all peers have arrived in this iteration
// Note: all ranks need to reach another barrier after this kernel, such that it is not possible for the barrier to be unblocked by an
// arrival of a rank for the next iteration if some other rank is slow.
cuda::atomic_ref<unsigned int,cuda::thread_scope_system> ac(arrival_counter_uc);
while (expected_count > ac.load(cuda::memory_order_acquire));
// Atomic load reduction from all replicas. It does not provide ordering so it can be relaxed.
asm volatile ("multimem.ld_reduce.relaxed.sys.global.add.f32 %0, [%1];" : "=f"(l2_norm_sum) : "l"(partial_l2_norm_mc) : "memory");
#else
#error "ERROR: multimem instructions require compute capability 9.0 or larger."
#endif
*l2_norm = std::sqrt(l2_norm_sum);
}
11. 流有序内存分配器
11.1. 简介
使用 cudaMalloc
和 cudaFree
管理内存分配会导致 GPU 在所有正在执行的 CUDA 流之间同步。流有序内存分配器使应用程序可以将内存分配和释放与其他工作(例如内核启动和异步复制)一起排序到 CUDA 流中。这通过利用流排序语义来重用内存分配,从而提高了应用程序的内存使用率。分配器还允许应用程序控制分配器的内存缓存行为。当使用适当的释放阈值进行设置时,缓存行为允许分配器在应用程序指示它愿意接受更大的内存占用时,避免对 OS 进行昂贵的调用。分配器还支持在进程之间轻松安全地共享分配。
对于许多应用程序,流有序内存分配器减少了对自定义内存管理抽象的需求,并使为需要它的应用程序创建高性能自定义内存管理变得更加容易。对于已经具有自定义内存分配器的应用程序和库,采用流有序内存分配器使多个库可以共享由驱动程序管理的公共内存池,从而减少了过多的内存消耗。此外,驱动程序可以根据其对分配器和其他流管理 API 的了解来执行优化。最后,Nsight Compute 和下一代 CUDA 调试器了解分配器,这是其 CUDA 11.3 工具包支持的一部分。
11.2. 查询支持
用户可以通过使用设备属性 cudaDevAttrMemoryPoolsSupported
调用 cudaDeviceGetAttribute()
来确定设备是否支持流有序内存分配器。
从 CUDA 11.3 开始,可以使用 cudaDevAttrMemoryPoolSupportedHandleTypes
设备属性查询 IPC 内存池支持。以前的驱动程序将返回 cudaErrorInvalidValue
,因为这些驱动程序不知道属性枚举。
int driverVersion = 0;
int deviceSupportsMemoryPools = 0;
int poolSupportedHandleTypes = 0;
cudaDriverGetVersion(&driverVersion);
if (driverVersion >= 11020) {
cudaDeviceGetAttribute(&deviceSupportsMemoryPools,
cudaDevAttrMemoryPoolsSupported, device);
}
if (deviceSupportsMemoryPools != 0) {
// `device` supports the Stream Ordered Memory Allocator
}
if (driverVersion >= 11030) {
cudaDeviceGetAttribute(&poolSupportedHandleTypes,
cudaDevAttrMemoryPoolSupportedHandleTypes, device);
}
if (poolSupportedHandleTypes & cudaMemHandleTypePosixFileDescriptor) {
// Pools on the specified device can be created with posix file descriptor-based IPC
}
在查询之前执行驱动程序版本检查可以避免在属性尚未定义的驱动程序上遇到 cudaErrorInvalidValue
错误。可以使用 cudaGetLastError
清除错误,而不是避免错误。
11.3. API 基础知识(cudaMallocAsync 和 cudaFreeAsync)
API cudaMallocAsync
和 cudaFreeAsync
构成了分配器的核心。cudaMallocAsync
返回分配,而 cudaFreeAsync
释放分配。这两个 API 都接受流参数,以定义分配何时将变为可用以及何时停止可用。由 cudaMallocAsync
返回的指针值是同步确定的,可用于构建未来的工作。重要的是要注意,当确定分配将驻留在何处时,cudaMallocAsync
会忽略当前设备/上下文。相反,cudaMallocAsync
根据指定的内存池或提供的流来确定驻留设备。最简单的使用模式是当内存被分配、使用,然后释放回同一流时。
void *ptr;
size_t size = 512;
cudaMallocAsync(&ptr, size, cudaStreamPerThread);
// do work using the allocation
kernel<<<..., cudaStreamPerThread>>>(ptr, ...);
// An asynchronous free can be specified without synchronizing the cpu and GPU
cudaFreeAsync(ptr, cudaStreamPerThread);
当在分配流以外的流中使用分配时,用户必须保证访问将在分配操作之后发生,否则行为未定义。用户可以通过同步分配流或使用 CUDA 事件来同步生成流和消费流来做出此保证。
cudaFreeAsync()
将释放操作插入到流中。用户必须保证释放操作发生在分配操作和分配的任何使用之后。此外,在释放操作开始后对分配的任何使用都会导致未定义的行为。应使用事件和/或流同步操作来保证在释放流开始释放操作之前,对其他流上分配的任何访问都已完成。
cudaMallocAsync(&ptr, size, stream1);
cudaEventRecord(event1, stream1);
//stream2 must wait for the allocation to be ready before accessing
cudaStreamWaitEvent(stream2, event1);
kernel<<<..., stream2>>>(ptr, ...);
cudaEventRecord(event2, stream2);
// stream3 must wait for stream2 to finish accessing the allocation before
// freeing the allocation
cudaStreamWaitEvent(stream3, event2);
cudaFreeAsync(ptr, stream3);
用户可以使用 cudaFreeAsync()
释放使用 cudaMalloc()
分配的分配。用户必须做出相同的保证,即访问在释放操作开始之前完成。
cudaMalloc(&ptr, size);
kernel<<<..., stream>>>(ptr, ...);
cudaFreeAsync(ptr, stream);
用户可以使用 cudaFree()
释放通过 cudaMallocAsync
分配的内存。当通过 cudaFree()
API 释放此类分配时,驱动程序会假定对该分配的所有访问均已完成,并且不再执行任何同步操作。用户可以使用 cudaStreamQuery
/ cudaStreamSynchronize
/ cudaEventQuery
/ cudaEventSynchronize
/ cudaDeviceSynchronize
来保证适当的异步工作已完成,并且 GPU 不会尝试访问该分配。
cudaMallocAsync(&ptr, size,stream);
kernel<<<..., stream>>>(ptr, ...);
// synchronize is needed to avoid prematurely freeing the memory
cudaStreamSynchronize(stream);
cudaFree(ptr);
11.4. 内存池和 cudaMemPool_t
内存池封装了虚拟地址和物理内存资源,这些资源根据池的属性和特性进行分配和管理。内存池的主要方面是其管理的内存类型和位置。
所有对 cudaMallocAsync
的调用都使用内存池的资源。在没有指定内存池的情况下,cudaMallocAsync
使用所提供流的设备的当前内存池。可以使用 cudaDeviceSetMempool
设置设备的当前内存池,并使用 cudaDeviceGetMempool
查询。默认情况下(在没有 cudaDeviceSetMempool
调用的情况下),当前内存池是设备的默认内存池。API cudaMallocFromPoolAsync
和 cudaMallocAsync 的 c++ 重载 允许用户指定用于分配的池,而无需将其设置为当前池。API cudaDeviceGetDefaultMempool
和 cudaMemPoolCreate
为用户提供内存池的句柄。
注意
设备的当前内存池将是该设备本地的。因此,在不指定内存池的情况下进行分配将始终产生对流设备本地的分配。
注意
cudaMemPoolSetAttribute
和 cudaMemPoolGetAttribute
控制内存池的属性。
11.5. 默认/隐式池
可以使用 cudaDeviceGetDefaultMempool
API 检索设备的默认内存池。从设备的默认内存池进行的分配是位于该设备上的非可迁移设备分配。这些分配将始终可以从该设备访问。可以使用 cudaMemPoolSetAccess
修改默认内存池的可访问性,并使用 cudaMemPoolGetAccess
查询。由于默认池不需要显式创建,因此有时被称为隐式池。设备的默认内存池不支持 IPC。
11.6. 显式池
API cudaMemPoolCreate
创建一个显式池。这允许应用程序请求超出默认/隐式池提供的分配属性。这些属性包括 IPC 功能、最大池大小、在受支持平台上的特定 CPU NUMA 节点上驻留的分配等。
// create a pool similar to the implicit pool on device 0
int device = 0;
cudaMemPoolProps poolProps = { };
poolProps.allocType = cudaMemAllocationTypePinned;
poolProps.location.id = device;
poolProps.location.type = cudaMemLocationTypeDevice;
cudaMemPoolCreate(&memPool, &poolProps));
以下代码片段演示了在有效的 CPU NUMA 节点上创建支持 IPC 的内存池的示例。
// create a pool resident on a CPU NUMA node that is capable of IPC sharing (via a file descriptor).
int cpu_numa_id = 0;
cudaMemPoolProps poolProps = { };
poolProps.allocType = cudaMemAllocationTypePinned;
poolProps.location.id = cpu_numa_id;
poolProps.location.type = cudaMemLocationTypeHostNuma;
poolProps.handleType = cudaMemHandleTypePosixFileDescriptor;
cudaMemPoolCreate(&ipcMemPool, &poolProps));
11.7. 物理页面缓存行为
默认情况下,分配器尝试最小化池拥有的物理内存。为了最大程度地减少操作系统调用以分配和释放物理内存,应用程序必须为每个池配置内存占用量。应用程序可以使用释放阈值属性 (cudaMemPoolAttrReleaseThreshold
) 来执行此操作。
释放阈值是池应保持的内存量(以字节为单位),超过此阈值后,池将尝试将内存释放回操作系统。当内存池持有的内存超过释放阈值字节时,分配器将在下次调用流、事件或设备同步时尝试将内存释放回操作系统。将释放阈值设置为 UINT64_MAX 将阻止驱动程序在每次同步后尝试缩小池。
Cuuint64_t setVal = UINT64_MAX;
cudaMemPoolSetAttribute(memPool, cudaMemPoolAttrReleaseThreshold, &setVal);
将 cudaMemPoolAttrReleaseThreshold
设置得足够高以有效地禁用内存池缩小的应用程序可能希望显式缩小内存池的内存占用量。cudaMemPoolTrimTo
允许此类应用程序执行此操作。在修剪内存池的占用量时,minBytesToKeep
参数允许应用程序保留一定数量的内存,它期望在后续执行阶段中需要这些内存。
Cuuint64_t setVal = UINT64_MAX;
cudaMemPoolSetAttribute(memPool, cudaMemPoolAttrReleaseThreshold, &setVal);
// application phase needing a lot of memory from the stream ordered allocator
for (i=0; i<10; i++) {
for (j=0; j<10; j++) {
cudaMallocAsync(&ptrs[j],size[j], stream);
}
kernel<<<...,stream>>>(ptrs,...);
for (j=0; j<10; j++) {
cudaFreeAsync(ptrs[j], stream);
}
}
// Process does not need as much memory for the next phase.
// Synchronize so that the trim operation will know that the allocations are no
// longer in use.
cudaStreamSynchronize(stream);
cudaMemPoolTrimTo(mempool, 0);
// Some other process/allocation mechanism can now use the physical memory
// released by the trimming operation.
11.8. 资源使用统计信息
在 CUDA 11.3 中,添加了池属性 cudaMemPoolAttrReservedMemCurrent
、cudaMemPoolAttrReservedMemHigh
、cudaMemPoolAttrUsedMemCurrent
和 cudaMemPoolAttrUsedMemHigh
,用于查询池的内存使用情况。
查询池的 cudaMemPoolAttrReservedMemCurrent
属性会报告池当前消耗的物理 GPU 内存总量。查询池的 cudaMemPoolAttrUsedMemCurrent
会返回从池中分配且不可重用的内存总大小。
cudaMemPoolAttr*MemHigh
属性是水印,记录自上次重置以来各个 cudaMemPoolAttr*MemCurrent
属性达到的最大值。可以使用 cudaMemPoolSetAttribute
API 将它们重置为当前值。
// sample helper functions for getting the usage statistics in bulk
struct usageStatistics {
cuuint64_t reserved;
cuuint64_t reservedHigh;
cuuint64_t used;
cuuint64_t usedHigh;
};
void getUsageStatistics(cudaMemoryPool_t memPool, struct usageStatistics *statistics)
{
cudaMemPoolGetAttribute(memPool, cudaMemPoolAttrReservedMemCurrent, statistics->reserved);
cudaMemPoolGetAttribute(memPool, cudaMemPoolAttrReservedMemHigh, statistics->reservedHigh);
cudaMemPoolGetAttribute(memPool, cudaMemPoolAttrUsedMemCurrent, statistics->used);
cudaMemPoolGetAttribute(memPool, cudaMemPoolAttrUsedMemHigh, statistics->usedHigh);
}
// resetting the watermarks will make them take on the current value.
void resetStatistics(cudaMemoryPool_t memPool)
{
cuuint64_t value = 0;
cudaMemPoolSetAttribute(memPool, cudaMemPoolAttrReservedMemHigh, &value);
cudaMemPoolSetAttribute(memPool, cudaMemPoolAttrUsedMemHigh, &value);
}
11.9. 内存重用策略
为了服务分配请求,驱动程序会尝试重用先前通过 cudaFreeAsync()
释放的内存,然后再尝试从操作系统分配更多内存。例如,在流中释放的内存可以立即重用于同一流中的后续分配请求。同样,当流与 CPU 同步时,先前在该流中释放的内存可以重用于任何流中的分配。
流有序分配器具有一些可控制的分配策略。池属性 cudaMemPoolReuseFollowEventDependencies
、cudaMemPoolReuseAllowOpportunistic
和 cudaMemPoolReuseAllowInternalDependencies
控制这些策略。升级到较新的 CUDA 驱动程序可能会更改、增强、扩充和/或重新排序重用策略。
11.9.1. cudaMemPoolReuseFollowEventDependencies
在分配更多物理 GPU 内存之前,分配器会检查 CUDA 事件建立的依赖关系信息,并尝试从在另一个流中释放的内存中进行分配。
cudaMallocAsync(&ptr, size, originalStream);
kernel<<<..., originalStream>>>(ptr, ...);
cudaFreeAsync(ptr, originalStream);
cudaEventRecord(event,originalStream);
// waiting on the event that captures the free in another stream
// allows the allocator to reuse the memory to satisfy
// a new allocation request in the other stream when
// cudaMemPoolReuseFollowEventDependencies is enabled.
cudaStreamWaitEvent(otherStream, event);
cudaMallocAsync(&ptr2, size, otherStream);
11.9.2. cudaMemPoolReuseAllowOpportunistic
根据 cudaMemPoolReuseAllowOpportunistic
策略,分配器会检查已释放的分配,以查看是否已满足释放的流顺序语义(例如,流是否已通过释放指示的执行点)。禁用此策略后,分配器仍将重用在流与 CPU 同步时可用的内存。禁用此策略不会阻止 cudaMemPoolReuseFollowEventDependencies
的应用。
cudaMallocAsync(&ptr, size, originalStream);
kernel<<<..., originalStream>>>(ptr, ...);
cudaFreeAsync(ptr, originalStream);
// after some time, the kernel finishes running
wait(10);
// When cudaMemPoolReuseAllowOpportunistic is enabled this allocation request
// can be fulfilled with the prior allocation based on the progress of originalStream.
cudaMallocAsync(&ptr2, size, otherStream);
11.9.3. cudaMemPoolReuseAllowInternalDependencies
如果无法从操作系统分配和映射更多物理内存,驱动程序将查找其可用性取决于另一个流的待处理进度的内存。如果找到此类内存,驱动程序会将所需的依赖关系插入到分配流中并重用该内存。
cudaMallocAsync(&ptr, size, originalStream);
kernel<<<..., originalStream>>>(ptr, ...);
cudaFreeAsync(ptr, originalStream);
// When cudaMemPoolReuseAllowInternalDependencies is enabled
// and the driver fails to allocate more physical memory, the driver may
// effectively perform a cudaStreamWaitEvent in the allocating stream
// to make sure that future work in ‘otherStream’ happens after the work
// in the original stream that would be allowed to access the original allocation.
cudaMallocAsync(&ptr2, size, otherStream);
11.9.4. 禁用重用策略
虽然可控制的重用策略提高了内存重用率,但用户可能希望禁用它们。允许机会性重用(例如 cudaMemPoolReuseAllowOpportunistic
)会根据 CPU 和 GPU 执行的交错引入分配模式的运行间差异。内部依赖关系插入(例如 cudaMemPoolReuseAllowInternalDependencies
)可能会以意外且可能不确定的方式串行化工作,而用户宁愿在分配失败时显式同步事件或流。
11.10. 多 GPU 支持的设备可访问性
就像通过虚拟内存管理 API 控制的分配可访问性一样,内存池分配可访问性不遵循 cudaDeviceEnablePeerAccess
或 cuCtxEnablePeerAccess
。相反,API cudaMemPoolSetAccess
修改哪些设备可以访问池中的分配。默认情况下,分配可以从分配所在的设备访问。此访问权限无法撤销。要启用从其他设备的访问,访问设备必须与内存池的设备具有对等功能;使用 cudaDeviceCanAccessPeer
进行检查。如果未检查对等功能,则设置访问可能会失败并显示 cudaErrorInvalidDevice
。如果池中没有进行任何分配,即使设备不具备对等功能,cudaMemPoolSetAccess
调用也可能会成功;在这种情况下,池中的下一个分配将失败。
值得注意的是,cudaMemPoolSetAccess
会影响内存池中的所有分配,而不仅仅是未来的分配。此外,cudaMemPoolGetAccess
报告的可访问性适用于池中的所有分配,而不仅仅是未来的分配。建议不要频繁更改给定 GPU 池的可访问性设置;一旦池可以从给定的 GPU 访问,它应该在该池的生命周期内保持可以从该 GPU 访问。
// snippet showing usage of cudaMemPoolSetAccess:
cudaError_t setAccessOnDevice(cudaMemPool_t memPool, int residentDevice,
int accessingDevice) {
cudaMemAccessDesc accessDesc = {};
accessDesc.location.type = cudaMemLocationTypeDevice;
accessDesc.location.id = accessingDevice;
accessDesc.flags = cudaMemAccessFlagsProtReadWrite;
int canAccess = 0;
cudaError_t error = cudaDeviceCanAccessPeer(&canAccess, accessingDevice,
residentDevice);
if (error != cudaSuccess) {
return error;
} else if (canAccess == 0) {
return cudaErrorPeerAccessUnsupported;
}
// Make the address accessible
return cudaMemPoolSetAccess(memPool, &accessDesc, 1);
}
11.11. IPC 内存池
支持 IPC 的内存池允许在进程之间轻松、高效且安全地共享 GPU 内存。CUDA 的 IPC 内存池提供与 CUDA 虚拟内存管理 API 相同的安全优势。
使用内存池在进程之间共享内存分为两个阶段。进程首先需要共享对池的访问权限,然后共享来自该池的特定分配。第一阶段建立并强制执行安全性。第二阶段协调每个进程中使用的虚拟地址以及导入进程中何时需要有效的映射。
11.11.1. 创建和共享 IPC 内存池
共享对池的访问权限涉及检索池的操作系统本机句柄(使用 cudaMemPoolExportToShareableHandle()
API),使用通常的操作系统本机 IPC 机制将句柄传输到导入进程,以及创建导入的内存池(使用 cudaMemPoolImportFromShareableHandle()
API)。为了使 cudaMemPoolExportToShareableHandle
成功,内存池必须已使用池属性结构中指定的请求句柄类型创建。请参考示例,了解在进程之间传输操作系统本机句柄的适当 IPC 机制。其余步骤可以在以下代码片段中找到。
// in exporting process
// create an exportable IPC capable pool on device 0
cudaMemPoolProps poolProps = { };
poolProps.allocType = cudaMemAllocationTypePinned;
poolProps.location.id = 0;
poolProps.location.type = cudaMemLocationTypeDevice;
// Setting handleTypes to a non zero value will make the pool exportable (IPC capable)
poolProps.handleTypes = CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR;
cudaMemPoolCreate(&memPool, &poolProps));
// FD based handles are integer types
int fdHandle = 0;
// Retrieve an OS native handle to the pool.
// Note that a pointer to the handle memory is passed in here.
cudaMemPoolExportToShareableHandle(&fdHandle,
memPool,
CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR,
0);
// The handle must be sent to the importing process with the appropriate
// OS specific APIs.
// in importing process
int fdHandle;
// The handle needs to be retrieved from the exporting process with the
// appropriate OS specific APIs.
// Create an imported pool from the shareable handle.
// Note that the handle is passed by value here.
cudaMemPoolImportFromShareableHandle(&importedMemPool,
(void*)fdHandle,
CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR,
0);
11.11.2. 导入进程中的设置访问权限
导入的内存池最初只能从其驻留设备访问。导入的内存池不会继承导出进程设置的任何可访问性。导入进程需要从计划访问内存的任何 GPU 启用访问权限(使用 cudaMemPoolSetAccess
)。
如果导入的内存池属于导入进程中不可见的设备,则用户必须使用 cudaMemPoolSetAccess
API 从将要使用分配的 GPU 启用访问权限。
11.11.3. 从导出的池创建和共享分配
池共享后,在导出进程中使用 cudaMallocAsync()
从池中进行的分配可以与已导入池的其他进程共享。由于池的安全策略是在池级别建立和验证的,因此操作系统不需要额外的簿记来为特定的池分配提供安全性;换句话说,导入池分配所需的不透明 cudaMemPoolPtrExportData
可以使用任何机制发送到导入进程。
虽然可以导出甚至导入分配,而无需以任何方式与分配流同步,但导入进程在访问分配时必须遵循与导出进程相同的规则。即,对分配的访问必须在分配流中分配操作的流排序之后发生。以下两个代码片段显示了 cudaMemPoolExportPointer()
和 cudaMemPoolImportPointer()
共享分配,并使用 IPC 事件来保证在分配准备就绪之前不会在导入进程中访问该分配。
// preparing an allocation in the exporting process
cudaMemPoolPtrExportData exportData;
cudaEvent_t readyIpcEvent;
cudaIpcEventHandle_t readyIpcEventHandle;
// ipc event for coordinating between processes
// cudaEventInterprocess flag makes the event an ipc event
// cudaEventDisableTiming is set for performance reasons
cudaEventCreate(
&readyIpcEvent, cudaEventDisableTiming | cudaEventInterprocess)
// allocate from the exporting mem pool
cudaMallocAsync(&ptr, size,exportMemPool, stream);
// event for sharing when the allocation is ready.
cudaEventRecord(readyIpcEvent, stream);
cudaMemPoolExportPointer(&exportData, ptr);
cudaIpcGetEventHandle(&readyIpcEventHandle, readyIpcEvent);
// Share IPC event and pointer export data with the importing process using
// any mechanism. Here we copy the data into shared memory
shmem->ptrData = exportData;
shmem->readyIpcEventHandle = readyIpcEventHandle;
// signal consumers data is ready
// Importing an allocation
cudaMemPoolPtrExportData *importData = &shmem->prtData;
cudaEvent_t readyIpcEvent;
cudaIpcEventHandle_t *readyIpcEventHandle = &shmem->readyIpcEventHandle;
// Need to retrieve the ipc event handle and the export data from the
// exporting process using any mechanism. Here we are using shmem and just
// need synchronization to make sure the shared memory is filled in.
cudaIpcOpenEventHandle(&readyIpcEvent, readyIpcEventHandle);
// import the allocation. The operation does not block on the allocation being ready.
cudaMemPoolImportPointer(&ptr, importedMemPool, importData);
// Wait for the prior stream operations in the allocating stream to complete before
// using the allocation in the importing process.
cudaStreamWaitEvent(stream, readyIpcEvent);
kernel<<<..., stream>>>(ptr, ...);
释放分配时,需要在导出进程中释放分配之前在导入进程中释放分配。以下代码片段演示了如何使用 CUDA IPC 事件来提供两个进程中 cudaFreeAsync
操作之间所需的同步。从导入进程对分配的访问显然受到导入进程端的释放操作的限制。值得注意的是,可以使用 cudaFree
在两个进程中释放分配,并且可以使用其他流同步 API 来代替 CUDA IPC 事件。
// The free must happen in importing process before the exporting process
kernel<<<..., stream>>>(ptr, ...);
// Last access in importing process
cudaFreeAsync(ptr, stream);
// Access not allowed in the importing process after the free
cudaIpcEventRecord(finishedIpcEvent, stream);
// Exporting process
// The exporting process needs to coordinate its free with the stream order
// of the importing process’s free.
cudaStreamWaitEvent(stream, finishedIpcEvent);
kernel<<<..., stream>>>(ptrInExportingProcess, ...);
// The free in the importing process doesn’t stop the exporting process
// from using the allocation.
cudFreeAsync(ptrInExportingProcess,stream);
11.11.4. IPC 导出池限制
IPC 池当前不支持将物理块释放回操作系统。因此,cudaMemPoolTrimTo
API 充当空操作,并且 cudaMemPoolAttrReleaseThreshold
实际上被忽略。此行为由驱动程序控制,而不是运行时控制,并且可能会在未来的驱动程序更新中更改。
11.11.5. IPC 导入池限制
不允许从导入池进行分配;具体来说,导入池不能设置为当前池,也不能在 cudaMallocFromPoolAsync
API 中使用。因此,分配重用策略属性对于这些池没有意义。
IPC 池当前不支持将物理块释放回操作系统。因此,cudaMemPoolTrimTo
API 充当空操作,并且 cudaMemPoolAttrReleaseThreshold
实际上被忽略。
资源使用统计信息属性查询仅反映导入到进程中的分配和关联的物理内存。
11.12. 同步 API 操作
分配器作为 CUDA 驱动程序的一部分带来的优化之一是与同步 API 的集成。当用户请求 CUDA 驱动程序同步时,驱动程序会等待异步工作完成。在返回之前,驱动程序将确定同步保证完成的释放操作。这些分配可用于分配,而与指定的流或禁用的分配策略无关。驱动程序还会在此处检查 cudaMemPoolAttrReleaseThreshold
,并释放它可以释放的任何多余的物理内存。
11.13. 附录
11.13.1. cudaMemcpyAsync 当前上下文/设备敏感性
在当前的 CUDA 驱动程序中,任何涉及来自 cudaMallocAsync
的内存的异步 memcpy
都应使用指定流的上下文作为调用线程的当前上下文来完成。对于 cudaMemcpyPeerAsync
,这不是必需的,因为 API 中指定的设备主上下文被引用,而不是当前上下文。
11.13.2. cuPointerGetAttribute 查询
在调用 cudaFreeAsync
后在分配上调用 cuPointerGetAttribute
会导致未定义的行为。具体来说,分配是否仍然可以从给定的流访问无关紧要:行为仍然是未定义的。
11.13.3. cuGraphAddMemsetNode
cuGraphAddMemsetNode
不适用于通过流有序分配器分配的内存。但是,可以流捕获分配的 memsets。
11.13.4. 指针属性
cuPointerGetAttributes
查询适用于流有序分配。由于流有序分配不与上下文关联,因此查询 CU_POINTER_ATTRIBUTE_CONTEXT
将会成功,但在 *data
中返回 NULL。属性 CU_POINTER_ATTRIBUTE_DEVICE_ORDINAL
可用于确定分配的位置:这在选择上下文以使用 cudaMemcpyPeerAsync
进行 p2h2p 复制时非常有用。属性 CU_POINTER_ATTRIBUTE_MEMPOOL_HANDLE
在 CUDA 11.3 中添加,可用于调试和确认分配来自哪个池,然后再执行 IPC。
12. 图内存节点
12.1. 介绍
图内存节点允许图创建和拥有内存分配。图内存节点具有 GPU 有序的生命周期语义,这决定了何时允许在设备上访问内存。这些 GPU 有序的生命周期语义启用了驱动程序管理的内存重用,并与流有序分配 API cudaMallocAsync
和 cudaFreeAsync
的语义相匹配,这些 API 可以在创建图时捕获。
图分配在图的生命周期内(包括重复实例化和启动)具有固定的地址。这允许内存被图内的其他操作直接引用,而无需图更新,即使 CUDA 更改了后备物理内存也是如此。在图中,图有序生命周期不重叠的分配可以使用相同的底层物理内存。
CUDA 可能会为跨多个图的分配重用相同的物理内存,并根据 GPU 有序的生命周期语义对虚拟地址映射进行别名。例如,当将不同的图启动到同一流中时,CUDA 可能会虚拟别名相同的物理内存,以满足具有单图生命周期的分配的需求。
12.2. 支持和兼容性
图内存节点需要 11.4 兼容的 CUDA 驱动程序以及 GPU 上对流有序分配器的支持。以下代码片段显示了如何在给定设备上检查支持。
int driverVersion = 0;
int deviceSupportsMemoryPools = 0;
int deviceSupportsMemoryNodes = 0;
cudaDriverGetVersion(&driverVersion);
if (driverVersion >= 11020) { // avoid invalid value error in cudaDeviceGetAttribute
cudaDeviceGetAttribute(&deviceSupportsMemoryPools, cudaDevAttrMemoryPoolsSupported, device);
}
deviceSupportsMemoryNodes = (driverVersion >= 11040) && (deviceSupportsMemoryPools != 0);
在驱动程序版本检查内部执行属性查询可以避免在 11.0 和 11.1 驱动程序上返回无效的值返回代码。请注意,当计算 санитайзер 检测到 CUDA 返回错误代码时,它会发出警告,并且在读取属性之前的版本检查将避免这种情况。图内存节点仅在驱动程序版本 11.4 及更高版本上受支持。
12.3. API 基础知识
图内存节点是表示内存分配或释放操作的图节点。作为简写,分配内存的节点称为分配节点。同样,释放内存的节点称为释放节点。分配节点创建的分配称为图分配。CUDA 在节点创建时为图分配分配虚拟地址。虽然这些虚拟地址在分配节点的生命周期内是固定的,但分配内容在释放操作后不会持久存在,并且可能会被引用不同分配的访问覆盖。
每次图运行时,图分配都被认为是重新创建的。图分配的生命周期(与节点的生命周期不同)在 GPU 执行到达分配图节点时开始,并在以下情况之一发生时结束
GPU 执行到达释放图节点
GPU 执行到达释放
cudaFreeAsync()
流调用在调用
cudaFree()
时立即
注意
图销毁不会自动释放任何活动的图分配内存,即使它结束了分配节点的生命周期。分配必须随后在另一个图中释放,或使用 cudaFreeAsync()
/cudaFree()
释放。
与其他 图结构 一样,图内存节点在图中通过依赖边进行排序。程序必须保证访问图内存的操作
在分配节点之后排序
在释放内存的操作之前排序
图分配生命周期开始和通常结束根据 GPU 执行(而不是 API 调用)。GPU 排序是工作在 GPU 上运行的顺序,而不是工作入队或描述的顺序。因此,图分配被认为是“GPU 有序的”。
12.3.1. 图节点 API
可以使用内存节点创建 API cudaGraphAddMemAllocNode
和 cudaGraphAddMemFreeNode
显式创建图内存节点。cudaGraphAddMemAllocNode
分配的地址在传递的 CUDA_MEM_ALLOC_NODE_PARAMS
结构的 dptr
字段中返回给用户。在分配图中所有使用图分配的操作都必须在分配节点之后排序。同样,任何释放节点都必须在图中所有使用分配之后排序。cudaGraphAddMemFreeNode
创建释放节点。
在下图中,有一个带有分配节点和释放节点的示例图。内核节点 a、b 和 c 在分配节点之后和释放节点之前排序,以便内核可以访问分配。内核节点 e 未在分配节点之后排序,因此无法安全地访问内存。内核节点 d 未在释放节点之前排序,因此无法安全地访问内存。

图 31 内核节点
以下代码片段建立了此图中的图
// Create the graph - it starts out empty
cudaGraphCreate(&graph, 0);
// parameters for a basic allocation
cudaMemAllocNodeParams params = {};
params.poolProps.allocType = cudaMemAllocationTypePinned;
params.poolProps.location.type = cudaMemLocationTypeDevice;
// specify device 0 as the resident device
params.poolProps.location.id = 0;
params.bytesize = size;
cudaGraphAddMemAllocNode(&allocNode, graph, NULL, 0, ¶ms);
nodeParams->kernelParams[0] = params.dptr;
cudaGraphAddKernelNode(&a, graph, &allocNode, 1, &nodeParams);
cudaGraphAddKernelNode(&b, graph, &a, 1, &nodeParams);
cudaGraphAddKernelNode(&c, graph, &a, 1, &nodeParams);
cudaGraphNode_t dependencies[2];
// kernel nodes b and c are using the graph allocation, so the freeing node must depend on them. Since the dependency of node b on node a establishes an indirect dependency, the free node does not need to explicitly depend on node a.
dependencies[0] = b;
dependencies[1] = c;
cudaGraphAddMemFreeNode(&freeNode, graph, dependencies, 2, params.dptr);
// free node does not depend on kernel node d, so it must not access the freed graph allocation.
cudaGraphAddKernelNode(&d, graph, &c, 1, &nodeParams);
// node e does not depend on the allocation node, so it must not access the allocation. This would be true even if the freeNode depended on kernel node e.
cudaGraphAddKernelNode(&e, graph, NULL, 0, &nodeParams);
12.3.2. 流捕获
可以通过捕获相应的流有序分配和释放调用 cudaMallocAsync
和 cudaFreeAsync
来创建图内存节点。在这种情况下,捕获的分配 API 返回的虚拟地址可以被图内的其他操作使用。由于流有序依赖关系将被捕获到图中,因此流有序分配 API 的排序要求保证图内存节点将相对于捕获的流操作正确排序(对于正确编写的流代码)。
为了清晰起见,忽略内核节点 d 和 e,以下代码片段显示了如何使用流捕获从上图创建图
cudaMallocAsync(&dptr, size, stream1);
kernel_A<<< ..., stream1 >>>(dptr, ...);
// Fork into stream2
cudaEventRecord(event1, stream1);
cudaStreamWaitEvent(stream2, event1);
kernel_B<<< ..., stream1 >>>(dptr, ...);
// event dependencies translated into graph dependencies, so the kernel node created by the capture of kernel C will depend on the allocation node created by capturing the cudaMallocAsync call.
kernel_C<<< ..., stream2 >>>(dptr, ...);
// Join stream2 back to origin stream (stream1)
cudaEventRecord(event2, stream2);
cudaStreamWaitEvent(stream1, event2);
// Free depends on all work accessing the memory.
cudaFreeAsync(dptr, stream1);
// End capture in the origin stream
cudaStreamEndCapture(stream1, &graph);
12.3.3. 在分配图外部访问和释放图内存
图分配不必由分配图释放。当图不释放分配时,该分配会持续存在于图的执行之外,并且可以被后续 CUDA 操作访问。只要访问操作通过 CUDA 事件和其他流排序机制在分配之后排序,就可以在另一个图中或直接使用流操作访问这些分配。分配随后可以通过常规调用 cudaFree
、cudaFreeAsync
或通过启动另一个带有相应 free 节点的图,或者后续启动分配图(如果它是使用 cudaGraphInstantiateFlagAutoFreeOnLaunch 标志实例化的)来释放。在内存被释放后访问它是非法的 - 释放操作必须在使用图依赖项、CUDA 事件和其他流排序机制访问内存的所有操作之后排序。
注意
由于图分配可能彼此共享底层物理内存,因此必须考虑与一致性和连贯性相关的 虚拟别名支持 规则。简而言之,释放操作必须在完整的设备操作(例如,计算内核/memcpy)完成后排序。具体而言,带外同步 - 例如,通过内存作为访问图分配内存的计算内核的一部分的握手 - 不足以在写入图内存的内存写入和该图内存的释放操作之间提供排序保证。
以下代码片段演示了在分配图外部访问图分配,并通过以下方式正确建立排序:使用单个流、在流之间使用事件以及使用烘焙到分配和释放图中的事件。
通过使用单个流建立排序
void *dptr;
cudaGraphAddMemAllocNode(&allocNode, allocGraph, NULL, 0, ¶ms);
dptr = params.dptr;
cudaGraphInstantiate(&allocGraphExec, allocGraph, NULL, NULL, 0);
cudaGraphLaunch(allocGraphExec, stream);
kernel<<< …, stream >>>(dptr, …);
cudaFreeAsync(dptr, stream);
通过记录和等待 CUDA 事件建立排序
void *dptr;
// Contents of allocating graph
cudaGraphAddMemAllocNode(&allocNode, allocGraph, NULL, 0, ¶ms);
dptr = params.dptr;
// contents of consuming/freeing graph
nodeParams->kernelParams[0] = params.dptr;
cudaGraphAddKernelNode(&a, graph, NULL, 0, &nodeParams);
cudaGraphAddMemFreeNode(&freeNode, freeGraph, &a, 1, dptr);
cudaGraphInstantiate(&allocGraphExec, allocGraph, NULL, NULL, 0);
cudaGraphInstantiate(&freeGraphExec, freeGraph, NULL, NULL, 0);
cudaGraphLaunch(allocGraphExec, allocStream);
// establish the dependency of stream2 on the allocation node
// note: the dependency could also have been established with a stream synchronize operation
cudaEventRecord(allocEvent, allocStream)
cudaStreamWaitEvent(stream2, allocEvent);
kernel<<< …, stream2 >>> (dptr, …);
// establish the dependency between the stream 3 and the allocation use
cudaStreamRecordEvent(streamUseDoneEvent, stream2);
cudaStreamWaitEvent(stream3, streamUseDoneEvent);
// it is now safe to launch the freeing graph, which may also access the memory
cudaGraphLaunch(freeGraphExec, stream3);
通过使用图外部事件节点建立排序
void *dptr;
cudaEvent_t allocEvent; // event indicating when the allocation will be ready for use.
cudaEvent_t streamUseDoneEvent; // event indicating when the stream operations are done with the allocation.
// Contents of allocating graph with event record node
cudaGraphAddMemAllocNode(&allocNode, allocGraph, NULL, 0, ¶ms);
dptr = params.dptr;
// note: this event record node depends on the alloc node
cudaGraphAddEventRecordNode(&recordNode, allocGraph, &allocNode, 1, allocEvent);
cudaGraphInstantiate(&allocGraphExec, allocGraph, NULL, NULL, 0);
// contents of consuming/freeing graph with event wait nodes
cudaGraphAddEventWaitNode(&streamUseDoneEventNode, waitAndFreeGraph, NULL, 0, streamUseDoneEvent);
cudaGraphAddEventWaitNode(&allocReadyEventNode, waitAndFreeGraph, NULL, 0, allocEvent);
nodeParams->kernelParams[0] = params.dptr;
// The allocReadyEventNode provides ordering with the alloc node for use in a consuming graph.
cudaGraphAddKernelNode(&kernelNode, waitAndFreeGraph, &allocReadyEventNode, 1, &nodeParams);
// The free node has to be ordered after both external and internal users.
// Thus the node must depend on both the kernelNode and the
// streamUseDoneEventNode.
dependencies[0] = kernelNode;
dependencies[1] = streamUseDoneEventNode;
cudaGraphAddMemFreeNode(&freeNode, waitAndFreeGraph, &dependencies, 2, dptr);
cudaGraphInstantiate(&waitAndFreeGraphExec, waitAndFreeGraph, NULL, NULL, 0);
cudaGraphLaunch(allocGraphExec, allocStream);
// establish the dependency of stream2 on the event node satisfies the ordering requirement
cudaStreamWaitEvent(stream2, allocEvent);
kernel<<< …, stream2 >>> (dptr, …);
cudaStreamRecordEvent(streamUseDoneEvent, stream2);
// the event wait node in the waitAndFreeGraphExec establishes the dependency on the “readyForFreeEvent” that is needed to prevent the kernel running in stream two from accessing the allocation after the free node in execution order.
cudaGraphLaunch(waitAndFreeGraphExec, stream3);
12.3.4. cudaGraphInstantiateFlagAutoFreeOnLaunch
在正常情况下,如果 CUDA 图有未释放的内存分配,CUDA 将阻止重新启动该图,因为同一地址的多个分配将导致内存泄漏。使用 cudaGraphInstantiateFlagAutoFreeOnLaunch
标志实例化图允许在图仍有未释放的分配时重新启动该图。在这种情况下,启动会自动插入未释放分配的异步释放。
启动时自动释放对于单生产者多消费者算法很有用。在每次迭代中,生产者图创建多个分配,并且根据运行时条件,一组不同的消费者访问这些分配。这种类型的可变执行序列意味着消费者无法释放分配,因为后续消费者可能需要访问。启动时自动释放意味着启动循环不需要跟踪生产者的分配 - 相反,该信息仍然隔离在生产者的创建和销毁逻辑中。一般来说,启动时自动释放简化了算法,否则该算法需要在每次重新启动之前释放图拥有的所有分配。
注意
cudaGraphInstantiateFlagAutoFreeOnLaunch
标志不会更改图销毁的行为。应用程序必须显式释放未释放的内存以避免内存泄漏,即使对于使用该标志实例化的图也是如此。以下代码显示了如何使用 cudaGraphInstantiateFlagAutoFreeOnLaunch
来简化单生产者/多消费者算法
// Create producer graph which allocates memory and populates it with data
cudaStreamBeginCapture(cudaStreamPerThread, cudaStreamCaptureModeGlobal);
cudaMallocAsync(&data1, blocks * threads, cudaStreamPerThread);
cudaMallocAsync(&data2, blocks * threads, cudaStreamPerThread);
produce<<<blocks, threads, 0, cudaStreamPerThread>>>(data1, data2);
...
cudaStreamEndCapture(cudaStreamPerThread, &graph);
cudaGraphInstantiateWithFlags(&producer,
graph,
cudaGraphInstantiateFlagAutoFreeOnLaunch);
cudaGraphDestroy(graph);
// Create first consumer graph by capturing an asynchronous library call
cudaStreamBeginCapture(cudaStreamPerThread, cudaStreamCaptureModeGlobal);
consumerFromLibrary(data1, cudaStreamPerThread);
cudaStreamEndCapture(cudaStreamPerThread, &graph);
cudaGraphInstantiateWithFlags(&consumer1, graph, 0); //regular instantiation
cudaGraphDestroy(graph);
// Create second consumer graph
cudaStreamBeginCapture(cudaStreamPerThread, cudaStreamCaptureModeGlobal);
consume2<<<blocks, threads, 0, cudaStreamPerThread>>>(data2);
...
cudaStreamEndCapture(cudaStreamPerThread, &graph);
cudaGraphInstantiateWithFlags(&consumer2, graph, 0);
cudaGraphDestroy(graph);
// Launch in a loop
bool launchConsumer2 = false;
do {
cudaGraphLaunch(producer, myStream);
cudaGraphLaunch(consumer1, myStream);
if (launchConsumer2) {
cudaGraphLaunch(consumer2, myStream);
}
} while (determineAction(&launchConsumer2));
cudaFreeAsync(data1, myStream);
cudaFreeAsync(data2, myStream);
cudaGraphExecDestroy(producer);
cudaGraphExecDestroy(consumer1);
cudaGraphExecDestroy(consumer2);
12.4. 优化的内存重用
CUDA 以两种方式重用内存
图内的虚拟和物理内存重用基于虚拟地址分配,就像在流排序分配器中一样。
图之间的物理内存重用通过虚拟别名完成:不同的图可以将相同的物理内存映射到其唯一的虚拟地址。
12.4.1. 图内的地址重用
CUDA 可以在图中重用内存,方法是将相同的虚拟地址范围分配给生命周期不重叠的不同分配。由于虚拟地址可能会被重用,因此指向具有不相交生命周期的不同分配的指针不能保证是唯一的。
下图显示了添加一个新的分配节点 (2),该节点可以重用依赖节点 (1) 释放的地址。

图 32 添加新的分配节点 2
下图显示了添加新的分配节点 (4)。新的分配节点不依赖于释放节点 (2),因此无法重用来自关联分配节点 (2) 的地址。如果分配节点 (2) 使用了释放节点 (1) 释放的地址,则新的分配节点 3 将需要一个新的地址。

图 33 添加新的分配节点 3
12.4.2. 物理内存管理和共享
CUDA 负责在 GPU 顺序到达分配节点之前将物理内存映射到虚拟地址。作为内存占用和映射开销的优化,如果多个图不会同时运行,则它们可以对不同的分配使用相同的物理内存;但是,如果物理页面绑定到多个同时执行的图,或绑定到保持未释放的图分配,则不能重用物理页面。
CUDA 可能会在图实例化、启动或执行期间的任何时间更新物理内存映射。CUDA 也可能会在未来的图启动之间引入同步,以防止活动的图分配引用相同的物理内存。对于任何分配-释放-分配模式,如果程序访问分配生命周期之外的指针,则错误的访问可能会静默地读取或写入另一个分配拥有的活动数据(即使分配的虚拟地址是唯一的)。使用计算 санитайзер 工具可以捕获此错误。
下图显示了在同一流中顺序启动的图。在此示例中,每个图都释放了它分配的所有内存。由于同一流中的图永远不会并发运行,因此 CUDA 可以并且应该使用相同的物理内存来满足所有分配。

图 34 顺序启动的图
12.5. 性能考虑因素
当多个图启动到同一流中时,CUDA 尝试为它们分配相同的物理内存,因为这些图的执行不能重叠。图的物理映射在启动之间保留,作为避免重新映射成本的优化。如果在稍后的时间,启动其中一个图,使其执行可能与其他图重叠(例如,如果它启动到不同的流中),那么 CUDA 必须执行一些重新映射,因为并发图需要不同的内存以避免数据损坏。
一般来说,CUDA 中图内存的重新映射可能由以下操作引起
更改图启动到的流
对图内存池执行 trim 操作,该操作显式释放未使用的内存(在 物理内存占用 中讨论)
当另一个图的未释放分配映射到同一内存时,重新启动图将导致在重新启动之前重新映射内存
重新映射必须按执行顺序发生,但在该图的任何先前执行完成后发生(否则,仍在使用中的内存可能会被取消映射)。由于这种排序依赖性,以及因为映射操作是 OS 调用,所以映射操作可能相对昂贵。应用程序可以通过始终将包含分配内存节点的图启动到同一流中来避免此成本。
12.5.1. 首次启动 / cudaGraphUpload
物理内存不能在图实例化期间分配或映射,因为图将在其中执行的流是未知的。映射改为在图启动期间完成。调用 cudaGraphUpload
可以通过立即执行该图的所有映射并将该图与上传流关联来将分配成本与启动分开。如果图随后启动到同一流中,它将启动而无需任何额外的重新映射。
对图上传和图启动使用不同的流的行为类似于切换流,可能会导致重新映射操作。此外,允许不相关的内存池管理从空闲流中提取内存,这可能会抵消上传的影响。
12.6. 物理内存占用
异步分配的池管理行为意味着销毁包含内存节点的图(即使它们的分配是空闲的)也不会立即将物理内存返回给 OS 以供其他进程使用。要显式地将内存释放回 OS,应用程序应使用 cudaDeviceGraphMemTrim
API。
cudaDeviceGraphMemTrim
将取消映射并释放图内存节点保留的任何未被积极使用的物理内存。尚未释放的分配和已调度或正在运行的图被认为正在积极使用物理内存,并且不会受到影响。使用 trim API 将使物理内存可用于其他分配 API 和其他应用程序或进程,但会导致 CUDA 在下次启动修剪后的图时重新分配和重新映射内存。请注意,cudaDeviceGraphMemTrim
在与 cudaMemPoolTrimTo()
不同的池上运行。图内存池不暴露给流排序内存分配器。CUDA 允许应用程序通过 cudaDeviceGetGraphMemAttribute
API 查询其图内存占用。查询属性 cudaGraphMemAttrReservedMemCurrent
返回驱动程序为当前进程中的图分配保留的物理内存量。查询 cudaGraphMemAttrUsedMemCurrent
返回当前至少由一个图映射的物理内存量。这些属性中的任何一个都可用于跟踪 CUDA 何时为了分配图而获取新的物理内存。这两个属性都可用于检查共享机制节省了多少内存。
12.7. 对等访问
图分配可以配置为从多个 GPU 访问,在这种情况下,CUDA 将根据需要将分配映射到对等 GPU 上。CUDA 允许需要不同映射的图分配重用相同的虚拟地址。当这种情况发生时,地址范围将映射到不同分配所需的所有 GPU 上。这意味着分配有时可能允许比创建期间请求的更多的对等访问;但是,依赖这些额外的映射仍然是错误的。
12.7.1. 使用图节点 API 进行对等访问
cudaGraphAddMemAllocNode
API 接受节点参数结构的 accessDescs
数组字段中的映射请求。poolProps.location
嵌入式结构指定分配的驻留设备。假定需要来自分配 GPU 的访问,因此应用程序不需要在 accessDescs
数组中为驻留设备指定条目。
cudaMemAllocNodeParams params = {};
params.poolProps.allocType = cudaMemAllocationTypePinned;
params.poolProps.location.type = cudaMemLocationTypeDevice;
// specify device 1 as the resident device
params.poolProps.location.id = 1;
params.bytesize = size;
// allocate an allocation resident on device 1 accessible from device 1
cudaGraphAddMemAllocNode(&allocNode, graph, NULL, 0, ¶ms);
accessDescs[2];
// boilerplate for the access descs (only ReadWrite and Device access supported by the add node api)
accessDescs[0].flags = cudaMemAccessFlagsProtReadWrite;
accessDescs[0].location.type = cudaMemLocationTypeDevice;
accessDescs[1].flags = cudaMemAccessFlagsProtReadWrite;
accessDescs[1].location.type = cudaMemLocationTypeDevice;
// access being requested for device 0 & 2. Device 1 access requirement left implicit.
accessDescs[0].location.id = 0;
accessDescs[1].location.id = 2;
// access request array has 2 entries.
params.accessDescCount = 2;
params.accessDescs = accessDescs;
// allocate an allocation resident on device 1 accessible from devices 0, 1 and 2. (0 & 2 from the descriptors, 1 from it being the resident device).
cudaGraphAddMemAllocNode(&allocNode, graph, NULL, 0, ¶ms);
12.7.2. 使用流捕获进行对等访问
对于流捕获,分配节点记录捕获时分配池的对等可访问性。在捕获 cudaMallocFromPoolAsync
调用后更改分配池的对等可访问性不会影响图将为分配进行的映射。
// boilerplate for the access descs (only ReadWrite and Device access supported by the add node api)
accessDesc.flags = cudaMemAccessFlagsProtReadWrite;
accessDesc.location.type = cudaMemLocationTypeDevice;
accessDesc.location.id = 1;
// let memPool be resident and accessible on device 0
cudaStreamBeginCapture(stream);
cudaMallocAsync(&dptr1, size, memPool, stream);
cudaStreamEndCapture(stream, &graph1);
cudaMemPoolSetAccess(memPool, &accessDesc, 1);
cudaStreamBeginCapture(stream);
cudaMallocAsync(&dptr2, size, memPool, stream);
cudaStreamEndCapture(stream, &graph2);
//The graph node allocating dptr1 would only have the device 0 accessibility even though memPool now has device 1 accessibility.
//The graph node allocating dptr2 will have device 0 and device 1 accessibility, since that was the pool accessibility at the time of the cudaMallocAsync call.
13. 数学函数
参考手册列出了设备代码中支持的所有 C/C++ 标准库数学函数以及所有内部函数(仅在设备代码中支持),以及它们的描述。
本节提供了其中一些函数在适用时的精度信息。它使用 ULP 进行量化。有关最后一位单位 (ULP) 的定义的更多信息,请参阅 Jean-Michel Muller 的论文 On the definition of ulp(x), RR-5504, LIP RR-2005-09, INRIA, LIP. 2005, pp.16 at https://hal.inria.fr/inria-00070503/document。
设备代码中支持的数学函数不设置全局 errno
变量,也不报告任何浮点异常来指示错误;因此,如果需要错误诊断机制,用户应为函数的输入和输出实现额外的筛选。用户负责指针参数的有效性。用户不得将未初始化的参数传递给数学函数,因为这可能会导致未定义的行为:函数在用户程序中内联,因此会受到编译器优化的影响。
13.1. 标准函数
本节中的函数可以在主机代码和设备代码中使用。
本节指定了每个函数在设备上执行时的误差界限,以及在主机不提供该函数的情况下在主机上执行时的误差界限。
误差界限来自广泛但非详尽的测试生成,因此它们不是保证的界限。
单精度浮点函数
加法和乘法符合 IEEE 标准,因此最大误差为 0.5 ulp。
将单精度浮点操作数舍入为整数的推荐方法,结果为单精度浮点数是 rintf()
,而不是 roundf()
。原因是 roundf()
在设备上映射到 4 指令序列,而 rintf()
映射到单个指令。truncf()
、ceilf()
和 floorf()
也各自映射到单个指令。
函数 |
最大 ulp 误差 |
---|---|
|
0 (IEEE-754 舍入到最接近的偶数) |
|
0 (IEEE-754 舍入到最接近的偶数) |
|
对于计算能力 \(\ge 2\),当使用 2(全范围),否则 |
|
对于计算能力 \(\ge 2\),当使用 1(全范围),否则 |
|
2(全范围) 仅当编译器将其转换为 |
|
当使用 否则,对于计算能力 \(\ge 5.2\) 为 1 对于较旧的架构为 3 |
|
1(全范围) |
|
1(全范围) |
|
3(全范围) |
|
2(全范围) |
|
3(全范围) |
|
2(全范围) |
|
3(全范围) |
|
2(全范围) |
|
由于使用了快速算法,精度会因舍入而损失,因此无法提供误差界限。 |
|
由于使用了快速算法,精度会因舍入而损失,因此无法提供误差界限。 |
|
2(全范围) |
|
2(全范围) |
|
2(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
2(全范围) |
|
1(全范围) |
|
2(全范围) |
|
2(全范围) |
|
4(全范围) |
|
2(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
2(全范围) |
|
2(全范围) |
|
2(全范围) |
|
3(全范围) |
|
3(全范围) |
|
2(全范围) |
|
2(全范围) |
|
3(全范围) |
|
4(全范围) |
|
3(全范围) |
|
4(全范围) |
|
2(全范围) |
|
4(全范围) |
|
2(全范围) |
|
4(全范围) |
|
4(全范围) |
|
5(全范围) |
|
5(全范围) |
|
6(在区间 -10.001 … -2.264 之外;内部较大) |
|
5(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
对于 |x| < 8 为 9 否则,最大绝对误差为 2.2 x 10-6 |
|
对于 |x| < 8 为 9 否则,最大绝对误差为 2.2 x 10-6 |
|
对于 n = 128,最大绝对误差为 2.2 x 10-6 |
|
对于 |x| < 8 为 9 否则,最大绝对误差为 2.2 x 10-6 |
|
对于 |x| < 8 为 9 否则,最大绝对误差为 2.2 x 10-6 |
|
对于 |x| < n 为 ceil(2 + 2.5n) 否则,最大绝对误差为 2.2 x 10-6 |
|
6(全范围) |
|
6(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
双精度浮点函数
将双精度浮点操作数舍入为整数的推荐方法,结果为双精度浮点数是 rint()
,而不是 round()
。原因是 round()
在设备上映射到 5 指令序列,而 rint()
映射到单个指令。trunc()
、ceil()
和 floor()
也各自映射到单个指令。
函数 |
最大 ulp 误差 |
---|---|
|
0 (IEEE-754 舍入到最接近的偶数) |
|
0 (IEEE-754 舍入到最接近的偶数) |
|
0 (IEEE-754 舍入到最接近的偶数) |
|
0 (IEEE-754 舍入到最接近的偶数) |
|
0 (IEEE-754 舍入到最接近的偶数) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
2(全范围) |
|
1(全范围) |
|
2(全范围) |
|
1(全范围) |
|
2(全范围) |
|
1(全范围) |
|
由于使用了快速算法,精度会因舍入而损失,因此无法提供误差界限。 |
|
由于使用了快速算法,精度会因舍入而损失,因此无法提供误差界限。 |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
2(全范围) |
|
2(全范围) |
|
2(全范围) |
|
2(全范围) |
|
2(全范围) |
|
2(全范围) |
|
2(全范围) |
|
2(全范围) |
|
2(全范围) |
|
2(全范围) |
|
2(全范围) |
|
2(全范围) |
|
1(全范围) |
|
1(全范围) |
|
3(全范围) |
|
3(全范围) |
|
2(全范围) |
|
2(全范围) |
|
2(全范围) |
|
5(全范围) |
|
5(全范围) |
|
6(全范围) |
|
4(全范围) |
|
5(全范围) |
|
8(全范围) |
|
4(在区间 -23.0001 … -2.2637 之外;内部较大) |
|
10(全范围) |
|
0 (IEEE-754 舍入到最接近的偶数) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
对于 |x| < 8 为 7 否则,最大绝对误差为 5 x 10-12 |
|
对于 |x| < 8 为 7 否则,最大绝对误差为 5 x 10-12 |
|
对于 n = 128,最大绝对误差为 5 x 10-12 |
|
对于 |x| < 8 为 7 否则,最大绝对误差为 5 x 10-12 |
|
对于 |x| < 8 为 7 否则,最大绝对误差为 5 x 10-12 |
|
对于 |x| > 1.5n,最大绝对误差为 5 x 10-12 |
|
6(全范围) |
|
6(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
四精度浮点函数
请注意,四精度数学函数当前仅适用于计算能力为 10.0 及更高的设备。由于实现的特殊性,设备代码中对 __float128
和 _Float128
类型的支持也仅限于主机平台的选择组合,另请参见 主机编译器扩展。
函数 |
最大 ulp 误差 |
---|---|
|
0 (IEEE-754 舍入到最接近的偶数) |
|
0 (IEEE-754 舍入到最接近的偶数) |
|
0 (IEEE-754 舍入到最接近的偶数) |
|
0 (IEEE-754 舍入到最接近的偶数) |
|
0 (IEEE-754 舍入到最接近的偶数) |
|
0 (IEEE-754 舍入到最接近的偶数) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
1(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
|
0(全范围) |
13.2. 内部函数
本节中的函数只能在设备代码中使用。
这些函数中包含一些精度较低但速度更快的 标准函数 的版本。它们的名称相同,但前缀为 __
(例如 __sinf(x)
)。它们之所以更快,是因为它们映射到更少的本机指令。编译器有一个选项 (-use_fast_math
),它强制 表 17 中的每个函数都编译为其内部对应项。除了降低受影响函数的精度外,它还可能导致特殊情况处理方面的一些差异。更稳健的方法是仅在性能提升值得且可以容忍精度降低和不同特殊情况处理等更改的属性的情况下,有选择地将数学函数调用替换为对内部函数的调用。
运算符/函数 |
设备函数 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
单精度浮点函数
__fadd_[rn,rz,ru,rd]()
和 __fmul_[rn,rz,ru,rd]()
映射到编译器永远不会合并到 FMAD 中的加法和乘法运算。相比之下,从“*”和“+”运算符生成的加法和乘法运算通常会合并到 FMAD 中。
带有 _rn
后缀的函数使用舍入到最接近的偶数舍入模式运行。
带有 _rz
后缀的函数使用朝零舍入模式运行。
带有 _ru
后缀的函数使用向上舍入(到正无穷大)舍入模式运行。
带有 _rd
后缀的函数使用向下舍入(到负无穷大)舍入模式运行。
浮点除法的精度因代码是否使用 -prec-div=false
或 -prec-div=true
编译而异。当代码使用 -prec-div=false
编译时,常规除法 /
运算符和 __fdividef(x,y)
具有相同的精度,但对于 2126 < |y|
< 2128,__fdividef(x,y)
产生零结果,而 /
运算符在 表 18 中声明的精度范围内提供正确的结果。此外,对于 2126 < |y|
< 2128,如果 x
是无穷大,__fdividef(x,y)
产生 NaN
(作为无穷大乘以零的结果),而 /
运算符返回无穷大。另一方面,当代码使用 -prec-div=true
或不使用任何 -prec-div
选项编译时,/
运算符符合 IEEE 标准,因为其默认值为 true。
函数 |
误差界限 |
---|---|
|
符合 IEEE 标准。 |
|
符合 IEEE 标准。 |
|
符合 IEEE 标准。 |
|
符合 IEEE 标准。 |
|
符合 IEEE 标准。 |
|
符合 IEEE 标准。 |
|
符合 IEEE 标准。 |
|
符合 IEEE 标准。 |
|
对于 |
|
最大 ulp 误差为 |
|
最大 ulp 误差为 |
|
对于 |
|
对于 |
|
对于 |
|
对于 |
|
对于 |
|
与 |
|
源自其实现 |
|
源自其实现 |
|
当前实现的最大相对误差为 \(2^{-11}\)。即使在 |
双精度浮点函数
__dadd_rn()
和 __dmul_rn()
映射到编译器永远不会合并到 FMAD 中的加法和乘法运算。相比之下,从“*”和“+”运算符生成的加法和乘法运算通常会合并到 FMAD 中。
函数 |
误差界限 |
---|---|
|
符合 IEEE 标准。 |
|
符合 IEEE 标准。 |
|
符合 IEEE 标准。 |
|
符合 IEEE 标准。 |
|
符合 IEEE 标准。 Requires compute capability > 2. |
|
符合 IEEE 标准。 Requires compute capability > 2. |
|
符合 IEEE 标准。 Requires compute capability > 2. |
14. C++ 语言支持
如 使用 NVCC 编译 中所述,使用 nvcc
编译的 CUDA 源文件可以包含主机代码和设备代码的混合。CUDA 前端编译器旨在模拟主机编译器在 C++ 输入代码方面的行为。输入源代码根据 C++ ISO/IEC 14882:2003、C++ ISO/IEC 14882:2011、C++ ISO/IEC 14882:2014 或 C++ ISO/IEC 14882:2017 规范进行处理,CUDA 前端编译器旨在模拟任何主机编译器与 ISO 规范的偏差。此外,支持的语言通过本文档 13 中描述的 CUDA 特定构造进行扩展,并受以下描述的限制约束。
C++11 语言特性、C++14 语言特性 和 C++17 语言特性 分别为 C++11、C++14、C++17 和 C++20 特性提供了支持矩阵。限制 列出了语言限制。多态函数包装器 和 扩展 Lambda 描述了其他特性。代码示例 给出了代码示例。
14.1. C++11 语言特性
下表列出了已被 C++11 标准接受的新语言特性。“提案”列提供了指向描述该特性的 ISO C++ 委员会提案的链接,而“在 nvcc 中可用(设备代码)”列指示了包含此特性的设备代码实现的第一个 nvcc 版本(如果已实现)。
语言特性 |
C++11 提案 |
在 nvcc 中可用(设备代码) |
---|---|---|
右值引用 |
7.0 |
|
|
7.0 |
|
通过右值初始化类对象 |
7.0 |
|
非静态数据成员初始化器 |
7.0 |
|
可变参数模板 |
7.0 |
|
扩展可变参数模板模板参数 |
7.0 |
|
初始化列表 |
7.0 |
|
静态断言 |
7.0 |
|
|
7.0 |
|
多声明符 |
7.0 |
|
删除 auto 作为存储类说明符 |
7.0 |
|
新函数声明符语法 |
7.0 |
|
Lambda 表达式 |
7.0 |
|
表达式的声明类型 |
7.0 |
|
不完整的返回类型 |
7.0 |
|
右尖括号 |
7.0 |
|
函数模板的默认模板参数 |
7.0 |
|
解决表达式的 SFINAE 问题 |
7.0 |
|
别名模板 |
7.0 |
|
外部模板 |
7.0 |
|
空指针常量 |
7.0 |
|
强类型枚举 |
7.0 |
|
枚举的前向声明 |
7.0 |
|
标准化的属性语法 |
7.0 |
|
广义常量表达式 |
7.0 |
|
对齐支持 |
7.0 |
|
条件支持行为 |
7.0 |
|
将未定义行为更改为可诊断错误 |
7.0 |
|
委托构造函数 |
7.0 |
|
继承构造函数 |
7.0 |
|
显式转换运算符 |
7.0 |
|
新字符类型 |
7.0 |
|
Unicode 字符串字面量 |
7.0 |
|
原始字符串字面量 |
7.0 |
|
字面量中的通用字符名称 |
7.0 |
|
用户定义的字面量 |
7.0 |
|
标准布局类型 |
7.0 |
|
默认函数 |
7.0 |
|
删除的函数 |
7.0 |
|
扩展的友元声明 |
7.0 |
|
扩展 |
7.0 |
|
内联命名空间 |
7.0 |
|
无限制联合 |
7.0 |
|
局部和未命名类型作为模板参数 |
7.0 |
|
基于范围的 for 循环 |
7.0 |
|
显式虚拟覆盖 |
7.0 |
|
对垃圾回收和基于可达性的泄漏检测的最低限度支持 |
不适用(参见 限制) |
|
允许移动构造函数抛出异常 [noexcept] |
7.0 |
|
定义移动特殊成员函数 |
7.0 |
|
并发 |
||
序列点 |
||
原子操作 |
||
强比较和交换 |
||
双向栅栏 |
||
内存模型 |
||
数据依赖排序:原子和内存模型 |
||
传播异常 |
||
允许在信号处理程序中使用原子操作 |
||
线程局部存储 |
||
具有并发性的动态初始化和销毁 |
||
C++11 中的 C99 特性 |
||
|
7.0 |
|
C99 预处理器 |
7.0 |
|
|
7.0 |
|
扩展的整数类型 |
14.2. C++14 语言特性
下表列出了已被 C++14 标准接受的新语言特性。
语言特性 |
C++14 提案 |
在 nvcc 中可用(设备代码) |
---|---|---|
对某些 C++ 上下文转换的调整 |
9.0 |
|
二进制字面量 |
9.0 |
|
具有推导返回类型的函数 |
9.0 |
|
广义 lambda 捕获(init-capture) |
9.0 |
|
泛型(多态)lambda 表达式 |
9.0 |
|
变量模板 |
9.0 |
|
放宽对 constexpr 函数的要求 |
9.0 |
|
成员初始化器和聚合 |
9.0 |
|
澄清内存分配 |
||
大小调整的释放 |
||
|
9.0 |
|
单引号作为数字分隔符 |
9.0 |
14.3. C++17 语言特性
nvcc 版本 11.0 及更高版本支持所有 C++17 语言特性,但受 此处 描述的限制约束。
14.4. C++20 语言特性
nvcc 版本 12.0 及更高版本支持所有 C++20 语言特性,但受 此处 描述的限制约束。
14.5. 限制
14.5.1. 主机编译器扩展
设备代码不支持主机编译器特定的语言扩展。
__Complex
类型仅在主机代码中受支持。
当与支持 __int128
类型的宿主机编译器结合编译时,设备代码支持 __int128
类型。
当与支持该类型的宿主机编译器结合编译时,计算能力为 10.0 及更高版本的设备支持 __float128
类型。__float128
类型的常量表达式可以由编译器以较低精度的浮点表示形式处理。
14.5.2. 预处理器符号
14.5.2.1. __CUDA_ARCH__
以下实体的类型签名不应取决于是否定义了
__CUDA_ARCH__
,或者取决于__CUDA_ARCH__
的特定值__global__
函数和函数模板__device__
和__constant__
变量纹理和表面
示例
#if !defined(__CUDA_ARCH__) typedef int mytype; #else typedef double mytype; #endif __device__ mytype xxx; // error: xxx's type depends on __CUDA_ARCH__ __global__ void foo(mytype in, // error: foo's type depends on __CUDA_ARCH__ mytype *ptr) { *ptr = in; }
如果从主机实例化和启动
__global__
函数模板,则无论是否定义了__CUDA_ARCH__
以及__CUDA_ARCH__
的值如何,都必须使用相同的模板参数实例化该函数模板。示例
__device__ int result; template <typename T> __global__ void kern(T in) { result = in; } __host__ __device__ void foo(void) { #if !defined(__CUDA_ARCH__) kern<<<1,1>>>(1); // error: "kern<int>" instantiation only // when __CUDA_ARCH__ is undefined! #endif } int main(void) { foo(); cudaDeviceSynchronize(); return 0; }
在单独编译模式下,具有外部链接的函数或变量的定义的有无不应取决于是否定义了
__CUDA_ARCH__
或__CUDA_ARCH__
的特定值14。示例
#if !defined(__CUDA_ARCH__) void foo(void) { } // error: The definition of foo() // is only present when __CUDA_ARCH__ // is undefined #endif
在单独编译中,
__CUDA_ARCH__
不得在头文件中使用,以致不同的对象可能包含不同的行为。或者,必须保证所有对象都将为相同的 compute_arch 编译。如果弱函数或模板函数在头文件中定义,并且其行为取决于__CUDA_ARCH__
,则如果对象是为不同的计算架构编译的,则该函数在对象中的实例可能会冲突。例如,如果 a.h 包含
template<typename T> __device__ T* getptr(void) { #if __CUDA_ARCH__ == 700 return NULL; /* no address */ #else __shared__ T arr[256]; return arr; #endif }
那么,如果
a.cu
和b.cu
都包含a.h
并为相同的类型实例化getptr
,并且b.cu
期望一个非 NULL 地址,并使用以下命令编译nvcc –arch=compute_70 –dc a.cu nvcc –arch=compute_80 –dc b.cu nvcc –arch=sm_80 a.o b.o
在链接时,仅使用
getptr
的一个版本,因此行为将取决于选择的版本。为了避免这种情况,a.cu
和b.cu
必须为相同的计算架构编译,或者__CUDA_ARCH__
不应在共享头文件函数中使用。
编译器不保证会为上述不支持的 __CUDA_ARCH__
用法生成诊断信息。
14.5.3. 限定符
14.5.3.1. 设备内存空间说明符
在以下项中不允许使用 __device__
、__shared__
、__managed__
和 __constant__
内存空间说明符
class
、struct
和union
数据成员,形式参数,
在主机上执行的函数内的非 extern 变量声明。
在设备上执行的函数中,既不是 extern 也不是 static 的变量声明中不允许使用 __device__
、__constant__
和 __managed__
内存空间说明符。
__device__
、__constant__
、__managed__
或 __shared__
变量定义不能具有带有非空构造函数或非空析构函数的类类型。如果类类型的构造函数是平凡构造函数,或者满足以下所有条件,则在转换单元中的某个点,该构造函数被视为空构造函数
构造函数函数已定义。
构造函数函数没有参数,初始化列表为空,并且函数体是一个空的复合语句。
它的类没有虚函数、没有虚基类并且没有非静态数据成员初始化器。
可以认为其类的所有基类的默认构造函数都是空的。
对于其类中所有属于类类型(或其数组)的非静态数据成员,可以认为默认构造函数是空的。
如果类类型的析构函数是平凡析构函数,或者满足以下所有条件,则在转换单元中的某个点,该析构函数被视为空析构函数
析构函数函数已定义。
析构函数函数体是一个空的复合语句。
它的类没有虚函数并且没有虚基类。
可以认为其类的所有基类的析构函数都是空的。
对于其类中所有属于类类型(或其数组)的非静态数据成员,可以认为析构函数是空的。
当在整个程序编译模式下编译时(有关此模式的描述,请参见 nvcc 用户手册),__device__
、__shared__
、__managed__
和 __constant__
变量不能使用 extern
关键字定义为外部变量。唯一的例外是 __shared__ 中描述的动态分配的 __shared__
变量。
当在单独编译模式下编译时(有关此模式的描述,请参见 nvcc 用户手册),可以使用 extern
关键字将 __device__
、__shared__
、__managed__
和 __constant__
变量定义为外部变量。nvlink
在找不到外部变量的定义时会生成错误(除非它是动态分配的 __shared__
变量)。
14.5.3.2. __managed__ 内存空间说明符
标记有 __managed__
内存空间说明符(“托管”变量)的变量具有以下限制
托管变量的地址不是常量表达式。
托管变量不应具有 const 限定类型。
托管变量不应具有引用类型。
在 CUDA 运行时可能未处于有效状态时,不得使用托管变量的地址或值,包括以下情况
在具有静态或线程局部存储持续时间的对象进行静态/动态初始化或销毁时。
在调用 exit() 后执行的代码中(例如,标记有 gcc 的“
__attribute__((destructor))
”的函数)。在 CUDA 运行时可能未初始化时执行的代码中(例如,标记有 gcc 的“
__attribute__((constructor))
”的函数)。
托管变量不能用作
decltype()
表达式的未加括号的 id-expression 参数。托管变量具有与动态分配的托管内存相同的相干性和一致性行为。
当包含托管变量的 CUDA 程序在具有多个 GPU 的执行平台上运行时,变量仅分配一次,而不是每个 GPU 分配一次。
在主机上执行的函数中,不允许使用没有 extern 链接的托管变量声明。
在设备上执行的函数中,不允许使用没有 extern 或 static 链接的托管变量声明。
以下是托管变量的合法和非法用法的示例
__device__ __managed__ int xxx = 10; // OK
int *ptr = &xxx; // error: use of managed variable
// (xxx) in static initialization
struct S1_t {
int field;
S1_t(void) : field(xxx) { };
};
struct S2_t {
~S2_t(void) { xxx = 10; }
};
S1_t temp1; // error: use of managed variable
// (xxx) in dynamic initialization
S2_t temp2; // error: use of managed variable
// (xxx) in the destructor of
// object with static storage
// duration
__device__ __managed__ const int yyy = 10; // error: const qualified type
__device__ __managed__ int &zzz = xxx; // error: reference type
template <int *addr> struct S3_t { };
S3_t<&xxx> temp; // error: address of managed
// variable(xxx) not a
// constant expression
__global__ void kern(int *ptr)
{
assert(ptr == &xxx); // OK
xxx = 20; // OK
}
int main(void)
{
int *ptr = &xxx; // OK
kern<<<1,1>>>(ptr);
cudaDeviceSynchronize();
xxx++; // OK
decltype(xxx) qqq; // error: managed variable(xxx) used
// as unparenthized argument to
// decltype
decltype((xxx)) zzz = yyy; // OK
}
14.5.3.3. volatile 限定符
编译器可以自由地优化对全局或共享内存的读取和写入(例如,通过将全局读取缓存到寄存器或 L1 缓存中),只要它尊重内存栅栏函数(内存栅栏函数)的内存排序语义和同步函数(同步函数)的内存可见性语义。
可以使用 volatile
关键字禁用这些优化:如果声明为 volatile 的变量位于全局或共享内存中,则编译器会假定其值可以随时被另一个线程更改或使用,因此对该变量的任何引用都会编译为实际的内存读取或写入指令。
14.5.4. 指针
在主机上执行的代码中,解引用指向全局或共享内存的指针,或者在设备上执行的代码中,解引用指向主机内存的指针,都会导致未定义的行为,最常见的是段错误和应用程序终止。
通过获取 __device__
、__shared__
或 __constant__
变量的地址而获得的地址只能在设备代码中使用。通过 设备内存 中描述的 cudaGetSymbolAddress()
获得的 __device__
或 __constant__
变量的地址只能在主机代码中使用。
14.5.5. 运算符
14.5.5.1. 赋值运算符
__constant__
变量只能通过运行时函数从主机代码赋值(设备内存);它们不能从设备代码赋值。
__shared__
变量在其声明中不能具有初始化。
不允许为 内置变量 中定义的任何内置变量赋值。
14.5.5.2. 地址运算符
不允许获取 内置变量 中定义的任何内置变量的地址。
14.5.6. 运行时类型信息 (RTTI)
以下 RTTI 相关特性在主机代码中受支持,但在设备代码中不受支持。
typeid
运算符std::type_info
dynamic_cast
运算符
14.5.7. 异常处理
异常处理仅在主机代码中受支持,但在设备代码中不受支持。
不支持 __global__
函数的异常规范。
14.5.8. 标准库
标准库仅在主机代码中受支持,但在设备代码中不受支持,除非另有说明。
14.5.9. 命名空间保留
除非另有说明,否则向 cuda::
、nv::
、cooperative_groups::
或其中嵌套的任何命名空间添加任何声明或定义都是未定义的行为。
示例
namespace cuda{
// Bad: class declaration added to namespace cuda
struct foo{};
// Bad: function definition added to namespace cuda
cudaStream_t make_stream(){
cudaStream_t s;
cudaStreamCreate(&s);
return s;
}
} // namespace cuda
namespace cuda{
namespace utils{
// Bad: function definition added to namespace nested within cuda
cudaStream_t make_stream(){
cudaStream_t s;
cudaStreamCreate(&s);
return s;
}
} // namespace utils
} // namespace cuda
namespace utils{
namespace cuda{
// Okay: namespace cuda may be used nested within a non-reserved namespace
cudaStream_t make_stream(){
cudaStream_t s;
cudaStreamCreate(&s);
return s;
}
} // namespace cuda
} // namespace utils
// Bad: Equivalent to adding symbols to namespace cuda at global scope
using namespace utils;
14.5.10. 函数
14.5.10.1. 外部链接
仅当使用 extern 限定符声明的函数在与设备代码相同的编译单元(即,单个文件或使用可重定位设备代码和 nvlink 链接在一起的多个文件)中定义时,才允许在某些设备代码中调用该函数。
14.5.10.2. 隐式声明和显式默认函数
令 F
表示在其首次声明时隐式声明或显式默认的函数。F
的执行空间说明符(__host__
、__device__
)是调用它的所有函数的执行空间说明符的并集(请注意,对于此分析,__global__
调用方将被视为 __device__
调用方)。例如
class Base {
int x;
public:
__host__ __device__ Base(void) : x(10) {}
};
class Derived : public Base {
int y;
};
class Other: public Base {
int z;
};
__device__ void foo(void)
{
Derived D1;
Other D2;
}
__host__ void bar(void)
{
Other D3;
}
在此,隐式声明的构造函数函数 “Derived::Derived” 将被视为 __device__
函数,因为它仅从 __device__
函数 “foo” 调用。隐式声明的构造函数函数 “Other::Other” 将被视为 __host__ __device__
函数,因为它既从 __device__
函数 “foo” 又从 __host__
函数 “bar” 调用。
此外,如果 F
是虚析构函数,则由 F
覆盖的每个虚析构函数 D
的执行空间将添加到 F
的执行空间集中,如果 D
不是隐式定义的,或者是在其首次声明以外的声明中显式默认的。
例如
struct Base1 { virtual __host__ __device__ ~Base1() { } };
struct Derived1 : Base1 { }; // implicitly-declared virtual destructor
// ~Derived1 has __host__ __device__
// execution space specifiers
struct Base2 { virtual __device__ ~Base2(); };
__device__ Base2::~Base2() = default;
struct Derived2 : Base2 { }; // implicitly-declared virtual destructor
// ~Derived2 has __device__ execution
// space specifiers
14.5.10.3. 函数参数
__global__
函数参数通过常量内存传递到设备,并且从 Volta 开始限制为 32,764 字节,在较旧的架构上限制为 4 KB。
__global__
函数不能具有可变数量的参数。
__global__
函数参数不能按引用传递。
在单独编译模式下,如果 __device__
或 __global__
函数在特定的转换单元中被 ODR 使用,则该函数的参数和返回类型必须在该转换单元中是完整的。
示例
//first.cu:
struct S;
__device__ void foo(S); // error: type 'S' is incomplete
__device__ auto *ptr = foo;
int main() { }
//second.cu:
struct S { int x; };
__device__ void foo(S) { }
//compiler invocation
$nvcc -std=c++14 -rdc=true first.cu second.cu -o first
nvlink error : Prototype doesn't match for '_Z3foo1S' in '/tmp/tmpxft_00005c8c_00000000-18_second.o', first defined in '/tmp/tmpxft_00005c8c_00000000-18_second.o'
nvlink fatal : merge_elf failed
14.5.10.3.1. __global__
函数参数处理
当从设备代码启动 __global__
函数时,每个参数都必须是可平凡复制和可平凡销毁的。
当从主机代码启动 __global__
函数时,允许每个参数类型为非平凡可复制或非平凡可销毁的,但是此类类型的处理不遵循标准 C++ 模型,如下所述。用户代码必须确保此工作流程不会影响程序的正确性。该工作流程在两个方面偏离了标准 C++
Memcpy 而不是复制构造函数调用
当从主机代码降低
__global__
函数启动时,编译器会生成存根函数,这些函数会按值复制参数一次或多次,然后最终使用memcpy
将参数复制到设备上__global__
函数的参数内存中。即使参数是非平凡可复制的,也会发生这种情况,因此可能会破坏复制构造函数具有副作用的程序。示例
#include <cassert> struct S { int x; int *ptr; __host__ __device__ S() { } __host__ __device__ S(const S &) { ptr = &x; } }; __global__ void foo(S in) { // this assert may fail, because the compiler // generated code will memcpy the contents of "in" // from host to kernel parameter memory, so the // "in.ptr" is not initialized to "&in.x" because // the copy constructor is skipped. assert(in.ptr == &in.x); } int main() { S tmp; foo<<<1,1>>>(tmp); cudaDeviceSynchronize(); }
示例
#include <cassert> __managed__ int counter; struct S1 { S1() { } S1(const S1 &) { ++counter; } }; __global__ void foo(S1) { /* this assertion may fail, because the compiler generates stub functions on the host for a kernel launch, and they may copy the argument by value more than once. */ assert(counter == 1); } int main() { S1 V; foo<<<1,1>>>(V); cudaDeviceSynchronize(); }
析构函数可能在 ``__global__`` 函数完成之前被调用
内核启动与主机执行是异步的。因此,如果
__global__
函数参数具有非平凡析构函数,则即使在__global__
函数完成执行之前,析构函数也可能在主机代码中执行。这可能会破坏析构函数具有副作用的程序。示例
struct S { int *ptr; S() : ptr(nullptr) { } S(const S &) { cudaMallocManaged(&ptr, sizeof(int)); } ~S() { cudaFree(ptr); } }; __global__ void foo(S in) { //error: This store may write to memory that has already been // freed (see below). *(in.ptr) = 4; } int main() { S V; /* The object 'V' is first copied by value to a compiler-generated * stub function that does the kernel launch, and the stub function * bitwise copies the contents of the argument to kernel parameter * memory. * However, GPU kernel execution is asynchronous with host * execution. * As a result, S::~S() will execute when the stub function returns, releasing allocated memory, even though the kernel may not have finished execution. */ foo<<<1,1>>>(V); cudaDeviceSynchronize(); }
14.5.10.3.2. 工具包和驱动程序兼容性
开发人员必须使用 12.1 工具包和 r530 驱动程序或更高版本来编译、启动和调试接受大于 4KB 参数的内核。如果此类内核在较旧的驱动程序上启动,CUDA 将发出错误 CUDA_ERROR_NOT_SUPPORTED
。
14.5.10.3.3. 跨工具包修订版本的链接兼容性
链接设备对象时,如果至少有一个设备对象包含参数大于 4KB 的内核,则开发人员必须在使用 12.1 工具包或更高版本将所有对象链接在一起之前,使用 12.1 工具包或更高版本从其各自的设备源重新编译所有对象。否则将导致链接器错误。
14.5.10.4. 函数内的静态变量
变量内存空间说明符在函数 F
的直接或嵌套块作用域内的静态变量 V
的声明中是允许的,其中
F
是__global__
或__device__
-only 函数。F
是__host__ __device__
函数并且定义了__CUDA_ARCH__
18。
如果在 V
的声明中没有显式的内存空间说明符,则在设备编译期间假定为隐式的 __device__
说明符。
V
具有与在命名空间作用域中声明的具有相同内存空间限定符的变量相同的初始化限制。例如,__device__
变量不能具有“非空”构造函数(请参阅设备内存空间限定符)。
下面显示了函数作用域静态变量的合法和非法用例示例。
struct S1_t {
int x;
};
struct S2_t {
int x;
__device__ S2_t(void) { x = 10; }
};
struct S3_t {
int x;
__device__ S3_t(int p) : x(p) { }
};
__device__ void f1() {
static int i1; // OK, implicit __device__ memory space specifier
static int i2 = 11; // OK, implicit __device__ memory space specifier
static __managed__ int m1; // OK
static __device__ int d1; // OK
static __constant__ int c1; // OK
static S1_t i3; // OK, implicit __device__ memory space specifier
static S1_t i4 = {22}; // OK, implicit __device__ memory space specifier
static __shared__ int i5; // OK
int x = 33;
static int i6 = x; // error: dynamic initialization is not allowed
static S1_t i7 = {x}; // error: dynamic initialization is not allowed
static S2_t i8; // error: dynamic initialization is not allowed
static S3_t i9(44); // error: dynamic initialization is not allowed
}
__host__ __device__ void f2() {
static int i1; // OK, implicit __device__ memory space specifier
// during device compilation.
#ifdef __CUDA_ARCH__
static __device__ int d1; // OK, declaration is only visible during device
// compilation (__CUDA_ARCH__ is defined)
#else
static int d0; // OK, declaration is only visible during host
// compilation (__CUDA_ARCH__ is not defined)
#endif
static __device__ int d2; // error: __device__ variable inside
// a host function during host compilation
// i.e. when __CUDA_ARCH__ is not defined
static __shared__ int i2; // error: __shared__ variable inside
// a host function during host compilation
// i.e. when __CUDA_ARCH__ is not defined
}
14.5.10.5. 函数指针
在主机代码中获取的 __global__
函数的地址不能在设备代码中使用(例如,启动内核)。 同样,在设备代码中获取的 __global__
函数的地址不能在主机代码中使用。
不允许在主机代码中获取 __device__
函数的地址。
14.5.10.6. 函数递归
__global__
函数不支持递归。
14.5.10.7. 友元函数
__global__
函数或函数模板不能在友元声明中定义。
示例
struct S1_t {
friend __global__
void foo1(void); // OK: not a definition
template<typename T>
friend __global__
void foo2(void); // OK: not a definition
friend __global__
void foo3(void) { } // error: definition in friend declaration
template<typename T>
friend __global__
void foo4(void) { } // error: definition in friend declaration
};
14.5.10.8. 运算符函数
运算符函数不能是 __global__
函数。
14.5.10.9. 分配和释放函数
用户定义的 operator new
、operator new[]
、operator delete
或 operator delete[]
不能用于替换编译器提供的相应的 __host__
或 __device__
内置函数。
14.5.11. 类
14.5.11.1. 数据成员
不支持静态数据成员,除非它们也是 const 限定的(请参阅Const 限定变量)。
14.5.11.2. 函数成员
静态成员函数不能是 __global__
函数。
14.5.11.3. 虚函数
当派生类中的函数覆盖基类中的虚函数时,被覆盖函数和覆盖函数上的执行空间限定符(即,__host__
、__device__
)必须匹配。
不允许将具有虚函数的类的对象作为参数传递给 __global__
函数。
如果在主机代码中创建对象,则在设备代码中为该对象调用虚函数具有未定义的行为。
如果在设备代码中创建对象,则在主机代码中为该对象调用虚函数具有未定义的行为。
有关使用 Microsoft 主机编译器时的其他约束,请参阅Windows 特定。
示例
struct S1 { virtual __host__ __device__ void foo() { } };
__managed__ S1 *ptr1, *ptr2;
__managed__ __align__(16) char buf1[128];
__global__ void kern() {
ptr1->foo(); // error: virtual function call on a object
// created in host code.
ptr2 = new(buf1) S1();
}
int main(void) {
void *buf;
cudaMallocManaged(&buf, sizeof(S1), cudaMemAttachGlobal);
ptr1 = new (buf) S1();
kern<<<1,1>>>();
cudaDeviceSynchronize();
ptr2->foo(); // error: virtual function call on an object
// created in device code.
}
14.5.11.4. 虚基类
不允许将从虚基类派生的类的对象作为参数传递给 __global__
函数。
有关使用 Microsoft 主机编译器时的其他约束,请参阅Windows 特定。
14.5.11.5. 匿名联合
命名空间作用域匿名联合的成员变量不能在 __global__
或 __device__
函数中引用。
14.5.11.6. Windows 特定
CUDA 编译器遵循 IA64 ABI 进行类布局,而 Microsoft 主机编译器则不遵循。 令 T
表示指向成员类型的指针,或满足以下任一条件的类类型
T
具有虚函数。T
具有虚基类。T
具有多重继承,其中包含多个直接或间接空基类。T
的所有直接和间接基类B
都是空的,并且T
的第一个字段F
的类型在其定义中使用了B
,以便B
在F
的定义中布局在偏移量 0 处。
令 C
表示 T
或具有 T
作为字段类型或基类类型的类类型。 对于类型 C
,CUDA 编译器计算的类布局和大小可能与 Microsoft 主机编译器不同。
只要类型 C
仅在主机或设备代码中使用,程序就应该可以正常工作。
在主机和设备代码之间传递类型为 C
的对象具有未定义的行为,例如,作为 __global__
函数的参数或通过 cudaMemcpy*()
调用。
如果对象是在主机代码中创建的,则在设备代码中访问类型为 C
的对象或任何子对象,或者在设备代码中调用成员函数,则具有未定义的行为。
如果对象是在设备代码中创建的 19,则在主机代码中访问类型为 C
的对象或任何子对象,或者在主机代码中调用成员函数,则具有未定义的行为。
14.5.12. 模板
如果满足以下任一条件,则类型或模板不能在 __global__
函数模板实例化或 __device__/__constant__
变量实例化的类型、非类型或模板模板参数中使用
类型或模板在
__host__
或__host__ __device__
中定义。类型或模板是具有
private
或protected
访问权限的类成员,并且其父类未在__device__
或__global__
函数中定义。类型是未命名的。
类型由上述任何类型复合而成。
示例
template <typename T>
__global__ void myKernel(void) { }
class myClass {
private:
struct inner_t { };
public:
static void launch(void)
{
// error: inner_t is used in template argument
// but it is private
myKernel<inner_t><<<1,1>>>();
}
};
// C++14 only
template <typename T> __device__ T d1;
template <typename T1, typename T2> __device__ T1 d2;
void fn() {
struct S1_t { };
// error (C++14 only): S1_t is local to the function fn
d1<S1_t> = {};
auto lam1 = [] { };
// error (C++14 only): a closure type cannot be used for
// instantiating a variable template
d2<int, decltype(lam1)> = 10;
}
14.5.13. 三字符组和双字符组
任何平台都不支持三字符组。 Windows 不支持双字符组。
14.5.14. Const 限定变量
令 ‘V’ 表示命名空间作用域变量或具有 const 限定类型且没有执行空间注解(例如,__device__、 __constant__、 __shared__
)的类静态成员变量。 V 被认为是主机代码变量。
如果满足以下条件,则 V 的值可以直接在设备代码中使用:
V 在使用点之前已使用常量表达式初始化,
V 的类型不是 volatile 限定的,并且
它具有以下类型之一
内置浮点类型,除非使用 Microsoft 编译器作为主机编译器,
内置整型。
设备源代码不能包含对 V 的引用或获取 V 的地址。
示例
const int xxx = 10;
struct S1_t { static const int yyy = 20; };
extern const int zzz;
const float www = 5.0;
__device__ void foo(void) {
int local1[xxx]; // OK
int local2[S1_t::yyy]; // OK
int val1 = xxx; // OK
int val2 = S1_t::yyy; // OK
int val3 = zzz; // error: zzz not initialized with constant
// expression at the point of use.
const int &val3 = xxx; // error: reference to host variable
const int *val4 = &xxx; // error: address of host variable
const float val5 = www; // OK except when the Microsoft compiler is used as
// the host compiler.
}
const int zzz = 20;
14.5.15. Long Double
设备代码不支持使用 long double
类型。
14.5.16. 弃用注解
当使用 gcc
、clang
、xlC
、icc
或 pgcc
主机编译器时,nvcc 支持使用 deprecated
属性,当使用 cl.exe
主机编译器时,支持使用 deprecated
declspec。 当 C++14 方言已启用时,它还支持 [[deprecated]]
标准属性。 当 __CUDA_ARCH__
被定义时(即,在设备编译阶段),CUDA 前端编译器将为从 __device__
、__global__
或 __host__ __device__
函数体内部引用的已弃用实体生成弃用诊断信息。 对已弃用实体的其他引用将由主机编译器处理,例如,从 __host__
函数内部的引用。
CUDA 前端编译器不支持各种主机编译器支持的 #pragma gcc diagnostic
或 #pragma warning
机制。 因此,由 CUDA 前端编译器生成的弃用诊断信息不受这些 pragma 的影响,但由主机编译器生成的诊断信息将受到影响。 要抑制设备代码的警告,用户可以使用 NVIDIA 特定的 pragma #pragma nv_diag_suppress。nvcc
标志 -Wno-deprecated-declarations
可用于抑制所有弃用警告,标志 -Werror=deprecated-declarations
可用于将弃用警告转换为错误。
14.5.17. Noreturn 注解
当使用 gcc
、clang
、xlC
、icc
或 pgcc
主机编译器时,nvcc 支持使用 noreturn
属性,当使用 cl.exe
主机编译器时,支持使用 noreturn
declspec。 当 C++11 方言已启用时,它还支持 [[noreturn]]
标准属性。
该属性/declspec 可在主机代码和设备代码中使用。
14.5.18. [[likely]] / [[unlikely]] 标准属性
在所有支持 C++ 标准属性语法的配置中,都接受这些属性。 这些属性可用于向设备编译器优化器提示,与不包含该语句的任何替代路径相比,某个语句更可能或更不可能被执行。
示例
__device__ int foo(int x) {
if (i < 10) [[likely]] { // the 'if' block will likely be entered
return 4;
}
if (i < 20) [[unlikely]] { // the 'if' block will not likely be entered
return 1;
}
return 0;
}
如果在 __CUDA_ARCH__
未定义时在主机代码中使用这些属性,则它们将存在于主机编译器解析的代码中,如果不支持这些属性,则主机编译器可能会生成警告。 例如,clang
11 主机编译器将生成“未知属性”警告。
14.5.19. const 和 pure GNU 属性
当使用也支持这些属性的语言方言和主机编译器时,例如使用 g++ 主机编译器时,主机和设备函数都支持这些属性。
对于使用 pure
属性注解的设备函数,设备代码优化器假定该函数不会更改对调用函数可见的任何可变状态(例如,内存)。
对于使用 const
属性注解的设备函数,设备代码优化器假定该函数不会访问或更改对调用函数可见的任何可变状态(例如,内存)。
示例
__attribute__((const)) __device__ int get(int in);
__device__ int doit(int in) {
int sum = 0;
//because 'get' is marked with 'const' attribute
//device code optimizer can recognize that the
//second call to get() can be commoned out.
sum = get(in);
sum += get(in);
return sum;
}
14.5.20. __nv_pure__ 属性
主机和设备函数都支持 __nv_pure__
属性。 对于主机函数,当使用支持 pure
GNU 属性的语言方言时,__nv_pure__
属性将转换为 pure
GNU 属性。 同样,当使用 MSVC 作为主机编译器时,该属性将转换为 MSVC noalias
属性。
当设备函数使用 __nv_pure__
属性注解时,设备代码优化器假定该函数不会更改对调用函数可见的任何可变状态(例如,内存)。
14.5.21. Intel 主机编译器特定
CUDA 前端编译器解析器无法识别 Intel 编译器(例如 icc
)支持的某些内在函数。 当使用 Intel 编译器作为主机编译器时,nvcc
因此将在预处理期间启用宏 __INTEL_COMPILER_USE_INTRINSIC_PROTOTYPES
。 此宏允许在关联的头文件中显式声明 Intel 编译器内在函数,从而允许 nvcc
支持在主机代码中使用此类函数20。
14.5.22. C++11 特性
nvcc 也支持主机编译器默认启用的 C++11 特性,但受本文档中描述的限制约束。 此外,使用 -std=c++11
标志调用 nvcc 会启用所有 C++11 特性,并使用相应的 C++11 方言选项调用主机预处理器、编译器和链接器21。
14.5.22.1. Lambda 表达式
与 lambda 表达式关联的闭包类的所有成员函数22 的执行空间限定符由编译器按如下方式派生。 如 C++11 标准中所述,编译器在包含 lambda 表达式的最小块作用域、类作用域或命名空间作用域中创建闭包类型。 计算包含闭包类型的最内层函数作用域,并将相应函数的执行空间限定符分配给闭包类成员函数。 如果没有封闭函数作用域,则执行空间限定符为 __host__
。
下面显示了 lambda 表达式和计算的执行空间限定符的示例(在注释中)。
auto globalVar = [] { return 0; }; // __host__
void f1(void) {
auto l1 = [] { return 1; }; // __host__
}
__device__ void f2(void) {
auto l2 = [] { return 2; }; // __device__
}
__host__ __device__ void f3(void) {
auto l3 = [] { return 3; }; // __host__ __device__
}
__device__ void f4(int (*fp)() = [] { return 4; } /* __host__ */) {
}
__global__ void f5(void) {
auto l5 = [] { return 5; }; // __device__
}
__device__ void f6(void) {
struct S1_t {
static void helper(int (*fp)() = [] {return 6; } /* __device__ */) {
}
};
}
lambda 表达式的闭包类型不能在 __global__
函数模板实例化的类型或非类型参数中使用,除非 lambda 在 __device__
或 __global__
函数中定义。
示例
template <typename T>
__global__ void foo(T in) { };
template <typename T>
struct S1_t { };
void bar(void) {
auto temp1 = [] { };
foo<<<1,1>>>(temp1); // error: lambda closure type used in
// template type argument
foo<<<1,1>>>( S1_t<decltype(temp1)>()); // error: lambda closure type used in
// template type argument
}
14.5.22.2. std::initializer_list
默认情况下,CUDA 编译器将隐式地将 std::initializer_list
的成员函数视为具有 __host__ __device__
执行空间限定符,因此可以直接从设备代码中调用它们。 nvcc 标志 --no-host-device-initializer-list
将禁用此行为; std::initializer_list
的成员函数将被视为 __host__
函数,并且不能直接从设备代码中调用。
示例
#include <initializer_list>
__device__ int foo(std::initializer_list<int> in);
__device__ void bar(void)
{
foo({4,5,6}); // (a) initializer list containing only
// constant expressions.
int i = 4;
foo({i,5,6}); // (b) initializer list with at least one
// non-constant element.
// This form may have better performance than (a).
}
14.5.22.3. 右值引用
默认情况下,CUDA 编译器将隐式地将 std::move
和 std::forward
函数模板视为具有 __host__ __device__
执行空间限定符,因此可以直接从设备代码中调用它们。 nvcc 标志 --no-host-device-move-forward
将禁用此行为; std::move
和 std::forward
将被视为 __host__
函数,并且不能直接从设备代码中调用。
14.5.22.4. Constexpr 函数和函数模板
默认情况下,constexpr 函数不能从具有不兼容执行空间的函数调用 23。 实验性的 nvcc 标志 --expt-relaxed-constexpr
取消了此限制 24。 当指定此标志时,主机代码可以调用 __device__
constexpr 函数,设备代码可以调用 __host__
constexpr 函数。 当指定 --expt-relaxed-constexpr
时,nvcc 将定义宏 __CUDACC_RELAXED_CONSTEXPR__
。 请注意,即使对应的模板标记有关键字 constexpr
(C++11 标准章节 [dcl.constexpr.p6]
),函数模板实例化也可能不是 constexpr 函数。
14.5.22.5. Constexpr 变量
令 ‘V’ 表示命名空间作用域变量或已标记为 constexpr 且没有执行空间注解(例如,__device__、 __constant__、 __shared__
)的类静态成员变量。 V 被认为是主机代码变量。
如果 V 是标量类型 25,而不是 long double
,并且该类型不是 volatile 限定的,则 V 的值可以直接在设备代码中使用。 此外,如果 V 是非标量类型,则 V 的标量元素可以在 constexpr __device__
或 __host__ __device__
函数内部使用,如果对该函数的调用是常量表达式 26。 设备源代码不能包含对 V 的引用或获取 V 的地址。
示例
constexpr int xxx = 10;
constexpr int yyy = xxx + 4;
struct S1_t { static constexpr int qqq = 100; };
constexpr int host_arr[] = { 1, 2, 3};
constexpr __device__ int get(int idx) { return host_arr[idx]; }
__device__ int foo(int idx) {
int v1 = xxx + yyy + S1_t::qqq; // OK
const int &v2 = xxx; // error: reference to host constexpr
// variable
const int *v3 = &xxx; // error: address of host constexpr
// variable
const int &v4 = S1_t::qqq; // error: reference to host constexpr
// variable
const int *v5 = &S1_t::qqq; // error: address of host constexpr
// variable
v1 += get(2); // OK: 'get(2)' is a constant
// expression.
v1 += get(idx); // error: 'get(idx)' is not a constant
// expression
v1 += host_arr[2]; // error: 'host_arr' does not have
// scalar type.
return v1;
}
14.5.22.6. 内联命名空间
对于输入的 CUDA 翻译单元,CUDA 编译器可能会调用主机编译器来编译翻译单元中的主机代码。 在传递给主机编译器的代码中,如果输入的 CUDA 翻译单元包含以下任何实体的定义,则 CUDA 编译器将注入额外的编译器生成的代码
__global__
函数或函数模板实例化__device__
、__constant__
具有 surface 或 texture 类型的变量
编译器生成的代码包含对已定义实体的引用。 如果实体在内联命名空间中定义,并且在封闭命名空间中定义了另一个具有相同名称和类型签名的实体,则此引用可能被主机编译器视为不明确,并且主机编译将失败。
可以通过为在内联命名空间中定义的此类实体使用唯一名称来避免此限制。
示例
__device__ int Gvar;
inline namespace N1 {
__device__ int Gvar;
}
// <-- CUDA compiler inserts a reference to "Gvar" at this point in the
// translation unit. This reference will be considered ambiguous by the
// host compiler and compilation will fail.
示例
inline namespace N1 {
namespace N2 {
__device__ int Gvar;
}
}
namespace N2 {
__device__ int Gvar;
}
// <-- CUDA compiler inserts reference to "::N2::Gvar" at this point in
// the translation unit. This reference will be considered ambiguous by
// the host compiler and compilation will fail.
14.5.22.6.1. 内联未命名命名空间
以下实体不能在内联未命名命名空间中的命名空间作用域内声明
__managed__
、__device__
、__shared__
和__constant__
变量__global__
函数和函数模板具有 surface 或 texture 类型的变量
示例
inline namespace {
namespace N2 {
template <typename T>
__global__ void foo(void); // error
__global__ void bar(void) { } // error
template <>
__global__ void foo<int>(void) { } // error
__device__ int x1b; // error
__constant__ int x2b; // error
__shared__ int x3b; // error
texture<int> q2; // error
surface<int> s2; // error
}
};
14.5.22.7. thread_local
设备代码中不允许使用 thread_local
存储限定符。
14.5.22.8. __global__ 函数和函数模板
如果与 lambda 表达式关联的闭包类型在 __global__
函数模板实例化的模板参数中使用,则 lambda 表达式必须在 __device__
或 __global__
函数的直接或嵌套块作用域中定义,或者必须是扩展 lambda。
示例
template <typename T>
__global__ void kernel(T in) { }
__device__ void foo_device(void)
{
// All kernel instantiations in this function
// are valid, since the lambdas are defined inside
// a __device__ function.
kernel<<<1,1>>>( [] __device__ { } );
kernel<<<1,1>>>( [] __host__ __device__ { } );
kernel<<<1,1>>>( [] { } );
}
auto lam1 = [] { };
auto lam2 = [] __host__ __device__ { };
void foo_host(void)
{
// OK: instantiated with closure type of an extended __device__ lambda
kernel<<<1,1>>>( [] __device__ { } );
// OK: instantiated with closure type of an extended __host__ __device__
// lambda
kernel<<<1,1>>>( [] __host__ __device__ { } );
// error: unsupported: instantiated with closure type of a lambda
// that is not an extended lambda
kernel<<<1,1>>>( [] { } );
// error: unsupported: instantiated with closure type of a lambda
// that is not an extended lambda
kernel<<<1,1>>>( lam1);
// error: unsupported: instantiated with closure type of a lambda
// that is not an extended lambda
kernel<<<1,1>>>( lam2);
}
__global__
函数或函数模板不能声明为 constexpr
。
__global__
函数或函数模板不能具有 std::initializer_list
或 va_list
类型的参数。
__global__
函数不能具有右值引用类型的参数。
可变参数 __global__
函数模板具有以下限制
只允许使用单个包参数。
包参数必须列在模板参数列表的末尾。
示例
// ok
template <template <typename...> class Wrapper, typename... Pack>
__global__ void foo1(Wrapper<Pack...>);
// error: pack parameter is not last in parameter list
template <typename... Pack, template <typename...> class Wrapper>
__global__ void foo2(Wrapper<Pack...>);
// error: multiple parameter packs
template <typename... Pack1, int...Pack2, template<typename...> class Wrapper1,
template<int...> class Wrapper2>
__global__ void foo3(Wrapper1<Pack1...>, Wrapper2<Pack2...>);
14.5.22.10. 默认函数
CUDA 编译器会忽略在其首次声明时显式默认的函数上的执行空间限定符。 相反,CUDA 编译器将按照隐式声明和显式默认函数中所述推断执行空间限定符。
如果函数是显式默认的,但不是在其首次声明时,则不会忽略执行空间限定符。
示例
struct S1 {
// warning: __host__ annotation is ignored on a function that
// is explicitly-defaulted on its first declaration
__host__ S1() = default;
};
__device__ void foo1() {
//note: __device__ execution space is derived for S1::S1
// based on implicit call from within __device__ function
// foo1
S1 s1;
}
struct S2 {
__host__ S2();
};
//note: S2::S2 is not defaulted on its first declaration, and
// its execution space is fixed to __host__ based on its
// first declaration.
S2::S2() = default;
__device__ void foo2() {
// error: call from __device__ function 'foo2' to
// __host__ function 'S2::S2'
S2 s2;
}
14.5.23. C++14 特性
nvcc 也支持主机编译器默认启用的 C++14 特性。 传递 nvcc -std=c++14
标志会启用所有 C++14 特性,并使用相应的 C++14 方言选项调用主机预处理器、编译器和链接器27。 本节介绍了对支持的 C++14 特性的限制。
14.5.23.1. 具有推导返回类型的函数
__global__
函数不能具有推导返回类型。
如果一个 __device__
函数具有推导返回类型,CUDA 前端编译器将在调用主机编译器之前更改函数声明,使其具有 void
返回类型。这可能会导致在主机代码中内省 __device__
函数的推导返回类型时出现问题。因此,CUDA 编译器将针对在设备函数体外部引用此类推导返回类型的情况发出编译时错误,除非在未定义 __CUDA_ARCH__
时不存在此类引用。
示例
__device__ auto fn1(int x) {
return x;
}
__device__ decltype(auto) fn2(int x) {
return x;
}
__device__ void device_fn1() {
// OK
int (*p1)(int) = fn1;
}
// error: referenced outside device function bodies
decltype(fn1(10)) g1;
void host_fn1() {
// error: referenced outside device function bodies
int (*p1)(int) = fn1;
struct S_local_t {
// error: referenced outside device function bodies
decltype(fn2(10)) m1;
S_local_t() : m1(10) { }
};
}
// error: referenced outside device function bodies
template <typename T = decltype(fn2)>
void host_fn2() { }
template<typename T> struct S1_t { };
// error: referenced outside device function bodies
struct S1_derived_t : S1_t<decltype(fn1)> { };
14.5.23.2. 变量模板
当使用 Microsoft 主机编译器时,__device__/__constant__
变量模板不能具有 const 限定类型。
示例
// error: a __device__ variable template cannot
// have a const qualified type on Windows
template <typename T>
__device__ const T d1(2);
int *const x = nullptr;
// error: a __device__ variable template cannot
// have a const qualified type on Windows
template <typename T>
__device__ T *const d2(x);
// OK
template <typename T>
__device__ const T *d3;
__device__ void fn() {
int t1 = d1<int>;
int *const t2 = d2<int>;
const int *t3 = d3<int>;
}
14.5.24. C++17 特性
主机编译器默认启用的 C++17 特性也受 nvcc 支持。传递 nvcc -std=c++17
标志会启用所有 C++17 特性,并且还会使用相应的 C++17 方言选项调用主机预处理器、编译器和链接器 28。本节介绍对受支持的 C++17 特性的限制。
14.5.24.1. 内联变量
如果代码在整个程序编译模式下使用 nvcc 编译,则使用
__device__
或__constant__
或__managed__
内存空间限定符声明的命名空间作用域内联变量必须具有内部链接。示例
inline __device__ int xxx; //error when compiled with nvcc in //whole program compilation mode. //ok when compiled with nvcc in //separate compilation mode. inline __shared__ int yyy0; // ok. static inline __device__ int yyy; // ok: internal linkage namespace { inline __device__ int zzz; // ok: internal linkage }
当使用 g++ 主机编译器时,使用
__managed__
内存空间限定符声明的内联变量可能对调试器不可见。
14.5.24.2. 结构化绑定
结构化绑定不能使用变量内存空间限定符声明。
示例
struct S { int x; int y; };
__device__ auto [a1, b1] = S{4,5}; // error
14.5.25. C++20 特性
主机编译器默认启用的 C++20 特性也受 nvcc 支持。传递 nvcc -std=c++20
标志会启用所有 C++20 特性,并且还会使用相应的 C++20 方言选项调用主机预处理器、编译器和链接器 29。本节介绍对受支持的 C++20 特性的限制。
14.5.25.1. 模块支持
CUDA C++ 中不支持模块,无论是在主机代码还是设备代码中。module
、export
和 import
关键字的使用将被诊断为错误。
14.5.25.2. 协程支持
设备代码中不支持协程。co_await
、co_yield
和 co_return
关键字在设备函数范围内的使用将在设备编译期间被诊断为错误。
14.5.25.3. 三路比较运算符
三路比较运算符在主机代码和设备代码中都受支持,但某些用法隐式依赖于主机实现提供的标准模板库中的功能。这些运算符的使用可能需要指定标志 --expt-relaxed-constexpr
以消除警告,并且该功能要求主机实现满足设备代码的要求。
示例
#include<compare>
struct S {
int x, y, z;
auto operator<=>(const S& rhs) const = default;
__host__ __device__ bool operator<=>(int rhs) const { return false; }
};
__host__ __device__ bool f(S a, S b) {
if (a <=> 1) // ok, calls a user-defined host-device overload
return true;
return a < b; // call to an implicitly-declared function and requires
// a device-compatible std::strong_ordering implementation
}
14.5.25.4. Consteval 函数
通常,不允许跨执行空间调用,并且会导致编译器诊断(警告或错误)。当被调用函数使用 consteval
限定符声明时,此限制不适用。因此,__device__
或 __global__
函数可以调用 __host__
consteval
函数,而 __host__
函数可以调用 __device__ consteval
函数。
示例
namespace N1 {
//consteval host function
consteval int hcallee() { return 10; }
__device__ int dfunc() { return hcallee(); /* OK */ }
__global__ void gfunc() { (void)hcallee(); /* OK */ }
__host__ __device__ int hdfunc() { return hcallee(); /* OK */ }
int hfunc() { return hcallee(); /* OK */ }
} // namespace N1
namespace N2 {
//consteval device function
consteval __device__ int dcallee() { return 10; }
__device__ int dfunc() { return dcallee(); /* OK */ }
__global__ void gfunc() { (void)dcallee(); /* OK */ }
__host__ __device__ int hdfunc() { return dcallee(); /* OK */ }
int hfunc() { return dcallee(); /* OK */ }
}
14.6. 多态函数包装器
多态函数包装器类模板 nvstd::function
在 nvfunctional
头文件中提供。此class模板的实例可以用于存储、复制和调用任何可调用目标,例如 lambda 表达式。nvstd::function
可以在主机代码和设备代码中使用。
示例
#include <nvfunctional>
__device__ int foo_d() { return 1; }
__host__ __device__ int foo_hd () { return 2; }
__host__ int foo_h() { return 3; }
__global__ void kernel(int *result) {
nvstd::function<int()> fn1 = foo_d;
nvstd::function<int()> fn2 = foo_hd;
nvstd::function<int()> fn3 = []() { return 10; };
*result = fn1() + fn2() + fn3();
}
__host__ __device__ void hostdevice_func(int *result) {
nvstd::function<int()> fn1 = foo_hd;
nvstd::function<int()> fn2 = []() { return 10; };
*result = fn1() + fn2();
}
__host__ void host_func(int *result) {
nvstd::function<int()> fn1 = foo_h;
nvstd::function<int()> fn2 = foo_hd;
nvstd::function<int()> fn3 = []() { return 10; };
*result = fn1() + fn2() + fn3();
}
主机代码中的 nvstd::function
实例不能使用 __device__
函数的地址或 operator()
是 __device__
函数的 functor 进行初始化。设备代码中的 nvstd::function
实例不能使用 __host__
函数的地址或 operator()
是 __host__
函数的 functor 进行初始化。
nvstd::function
实例不能在运行时从主机代码传递到设备代码(反之亦然)。如果 __global__
函数是从主机代码启动的,则 nvstd::function
不能在 __global__
函数的参数类型中使用。
示例
#include <nvfunctional>
__device__ int foo_d() { return 1; }
__host__ int foo_h() { return 3; }
auto lam_h = [] { return 0; };
__global__ void k(void) {
// error: initialized with address of __host__ function
nvstd::function<int()> fn1 = foo_h;
// error: initialized with address of functor with
// __host__ operator() function
nvstd::function<int()> fn2 = lam_h;
}
__global__ void kern(nvstd::function<int()> f1) { }
void foo(void) {
// error: initialized with address of __device__ function
nvstd::function<int()> fn1 = foo_d;
auto lam_d = [=] __device__ { return 1; };
// error: initialized with address of functor with
// __device__ operator() function
nvstd::function<int()> fn2 = lam_d;
// error: passing nvstd::function from host to device
kern<<<1,1>>>(fn2);
}
nvstd::function
在 nvfunctional
头文件中定义如下
namespace nvstd {
template <class _RetType, class ..._ArgTypes>
class function<_RetType(_ArgTypes...)>
{
public:
// constructors
__device__ __host__ function() noexcept;
__device__ __host__ function(nullptr_t) noexcept;
__device__ __host__ function(const function &);
__device__ __host__ function(function &&);
template<class _F>
__device__ __host__ function(_F);
// destructor
__device__ __host__ ~function();
// assignment operators
__device__ __host__ function& operator=(const function&);
__device__ __host__ function& operator=(function&&);
__device__ __host__ function& operator=(nullptr_t);
__device__ __host__ function& operator=(_F&&);
// swap
__device__ __host__ void swap(function&) noexcept;
// function capacity
__device__ __host__ explicit operator bool() const noexcept;
// function invocation
__device__ _RetType operator()(_ArgTypes...) const;
};
// null pointer comparisons
template <class _R, class... _ArgTypes>
__device__ __host__
bool operator==(const function<_R(_ArgTypes...)>&, nullptr_t) noexcept;
template <class _R, class... _ArgTypes>
__device__ __host__
bool operator==(nullptr_t, const function<_R(_ArgTypes...)>&) noexcept;
template <class _R, class... _ArgTypes>
__device__ __host__
bool operator!=(const function<_R(_ArgTypes...)>&, nullptr_t) noexcept;
template <class _R, class... _ArgTypes>
__device__ __host__
bool operator!=(nullptr_t, const function<_R(_ArgTypes...)>&) noexcept;
// specialized algorithms
template <class _R, class... _ArgTypes>
__device__ __host__
void swap(function<_R(_ArgTypes...)>&, function<_R(_ArgTypes...)>&);
}
14.7. 扩展 Lambda
nvcc 标志 '--extended-lambda'
允许在 lambda 表达式中显式执行空间注释 30。执行空间注释应位于 'lambda-introducer' 之后和可选的 'lambda-declarator' 之前。当指定 '--extended-lambda'
标志时,nvcc 将定义宏 __CUDACC_EXTENDED_LAMBDA__
。
“扩展 __device__
lambda” 是一个 lambda 表达式,它显式地使用 ‘__device__
’ 注释,并且在 __host__
或 __host__ __device__
函数的直接或嵌套块作用域内定义。
“扩展 __host__ __device__
lambda” 是一个 lambda 表达式,它显式地同时使用 ‘__host__
’ 和 ‘__device__
’ 注释,并且在 __host__
或 __host__ __device__
函数的直接或嵌套块作用域内定义。
“扩展 lambda” 表示扩展 __device__
lambda 或扩展 __host__ __device__
lambda。扩展 lambda 可用于 __global__ 函数模板实例化 的类型参数中。
如果未显式指定执行空间注释,则会根据包围与 lambda 关联的闭包类的作用域来计算它们,如关于 C++11 支持的部分中所述。执行空间注释应用于与 lambda 关联的闭包类的所有方法。
示例
void foo_host(void) {
// not an extended lambda: no explicit execution space annotations
auto lam1 = [] { };
// extended __device__ lambda
auto lam2 = [] __device__ { };
// extended __host__ __device__ lambda
auto lam3 = [] __host__ __device__ { };
// not an extended lambda: explicitly annotated with only '__host__'
auto lam4 = [] __host__ { };
}
__host__ __device__ void foo_host_device(void) {
// not an extended lambda: no explicit execution space annotations
auto lam1 = [] { };
// extended __device__ lambda
auto lam2 = [] __device__ { };
// extended __host__ __device__ lambda
auto lam3 = [] __host__ __device__ { };
// not an extended lambda: explicitly annotated with only '__host__'
auto lam4 = [] __host__ { };
}
__device__ void foo_device(void) {
// none of the lambdas within this function are extended lambdas,
// because the enclosing function is not a __host__ or __host__ __device__
// function.
auto lam1 = [] { };
auto lam2 = [] __device__ { };
auto lam3 = [] __host__ __device__ { };
auto lam4 = [] __host__ { };
}
// lam1 and lam2 are not extended lambdas because they are not defined
// within a __host__ or __host__ __device__ function.
auto lam1 = [] { };
auto lam2 = [] __host__ __device__ { };
14.7.1. 扩展 Lambda 类型特征
编译器提供类型特征,以便在编译时检测扩展 lambda 的闭包类型
__nv_is_extended_device_lambda_closure_type(type)
:如果 ‘type’ 是为扩展 __device__
lambda 创建的闭包类,则该特征为 true,否则为 false。
__nv_is_extended_device_lambda_with_preserved_return_type(type)
:如果 ‘type’ 是为扩展 __device__
lambda 创建的闭包类,并且 lambda 是使用尾随返回类型(带有约束)定义的,则该特征为 true,否则为 false。如果尾随返回类型定义引用任何 lambda 参数名称,则不会保留返回类型。
__nv_is_extended_host_device_lambda_closure_type(type)
:如果 ‘type’ 是为扩展 __host__ __device__
lambda 创建的闭包类,则该特征为 true,否则为 false。
这些特征可以在所有编译模式下使用,无论是否启用 lambda 或扩展 lambda31。
示例
#define IS_D_LAMBDA(X) __nv_is_extended_device_lambda_closure_type(X)
#define IS_DPRT_LAMBDA(X) __nv_is_extended_device_lambda_with_preserved_return_type(X)
#define IS_HD_LAMBDA(X) __nv_is_extended_host_device_lambda_closure_type(X)
auto lam0 = [] __host__ __device__ { };
void foo(void) {
auto lam1 = [] { };
auto lam2 = [] __device__ { };
auto lam3 = [] __host__ __device__ { };
auto lam4 = [] __device__ () --> double { return 3.14; }
auto lam5 = [] __device__ (int x) --> decltype(&x) { return 0; }
// lam0 is not an extended lambda (since defined outside function scope)
static_assert(!IS_D_LAMBDA(decltype(lam0)), "");
static_assert(!IS_DPRT_LAMBDA(decltype(lam0)), "");
static_assert(!IS_HD_LAMBDA(decltype(lam0)), "");
// lam1 is not an extended lambda (since no execution space annotations)
static_assert(!IS_D_LAMBDA(decltype(lam1)), "");
static_assert(!IS_DPRT_LAMBDA(decltype(lam1)), "");
static_assert(!IS_HD_LAMBDA(decltype(lam1)), "");
// lam2 is an extended __device__ lambda
static_assert(IS_D_LAMBDA(decltype(lam2)), "");
static_assert(!IS_DPRT_LAMBDA(decltype(lam2)), "");
static_assert(!IS_HD_LAMBDA(decltype(lam2)), "");
// lam3 is an extended __host__ __device__ lambda
static_assert(!IS_D_LAMBDA(decltype(lam3)), "");
static_assert(!IS_DPRT_LAMBDA(decltype(lam3)), "");
static_assert(IS_HD_LAMBDA(decltype(lam3)), "");
// lam4 is an extended __device__ lambda with preserved return type
static_assert(IS_D_LAMBDA(decltype(lam4)), "");
static_assert(IS_DPRT_LAMBDA(decltype(lam4)), "");
static_assert(!IS_HD_LAMBDA(decltype(lam4)), "");
// lam5 is not an extended __device__ lambda with preserved return type
// because it references the operator()'s parameter types in the trailing return type.
static_assert(IS_D_LAMBDA(decltype(lam5)), "");
static_assert(!IS_DPRT_LAMBDA(decltype(lam5)), "");
static_assert(!IS_HD_LAMBDA(decltype(lam5)), "");
}
14.7.2. 扩展 Lambda 限制
CUDA 编译器将在调用主机编译器之前,用命名空间作用域中定义的占位符类型的实例替换扩展 lambda 表达式。占位符类型的模板参数需要获取包含原始扩展 lambda 表达式的函数的地址。这是 __global__
函数模板的模板参数涉及扩展 lambda 的闭包类型时,正确执行所必需的。外围函数 的计算方式如下。
根据定义,扩展 lambda 存在于 __host__
或 __host__ __device__
函数的直接或嵌套块作用域内。如果此函数不是 lambda 表达式的 operator()
,则它被认为是扩展 lambda 的外围函数。否则,扩展 lambda 在一个或多个外围 lambda 表达式的 operator()
的直接或嵌套块作用域内定义。如果最外层的此类 lambda 表达式在函数 F
的直接或嵌套块作用域内定义,则 F
是计算出的外围函数,否则外围函数不存在。
示例
void foo(void) {
// enclosing function for lam1 is "foo"
auto lam1 = [] __device__ { };
auto lam2 = [] {
auto lam3 = [] {
// enclosing function for lam4 is "foo"
auto lam4 = [] __host__ __device__ { };
};
};
}
auto lam6 = [] {
// enclosing function for lam7 does not exist
auto lam7 = [] __host__ __device__ { };
};
以下是关于扩展 lambda 的限制
扩展 lambda 不能在另一个扩展 lambda 表达式内部定义。
示例
void foo(void) { auto lam1 = [] __host__ __device__ { // error: extended lambda defined within another extended lambda auto lam2 = [] __host__ __device__ { }; }; }
扩展 lambda 不能在泛型 lambda 表达式内部定义。
示例
void foo(void) { auto lam1 = [] (auto) { // error: extended lambda defined within a generic lambda auto lam2 = [] __host__ __device__ { }; }; }
如果扩展 lambda 在一个或多个嵌套 lambda 表达式的直接或嵌套块作用域内定义,则最外层的此类 lambda 表达式必须在函数的直接或嵌套块作用域内定义。
示例
auto lam1 = [] { // error: outer enclosing lambda is not defined within a // non-lambda-operator() function. auto lam2 = [] __host__ __device__ { }; };
扩展 lambda 的外围函数必须是命名的,并且可以获取其地址。如果外围函数是类成员,则必须满足以下条件
所有包含成员函数的类都必须具有名称。
成员函数在其父类中不能具有私有或受保护的访问权限。
所有外围类在其各自的父类中都不能具有私有或受保护的访问权限。
示例
void foo(void) { // OK auto lam1 = [] __device__ { return 0; }; { // OK auto lam2 = [] __device__ { return 0; }; // OK auto lam3 = [] __device__ __host__ { return 0; }; } } struct S1_t { S1_t(void) { // Error: cannot take address of enclosing function auto lam4 = [] __device__ { return 0; }; } }; class C0_t { void foo(void) { // Error: enclosing function has private access in parent class auto temp1 = [] __device__ { return 10; }; } struct S2_t { void foo(void) { // Error: enclosing class S2_t has private access in its // parent class auto temp1 = [] __device__ { return 10; }; } }; };
在定义扩展 lambda 的点,必须可以明确地获取外围例程的地址。在某些情况下,这可能不可行,例如,当类 typedef 遮蔽了同名的模板类型参数时。
示例
template <typename> struct A { typedef void Bar; void test(); }; template<> struct A<void> { }; template <typename Bar> void A<Bar>::test() { /* In code sent to host compiler, nvcc will inject an address expression here, of the form: (void (A< Bar> ::*)(void))(&A::test)) However, the class typedef 'Bar' (to void) shadows the template argument 'Bar', causing the address expression in A<int>::test to actually refer to: (void (A< void> ::*)(void))(&A::test)) ..which doesn't take the address of the enclosing routine 'A<int>::test' correctly. */ auto lam1 = [] __host__ __device__ { return 4; }; } int main() { A<int> xxx; xxx.test(); }
扩展 lambda 不能在函数本地的类中定义。
示例
void foo(void) { struct S1_t { void bar(void) { // Error: bar is member of a class that is local to a function. auto lam4 = [] __host__ __device__ { return 0; }; } }; }
扩展 lambda 的外围函数不能具有推导返回类型。
示例
auto foo(void) { // Error: the return type of foo is deduced. auto lam1 = [] __host__ __device__ { return 0; }; }
__host__ __device__ 扩展 lambda 不能是泛型 lambda。
示例
void foo(void) { // Error: __host__ __device__ extended lambdas cannot be // generic lambdas. auto lam1 = [] __host__ __device__ (auto i) { return i; }; // Error: __host__ __device__ extended lambdas cannot be // generic lambdas. auto lam2 = [] __host__ __device__ (auto ...i) { return sizeof...(i); }; }
如果外围函数是函数模板或成员函数模板的实例化,和/或该函数是类模板的成员,则模板必须满足以下约束
模板最多只能有一个可变参数,并且它必须在模板参数列表中最后列出。
模板参数必须被命名。
模板实例化参数类型不能涉及函数本地的类型(扩展 lambda 的闭包类型除外),或者私有或受保护的类成员。
示例
template <typename T> __global__ void kern(T in) { in(); } template <typename... T> struct foo {}; template < template <typename...> class T, typename... P1, typename... P2> void bar1(const T<P1...>, const T<P2...>) { // Error: enclosing function has multiple parameter packs auto lam1 = [] __device__ { return 10; }; } template < template <typename...> class T, typename... P1, typename T2> void bar2(const T<P1...>, T2) { // Error: for enclosing function, the // parameter pack is not last in the template parameter list. auto lam1 = [] __device__ { return 10; }; } template <typename T, T> void bar3(void) { // Error: for enclosing function, the second template // parameter is not named. auto lam1 = [] __device__ { return 10; }; } int main() { foo<char, int, float> f1; foo<char, int> f2; bar1(f1, f2); bar2(f1, 10); bar3<int, 10>(); }
示例
template <typename T> __global__ void kern(T in) { in(); } template <typename T> void bar4(void) { auto lam1 = [] __device__ { return 10; }; kern<<<1,1>>>(lam1); } struct C1_t { struct S1_t { }; friend int main(void); }; int main() { struct S1_t { }; // Error: enclosing function for device lambda in bar4 // is instantiated with a type local to main. bar4<S1_t>(); // Error: enclosing function for device lambda in bar4 // is instantiated with a type that is a private member // of a class. bar4<C1_t::S1_t>(); }
对于 Visual Studio 主机编译器,外围函数必须具有外部链接。存在此限制是因为此主机编译器不支持使用非外部链接函数的地址作为模板参数,而 CUDA 编译器转换需要这样做来支持扩展 lambda。
对于 Visual Studio 主机编译器,扩展 lambda 不得在 ‘if-constexpr’ 块的主体内定义。
扩展 lambda 对捕获的变量具有以下限制
在发送到主机编译器的代码中,变量可以通过值传递给一系列辅助函数,然后用于直接初始化类类型的字段,该类类型用于表示扩展 lambda 的闭包类型32。
变量只能按值捕获。
如果数组维数大于 7,则不能捕获数组类型的变量。
对于数组类型的变量,在发送到主机编译器的代码中,闭包类型的数组字段首先被默认初始化,然后数组字段的每个元素都从捕获的数组变量的相应元素复制赋值。因此,数组元素类型必须在主机代码中是默认可构造和可复制赋值的。
作为可变参数包的元素的函数参数不能被捕获。
捕获的变量的类型不能涉及函数本地的类型(扩展 lambda 的闭包类型除外),或者私有或受保护的类成员。
对于 __host__ __device__ 扩展 lambda,lambda 表达式的
operator()
的返回类型或参数类型中使用的类型不能涉及函数本地的类型(扩展 lambda 的闭包类型除外),或者私有或受保护的类成员。__host__ __device__ 扩展 lambda 不支持 Init-capture。__device__ 扩展 lambda 支持 Init-capture,除非 init-capture 是数组类型或
std::initializer_list
类型。扩展 lambda 的函数调用运算符不是 constexpr。扩展 lambda 的闭包类型不是字面类型。constexpr 和 consteval 限定符不能在扩展 lambda 的声明中使用。
除非变量已经更早地在 if-constexpr 块外部隐式捕获,或者出现在扩展 lambda 的显式捕获列表中(见下面的示例),否则不能在词法上嵌套在扩展 lambda 内部的 if-constexpr 块内隐式捕获变量。
示例
void foo(void) { // OK: an init-capture is allowed for an // extended __device__ lambda. auto lam1 = [x = 1] __device__ () { return x; }; // Error: an init-capture is not allowed for // an extended __host__ __device__ lambda. auto lam2 = [x = 1] __host__ __device__ () { return x; }; int a = 1; // Error: an extended __device__ lambda cannot capture // variables by reference. auto lam3 = [&a] __device__ () { return a; }; // Error: by-reference capture is not allowed // for an extended __device__ lambda. auto lam4 = [&x = a] __device__ () { return x; }; struct S1_t { }; S1_t s1; // Error: a type local to a function cannot be used in the type // of a captured variable. auto lam6 = [s1] __device__ () { }; // Error: an init-capture cannot be of type std::initializer_list. auto lam7 = [x = {11}] __device__ () { }; std::initializer_list<int> b = {11,22,33}; // Error: an init-capture cannot be of type std::initializer_list. auto lam8 = [x = b] __device__ () { }; // Error scenario (lam9) and supported scenarios (lam10, lam11) // for capture within 'if-constexpr' block int yyy = 4; auto lam9 = [=] __device__ { int result = 0; if constexpr(false) { //Error: An extended __device__ lambda cannot first-capture // 'yyy' in constexpr-if context result += yyy; } return result; }; auto lam10 = [yyy] __device__ { int result = 0; if constexpr(false) { //OK: 'yyy' already listed in explicit capture list for the extended lambda result += yyy; } return result; }; auto lam11 = [=] __device__ { int result = yyy; if constexpr(false) { //OK: 'yyy' already implicit captured outside the 'if-constexpr' block result += yyy; } return result; }; }
在解析函数时,CUDA 编译器会为该函数中的每个扩展 lambda 分配一个计数器值。此计数器值用于传递给主机编译器的替换命名类型中。因此,扩展 lambda 是否在函数内定义不应取决于
__CUDA_ARCH__
的特定值,或取决于__CUDA_ARCH__
是否未定义。示例
template <typename T> __global__ void kernel(T in) { in(); } __host__ __device__ void foo(void) { // Error: the number and relative declaration // order of extended lambdas depends on // __CUDA_ARCH__ #if defined(__CUDA_ARCH__) auto lam1 = [] __device__ { return 0; }; auto lam1b = [] __host___ __device__ { return 10; }; #endif auto lam2 = [] __device__ { return 4; }; kernel<<<1,1>>>(lam2); }
如上所述,CUDA 编译器将主机函数中定义的
__device__
扩展 lambda 替换为命名空间作用域中定义的占位符类型。除非特征__nv_is_extended_device_lambda_with_preserved_return_type()
对于扩展 lambda 的闭包类型返回 true,否则占位符类型不会定义等效于原始 lambda 声明的operator()
函数。因此,尝试确定此类 lambda 的operator()
函数的返回类型或参数类型在主机代码中可能无法正常工作,因为主机编译器处理的代码在语义上将不同于 CUDA 编译器处理的输入代码。但是,在设备代码中内省operator()
函数的返回类型或参数类型是可以的。请注意,此限制不适用于__host__ __device__
扩展 lambda,或特征__nv_is_extended_device_lambda_with_preserved_return_type()
返回 true 的__device__
扩展 lambda。示例
#include <type_traits> const char& getRef(const char* p) { return *p; } void foo(void) { auto lam1 = [] __device__ { return "10"; }; // Error: attempt to extract the return type // of a __device__ lambda in host code std::result_of<decltype(lam1)()>::type xx1 = "abc"; auto lam2 = [] __host__ __device__ { return "10"; }; // OK : lam2 represents a __host__ __device__ extended lambda std::result_of<decltype(lam2)()>::type xx2 = "abc"; auto lam3 = [] __device__ () -> const char * { return "10"; }; // OK : lam3 represents a __device__ extended lambda with preserved return type std::result_of<decltype(lam3)()>::type xx2 = "abc"; static_assert( std::is_same_v< std::result_of<decltype(lam3)()>::type, const char *>); auto lam4 = [] __device__ (char x) -> decltype(getRef(&x)) { return 0; }; // lam4's return type is not preserved because it references the operator()'s // parameter types in the trailing return type. static_assert( ! __nv_is_extended_device_lambda_with_preserved_return_type(decltype(lam4)), "" ); }
对于扩展设备 lambda: - 仅在设备代码中支持内省 operator() 的参数类型。 - 仅在设备代码中支持内省 operator() 的返回类型,除非特征函数 __nv_is_extended_device_lambda_with_preserved_return_type() 返回 true。
如果由扩展 lambda 表示的 functor 对象从主机代码传递到设备代码(例如,作为
__global__
函数的参数),则 lambda 表达式主体中捕获变量的任何表达式都必须保持不变,无论是否定义了__CUDA_ARCH__
宏,以及该宏是否具有特定值。出现此限制是因为 lambda 的闭包类布局取决于编译器处理 lambda 表达式时遇到捕获变量的顺序;如果闭包类布局在设备和主机编译中不同,则程序可能无法正确执行。示例
__device__ int result; template <typename T> __global__ void kernel(T in) { result = in(); } void foo(void) { int x1 = 1; auto lam1 = [=] __host__ __device__ { // Error: "x1" is only captured when __CUDA_ARCH__ is defined. #ifdef __CUDA_ARCH__ return x1 + 1; #else return 10; #endif }; kernel<<<1,1>>>(lam1); }
如前所述,CUDA 编译器将扩展
__device__
lambda 表达式替换为发送到主机编译器的代码中的占位符类型的实例。此占位符类型在主机代码中未定义指向函数转换运算符的指针,但在设备代码中提供了转换运算符。请注意,此限制不适用于__host__ __device__
扩展 lambda。示例
template <typename T> __global__ void kern(T in) { int (*fp)(double) = in; // OK: conversion in device code is supported fp(0); auto lam1 = [](double) { return 1; }; // OK: conversion in device code is supported fp = lam1; fp(0); } void foo(void) { auto lam_d = [] __device__ (double) { return 1; }; auto lam_hd = [] __host__ __device__ (double) { return 1; }; kern<<<1,1>>>(lam_d); kern<<<1,1>>>(lam_hd); // OK : conversion for __host__ __device__ lambda is supported // in host code int (*fp)(double) = lam_hd; // Error: conversion for __device__ lambda is not supported in // host code. int (*fp2)(double) = lam_d; }
如前所述,CUDA 编译器将扩展
__device__
或__host__ __device__
lambda 表达式替换为发送到主机编译器的代码中的占位符类型的实例。此占位符类型可以定义 C++ 特殊成员函数(例如构造函数、析构函数)。因此,在 CUDA 前端编译器与主机编译器中,某些标准 C++ 类型特征可能会为扩展 lambda 的闭包类型返回不同的结果。以下类型特征受到影响:std::is_trivially_copyable
、std::is_trivially_constructible
、std::is_trivially_copy_constructible
、std::is_trivially_move_constructible
、std::is_trivially_destructible
。必须注意,这些类型特征的结果不能在
__global__
函数模板实例化或__device__ / __constant__ / __managed__
变量模板实例化中使用。示例
template <bool b> void __global__ foo() { printf("hi"); } template <typename T> void dolaunch() { // ERROR: this kernel launch may fail, because CUDA frontend compiler // and host compiler may disagree on the result of // std::is_trivially_copyable() trait on the closure type of the // extended lambda foo<std::is_trivially_copyable<T>::value><<<1,1>>>(); cudaDeviceSynchronize(); } int main() { int x = 0; auto lam1 = [=] __host__ __device__ () { return x; }; dolaunch<decltype(lam1)>(); }
CUDA 编译器将为 1-12 中描述的案例子集生成编译器诊断;对于案例 13-17 不会生成诊断,但主机编译器可能无法编译生成的代码。
14.7.3. 关于 __host__ __device__ lambda 的说明
与 __device__
lambda 不同,__host__ __device__
lambda 可以从主机代码调用。如前所述,CUDA 编译器将主机代码中定义的扩展 lambda 表达式替换为命名占位符类型的实例。扩展 __host__ __device__
lambda 的占位符类型使用间接函数调用 31 调用原始 lambda 的 operator()
。
间接函数调用的存在可能导致扩展 __host__ __device__
lambda 不如隐式或显式 __host__
only 的 lambda 受主机编译器优化。在后一种情况下,主机编译器可以轻松地将 lambda 的主体内联到调用上下文中。但是在扩展 __host__ __device__
lambda 的情况下,主机编译器会遇到间接函数调用,并且可能无法轻松地内联原始 __host__ __device__
lambda 主体。
14.7.4. 按值捕获 *this
当 lambda 在非静态类成员函数中定义,并且 lambda 的主体引用类成员变量时,C++11/C++14 规则要求按值捕获类的 this
指针,而不是引用的成员变量。如果 lambda 是在主机函数中定义的扩展 __device__
或 __host__
__device__
lambda,并且 lambda 在 GPU 上执行,则如果 this
指针指向主机内存,则访问 GPU 上引用的成员变量将导致运行时错误。
示例
#include <cstdio>
template <typename T>
__global__ void foo(T in) { printf("\n value = %d", in()); }
struct S1_t {
int xxx;
__host__ __device__ S1_t(void) : xxx(10) { };
void doit(void) {
auto lam1 = [=] __device__ {
// reference to "xxx" causes
// the 'this' pointer (S1_t*) to be captured by value
return xxx + 1;
};
// Kernel launch fails at run time because 'this->xxx'
// is not accessible from the GPU
foo<<<1,1>>>(lam1);
cudaDeviceSynchronize();
}
};
int main(void) {
S1_t s1;
s1.doit();
}
C++17 通过添加新的“*this”捕获模式解决了这个问题。在此模式下,编译器会复制 “*this” 表示的对象,而不是按值捕获指针 this
。“*this” 捕获模式在此处进行了更详细的描述:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0018r3.html
。
当使用 --extended-lambda
nvcc 标志时,CUDA 编译器支持在 __device__
和 __global__
函数中定义的 lambda 以及在主机代码中定义的扩展 __device__
lambda 的 “*this” 捕获模式。
以下是修改为使用 “*this” 捕获模式的上述示例
#include <cstdio>
template <typename T>
__global__ void foo(T in) { printf("\n value = %d", in()); }
struct S1_t {
int xxx;
__host__ __device__ S1_t(void) : xxx(10) { };
void doit(void) {
// note the "*this" capture specification
auto lam1 = [=, *this] __device__ {
// reference to "xxx" causes
// the object denoted by '*this' to be captured by
// value, and the GPU code will access copy_of_star_this->xxx
return xxx + 1;
};
// Kernel launch succeeds
foo<<<1,1>>>(lam1);
cudaDeviceSynchronize();
}
};
int main(void) {
S1_t s1;
s1.doit();
}
主机代码中定义的未注释 lambda 或扩展 __host__
__device__
lambda 不允许使用 “*this” 捕获模式。支持和不支持的用法示例
struct S1_t {
int xxx;
__host__ __device__ S1_t(void) : xxx(10) { };
void host_func(void) {
// OK: use in an extended __device__ lambda
auto lam1 = [=, *this] __device__ { return xxx; };
// Error: use in an extended __host__ __device__ lambda
auto lam2 = [=, *this] __host__ __device__ { return xxx; };
// Error: use in an unannotated lambda in host function
auto lam3 = [=, *this] { return xxx; };
}
__device__ void device_func(void) {
// OK: use in a lambda defined in a __device__ function
auto lam1 = [=, *this] __device__ { return xxx; };
// OK: use in a lambda defined in a __device__ function
auto lam2 = [=, *this] __host__ __device__ { return xxx; };
// OK: use in a lambda defined in a __device__ function
auto lam3 = [=, *this] { return xxx; };
}
__host__ __device__ void host_device_func(void) {
// OK: use in an extended __device__ lambda
auto lam1 = [=, *this] __device__ { return xxx; };
// Error: use in an extended __host__ __device__ lambda
auto lam2 = [=, *this] __host__ __device__ { return xxx; };
// Error: use in an unannotated lambda in a __host__ __device__ function
auto lam3 = [=, *this] { return xxx; };
}
};
14.7.5. 其他说明
ADL 查找
:如前所述,CUDA 编译器将在调用主机编译器之前,用占位符类型的实例替换扩展 lambda 表达式。占位符类型的一个模板参数使用包含原始 lambda 表达式的函数的地址。对于任何参数类型涉及扩展 lambda 表达式的闭包类型的主机函数调用,这可能会导致其他命名空间参与参数依赖查找 (ADL)。这可能会导致主机编译器选择不正确的函数。示例
namespace N1 { struct S1_t { }; template <typename T> void foo(T); }; namespace N2 { template <typename T> int foo(T); template <typename T> void doit(T in) { foo(in); } } void bar(N1::S1_t in) { /* extended __device__ lambda. In the code sent to the host compiler, this is replaced with the placeholder type instantiation expression ' __nv_dl_wrapper_t< __nv_dl_tag<void (*)(N1::S1_t in),(&bar),1> > { }' As a result, the namespace 'N1' participates in ADL lookup of the call to "foo" in the body of N2::doit, causing ambiguity. */ auto lam1 = [=] __device__ { }; N2::doit(lam1); }
在上面的示例中,CUDA 编译器用涉及
N1
命名空间的占位符类型替换了扩展 lambda。因此,命名空间N1
参与了N2::doit
主体中foo(in)
的 ADL 查找,并且主机编译失败,因为找到了多个重载候选项N1::foo
和N2::foo
。
14.8. 放宽 Constexpr (-expt-relaxed-constexpr)
默认情况下,不支持以下跨执行空间调用
在主机代码生成阶段(即,当
__CUDA_ARCH__
宏未定义时),从__host__
函数调用__device__
-onlyconstexpr
函数。示例constexpr __device__ int D() { return 0; } int main() { int x = D(); //ERROR: calling a __device__-only constexpr function from host code }
在设备代码生成阶段(即,当
__CUDA_ARCH__
宏已定义时),从__device__
或__global__
函数调用__host__
-onlyconstexpr
函数。示例constexpr int H() { return 0; } __device__ void dmain() { int x = H(); //ERROR: calling a __host__-only constexpr function from device code }
实验性标志 -expt-relaxed-constexpr
可用于放宽此约束。当指定此标志时,编译器将支持上述跨执行空间调用,如下所示
如果跨执行空间调用发生在需要常量求值的上下文中,例如,在 constexpr 变量的初始化程序中,则支持对 constexpr 函数的跨执行空间调用。示例
constexpr __host__ int H(int x) { return x+1; }; __global__ void doit() { constexpr int val = H(1); // OK: call is in a context that // requires constant evaluation. } constexpr __device__ int D(int x) { return x+1; } int main() { constexpr int val = D(1); // OK: call is in a context that // requires constant evaluation. }
否则
在设备代码生成期间,将为主机代码生成
__host__
-only constexpr 函数H
的主体,除非H
未使用或仅在 constexpr 上下文中调用。示例// NOTE: "H" is emitted in generated device code because it is // called from device code in a non-constexpr context constexpr __host__ int H(int x) { return x+1; } __device__ int doit(int in) { in = H(in); // OK, even though argument is not a constant expression return in; }
适用于 ``__device__`` 函数的所有代码限制也适用于从设备代码调用的 ``constexpr host``-only 函数 ``H``。但是,编译器可能不会针对 ``H`` 的这些限制发出任何构建时诊断信息 15 。
例如,以下代码模式在
H
的主体中不受支持(与任何__device__
函数一样),但可能不会生成任何编译器诊断信息主机变量或
__host__
-only 非 constexpr 函数的 ODR 使用。示例int qqq, www; constexpr __host__ int* H(bool b) { return b ? &qqq : &www; }; __device__ int doit(bool flag) { int *ptr; ptr = H(flag); // ERROR: H() attempts to refer to host variables 'qqq' and 'www'. // code will compile, but will NOT execute correctly. return *ptr; }
异常 (
throw/catch
) 和 RTTI (typeid, dynamic_cast
) 的使用。示例struct Base { }; struct Derived : public Base { }; // NOTE: "H" is emitted in generated device code constexpr int H(bool b, Base *ptr) { if (b) { return 1; } else if (typeid(ptr) == typeid(Derived)) { // ERROR: use of typeid in code executing on the GPU return 2; } else { throw int{4}; // ERROR: use of throw in code executing on the GPU } } __device__ void doit(bool flag) { int val; Derived d; val = H(flag, &d); //ERROR: H() attempts use typeid and throw(), which are not allowed in code that executes on the GPU }
在主机代码生成期间,
__device__
-only constexpr 函数D
的主体保留在发送到主机编译器的代码中。如果D
的主体尝试 ODR 使用命名空间作用域设备变量或__device__
-only 非 constexpr 函数,则不支持从主机代码调用D
(代码可能在没有编译器诊断信息的情况下构建,但在运行时可能行为不正确)。示例__device__ int qqq, www; constexpr __device__ int* D(bool b) { return b ? &qqq : &www; }; int doit(bool flag) { int *ptr; ptr = D(flag); // ERROR: D() attempts to refer to device variables 'qqq' and 'www' // code will compile, but will NOT execute correctly. return *ptr; }
注意:鉴于上述限制和缺乏针对不正确用法的编译器诊断信息,从设备代码调用标准 C++ 头文件中的 constexpr __host__ 函数时要小心,因为函数的实现会因主机平台而异,例如,基于 gcc 主机编译器的
libstdc++
版本。当移植到不同的平台或主机编译器版本时,此类代码可能会静默地中断(如果目标 C++ 库实现 odr 使用主机代码变量或函数,如前所述)。示例
__device__ int get(int in) { int val = std::foo(in); // "std::foo" is constexpr function defined in the host compiler's standard library header // WARNING: if std::foo implementation ODR-uses host variables or functions, // code will not work correctly }
- 15
诊断通常在解析期间生成,但主机-only 函数
H
可能已经在稍后在转换单元中遇到从设备代码调用H
之前被解析。
14.9. 代码示例
14.9.1. 数据聚合类
class PixelRGBA {
public:
__device__ PixelRGBA(): r_(0), g_(0), b_(0), a_(0) { }
__device__ PixelRGBA(unsigned char r, unsigned char g,
unsigned char b, unsigned char a = 255):
r_(r), g_(g), b_(b), a_(a) { }
private:
unsigned char r_, g_, b_, a_;
friend PixelRGBA operator+(const PixelRGBA&, const PixelRGBA&);
};
__device__
PixelRGBA operator+(const PixelRGBA& p1, const PixelRGBA& p2)
{
return PixelRGBA(p1.r_ + p2.r_, p1.g_ + p2.g_,
p1.b_ + p2.b_, p1.a_ + p2.a_);
}
__device__ void func(void)
{
PixelRGBA p1, p2;
// ... // Initialization of p1 and p2 here
PixelRGBA p3 = p1 + p2;
}
14.9.2. 派生类
__device__ void* operator new(size_t bytes, MemoryPool& p);
__device__ void operator delete(void*, MemoryPool& p);
class Shape {
public:
__device__ Shape(void) { }
__device__ void putThis(PrintBuffer *p) const;
__device__ virtual void Draw(PrintBuffer *p) const {
p->put("Shapeless");
}
__device__ virtual ~Shape() {}
};
class Point : public Shape {
public:
__device__ Point() : x(0), y(0) {}
__device__ Point(int ix, int iy) : x(ix), y(iy) { }
__device__ void PutCoord(PrintBuffer *p) const;
__device__ void Draw(PrintBuffer *p) const;
__device__ ~Point() {}
private:
int x, y;
};
__device__ Shape* GetPointObj(MemoryPool& pool)
{
Shape* shape = new(pool) Point(rand(-20,10), rand(-100,-20));
return shape;
}
14.9.3. 类模板
template <class T>
class myValues {
T values[MAX_VALUES];
public:
__device__ myValues(T clear) { ... }
__device__ void setValue(int Idx, T value) { ... }
__device__ void putToMemory(T* valueLocation) { ... }
};
template <class T>
void __global__ useValues(T* memoryBuffer) {
myValues<T> myLocation(0);
...
}
__device__ void* buffer;
int main()
{
...
useValues<int><<<blocks, threads>>>(buffer);
...
}
14.9.4. 函数模板
template <typename T>
__device__ bool func(T x)
{
...
return (...);
}
template <>
__device__ bool func<int>(T x) // Specialization
{
return true;
}
// Explicit argument specification
bool result = func<double>(0.5);
// Implicit argument deduction
int x = 1;
bool result = func(x);
14.9.5. Functor 类
class Add {
public:
__device__ float operator() (float a, float b) const
{
return a + b;
}
};
class Sub {
public:
__device__ float operator() (float a, float b) const
{
return a - b;
}
};
// Device code
template<class O> __global__
void VectorOperation(const float * A, const float * B, float * C,
unsigned int N, O op)
{
unsigned int iElement = blockDim.x * blockIdx.x + threadIdx.x;
if (iElement < N)
C[iElement] = op(A[iElement], B[iElement]);
}
// Host code
int main()
{
...
VectorOperation<<<blocks, threads>>>(v1, v2, v3, N, Add());
...
}
- 16
例如,用于启动内核的
<<<...>>>
语法。- 17
这不适用于可能在多个转换单元中定义的实体,例如编译器生成的模板实例化。
- 18
目的是允许在设备编译期间为
__host__ __device__
函数中的静态变量使用变量内存空间限定符,但在主机编译期间不允许这样做- 19
调试类型
C
的可疑布局不匹配的一种方法是在主机和设备代码中使用printf
输出sizeof(C)
和offsetof(C, field)
的值。- 20
请注意,由于存在额外的声明,这可能会对编译时间产生负面影响。
- 21
目前,
-std=c++11
标志仅支持以下主机编译器:gcc 版本 >= 4.7、clang、icc >= 15 和 xlc >= 13.1- 22
包括
operator()
- 23
限制与非 constexpr 被调用函数相同。
- 24
请注意,实验性标志的行为可能会在未来的编译器版本中更改。
- 25
C++ 标准部分
[basic.types]
- 26
C++ 标准部分
[expr.const]
- 27
目前,
-std=c++14
标志仅支持以下主机编译器:gcc 版本 >= 5.1、clang 版本 >= 3.7 和 icc 版本 >= 17- 28
目前,以下主机编译器支持
-std=c++17
标志:gcc 版本 >= 7.0,clang 版本 >= 8.0,Visual Studio 版本 >= 2017,pgi 编译器版本 >= 19.0,icc 编译器版本 >= 19.0- 29
目前,以下主机编译器支持
-std=c++20
标志:gcc 版本 >= 10.0,clang 版本 >= 10.0,Visual Studio 版本 >= 2022 和 nvc++ 版本 >= 20.7。- 30
当使用 icc 主机编译器时,此标志仅适用于 icc >= 1800。
- 31(1,2)
如果扩展 lambda 模式未激活,则 traits 将始终返回 false。
- 32
相比之下,C++ 标准规定捕获的变量用于直接初始化闭包类型的字段。
15. 纹理获取
本节给出了用于计算 纹理函数 的纹理函数返回值的公式,具体取决于纹理对象的各种属性(请参阅 纹理和表面内存)。
绑定到纹理对象的纹理表示为一个数组 T,其中包含
对于一维纹理,N 个纹素,
对于二维纹理,N x M 个纹素,
对于三维纹理,N x M x L 个纹素。
它使用非归一化纹理坐标 x、y 和 z,或归一化纹理坐标 x/N、y/M 和 z/L 进行获取,如 纹理内存 中所述。在本节中,假定坐标在有效范围内。纹理内存 解释了如何根据寻址模式将超出范围的坐标重新映射到有效范围。
15.1. 最近点采样
在这种滤波模式下,纹理获取返回的值为
对于一维纹理,tex(x)=T[i],
对于二维纹理,tex(x,y)=T[i,j],
对于三维纹理,tex(x,y,z)=T[i,j,k],
其中 i=floor(x),j=floor(y),k=floor(z)。
图 32 说明了 N=4 的一维纹理的最近点采样。

图 35 最近点采样滤波模式
对于整数纹理,纹理获取返回的值可以选择性地重新映射到 [0.0, 1.0](请参阅 纹理内存)。
15.2. 线性滤波
在这种滤波模式下,仅适用于浮点纹理,纹理获取返回的值为
\(tex(x)=(1-\alpha)T[i]+{\alpha}T[i+1]\) 对于一维纹理,
\(tex(x)=(1-\alpha)T[i]+{\alpha}T[i+1]\) 对于一维纹理,
\(tex(x,y)=(1-\alpha)(1-\beta)T[i,j]+\alpha(1-\beta)T[i+1,j]+(1-\alpha){\beta}T[i,j+1]+\alpha{\beta}T[i+1,j+1]\) 对于二维纹理,
\(tex(x,y,z)\) =
\((1-\alpha)(1-\beta)(1-\gamma)T[i,j,k]+\alpha(1-\beta)(1-\gamma)T[i+1,j,k]+\)
\((1-\alpha)\beta(1-\gamma)T[i,j+1,k]+\alpha\beta(1-\gamma)T[i+1,j+1,k]+\)
\((1-\alpha)(1-\beta){\gamma}T[i,j,k+1]+\alpha(1-\beta){\gamma}T[i+1,j,k+1]+\)
\((1-\alpha)\beta{\gamma}T[i,j+1,k+1]+\alpha\beta{\gamma}T[i+1,j+1,k+1]\)
对于三维纹理,
其中
\(i=floor(x\ B)*, \alpha=frac(x\ B)*, *x\ B\ =x-0.5,\)
\(j=floor(y\ B)*, \beta=frac(y\ B)*, *y\ B\ =y-0.5,\)
\(k=floor(z\ B)*, \gamma=frac(z\ B)*, *z\ B\ = z-0.5,\)
\(\alpha\)、 \(\beta\) 和 \(\gamma\) 以 9 位定点格式存储,其中 8 位为小数位值(因此 1.0 可以精确表示)。
图 33 说明了 N=4 的一维纹理的线性滤波。

图 36 线性滤波模式
15.3. 查找表
查找表 TL(x),其中 x 跨越区间 [0,R],可以实现为 TL(x)=tex((N-1)/R)x+0.5),以确保 TL(0)=T[0] 和 TL(R)=T[N-1]。
图 34 说明了使用纹理滤波来实现查找表,其中 R=4 或 R=1 来自 N=4 的一维纹理。

图 37 使用线性滤波的一维查找表
16. 计算能力
计算设备的通用规范和功能取决于其计算能力(请参阅 计算能力)。
表 20 和 表 21 显示了与当前支持的每种计算能力相关联的功能和技术规范。
浮点标准 回顾了对 IEEE 浮点标准的合规性。
章节 计算能力 5.x、 计算能力 6.x、 计算能力 7.x、 计算能力 8.x 和 计算能力 9.0 分别详细介绍了计算能力为 5.x、6.x、7.x、8.x 和 9.0 的设备的架构。
16.1. 功能可用性
计算功能是随着计算架构引入的,目的是使该功能在所有后续架构上都可用。表 20 中的“是”表示该功能在其引入后的计算能力上的可用性。
随着架构引入的高度专业化的计算功能可能无法保证在所有后续计算能力上都可用。这些功能旨在加速不适用于所有类别的计算能力(由计算能力的次要版本号表示)或可能在未来几代中发生重大变化的专用操作(由计算能力的主要版本号表示)。
对于给定的计算能力,可能存在两组计算功能
计算能力 #.#:随着引入的,旨在用于后续计算架构的主要计算功能集。这些功能及其可用性汇总在表 20 中。
计算能力 #.#a:一小部分高度专业化的功能集,旨在加速专用操作,这些操作不能保证可用或可能在后续计算架构上发生重大变化。这些功能汇总在各自的“计算能力 #.#”子节中。
设备代码的编译目标是特定的计算能力。设备代码中出现的功能必须适用于目标计算能力。例如
compute_90
编译目标允许使用计算能力 9.0 的功能,但不允许使用计算能力 9.0a 的功能。compute_90a
编译目标允许使用全套计算设备功能,包括 9.0a 功能和 9.0 功能。
16.2. 功能和技术规范
功能支持 |
计算能力 |
|||||||
---|---|---|---|---|---|---|---|---|
(所有计算能力均支持未列出的功能) |
5.0, 5.2 |
5.3 |
6.x |
7.x |
8.x |
9.0 |
10.x |
12.0 |
对全局内存中的 32 位整数值进行操作的原子函数(原子函数) |
是 |
|||||||
对共享内存中的 32 位整数值进行操作的原子函数(原子函数) |
是 |
|||||||
对全局内存中的 64 位整数值进行操作的原子函数(原子函数) |
是 |
|||||||
对共享内存中的 64 位整数值进行操作的原子函数(原子函数) |
是 |
|||||||
对全局内存中的 128 位整数值进行操作的原子函数(原子函数) |
否 |
是 |
||||||
对共享内存中的 128 位整数值进行操作的原子函数(原子函数) |
否 |
是 |
||||||
对全局和共享内存中的 32 位浮点值进行操作的原子加法(atomicAdd()) |
是 |
|||||||
对全局内存和共享内存中的 64 位浮点值进行操作的原子加法(atomicAdd()) |
否 |
是 |
||||||
对全局内存中的 float2 和 float4 浮点向量进行操作的原子加法(atomicAdd()) |
否 |
是 |
||||||
Warp 投票函数(Warp 投票函数) |
是 |
|||||||
内存栅栏函数(内存栅栏函数) |
是 |
|||||||
同步函数(同步函数) |
是 |
|||||||
表面函数(表面函数) |
是 |
|||||||
统一内存编程(统一内存编程) |
是 |
|||||||
动态并行性(CUDA 动态并行性) |
是 |
|||||||
半精度浮点运算:加法、减法、乘法、比较、warp shuffle 函数、转换 |
否 |
是 |
||||||
Bfloat16 精度浮点运算:加法、减法、乘法、比较、warp shuffle 函数、转换 |
否 |
是 |
||||||
Tensor Cores |
否 |
是 |
||||||
混合精度 Warp-Matrix 函数(Warp Matrix 函数) |
否 |
是 |
||||||
硬件加速的 |
否 |
是 |
||||||
硬件加速的 Split Arrive/Wait Barrier(异步 Barrier) |
否 |
是 |
||||||
L2 缓存驻留管理(设备内存 L2 访问管理) |
否 |
是 |
||||||
用于加速动态编程的 DPX 指令 |
否 |
是 |
||||||
分布式共享内存 |
否 |
是 |
||||||
线程块集群 |
否 |
是 |
||||||
Tensor 内存加速器 (TMA) 单元 |
否 |
是 |
请注意,下表中使用 KB 和 K 单位分别对应 1024 字节(即 KiB)和 1024。
计算能力 |
||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
技术规范 |
5.0 |
5.2 |
5.3 |
6.0 |
6.1 |
6.2 |
7.0 |
7.2 |
7.5 |
8.0 |
8.6 |
8.7 |
8.9 |
9.0 |
10.x |
12.0 |
每个设备的最大常驻网格数(并发内核执行) |
32 |
16 |
128 |
32 |
16 |
128 |
16 |
128 |
||||||||
线程块网格的最大维度 |
3 |
|||||||||||||||
线程块网格的最大 x 维度 [线程块] |
231-1 |
|||||||||||||||
线程块网格的最大 y 或 z 维度 |
65535 |
|||||||||||||||
线程块的最大维度 |
3 |
|||||||||||||||
块的最大 x 或 y 维度 |
1024 |
|||||||||||||||
块的最大 z 维度 |
64 |
|||||||||||||||
每个块的最大线程数 |
1024 |
|||||||||||||||
Warp 大小 |
32 |
|||||||||||||||
每个 SM 的最大常驻块数 |
32 |
16 |
32 |
16 |
24 |
32 |
||||||||||
每个 SM 的最大常驻 warp 数 |
64 |
32 |
64 |
48 |
64 |
48 |
||||||||||
每个 SM 的最大常驻线程数 |
2048 |
1024 |
2048 |
1536 |
2048 |
1536 |
||||||||||
每个 SM 的 32 位寄存器数 |
64 K |
|||||||||||||||
每个线程块的最大 32 位寄存器数 |
64 K |
32 K |
64 K |
32 K |
64 K |
|||||||||||
每个线程的最大 32 位寄存器数 |
255 |
|||||||||||||||
每个 SM 的最大共享内存量 |
64 KB |
96 KB |
64 KB |
96 KB |
64 KB |
96 KB |
64 KB |
164 KB |
100 KB |
164 KB |
100 KB |
228 KB |
100 KB |
|||
每个线程块的最大共享内存量 33 |
48 KB |
96 KB |
96 KB |
64 KB |
163 KB |
99 KB |
163 KB |
99 KB |
227 KB |
99 KB |
||||||
共享内存库的数量 |
32 |
|||||||||||||||
每个线程的最大本地内存量 |
512 KB |
|||||||||||||||
常量内存大小 |
64 KB |
|||||||||||||||
每个 SM 的常量内存缓存工作集 |
8 KB |
4 KB |
8 KB |
|||||||||||||
每个 SM 的纹理内存缓存工作集 |
介于 12 KB 和 48 KB 之间 |
介于 24 KB 和 48 KB 之间 |
32 ~ 128 KB |
32 或 64 KB |
28 KB ~ 192 KB |
28 KB ~ 128 KB |
28 KB ~ 192 KB |
28 KB ~ 128 KB |
28 KB ~ 256 KB |
28 KB ~ 128 KB |
||||||
使用 CUDA 数组的 1D 纹理对象的最大宽度 |
65536 |
131072 |
||||||||||||||
使用线性内存的 1D 纹理对象的最大宽度 |
227 |
228 |
227 |
228 |
227 |
228 |
||||||||||
1D 分层纹理对象的最大宽度和层数 |
16384 x 2048 |
32768 x 2048 |
||||||||||||||
使用 CUDA 数组的 2D 纹理对象的最大宽度和高度 |
65536 x 65536 |
131072 x 65536 |
||||||||||||||
使用线性内存的 2D 纹理对象的最大宽度和高度 |
65536 x 65536 |
131072 x 65000 |
||||||||||||||
使用支持纹理 gather 的 CUDA 数组的 2D 纹理对象的最大宽度和高度 |
16384 x 16384 |
32768 x 32768 |
||||||||||||||
2D 分层纹理对象的最大宽度、高度和层数 |
16384 x 16384 x 2048 |
32768 x 32768 x 2048 |
||||||||||||||
使用 CUDA 数组的 3D 纹理对象的最大宽度、高度和深度 |
4096 x 4096 x 4096 |
16384 x 16384 x 16384 |
||||||||||||||
立方体贴图纹理对象的最大宽度(和高度) |
16384 |
32768 |
||||||||||||||
立方体贴图分层纹理对象的最大宽度(和高度)和层数 |
16384 x 2046 |
32768 x 2046 |
||||||||||||||
可以绑定到内核的最大纹理数 |
256 |
|||||||||||||||
使用 CUDA 数组的 1D 表面对象的最大宽度 |
16384 |
32768 |
||||||||||||||
1D 分层表面对象的最大宽度和层数 |
16384 x 2048 |
32768 x 2048 |
||||||||||||||
使用 CUDA 数组的 2D 表面对象的最大宽度和高度 |
65536 x 65536 |
1 31072 x 65536 |
||||||||||||||
2D 分层表面对象的最大宽度、高度和层数 |
16384 x 16384 x 2048 |
32768 x 32768 x 1048 |
||||||||||||||
使用 CUDA 数组的 3D 表面对象的最大宽度、高度和深度 |
4096 x 4096 x 4096 |
16384 x 16384 x 16384 |
||||||||||||||
使用 CUDA 数组的立方体贴图表面对象的最大宽度(和高度) |
16384 |
32768 |
||||||||||||||
立方体贴图分层表面对象的最大宽度(和高度)和层数 |
16384 x 2046 |
32768 x 2046 |
||||||||||||||
可以使用内核的最大表面数 |
16 |
32 |
16.3. 浮点标准
所有计算设备都遵循 IEEE 754-2008 二进制浮点算术标准,但存在以下偏差
没有动态可配置的舍入模式;但是,大多数操作都支持多种 IEEE 舍入模式,通过设备内部函数公开。
没有用于检测是否发生浮点异常的机制,并且所有操作的行为都如同 IEEE-754 异常始终被屏蔽一样,并且如果发生异常事件,则传递 IEEE-754 定义的屏蔽响应。出于同样的原因,虽然支持 SNaN 编码,但它们不是 signaling 的,而是作为 quiet 处理。
涉及一个或多个输入 NaN 的单精度浮点运算的结果是位模式为 0x7fffffff 的 quiet NaN。
双精度浮点绝对值和取反在 NaN 方面不符合 IEEE-754;这些值保持不变地传递。
代码必须使用 -ftz=false
、-prec-div=true
和 -prec-sqrt=true
进行编译,以确保符合 IEEE 标准(这是默认设置;有关这些编译标志的描述,请参阅 nvcc
用户手册)。
无论编译器标志 -ftz
的设置如何,
全局内存上的原子单精度浮点加法始终以 flush-to-zero 模式运行,即行为等同于
FADD.F32.FTZ.RN
,共享内存上的原子单精度浮点加法始终以非规格化数支持运行,即行为等同于
FADD.F32.RN
。
根据 IEEE-754R 标准,如果 fminf()
、fmin()
、fmaxf()
或 fmax()
的输入参数之一是 NaN,但另一个不是,则结果为非 NaN 参数。
在浮点值超出整数格式范围的情况下,将浮点值转换为整数值的行为由 IEEE-754 未定义。对于计算设备,行为是钳制到支持范围的末尾。这与 x86 架构的行为不同。
IEEE-754 未定义整数除以零和整数溢出的行为。对于计算设备,没有用于检测是否发生此类整数运算异常的机制。整数除以零会产生未指定的、特定于机器的值。
https://developer.nvidia.com/content/precision-performance-floating-point-and-ieee-754-compliance-nvidia-gpus 包含有关 NVIDIA GPU 的浮点精度和合规性的更多信息。
16.4. 计算能力 5.x
16.4.1. 架构
一个 SM 由以下部分组成
128 个 CUDA 核心用于算术运算(有关算术运算的吞吐量,请参阅 算术指令),
32 个特殊功能单元用于单精度浮点超越函数,
4 个 warp 调度器。
当为 SM 提供要执行的 warp 时,它首先将它们分配给四个调度器。然后,在每个指令发出时间,每个调度器为其分配的准备好执行的 warp 之一发出一个指令(如果有)。
一个 SM 具有
一个只读常量缓存,所有功能单元共享该缓存,并加速从常量内存空间(驻留在设备内存中)的读取,
一个 24 KB 的统一 L1/纹理缓存,用于缓存从全局内存的读取,
对于计算能力 5.0 的设备为 64 KB 共享内存,对于计算能力 5.2 的设备为 96 KB 共享内存。
统一 L1/纹理缓存也由纹理单元使用,该纹理单元实现了 纹理和表面内存 中提到的各种寻址模式和数据滤波。
还有一个由所有 SM 共享的 L2 缓存,用于缓存对本地或全局内存的访问,包括临时寄存器溢出。应用程序可以通过检查 l2CacheSize
设备属性来查询 L2 缓存大小(请参阅 设备枚举)。
缓存行为(例如,读取是否在统一 L1/纹理缓存和 L2 中缓存,还是仅在 L2 中缓存)可以在每次访问的基础上使用加载指令的修饰符部分配置。
16.4.2. 全局内存
全局内存访问始终在 L2 中缓存。
内核的整个生命周期内只读的数据也可以通过使用 __ldg()
函数读取(请参阅 只读数据缓存加载函数)在上一节中描述的统一 L1/纹理缓存中缓存。当编译器检测到某些数据满足只读条件时,它将使用 __ldg()
读取它。编译器可能并非始终能够检测到某些数据满足只读条件。使用 const
和 __restrict__
限定符标记用于加载此类数据的指针会增加编译器检测到只读条件的可能性。
对于计算能力 5.0 的设备,内核的整个生命周期内不是只读的数据无法在统一 L1/纹理缓存中缓存。对于计算能力 5.2 的设备,默认情况下,它不在统一 L1/纹理缓存中缓存,但可以使用以下机制启用缓存
使用内联汇编和适当的修饰符执行读取,如 PTX 参考手册中所述;
使用
-Xptxas -dlcm=ca
编译标志进行编译,在这种情况下,所有读取都将被缓存,除了使用内联汇编执行的且具有禁用缓存的修饰符的读取;使用
-Xptxas -fscm=ca
编译标志进行编译,在这种情况下,所有读取都将被缓存,包括使用内联汇编执行的读取,无论使用何种修饰符。
当使用上面列出的三种机制之一启用缓存时,对于除线程块消耗过多 SM 寄存器文件的内核启动之外的所有内核启动,计算能力 5.2 的设备将在统一 L1/纹理缓存中缓存全局内存读取。分析器会报告这些异常。
16.5. 计算能力 6.x
16.5.1. 架构
一个 SM 由以下部分组成
64 个(计算能力 6.0)或 128 个(6.1 和 6.2)CUDA 核心用于算术运算,
16 个(6.0)或 32 个(6.1 和 6.2)特殊功能单元用于单精度浮点超越函数,
2 个(6.0)或 4 个(6.1 和 6.2)warp 调度器。
当为 SM 提供要执行的 warp 时,它首先将它们分配给其调度器。然后,在每个指令发出时间,每个调度器为其分配的准备好执行的 warp 之一发出一个指令(如果有)。
一个 SM 具有
一个只读常量缓存,所有功能单元共享该缓存,并加速从常量内存空间(驻留在设备内存中)的读取,
一个用于从全局内存读取的统一 L1/纹理缓存,大小为 24 KB(6.0 和 6.2)或 48 KB(6.1),
一个共享内存,大小为 64 KB(6.0 和 6.2)或 96 KB(6.1)。
统一 L1/纹理缓存也由纹理单元使用,该纹理单元实现了 纹理和表面内存 中提到的各种寻址模式和数据滤波。
还有一个由所有 SM 共享的 L2 缓存,用于缓存对本地或全局内存的访问,包括临时寄存器溢出。应用程序可以通过检查 l2CacheSize
设备属性来查询 L2 缓存大小(请参阅 设备枚举)。
缓存行为(例如,读取是否在统一 L1/纹理缓存和 L2 中缓存,还是仅在 L2 中缓存)可以在每次访问的基础上使用加载指令的修饰符部分配置。
16.5.2. 全局内存
全局内存的行为与计算能力 5.x 的设备相同(请参阅 全局内存)。
16.6. 计算能力 7.x
16.6.1. 架构
一个 SM 由以下部分组成
64 个 FP32 核心用于单精度算术运算,
32 个 FP64 核心用于双精度算术运算,34
64 个 INT32 核心用于整数数学,
8 个混合精度 Tensor Core 用于深度学习矩阵算术
16 个特殊功能单元用于单精度浮点超越函数,
4 个 warp 调度器。
一个 SM 静态地将其 warp 分配给其调度器。然后,在每个指令发出时间,每个调度器为其分配的准备好执行的 warp 之一发出一个指令(如果有)。
一个 SM 具有
一个只读常量缓存,所有功能单元共享该缓存,并加速从常量内存空间(驻留在设备内存中)的读取,
一个统一数据缓存和共享内存,总大小为 128 KB(Volta)或 96 KB(Turing)。
共享内存从统一数据缓存中划分出来,并且可以配置为各种大小(请参阅 共享内存。)剩余的数据缓存充当 L1 缓存,并且也由纹理单元使用,该纹理单元实现了 纹理和表面内存 中提到的各种寻址和数据滤波模式。
16.6.2. 独立线程调度
Volta 架构引入了 warp 中线程之间的独立线程调度,从而实现了以前不可用的 warp 内同步模式,并简化了移植 CPU 代码时的代码更改。但是,如果开发人员对先前硬件架构的 warp 同步性做出假设,则这可能会导致与预期参与执行代码的线程集完全不同。
以下是需要关注的代码模式以及针对 Volta 安全代码的建议纠正措施。
对于使用 warp 内部函数(
__shfl*
、__any
、__all
、__ballot
)的应用程序,开发人员必须将其代码移植到新的、安全的、同步的对应项,并带有*_sync
后缀。新的 warp 内部函数接受线程掩码,该掩码显式定义了哪些 lane(warp 的线程)必须参与 warp 内部函数。有关详细信息,请参阅 Warp 投票函数 和 Warp Shuffle 函数。
由于这些内部函数在 CUDA 9.0+ 中可用,(如果必要)可以使用以下预处理器宏有条件地执行代码
#if defined(CUDART_VERSION) && CUDART_VERSION >= 9000
// *_sync intrinsic
#endif
这些内部函数在所有架构上都可用,而不仅仅是 Volta 或 Turing,并且在大多数情况下,单个代码库就足以满足所有架构。但是请注意,对于 Pascal 和更早的架构,掩码中的所有线程都必须以收敛方式执行相同的 warp 内部函数指令,并且掩码中所有值的并集必须等于 warp 的活动掩码。以下代码模式在 Volta 上有效,但在 Pascal 或更早的架构上无效。
if (tid % warpSize < 16) { ... float swapped = __shfl_xor_sync(0xffffffff, val, 16); ... } else { ... float swapped = __shfl_xor_sync(0xffffffff, val, 16); ... }
__ballot(1)
的替代项是 __activemask()
。请注意,warp 中的线程甚至可以在单个代码路径中发散。因此,__activemask()
和 __ballot(1)
可能只返回当前代码路径上线程的子集。以下无效代码示例在 data[i]
大于 threshold
时将 output
的位 i
设置为 1。__activemask()
用于尝试启用 dataLen
不是 32 倍数的情况。
// Sets bit in output[] to 1 if the correspond element in data[i] // is greater than 'threshold', using 32 threads in a warp. for (int i = warpLane; i < dataLen; i += warpSize) { unsigned active = __activemask(); unsigned bitPack = __ballot_sync(active, data[i] > threshold); if (warpLane == 0) { output[i / 32] = bitPack; } }
此代码无效,因为 CUDA 不保证 warp 仅在循环条件处发散。当由于其他原因发生发散时,warp 中不同线程子集将为同一个 32 位输出元素计算冲突的结果。正确的代码可以使用非发散循环条件以及 __ballot_sync()
来安全地枚举参与阈值计算的 warp 中的线程集,如下所示。
for (int i = warpLane; i - warpLane < dataLen; i += warpSize) { unsigned active = __ballot_sync(0xFFFFFFFF, i < dataLen); if (i < dataLen) { unsigned bitPack = __ballot_sync(active, data[i] > threshold); if (warpLane == 0) { output[i / 32] = bitPack; } } }
Discovery Pattern 演示了 __activemask()
的有效用例。
如果应用程序具有 warp 同步代码,则需要在通过全局或共享内存交换线程之间的数据的任何步骤之间插入新的
__syncwarp()
warp 范围的 barrier 同步指令。关于代码以 lockstep 方式执行或来自不同线程的读取/写入在没有同步的情况下在 warp 中可见的假设是无效的。__shared__ float s_buff[BLOCK_SIZE]; s_buff[tid] = val; __syncthreads(); // Inter-warp reduction for (int i = BLOCK_SIZE / 2; i >= 32; i /= 2) { if (tid < i) { s_buff[tid] += s_buff[tid+i]; } __syncthreads(); } // Intra-warp reduction // Butterfly reduction simplifies syncwarp mask if (tid < 32) { float temp; temp = s_buff[tid ^ 16]; __syncwarp(); s_buff[tid] += temp; __syncwarp(); temp = s_buff[tid ^ 8]; __syncwarp(); s_buff[tid] += temp; __syncwarp(); temp = s_buff[tid ^ 4]; __syncwarp(); s_buff[tid] += temp; __syncwarp(); temp = s_buff[tid ^ 2]; __syncwarp(); s_buff[tid] += temp; __syncwarp(); } if (tid == 0) { *output = s_buff[0] + s_buff[1]; } __syncthreads();
虽然
__syncthreads()
一直被文档记录为同步线程块中的所有线程,但 Pascal 及之前的架构只能在 warp 级别强制同步。在某些情况下,这允许 barrier 成功,而无需每个线程都执行,只要每个 warp 中至少有一个线程到达 barrier。从 Volta 架构开始,CUDA 内置函数__syncthreads()
和 PTX 指令bar.sync
(及其衍生指令)是按线程强制执行的,因此只有当线程块中所有未退出的线程都到达时才会成功。利用先前行为的代码很可能会死锁,必须进行修改以确保所有未退出的线程都到达 barrier。
由 compute-saniter
提供的 racecheck
和 synccheck
工具可以帮助定位违规行为。
为了在实施上述纠正措施的同时帮助迁移,开发人员可以选择不支持独立线程调度的 Pascal 调度模型。有关详细信息,请参阅 应用程序兼容性。
16.6.3. 全局内存
全局内存的行为与计算能力 5.x 的设备相同(请参阅 全局内存)。
16.7. 计算能力 8.x
16.7.1. 架构
一个流式多处理器 (SM) 由以下部分组成:
计算能力为 8.0 的设备中有 64 个 FP32 内核用于单精度算术运算,计算能力为 8.6、8.7 和 8.9 的设备中有 128 个 FP32 内核,
计算能力为 8.0 的设备中有 32 个 FP64 内核用于双精度算术运算,计算能力为 8.6、8.7 和 8.9 的设备中有 2 个 FP64 内核
64 个 INT32 核心用于整数数学,
4 个混合精度第三代 Tensor Core,支持半精度 (fp16)、
__nv_bfloat16
、tf32
、亚字节和双精度 (fp64) 矩阵运算,适用于计算能力 8.0、8.6 和 8.7(有关详细信息,请参阅 Warp 矩阵函数),4 个混合精度第四代 Tensor Core,支持
fp8
、fp16
、__nv_bfloat16
、tf32
、亚字节和fp64
,适用于计算能力 8.9(有关详细信息,请参阅 Warp 矩阵函数),16 个特殊功能单元用于单精度浮点超越函数,
4 个 warp 调度器。
一个 SM 静态地将其 warp 分配给其调度器。然后,在每个指令发出时间,每个调度器为其分配的准备好执行的 warp 之一发出一个指令(如果有)。
一个 SM 具有
一个只读常量缓存,所有功能单元共享该缓存,并加速从常量内存空间(驻留在设备内存中)的读取,
一个统一数据缓存和共享内存,计算能力为 8.0 和 8.7 的设备总大小为 192 KB(是 Volta 的 128 KB 容量的 1.5 倍),计算能力为 8.6 和 8.9 的设备总大小为 128 KB。
共享内存从统一数据缓存中划分出来,可以配置为各种大小(请参阅 共享内存)。剩余的数据缓存用作 L1 缓存,也供纹理单元使用,纹理单元实现了 纹理和表面内存 中提到的各种寻址和数据过滤模式。
16.7.2. 全局内存
全局内存的行为方式与计算能力为 5.x 的设备相同(请参阅 全局内存)。
16.8. 计算能力 9.0
16.8.1. 架构
一个流式多处理器 (SM) 由以下部分组成:
128 个 FP32 内核用于单精度算术运算,
64 个 FP64 内核用于双精度算术运算,
64 个 INT32 核心用于整数数学,
4 个混合精度第四代 Tensor Core,支持新的
FP8
输入类型,可以是E4M3
或E5M2
用于指数 (E) 和尾数 (M)、半精度 (fp16)、__nv_bfloat16
、tf32
、INT8 和双精度 (fp64) 矩阵运算(有关详细信息,请参阅 Warp 矩阵函数),并支持稀疏性,16 个特殊功能单元用于单精度浮点超越函数,
4 个 warp 调度器。
一个 SM 静态地将其 warp 分配给其调度器。然后,在每个指令发出时间,每个调度器为其分配的准备好执行的 warp 之一发出一个指令(如果有)。
一个 SM 具有
一个只读常量缓存,所有功能单元共享该缓存,并加速从常量内存空间(驻留在设备内存中)的读取,
一个统一数据缓存和共享内存,计算能力为 9.0 的设备总大小为 256 KB(是 NVIDIA Ampere GPU 架构 的 192 KB 容量的 1.33 倍)。
共享内存从统一数据缓存中划分出来,可以配置为各种大小(请参阅 共享内存)。剩余的数据缓存用作 L1 缓存,也供纹理单元使用,纹理单元实现了 纹理和表面内存 中提到的各种寻址和数据过滤模式。
16.8.2. 全局内存
全局内存的行为方式与计算能力为 5.x 的设备相同(请参阅 全局内存)。
16.8.4. 加速专用计算的功能
NVIDIA Hopper GPU 架构包括加速矩阵乘法累加 (MMA) 计算的功能,具有
MMA 指令的异步执行
作用于跨 warp-group 的大型矩阵的 MMA 指令
warp-group 之间寄存器容量的动态重新分配,以支持更大的矩阵,以及
直接从共享内存访问的操作数矩阵
此功能集仅在 CUDA 编译工具链中通过内联 PTX 提供。
强烈建议应用程序通过 CUDA-X 库(例如 cuBLAS、cuDNN 或 cuFFT)利用此复杂功能集。
强烈建议设备内核通过 CUTLASS 利用此复杂功能集,CUTLASS 是 CUDA C++ 模板抽象的集合,用于在 CUDA 内的所有级别和规模上实现高性能矩阵乘法 (GEMM) 和相关计算。
16.9. 计算能力 10.0
16.9.1. 架构
一个流式多处理器 (SM) 由以下部分组成:
128 个 FP32 内核用于单精度算术运算,
64 个 FP64 内核用于双精度算术运算,
64 个 INT32 核心用于整数数学,
4 个混合精度第五代 Tensor Core,支持
FP8
输入类型,可以是E4M3
或E5M2
用于指数 (E) 和尾数 (M)、半精度 (fp16)、__nv_bfloat16
、tf32
、INT8 和双精度 (fp64) 矩阵运算(有关详细信息,请参阅 Warp 矩阵函数),并支持稀疏性,16 个特殊功能单元用于单精度浮点超越函数,
4 个 warp 调度器。
一个 SM 静态地将其 warp 分配给其调度器。然后,在每个指令发出时间,每个调度器为其分配的准备好执行的 warp 之一发出一个指令(如果有)。
一个 SM 具有
一个只读常量缓存,所有功能单元共享该缓存,并加速从常量内存空间(驻留在设备内存中)的读取,
一个统一数据缓存和共享内存,计算能力为 10.0 的设备总大小为 256 KB
共享内存从统一数据缓存中划分出来,可以配置为各种大小(请参阅 共享内存)。剩余的数据缓存用作 L1 缓存,也供纹理单元使用,纹理单元实现了 纹理和表面内存 中提到的各种寻址和数据过滤模式。
16.9.2. 全局内存
全局内存的行为方式与计算能力为 5.x 的设备相同(请参阅 全局内存)。
16.9.4. 加速专用计算的功能
NVIDIA Blackwell GPU 架构扩展了 NVIDIA Hopper GPU 架构中的功能,以加速矩阵乘法累加 (MMA)。
此功能集仅在 CUDA 编译工具链中通过内联 PTX 提供。
强烈建议应用程序通过 CUDA-X 库(例如 cuBLAS、cuDNN 或 cuFFT)利用此复杂功能集。
强烈建议设备内核通过 CUTLASS 利用此复杂功能集,CUTLASS 是 CUDA C++ 模板抽象的集合,用于在 CUDA 内的所有级别和规模上实现高性能矩阵乘法 (GEMM) 和相关计算。
16.10. 计算能力 12.0
16.10.1. 架构
一个流式多处理器 (SM) 由以下部分组成:
128 个 FP32 内核用于单精度算术运算,
2 个 FP64 内核用于双精度算术运算,
64 个 INT32 核心用于整数数学,
混合精度第五代 Tensor Core,支持
FP8
输入类型,可以是E4M3
或E5M2
用于指数 (E) 和尾数 (M)、半精度 (fp16)、__nv_bfloat16
、tf32
、INT8 和双精度 (fp64) 矩阵运算(有关详细信息,请参阅 Warp 矩阵函数),并支持稀疏性,16 个特殊功能单元用于单精度浮点超越函数,
4 个 warp 调度器。
一个 SM 静态地将其 warp 分配给其调度器。然后,在每个指令发出时间,每个调度器为其分配的准备好执行的 warp 之一发出一个指令(如果有)。
一个 SM 具有
一个只读常量缓存,所有功能单元共享该缓存,并加速从常量内存空间(驻留在设备内存中)的读取,
一个统一数据缓存和共享内存,计算能力为 12.0 的设备总大小为 128 KB
共享内存从统一数据缓存中划分出来,可以配置为各种大小(请参阅 共享内存)。剩余的数据缓存用作 L1 缓存,也供纹理单元使用,纹理单元实现了 纹理和表面内存 中提到的各种寻址和数据过滤模式。
16.10.2. 全局内存
全局内存的行为方式与计算能力为 5.x 的设备相同(请参阅 全局内存)。
16.10.4. 加速专用计算的功能
NVIDIA Blackwell GPU 架构扩展了 NVIDIA Hopper GPU 架构中的功能,以加速矩阵乘法累加 (MMA)。
此功能集仅在 CUDA 编译工具链中通过内联 PTX 提供。
强烈建议应用程序通过 CUDA-X 库(例如 cuBLAS、cuDNN 或 cuFFT)利用此复杂功能集。
强烈建议设备内核通过 CUTLASS 利用此复杂功能集,CUTLASS 是 CUDA C++ 模板抽象的集合,用于在 CUDA 内的所有级别和规模上实现高性能矩阵乘法 (GEMM) 和相关计算。
17. 驱动程序 API
本节假定读者了解 CUDA 运行时 中描述的概念。
驱动程序 API 在 cuda
动态库(cuda.dll
或 cuda.so
)中实现,该库在设备驱动程序的安装期间复制到系统上。其所有入口点都以 cu 为前缀。
它是一个基于句柄的命令式 API:大多数对象都通过不透明的句柄引用,这些句柄可以指定给函数来操作对象。
驱动程序 API 中可用的对象在 表 22 中进行了总结。
对象 |
句柄 |
描述 |
---|---|---|
设备 |
CUdevice |
启用 CUDA 的设备 |
上下文 |
CUcontext |
大致相当于 CPU 进程 |
模块 |
CUmodule |
大致相当于动态库 |
函数 |
CUfunction |
内核 |
堆内存 |
CUdeviceptr |
指向设备内存的指针 |
CUDA 数组 |
CUarray |
设备上的一维或二维数据的不透明容器,可通过纹理或表面引用读取 |
纹理对象 |
CUtexref |
描述如何解释纹理内存数据的对象 |
表面引用 |
CUsurfref |
描述如何读取或写入 CUDA 数组的对象 |
流 |
CUstream |
描述 CUDA 流的对象 |
事件 |
CUevent |
描述 CUDA 事件的对象 |
驱动程序 API 必须在使用驱动程序 API 中的任何函数之前使用 cuInit()
初始化。然后必须创建一个 CUDA 上下文,该上下文附加到特定设备并设置为调用主机线程的当前上下文,如 上下文 中详述。
在 CUDA 上下文中,内核由主机代码显式加载为 PTX 或二进制对象,如 模块 中所述。因此,用 C++ 编写的内核必须单独编译为 PTX 或二进制对象。内核使用 API 入口点启动,如 内核执行 中所述。
任何想要在未来设备架构上运行的应用程序都必须加载 PTX,而不是二进制代码。这是因为二进制代码是特定于架构的,因此与未来架构不兼容,而 PTX 代码在加载时由设备驱动程序编译为二进制代码。
这是使用驱动程序 API 编写的 内核 中的示例的主机代码
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);
// Initialize input vectors
...
// Initialize
cuInit(0);
// Get number of devices supporting CUDA
int deviceCount = 0;
cuDeviceGetCount(&deviceCount);
if (deviceCount == 0) {
printf("There is no device supporting CUDA.\n");
exit (0);
}
// Get handle for device 0
CUdevice cuDevice;
cuDeviceGet(&cuDevice, 0);
// Create context
CUcontext cuContext;
cuCtxCreate(&cuContext, 0, cuDevice);
// Create module from binary file
CUmodule cuModule;
cuModuleLoad(&cuModule, "VecAdd.ptx");
// Allocate vectors in device memory
CUdeviceptr d_A;
cuMemAlloc(&d_A, size);
CUdeviceptr d_B;
cuMemAlloc(&d_B, size);
CUdeviceptr d_C;
cuMemAlloc(&d_C, size);
// Copy vectors from host memory to device memory
cuMemcpyHtoD(d_A, h_A, size);
cuMemcpyHtoD(d_B, h_B, size);
// Get function handle from module
CUfunction vecAdd;
cuModuleGetFunction(&vecAdd, cuModule, "VecAdd");
// Invoke kernel
int threadsPerBlock = 256;
int blocksPerGrid =
(N + threadsPerBlock - 1) / threadsPerBlock;
void* args[] = { &d_A, &d_B, &d_C, &N };
cuLaunchKernel(vecAdd,
blocksPerGrid, 1, 1, threadsPerBlock, 1, 1,
0, 0, args, 0);
...
}
完整代码可以在 vectorAddDrv
CUDA 示例中找到。
17.1. 上下文
CUDA 上下文类似于 CPU 进程。在驱动程序 API 中执行的所有资源和操作都封装在 CUDA 上下文中,并且当上下文被销毁时,系统会自动清理这些资源。除了模块和纹理或表面引用等对象外,每个上下文都有其自己不同的地址空间。因此,来自不同上下文的 CUdeviceptr
值引用不同的内存位置。
一个主机线程一次只能有一个设备上下文处于当前状态。当使用 cuCtxCreate(
) 创建上下文时,它将成为调用主机线程的当前上下文。如果线程没有有效的当前上下文,则在上下文中运行的 CUDA 函数(大多数不涉及设备枚举或上下文管理的函数)将返回 CUDA_ERROR_INVALID_CONTEXT
。
每个主机线程都有一个当前上下文堆栈。cuCtxCreate()
将新上下文推送到堆栈顶部。cuCtxPopCurrent()
可以被调用以将上下文从主机线程分离。然后,上下文变为“浮动”状态,可以作为任何主机线程的当前上下文推送。cuCtxPopCurrent()
还会恢复先前的当前上下文(如果有)。
每个上下文还维护一个使用计数。cuCtxCreate()
创建一个使用计数为 1 的上下文。cuCtxAttach()
递增使用计数,cuCtxDetach()
递减使用计数。当调用 cuCtxDetach()
或 cuCtxDestroy()
时,当使用计数变为 0 时,上下文将被销毁。
驱动程序 API 可与运行时互操作,并且可以从驱动程序 API 通过 cuDevicePrimaryCtxRetain()
访问由运行时管理的主上下文(请参阅 初始化)。
使用计数有助于在同一上下文中运行的第三方编写的代码之间实现互操作性。例如,如果加载了三个库以使用相同的上下文,则每个库都会调用 cuCtxAttach()
以递增使用计数,并在库完成使用上下文时调用 cuCtxDetach()
以递减使用计数。对于大多数库,预期应用程序将在加载或初始化库之前创建上下文;这样,应用程序可以使用自己的启发式方法创建上下文,而库只需在其传递给它的上下文中运行。希望创建自己的上下文的库(API 客户端可能不知道他们是否已创建了自己的上下文)将使用 cuCtxPushCurrent()
和 cuCtxPopCurrent()
,如下图所示。

图 40 库上下文管理
17.2. 模块
模块是设备代码和数据的动态可加载包,类似于 Windows 中的 DLL,由 nvcc 输出(请参阅 使用 NVCC 编译)。所有符号(包括函数、全局变量以及纹理或表面引用)的名称都保持在模块范围内,以便独立第三方编写的模块可以在同一 CUDA 上下文中互操作。
此代码示例加载一个模块并检索一些内核的句柄
CUmodule cuModule;
cuModuleLoad(&cuModule, "myModule.ptx");
CUfunction myKernel;
cuModuleGetFunction(&myKernel, cuModule, "MyKernel");
此代码示例从 PTX 代码编译和加载新模块并解析编译错误
#define BUFFER_SIZE 8192
CUmodule cuModule;
CUjit_option options[3];
void* values[3];
char* PTXCode = "some PTX code";
char error_log[BUFFER_SIZE];
int err;
options[0] = CU_JIT_ERROR_LOG_BUFFER;
values[0] = (void*)error_log;
options[1] = CU_JIT_ERROR_LOG_BUFFER_SIZE_BYTES;
values[1] = (void*)BUFFER_SIZE;
options[2] = CU_JIT_TARGET_FROM_CUCONTEXT;
values[2] = 0;
err = cuModuleLoadDataEx(&cuModule, PTXCode, 3, options, values);
if (err != CUDA_SUCCESS)
printf("Link error:\n%s\n", error_log);
此代码示例从多个 PTX 代码编译、链接和加载新模块并解析链接和编译错误
#define BUFFER_SIZE 8192
CUmodule cuModule;
CUjit_option options[6];
void* values[6];
float walltime;
char error_log[BUFFER_SIZE], info_log[BUFFER_SIZE];
char* PTXCode0 = "some PTX code";
char* PTXCode1 = "some other PTX code";
CUlinkState linkState;
int err;
void* cubin;
size_t cubinSize;
options[0] = CU_JIT_WALL_TIME;
values[0] = (void*)&walltime;
options[1] = CU_JIT_INFO_LOG_BUFFER;
values[1] = (void*)info_log;
options[2] = CU_JIT_INFO_LOG_BUFFER_SIZE_BYTES;
values[2] = (void*)BUFFER_SIZE;
options[3] = CU_JIT_ERROR_LOG_BUFFER;
values[3] = (void*)error_log;
options[4] = CU_JIT_ERROR_LOG_BUFFER_SIZE_BYTES;
values[4] = (void*)BUFFER_SIZE;
options[5] = CU_JIT_LOG_VERBOSE;
values[5] = (void*)1;
cuLinkCreate(6, options, values, &linkState);
err = cuLinkAddData(linkState, CU_JIT_INPUT_PTX,
(void*)PTXCode0, strlen(PTXCode0) + 1, 0, 0, 0, 0);
if (err != CUDA_SUCCESS)
printf("Link error:\n%s\n", error_log);
err = cuLinkAddData(linkState, CU_JIT_INPUT_PTX,
(void*)PTXCode1, strlen(PTXCode1) + 1, 0, 0, 0, 0);
if (err != CUDA_SUCCESS)
printf("Link error:\n%s\n", error_log);
cuLinkComplete(linkState, &cubin, &cubinSize);
printf("Link completed in %fms. Linker Output:\n%s\n", walltime, info_log);
cuModuleLoadData(cuModule, cubin);
cuLinkDestroy(linkState);
完整代码可以在 ptxjit
CUDA 示例中找到。
17.3. 内核执行
cuLaunchKernel()
使用给定的执行配置启动内核。
参数作为指针数组传递(cuLaunchKernel()
的倒数第二个参数),其中第 n 个指针对应于第 n 个参数,并指向从中复制参数的内存区域,或者作为额外的选项之一(cuLaunchKernel()
的最后一个参数)。
当参数作为额外的选项传递(CU_LAUNCH_PARAM_BUFFER_POINTER
选项)时,它们作为指向单个缓冲区的指针传递,其中假定参数通过匹配设备代码中每种参数类型的对齐要求而彼此正确偏移。
设备代码中内置向量类型的对齐要求在表 5中列出。对于所有其他基本类型,设备代码中的对齐要求与主机代码中的对齐要求相匹配,因此可以使用 __alignof()
获取。唯一的例外是当主机编译器在单字边界而不是双字边界上对齐 double
和 long long
(以及 64 位系统上的 long
)时(例如,使用 gcc
的编译标志 -mno-align-double
),因为在设备代码中,这些类型始终在双字边界上对齐。
CUdeviceptr
是一个整数,但表示一个指针,因此其对齐要求为 __alignof(void*)
。
以下代码示例使用宏 (ALIGN_UP()
) 调整每个参数的偏移量以满足其对齐要求,并使用另一个宏 (ADD_TO_PARAM_BUFFER()
) 将每个参数添加到传递给 CU_LAUNCH_PARAM_BUFFER_POINTER
选项的参数缓冲区。
#define ALIGN_UP(offset, alignment) \
(offset) = ((offset) + (alignment) - 1) & ~((alignment) - 1)
char paramBuffer[1024];
size_t paramBufferSize = 0;
#define ADD_TO_PARAM_BUFFER(value, alignment) \
do { \
paramBufferSize = ALIGN_UP(paramBufferSize, alignment); \
memcpy(paramBuffer + paramBufferSize, \
&(value), sizeof(value)); \
paramBufferSize += sizeof(value); \
} while (0)
int i;
ADD_TO_PARAM_BUFFER(i, __alignof(i));
float4 f4;
ADD_TO_PARAM_BUFFER(f4, 16); // float4's alignment is 16
char c;
ADD_TO_PARAM_BUFFER(c, __alignof(c));
float f;
ADD_TO_PARAM_BUFFER(f, __alignof(f));
CUdeviceptr devPtr;
ADD_TO_PARAM_BUFFER(devPtr, __alignof(devPtr));
float2 f2;
ADD_TO_PARAM_BUFFER(f2, 8); // float2's alignment is 8
void* extra[] = {
CU_LAUNCH_PARAM_BUFFER_POINTER, paramBuffer,
CU_LAUNCH_PARAM_BUFFER_SIZE, ¶mBufferSize,
CU_LAUNCH_PARAM_END
};
cuLaunchKernel(cuFunction,
blockWidth, blockHeight, blockDepth,
gridWidth, gridHeight, gridDepth,
0, 0, 0, extra);
结构的对齐要求等于其字段的对齐要求的最大值。包含内置向量类型、CUdeviceptr
或未对齐的 double
和 long long
的结构的对齐要求可能在设备代码和主机代码之间有所不同。这样的结构也可能以不同的方式进行填充。例如,以下结构在主机代码中根本没有填充,但在设备代码中,在字段 f
之后填充了 12 个字节,因为字段 f4
的对齐要求为 16。
typedef struct {
float f;
float4 f4;
} myStruct;
17.4. 运行时 API 和驱动程序 API 之间的互操作性
应用程序可以将运行时 API 代码与驱动程序 API 代码混合使用。
如果通过驱动程序 API 创建并使上下文变为当前上下文,则后续的运行时调用将拾取此上下文,而不是创建新的上下文。
如果运行时已初始化(如CUDA 运行时中所述隐式初始化),则可以使用 cuCtxGetCurrent()
来检索初始化期间创建的上下文。此上下文可以供后续的驱动程序 API 调用使用。
来自运行时的隐式创建的上下文称为主上下文(参见初始化)。可以使用 主上下文管理 函数从驱动程序 API 进行管理。
可以使用任一 API 分配和释放设备内存。CUdeviceptr
可以强制转换为常规指针,反之亦然
CUdeviceptr devPtr;
float* d_data;
// Allocation using driver API
cuMemAlloc(&devPtr, size);
d_data = (float*)devPtr;
// Allocation using runtime API
cudaMalloc(&d_data, size);
devPtr = (CUdeviceptr)d_data;
特别是,这意味着使用驱动程序 API 编写的应用程序可以调用使用运行时 API 编写的库(例如 cuFFT、cuBLAS 等)。
参考手册的设备和版本管理部分中的所有函数都可以互换使用。
17.5. 驱动程序入口点访问
17.5.1. 简介
驱动程序入口点访问 API
提供了一种检索 CUDA 驱动程序函数地址的方法。从 CUDA 11.3 开始,用户可以使用从这些 API 获取的函数指针调用可用的 CUDA 驱动程序 API。
这些 API 提供的功能类似于 POSIX 平台上的 dlsym 和 Windows 上的 GetProcAddress。提供的 API 将允许用户
使用
CUDA 驱动程序 API
检索驱动程序函数的地址。使用
CUDA 运行时 API
检索驱动程序函数的地址。请求 CUDA 驱动程序函数的每线程默认流版本。有关更多详细信息,请参阅检索每线程默认流版本。
在较旧的工具包但在较新的驱动程序上访问新的 CUDA 功能。
17.5.2. 驱动程序函数 Typedefs
为了帮助检索 CUDA 驱动程序 API 入口点,CUDA 工具包提供了对包含所有 CUDA 驱动程序 API 的函数指针定义的标头的访问权限。这些标头与 CUDA 工具包一起安装,并在工具包的 include/
目录中提供。下表总结了包含每个 CUDA API 标头文件的 typedefs
的标头文件。
API 标头文件 |
API Typedef 标头文件 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
以上标头不定义实际的函数指针本身;它们定义了函数指针的 typedefs。例如,cudaTypedefs.h
具有驱动程序 API cuMemAlloc
的以下 typedefs
typedef CUresult (CUDAAPI *PFN_cuMemAlloc_v3020)(CUdeviceptr_v2 *dptr, size_t bytesize);
typedef CUresult (CUDAAPI *PFN_cuMemAlloc_v2000)(CUdeviceptr_v1 *dptr, unsigned int bytesize);
CUDA 驱动程序符号具有基于版本的命名方案,其名称中带有 _v*
扩展名,除了第一个版本。当特定 CUDA 驱动程序 API 的签名或语义发生更改时,我们会增加相应驱动程序符号的版本号。对于 cuMemAlloc
驱动程序 API,第一个驱动程序符号名称为 cuMemAlloc
,下一个符号名称为 cuMemAlloc_v2
。CUDA 2.0 (2000) 中引入的第一个版本的 typedef 为 PFN_cuMemAlloc_v2000
。CUDA 3.2 (3020) 中引入的下一个版本的 typedef 为 PFN_cuMemAlloc_v3020
。
typedefs
可以用于更轻松地在代码中定义适当类型的函数指针
PFN_cuMemAlloc_v3020 pfn_cuMemAlloc_v2;
PFN_cuMemAlloc_v2000 pfn_cuMemAlloc_v1;
如果用户对 API 的特定版本感兴趣,则上述方法更可取。此外,标头为已安装的 CUDA 工具包发布时可用的所有驱动程序符号的最新版本预定义了宏;这些 typedefs 没有 _v*
后缀。对于 CUDA 11.3 工具包,cuMemAlloc_v2
是最新版本,因此我们也可以将其函数指针定义如下
PFN_cuMemAlloc pfn_cuMemAlloc;
17.5.3. 驱动程序函数检索
使用驱动程序入口点访问 API 和适当的 typedef,我们可以获取指向任何 CUDA 驱动程序 API 的函数指针。
17.5.3.1. 使用驱动程序 API
驱动程序 API 需要 CUDA 版本作为参数,以获取请求的驱动程序符号的 ABI 兼容版本。CUDA 驱动程序 API 具有每个函数的 ABI,用 _v*
扩展名表示。例如,考虑 cuStreamBeginCapture
的版本及其来自 cudaTypedefs.h
的相应 typedefs
// cuda.h
CUresult CUDAAPI cuStreamBeginCapture(CUstream hStream);
CUresult CUDAAPI cuStreamBeginCapture_v2(CUstream hStream, CUstreamCaptureMode mode);
// cudaTypedefs.h
typedef CUresult (CUDAAPI *PFN_cuStreamBeginCapture_v10000)(CUstream hStream);
typedef CUresult (CUDAAPI *PFN_cuStreamBeginCapture_v10010)(CUstream hStream, CUstreamCaptureMode mode);
从上面代码片段中的 typedefs
可以看出,版本后缀 _v10000
和 _v10010
表示上述 API 分别在 CUDA 10.0 和 CUDA 10.1 中引入。
#include <cudaTypedefs.h>
// Declare the entry points for cuStreamBeginCapture
PFN_cuStreamBeginCapture_v10000 pfn_cuStreamBeginCapture_v1;
PFN_cuStreamBeginCapture_v10010 pfn_cuStreamBeginCapture_v2;
// Get the function pointer to the cuStreamBeginCapture driver symbol
cuGetProcAddress("cuStreamBeginCapture", &pfn_cuStreamBeginCapture_v1, 10000, CU_GET_PROC_ADDRESS_DEFAULT, &driverStatus);
// Get the function pointer to the cuStreamBeginCapture_v2 driver symbol
cuGetProcAddress("cuStreamBeginCapture", &pfn_cuStreamBeginCapture_v2, 10010, CU_GET_PROC_ADDRESS_DEFAULT, &driverStatus);
参考上面的代码片段,要检索驱动程序 API cuStreamBeginCapture
的 _v1
版本的地址,CUDA 版本参数应完全为 10.0 (10000)。同样,检索 API 的 _v2
版本的地址的 CUDA 版本应为 10.1 (10010)。为检索特定版本的驱动程序 API 指定更高的 CUDA 版本可能并不总是可移植的。例如,此处使用 11030 仍然会返回 _v2
符号,但如果在 CUDA 11.3 中发布了假设的 _v3
版本,则 cuGetProcAddress
API 将开始返回较新的 _v3
符号,当与 CUDA 11.3 驱动程序配对时。由于 _v2
和 _v3
符号的 ABI 和函数签名可能不同,因此使用为 _v2
符号设计的 _v10010
typedef 调用 _v3
函数将表现出未定义的行为。
要检索给定 CUDA 工具包的驱动程序 API 的最新版本,我们还可以将 CUDA_VERSION 指定为 version
参数,并使用未版本化的 typedef 来定义函数指针。由于 _v2
是 CUDA 11.3 中驱动程序 API cuStreamBeginCapture
的最新版本,因此以下代码片段显示了检索它的另一种方法。
// Assuming we are using CUDA 11.3 Toolkit
#include <cudaTypedefs.h>
// Declare the entry point
PFN_cuStreamBeginCapture pfn_cuStreamBeginCapture_latest;
// Intialize the entry point. Specifying CUDA_VERSION will give the function pointer to the
// cuStreamBeginCapture_v2 symbol since it is latest version on CUDA 11.3.
cuGetProcAddress("cuStreamBeginCapture", &pfn_cuStreamBeginCapture_latest, CUDA_VERSION, CU_GET_PROC_ADDRESS_DEFAULT, &driverStatus);
请注意,请求具有无效 CUDA 版本的驱动程序 API 将返回错误 CUDA_ERROR_NOT_FOUND
。在上面的代码示例中,传入的版本小于 10000 (CUDA 10.0) 将无效。
17.5.3.2. 使用运行时 API
运行时 API cudaGetDriverEntryPoint
使用 CUDA 运行时版本来获取请求的驱动程序符号的 ABI 兼容版本。在下面的代码片段中,所需的最低 CUDA 运行时版本为 CUDA 11.2,因为 cuMemAllocAsync
是在那时引入的。
#include <cudaTypedefs.h>
// Declare the entry point
PFN_cuMemAllocAsync pfn_cuMemAllocAsync;
// Intialize the entry point. Assuming CUDA runtime version >= 11.2
cudaGetDriverEntryPoint("cuMemAllocAsync", &pfn_cuMemAllocAsync, cudaEnableDefault, &driverStatus);
// Call the entry point
if(driverStatus == cudaDriverEntryPointSuccess && pfn_cuMemAllocAsync) {
pfn_cuMemAllocAsync(...);
}
运行时 API cudaGetDriverEntryPointByVersion
使用用户提供的 CUDA 版本来获取请求的驱动程序符号的 ABI 兼容版本。这允许对请求的 ABI 版本进行更具体的控制。
17.5.3.3. 检索每线程默认流版本
某些 CUDA 驱动程序 API 可以配置为具有默认流或每线程默认流语义。具有每线程默认流语义的驱动程序 API 在其名称中带有 _ptsz 或 _ptds 后缀。例如,cuLaunchKernel
具有名为 cuLaunchKernel_ptsz
的每线程默认流变体。借助驱动程序入口点访问 API,用户可以请求驱动程序 API cuLaunchKernel
的每线程默认流版本,而不是默认流版本。为 默认流 或 每线程默认流 语义配置 CUDA 驱动程序 API 会影响同步行为。更多详细信息可以在此处找到。
可以通过以下方式之一获得驱动程序 API 的默认流或每线程默认流版本
使用编译标志
--default-stream per-thread
或定义宏CUDA_API_PER_THREAD_DEFAULT_STREAM
以获得每线程默认流行为。分别使用标志
CU_GET_PROC_ADDRESS_LEGACY_STREAM/cudaEnableLegacyStream
或CU_GET_PROC_ADDRESS_PER_THREAD_DEFAULT_STREAM/cudaEnablePerThreadDefaultStream
强制默认流或每线程默认流行为。
17.5.3.4. 访问新的 CUDA 功能
始终建议安装最新的 CUDA 工具包以访问新的 CUDA 驱动程序功能,但如果出于某种原因,用户不想更新或无法访问最新的工具包,则可以使用 API 仅通过更新的 CUDA 驱动程序来访问新的 CUDA 功能。为了讨论,让我们假设用户正在使用 CUDA 11.3,并且想要使用 CUDA 12.0 驱动程序中可用的新驱动程序 API cuFoo
。以下代码片段说明了此用例
int main()
{
// Assuming we have CUDA 12.0 driver installed.
// Manually define the prototype as cudaTypedefs.h in CUDA 11.3 does not have the cuFoo typedef
typedef CUresult (CUDAAPI *PFN_cuFoo)(...);
PFN_cuFoo pfn_cuFoo = NULL;
CUdriverProcAddressQueryResult driverStatus;
// Get the address for cuFoo API using cuGetProcAddress. Specify CUDA version as
// 12000 since cuFoo was introduced then or get the driver version dynamically
// using cuDriverGetVersion
int driverVersion;
cuDriverGetVersion(&driverVersion);
CUresult status = cuGetProcAddress("cuFoo", &pfn_cuFoo, driverVersion, CU_GET_PROC_ADDRESS_DEFAULT, &driverStatus);
if (status == CUDA_SUCCESS && pfn_cuFoo) {
pfn_cuFoo(...);
}
else {
printf("Cannot retrieve the address to cuFoo - driverStatus = %d. Check if the latest driver for CUDA 12.0 is installed.\n", driverStatus);
assert(0);
}
// rest of code here
}
17.5.4. cuGetProcAddress 的潜在影响
以下是关于 cuGetProcAddress
和 cudaGetDriverEntryPoint
的潜在问题的具体和理论示例集。
17.5.4.1. cuGetProcAddress 与隐式链接的影响
cuDeviceGetUuid
在 CUDA 9.2 中引入。此 API 在 CUDA 11.4 中引入了较新的修订版 (cuDeviceGetUuid_v2
)。为了保持次要版本兼容性,在 CUDA 12.0 之前,cuDeviceGetUuid
不会版本升级到 cuda.h 中的 cuDeviceGetUuid_v2
。这意味着通过获取指向它的函数指针来调用它 cuGetProcAddress
可能具有不同的行为。直接使用 API 的示例
#include <cuda.h>
CUuuid uuid;
CUdevice dev;
CUresult status;
status = cuDeviceGet(&dev, 0); // Get device 0
// handle status
status = cuDeviceGetUuid(&uuid, dev) // Get uuid of device 0
在此示例中,假设用户使用 CUDA 11.4 进行编译。请注意,这将执行 cuDeviceGetUuid
的行为,而不是 _v2 版本。现在是使用 cuGetProcAddress
的示例
#include <cudaTypedefs.h>
CUuuid uuid;
CUdevice dev;
CUresult status;
CUdriverProcAddressQueryResult driverStatus;
status = cuDeviceGet(&dev, 0); // Get device 0
// handle status
PFN_cuDeviceGetUuid pfn_cuDeviceGetUuid;
status = cuGetProcAddress("cuDeviceGetUuid", &pfn_cuDeviceGetUuid, CUDA_VERSION, CU_GET_PROC_ADDRESS_DEFAULT, &driverStatus);
if(CUDA_SUCCESS == status && pfn_cuDeviceGetUuid) {
// pfn_cuDeviceGetUuid points to ???
}
在此示例中,假设用户使用 CUDA 11.4 进行编译。这将获取 cuDeviceGetUuid_v2
的函数指针。然后调用函数指针将调用新的 _v2 函数,而不是上一个示例中显示的相同 cuDeviceGetUuid
。
17.5.4.2. cuGetProcAddress 中的编译时与运行时版本使用
让我们采用相同的问题并进行一个小小的调整。最后一个示例使用 CUDA_VERSION 的编译时常量来确定要获取哪个函数指针。如果用户使用 cuDriverGetVersion
或 cudaDriverGetVersion
动态查询驱动程序版本以传递给 cuGetProcAddress
,则会出现更多复杂情况。示例
#include <cudaTypedefs.h>
CUuuid uuid;
CUdevice dev;
CUresult status;
int cudaVersion;
CUdriverProcAddressQueryResult driverStatus;
status = cuDeviceGet(&dev, 0); // Get device 0
// handle status
status = cuDriverGetVersion(&cudaVersion);
// handle status
PFN_cuDeviceGetUuid pfn_cuDeviceGetUuid;
status = cuGetProcAddress("cuDeviceGetUuid", &pfn_cuDeviceGetUuid, cudaVersion, CU_GET_PROC_ADDRESS_DEFAULT, &driverStatus);
if(CUDA_SUCCESS == status && pfn_cuDeviceGetUuid) {
// pfn_cuDeviceGetUuid points to ???
}
在此示例中,假设用户使用 CUDA 11.3 进行编译。用户将调试、测试和部署此应用程序,并了解获取 cuDeviceGetUuid
(而不是 _v2 版本)的已知行为。由于 CUDA 保证了次要版本之间的 ABI 兼容性,因此预计在驱动程序升级到 CUDA 11.4 后(无需更新工具包和运行时),此同一应用程序将能够运行,而无需重新编译。但这将具有未定义的行为,因为现在 PFN_cuDeviceGetUuid
的 typedef 仍将是原始版本的签名,但由于 cudaVersion
现在将为 11040 (CUDA 11.4),cuGetProcAddress
将返回指向 _v2 版本的函数指针,这意味着调用它可能具有未定义的行为。
请注意,在这种情况下,原始的(而不是 _v2 版本) typedef 看起来像
typedef CUresult (CUDAAPI *PFN_cuDeviceGetUuid_v9020)(CUuuid *uuid, CUdevice_v1 dev);
但是 _v2 版本 typedef 看起来像
typedef CUresult (CUDAAPI *PFN_cuDeviceGetUuid_v11040)(CUuuid *uuid, CUdevice_v1 dev);
因此,在这种情况下,API/ABI 将是相同的,并且运行时 API 调用可能不会引起问题 - 只有未知的 uuid 返回的可能性。在API/ABI 的影响中,我们讨论了 API/ABI 兼容性方面更成问题的情况。
17.5.4.3. 带有显式版本检查的 API 版本升级
上面是一个具体的示例。现在,例如,让我们使用一个理论示例,该示例仍然存在跨驱动程序版本的兼容性问题。示例
CUresult cuFoo(int bar); // Introduced in CUDA 11.4
CUresult cuFoo_v2(int bar); // Introduced in CUDA 11.5
CUresult cuFoo_v3(int bar, void* jazz); // Introduced in CUDA 11.6
typedef CUresult (CUDAAPI *PFN_cuFoo_v11040)(int bar);
typedef CUresult (CUDAAPI *PFN_cuFoo_v11050)(int bar);
typedef CUresult (CUDAAPI *PFN_cuFoo_v11060)(int bar, void* jazz);
请注意,自 CUDA 11.4 中的原始创建以来,API 已修改两次,而 CUDA 11.6 中的最新版本也修改了该函数的 API/ABI 接口。根据 CUDA 11.5 编译的用户代码中的用法是
#include <cuda.h>
#include <cudaTypedefs.h>
CUresult status;
int cudaVersion;
CUdriverProcAddressQueryResult driverStatus;
status = cuDriverGetVersion(&cudaVersion);
// handle status
PFN_cuFoo_v11040 pfn_cuFoo_v11040;
PFN_cuFoo_v11050 pfn_cuFoo_v11050;
if(cudaVersion < 11050 ) {
// We know to get the CUDA 11.4 version
status = cuGetProcAddress("cuFoo", &pfn_cuFoo_v11040, cudaVersion, CU_GET_PROC_ADDRESS_DEFAULT, &driverStatus);
// Handle status and validating pfn_cuFoo_v11040
}
else {
// Assume >= CUDA 11.5 version we can use the second version
status = cuGetProcAddress("cuFoo", &pfn_cuFoo_v11050, cudaVersion, CU_GET_PROC_ADDRESS_DEFAULT, &driverStatus);
// Handle status and validating pfn_cuFoo_v11050
}
在此示例中,如果没有针对 CUDA 11.6 中新 typedef 的更新以及使用这些新 typedef 和案例处理重新编译应用程序,则应用程序将获得返回的 cuFoo_v3 函数指针,然后对该函数的任何使用都将导致未定义的行为。此示例的重点是说明即使针对 cuGetProcAddress
的显式版本检查也可能无法安全地涵盖 CUDA 主要版本中的次要版本升级。
17.5.4.4. 运行时 API 使用的问题
以上示例侧重于使用驱动程序 API 获取驱动程序 API 的函数指针的问题。现在我们将讨论使用运行时 API cudaApiGetDriverEntryPoint
的潜在问题。
我们将首先使用类似于上述示例的运行时 API。
#include <cuda.h>
#include <cudaTypedefs.h>
#include <cuda_runtime.h>
CUresult status;
cudaError_t error;
int driverVersion, runtimeVersion;
CUdriverProcAddressQueryResult driverStatus;
// Ask the runtime for the function
PFN_cuDeviceGetUuid pfn_cuDeviceGetUuidRuntime;
error = cudaGetDriverEntryPoint ("cuDeviceGetUuid", &pfn_cuDeviceGetUuidRuntime, cudaEnableDefault, &driverStatus);
if(cudaSuccess == error && pfn_cuDeviceGetUuidRuntime) {
// pfn_cuDeviceGetUuid points to ???
}
此示例中的函数指针比上面的仅驱动程序示例更复杂,因为无法控制要获取的函数版本;它将始终获取当前 CUDA 运行时版本的 API。有关更多信息,请参见下表
静态运行时版本链接 |
||
---|---|---|
已安装的驱动程序版本 |
V11.3 |
V11.4 |
V11.3 |
v1 |
v1x |
V11.4 |
v1 |
v2 |
V11.3 => 11.3 CUDA Runtime and Toolkit (includes header files cuda.h and cudaTypedefs.h)
V11.4 => 11.4 CUDA Runtime and Toolkit (includes header files cuda.h and cudaTypedefs.h)
v1 => cuDeviceGetUuid
v2 => cuDeviceGetUuid_v2
x => Implies the typedef function pointer won't match the returned
function pointer. In these cases, the typedef at compile time
using a CUDA 11.4 runtime, would match the _v2 version, but the
returned function pointer would be the original (non _v2) function.
表中的问题出现在较新的 CUDA 11.4 运行时和工具包与较旧的驱动程序 (CUDA 11.3) 组合中,在上表中标记为 v1x。此组合将使驱动程序返回指向较旧函数(非 _v2)的指针,但是应用程序中使用的 typedef 将用于新的函数指针。
17.5.4.5. 运行时 API 和动态版本控制的问题
当我们考虑应用程序编译时使用的 CUDA 版本、CUDA 运行时版本以及应用程序动态链接到的 CUDA 驱动程序版本的不同组合时,会出现更多复杂情况。
#include <cuda.h>
#include <cudaTypedefs.h>
#include <cuda_runtime.h>
CUresult status;
cudaError_t error;
int driverVersion, runtimeVersion;
CUdriverProcAddressQueryResult driverStatus;
enum cudaDriverEntryPointQueryResult runtimeStatus;
PFN_cuDeviceGetUuid pfn_cuDeviceGetUuidDriver;
status = cuGetProcAddress("cuDeviceGetUuid", &pfn_cuDeviceGetUuidDriver, CUDA_VERSION, CU_GET_PROC_ADDRESS_DEFAULT, &driverStatus);
if(CUDA_SUCCESS == status && pfn_cuDeviceGetUuidDriver) {
// pfn_cuDeviceGetUuidDriver points to ???
}
// Ask the runtime for the function
PFN_cuDeviceGetUuid pfn_cuDeviceGetUuidRuntime;
error = cudaGetDriverEntryPoint ("cuDeviceGetUuid", &pfn_cuDeviceGetUuidRuntime, cudaEnableDefault, &runtimeStatus);
if(cudaSuccess == error && pfn_cuDeviceGetUuidRuntime) {
// pfn_cuDeviceGetUuidRuntime points to ???
}
// Ask the driver for the function based on the driver version (obtained via runtime)
error = cudaDriverGetVersion(&driverVersion);
PFN_cuDeviceGetUuid pfn_cuDeviceGetUuidDriverDriverVer;
status = cuGetProcAddress ("cuDeviceGetUuid", &pfn_cuDeviceGetUuidDriverDriverVer, driverVersion, CU_GET_PROC_ADDRESS_DEFAULT, &driverStatus);
if(CUDA_SUCCESS == status && pfn_cuDeviceGetUuidDriverDriverVer) {
// pfn_cuDeviceGetUuidDriverDriverVer points to ???
}
预期的函数指针矩阵如下
函数指针 |
应用程序编译/运行时动态链接版本/驱动程序版本 |
|||||||
(3 => CUDA 11.3 和 4 => CUDA 11.4) |
||||||||
3/3/3 |
3/3/4 |
3/4/3 |
3/4/4 |
4/3/3 |
4/3/4 |
4/4/3 |
4/4/4 |
|
|
t1/v1 |
t1/v1 |
t1/v1 |
t1/v1 |
不适用 |
不适用 |
t2/v1 |
t2/v2 |
|
t1/v1 |
t1/v1 |
t1/v1 |
t1/v2 |
不适用 |
不适用 |
t2/v1 |
t2/v2 |
|
t1/v1 |
t1/v2 |
t1/v1 |
t1/v2 |
不适用 |
不适用 |
t2/v1 |
t2/v2 |
tX -> Typedef version used at compile time
vX -> Version returned/used at runtime
如果应用程序是针对 CUDA 版本 11.3 编译的,则它将具有原始函数的 typedef,但是如果针对 CUDA 版本 11.4 编译,它将具有 _v2 函数的 typedef。因此,请注意 typedef 与返回/使用的实际版本不匹配的情况数量。
17.5.4.6. 运行时 API 允许 CUDA 版本的问题
除非另有说明,否则 CUDA 运行时 API cudaGetDriverEntryPointByVersion
将具有与驱动程序入口点 cuGetProcAddress
相似的影响,因为它允许用户请求特定的 CUDA 驱动程序版本。
17.5.4.7. API/ABI 的影响
在上面使用 cuDeviceGetUuid
的示例中,API 不匹配的影响很小,并且对于许多用户而言可能并不完全明显,因为添加 _v2 是为了支持多实例 GPU (MIG) 模式。因此,在没有 MIG 的系统上,用户甚至可能没有意识到他们正在获得不同的 API。
更成问题的是更改其应用程序签名(以及 ABI)的 API,例如 cuCtxCreate
。在 CUDA 3.2 中引入的 _v2 版本当前用作使用 cuda.h
时的默认 cuCtxCreate
,但现在在 CUDA 11.4 中引入了较新的版本 (cuCtxCreate_v3
)。API 签名也已修改,现在采用额外的参数。因此,在上面的某些情况下,其中函数指针的 typedef 与返回的函数指针不匹配,则存在非明显的 ABI 不兼容性的可能性,这将导致未定义的行为。
例如,假设以下代码是针对 CUDA 11.3 工具包和已安装的 CUDA 11.4 驱动程序编译的
PFN_cuCtxCreate cuUnknown;
CUdriverProcAddressQueryResult driverStatus;
status = cuGetProcAddress("cuCtxCreate", (void**)&cuUnknown, cudaVersion, CU_GET_PROC_ADDRESS_DEFAULT, &driverStatus);
if(CUDA_SUCCESS == status && cuUnknown) {
status = cuUnknown(&ctx, 0, dev);
}
运行此代码,其中 cudaVersion
设置为任何 >=11040(指示 CUDA 11.4)都可能由于未充分提供 cuCtxCreate_v3
API 的 _v3 版本所需的所有参数而导致未定义的行为。
17.5.5. 确定 cuGetProcAddress 失败原因
cuGetProcAddress 存在两种类型的错误。它们是 (1) API/使用错误和 (2) 无法找到请求的驱动程序 API。第一种错误类型将通过 CUresult 返回值返回 API 的错误代码。例如,将 NULL 作为 pfn
变量传递或传递无效的 flags
。
第二种错误类型在 CUdriverProcAddressQueryResult *symbolStatus
中编码,可用于帮助区分驱动程序无法找到请求的符号的潜在问题。以下示例
// cuDeviceGetExecAffinitySupport was introduced in release CUDA 11.4
#include <cuda.h>
CUdriverProcAddressQueryResult driverStatus;
cudaVersion = ...;
status = cuGetProcAddress("cuDeviceGetExecAffinitySupport", &pfn, cudaVersion, 0, &driverStatus);
if (CUDA_SUCCESS == status) {
if (CU_GET_PROC_ADDRESS_VERSION_NOT_SUFFICIENT == driverStatus) {
printf("We can use the new feature when you upgrade cudaVersion to 11.4, but CUDA driver is good to go!\n");
// Indicating cudaVersion was < 11.4 but run against a CUDA driver >= 11.4
}
else if (CU_GET_PROC_ADDRESS_SYMBOL_NOT_FOUND == driverStatus) {
printf("Please update both CUDA driver and cudaVersion to at least 11.4 to use the new feature!\n");
// Indicating driver is < 11.4 since string not found, doesn't matter what cudaVersion was
}
else if (CU_GET_PROC_ADDRESS_SUCCESS == driverStatus && pfn) {
printf("You're using cudaVersion and CUDA driver >= 11.4, using new feature!\n");
pfn();
}
}
返回代码为 CU_GET_PROC_ADDRESS_VERSION_NOT_SUFFICIENT
的第一种情况表明在 CUDA 驱动程序中搜索时找到了 symbol
,但它是在提供的 cudaVersion
之后添加的。在示例中,将 cudaVersion
指定为 11030 或更低,并在针对 CUDA 驱动程序 >= CUDA 11.4 运行时将给出 CU_GET_PROC_ADDRESS_VERSION_NOT_SUFFICIENT
的结果。这是因为 cuDeviceGetExecAffinitySupport
是在 CUDA 11.4 (11040) 中添加的。
返回代码为 CU_GET_PROC_ADDRESS_SYMBOL_NOT_FOUND
的第二种情况表明在 CUDA 驱动程序中搜索时未找到 symbol
。这可能是由于多种原因造成的,例如由于驱动程序较旧而不支持 CUDA 函数以及只是拼写错误。在后一种情况下,类似于最后一个示例,如果用户将 symbol
放入 CUDeviceGetExecAffinitySupport - 请注意以大写 CU 开头的字符串 - cuGetProcAddress
将无法找到 API,因为字符串不匹配。在前一种情况下,一个示例可能是用户针对支持新 API 的 CUDA 驱动程序开发应用程序,并针对较旧的 CUDA 驱动程序部署应用程序。使用最后一个示例,如果开发人员针对 CUDA 11.4 或更高版本进行开发,但针对 CUDA 11.3 驱动程序进行部署,则在其开发期间,他们可能已成功 cuGetProcAddress
,但在针对 CUDA 11.3 驱动程序运行应用程序时,调用将不再起作用,并且在 driverStatus
中返回 CU_GET_PROC_ADDRESS_SYMBOL_NOT_FOUND
。
18. CUDA 环境变量
下表列出了 CUDA 环境变量。与多进程服务相关的环境变量在 GPU 部署和管理指南的多进程服务部分中进行了说明。
变量 |
值 |
描述 |
---|---|---|
设备枚举和属性 |
||
CUDA_VISIBLE_DEVICES |
GPU 标识符逗号分隔序列 MIG 支持: |
GPU 标识符以整数索引或 UUID 字符串形式给出。GPU UUID 字符串应遵循 nvidia-smi 给出的相同格式,例如 GPU-8932f937-d72c-4106-c12f-20bd9faed9f6。但是,为方便起见,允许使用缩写形式;只需从 GPU UUID 的开头指定足够的数字,以便在目标系统中唯一标识该 GPU。例如,CUDA_VISIBLE_DEVICES=GPU-8932f937 可能是引用上述 GPU UUID 的有效方式,前提是系统中没有其他 GPU 共享此前缀。只有索引出现在序列中的设备对 CUDA 应用程序可见,并且它们按照序列的顺序枚举。如果其中一个索引无效,则只有索引在无效索引之前的设备对 CUDA 应用程序可见。例如,将 CUDA_VISIBLE_DEVICES 设置为 2,1 会导致设备 0 不可见,设备 2 在设备 1 之前枚举。将 CUDA_VISIBLE_DEVICES 设置为 0,2,-1,1 会导致设备 0 和 2 可见,设备 1 不可见。MIG 格式以 MIG 关键字开头,GPU UUID 应遵循 nvidia-smi 给出的相同格式。例如,MIG-GPU-8932f937-d72c-4106-c12f-20bd9faed9f6/1/2。仅支持单个 MIG 实例枚举。 |
CUDA_MANAGED_FORCE_DEVICE_ALLOC |
0 或 1(默认为 0) |
强制驱动程序将所有托管分配放置在设备内存中。 |
CUDA_DEVICE_ORDER |
FASTEST_FIRST, PCI_BUS_ID, (默认为 FASTEST_FIRST) |
FASTEST_FIRST 使 CUDA 使用简单的启发式方法以最快到最慢的顺序枚举可用设备。PCI_BUS_ID 按 PCI 总线 ID 升序排列设备。 |
编译 |
||
CUDA_CACHE_DISABLE |
0 或 1(默认为 0) |
禁用缓存(设置为 1 时)或启用缓存(设置为 0 时)以进行即时编译。禁用后,不会向缓存添加或从缓存检索任何二进制代码。 |
CUDA_CACHE_PATH |
文件路径 |
指定即时编译器缓存二进制代码的文件夹;默认值为
|
CUDA_CACHE_MAXSIZE |
整数(桌面/服务器平台的默认值为 1073741824 (1 GiB),嵌入式平台的默认值为 268435456 (256 MiB),最大值为 4294967296 (4 GiB)) |
指定即时编译器使用的缓存大小(以字节为单位)。大小超过缓存大小的二进制代码不会被缓存。如果需要为较新的二进制代码腾出空间,则较旧的二进制代码将从缓存中逐出。 |
CUDA_FORCE_PTX_JIT |
0 或 1(默认为 0) |
设置为 1 时,强制设备驱动程序忽略嵌入在应用程序中的任何二进制代码(请参阅 应用程序兼容性)并即时编译嵌入的 PTX 代码。如果内核没有嵌入 PTX 代码,则加载将失败。此环境变量可用于验证 PTX 代码是否嵌入在应用程序中,并且其即时编译是否按预期工作,以保证应用程序与未来架构的向前兼容性(请参阅 即时编译)。 |
CUDA_DISABLE_PTX_JIT |
0 或 1(默认为 0) |
设置为 1 时,禁用嵌入式 PTX 代码的即时编译,并使用嵌入在应用程序中的兼容二进制代码(请参阅 应用程序兼容性)。如果内核没有嵌入二进制代码或嵌入的二进制代码是为不兼容的架构编译的,则加载将失败。此环境变量可用于验证应用程序是否具有为每个内核生成的兼容 SASS 代码。(请参阅 二进制兼容性)。 |
CUDA_FORCE_JIT |
0 或 1(默认为 0) |
设置为 1 时,强制设备驱动程序忽略嵌入在应用程序中的任何二进制代码(请参阅 应用程序兼容性)并即时编译嵌入的 PTX 代码。如果内核没有嵌入 PTX 代码,则加载将失败。此环境变量可用于验证 PTX 代码是否嵌入在应用程序中,并且其即时编译是否按预期工作,以保证应用程序与未来架构的向前兼容性(请参阅 即时编译)。可以通过设置 |
CUDA_DISABLE_JIT |
0 或 1(默认为 0) |
设置为 1 时,禁用嵌入式 PTX 代码的即时编译,并使用嵌入在应用程序中的兼容二进制代码(请参阅 应用程序兼容性)。如果内核没有嵌入二进制代码或嵌入的二进制代码是为不兼容的架构编译的,则加载将失败。此环境变量可用于验证应用程序是否具有为每个内核生成的兼容 SASS 代码。(请参阅 二进制兼容性)。可以通过设置 |
执行 |
||
CUDA_LAUNCH_BLOCKING |
0 或 1(默认为 0) |
禁用(设置为 1 时)或启用(设置为 0 时)异步内核启动。 |
CUDA_DEVICE_MAX_CONNECTIONS |
1 到 32(默认为 8) |
设置从主机到计算能力为 3.5 及更高版本的每个设备的计算和复制引擎并发连接数(工作队列)。 |
CUDA_DEVICE_MAX_COPY_CONNECTIONS |
1 到 32(默认为 8) |
设置从主机到计算能力为 8.0 及更高版本的每个设备的每个异步复制引擎的复制引擎并发连接数(工作队列)。当同时设置 CUDA_DEVICE_MAX_CONNECTIONS 和 CUDA_DEVICE_MAX_COPY_CONNECTIONS 时,只有 CUDA_DEVICE_MAX_CONNECTIONS 设置的复制连接数将被覆盖。 |
CUDA_AUTO_BOOST |
0 或 1 |
覆盖 nvidia-smi 的 –auto-boost-default 选项设置的自动加速行为。如果应用程序通过此环境变量请求与 nvidia-smi 不同的行为,则如果当前在同一 GPU 上运行的其他应用程序未成功请求不同的行为,则其请求将被接受,否则将被忽略。 |
CUDA_SCALE_LAUNCH_QUEUES |
“0.25x”、“0.5x”、“2x” 或 “4x” |
按固定乘数缩放可用于启动工作的工作队列的大小。 |
cuda-gdb(在 Linux 平台上) |
||
CUDA_DEVICE_WAITS_ON_EXCEPTION |
0 或 1(默认为 0) |
设置为 1 时,当设备异常发生时,CUDA 应用程序将暂停,从而允许附加调试器以进行进一步调试。 |
MPS 服务(在 Linux 平台上) |
||
CUDA_DEVICE_DEFAULT_PERSISTING_L2_CACHE_PERCENTAGE_LIMIT |
百分比值(介于 0 - 100 之间,默认为 0) |
计算能力为 8.x 的设备允许将一部分 L2 缓存设置为持久化对全局内存的数据访问。使用 CUDA MPS 服务时,只能在启动 CUDA MPS 控制守护程序之前使用此环境变量控制设置大小。即,应在运行命令 |
模块加载 |
||
CUDA_MODULE_LOADING |
DEFAULT、LAZY、EAGER(默认为 LAZY) |
指定应用程序的模块加载模式。设置为 EAGER 时,来自 cubin、fatbin 或 PTX 文件中的所有内核和数据都会在相应的 |
CUDA_MODULE_DATA_LOADING |
DEFAULT、LAZY、EAGER(默认为 LAZY) |
指定应用程序的数据加载模式。设置为 EAGER 时,来自 cubin、fatbin 或 PTX 文件中的所有数据都会在相应的 |
预加载依赖库 |
||
CUDA_FORCE_PRELOAD_LIBRARIES |
0 或 1(默认为 0) |
设置为 1 时,强制驱动程序在驱动程序初始化期间预加载 NVVM 和 PTX 即时编译所需的库。这将增加内存占用和 CUDA 驱动程序初始化所需的时间。需要设置此环境变量以避免涉及多个 CUDA 线程的某些死锁情况。 |
CUDA Graphs |
||
CUDA_GRAPHS_USE_NODE_PRIORITY |
0 或 1 |
覆盖图实例化上的 cudaGraphInstantiateFlagUseNodePriority 标志。设置为 1 时,将为所有图设置该标志;设置为 0 时,将为所有图清除该标志。 |
19. 统一内存编程
注意
除非另有说明,否则本章适用于计算能力为 5.0 或更高的设备。对于计算能力低于 5.0 的设备,请参阅 CUDA 11.8 的 CUDA 工具包文档。
关于统一内存的文档分为 3 个部分
19.1. 统一内存简介
CUDA 统一内存为所有处理器提供
单个统一内存池,即,单个指针值使系统中的所有处理器(所有 CPU、所有 GPU 等)能够使用其所有本机内存操作(指针解引用、原子操作等)访问此内存。
系统中所有处理器对统一内存池的并发访问。
统一内存通过以下几种方式改进 GPU 编程
生产力:GPU 程序可以从 GPU 和 CPU 线程并发访问统一内存,而无需创建单独的分配 (
cudaMalloc()
) 并手动来回复制内存 (cudaMemcpy*()
)。性能:
通过将数据迁移到最频繁访问它的处理器,可以最大限度地提高数据访问速度。应用程序可以触发数据的手动迁移,并可以使用提示来控制迁移启发式方法。
通过避免在 CPU 和 GPU 上复制内存,可以减少总系统内存使用量。
功能:它使 GPU 程序能够在超过 GPU 内存容量的数据上工作。
使用 CUDA 统一内存,数据移动仍然会发生,提示可能会提高性能。这些提示不是正确性或功能所必需的,也就是说,程序员可以首先专注于跨 GPU 和 CPU 并行化其应用程序,然后在开发周期的后期担心数据移动,作为性能优化。请注意,数据的物理位置对程序不可见,并且可能随时更改,但从任何处理器访问数据的虚拟地址将保持有效和一致,而与局部性无关。
有两种主要方法可以获得 CUDA 统一内存
系统分配内存:在主机上使用系统 API 分配的内存:堆栈变量、全局/文件作用域变量、
malloc()
/mmap()
(有关深入示例,请参阅 系统分配内存:深入示例)、线程局部变量等。显式分配统一内存的 CUDA API:例如,使用
cudaMallocManaged()
分配的内存在更多系统上可用,并且可能比系统分配内存性能更好。
19.1.1. 统一内存的系统要求
下表显示了对 CUDA 统一内存的不同级别的支持、检测这些支持级别所需的设备属性以及指向每个支持级别特定文档的链接
统一内存支持级别 |
系统设备属性 |
更多文档 |
---|---|---|
完整 CUDA 统一内存:所有内存都具有完整支持。这包括系统分配内存和 CUDA 托管内存。 |
设置为 1:
pageableMemoryAccess 具有硬件加速的系统 也将以下属性设置为 1
hostNativeAtomicSupported 、pageableMemoryAccessUsesHostPageTables 、directManagedMemAccessFromHost |
|
只有 CUDA 托管内存具有完整支持。 |
设置为 1:
concurrentManagedAccess 设置为 0:
pageableMemoryAccess |
|
没有完整支持的 CUDA 托管内存:统一寻址,但没有并发访问。 |
设置为 1:
managedMemory 设置为 0:
concurrentManagedAccess |
|
没有统一内存支持。 |
设置为 0: |
尝试在不支持统一内存的系统上使用统一内存的应用程序的行为是未定义的。以下属性使 CUDA 应用程序能够检查系统对统一内存的支持级别,并在具有不同支持级别的系统之间实现可移植性
pageableMemoryAccess
:在具有 CUDA 统一内存支持的系统上,此属性设置为 1,其中所有线程都可以访问系统分配内存和 CUDA 托管内存。这些系统包括 NVIDIA Grace Hopper、IBM Power9 + Volta 以及启用 HMM 的现代 Linux 系统(请参阅下一项),等等。Linux HMM 需要 Linux 内核版本 6.1.24+、6.2.11+ 或 6.3+,计算能力为 7.5 或更高的设备,以及安装了 Open Kernel Modules 的 CUDA 驱动程序版本 535+。
concurrentManagedAccess
:在具有完整 CUDA 托管内存支持的系统上,此属性设置为 1。当此属性设置为 0 时,CUDA 托管内存中仅部分支持统一内存。有关 Tegra 对统一内存的支持,请参阅 Tegra 的 CUDA 内存管理。
程序可以通过使用 cudaGetDeviceProperties()
查询上面表 统一内存支持级别概述 中的属性来查询 GPU 对 CUDA 统一内存的支持级别。
19.1.2. 编程模型
使用 CUDA 统一内存,不再需要主机和设备之间的单独分配以及它们之间的显式内存传输。程序可以通过以下方式分配统一内存
系统分配 API:在 具有完整 CUDA 统一内存支持的系统 上,通过主机进程的任何系统分配(C 的
malloc()
、C++ 的new
运算符、POSIX 的mmap
等)。CUDA 托管内存分配 API:通过
cudaMallocManaged()
API,该 API 在语法上类似于cudaMalloc()
。CUDA 托管变量:使用
__managed__
声明的变量,其语义类似于__device__
变量。
本章中的大多数示例都提供了至少两个版本,一个使用 CUDA 托管内存,另一个使用系统分配内存。选项卡允许您在它们之间进行选择。以下示例说明了统一内存如何简化 CUDA 程序
__global__ void write_value(int* ptr, int v) {
*ptr = v;
}
int main() {
int* d_ptr = nullptr;
// Does not require any unified memory support
cudaMalloc(&d_ptr, sizeof(int));
write_value<<<1, 1>>>(d_ptr, 1);
int h_value;
// Copy memory back to the host and synchronize
cudaMemcpy(&h_value, d_ptr, sizeof(int),
cudaMemcpyDefault);
printf("value = %d\n", h_value);
cudaFree(d_ptr);
return 0;
}
|
__global__ void write_value(int* ptr, int v) {
*ptr = v;
}
int main() {
// Requires System-Allocated Memory support
int* ptr = (int*)malloc(sizeof(int));
write_value<<<1, 1>>>(ptr, 1);
// Synchronize required
// (before, cudaMemcpy was synchronizing)
cudaDeviceSynchronize();
printf("value = %d\n", *ptr);
free(ptr);
return 0;
}
|
__global__ void write_value(int* ptr, int v) {
*ptr = v;
}
int main() {
int* d_ptr = nullptr;
// Does not require any unified memory support
cudaMalloc(&d_ptr, sizeof(int));
write_value<<<1, 1>>>(d_ptr, 1);
int h_value;
// Copy memory back to the host and synchronize
cudaMemcpy(&h_value, d_ptr, sizeof(int),
cudaMemcpyDefault);
printf("value = %d\n", h_value);
cudaFree(d_ptr);
return 0;
}
|
__global__ void write_value(int* ptr, int v) {
*ptr = v;
}
int main() {
// Requires System-Allocated Memory support
int value;
write_value<<<1, 1>>>(&value, 1);
// Synchronize required
// (before, cudaMemcpy was synchronizing)
cudaDeviceSynchronize();
printf("value = %d\n", value);
return 0;
}
|
__global__ void write_value(int* ptr, int v) {
*ptr = v;
}
int main() {
int* d_ptr = nullptr;
// Does not require any unified memory support
cudaMalloc(&d_ptr, sizeof(int));
write_value<<<1, 1>>>(d_ptr, 1);
int h_value;
// Copy memory back to the host and synchronize
cudaMemcpy(&h_value, d_ptr, sizeof(int),
cudaMemcpyDefault);
printf("value = %d\n", h_value);
cudaFree(d_ptr);
return 0;
}
|
__global__ void write_value(int* ptr, int v) {
*ptr = v;
}
int main() {
int* ptr = nullptr;
// Requires CUDA Managed Memory support
cudaMallocManaged(&ptr, sizeof(int));
write_value<<<1, 1>>>(ptr, 1);
// Synchronize required
// (before, cudaMemcpy was synchronizing)
cudaDeviceSynchronize();
printf("value = %d\n", *ptr);
cudaFree(ptr);
return 0;
}
|
__global__ void write_value(int* ptr, int v) {
*ptr = v;
}
int main() {
int* d_ptr = nullptr;
// Does not require any unified memory support
cudaMalloc(&d_ptr, sizeof(int));
write_value<<<1, 1>>>(d_ptr, 1);
int h_value;
// Copy memory back to the host and synchronize
cudaMemcpy(&h_value, d_ptr, sizeof(int),
cudaMemcpyDefault);
printf("value = %d\n", h_value);
cudaFree(d_ptr);
return 0;
}
|
__global__ void write_value(int* ptr, int v) {
*ptr = v;
}
// Requires CUDA Managed Memory support
__managed__ int value;
int main() {
write_value<<<1, 1>>>(&value, 1);
// Synchronize required
// (before, cudaMemcpy was synchronizing)
cudaDeviceSynchronize();
printf("value = %d\n", value);
return 0;
}
|
在上面的示例中,设备写入一个值,然后由主机读取
没有统一内存:需要为写入的值提供主机端和设备端存储 (
h_value
和示例中的d_ptr
),并且还需要使用cudaMemcpy()
在两者之间进行显式复制。使用统一内存:设备直接从主机访问数据。
ptr
/value
可以使用,而无需单独的h_value
/d_ptr
分配,并且不需要复制例程,从而大大简化并减小程序的大小。使用系统分配:无需其他更改。
托管内存:数据分配更改为使用
cudaMallocManaged()
,它返回一个对主机和设备代码都有效的指针。
19.1.2.1. 系统分配内存的分配 API
在 具有完整 CUDA 统一内存支持的系统 上,所有内存都是统一内存。这包括使用系统分配 API 分配的内存,例如 malloc()
、mmap()
、C++ new()
运算符,以及 CPU 线程堆栈上的自动变量、线程局部变量、全局变量等等。
系统分配内存可能会在首次访问时填充,具体取决于使用的 API 和系统设置。首次访问意味着
分配 API 分配虚拟内存并立即返回,并且
当线程首次访问内存时,会填充物理内存。
通常,物理内存将被选择为“接近”运行该线程的处理器。例如,
GPU 线程首先访问它:选择运行该线程的 GPU 的物理 GPU 内存。
CPU 线程首先访问它:选择运行该线程的 CPU 核心的内存 NUMA 节点中的物理 CPU 内存。
CUDA 统一内存提示和预取 API、cudaMemAdvise
和 cudaMemPreftchAsync
可以在系统分配内存上使用。这些 API 在下面的 数据使用提示 部分中介绍。
__global__ void printme(char *str) {
printf(str);
}
int main() {
// Allocate 100 bytes of memory, accessible to both Host and Device code
char *s = (char*)malloc(100);
// Physical allocation placed in CPU memory because host accesses "s" first
strncpy(s, "Hello Unified Memory\n", 99);
// Here we pass "s" to a kernel without explicitly copying
printme<<< 1, 1 >>>(s);
cudaDeviceSynchronize();
// Free as for normal CUDA allocations
cudaFree(s);
return 0;
}
19.1.2.2. CUDA 托管内存的分配 API:cudaMallocManaged()
在具有 CUDA 托管内存支持的系统上,可以使用以下方法分配统一内存
__host__ cudaError_t cudaMallocManaged(void **devPtr, size_t size);
此 API 在语法上与 cudaMalloc()
相同:它分配 size
字节的托管内存,并将 devPtr
设置为引用该分配。CUDA 托管内存也使用 cudaFree()
释放。
在 具有完整 CUDA 托管内存支持的系统 上,系统中的所有 CPU 和 GPU 可以并发访问托管内存分配。将主机对 cudaMalloc()
的调用替换为 cudaMallocManaged()
不会影响这些系统上的程序语义;设备代码无法调用 cudaMallocManaged()
。
以下示例显示了 cudaMallocManaged()
的用法
__global__ void printme(char *str) {
printf(str);
}
int main() {
// Allocate 100 bytes of memory, accessible to both Host and Device code
char *s;
cudaMallocManaged(&s, 100);
// Note direct Host-code use of "s"
strncpy(s, "Hello Unified Memory\n", 99);
// Here we pass "s" to a kernel without explicitly copying
printme<<< 1, 1 >>>(s);
cudaDeviceSynchronize();
// Free as for normal CUDA allocations
cudaFree(s);
return 0;
}
注意
对于支持 CUDA 托管内存分配但不提供完整支持的系统,请参阅 一致性和并发性。实现细节(可能随时更改)
计算能力为 5.x 的设备在 GPU 上分配 CUDA 托管内存。
计算能力为 6.x 及更高的设备在首次访问时填充内存,就像系统分配内存 API 一样。
19.1.2.3. 使用 __managed__
的全局作用域托管变量
CUDA __managed__
变量的行为就像它们是通过 cudaMallocManaged()
分配的一样(请参阅 CUDA 托管内存的分配 API:cudaMallocManaged())。它们简化了带有全局变量的程序,使在主机和设备之间交换数据变得特别容易,而无需手动分配或复制。
在 具有完整 CUDA 统一内存支持的系统 上,设备代码无法直接访问文件作用域或全局作用域变量。但是,指向这些变量的指针可以作为参数传递给内核,有关示例,请参阅 系统分配内存:深入示例。
__global__ void write_value(int* ptr, int v) {
*ptr = v;
}
int main() {
// Requires System-Allocated Memory support
int value;
write_value<<<1, 1>>>(&value, 1);
// Synchronize required
// (before, cudaMemcpy was synchronizing)
cudaDeviceSynchronize();
printf("value = %d\n", value);
return 0;
}
__global__ void write_value(int* ptr, int v) {
*ptr = v;
}
// Requires CUDA Managed Memory support
__managed__ int value;
int main() {
write_value<<<1, 1>>>(&value, 1);
// Synchronize required
// (before, cudaMemcpy was synchronizing)
cudaDeviceSynchronize();
printf("value = %d\n", value);
return 0;
}
请注意,缺少显式的 cudaMemcpy()
命令,以及写入值 value
在 CPU 和 GPU 上都可见的事实。
CUDA __managed__
变量意味着 __device__
,并且等效于 __managed__ __device__
,这也是允许的。标记为 __constant__
的变量不能标记为 __managed__
。
有效的 CUDA 上下文对于 __managed__
变量的正确操作是必要的。如果尚未为当前设备创建上下文,则访问 __managed__
变量可能会触发 CUDA 上下文创建。在上面的示例中,在内核启动之前访问 value
会触发默认设备上的上下文创建。在没有该访问的情况下,内核启动将触发上下文创建。
声明为 __managed__
的 C++ 对象受到某些特定约束,尤其是在静态初始化程序方面。有关这些约束的列表,请参阅 C++ 语言支持。
注意
对于 不具有完整支持的 CUDA 托管内存设备,在 CUDA 流中执行的异步操作的 __managed__
变量的可见性在关于 管理数据可见性和 CPU + GPU 使用流的并发访问 的部分中讨论。
19.1.2.4. 统一内存和映射内存之间的区别
统一内存和 映射内存 之间的主要区别在于,CUDA 映射内存不保证所有类型的内存访问(例如原子操作)在所有系统上都受支持,而统一内存则保证。CUDA 映射内存保证可移植支持的有限内存操作集比统一内存在更多系统上可用。
19.1.2.5. 指针属性
CUDA 程序可以通过调用 cudaPointerGetAttributes()
并测试指针属性 value
是否为 cudaMemoryTypeManaged
来检查指针是否寻址 CUDA 托管内存分配。
对于已使用 cudaHostRegister()
注册的系统分配内存,此 API 返回 cudaMemoryTypeHost
,对于 CUDA 不知道的系统分配内存,此 API 返回 cudaMemoryTypeUnregistered
。
指针属性不说明内存驻留在哪里,它们说明内存是如何分配或注册的。
以下示例显示了如何在运行时检测指针类型
char const* kind(cudaPointerAttributes a, bool pma, bool cma) {
switch(a.type) {
case cudaMemoryTypeHost: return pma?
"Unified: CUDA Host or Registered Memory" :
"Not Unified: CUDA Host or Registered Memory";
case cudaMemoryTypeDevice: return "Not Unified: CUDA Device Memory";
case cudaMemoryTypeManaged: return cma?
"Unified: CUDA Managed Memory" : "Not Unified: CUDA Managed Memory";
case cudaMemoryTypeUnregistered: return pma?
"Unified: System-Allocated Memory" :
"Not Unified: System-Allocated Memory";
default: return "unknown";
}
}
void check_pointer(int i, void* ptr) {
cudaPointerAttributes attr;
cudaPointerGetAttributes(&attr, ptr);
int pma = 0, cma = 0, device = 0;
cudaGetDevice(&device);
cudaDeviceGetAttribute(&pma, cudaDevAttrPageableMemoryAccess, device);
cudaDeviceGetAttribute(&cma, cudaDevAttrConcurrentManagedAccess, device);
printf("Pointer %d: memory is %s\n", i, kind(attr, pma, cma));
}
__managed__ int managed_var = 5;
int main() {
int* ptr[5];
ptr[0] = (int*)malloc(sizeof(int));
cudaMallocManaged(&ptr[1], sizeof(int));
cudaMallocHost(&ptr[2], sizeof(int));
cudaMalloc(&ptr[3], sizeof(int));
ptr[4] = &managed_var;
for (int i = 0; i < 5; ++i) check_pointer(i, ptr[i]);
cudaFree(ptr[3]);
cudaFreeHost(ptr[2]);
cudaFree(ptr[1]);
free(ptr[0]);
return 0;
}
19.1.2.6. 运行时检测统一内存支持级别
以下示例显示了如何在运行时检测统一内存支持级别
int main() {
int d;
cudaGetDevice(&d);
int pma = 0;
cudaDeviceGetAttribute(&pma, cudaDevAttrPageableMemoryAccess, d);
printf("Full Unified Memory Support: %s\n", pma == 1? "YES" : "NO");
int cma = 0;
cudaDeviceGetAttribute(&cma, cudaDevAttrConcurrentManagedAccess, d);
printf("CUDA Managed Memory with full support: %s\n", cma == 1? "YES" : "NO");
return 0;
}
19.1.2.7. GPU 内存超额订阅
统一内存使应用程序能够超额订阅任何单个处理器的内存:换句话说,它们可以分配和共享大于系统中任何单个处理器内存容量的数组,从而实现超出单个 GPU 的数据集的核心外处理,而不会给编程模型增加显著的复杂性。
19.1.2.8. 性能提示
以下章节介绍可用的统一内存性能提示,这些提示可用于所有统一内存,例如 CUDA 托管内存,或者在具有完整 CUDA 统一内存支持的系统上,也可用于所有系统分配的内存。 这些 API 是提示,也就是说,它们不会影响应用程序的语义,只会影响其性能。 也就是说,它们可以添加到任何应用程序的任何位置或从任何位置删除,而不会影响其结果。
CUDA 统一内存可能并不总是拥有做出与统一内存相关的最佳性能决策所需的所有信息。 这些性能提示使应用程序能够向 CUDA 提供更多信息。
请注意,应用程序只有在这些提示可以提高其性能时才应使用它们。
19.1.2.8.1. 数据预取
cudaMemPrefetchAsync
API 是一个异步的、流有序的 API,它可以将数据迁移到更靠近指定处理器的位置。 数据可以在预取时被访问。 迁移在流中所有先前的操作完成之后才会开始,并在流中任何后续操作之前完成。
cudaError_t cudaMemPrefetchAsync(const void *devPtr,
size_t count,
int dstDevice,
cudaStream_t stream);
当预取任务在给定的 stream
中执行时,包含 [devPtr, devPtr + count)
的内存区域可能会被迁移到目标设备 dstDevice
- 或者如果使用 cudaCpuDeviceId
则迁移到 CPU。
考虑以下简单的代码示例
void test_prefetch_sam(cudaStream_t s) {
char *data = (char*)malloc(N);
init_data(data, N); // execute on CPU
cudaMemPrefetchAsync(data, N, myGpuId, s); // prefetch to GPU
mykernel<<<(N + TPB - 1) / TPB, TPB, 0, s>>>(data, N); // execute on GPU
cudaMemPrefetchAsync(data, N, cudaCpuDeviceId, s); // prefetch to CPU
cudaStreamSynchronize(s);
use_data(data, N);
free(data);
}
void test_prefetch_managed(cudaStream_t s) {
char *data;
cudaMallocManaged(&data, N);
init_data(data, N); // execute on CPU
cudaMemPrefetchAsync(data, N, myGpuId, s); // prefetch to GPU
mykernel<<<(N + TPB - 1) / TPB, TPB, 0, s>>>(data, N); // execute on GPU
cudaMemPrefetchAsync(data, N, cudaCpuDeviceId, s); // prefetch to CPU
cudaStreamSynchronize(s);
use_data(data, N);
cudaFree(data);
}
19.1.2.8.2. 数据使用提示
当多个处理器同时访问相同的数据时,可以使用 cudaMemAdvise
来提示如何访问 [devPtr, devPtr + count)
处的数据
cudaError_t cudaMemAdvise(const void *devPtr,
size_t count,
enum cudaMemoryAdvise advice,
int device);
其中 advice
可以取以下值
cudaMemAdviseSetReadMostly
: 这意味着数据主要将被读取,只有偶尔会被写入。 通常,它允许在此区域上权衡读取带宽和写入带宽。 示例
void test_advise_managed(cudaStream_t stream) {
char *dataPtr;
size_t dataSize = 64 * TPB; // 16 KiB
// Allocate memory using cudaMallocManaged
// (malloc may be used on systems with full CUDA Unified memory support)
cudaMallocManaged(&dataPtr, dataSize);
// Set the advice on the memory region
cudaMemAdvise(dataPtr, dataSize, cudaMemAdviseSetReadMostly, myGpuId);
int outerLoopIter = 0;
while (outerLoopIter < maxOuterLoopIter) {
// The data is written to in the outer loop on the CPU
init_data(dataPtr, dataSize);
// The data is made available to all GPUs by prefetching.
// Prefetching here causes read duplication of data instead
// of data migration
for (int device = 0; device < maxDevices; device++) {
cudaMemPrefetchAsync(dataPtr, dataSize, device, stream);
}
// The kernel only reads this data in the inner loop
int innerLoopIter = 0;
while (innerLoopIter < maxInnerLoopIter) {
mykernel<<<32, TPB, 0, stream>>>((const char *)dataPtr, dataSize);
innerLoopIter++;
}
outerLoopIter++;
}
cudaFree(dataPtr);
}
cudaMemAdviseSetPreferredLocation
: 通常,任何内存都可能在任何时候迁移到任何位置,例如,当给定处理器物理内存不足时。 此提示告诉系统,不希望将此内存区域从其首选位置迁移走,方法是将数据的首选位置设置为属于设备的物理内存。 为 device 传入cudaCpuDeviceId
值会将首选位置设置为 CPU 内存。 其他提示,如cudaMemPrefetchAsync
,可能会覆盖此提示,导致内存从其首选位置迁移走。
cudaMemAdviseSetAccessedBy
: 在某些系统中,在从给定处理器访问数据之前,建立到内存的映射可能对性能有利。 此提示告诉系统,数据将经常被device
访问,使系统能够假设创建这些映射是值得的。 此提示并不意味着数据应该驻留在哪里,但它可以与cudaMemAdviseSetPreferredLocation
结合使用来指定这一点。
每个建议也可以使用以下值之一取消设置:cudaMemAdviseUnsetReadMostly
、cudaMemAdviseUnsetPreferredLocation
和 cudaMemAdviseUnsetAccessedBy
。
19.1.2.8.3. 查询托管内存上的数据使用属性
程序可以使用以下 API 查询通过 cudaMemAdvise
或 cudaMemPrefetchAsync
在 CUDA 托管内存上分配的内存范围属性
cudaMemRangeGetAttribute(void *data,
size_t dataSize,
enum cudaMemRangeAttribute attribute,
const void *devPtr,
size_t count);
此函数查询从 devPtr
开始、大小为 count
字节的内存范围的属性。 内存范围必须引用通过 cudaMallocManaged
分配或通过 __managed__
变量声明的托管内存。 可以查询以下属性
cudaMemRangeAttributeReadMostly
: 如果整个内存范围设置了cudaMemAdviseSetReadMostly
属性,则返回的结果将为 1,否则为 0。cudaMemRangeAttributePreferredLocation
: 如果整个内存范围将相应的处理器作为首选位置,则返回的结果将是 GPU 设备 ID 或cudaCpuDeviceId
,否则将返回cudaInvalidDeviceId
。 应用程序可以使用此查询 API 来决定是通过 CPU 还是 GPU 暂存数据,具体取决于托管指针的首选位置属性。 请注意,在查询时,内存范围的实际位置可能与首选位置不同。cudaMemRangeAttributeAccessedBy
: 将返回为该内存范围设置了该建议的设备列表。cudaMemRangeAttributeLastPrefetchLocation
: 将返回使用cudaMemPrefetchAsync
显式预取内存范围的最后一个位置。 请注意,这仅返回应用程序请求将内存范围预取到的最后一个位置。 它不指示到该位置的预取操作是否已完成甚至开始。
此外,可以使用相应的 cudaMemRangeGetAttributes
函数查询多个属性。
19.2. 在具有完整 CUDA 统一内存支持的设备上使用统一内存
19.2.1. 系统分配的内存:深入示例
具有完整 CUDA 统一内存支持的系统允许设备访问与设备交互的主机进程拥有的任何内存。 本节展示了一些高级用例,使用一个内核,该内核只是将输入字符数组的前 8 个字符打印到标准输出流
__global__ void kernel(const char* type, const char* data) {
static const int n_char = 8;
printf("%s - first %d characters: '", type, n_char);
for (int i = 0; i < n_char; ++i) printf("%c", data[i]);
printf("'\n");
}
以下选项卡显示了调用此内核的各种方式
void test_malloc() {
const char test_string[] = "Hello World";
char* heap_data = (char*)malloc(sizeof(test_string));
strncpy(heap_data, test_string, sizeof(test_string));
kernel<<<1, 1>>>("malloc", heap_data);
ASSERT(cudaDeviceSynchronize() == cudaSuccess,
"CUDA failed with '%s'", cudaGetErrorString(cudaGetLastError()));
free(heap_data);
}
void test_managed() {
const char test_string[] = "Hello World";
char* data;
cudaMallocManaged(&data, sizeof(test_string));
strncpy(data, test_string, sizeof(test_string));
kernel<<<1, 1>>>("managed", data);
ASSERT(cudaDeviceSynchronize() == cudaSuccess,
"CUDA failed with '%s'", cudaGetErrorString(cudaGetLastError()));
cudaFree(data);
}
void test_stack() {
const char test_string[] = "Hello World";
kernel<<<1, 1>>>("stack", test_string);
ASSERT(cudaDeviceSynchronize() == cudaSuccess,
"CUDA failed with '%s'", cudaGetErrorString(cudaGetLastError()));
}
void test_static() {
static const char test_string[] = "Hello World";
kernel<<<1, 1>>>("static", test_string);
ASSERT(cudaDeviceSynchronize() == cudaSuccess,
"CUDA failed with '%s'", cudaGetErrorString(cudaGetLastError()));
}
const char global_string[] = "Hello World";
void test_global() {
kernel<<<1, 1>>>("global", global_string);
ASSERT(cudaDeviceSynchronize() == cudaSuccess,
"CUDA failed with '%s'", cudaGetErrorString(cudaGetLastError()));
}
// declared in separate file, see below
extern char* ext_data;
void test_extern() {
kernel<<<1, 1>>>("extern", ext_data);
ASSERT(cudaDeviceSynchronize() == cudaSuccess,
"CUDA failed with '%s'", cudaGetErrorString(cudaGetLastError()));
}
/** This may be a non-CUDA file */
char* ext_data;
static const char global_string[] = "Hello World";
void __attribute__ ((constructor)) setup(void) {
ext_data = (char*)malloc(sizeof(global_string));
strncpy(ext_data, global_string, sizeof(global_string));
}
void __attribute__ ((destructor)) tear_down(void) {
free(ext_data);
}
以上前三个选项卡显示了 编程模型部分 中已详细介绍的示例。 接下来的三个选项卡显示了从设备访问文件作用域或全局作用域变量的各种方式。
请注意,对于外部变量,它可以由不与 CUDA 交互的第三方库声明并拥有和管理其内存。
另请注意,堆栈变量以及文件作用域和全局作用域变量只能通过 GPU 的指针访问。 在此特定示例中,这很方便,因为字符数组已声明为指针:const char*
。 但是,请考虑以下带有全局作用域整数的示例
// this variable is declared at global scope
int global_variable;
__global__ void kernel_uncompilable() {
// this causes a compilation error: global (__host__) variables must not
// be accessed from __device__ / __global__ code
printf("%d\n", global_variable);
}
// On systems with pageableMemoryAccess set to 1, we can access the address
// of a global variable. The below kernel takes that address as an argument
__global__ void kernel(int* global_variable_addr) {
printf("%d\n", *global_variable_addr);
}
int main() {
kernel<<<1, 1>>>(&global_variable);
...
return 0;
}
在上面的示例中,我们需要确保将全局变量的指针传递给内核,而不是直接在内核中访问全局变量。 这是因为默认情况下,没有 __managed__
说明符的全局变量被声明为仅 __host__
,因此到目前为止,大多数编译器不允许在设备代码中直接使用这些变量。
19.2.1.1. 文件支持的统一内存
由于具有完整 CUDA 统一内存支持的系统允许设备访问主机进程拥有的任何内存,因此它们可以直接访问文件支持的内存。
在这里,我们展示了前一节中显示的初始示例的修改版本,以使用文件支持的内存,以便从 GPU 打印字符串,直接从输入文件读取。 在以下示例中,内存由物理文件支持,但该示例也适用于内存支持的文件,如关于 与统一内存的进程间通信 (IPC) 的章节中所详述。
__global__ void kernel(const char* type, const char* data) {
static const int n_char = 8;
printf("%s - first %d characters: '", type, n_char);
for (int i = 0; i < n_char; ++i) printf("%c", data[i]);
printf("'\n");
}
void test_file_backed() {
int fd = open(INPUT_FILE_NAME, O_RDONLY);
ASSERT(fd >= 0, "Invalid file handle");
struct stat file_stat;
int status = fstat(fd, &file_stat);
ASSERT(status >= 0, "Invalid file stats");
char* mapped = (char*)mmap(0, file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
ASSERT(mapped != MAP_FAILED, "Cannot map file into memory");
kernel<<<1, 1>>>("file-backed", mapped);
ASSERT(cudaDeviceSynchronize() == cudaSuccess,
"CUDA failed with '%s'", cudaGetErrorString(cudaGetLastError()));
ASSERT(munmap(mapped, file_stat.st_size) == 0, "Cannot unmap file");
ASSERT(close(fd) == 0, "Cannot close file");
}
请注意,在没有 hostNativeAtomicSupported
属性的系统上,包括启用了 Linux HMM 的系统,不支持对文件支持的内存的原子访问。
19.2.1.2. 与统一内存的进程间通信 (IPC)
注意
到目前为止,将 IPC 与统一内存一起使用可能会对性能产生重大影响。
许多应用程序更喜欢每个进程管理一个 GPU,但仍然需要使用统一内存,例如用于过度订阅,并从多个 GPU 访问它。
CUDA IPC(参见 进程间通信)不支持托管内存:无法通过本节中讨论的任何机制共享此类内存的句柄。 在具有完整 CUDA 统一内存支持的系统上,系统分配的内存是进程间通信 (IPC) 兼容的。 一旦系统分配的内存的访问权限与其他进程共享,相同的 编程模型 适用,类似于 文件支持的统一内存。
有关在 Linux 下创建支持 IPC 的系统分配内存的各种方法的更多信息,请参见以下参考资料
请注意,使用此技术无法在不同的主机及其设备之间共享内存。
19.2.2. 性能调优
为了在使用统一内存时获得良好的性能,重要的是
了解分页在您的系统上是如何工作的,以及如何避免不必要的分页错误。
了解各种机制,这些机制允许将数据保持在访问处理器的本地。
考虑针对系统的内存传输粒度调整您的应用程序。
作为一般建议,性能提示可能会提供更高的性能,但不正确地使用它们可能会降低性能,与默认行为相比。 另请注意,任何提示都具有与之相关联的主机上的性能成本,因此有用的提示必须至少将性能提高到足以克服此成本。
19.2.2.1. 内存分页和页面大小
许多关于统一内存性能调优的章节都假设了关于虚拟寻址、内存页和页面大小的先验知识。 本节尝试定义所有必要的术语,并解释为什么分页对性能很重要。
所有当前支持统一内存的系统都使用虚拟地址空间:这意味着应用程序使用的内存地址表示虚拟位置,该位置可能映射到内存实际驻留的物理位置。
所有当前支持的处理器,包括 CPU 和 GPU,还使用内存分页。 因为所有系统都使用虚拟地址空间,所以有两种类型的内存页
虚拟页:这表示操作系统跟踪的每个进程的固定大小的连续虚拟内存块,可以映射到物理内存中。 请注意,虚拟页链接到映射:例如,可以使用不同的页面大小将单个虚拟地址映射到物理内存中。
物理页:这表示处理器的主内存管理单元 (MMU) 支持的固定大小的连续内存块,虚拟页可以映射到其中。
目前,所有 x86_64 CPU 都使用 4KiB 物理页。 Arm CPU 支持多种物理页面大小 - 4KiB、16KiB、32KiB 和 64KiB - 具体取决于确切的 CPU。 最后,NVIDIA GPU 支持多种物理页面大小,但更喜欢 2MiB 或更大的物理页。 请注意,这些大小可能会在未来的硬件中发生变化。
虚拟页的默认页面大小通常对应于物理页面大小,但应用程序可以使用不同的页面大小,只要它们受操作系统和硬件支持即可。 通常,支持的虚拟页面大小必须是 2 的幂,并且是物理页面大小的倍数。
跟踪虚拟页到物理页的映射的逻辑实体将被称为页表,并且给定虚拟页与给定虚拟大小到物理页的每个映射都称为页表条目 (PTE)。 所有支持的处理器都为页表提供特定的缓存,以加速虚拟地址到物理地址的转换。 这些缓存称为转换后备缓冲区 (TLB)。
应用程序的性能调优有两个重要方面
虚拟页面大小的选择,
系统是否提供 CPU 和 GPU 都使用的组合页表,还是每个 CPU 和 GPU 分别使用单独的页表。
19.2.2.1.1. 选择合适的页面大小
通常,小页面大小会导致更少的(虚拟)内存碎片,但会导致更多的 TLB 未命中,而大页面大小会导致更多的内存碎片,但会导致更少的 TLB 未命中。 此外,与小页面大小相比,使用大页面大小的内存迁移通常更昂贵,因为我们通常迁移完整的内存页。 这可能会在使用大页面大小的应用程序中导致更大的延迟峰值。 另请参见下一节,了解有关页面错误的更多详细信息。
性能调优的一个重要方面是,与 CPU 相比,GPU 上的 TLB 未命中通常明显更昂贵。 这意味着,如果 GPU 线程频繁访问使用足够小的页面大小映射的统一内存的随机位置,则与使用足够大的页面大小映射的统一内存的相同访问相比,它可能会明显更慢。 虽然 CPU 线程随机访问使用小页面大小映射的大片内存区域也可能发生类似的效果,但速度下降不太明显,这意味着应用程序可能希望权衡这种速度下降,以减少内存碎片。
请注意,通常,应用程序不应将其性能调整为给定处理器的物理页面大小,因为物理页面大小可能会根据硬件而变化。 上述建议仅适用于虚拟页面大小。
19.2.2.1.2. CPU 和 GPU 页表:硬件一致性与软件一致性
注意
在性能调优文档的其余部分中,我们将 CPU 和 GPU 都使用组合页表的系统称为硬件一致性系统。 CPU 和 GPU 使用单独页表的系统称为软件一致性系统。
硬件一致性系统(如 NVIDIA Grace Hopper)为 CPU 和 GPU 提供逻辑上组合的页表。 这很重要,因为为了从 GPU 访问系统分配的内存,GPU 使用 CPU 为请求的内存创建的任何页表条目。 如果该页表条目使用 4KiB 或 64KiB 的默认 CPU 页面大小,则对大型虚拟内存区域的访问将导致大量的 TLB 未命中,从而导致明显的减速。
有关如何确保系统分配的内存使用足够大的页面大小以避免此类问题的示例,请参见关于配置巨页的章节。
另一方面,在 CPU 和 GPU 各自拥有自己的逻辑页表的系统上,应考虑不同的性能调优方面:为了保证一致性,在处理器访问映射到不同处理器的物理内存中的内存地址的情况下,这些系统通常使用页面错误。 这样的页面错误意味着
需要确保当前所有者处理器(物理页面当前驻留的位置)不能再访问此页面,方法是删除页表条目或更新它。
需要确保请求访问的处理器可以访问此页面,方法是创建新的页表条目或更新现有条目,使其变为有效/活动状态。
支持此虚拟页面的物理页面必须移动/迁移到请求访问的处理器:这可能是一个昂贵的操作,并且工作量与页面大小成正比。
总的来说,在 CPU 和 GPU 线程频繁并发访问同一内存页的情况下,与软件一致性系统相比,硬件一致性系统提供了显着的性能优势
更少的页面错误:这些系统不需要使用页面错误来模拟一致性或迁移内存,
更少的争用:这些系统在缓存行粒度而不是页面大小粒度上是一致的,也就是说,当多个处理器在缓存行内发生争用时,只交换缓存行,这比最小的页面大小小得多,并且当不同的处理器访问页面内的不同缓存行时,则没有争用。
这会影响以下场景的性能
从 CPU 和 GPU 并发地对同一地址进行原子更新。
从 CPU 线程或反之亦然向 GPU 线程发出信号。
19.2.2.2. 从主机直接访问统一内存
某些设备具有硬件支持,用于从主机对 GPU 驻留的统一内存进行一致的读取、存储和原子访问。 这些设备将属性 cudaDevAttrDirectManagedMemAccessFromHost
设置为 1。 请注意,所有硬件一致性系统都为 NVLink 连接的设备设置了此属性。 在这些系统上,主机可以直接访问 GPU 驻留的内存,而无需页面错误和数据迁移(有关内存使用提示的更多详细信息,请参见数据使用提示)。 请注意,对于 CUDA 托管内存,cudaMemAdviseSetAccessedBy
提示与 cudaCpuDeviceId
是必要的,以启用此直接访问而无需页面错误。
考虑以下代码示例
__global__ void write(int *ret, int a, int b) {
ret[threadIdx.x] = a + b + threadIdx.x;
}
__global__ void append(int *ret, int a, int b) {
ret[threadIdx.x] += a + b + threadIdx.x;
}
void test_malloc() {
int *ret = (int*)malloc(1000 * sizeof(int));
// for shared page table systems, the following hint is not necesary
cudaMemAdvise(ret, 1000 * sizeof(int), cudaMemAdviseSetAccessedBy, cudaCpuDeviceId);
write<<< 1, 1000 >>>(ret, 10, 100); // pages populated in GPU memory
cudaDeviceSynchronize();
for(int i = 0; i < 1000; i++)
printf("%d: A+B = %d\n", i, ret[i]); // directManagedMemAccessFromHost=1: CPU accesses GPU memory directly without migrations
// directManagedMemAccessFromHost=0: CPU faults and triggers device-to-host migrations
append<<< 1, 1000 >>>(ret, 10, 100); // directManagedMemAccessFromHost=1: GPU accesses GPU memory without migrations
cudaDeviceSynchronize(); // directManagedMemAccessFromHost=0: GPU faults and triggers host-to-device migrations
free(ret);
}
__global__ void write(int *ret, int a, int b) {
ret[threadIdx.x] = a + b + threadIdx.x;
}
__global__ void append(int *ret, int a, int b) {
ret[threadIdx.x] += a + b + threadIdx.x;
}
void test_managed() {
int *ret;
cudaMallocManaged(&ret, 1000 * sizeof(int));
cudaMemAdvise(ret, 1000 * sizeof(int), cudaMemAdviseSetAccessedBy, cudaCpuDeviceId); // set direct access hint
write<<< 1, 1000 >>>(ret, 10, 100); // pages populated in GPU memory
cudaDeviceSynchronize();
for(int i = 0; i < 1000; i++)
printf("%d: A+B = %d\n", i, ret[i]); // directManagedMemAccessFromHost=1: CPU accesses GPU memory directly without migrations
// directManagedMemAccessFromHost=0: CPU faults and triggers device-to-host migrations
append<<< 1, 1000 >>>(ret, 10, 100); // directManagedMemAccessFromHost=1: GPU accesses GPU memory without migrations
cudaDeviceSynchronize(); // directManagedMemAccessFromHost=0: GPU faults and triggers host-to-device migrations
cudaFree(ret);
}
在 write
内核完成后,将在 GPU 内存中创建和初始化 ret
。 接下来,CPU 将访问 ret
,然后 append
内核再次使用相同的 ret
内存。 此代码将根据系统架构和硬件一致性支持显示不同的行为
在
directManagedMemAccessFromHost=1
的系统上:CPU 对托管缓冲区的访问不会触发任何迁移; 数据将保持驻留在 GPU 内存中,并且任何后续的 GPU 内核都可以继续直接访问它,而不会导致错误或迁移。在
directManagedMemAccessFromHost=0
的系统上:CPU 对托管缓冲区的访问将导致页面错误并启动数据迁移; 任何首次尝试访问相同数据的 GPU 内核都将导致页面错误并将页面迁移回 GPU 内存。
19.2.2.3. 主机原生原子操作
某些设备,包括硬件一致性系统中的 NVLink 连接的设备,支持硬件加速的原子访问 CPU 驻留内存。 这意味着对主机内存的原子访问不必使用页面错误来模拟。 对于这些设备,属性 cudaDevAttrHostNativeAtomicSupported
设置为 1。
19.2.2.4. 原子访问和同步原语
CUDA 统一内存支持主机和设备线程可用的所有原子操作,使所有线程能够通过并发访问相同的共享内存位置进行协作。CUDA C++ 标准库提供了许多为在主机和设备线程之间并发使用而调整的异构同步原语,包括 cuda::atomic
、cuda::atomic_ref
、cuda::barrier
、cuda::semaphore
等等。
在没有CPU 和 GPU 页表:硬件一致性与软件一致性的系统上,不支持从设备到文件支持的主机内存的原子访问。 以下示例代码在具有CPU 和 GPU 页表:硬件一致性与软件一致性的系统上有效,但在其他系统上表现出未定义的行为
#include <cuda/atomic>
#include <cstdio>
#include <fcntl.h>
#include <sys/mman.h>
#define ERR(msg, ...) { fprintf(stderr, msg, ##__VA_ARGS__); return EXIT_FAILURE; }
__global__ void kernel(int* ptr) {
cuda::atomic_ref{*ptr}.store(2);
}
int main() {
// this will be closed/deleted by default on exit
FILE* tmp_file = tmpfile64();
// need to allcate space in the file, we do this with posix_fallocate here
int status = posix_fallocate(fileno(tmp_file), 0, 4096);
if (status != 0) ERR("Failed to allocate space in temp file\n");
int* ptr = (int*)mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE, fileno(tmp_file), 0);
if (ptr == MAP_FAILED) ERR("Failed to map temp file\n");
// initialize the value in our file-backed memory
*ptr = 1;
printf("Atom value: %d\n", *ptr);
// device and host thread access ptr concurrently, using cuda::atomic_ref
kernel<<<1, 1>>>(ptr);
while (cuda::atomic_ref{*ptr}.load() != 2);
// this will always be 2
printf("Atom value: %d\n", *ptr);
return EXIT_SUCCESS;
}
在没有CPU 和 GPU 页表:硬件一致性与软件一致性的系统上,对统一内存的原子访问可能会导致页面错误,这可能会导致显着的延迟。 请注意,并非在这些系统上所有 GPU 到 CPU 内存的原子操作都是这种情况:nvidia-smi -q | grep "Atomic Caps Outbound"
列出的操作可能会避免页面错误。
在具有CPU 和 GPU 页表:硬件一致性与软件一致性的系统上,主机和设备之间的原子操作不需要页面错误,但仍然可能因任何内存访问可能导致错误的其他原因而导致错误。
19.2.2.5. Memcpy()/Memset() 与统一内存的行为
cudaMemcpy*()
和 cudaMemset*()
接受任何统一内存指针作为参数。
对于 cudaMemcpy*()
,指定为 cudaMemcpyKind
的方向是一个性能提示,如果任何参数是统一内存指针,则它可能具有更高的性能影响。
因此,建议遵循以下性能建议
当统一内存的物理位置已知时,请使用准确的
cudaMemcpyKind
提示。优先使用
cudaMemcpyKindDefault
而不是不准确的cudaMemcpyKind
提示。始终使用已填充(已初始化)的缓冲区:避免使用这些 API 来初始化内存。
如果两个指针都指向系统分配的内存,请避免使用
cudaMemcpy*()
:启动内核或使用 CPU 内存复制算法(例如std::memcpy
)代替。
19.3. 在不支持完整 CUDA 统一内存的设备上使用统一内存
19.3.1. 在仅支持 CUDA 托管内存的设备上使用统一内存
对于计算能力为 6.x 或更高但没有可分页内存访问的设备,CUDA 托管内存得到完全支持且是一致的。 统一内存的编程模型和性能调优在很大程度上类似于 在具有完整 CUDA 统一内存支持的设备上使用统一内存 中描述的模型,但值得注意的例外是系统分配器不能用于分配内存。 因此,以下子章节列表不适用
19.3.2. 在 Windows 或计算能力为 5.x 的设备上使用统一内存
计算能力低于 6.0 的设备或 Windows 平台支持 CUDA 托管内存 v1.0,对数据迁移和一致性以及内存过度订阅的支持有限。 以下子章节更详细地描述了如何在这些平台上使用和优化托管内存。
19.3.2.1. 数据迁移和一致性
计算能力低于 6.0 的 GPU 架构不支持将托管数据精细地按需移动到 GPU。 每当启动 GPU 内核时,通常必须将所有托管内存传输到 GPU 内存,以避免在内存访问时出错。 借助计算能力 6.x,引入了一种新的 GPU 页面错误机制,该机制提供了更无缝的统一内存功能。 结合系统范围的虚拟地址空间,页面错误提供了几个好处。 首先,页面错误意味着 CUDA 系统软件不需要在每次内核启动之前将所有托管内存分配同步到 GPU。 如果在 GPU 上运行的内核访问了不在其内存中的页面,则会发生错误,从而允许页面按需自动迁移到 GPU 内存。 或者,页面可以映射到 GPU 地址空间,以便通过 PCIe 或 NVLink 互连进行访问(按需映射有时可能比迁移更快)。 请注意,统一内存是系统范围的:GPU(和 CPU)可能会在页面错误时迁移内存页面,无论是从 CPU 内存还是从系统中其他 GPU 的内存迁移。
19.3.2.2. GPU 内存过度订阅
计算能力低于 6.0 的设备无法分配超过 GPU 内存物理大小的托管内存。
19.3.2.3. 多 GPU
在计算能力低于 6.0 的设备的系统上,托管分配通过 GPU 的对等功能自动对系统中的所有 GPU 可见。 托管内存分配的行为类似于使用 cudaMalloc()
分配的非托管内存:当前活动设备是物理分配的主页,但系统中的其他 GPU 将通过 PCIe 总线以降低的带宽访问内存。
在 Linux 上,只要程序正在积极使用的所有 GPU 都具有对等支持,托管内存就会分配在 GPU 内存中。 如果应用程序在任何时候开始使用与任何具有托管分配的其他 GPU 都没有对等支持的 GPU,则驱动程序会将所有托管分配迁移到系统内存。 在这种情况下,所有 GPU 都会遇到 PCIe 带宽限制。
在 Windows 上,如果对等映射不可用(例如,在不同架构的 GPU 之间),则系统将自动回退到使用零复制内存,而不管程序是否实际使用了两个 GPU。 如果实际上只使用一个 GPU,则需要在启动程序之前设置 CUDA_VISIBLE_DEVICES
环境变量。 这限制了哪些 GPU 是可见的,并允许在 GPU 内存中分配托管内存。
或者,在 Windows 上,用户还可以将 CUDA_MANAGED_FORCE_DEVICE_ALLOC
设置为非零值,以强制驱动程序始终使用设备内存进行物理存储。 当此环境变量设置为非零值时,在该进程中使用的所有支持托管内存的设备都必须彼此对等兼容。 如果使用了支持托管内存的设备,并且该设备与该进程中先前使用的任何其他支持托管内存的设备都不对等兼容,即使已在这些设备上调用了 ::cudaDeviceReset
,也会返回错误 ::cudaErrorInvalidDevice
。 这些环境变量在CUDA 环境变量中进行了描述。 请注意,从 CUDA 8.0 开始,CUDA_MANAGED_FORCE_DEVICE_ALLOC
对 Linux 操作系统没有影响。
19.3.2.4. 一致性和并发性
无法同时访问计算能力低于 6.0 的设备上的托管内存,因为如果 CPU 在 GPU 内核处于活动状态时访问统一内存分配,则无法保证一致性。
19.3.2.4.1. GPU 对托管内存的独占访问
为了确保在 6.x 之前的 GPU 架构上的一致性,统一内存编程模型对数据访问施加了约束,尤其是在 CPU 和 GPU 同时执行时。实际上,当任何内核操作正在执行时,GPU 独占访问所有托管数据,无论特定内核是否正在主动使用该数据。当托管数据与 cudaMemcpy*()
或 cudaMemset*()
一起使用时,系统可能会选择从主机或设备访问源或目标,这将限制在 cudaMemcpy*()
或 cudaMemset*()
执行期间 CPU 对该数据的并发访问。有关更多详细信息,请参阅 统一内存的 Memcpy()/Memset() 行为。
对于 concurrentManagedAccess
属性设置为 0 的设备,当 GPU 处于活动状态时,CPU 不允许访问任何托管分配或变量。在这些系统上,即使是对不同的托管内存分配进行并发 CPU/GPU 访问,也会导致段错误,因为该页面被认为对 CPU 不可访问。
__device__ __managed__ int x, y=2;
__global__ void kernel() {
x = 10;
}
int main() {
kernel<<< 1, 1 >>>();
y = 20; // Error on GPUs not supporting concurrent access
cudaDeviceSynchronize();
return 0;
}
在上面的示例中,当 CPU 访问 y
时,GPU 程序 kernel
仍然处于活动状态。(请注意它发生在 cudaDeviceSynchronize()
之前。)由于 GPU 的页面错误处理能力,该代码在计算能力为 6.x 的设备上成功运行,这消除了对同时访问的所有限制。然而,即使 CPU 访问的数据与 GPU 不同,这种内存访问在 6.x 之前的架构上也是无效的。程序必须在访问 y
之前显式地与 GPU 同步。
__device__ __managed__ int x, y=2;
__global__ void kernel() {
x = 10;
}
int main() {
kernel<<< 1, 1 >>>();
cudaDeviceSynchronize();
y = 20; // Success on GPUs not supporing concurrent access
return 0;
}
正如本示例所示,在 6.x 之前的 GPU 架构的系统上,CPU 线程可能无法在内核启动和后续同步调用之间访问任何托管数据,无论 GPU 内核是否实际访问相同的数据(或任何托管数据)。仅仅是 CPU 和 GPU 并发访问的可能性就足以引发进程级异常。
请注意,如果在 GPU 处于活动状态时使用 cudaMallocManaged()
或 cuMemAllocManaged()
动态分配内存,则内存的行为是不确定的,直到启动额外的工作或 GPU 同步。在此期间尝试在 CPU 上访问内存可能会或可能不会导致段错误。这不适用于使用标志 cudaMemAttachHost
或 CU_MEM_ATTACH_HOST
分配的内存。
19.3.2.4.2. 显式同步和逻辑 GPU 活动
请注意,即使在上面的示例中 kernel
运行速度很快并在 CPU 访问 y
之前完成,也需要显式同步。统一内存使用逻辑活动来确定 GPU 是否空闲。这与 CUDA 编程模型一致,该模型规定内核可以在启动后的任何时间运行,并且不保证在主机发出同步调用之前完成。
任何逻辑上保证 GPU 完成其工作的功能调用都是有效的。这包括 cudaDeviceSynchronize()
;cudaStreamSynchronize()
和 cudaStreamQuery()
(前提是它返回 cudaSuccess
而不是 cudaErrorNotReady
),其中指定的流是 GPU 上仍在执行的唯一流;cudaEventSynchronize()
和 cudaEventQuery()
,在指定事件之后没有任何设备工作的情况下;以及 cudaMemcpy()
和 cudaMemset()
的使用,这些使用被记录为相对于主机完全同步。
在流之间创建的依赖关系将被遵循,以便通过同步流或事件来推断其他流的完成情况。依赖关系可以通过 cudaStreamWaitEvent()
或在使用默认 (NULL) 流时隐式创建。
CPU 从流回调中访问托管数据是合法的,前提是没有其他可能正在访问托管数据的流在 GPU 上处于活动状态。此外,不跟随任何设备工作的回调可以用于同步:例如,通过从回调内部发出条件变量信号;否则,CPU 访问仅在回调期间有效。
有几个重要的注意事项
当 GPU 处于活动状态时,CPU 始终可以访问非托管零复制数据。
当 GPU 正在运行任何内核时,即使该内核不使用托管数据,也认为 GPU 处于活动状态。如果内核可能使用数据,则禁止访问,除非设备属性
concurrentManagedAccess
为 1。除了适用于非托管内存的多 GPU 访问的约束外,对托管内存的并发 GPU 间访问没有约束。
对并发 GPU 内核访问托管数据没有约束。
请注意,最后一点允许 GPU 内核之间的竞争,就像当前非托管 GPU 内存的情况一样。如前所述,从 GPU 的角度来看,托管内存的功能与非托管内存相同。以下代码示例说明了这些要点
int main() {
cudaStream_t stream1, stream2;
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);
int *non_managed, *managed, *also_managed;
cudaMallocHost(&non_managed, 4); // Non-managed, CPU-accessible memory
cudaMallocManaged(&managed, 4);
cudaMallocManaged(&also_managed, 4);
// Point 1: CPU can access non-managed data.
kernel<<< 1, 1, 0, stream1 >>>(managed);
*non_managed = 1;
// Point 2: CPU cannot access any managed data while GPU is busy,
// unless concurrentManagedAccess = 1
// Note we have not yet synchronized, so "kernel" is still active.
*also_managed = 2; // Will issue segmentation fault
// Point 3: Concurrent GPU kernels can access the same data.
kernel<<< 1, 1, 0, stream2 >>>(managed);
// Point 4: Multi-GPU concurrent access is also permitted.
cudaSetDevice(1);
kernel<<< 1, 1 >>>(managed);
return 0;
}
19.3.2.4.3. 使用流管理数据可见性和并发 CPU + GPU 访问
到目前为止,我们假设对于 6.x 之前的 SM 架构:1) 任何活动的内核都可以使用任何托管内存,以及 2) 当内核处于活动状态时,从 CPU 使用托管内存是无效的。在这里,我们介绍一个用于更精细地控制托管内存的系统,该系统旨在在支持托管内存的所有设备上工作,包括 concurrentManagedAccess
等于 0 的旧架构。
CUDA 编程模型提供了流作为程序指示内核启动之间依赖性和独立性的机制。启动到同一流中的内核保证连续执行,而启动到不同流中的内核则允许并发执行。流描述了工作项之间的独立性,因此可以通过并发性提高潜在的效率。
统一内存建立在流独立性模型之上,允许 CUDA 程序将托管分配显式地与 CUDA 流关联。通过这种方式,程序员根据内核是否启动到指定的流中来指示内核对数据的使用。这为基于程序特定数据访问模式的并发性提供了机会。用于控制此行为的函数是
cudaError_t cudaStreamAttachMemAsync(cudaStream_t stream,
void *ptr,
size_t length=0,
unsigned int flags=0);
cudaStreamAttachMemAsync()
函数将从 ptr
开始的 length
字节的内存与指定的 stream
关联。(目前,length
必须始终为 0,以指示应附加整个区域。)由于这种关联,只要 stream
中的所有操作都已完成,统一内存系统就允许 CPU 访问此内存区域,无论其他流是否处于活动状态。实际上,这会将活动 GPU 对托管内存区域的独占所有权限制为按流活动,而不是整个 GPU 活动。
最重要的是,如果分配未与特定流关联,则无论其流如何,它对所有正在运行的内核都可见。这是 cudaMallocManaged()
分配或 __managed__
变量的默认可见性;因此,简单的规则是,当任何内核正在运行时,CPU 可能无法访问数据。
通过将分配与特定流关联,程序保证只有启动到该流中的内核才会访问该数据。统一内存系统不执行任何错误检查:程序员有责任确保遵守该保证。
除了允许更高的并发性之外,使用 cudaStreamAttachMemAsync()
还可以(并且通常会)在统一内存系统中启用数据传输优化,这可能会影响延迟和其他开销。
19.3.2.4.4. 流关联示例
将数据与流关联允许对 CPU + GPU 并发进行细粒度控制,但在使用计算能力低于 6.0 的设备时,必须记住哪些数据对哪些流可见。查看早期的同步示例
__device__ __managed__ int x, y=2;
__global__ void kernel() {
x = 10;
}
int main() {
cudaStream_t stream1;
cudaStreamCreate(&stream1);
cudaStreamAttachMemAsync(stream1, &y, 0, cudaMemAttachHost);
cudaDeviceSynchronize(); // Wait for Host attachment to occur.
kernel<<< 1, 1, 0, stream1 >>>(); // Note: Launches into stream1.
y = 20; // Success – a kernel is running but “y”
// has been associated with no stream.
return 0;
}
在这里,我们显式地将 y
与主机可访问性关联,从而允许 CPU 随时访问。(与之前一样,请注意在访问之前缺少 cudaDeviceSynchronize()
。)现在,GPU 运行 kernel
对 y
的访问将产生未定义的结果。
请注意,将变量与流关联不会更改任何其他变量的关联。例如,将 x
与 stream1
关联并不能确保只有 x
被 stream1
中启动的内核访问,因此此代码会导致错误
__device__ __managed__ int x, y=2;
__global__ void kernel() {
x = 10;
}
int main() {
cudaStream_t stream1;
cudaStreamCreate(&stream1);
cudaStreamAttachMemAsync(stream1, &x);// Associate “x” with stream1.
cudaDeviceSynchronize(); // Wait for “x” attachment to occur.
kernel<<< 1, 1, 0, stream1 >>>(); // Note: Launches into stream1.
y = 20; // ERROR: “y” is still associated globally
// with all streams by default
return 0;
}
请注意,对 y
的访问将导致错误,因为即使 x
已与流关联,我们也没有告诉系统关于谁可以查看 y
的任何信息。因此,系统保守地假设 kernel
可能会访问它,并阻止 CPU 这样做。
19.3.2.4.5. 多线程主机程序的流附加
cudaStreamAttachMemAsync()
的主要用途是使用 CPU 线程启用独立的任务并行性。通常,在此类程序中,CPU 线程为其生成的所有工作创建自己的流,因为使用 CUDA 的 NULL 流会导致线程之间产生依赖关系。
托管数据对任何 GPU 流的默认全局可见性可能会使多线程程序中难以避免 CPU 线程之间的交互。cudaStreamAttachMemAsync()
函数因此用于将线程的托管分配与该线程自己的流关联,并且通常在线程的生命周期内不会更改关联。
这样的程序只需添加一个对 cudaStreamAttachMemAsync()
的调用即可将统一内存用于其数据访问
// This function performs some task, in its own private stream.
void run_task(int *in, int *out, int length) {
// Create a stream for us to use.
cudaStream_t stream;
cudaStreamCreate(&stream);
// Allocate some managed data and associate with our stream.
// Note the use of the host-attach flag to cudaMallocManaged();
// we then associate the allocation with our stream so that
// our GPU kernel launches can access it.
int *data;
cudaMallocManaged((void **)&data, length, cudaMemAttachHost);
cudaStreamAttachMemAsync(stream, data);
cudaStreamSynchronize(stream);
// Iterate on the data in some way, using both Host & Device.
for(int i=0; i<N; i++) {
transform<<< 100, 256, 0, stream >>>(in, data, length);
cudaStreamSynchronize(stream);
host_process(data, length); // CPU uses managed data.
convert<<< 100, 256, 0, stream >>>(out, data, length);
}
cudaStreamSynchronize(stream);
cudaStreamDestroy(stream);
cudaFree(data);
}
在此示例中,分配-流关联仅建立一次,然后主机和设备重复使用 data
。结果是比显式地在主机和设备之间复制数据更简单的代码,尽管结果是相同的。
19.3.2.4.6. 高级主题:模块化程序和数据访问约束
在之前的示例中,cudaMallocManaged()
指定了 cudaMemAttachHost
标志,该标志创建了一个最初对设备端执行不可见的分配。(默认分配对所有流上的所有 GPU 内核都可见。)这确保了在数据分配和为特定流获取数据之间的间隔内,不会与另一个线程的执行发生意外交互。
如果没有此标志,如果另一个线程启动的内核碰巧正在运行,则新的分配将被视为在 GPU 上正在使用中。这可能会影响线程从 CPU(例如,在基类构造函数中)访问新分配的数据的能力,然后才能将其显式附加到私有流。因此,为了在线程之间实现安全的独立性,应通过指定此标志来进行分配。
注意
另一种方法是在分配已附加到流之后,在所有线程之间放置一个进程范围的屏障。这将确保所有线程在启动任何内核之前完成其数据/流关联,从而避免危险。在销毁流之前,还需要第二个屏障,因为流销毁会导致分配恢复为其默认可见性。cudaMemAttachHost
标志的存在既是为了简化此过程,也是因为并非总是在需要的地方插入全局屏障。
19.3.2.4.7. 流关联统一内存的 Memcpy()/Memset() 行为
有关 concurrentManagedAccess
设置的设备上 cudaMemcpy*
/ cudaMemset*
行为的一般概述,请参阅 统一内存的 Memcpy()/Memset() 行为。在未设置 concurrentManagedAccess
的设备上,以下规则适用
如果指定了 cudaMemcpyHostTo*
并且源数据是统一内存,则如果可以从主机在复制流中连贯地访问它,则将从主机访问它 (1);否则将从设备访问它。当指定 cudaMemcpy*ToHost
并且目标是统一内存时,类似的规则适用于目标。
如果指定了 cudaMemcpyDeviceTo*
并且源数据是统一内存,则将从设备访问它。源必须可以从设备在复制流中连贯地访问 (2);否则,将返回错误。当指定 cudaMemcpy*ToDevice
并且目标是统一内存时,类似的规则适用于目标。
如果指定了 cudaMemcpyDefault
,则如果统一内存无法从设备在复制流中连贯地访问 (2),或者如果数据的首选位置是 cudaCpuDeviceId
并且可以从主机在复制流中连贯地访问 (1),则将从主机访问统一内存;否则,将从设备访问它。
当将 cudaMemset*()
与统一内存一起使用时,数据必须可以从设备在用于 cudaMemset()
操作的流中连贯地访问 (2);否则,将返回错误。
当数据被 cudaMemcpy*
或 cudaMemset*
从设备访问时,操作流被认为在 GPU 上处于活动状态。在此期间,如果 GPU 的设备属性 concurrentManagedAccess
的值为零,则任何 CPU 访问与该流关联的数据或具有全局可见性的数据都将导致段错误。程序必须适当地同步以确保操作在从 CPU 访问任何关联数据之前完成。
在给定的流中可以从主机连贯地访问意味着内存既不具有全局可见性,也不与给定的流关联。
在给定的流中可以从设备连贯地访问意味着内存既具有全局可见性,又与给定的流关联。
20. 延迟加载
20.1. 什么是延迟加载?
延迟加载将 CUDA 模块和内核的加载从程序初始化延迟到更接近内核执行的时间。如果程序没有使用它包含的每个内核,那么一些内核将被不必要地加载。这非常常见,特别是如果您包含任何库。大多数时候,程序只使用它们包含的库中的少量内核。
由于延迟加载,程序能够仅加载它们实际要使用的内核,从而节省了初始化时间。这减少了内存开销,包括 GPU 内存和主机内存。
延迟加载通过将 CUDA_MODULE_LOADING
环境变量设置为 LAZY
来启用。
首先,CUDA 运行时将不再在程序初始化期间加载所有模块,但包含托管变量的模块除外。每个模块将在首次使用该模块的变量或内核时加载。此优化仅与 CUDA 运行时用户相关,使用 cuModuleLoad
的 CUDA 驱动程序用户不受影响。此优化在 CUDA 11.8 中发布。使用 cuLibraryLoad
将模块数据加载到内存中的 CUDA 驱动程序用户的行为可以通过设置 CUDA_MODULE_DATA_LOADING
环境变量来更改。
其次,加载模块(cuModuleLoad*()
系列函数)不会立即加载内核,而是会将内核的加载延迟到调用 cuModuleGetFunction()
时。这里有一些例外,某些内核必须在 cuModuleLoad*()
期间加载,例如指针存储在全局变量中的内核。此优化与 CUDA 运行时和 CUDA 驱动程序用户都相关。CUDA 运行时仅在首次使用/引用内核时调用 cuModuleGetFunction()
。此优化在 CUDA 11.7 中发布。
假设遵循 CUDA 编程模型,这两个优化都旨在对用户不可见。
20.2. 延迟加载版本支持
延迟加载是 CUDA 运行时和 CUDA 驱动程序功能。可能需要升级两者才能利用该功能。
20.2.1. 驱动程序
延迟加载需要 R515+ 用户模式库,但它支持向前兼容性,这意味着它可以运行在较旧的内核模式驱动程序之上。
如果没有 R515+ 用户模式库,即使工具包版本为 11.7+,延迟加载也无法以任何形式使用。
20.2.2. 工具包
延迟加载在 CUDA 11.7 中引入,并在 CUDA 11.8 中获得了重大升级。
如果您的应用程序使用 CUDA 运行时,那么为了从延迟加载中受益,您的应用程序必须使用 11.7+ CUDA 运行时。
由于 CUDA 运行时通常静态链接到程序和库中,这意味着您必须使用 CUDA 11.7+ 工具包重新编译您的程序,并使用 CUDA 11.7+ 库。
否则,即使您的驱动程序版本支持延迟加载,您也不会看到延迟加载的好处。
如果只有部分库是 11.7+,您将仅在这些库中看到延迟加载的好处。其他库仍将急切地加载所有内容。
20.2.3. 编译器
延迟加载不需要任何编译器支持。使用 11.7 之前版本的编译器编译的 SASS 和 PTX 都可以通过启用延迟加载来加载,并将看到该功能的全部好处。但是,仍然需要 11.7+ CUDA 运行时,如上所述。
20.3. 在延迟模式下触发内核加载
内核和变量的加载会自动发生,无需任何显式加载。只需启动内核或引用变量或内核,就会自动加载相关的模块和内核。
但是,如果出于任何原因您希望加载内核而不执行它或以任何方式修改它,我们建议以下方法。
20.3.1. CUDA 驱动程序 API
内核的加载发生在 cuModuleGetFunction()
调用期间。即使没有延迟加载,此调用也是必要的,因为它是获取内核句柄的唯一方法。
但是,您也可以使用此 API 以更精细的粒度控制何时加载内核。
20.3.2. CUDA 运行时 API
CUDA 运行时 API 自动管理模块管理,因此我们建议只需使用 cudaFuncGetAttributes()
来引用内核。
这将确保内核已加载,而不会更改状态。
20.4. 查询是否已启用延迟加载
为了检查用户是否启用了延迟加载,可以使用 CUresult cuModuleGetLoadingMode ( CUmoduleLoadingMode* mode )
。
重要的是要注意,必须先初始化 CUDA,然后才能运行此函数。示例用法可以在下面的代码片段中看到。
#include "cuda.h"
#include "assert.h"
#include "iostream"
int main() {
CUmoduleLoadingMode mode;
assert(CUDA_SUCCESS == cuInit(0));
assert(CUDA_SUCCESS == cuModuleGetLoadingMode(&mode));
std::cout << "CUDA Module Loading Mode is " << ((mode == CU_MODULE_LAZY_LOADING) ? "lazy" : "eager") << std::endl;
return 0;
}
20.5. 采用延迟加载时可能出现的问题
延迟加载的设计使其不应要求对应用程序进行任何修改即可使用它。也就是说,有一些注意事项,特别是当应用程序没有完全遵守 CUDA 编程模型时。
20.5.1. 并发执行
加载内核可能需要上下文同步。一些程序错误地将内核并发执行的可能性视为保证。在这种情况下,如果程序假设两个内核将能够并发执行,并且其中一个内核在另一个内核执行之前不会返回,则可能发生死锁。
如果内核 A 将在一个无限循环中旋转,直到内核 B 正在执行。在这种情况下,启动内核 B 将触发内核 B 的延迟加载。如果此加载需要上下文同步,那么我们就会遇到死锁:内核 A 正在等待内核 B,但加载内核 B 卡住等待内核 A 完成以同步上下文。
这样的程序是一种反模式,但如果出于任何原因您想保留它,您可以执行以下操作
在并发执行之前预加载您希望并发执行的所有内核
使用
CUDA_MODULE_DATA_LOADING=EAGER
运行应用程序,以强制急切加载数据,而无需强制每个函数都急切加载
20.5.2. 分配器
延迟加载将代码加载从程序的初始化阶段延迟到更接近执行阶段。将代码加载到 GPU 上需要内存分配。
如果您的应用程序尝试在启动时分配整个 VRAM,例如,将其用于自己的分配器,那么可能会发现没有剩余内存来加载内核。尽管事实上总体而言延迟加载为用户释放了更多内存。CUDA 将需要分配一些内存来加载每个内核,这通常发生在每个内核的首次启动时。如果您的应用程序分配器贪婪地分配了所有内容,CUDA 将无法分配内存。
可能的解决方案
使用
cudaMallocAsync()
而不是在启动时分配整个 VRAM 的分配器添加一些缓冲区来补偿内核的延迟加载
在尝试初始化分配器之前,预加载程序中将使用的所有内核
20.5.3. 自动调优
一些应用程序启动多个实现相同功能的内核,以确定哪个内核最快。虽然总体而言建议至少运行一次预热迭代,但在延迟加载的情况下,这变得尤为重要。毕竟,包括加载内核所花费的时间将使您的结果产生偏差。
可能的解决方案
在测量之前至少进行一次预热交互
在启动基准测试内核之前预加载它
21. 扩展 GPU 内存
扩展 GPU 内存 (EGM) 功能利用高带宽 NVLink-C2C,促进了单节点系统中 GPU 对所有系统内存的有效访问。EGM 适用于集成的 CPU-GPU NVIDIA 系统,允许物理内存分配,该分配可以从设置中的任何 GPU 线程访问。EGM 确保所有 GPU 都可以以 GPU-GPU NVLink 或 NVLink-C2C 的速度访问其资源。

在此设置中,内存访问通过本地高带宽 NVLink-C2C 进行。对于远程内存访问,使用 GPU NVLink,在某些情况下使用 NVLink-C2C。借助 EGM,GPU 线程能够访问所有可用内存资源,包括 CPU 连接的内存和 HBM3,通过 NVSwitch 结构。
21.1. 预备知识
在深入探讨 EGM 功能的 API 更改之前,我们将介绍当前支持的拓扑、标识符分配、虚拟内存管理的先决条件以及 EGM 的 CUDA 类型。
21.1.1. EGM 平台:系统拓扑
目前,EGM 可以在三个平台上启用:(1) 单节点、单 GPU:由基于 Arm 的 CPU、CPU 连接的内存和一个 GPU 组成。CPU 和 GPU 之间存在高带宽 C2C(芯片到芯片)互连。(2) 单节点、多 GPU:由完全连接的四个单节点、单 GPU 平台组成。(3) 多节点、单 GPU:两个或多个单节点多插槽系统。
注意
使用 cgroups
限制可用设备将阻止通过 EGM 路由并导致性能问题。请改用 CUDA_VISIBLE_DEVICES
。
21.1.2. 套接字标识符:它们是什么?如何访问它们?
NUMA(非均匀内存访问)是多处理器计算机系统中使用的内存架构,这样内存被划分为多个节点。每个节点都有自己的处理器和内存。在这种系统中,NUMA 将系统划分为节点,并为每个节点分配一个唯一的标识符 (numaID)。
EGM 使用由操作系统分配的 NUMA 节点标识符。请注意,此标识符与设备的序号不同,它与最近的主机节点关联。除了现有方法外,用户还可以通过调用 cuDeviceGetAttribute 和 CU_DEVICE_ATTRIBUTE_HOST_NUMA_ID
属性类型来获取主机节点 (numaID) 的标识符,如下所示
int numaId;
cuDeviceGetAttribute(&numaId, CU_DEVICE_ATTRIBUTE_HOST_NUMA_ID, deviceOrdinal);
21.1.3. 分配器和 EGM 支持
将系统内存映射为 EGM 不会导致任何性能问题。事实上,访问映射为 EGM 的远程套接字系统内存将会更快。因为,借助 EGM,流量保证通过 NVLink 路由。目前,cuMemCreate
和 cudaMemPoolCreate
分配器支持适当的位置类型和 NUMA 标识符。
21.1.4. 当前 API 的内存管理扩展
目前,EGM 内存可以使用虚拟内存 (cuMemCreate
) 或流有序内存 (cudaMemPoolCreate
) 分配器映射。用户负责分配物理内存并将其映射到所有套接字上的虚拟内存地址空间。
注意
多节点、单 GPU 平台需要进程间通信。因此,我们鼓励读者查看 第 3 章
已向 API 添加了新的 CUDA 属性类型,以允许这些方法理解使用类似 NUMA 节点标识符的分配位置
CUDA 类型 |
用于 |
|
|
|
|
注意
请参阅 CUDA Driver API 和 CUDA Runtime Data Types 以了解更多关于 NUMA 特定 CUDA 类型的信息。
21.2. 使用 EGM 接口
21.2.1. 单节点、单 GPU
任何现有的 CUDA 主机分配器以及系统分配的内存都可以用于受益于高带宽 C2C。对于用户而言,本地访问就是今天主机分配的含义。
注意
请参阅调优指南,以获取有关内存分配器和页面大小的更多信息。
21.2.2. 单节点、多 GPU
在多 GPU 系统中,用户必须提供主机信息以进行放置。正如我们所提到的,表达该信息的自然方式是使用 NUMA 节点 ID,而 EGM 也遵循这种方法。因此,用户应该能够使用 cuDeviceGetAttribute
函数来了解最近的 NUMA 节点 ID。(请参阅套接字标识符:它们是什么?如何访问它们?)。然后,用户可以使用 VMM(虚拟内存管理)API 或 CUDA 内存池来分配和管理 EGM 内存。
21.2.2.1. 使用 VMM API
使用虚拟内存管理 API 进行内存分配的第一步是创建一个物理内存块,该内存块将为分配提供后备支持。有关更多详细信息,请参阅 CUDA 编程指南的虚拟内存管理部分。在 EGM 分配中,用户必须显式提供 CU_MEM_LOCATION_TYPE_HOST_NUMA
作为位置类型,并提供 numaID 作为位置标识符。此外,在 EGM 中,分配必须与平台的适当粒度对齐。以下代码片段显示了使用 cuMemCreate
分配物理内存
CUmemAllocationProp prop{};
prop.type = CU_MEM_ALLOCATION_TYPE_PINNED;
prop.location.type = CU_MEM_LOCATION_TYPE_HOST_NUMA;
prop.location.id = numaId;
size_t granularity = 0;
cuMemGetAllocationGranularity(&granularity, &prop, MEM_ALLOC_GRANULARITY_MINIMUM);
size_t padded_size = ROUND_UP(size, granularity);
CUmemGenericAllocationHandle allocHandle;
cuMemCreate(&allocHandle, padded_size, &prop, 0);
在物理内存分配之后,我们必须保留地址空间并将其映射到指针。这些过程没有 EGM 特定的更改
CUdeviceptr dptr;
cuMemAddressReserve(&dptr, padded_size, 0, 0, 0);
cuMemMap(dptr, padded_size, 0, allocHandle, 0);
最后,用户必须显式保护映射的虚拟地址范围。否则,访问映射空间将导致崩溃。与内存分配类似,用户必须提供 CU_MEM_LOCATION_TYPE_HOST_NUMA
作为位置类型,并提供 numaId 作为位置标识符。以下代码片段为宿主节点和 GPU 创建访问描述符,以便为它们提供对映射内存的读写访问权限
CUmemAccessDesc accessDesc[2]{{}};
accessDesc[0].location.type = CU_MEM_LOCATION_TYPE_HOST_NUMA;
accessDesc[0].location.id = numaId;
accessDesc[0].flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE;
accessDesc[1].location.type = CU_MEM_LOCATION_TYPE_DEVICE;
accessDesc[1].location.id = currentDev;
accessDesc[1].flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE;
cuMemSetAccess(dptr, size, accessDesc, 2);
21.2.2.2. 使用 CUDA 内存池
为了定义 EGM,用户可以在节点上创建内存池并授予对等访问权限。在这种情况下,用户必须显式定义 cudaMemLocationTypeHostNuma
作为位置类型,并提供 numaId 作为位置标识符。以下代码片段显示了创建内存池 cudaMemPoolCreate
cudaSetDevice(homeDevice);
cudaMemPoolProps props{};
props.allocType = cudaMemAllocationTypePinned;
props.location.type = cudaMemLocationTypeHostNuma;
props.location.id = numaId;
cudaMemPoolCreate(&memPool, &props);
此外,对于直接连接对等访问,也可以使用现有的对等访问 API cudaMemPoolSetAccess
。以下代码片段中显示了 accessingDevice 的示例
cudaMemAccessDesc desc{};
desc.flags = cudaMemAccessFlagsProtReadWrite;
desc.location.type = cudaMemLocationTypeDevice;
desc.location.id = accessingDevice;
cudaMemPoolSetAccess(memPool, &desc, 1);
创建内存池并授予访问权限后,用户可以将创建的内存池设置为 residentDevice,并开始使用 cudaMallocAsync
分配内存
cudaDeviceSetMemPool(residentDevice, memPool);
cudaMallocAsync(&ptr, size, memPool, stream);
注意
EGM 映射为 2MB 页面。因此,当访问非常大的分配时,用户可能会遇到更多的 TLB 未命中。
21.2.3. 多节点、单 GPU
除了内存分配之外,远程对等访问没有 EGM 特定的修改,它遵循 CUDA 进程间 (IPC) 协议。有关 IPC 的更多详细信息,请参阅 CUDA 编程指南。
用户应使用 cuMemCreate
分配内存,并且用户必须再次显式提供 CU_MEM_LOCATION_TYPE_HOST_NUMA
作为位置类型,并提供 numaID 作为位置标识符。此外,应将 CU_MEM_HANDLE_TYPE_FABRIC
定义为请求的句柄类型。以下代码片段显示了在节点 A 上分配物理内存
CUmemAllocationProp prop{};
prop.type = CU_MEM_ALLOCATION_TYPE_PINNED;
prop.requestedHandleTypes = CU_MEM_HANDLE_TYPE_FABRIC;
prop.location.type = CU_MEM_LOCATION_TYPE_HOST_NUMA;
prop.location.id = numaId;
size_t granularity = 0;
cuMemGetAllocationGranularity(&granularity, &prop,
MEM_ALLOC_GRANULARITY_MINIMUM);
size_t padded_size = ROUND_UP(size, granularity);
size_t page_size = ...;
assert(padded_size % page_size == 0);
CUmemGenericAllocationHandle allocHandle;
cuMemCreate(&allocHandle, padded_size, &prop, 0);
使用 cuMemCreate
创建分配句柄后,用户可以调用 cuMemExportToShareableHandle
将该句柄导出到另一个节点节点 B
cuMemExportToShareableHandle(&fabricHandle, allocHandle,
CU_MEM_HANDLE_TYPE_FABRIC, 0);
// At this point, fabricHandle should be sent to Node B via TCP/IP.
在节点 B 上,可以使用 cuMemImportFromShareableHandle
导入句柄,并将其视为任何其他 fabric 句柄
// At this point, fabricHandle should be received from Node A via TCP/IP.
CUmemGenericAllocationHandle allocHandle;
cuMemImportFromShareableHandle(&allocHandle, &fabricHandle,
CU_MEM_HANDLE_TYPE_FABRIC);
在节点 B 导入句柄后,用户可以保留地址空间并在本地以常规方式映射它
size_t granularity = 0;
cuMemGetAllocationGranularity(&granularity, &prop,
MEM_ALLOC_GRANULARITY_MINIMUM);
size_t padded_size = ROUND_UP(size, granularity);
size_t page_size = ...;
assert(padded_size % page_size == 0);
CUdeviceptr dptr;
cuMemAddressReserve(&dptr, padded_size, 0, 0, 0);
cuMemMap(dptr, padded_size, 0, allocHandle, 0);
作为最后一步,用户应向节点 B 的每个本地 GPU 授予适当的访问权限。以下代码片段示例显示了向八个本地 GPU 授予读写访问权限
// Give all 8 local GPUS access to exported EGM memory located on Node A. |
CUmemAccessDesc accessDesc[8];
for (int i = 0; i < 8; i++) {
accessDesc[i].location.type = CU_MEM_LOCATION_TYPE_DEVICE;
accessDesc[i].location.id = i;
accessDesc[i].flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE;
}
cuMemSetAccess(dptr, size, accessDesc, 8);
22. 声明
22.1. 通知
本文档仅供参考,不得视为对产品的特定功能、条件或质量的保证。NVIDIA Corporation(“NVIDIA”)对本文档中包含信息的准确性或完整性不作任何明示或暗示的陈述或保证,并且对本文档中包含的任何错误不承担任何责任。NVIDIA 对因使用此类信息或因使用此类信息而可能导致的侵犯专利或第三方的其他权利的后果或使用不承担任何责任。本文档不构成开发、发布或交付任何材料(如下定义)、代码或功能的承诺。
NVIDIA 保留在任何时候对本文档进行更正、修改、增强、改进和任何其他更改的权利,恕不另行通知。
客户在下订单前应获取最新的相关信息,并应验证此类信息是否为最新且完整。
NVIDIA 产品根据订单确认时提供的 NVIDIA 标准销售条款和条件进行销售,除非 NVIDIA 和客户的授权代表签署的个别销售协议(“销售条款”)另有约定。NVIDIA 在此明确反对将任何客户通用条款和条件应用于购买本文档中引用的 NVIDIA 产品。本文档未直接或间接形成任何合同义务。
NVIDIA 产品并非设计、授权或保证适用于医疗、军事、航空、航天或生命维持设备,也不适用于 NVIDIA 产品的故障或失灵可能合理预期会导致人身伤害、死亡或财产或环境损害的应用。NVIDIA 对在上述设备或应用中包含和/或使用 NVIDIA 产品不承担任何责任,因此,此类包含和/或使用由客户自行承担风险。
NVIDIA 不保证基于本文档的产品适用于任何特定用途。NVIDIA 不一定会执行每个产品所有参数的测试。客户全权负责评估和确定本文档中包含的任何信息的适用性,确保产品适合并满足客户计划的应用,并为该应用执行必要的测试,以避免应用或产品的默认设置。客户产品设计中的缺陷可能会影响 NVIDIA 产品的质量和可靠性,并可能导致超出本文档中包含的附加或不同的条件和/或要求。NVIDIA 对可能基于或归因于以下原因的任何默认设置、损坏、成本或问题不承担任何责任:(i) 以任何与本文档相悖的方式使用 NVIDIA 产品或 (ii) 客户产品设计。
本文档未授予 NVIDIA 专利权、版权或 NVIDIA 其他知识产权下的任何明示或暗示的许可。NVIDIA 发布的有关第三方产品或服务的信息不构成 NVIDIA 授予使用此类产品或服务的许可,也不构成对其的保证或认可。使用此类信息可能需要获得第三方在其专利或其他知识产权下的许可,或获得 NVIDIA 在 NVIDIA 专利或其他知识产权下的许可。
只有在事先获得 NVIDIA 书面批准的情况下,才允许复制本文档中的信息,并且复制应在不进行修改且完全遵守所有适用的出口法律和法规的情况下进行,并且应附带所有相关的条件、限制和声明。
本文件和所有 NVIDIA 设计规范、参考板、文件、图纸、诊断程序、列表和其他文件(统称为“材料”,单独称为“材料”)均“按原样”提供。NVIDIA 对材料不作任何明示、暗示、法定或其他方面的保证,并明确否认所有关于不侵权、适销性和适用于特定用途的暗示保证。在法律未禁止的范围内,在任何情况下,NVIDIA 均不对因使用本文件而引起的任何损害(包括但不限于任何直接、间接、特殊、附带、惩罚性或后果性损害,无论因何种原因造成,也无论责任理论如何)承担责任,即使 NVIDIA 已被告知可能发生此类损害。尽管客户可能因任何原因遭受任何损害,但 NVIDIA 对本文所述产品的客户承担的累计和累积责任应根据产品的销售条款进行限制。
22.2. OpenCL
OpenCL 是 Apple Inc. 的商标,已获得 Khronos Group Inc. 的许可使用。
22.3. 商标
NVIDIA 和 NVIDIA 徽标是 NVIDIA Corporation 在美国和其他国家/地区的商标或注册商标。其他公司和产品名称可能是与其相关的各自公司的商标。